Spring Batch로 DB에 일괄적으로 업데이트 해보기

오진서·2023년 10월 17일
3
post-custom-banner

Background

현재 진행 중인 프로젝트에서는 유튜버 정보와 유튜브 영상 정보가 DB에 저장되어 있다.
영상이나 유튜버 정보 업데이트가 꼭 실시간으로 이루어져야 할 필요는 없다.
때문에 일정 주기로 업데이트하고 싶어졌다.
따라서 하루에 한 번씩 DB에 업데이트 쿼리를 날리는 방안을 선택하였다.



스프링 스케줄러만 사용했을 때의 문제점

  • 업데이트 작업 이력 확인이 어렵다.
    • 작업이 실패하거나 중지되었을 때 그 이유나 상황을 자세히 알기 위해서는 별도의 로깅 시스템이나 모니터링 도구를 활용해야 한다.
  • 작업이 실패했을 때 재시도 로직을 구현하려면 개발자가 직접 구현해야 한다.
  • 대용량 데이터 처리에 대해 아무런 기능을 제공하지 않아 속도가 느리다.
  • 스프링 스케줄러는 작업의 상태(실행 중, 대기 중, 완료 등)를 관리하는 기능이 없다.
  • 스프링 스케줄러만을 사용하여 복잡한 병렬 처리나 분산 처리 시나리오를 구현하기 어렵다.
    • Scale-Out이 어렵다.


스프링 배치의 도입 이유

요구사항

유튜브 영상 데이터는 앞으로도 DB에 꾸준히 쌓이는 대량 데이터이다.

  • 재시도 및 오류 처리 전략 등으로 대용량 데이터 무결성과 일관성을 보장하고 싶다.
  • 대량의 데이터를 업데이트하는 시간을 줄이고 싶다.
  • 체계적인 로깅 및 모니터링을 하고 싶다.
    • 어떤 작업이 언제 실행되었는지, 어떤 데이터가 처리되었는지, 어떠한 이유로 실패했는지 등의 정보를 상세하게 알고 싶다.

스프링 스케줄러는 간단하고 주기적인 작업을 처리하는 데에는 적합하지만, 복잡한 대용량 데이터 처리나 실패 시 재시도와 같은 요구 사항이 있는 경우 스프링 배치와 같은 솔루션을 고려해볼 필요가 있다.



스프링 배치 아키텍처

  1. JobLauncher : 스프링 배치를 시작한다.
  2. Job: 배치 처리의 한 단위. (유튜브 영상 업데이트 작업1, 유튜버 업데이트 작업2)
    • 한 개 이상의 Step으로 구성된다.
  3. Step: Reader, Processor, Writer로 구성된다.
    • Reader: 데이터 소스에서 데이터를 읽는다.
    • Processor: 읽은 데이터를 가공한다.
    • Writer: 가공된 데이터를 다른 곳에 저장한다.
  4. JobRepository: 배치 작업의 실행 상태와 관련된 메타데이터를 저장한다.


스프링 배치를 도입하고..

1. 상세한 실행 이력 관리

스프링 배치는 내부적으로 실행 이력(Job Repository)을 관리한다.

  • 위 테이블에서 어떤 작업이 언제 실행되었는지, 어떤 데이터가 처리되었는지, 어떠한 이유로 실패했는지 등의 정보를 상세하게 알 수 있다.

BATCH_JOB_EXECUTION 조회

  • EXIT_CODE : 작업 상태
  • EXIT_MESSAGE : 작업 메시지

BATCH_STEP_EXECUTION 조회

  • 업데이트 작업의 상태와 진행 상황을 명확하게 파악

2. 대량 데이터 처리 효율성

  • 'Chunk' 기반 처리를 제공
    • 일정량의 데이터 Chunk (사용자 정의)를 한 번에 읽어서 처리 후 DB에 저장하는 방식을 사용
    • 대량의 데이터를 처리할 때, 한 번에 한 건씩 처리하는 것보다는 훨씬 빠르고 효율적
    • DB와의 트랜잭션 횟수를 줄일 수 있어, 시스템의 부하를 감소시키고 성능을 향상

