Spring Batch 입문 38편 — Repeat · RepeatTemplate · CompletionPolicy

2026-05-17Spring Batch 입문에서 운영까지

Spring Batch 입문 38편. Batch 의 근본 building block — RepeatOperations. RepeatTemplate · RepeatCallback · RepeatStatus (CONTINUABLE/FINISHED), CompletionPolicy (SimpleCompletionPolicy 등), ExceptionHandler (SimpleLimitExceptionHandler), RepeatListener 5메서드, TaskExecutorRepeatTemplate 병렬 반복, RepeatOperationsInterceptor 선언적 반복까지 정리한 학습 노트.

📚 Spring Batch 입문에서 운영까지 · 38편 — Repeat · RepeatTemplate · CompletionPolicy

이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 38편이에요. 37편 까지 Step 의 확장 을 봤다면, 이번 38편은 Spring Batch 의 근본 building blockRepeat 추상화. Part 9 시작.

Batch = 반복 처리

Batch 처리 = 반복 작업. 이를 추상화 + 일반화 한 게 RepeatOperations 인터페이스. — 공식 reference

Chunk 처리, Tasklet 의 CONTINUABLE 반복, Skip, Retry 모두 반복 의 변형이고, Spring Batch 내부에서 RepeatOperations 위에 구현 되어 있어요.

→ 38편 = 그 내부 building block 들을 직접 활용 하는 자리.

RepeatOperations 인터페이스

public interface RepeatOperations {
    RepeatStatus iterate(RepeatCallback callback) throws RepeatException;
}

iterate(callback) 한 번 호출 = callback 을 반복 실행. 언제 멈출지 는 callback 이 직접 return 하거나 외부 정책이 결정해요.

RepeatCallback — 반복할 로직

public interface RepeatCallback {
    RepeatStatus doInIteration(RepeatContext context) throws Exception;
}

callback 의 return = RepeatStatus:

의미
CONTINUABLE 더 할 일 있음
FINISHED 더 할 일 없음

→ 19편의 Tasklet 의 RepeatStatus 와 동일 enum 이고, Tasklet 자체가 RepeatCallback 의 응용 이에요.

RepeatStatus 의 AND 연산

status1.and(status2)

논리 AND 라서 둘 중 하나라도 FINISHED 면 결과가 FINISHED, 둘 다 CONTINUABLE 일 때만 CONTINUABLE 이 돼요.

가장 단순한 사용

RepeatTemplate template = new RepeatTemplate();
template.setCompletionPolicy(new SimpleCompletionPolicy(2));   // 2회 반복

template.iterate(context -> {
    System.out.println("반복 중...");
    return RepeatStatus.CONTINUABLE;
});

// 출력: "반복 중..." 2번

SimpleCompletionPolicy(2) = 2회 반복 후 자동 종료.

callback 이 CONTINUABLE return 해도 policy 가 2회 도달 시 멈춤.

callback 이 FINISHED return 한 경우

template.iterate(context -> {
    if (allDone()) {
        return RepeatStatus.FINISHED;          // 조기 종료
    }
    return RepeatStatus.CONTINUABLE;
});

policy 의 한도와 무관하게 callback 이 직접 FINISHED → 즉시 멈춤.

RepeatContext — Attribute Bag

doInIteration(context)context 매개변수 = iteration scope 의 attribute 저장소:

template.iterate(ctx -> {
    Long count = (Long) ctx.getAttribute("counter");
    if (count == null) count = 0L;
    ctx.setAttribute("counter", count + 1);
    return RepeatStatus.CONTINUABLE;
});

iteration 이 끝나면 context 는 소멸 하니까, 임시 상태 추적 용도로 쓰는 게 맞아요.

Nested iteration 의 parent context

RepeatTemplate outer = new RepeatTemplate();
RepeatTemplate inner = new RepeatTemplate();

outer.iterate(outerCtx -> {
    inner.iterate(innerCtx -> {
        // innerCtx.getParent() == outerCtx
        return RepeatStatus.CONTINUABLE;
    });
    return RepeatStatus.CONTINUABLE;
});

중첩 iteration 에서는 inner 의 getParent() 가 outer context 를 가리키니까, iteration 간에 데이터를 공유 할 수 있어요.

CompletionPolicy — 종료 결정

RepeatTemplateiterate 종료 = CompletionPolicy 가 결정. — 공식 reference

RepeatTemplate 은 매 iteration:

  1. CompletionPolicy.update(context) 호출 → 상태 변경
  2. CompletionPolicy.isComplete(context) 검사
  3. complete = 멈춤, 아니면 다음 iteration

표준 구현

Policy 동작
SimpleCompletionPolicy(N) N 회 반복
TimeoutTerminationPolicy(ms) 시간 한도
CompositeCompletionPolicy 여러 policy AND/OR
DefaultResultCompletionPolicy callback 의 status 만 검사

