Spring Batch 입문 38편. Batch 의 근본 building block — RepeatOperations. RepeatTemplate · RepeatCallback · RepeatStatus (CONTINUABLE/FINISHED), CompletionPolicy (SimpleCompletionPolicy 등), ExceptionHandler (SimpleLimitExceptionHandler), RepeatListener 5메서드, TaskExecutorRepeatTemplate 병렬 반복, RepeatOperationsInterceptor 선언적 반복까지 정리한 학습 노트.
이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 38편이에요. 37편 까지 Step 의 확장 을 봤다면, 이번 38편은 Spring Batch 의 근본 building block — Repeat 추상화. 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 — 종료 결정
RepeatTemplate 의 iterate 종료 = CompletionPolicy 가 결정. — 공식 reference
RepeatTemplate 은 매 iteration:
CompletionPolicy.update(context)호출 → 상태 변경CompletionPolicy.isComplete(context)검사- 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;
}
세 가지 동작 선택:
throwre-throw = 즉시 종료- handle 후 return (예외 흡수) = iteration 계속
- 다른 예외 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 Step 의 Repeat 레벨 변형이라고 보면 돼요.
기본값은 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 scope 와 iteration 간 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) → RepeatStatusRepeatStatus= 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-throwuseParent = true= parent context 까지 누적RepeatListener5 메서드 = 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 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 33편 — Multi-File Input · MultiResourceItemReader
- 34편 — Database Reader · Writer · Cursor vs Paging
- 35편 — ItemProcessor · 변환 · 필터 · 검증
- 36편 — Reusing Services · ItemReaderAdapter · Process Indicator
- 37편 — Scaling · Parallel 6가지 전략 종합
다음 글: