Spring Batch 입문 16편. chunk transaction 의 세부 속성 — Isolation level·Propagation·Timeout 의 의미와 운영 환경 권장 설정, ItemReader/Writer 측 Spring @Transactional 과의 관계, 자주 만나는 함정까지 풀어쓴 학습 노트.
이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 16편이에요. 15편 까지 Skip·Retry 를 잡았다면, 이번 16편은 chunk(한 묶음 단위 처리) 트랜잭션(원자적 작업 단위) 의 세부 제어 — Transaction Attributes.
Transaction Attributes 세 가지
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setIsolationLevel(Isolation.READ_COMMITTED.value());
attribute.setPropagationBehavior(Propagation.REQUIRED.value());
attribute.setTimeout(30); // 30 초
return new StepBuilder("step1", repo)
.<X, Y>chunk(100, tx)
.reader(...).writer(...)
.transactionAttribute(attribute)
.build();
chunk transaction 을 제어하는 축은 셋이에요. Isolation Level 은 동시에 도는 transaction 끼리의 격리 수준을 정하고, Propagation 은 이미 transaction 이 열려 있을 때 어떻게 합류·분리할지를 정하고, Timeout 은 transaction 한 개의 최대 시간을 잡아둡니다.
Spring core 의 @Transactional 어노테이션과 같은 개념. Spring Batch 는 chunk 단위 transaction 에 적용.
Isolation Level — 5가지
JDBC(자바 DB 연결 표준) 표준 격리 수준 — 시리즈 2 PostgreSQL 38편 MVCC(다중 버전 동시성 제어) isolation 의 같은 영역.
| Level | 의미 | 자주 자리 |
|---|---|---|
| DEFAULT | DB 기본값 | 대부분 환경 (PostgreSQL = READ_COMMITTED) |
| READ_UNCOMMITTED | 가장 약함, dirty read 가능 | 거의 안 씀 |
| READ_COMMITTED | dirty read X, non-repeatable read 가능 | 대부분 OLTP(온라인 거래 처리) |
| REPEATABLE_READ | non-repeatable read X | 정밀 집계 |
| SERIALIZABLE | 가장 엄격, phantom read X | 금융·법적 보장 |
운영 권장
attribute.setIsolationLevel(Isolation.READ_COMMITTED.value());
대부분 환경에서는 READ_COMMITTED 나 DEFAULT 로 두면 성능과 일관성이 균형을 이룹니다.
SERIALIZABLE 사용 자리
attribute.setIsolationLevel(Isolation.SERIALIZABLE.value());
여러 chunk 가 같은 row 를 동시에 수정하는 환경에서 완전 격리가 필요할 때 씁니다. 다만 throughput 이 큰 폭으로 떨어질 수 있어요.
7편 JobRepository(잡 메타데이터 저장소) 의 ISOLATION_SERIALIZABLE 은 별개 (JobRepository 자체의 동시 createJobExecution 차단 용도).
Propagation — 7가지
REQUIRED 기본 — 있으면 참여, 없으면 새로
SUPPORTS 있으면 참여, 없으면 transaction 없이
MANDATORY 반드시 있어야 (없으면 exception)
REQUIRES_NEW 항상 새 transaction
NOT_SUPPORTED transaction 없이 실행
NEVER transaction 없어야 (있으면 exception)
NESTED 중첩 transaction (savepoint(부분 롤백 지점))
chunk 의 기본 = REQUIRED
attribute.setPropagationBehavior(Propagation.REQUIRED.value());
대부분 환경에서 각 chunk 가 자기 transaction 을 갖습니다.
REQUIRES_NEW 의 의미
// Listener·외부 호출 안에서
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logSkip(SkippedRecord record) {
skipRepository.save(record); // chunk transaction 과 분리
}
chunk 가 rollback 돼도 이 작업은 따로 commit 됩니다. SkipListener(스킵 이벤트 후처리 콜백)·로깅 같은 자리에 자주 씁니다.
NOT_SUPPORTED — Tasklet 에서 자주
@Bean
public Step nonTxTaskletStep(JobRepository repo, PlatformTransactionManager tx) {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setPropagationBehavior(Propagation.NOT_SUPPORTED.value());
return new StepBuilder("nonTxStep", repo)
.tasklet(externalShellTasklet, tx)
.transactionAttribute(attribute)
.build();
}
shell 명령이나 외부 API 호출처럼 transaction 의미가 없는 작업에 씁니다. transaction overhead 가 사라지죠.
여기서 시험 함정이 하나 있어요 — NOT_SUPPORTED 의 Step 은 ExecutionContext(Step 실행 중 상태 저장소) 영속 안 됨 가능. 재시작 안전성 약화. 단발 작업에만.
Timeout — 초 단위
attribute.setTimeout(30); // 30 초
chunk 한 개의 최대 시간. 30초 안에 transaction commit 이 안 끝나면 rollback 되면서 exception 이 떨어집니다.
권장 설정
| 시나리오 | Timeout |
|---|---|
| 일반 DB chunk | 30~120 초 |
| 외부 API 호출 포함 | 60~300 초 |
| 대량 batch insert | 300~1800 초 |
| Tasklet (단발 작업 Step) (압축·shell) | 환경에 따라 |
기본값은 -1, 즉 timeout 없음으로 DB 나 driver 의 기본값을 따라갑니다.
너무 짧은 timeout
timeout(10) 에 chunk size 5000 + 느린 외부 호출이 겹치면 timeout 이 폭증합니다. 측정한 뒤 여유 있게 잡아 두세요.
너무 긴 timeout
timeout(3600) (1시간) 이면 stuck job 검출이 어렵습니다. 합리적인 한계에 모니터링을 같이 두는 게 안전합니다.
DefaultTransactionAttribute — Bean 패턴
@Configuration
public class TransactionAttributesConfig {
@Bean
public TransactionAttribute defaultChunkTxAttribute() {
DefaultTransactionAttribute attr = new DefaultTransactionAttribute();
attr.setIsolationLevel(Isolation.READ_COMMITTED.value());
attr.setPropagationBehavior(Propagation.REQUIRED.value());
attr.setTimeout(120);
return attr;
}
@Bean
public TransactionAttribute longChunkTxAttribute() {
DefaultTransactionAttribute attr = new DefaultTransactionAttribute();
attr.setIsolationLevel(Isolation.READ_COMMITTED.value());
attr.setTimeout(1800); // 30 분
return attr;
}
}
여러 Step 에서 재사용:
@Bean
public Step myStep(JobRepository repo, PlatformTransactionManager tx,
TransactionAttribute defaultChunkTxAttribute) {
return new StepBuilder("myStep", repo)
.<X, Y>chunk(100, tx)
.reader(...).writer(...)
.transactionAttribute(defaultChunkTxAttribute)
.build();
}
Spring @Transactional 과의 관계
@Service
public class MyProcessor implements ItemProcessor<X, Y> {
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public Y process(X item) {
// ...
}
}
여기서 시험 함정이 하나 있어요 — Processor·Writer 의 @Transactional 이 chunk transaction 안에서 동작. Propagation 을 명시하지 않으면 기존 chunk transaction 에 그대로 참여합니다(REQUIRED).
자주 보는 자리는 두 갈래로 나뉘어요. REQUIRES_NEW 면 별도 transaction 으로 빠져서 chunk rollback 의 영향을 받지 않고, NOT_SUPPORTED 면 transaction 밖에서 돌아갑니다. 대부분 환경에서는 명시하지 않고 chunk transaction 을 그대로 따라가는 쪽이 보통입니다.
자주 쓰는 패턴
패턴 1: 일반 DB ETL(추출·변환·적재)
DefaultTransactionAttribute attr = new DefaultTransactionAttribute();
attr.setIsolationLevel(Isolation.READ_COMMITTED.value());
attr.setTimeout(120);
new StepBuilder(...)
.<X, Y>chunk(500, tx)
.reader(...).writer(...)
.transactionAttribute(attr)
.build();
대부분의 ETL.
패턴 2: 외부 API 호출 — 긴 timeout
attr.setTimeout(600); // 10 분 (외부 API timeout + buffer)
chunk size 작게 + timeout 충분히.
패턴 3: SkipListener 의 별도 transaction
@Component
public class DlqSkipListener implements SkipListener<X, Y> {
@Autowired
private DlqRepository repo;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void onSkipInProcess(X item, Throwable t) {
repo.save(new DlqRecord(item, t));
// chunk 가 rollback 돼도 DLQ 는 commit
}
}
DLQ(Dead Letter Queue, 실패 메시지 보관소) 저장이 chunk transaction 의 영향을 받지 않습니다.
패턴 4: 외부 명령 Tasklet — NOT_SUPPORTED
DefaultTransactionAttribute attr = new DefaultTransactionAttribute();
attr.setPropagationBehavior(Propagation.NOT_SUPPORTED.value());
new StepBuilder("compress-files", repo)
.tasklet(compressionTasklet, tx)
.transactionAttribute(attr)
.build();
파일 압축·shell 명령 — transaction 의미 없음.
chunk size 와 timeout 의 관계
chunk size 1000 + write 평균 100ms × 1000 = 100초 + overhead
chunk size 가 클수록 한 chunk 가 차지하는 시간도 길어집니다. timeout 도 그에 맞춰 늘려야 해요.
권장
timeout = chunk size × 평균 처리 시간 × 안전계수 (2~3)
예: chunk 100, 평균 50ms = 5초 → timeout 15~30초.
Isolation 변경의 위험
너무 느슨
READ_UNCOMMITTED 이면 dirty read 가 가능해져요. 다른 transaction 의 commit 되지 않은 데이터까지 보이니까 결과가 어긋납니다.
너무 엄격
SERIALIZABLE 이면 throughput 이 폭락합니다. PostgreSQL 에서는 serialization failure 도 자주 떨어집니다.
대부분 환경 = DEFAULT 또는 READ_COMMITTED.
한계·실무 함정
1. Timeout 누락
timeout=-1 이면 transaction 이 무한 대기할 수 있습니다. stuck transaction 위험이 그대로 남으니 명시 설정을 권장합니다.
2. Isolation 변경 후 deadlock(서로 잠금을 기다려 멈춤) 증가
SERIALIZABLE 이면 lock 이 늘어나면서 deadlock 도 잦아집니다. retry policy 를 같이 거세요(15편).
3. Propagation REQUIRES_NEW 의 connection
별도 transaction 은 별도 connection 을 쓰니까 connection pool(DB 연결 재사용 풀) 이 모자랄 수 있어요. pool size 를 모니터링해 두세요.
4. NOT_SUPPORTED 의 ExecutionContext
transaction 없이 실행되면 ExecutionContext 가 영속되지 않을 수 있어요. 재시작 안전성이 약해지니 단발 작업에만 쓰세요.
5. Spring @Transactional 충돌
Processor·Writer 의 @Transactional 이 chunk transaction 과 충돌할 수 있어요. Propagation 을 명시하거나 아예 떼고 chunk transaction 만 쓰는 쪽이 깔끔합니다.
6. Multi-DataSource(여러 DB 연결 동시 사용) 의 transaction
7편 JobRepository 와 business DB 가 분리된 환경에서는 chunk transaction 이 어느 DataSource 의 TM(트랜잭션 매니저) 을 쓸지 명시해 둬야 합니다.
시험 직전 한 번 더 — Transaction Attributes 함정 압축 노트
- 3가지 = Isolation · Propagation · Timeout
- 적용 =
transactionAttribute(DefaultTransactionAttribute)on Step - Spring core 의
@Transactional과 같은 모델, chunk 단위 적용 - Isolation 5가지 — DEFAULT · READ_UNCOMMITTED · READ_COMMITTED (권장) · REPEATABLE_READ · SERIALIZABLE
- 대부분 = DEFAULT 또는 READ_COMMITTED
- SERIALIZABLE = 금융·법적 보장, throughput 폭락
- Propagation 7가지 — REQUIRED (기본) · SUPPORTS · MANDATORY · REQUIRES_NEW · NOT_SUPPORTED · NEVER · NESTED
- chunk 기본 = REQUIRED
- REQUIRES_NEW = Listener·로깅·DLQ 가 chunk 와 분리
- NOT_SUPPORTED = transaction 의미 없는 Tasklet (shell·압축 등)
- Timeout 초 단위 = chunk 한 개 최대 시간
- 권장 — 일반 DB 30~120·외부 API 60~300·대량 batch 300~1800
- 기본 = -1 (무한)
- 너무 짧음 = timeout 폭증, 너무 김 = stuck job 검출 어려움
- DefaultTransactionAttribute Bean = 여러 Step 재사용
- Spring
@Transactional+ chunk transaction = 명시 안 하면 REQUIRED (chunk 참여) - REQUIRES_NEW = chunk rollback 영향 X
- chunk size 와 timeout 관계 = chunk × 평균 처리 시간 × 2~3
- NOT_SUPPORTED 의 ExecutionContext = 영속 안 될 수 있음 (재시작 안전성 약화)
- 함정 — Timeout 누락 = 무한 대기
- 함정 — Isolation 너무 엄격 = deadlock·retry 필요
- 함정 — REQUIRES_NEW 의 connection pool 고갈
- 함정 — NOT_SUPPORTED 의 재시작 안전성
- 함정 —
@Transactional충돌 - 함정 — Multi-DataSource 의 TM 명시
공식 문서: Transaction Attributes 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 11편 — Step 종합 + Chunk-oriented vs TaskletStep
- 12편 — Chunk Step 설정 + Commit Interval 튜닝
- 13편 — Step Restart + 부모 Step 상속
- 14편 — Skip Logic (부분 실패 무시 · SkipPolicy · SkipListener)
- 15편 — Retry Logic (일시적 실패 자동 재시도)
다음 글: