공공부하자개발 · 영어 학습 노트
자바
실무 배치대용량 데이터 처리0/6 완료
  • 01배치 프로세스 개념과 아키텍처
  • 02대용량 파일 I/O (NIO, Buffered)
  • 03Chunk 단위 처리와 OOM 방지
  • 04트랜잭션: 커밋과 롤백 시뮬레이션
  • 05Skip과 Retry 로직 구현
  • 06메서드 활용 패턴 (배치 유틸)
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 배치 › 04 / 6

트랜잭션: 커밋과 롤백 시뮬레이션

섹션 7진행 0 / 6
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 ACID 를 배치 관점으로

성질 의미 배치에서의 질문
Atomicity (원자성) 전부 반영되거나 전부 취소 "원자성의 단위"를 무엇으로 할 것인가 — 전체? 건별? 청크?
Consistency (일관성) 제약 조건이 항상 만족 청크 중간 상태(A 는 출금됐고 B 는 입금 전)가 커밋되지 않도록
Isolation (격리성) 다른 트랜잭션에 중간 상태 노출 안 됨 배치가 도는 동안 온라인 조회가 반쯤 처리된 데이터를 보지 않도록. 긴 트랜잭션은 락으로 온라인을 막음
Durability (지속성) 커밋되면 장애에도 남음 커밋 = 재시작 시 다시 할 필요 없는 지점

배치 설계의 핵심은 첫 줄입니다. 원자성의 단위를 청크로 정한다.

2.2 왜 건별도 전체도 아닌 청크 단위 커밋인가

text
전체 1 트랜잭션:  [───────────── 100만 건 ─────────────] commit
                  실패 시 전부 롤백. undo 로그 수 GB. 락 수 시간. 재시작 = 처음부터

건별 커밋:        [1]c[2]c[3]c ... [1,000,000]c
                  커밋 100만 번 = fsync 100만 번 = 매우 느림 (커밋당 1~10ms → 수 시간)

청크 커밋(1000):  [────1000────]c[────1000────]c ... [────1000────]c
                  커밋 1000번. 실패 시 최대 1000건 롤백. 재시작 = 마지막 커밋 청크 다음부터
기준 전체 건별 청크
커밋 횟수 1 N N / chunk
롤백 범위 전체 1건 청크
락 유지 시간 배치 전체 극히 짧음 청크 처리 시간
재시작 지점 처음 실패 건 마지막 커밋 청크
성능 커밋 비용은 최소지만 undo/락으로 오히려 느려짐 커밋 비용이 지배 균형

커밋이 비싼 이유는 디스크 동기화(fsync) 때문입니다. DB 는 커밋 시 로그를 디스크에 확실히 쓴 뒤에야 성공을 반환합니다(Durability). SSD 라도 fsync 는 ms 단위이고, 100만 번이면 그것만으로 수십 분입니다. 청크로 묶으면 그 비용이 1/1000 이 됩니다.

2.3 커밋 포인트와 재시작 지점

text
청크 1 ─commit─▶ 청크 2 ─commit─▶ 청크 3 ─commit─▶ 청크 4 ─(장애)
                                        ▲
                                        └── 마지막 커밋 포인트 = 재시작 지점
재시작:                                  청크 4 부터 다시 읽기 시작

커밋된 것은 지속되고, 커밋되지 않은 것은 사라진다(DB 가 보장). 그래서 재시작 지점은 항상 "마지막 커밋 청크의 끝"입니다. 청크 4 의 일부가 처리되던 중 죽었다면, DB 는 그 미커밋 변경을 자동으로 버립니다(커넥션이 끊기면 롤백). 문제는 "청크 3 까지 커밋됐다"는 사실을 배치 프로그램이 어떻게 아는가입니다. 그것을 기록하는 것이 체크포인트입니다.

2.4 체크포인트 — 실행 상태 저장

저장 위치 장점 단점
DB 테이블 (같은 DB) 데이터와 같은 트랜잭션으로 커밋 → 불일치 불가능 배치 메타 테이블 필요
파일 DB 없는 배치에서 간단 데이터 커밋과 체크포인트 저장 사이에 죽으면 불일치(아래)
다른 DB / Redis 여러 배치 공유 마찬가지로 불일치 창(window) 존재

