[모두의 랜덤 디펜스] Spring Batch 5.X로 업데이트된 문제 가져오는 Job 만들기 - 2편 성능비교

2024. 3. 10. 18:00Backend/Spring

https://morandi.co.kr

 

모두의 랜덤 디펜스

모두의 랜덤 디펜스: 코딩테스트를 준비하는 최고의 솔루션!

morandi.co.kr

모두의 랜덤 디펜스는 코딩 테스트 실전 모의고사 서비스입니다.

기존 서비스를 아주 살짝~~ 피봇팅하여 게이밍 포인트를 추가한 서비스를 개발중에 있습니다.

단일 Item vs List item

단일 Item으로 배치를 진행하는 것과 List형태의 Item으로 배치를 진행하는 것의 성능 차이가 궁금해져서 직접 실험을 해보게 되었습니다.

 

모두 chunk가 50일 때

기존 구현 1 : 50개 단위로 paging하는 API 호출 후 결과 모두를 queue에 저장 후, item read 호출 시 마다 하나씩 반환, (ProblemDTO)

비교하고자 하는 구현 2 : 50개 단위로 paging하는 API 호출 후 50개를 List로 묶어 item 하나로 반환 (ProblemResponse)

 

기존 구현 1(단일 Item)

기존 구현 1의 내용은 이전 포스팅에 있습니다. 

https://miiiinju.tistory.com/11

비교하고자 하는 구현 2(List Item):

Job과 Step 정의

 

NewProblemReader

기존 구현과 유사하지만 반환이 item을 반환하지 않고, 50 사이즈 List를 포함하는 problemResponse를 그대로 반환합니다.

NewProblemProcessor2

Process도 기존 거의 유사하며, stream과 map을 통해 Entity로 바꾸고 collect하는 부분만 바뀐 것을 알 수 있습니다.

 

NewProblemWriter2

 

Writer의 구현은

chunk의 item들이 각각 List<Problem>이기 때문에 

flatMap을 통해 평탄화하여 하나의 긴 List<Problem> stream으로 만든 뒤, JdbcTemplate를 이용해 bulk insert를 해주었습니다.


실험 계획

실험은 두 가지 환경에서 모두 시도했습니다.

환경 : 로컬 Intellij & 집에서 돌아가고 있는 라즈베리파이(Pi 4, 8GB)의 MariaDB를 Docker로 실행 (프로젝트에서 저희가 사용하는 DB성능은 일반적으로 라즈베리파이 정도 된다고 생각했습니다.)

 

실험은 문제 1000개를 가져오는 기준으로 비교했습니다.

 

실험 1 :  구현 1(단일 Item)과 구현 2(List Item)의 성능을 비교하면 구현 1(단일 Item)이 효과적일 것이다. 

로컬 MariaDB

 

구현 1: 3.727초

구현 2: 3.341초로 

오히려 List의 성능이 좋은 것으로 나타났습니다.

라즈베리 파이 MariaDB

 

구현 1: 9.798초

구현 2: 3.216초

이상하게 List Item으로 처리한 구현 2의 성능이 더 좋은 것으로 나타났습니다.


실험 2: DB에 insert될 때 problem의 수가 같으면 구현 2(List Item)가 더 느릴 것이다.

변인 통제 : 한 번에 insert되는 problem 수가 달라 db insert 요청 수가 달라서 의도하지 않은 결과 발생했을 것이다. 

1000문제 기준으로

아래는 chunk가 50개일 때 item 1개당 50개의 problem 즉 2500개의 problem이 한 번에 insert됩니다.

 

예상치 못한 결과가 발생하여 이는 Batch 성능 차이 보다도 DB 접근 라운드 타임으로 발생하는 이슈인가 싶어, 한 번에 write되는 item 수를 같다고 가정하고 비교해보겠습니다.

 

이 때 chunk size는 단일 Item일 때 200, List Item일 때 4로 처리하면 같은 크기의 DB insert가 수행됩니다.

환경 2, 구현 1

구현 1
환경 2, 구현 2

구현 2

 

 

이 때는 DB insert 요청의 수가 같기 때문에 예상한대로 chunk size를 맞추지 않은 구현 2의 성능이 더욱 비효율적인 것으로 나타났습니다.

실험의 신뢰도를 조금 더 높이기 위해 JdbcPagingItemWriter와 직접 만든 Jdbc Writer의 구현이 변경에도 차이점이 있기에,

 

변인을 완전히 통제하기 위해 구현이 같은 problemRepository.saveAll() 메소드를 통해 다시 Writer를 구현해서

다시 한 번 실험해 보았습니다.

TestWriter 구현

TestWriter (구현 1)

TestWriter (구현 1)
TestWriter2 (구현 2)

TestWriter 2 (구현 2)

saveAll로 고정시켜둔 뒤 성능 비교를 해보았습니다.

환경 2, (구현 1)

구현 1
환경 2, (구현 2)

구현 2

실제 예상대로 List<Problem>의 처리가 더 비효율적인 것으로 나타났습니다.

 

List Item을 다룰 때 성능이 잠깐 더 좋아졌던 것은 DB 접근 횟수가 줄어들어 라운드 트립 타임이 줄어들었기 때문으로 나타났습니다.

결국 같은 수의 Item을 같은 DB 횟수로 처리하면 List가 더 느리게 나타났습니다.

 

왜냐하면 List Item을 그대로 넘기면 page크기와 chunk크기가 다를 떄의 현상이 발생하기 떄문입니다.

왜냐하면 page사이즈는 변경할 수 없지만(모랜디 비즈니스 로직 특성) 한 번의 write 시 API 호출 여러 번이 필요하게 됩니다.

