Spring Batch 입문 16편 — Transaction Attributes (Isolation · Propagation · Timeout)

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

Spring Batch 입문 16편. chunk transaction 의 세부 속성 — Isolation level·Propagation·Timeout 의 의미와 운영 환경 권장 설정, ItemReader/Writer 측 Spring @Transactional 과의 관계, 자주 만나는 함정까지 풀어쓴 학습 노트.

📚 Spring Batch 입문에서 운영까지 · 16편 — Transaction Attributes (Isolation · Propagation · Timeout)

이 글은 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_COMMITTEDDEFAULT 로 두면 성능과 일관성이 균형을 이룹니다.

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 에서 원문을 확인할 수 있어요.

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

이전 글:

다음 글:

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

답글 남기기

error: Content is protected !!