체크포인트에 무엇을 기록하는가:

  • 파일 입력: 마지막 커밋 청크의 마지막 줄 번호. 재시작 시 그만큼 skip.
  • DB 키셋 입력: 마지막 커밋 청크의 마지막 키(id). 재시작 시 WHERE id > :lastId.
  • 청크 번호, Job 이름, 실행 ID: 로그 대조용.

체크포인트를 데이터와 다른 저장소에 기록하면 두 저장이 원자적이지 않습니다.

text
db.commit()   ← 성공
(여기서 죽음)
checkpoint.save(lineNo=400)   ← 실행 안 됨 → 체크포인트는 300 에 머묾
재시작: 301~400 을 다시 처리 → 이중 처리!

이 창(window)은 아주 짧지만 0 이 아닙니다. 해결책은 두 가지입니다. 체크포인트를 데이터와 같은 DB, 같은 트랜잭션에 기록하거나(이상적), 멱등성으로 이중 처리를 무해하게 만듭니다(현실적, 어차피 필요).

2.5 멱등성 — 두 번 실행해도 결과가 같게

멱등(idempotent)이란 "같은 연산을 여러 번 적용해도 결과가 한 번과 같다"는 뜻입니다. 재시작 배치는 반드시 일부 데이터를 다시 만나므로 멱등성 없이는 재시작이 불가능합니다.

기법 방법 예
처리 완료 마킹 건마다 "처리됨" 표시를 같은 트랜잭션에 기록. 재실행 시 표시가 있으면 건너뜀 done:{txId} 키, orders.processed_at 컬럼
Upsert INSERT 대신 "있으면 UPDATE, 없으면 INSERT" MERGE, INSERT ... ON CONFLICT UPDATE
절대값 갱신 balance = 900 처럼 절대값 대입은 재실행에 안전. 원본 상태는 알아야 함 등급 재산정 grade = calc(purchase)
멱등 키 외부 API 호출에 유일 키를 붙여 서버가 중복을 거부 결제 API 의 Idempotency-Key 헤더
출력 덮어쓰기 결과 파일을 append 가 아니라 통째로 다시 생성 정산 파일 재생성

핵심 규칙: 완료 마킹은 데이터 변경과 같은 트랜잭션에서 커밋되어야 합니다. 마킹만 커밋되고 데이터가 롤백되면(또는 반대) 영원히 불일치입니다. 이 레슨의 TransferBatch 는 잔액 변경과 done:{txId} 를 같은 begin~commit 안에서 씁니다.

2.6 순수 자바 트랜잭션 시뮬레이션 — 어떻게 동작하는가

text
             committed (확정)          pending (트랜잭션 버퍼)
begin()      { A=100 }                 {}            ← 새 버퍼
put(A, 50)   { A=100 }                 { A=50 }      ← 버퍼에만
get(A)       → pending 에 있으면 50   (자기 변경은 보인다 = read-your-writes)
rollback()   { A=100 }                 null          ← 버퍼 폐기
begin()      { A=100 }                 {}
put(A, 50)   { A=100 }                 { A=50 }
commit()     { A=50 }                  null          ← 버퍼를 확정에 반영
시뮬레이션 실제 JDBC 실제 DB 내부
begin() = 새 pending 맵 conn.setAutoCommit(false) 트랜잭션 ID 발급
put() = pending 에 기록 stmt.executeUpdate() 데이터 페이지 수정 + undo 로그 기록
get() = pending 우선 조회 같은 커넥션의 SELECT 자기 트랜잭션 변경은 보임
commit() = pending → committed conn.commit() redo 로그 fsync, 락 해제
rollback() = pending 폐기 conn.rollback() undo 로그로 원복, 락 해제
커넥션 끊김 미커밋 자동 롤백 동일

실제 DB 는 "버퍼에 뒀다가 반영"이 아니라 "바로 수정하고 undo 로 되돌리기"지만, 커밋 전 변경은 격리되어 있고 취소 가능하다는 계약은 같습니다. 이 계약만 이해하면 됩니다.

2.7 청크 중간 실패 시 무엇이 롤백되는가

text
청크 4 (건 301~400):
  begin
  301 apply ✓   302 apply ✓   ...   349 apply ✓
  350 apply ✗  InsufficientBalance!
  rollback  → 301~349 의 변경 49건이 전부 취소. 청크 1~3 (1~300) 은 커밋됐으므로 유지