예제 — Timeout

RepeatTemplate template = new RepeatTemplate();
template.setCompletionPolicy(new TimeoutTerminationPolicy(60_000));   // 60초

template.iterate(ctx -> {
    pollExternalSystem();
    return RepeatStatus.CONTINUABLE;
});

60초가 지나면 자동으로 멈춰서, 외부 시스템 폴링 패턴 (19편 Tasklet CONTINUABLE 의 안전 버전) 으로 쓸 수 있어요.

Custom Policy

public class BusinessHourPolicy implements CompletionPolicy {

    @Override
    public boolean isComplete(RepeatContext context) {
        return LocalTime.now().isAfter(LocalTime.of(18, 0));  // 6 PM 후 종료
    }

    @Override
    public boolean isComplete(RepeatContext context, RepeatStatus result) {
        return RepeatStatus.FINISHED == result || isComplete(context);
    }

    @Override
    public RepeatContext start(RepeatContext parent) {
        return new RepeatContextSupport(parent);
    }

    @Override
    public void update(RepeatContext context) { }
}

업무 시간이 지나면 종료시키는 식으로, online 시스템 가동 시간 회피 패턴에 활용해요.

ExceptionHandler — 예외 정책

callback 안 예외 발생 시 ExceptionHandler 가 처리:

public interface ExceptionHandler {
    void handleException(RepeatContext context, Throwable throwable) throws Throwable;
}

세 가지 동작 선택:

  1. throw re-throw = 즉시 종료
  2. handle 후 return (예외 흡수) = iteration 계속
  3. 다른 예외 throw = 변환

SimpleLimitExceptionHandler

SimpleLimitExceptionHandler handler = new SimpleLimitExceptionHandler();
handler.setLimit(5);                              // 5번까지 흡수
handler.setExceptionClasses(List.of(RuntimeException.class));

RepeatTemplate template = new RepeatTemplate();
template.setExceptionHandler(handler);

같은 type 의 예외라면 5번까지 흡수해서 계속 진행하고, 6번째 부터는 re-throw 해요. 다른 type 의 예외는 무조건 re-throw.

useParent 옵션

handler.setUseParent(true);

false (default) 면 현재 context 의 limit 만 보지만, true 로 두면 parent context 까지 누적해서 본다는 뜻이라, Step 의 chunk 들 사이 에 limit 을 공유하고 싶을 때 유용해요.

RethrowOnThresholdExceptionHandler

조금 더 유연한 버전인데 threshold 별로 다른 예외 처리 가 가능해요. drilling 시에 자세히 봐요.

RepeatListener — 5 메서드 hook

public interface RepeatListener {
    void open(RepeatContext context);                 // 전체 시작
    void before(RepeatContext context);               // 각 iteration 시작
    void after(RepeatContext context, RepeatStatus result);   // 각 iteration 종료
    void onError(RepeatContext context, Throwable e);          // 각 iteration 에러
    void close(RepeatContext context);                 // 전체 종료
}

open · close = 전체 iteration (1회). before · after · onError = 각 iteration (반복).

호출 순서

open
  before
  doInIteration → result
  after (또는 onError)
  before
  doInIteration → result
  after
  ...
  (CompletionPolicy 가 complete 판단)
close

여러 Listener 의 순서

When there is more than one listener, open and before are called in the same order while after, onError, and close are called in reverse order. — 공식 reference

before/open 은 등록 순서대로 호출되고, after/onError/close 는 역순으로 호출돼요.

→ 18편의 Step Listener 에서 본 나선형 wrapping 패턴과 같아요.

사용 예제 — 메시지 폴링

@Bean
public Tasklet pollingTasklet(MessageQueue queue) {
    return (contribution, chunkContext) -> {
        RepeatTemplate template = new RepeatTemplate();
        template.setCompletionPolicy(new CompositeCompletionPolicy(
            new SimpleCompletionPolicy(100),                  // 최대 100 메시지
            new TimeoutTerminationPolicy(30_000)              // 30초 한도
        ));

        template.iterate(context -> {
            Message msg = queue.poll();
            if (msg == null) return RepeatStatus.FINISHED;    // 큐 비면 종료

            processMessage(msg);
            return RepeatStatus.CONTINUABLE;
        });

        return RepeatStatus.FINISHED;     // Tasklet 의 RepeatStatus
    };
}

Tasklet 안에서 RepeatTemplate 을 활용해서 메시지 100개 또는 30초까지 처리하게 한 거예요. Tasklet 의 CONTINUABLE 무한 폴링 의 대안이 돼요.

TaskExecutorRepeatTemplate — 병렬 반복

