청크 20 커밋 | 누적 읽음 20,000 처리 19,735 스킵 265 재시도 1,018
청크 40 롤백: 전원 차단 시뮬레이션 (작업 중이던 500행 폐기, 체크포인트는 청크 39 유지)
잡 비정상 종료: 전원 차단 시뮬레이션
체크포인트 상태: 청크 39, 행 39,001, 처리 38,495, 스킵 505processChunk는 청크 40의 501번째 행(i == 500)에서 SimulatedCrash를 던진다. run의 catch (RuntimeException e)가 롤백 메시지를 찍고 다시 던지며, Main이 잡는다. 체크포인트를 다시 읽으면 청크 39, 행 39,001이다 — 청크 40의 500행은 어디에도 없다. 38,495 + 505 = 39,000 = 헤더를 뺀 39,000행이 정확히 39개 청크다.
재시도 1,018은 20,000행 중 게이트웨이를 호출한 약 19,760건의 5%인 ~988 + 2차 실패 ~49 ≈ 1,037 근처다. 해시 기반이므로 정확히 5%는 아니지만 통계적으로 맞는다.
체크포인트 발견: 청크 39개 커밋됨, 39002행부터 재개
청크 40 커밋 | 누적 읽음 40,000 처리 39,486 스킵 514 재시도 2,025ChunkReader가 39,001행까지 읽고 버린 뒤 39,002행부터 청크를 만든다. 청크 40 커밋 시점의 누적이 40,000이다 — 1차 실행에서 폐기된 500행이 한 번만 반영됐다. 만약 롤백 없이 committed에 직접 집계했다면 이 시점 누적이 40,500이 됐을 것이다.
재시작 결과 == 단일 실행 결과: true4절은 체크포인트 없이 100청크를 처음부터 돌린 straight와 result(39 + 61 청크)를 비교한다. 처리·스킵·매출·수수료·사유별 스킵이 전부 같다. 이것이 R23의 증명이다. 게이트웨이의 실패가 해시 기반이라 두 실행에서 같은 주문이 같은 시도에서 실패했기 때문에 가능하다 — Random이었다면 재시도 횟수와 RETRY_EXHAUSTED가 달라져 same이 false였을 것이다.
단, 한 가지는 같지 않다. 1차 실행에서 청크 40의 500행 중 정상 주문은 이미 게이트웨이를 호출했고, 2차 실행에서 다시 호출했다. 실제 게이트웨이라면 이중 결제다. 이 시뮬레이션은 게이트웨이가 상태를 갖지 않아 드러나지 않을 뿐이다. 해법은 멱등키(주문 ID)로 게이트웨이가 중복 요청을 거르는 것이고, 확장 과제 2에서 다룬다. 배치 레슨 04의 "재시작 가능한 잡은 멱등해야 한다"가 여기서 구체화된다.
| 사유 | 건수 | 생성기 비율 | 기대 건수 | 비고 |
|---|---|---|---|---|
QUANTITY |
571 | 0.4% (0) + 0.2% (음수) | ≈ 600 | 수량 0과 음수가 같은 사유로 합쳐짐 |
NUMBER_FORMAT |
305 | 0.3% | ≈ 300 | 단가 N/A |
FIELD_COUNT |
194 | 0.2% | ≈ 200 | 결제수단 컬럼 누락 → 7컬럼 |
PAY_TYPE |
102 | 0.1% | ≈ 100 | BITCOIN |
DECLINED |
95 | 해시 1/1000 | ≈ 99 | 정상 주문 98,8xx건의 0.1% |
RETRY_EXHAUSTED |
10 | 0.05³ | ≈ 12 | 3번 연속 일시 장애 |
합계 1,277 = 리포트의 스킵 수. 검증 스킵 4종(1,172건)은 게이트웨이를 호출하지 않았고, 결제 스킵 2종(105건)은 호출했다. 파싱 → 결제 순서 덕분에 깨진 행으로 게이트웨이를 부르는 낭비가 없다.
BANK 32,947건 매출 2,697,682,000 수수료 26,976,820 정산 2,670,705,180
CARD 49,207건 매출 4,057,434,500 수수료 101,434,212 정산 3,956,000,288
POINT 16,569건 매출 1,373,307,500 수수료 0 정산 1,373,307,500BANK: 2,697,682,000 × 1.0% = 26,976,820 — 정확히 일치한다(모든 금액이 100의 배수라 절사 없음). CARD: 4,057,434,500 × 2.5% = 101,435,862.5인데 실제는 101,434,212로 1,650원 적다.
OrderRecord.fee()가 건별로 amount × 250 / 10000을 정수 나눗셈하기 때문이다(예: 2,500원 노트 1개 → 6.25 → 6). 정산에서 "합계에 요율을 곱한 값"과 "건별 수수료의 합"은 다르며, 어느 쪽이 맞는지는 계약이 정한다. 리포트는 건별 합을 쓴다.
카테고리 합계 = 결제수단 합계 = 8,128,424,000. 같은 주문을 두 축으로 집계했으니 당연히 같아야 하고, 다르면 버그다. 이런 교차 검증은 정산 리포트에 반드시 넣는다.
게이트웨이 호출 수 (재개 실행): 63387. 2차 실행은 61,000행을 읽었고, 그중 검증 통과 약 60,270건 + 재시도 약 3,030회 + 거절 ~60건 ≈ 63,4xx. 재시도가 호출 수를 5% 늘렸다.
소요 시간의 대부분은 청크마다 체크포인트 파일을 쓰는 디스크 I/O(100회 쓰기 + 100회 원자적 이동)다. 파싱과 집계는 10만 행에 수백 밀리초면 끝난다. 청크 크기를 키우면 체크포인트 횟수가 줄어 빨라지지만, 장애 시 폐기되는 행이 늘어난다 — 확장 과제 3의 주제다.