로그에 이것이 명확히 보여야 합니다.

text
청크 1 커밋   (줄 100까지, 처리 100건)
청크 2 커밋   (줄 200까지, 처리 100건)
청크 3 커밋   (줄 300까지, 처리 100건)
청크 4 롤백   (TX-000350: ACC-04 잔액 1009000 < 이체 999999999) -> 이 청크의 49건 변경 전부 취소, 이전 청크는 유지
청크 5 커밋   ...

롤백된 청크를 어떻게 할지는 정책입니다. 이 레슨에서는 "기록하고 다음 청크로" 넘어갑니다(체크포인트 전진). 05 레슨에서 "실패 건만 빼고 나머지를 다시 커밋"하는 스킵 처리를 다룹니다. Spring Batch 는 청크 실패 시 건별로 다시 처리하며 실패 건만 스킵하는 방식을 씁니다.

2.8 보상 트랜잭션 — 되돌릴 수 없는 것을 되돌리기

DB 밖의 작업(메일 발송, 외부 결제 API, 타 시스템 파일 전송)은 rollback 이 없습니다. 이미 보낸 메일은 취소할 수 없습니다. 이럴 때는 반대 작업을 하는 별도 트랜잭션으로 논리적으로 되돌립니다.

원 작업 보상 작업
결제 승인 결제 취소 API
포인트 적립 포인트 차감
재고 차감 재고 복구
메일 발송 정정 메일 (완전 취소 불가)

보상은 완벽하지 않으므로, 순서를 잘 정합니다. 되돌릴 수 있는 것을 먼저, 되돌릴 수 없는 것을 마지막에. DB 변경(롤백 가능)을 먼저 하고 커밋 직전에 외부 API 를 호출하면, API 가 실패했을 때 DB 는 롤백하면 됩니다. API 성공 후 커밋이 실패하는 창은 남으며, 그때 보상 또는 멱등 키가 필요합니다.

2.9 파일 출력의 트랜잭션

파일에는 rollback 이 없지만 흉내 낼 수 있습니다.

목표 방법
전체 파일의 원자성 임시 파일에 쓰고 완료 후 ATOMIC_MOVE (02 레슨). 죽으면 .tmp 만 남음
청크 단위 커밋 청크마다 flush() + 쓴 줄 수를 체크포인트에 기록. 재시작 시 그 줄까지 truncate 후 append
재시작 시 단순화 결과 파일은 재생성이 싸다면 통째로 다시 만든다. 정산 파일은 대부분 이쪽

RandomAccessFile.setLength(n) 또는 FileChannel.truncate(n) 으로 마지막 커밋 위치까지 잘라내면, 죽기 직전 반쯤 쓰인 청크가 제거됩니다. 이 레슨의 예제는 가장 단순한 "임시 파일 + 원자적 이동 + 재생성" 방식을 씁니다.

2.10 이중 실행 방지

어제 배치가 안 끝났는데 오늘 스케줄이 또 시작하면 같은 데이터를 두 프로세스가 처리합니다. 방지책:

  • 메타데이터 테이블에 Job 상태 STARTED 가 있으면 시작 거부 (Spring Batch 기본 동작)
  • 파일 락: FileChannel.tryLock() 으로 락 파일 획득 실패 시 종료
  • DB 락: SELECT ... FOR UPDATE NOWAIT 로 Job 행 잠금
  • OS 수준: flock, 스케줄러의 "이전 인스턴스가 실행 중이면 시작 안 함" 옵션
핵심 원리
  • 2.1 ACID 를 배치 관점으로
  • 2.2 왜 건별도 전체도 아닌 청크 단위 커밋인가
  • 2.3 커밋 포인트와 재시작 지점
  • 2.4 체크포인트 — 실행 상태 저장
  • 2.5 멱등성 — 두 번 실행해도 결과가 같게
  • 2.6 순수 자바 트랜잭션 시뮬레이션 — 어떻게 동작하는가
  • 2.7 청크 중간 실패 시 무엇이 롤백되는가
  • 2.8 보상 트랜잭션 — 되돌릴 수 없는 것을 되돌리기
  • 2.9 파일 출력의 트랜잭션
  • 2.10 이중 실행 방지
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제