TaskExecutorRepeatTemplate template = new TaskExecutorRepeatTemplate();
template.setTaskExecutor(executor);
template.setCompletionPolicy(new SimpleCompletionPolicy(100));

template.iterate(ctx -> {
    doExpensiveWork();
    return RepeatStatus.CONTINUABLE;
});

매 iteration 의 callback 을 TaskExecutor 의 thread 들에 분배 해서 돌리는데, 37편 Multi-threaded StepRepeat 레벨 변형이라고 보면 돼요.

기본값은 SynchronousTaskExecutor (=RepeatTemplate 와 동일) 라서, 비동기 TaskExecutor 를 주입해야 진정한 병렬이 돼요.

RepeatOperationsInterceptor — 선언적 반복

AOP(관점 지향 프로그래밍) 로 메서드 호출을 자동 반복:

@Bean
public MyService myService() {
    ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader());
    factory.setInterfaces(MyService.class);
    factory.setTarget(new MyServiceImpl());

    MyService service = (MyService) factory.getProxy();

    JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
    pointcut.setPatterns(".*processMessage.*");

    RepeatOperationsInterceptor interceptor = new RepeatOperationsInterceptor();
    ((Advised) service).addAdvisor(new DefaultPointcutAdvisor(pointcut, interceptor));

    return service;
}

processMessage() 호출이 자동 반복 되는 거라서, 명시적으로 RepeatTemplate.iterate() 를 호출 하지 않아도 돼요.

Return 값에 따른 동작

return type 동작
void 항상 CONTINUABLE (무한 루프 위험 — CompletionPolicy 필수)
비-void, null 반환 FINISHED
비-void, non-null CONTINUABLE

→ 메시지 처리 메서드가 큐가 비면 null 을 반환 하게 두면 자동으로 종료 되는 식이에요.

안전성 — CompletionPolicy 필수

RepeatTemplate customTemplate = new RepeatTemplate();
customTemplate.setCompletionPolicy(new SimpleCompletionPolicy(1000));

RepeatOperationsInterceptor interceptor = new RepeatOperationsInterceptor();
interceptor.setRepeatOperations(customTemplate);

기본 RepeatTemplate 에 CompletionPolicy 가 없으면 무한 루프 위험이 있으니 항상 명시해 두는 게 좋아요.

Step 의 chunk 와 Repeat 의 관계

Step 의 chunk-oriented processing
  ↓ (내부 구현)
RepeatTemplate.iterate(chunkCallback)
  ↓ (매 iteration = 1 chunk)
  read → process → write → ExecutionContext.update → commit
  ↓
CompletionPolicy 가 *데이터 끝* 검사

Step 자체가 RepeatTemplate 의 응용 이라서, chunk 반복 이 곧 RepeatTemplate 의 callback 반복 인 셈이에요.

→ 38편의 Repeat 을 이해하는 게 곧 Spring Batch 내부 동작 이해 로 이어져요.

자주 만나는 사고

사고 1: 무한 루프

원인 — callback 이 항상 CONTINUABLE + CompletionPolicy 없음.

해결CompletionPolicy 명시 또는 callback 안 조기 종료 조건.

사고 2: CONTINUABLE 외부 의도 무시

원인 — callback 의 CONTINUABLE 인데 CompletionPolicy 가 N 회 만에 멈춤.

해결외부 policy 가 우선 인 게 정상. callback 의 CONTINUABLE 은 추가 신호.

사고 3: ExceptionHandler 가 안 불림

원인checked exception throw 인데 callback 이 RuntimeException 만 처리.

해결 — ExceptionHandler 의 클래스 list 에 해당 type 포함.

사고 4: RepeatContext attribute 가 다음 iteration 에 사라짐

원인 — context 의 iteration scopeiteration 간 scope 혼동.

해결 — iteration 간 공유는 parent context 또는 외부 변수.

사고 5: TaskExecutorRepeatTemplate 의 callback thread-safety

원인 — 병렬 callback 이 공유 state 변경.

해결 — callback 안 공유 state 회피 또는 thread-safe 컬렉션.

사고 6: Listener 순서 혼동

원인 — before 와 after 의 대칭 가정.

해결 — before = 등록 순, after = 역순 (nested wrapping 의미).

사고 7: RepeatOperationsInterceptor 의 void 메서드 무한 루프

원인 — void return 은 항상 CONTINUABLE → CompletionPolicy 없으면 무한.

해결 — 명시적 CompletionPolicy 또는 비-void return (null 시 종료).

운영 권장 패턴

Pattern 1: Polling with timeout

RepeatTemplate template = new RepeatTemplate();
template.setCompletionPolicy(new CompositeCompletionPolicy(
    new TimeoutTerminationPolicy(60_000),
    new SimpleCompletionPolicy(1000)
));