ex) 50개 List 그대로 반환, chunk Size가 50이면 2500개를 읽고, 한 번에 write하게 됨

 

일반적으로 Spring Batch chunk에서 JdbcPagingItemReader를 사용할 때에는

page 크기와 chunk size를 같게 유지하는 것을 추천합니다.

page 사이즈를 충분히 크게 하고, page size와 맞는 commit interval을 사용하면 성능이 향상된다.

 

 

사실 API 호출이라 큰 상관은 없겠지만 DB를 읽는 JpaPagingItemReader의 경우에는, chunk size보다 page size가 작아지면

processor로 넘어갈 당시의 마지막 page를 제외한 이전 page의 트랜잭션이 종료되어, lazy loading exception이 발생할 가능성이 있습니다. 해당 문제에 관해서는 다음 블로그 포스팅에서 공부했습니다.

https://jojoldu.tistory.com/146

 

Spring Batch에서 영속성 컨텍스트 문제 (processor에서 lazyException 발생할때)

안녕하세요? 이번 시간엔 springboot-batch에서 reader로 읽은 데이터를 processor로 넘길때 영속성 컨텍스트가 문제가 되는 상황을 해결해보려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면

jojoldu.tistory.com

 

 

 

 

 

 

그렇다면 DB접근 수를 줄이기 위해 chunk 크기를 늘이면 좋을까? 라는 생각이 들 수 있습니다.
그래서 기존 구현 1로 실험을 반복해보겠습니다.


실험 3 : page == chunk일 때 chunk 크기가 커지면 더욱 효과적일까?
chunk size 25, 50, 100, 200, 구현 2로 실험

chunk size 25

chunk size 25
chunk size 50

chunk size 50

chunk size 100

chunk size 100
chunk size 200

chunk size 200

문제 수가 1000개인 이유인지 큰 차이가 없습니다.

따라서, 문제 수를 약 10000개로 늘린 후 다시 실험해보겠습니다.


chunk size 25

chunk size 50

chunk size 100

chunk size 200

그렇게 극적인 차이가 나지는 않습니다,,,

 

하지만 chunk size를 너무 크게 유지하면, DB에 write 되기 전에 메모리에 상주하게 되는 item 수가 많아지는 것이기에 적절한 크기를 가지는 것이 좋습니다.

 

번외 실험 : ItemWriter 커스텀 구현과, JdbcBatchItemWriter 성능 차이는?

ItemWriter를 JdbcTemplate를 통한 구현입니다. 

JdbcPagingItemWriter

JdbcBatchItemWriter
ItemWriter를 JdbcTemplate를 통한 구현

ItemWriter를 JdbcTemplate를 통한 구현

 

최적화가 잘 되어있는 JdbcBatchItemWriter이고, 이 외에도 실제로 JdbcBatchItemWriter는 Spring Batch의 트랜잭션 관리와 통합되어 있어 더 유용한 관리에 도움을 줍니다.

 

하지만 jdbcTemplate을 이용하면 연관관계에 대한 처리가 힘들어 지는 점이 존재합니다.


결론

모두의 랜덤 디펜스의 사례에서는 api 요청의 page 크기가 50으로 고정돼있어 특수한 결과를 얻었는데

 

DB 접근 횟수가 최소화되어 List Item 처리 방식이 단일 Item 처리보다 더 빠르게 보일 수 있습니다. 대량의 데이터를 한 번에 처리할 수 있는 List Item 방식은 page, chunk불일치로 발생하는 overhead보다 더 줄일 수 있기는 합니다.

 

하지만 큰 크기의 List를 메모리에 저장하게 되면, 사용 가능한 메모리 리소스에 부담을 줄 수 있습니다.

 

또한 List Item을 그대로 넘기면 page크기와 chunk크기가 다를 떄의 현상이 발생하여 꼭 필요한 경우가 아니라면 단일 Item으로 쓰는 것을 추천합니다. List Item이 DB 접근 횟수는 줄이지만 완전히 최적화가 된 구조는 아니기 떄문입니다.

 

그렇지만 저희 상황처럼 특수하게 page 사이즈가 50으로 고정된 경우가 아니라면 사실 List나 단일 Item이나 큰 차이는 없을 것 같다는 생각이 들었습니다!

List Item을 선택할 때 한 번에 처리하는 데이터의 양이 너무 커지는 것만 아니라면 문제될 것이 거의 없습니다. 비즈니스 상황에 맞추어 Processor에서 처리하기 용이하게 사용하면 될 것 같습니다. 

 

 

이 외의 성과

1. DB에 batch insert하고 싶으면 꼭 필요한 경우가 아니면 구현 직접 하지 말고 JdbcBatchItemWriter를 쓰자

 

2. chunk가 커지면 db 접근 수가 줄어 어쩌면 더 효율적일 수도 있지만, page 수와 chunk의 크기를 같게하면 좋다. 

실험 결과에 따르면 페이지 사이즈와 청크 사이즈의 일치는 성능 최적화에 중요합니다. 페이지 사이즈와 청크 사이즈가 같을 경우, 데이터베이스의 접근 횟수를 줄이고 JVM 메모리 사용을 최적화하여 전체적인 처리 성능을 향상시킬 수 있습니다

- chunk과 page 크기의 불일치 때문에 성능에 악영향을 끼칠 수 있습니다. 

- DB 트랜잭션의 경우 lazy initialization exception 문제가 생길 수 있습니다.  (JpaPagingItemReader를 사용하는 경우)

 

3. chunk랑 page가 같으면 JVM 메모리가 가장 최적화된다.