3. 안정적인 에러 핸들링 및 복구

  • 특정 건에 에러가 발생하면 해당 Chunk의 처리를 롤백하고, 설정에 따라 자동 재시도나 에러 로깅할 수 있음
    • 유튜브 정보 업데이트 중 문제가 발생하더라도, 전체 배치 작업이 실패하는 것이 아니라 해당 부분만 롤백되므로 데이터의 일관성을 유지

4. 병렬 및 분산 처리

  • 스프링 배치는 멀티 쓰레드나 병렬 스텝 실행, 원격 청크 처리 등 **다양한 병렬 및 분산 처리 방식을 지원**한다.
    • 따라서 배치 서버만 스케일 아웃이 가능하다.
  • 유튜브 정보가 대량으로 존재하고, 이를 빠르게 업데이트하고자 할 때, 여러 쓰레드나 서버에서 동시에 처리를 진행하여 전체 작업 시간을 크게 단축시킬 수 있다.


스케줄러와 배치 수행 시간 비교

현재 DB 상황 : 업데이트가 필요한 1000개의 데이터가 저장되어 있다.

#1 스케줄러 사용해서 진행

@Test
    public void testScheduler() throws Exception {
        long schedulerStartTime = System.currentTimeMillis();
        schedulerService.updateExerciseInfo();
        long schedulerEndTime = System.currentTimeMillis();

        long schedulerDuration = schedulerEndTime - schedulerStartTime;

        log.info("Scheduler Duration: " + schedulerDuration + "ms");
    }

1000개 영상 데이터 업데이트 수행 속도 (89688ms)

#2 배치 사용해서 진행

@Test
    public void testBatch() throws Exception{ // 33m
        long batchStartTime = System.currentTimeMillis();
        exerciseUpdateJobLauncherTestUtils.launchJob(new JobParameters());
        long batchEndTime = System.currentTimeMillis();

        long batchDuration = batchEndTime - batchStartTime;

        log.info("Batch Duration: " + batchDuration + "ms");
    }

1000개 영상 데이터 업데이트 수행 속도 (21495ms)

한 번 더 실행 (71ms)

스케줄러 실행 시간이 89688ms(1분 30초)이고, 배치의 실행 시간이 21000ms(21초)이다. 그래서 스케줄러가 4.3배 더 길다.



💡 왜 시간 차이가 날까?

1. 병렬 처리

@Bean
    public Step exerciseUpdateStep(JobRepository jobRepository, PlatformTransactionManager platformTransactionManager) {
        int chunkSize = 100;
        return new StepBuilder("exerciseUpdateStep", jobRepository)
                .chunk(chunkSize, platformTransactionManager)
                .reader(exerciseUpdateItemReader)
                .writer(exerciseUpdateItemWriter)
                .taskExecutor(new SimpleAsyncTaskExecutor()) // 멀티 스레드로 병렬 처리
                .transactionManager(platformTransactionManager)
                .build();
    }
  • 일정량 chunk의 데이터 (사용자 정의)를 한 번에 읽어서 처리 후 DB에 저장하는 방식을 사용

  • chunkSize가 100으로 설정되어 있고 데이터가 총 1000개라면, 총 10개의 청크로 데이터가 나누어질 수 있다.

    • 각 청크에 대한 처리 작업은 SimpleAsyncTaskExecutor에 의해 별도의 스레드에서 실행된다.
    • SimpleAsyncTaskExecutor는 요청된 작업을 처리하기 위해 매번 새로운 스레드를 생성한다.
    • 즉, 10개의 스레드가 병렬 처리 되면서 속도가 훨씬 빨라진다.

(모든 스레드가 동시에 실행되는지는 실행 환경에 따라 달라질 수 있다)

2. 트랜잭션 관리

  • 스프링 배치는 청크 기반의 트랜잭션 관리를 제공 (platformTransactionManager)
  • 청크 크기를 100으로 설정한다는 것은 100개의 항목을 처리한 후, 한 번에 데이터베이스에 커밋한다.
    • I/O 최소화
    • 지정된 청크 크기만큼의 데이터만 메모리에 로드 하므로 메모리 사용량이 최적화
  • 에러가 발생했을 때 해당 청크 전체를 롤백하여 다시 처리할 수 있다.
    • 데이터의 무결성이 보장
profile
안녕하세요
post-custom-banner

0개의 댓글