template.iterate(ctx -> {
    Item item = queue.poll(Duration.ofSeconds(5));
    if (item == null) return RepeatStatus.FINISHED;
    process(item);
    return RepeatStatus.CONTINUABLE;
});

시간 한도와 메시지 한도에 queue 비면 종료까지 더해서 3중 가드를 만든 거예요.

Pattern 2: Limited exception tolerance

SimpleLimitExceptionHandler handler = new SimpleLimitExceptionHandler();
handler.setLimit(10);
handler.setExceptionClasses(List.of(TransientException.class));

RepeatTemplate template = new RepeatTemplate();
template.setExceptionHandler(handler);
template.setCompletionPolicy(new SimpleCompletionPolicy(1000));

transient 예외를 10번까지 허용하면서 전체적으로는 1000회 한도로 묶어 둔 패턴이에요.

Pattern 3: 메트릭 Listener

public class MetricsRepeatListener implements RepeatListener {
    private long totalIterations = 0;
    private long errors = 0;

    @Override public void open(RepeatContext c) {}
    @Override public void before(RepeatContext c) { totalIterations++; }
    @Override public void after(RepeatContext c, RepeatStatus r) {}
    @Override public void onError(RepeatContext c, Throwable e) { errors++; }
    @Override public void close(RepeatContext c) {
        meterRegistry.counter("repeat.iterations").increment(totalIterations);
        meterRegistry.counter("repeat.errors").increment(errors);
    }
}

45편의 Observability 와 결합해서 쓰는 방식이에요.

Pattern 4: AOP 선언적 반복

@Bean
public RepeatOperationsInterceptor repeatInterceptor() {
    RepeatOperationsInterceptor interceptor = new RepeatOperationsInterceptor();
    RepeatTemplate template = new RepeatTemplate();
    template.setCompletionPolicy(new SimpleCompletionPolicy(100));
    interceptor.setRepeatOperations(template);
    return interceptor;
}

명시적으로 RepeatTemplate.iterate() 를 부르지 않고 메서드를 자동 반복 시키는 구성이에요.

시험 직전 한 번 더 — Repeat 함정 압축 노트

  • RepeatOperations = Spring Batch 의 반복 추상화 building block
  • Step 의 chunk 처리 = RepeatTemplate 의 응용
  • iterate(callback) = callback 반복 실행
  • RepeatCallback = doInIteration(context) → RepeatStatus
  • RepeatStatus = CONTINUABLE · FINISHED
  • AND 연산 — 하나라도 FINISHED → FINISHED
  • 19편 Tasklet 의 RepeatStatus 와 동일 enum
  • RepeatTemplate = 표준 구현
  • CompletionPolicy = 종료 결정 (외부 policy)
  • 표준 — SimpleCompletionPolicy(N) · TimeoutTerminationPolicy(ms) · CompositeCompletionPolicy · DefaultResultCompletionPolicy
  • callback 의 FINISHED = 조기 종료, policy 의 isComplete = 강제 종료
  • RepeatContext = iteration scope attribute 저장소
  • iteration 끝나면 소멸
  • nested iteration = getParent() 로 outer 접근
  • ExceptionHandler = callback 예외 정책 — re-throw / 흡수 / 변환
  • SimpleLimitExceptionHandler = 한도까지 흡수, 초과 시 re-throw
  • useParent = true = parent context 까지 누적
  • RepeatListener 5 메서드 = open · before · after · onError · close
  • open/close = 전체 1회, before/after/onError = 매 iteration
  • before/open = 등록 순, after/onError/close = 역순 (nested wrapping)
  • TaskExecutorRepeatTemplate = 병렬 iteration (TaskExecutor)
  • 기본 = SynchronousTaskExecutor (= RepeatTemplate)
  • callback thread-safety 주의
  • RepeatOperationsInterceptor = AOP 선언적 반복
  • void return = 항상 CONTINUABLE (무한 루프 위험)
  • 비-void null = FINISHED, non-null = CONTINUABLE
  • CompletionPolicy 명시 필수
  • Step 의 chunk 처리 = RepeatTemplate 응용 (이해 가치 ↑)
  • 함정 — 무한 루프 (CompletionPolicy 누락)
  • 함정 — 외부 policy 우선 (callback CONTINUABLE 무시)
  • 함정 — Exception class list 누락
  • 함정 — context attribute iteration 끝 소멸
  • 함정 — 병렬 callback shared state
  • 함정 — Listener 순서 (after 역순)
  • 함정 — RepeatOperationsInterceptor void 무한 루프
  • 패턴 — Polling with timeout (Composite policy)
  • 패턴 — Limited exception tolerance
  • 패턴 — 메트릭 Listener (Observability)
  • 패턴 — AOP 선언적 반복

공식 문서: Repeat 에서 원문을 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!