트러블 슈팅

Spring Batch ItemReader 성능개선기

송경훈 2024. 12. 16. 14:44
반응형

프로그래머스 데브코스에서 IT 취업 준비생을 위한 채용 맞춤형 뉴스레터를 개발하던 도중 트러블 슈팅이다.

 

사람인 채용 정보 API를 호출하여 채용 정보를 얻어오고 회원이 입력한 키워드에 맞게 필터링을 하여 맞춤화된 채용 정보를 제공하는 기능 구현에 있어서 Spring Batch를 사용하였다.

 

Spring Batch는 대용량 데이터 처리를 위한 프레임워크로, 배치 작업을 효율적으로 처리할 수 있게 도와준다. 주로 데이터베이스에서 데이터를 읽어와 처리한 후 다시 저장하는 등의 작업에 적합하며, 대규모 데이터를 한 번에 처리해야 하는 경우 많이 사용된다.

 

Spring Batch에서는 작업 단위를 Job이라고 부르고, 이 Job은 여러 개의 Step으로 구성된다. 각 Step은 읽기(Read), 처리(Process), 쓰기(Write)의 단계를 거치며, 이를 통해 데이터를 순차적으로 처리한다.

 

Step은 크게 세 가지 단계로 구성된다.

  1. ItemReader: 데이터를 읽어오는 단계다. 여기서 데이터베이스, 파일 또는 API에서 데이터를 가져올 수 있다.
  2. ItemProcessor: 읽어온 데이터를 처리하는 단계다. 필요한 비즈니스 로직을 적용하여 데이터를 변환하거나 필터링할 수 있다.
  3. ItemWriter: 처리된 데이터를 저장하는 단계다. 데이터베이스나 파일, 또는 다른 시스템에 데이터를 저장할 수 있다.

1,000만 개와 같은 대량의 데이터를 처리하는 경우에는 서버의 물리적 한계로 한 번에 처리할 수 없다. 1,000개씩 나누어 10,000번 처리해야 합니다. 이를 청크(chunk) processing 이라고 한다.

Chunk Processing을 하기 위해서 데이터를 일정 개수만큼 나누어야 한다. 이때, PageItemReader를 사용하게 되면 Page라는 단위로 데이터를 잘라서 처리할 수 있다.

 

1. RepositoryItemReader

아래는 스프링 배치에서 공식적으로 지원하는 PageItemReader인 RepositoryItemReader로 구현한 것이다.

위의 itemReader가 실행되면 아래의 쿼리문이 실행된다.

991번째부터 1000번째까지의 10개 job_posting 레코드를 가져오기 위해 990까지 읽어야 한다는 단점이 있고 이는 성능 저하를 일으킨다.

 

2. JPQL 사용의 한계

이 문제를 해결하기 위해 findByStartToEnd()와 같은 메서드를 사용하여 인덱스를 기반으로 데이터 범위를 조회하려고 했으나, JPQL로 작성하는 데 어려움이 있었다. JPQL은 복잡한 조건을 처리하는 데 한계가 있었고, 이를 통해 효율적으로 쿼리를 작성하기 어려웠다.

 

3. Querydsl 도입

Querydsl의 복잡한 쿼리 작성 및 타입 안정성, 자동완성, 컴파일 단계 문법체크, 공백 이슈 대응 등의 장점을 얻고자 도입을 결정하였다. 하지만 Spring Batch에서 공식적으로 QuerydslItemReader를 지원하지 않았다.

 

4. QuerydslNoOffsetPagingItemReader : Querydsl과 NoOffset을 사용한 성능 개선

Spring Batch 프레임워크에서 공식적으로 QuerydslItemReader를 지원하지 않기 때문에 큰 변경 없이 Spring Batch에 QuerydslItemReader를 사용한다면 다음과 같이 AbstractPagingItemReader를 상속한 ItemReader 생성해야만 했다.

AbstractPagingItemReader 클래스의 전체적인 구조는 아래와 같다.

AbstractPagingItemReader를 상속받는 JpaPagingItemReader를 확인해 보면 아래와 같다.

createQuery()와 이를 호출하는 doReadPage()를 확인할 수 있다. 위 동작을 Querydsl로 변경하기 위해 private으로 선언된 createQuery()로 인해 JpaPagingItemReader가 아닌 AbstractPagingItemReader를 상속받는 것이다.

 

AbstractPagingItemReader의 모든 메서드를 상속받은 QuerydslPagingItemReader 클래스를 생성하였고 doReadPage를 새롭게 오버라이딩했다.

기존의 doReadPage()에는 Offset 설정이 있었지만 조건(where)에서 현재까지 읽은 데이터의 마지막 id(pk)를 시작으로 pageSize만큼 가져오는 것을 확인할 수 있다.

위 그림은 이해를 돕기 위해 kakaopay의 tech log에서 가져온 그림이다.

이로써 Spring Batch를 통해 수만 건의 데이터를 조회할 때 성능을 크게 향상시킬 수 있었다.

 

기존 43.64s -----> 개선 2.46s

기존의 RepositoryItemReader
QuerydslNoOffsetPagingItemReader

 

 

느낀 점

Spring Batch와 Querydsl을 함께 사용하는 사례가 많지 않아 참고할 수 있는 레퍼런스가 적었다. 해결책을 찾기 위해 카카오페이, 우아한형제들 등 대기업 기술 블로그를 깊이 탐구했다. 평소 대기업 기술 블로그를 보며 높은 기술적 난이도에 감탄만 했던 나였지만, 이번 프로젝트에서는 해당 내용을 실제로 내 코드에 적용하며 해결책을 완성할 수 있었다. 이 경험을 통해 모르는 문제 앞에서 막막함을 느끼던 내가, 이를 오히려 성장의 기회로 받아들이는 태도로 변화할 수 있게 해준 값진 경험이었다.

 

 

 

 

출처/참고 : https://tech.kakaopay.com/post/ifkakao2022-batch-performance-read/

 

[if kakao 2022] Batch Performance를 고려한 최선의 Reader | 카카오페이 기술 블로그

if(kakao)2022 대량의 데이터를 Batch로 읽을 때의 노하우를 공유합니다.

tech.kakaopay.com