snp-batch-validation/docs/recollection-process.md
HYOJIN e9ef8b9df5 fix(batch): 자동 재수집 파라미터 오버플로우 수정 및 실패 레코드 관리 개선
- retryRecordKeys JobParameter 제거 → DB 직접 조회 (VARCHAR 2500 제한 해결)
- retryCount 세마틱 수정 (0부터 시작, 재수집 실패 시 +1)
- 실패 레코드 Upsert로 중복 방지 (동일 키 1건만 유지)
- 동기 저장으로 RECOLLECT 타이밍 경합 해결
- 재시도 초과 레코드 초기화 API/UI 추가
- 실행 확인 모달 시작/종료일시 항목 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:02:49 +09:00

14 KiB

재수집 프로세스 분석 및 테스트 시나리오

1. 트리거 경로 3가지 비교

항목 자동 재수집 (AUTO_RETRY) 수동 실패건 재수집 (MANUAL_RETRY) 수동 날짜범위 재수집 (MANUAL)
트리거 Job COMPLETED 후 리스너 자동 UI "실패 건 재수집" 버튼 UI "재수집 실행" 버튼
대상 실패한 record key 실패한 record key 날짜 범위 전체
executionMode RECOLLECT RECOLLECT RECOLLECT
executor AUTO_RETRY MANUAL_RETRY MANUAL
sourceStepExecutionId 있음 있음 없음
apiKey 있음 (ExecutionContext) 없음 (resolveApiKey fallback) 있음 (파라미터)
Reader 모드 retry (DB에서 키 조회) retry (DB에서 키 조회) normal (API 전체 호출)
last_success_date Tasklet 스킵 → 불변 Tasklet 스킵 → 불변 Tasklet 실행 → 갱신 → 원복
이력 날짜범위 NULL / NULL NULL / NULL from / to
중복 검사 안함 안함 함 (findOverlappingHistories)

2. 데이터 흐름 다이어그램

자동 재수집 흐름 (AUTO_RETRY)

[정상 배치 실행]
    │
    ├─ Reader.fetchNextBatch() → API 호출 실패 (3회 재시도 소진)
    │   └─ failedImoNumbers에 누적
    │
    ├─ Reader.afterFetch(null) [일반 모드]
    │   ├─ BatchFailedRecordService.saveFailedRecordsSync() [동기 저장]
    │   │   ├─ 기존 FAILED 레코드 존재 → UPDATE (실행정보 갱신, retryCount 유지)
    │   │   └─ 신규 키 → INSERT (retryCount=0, status=FAILED)
    │   └─ finally: ExecutionContext에 failedRecordKeys/stepId/apiKey 저장
    │
    ├─ [Job COMPLETED]
    │
    ├─ AutoRetryJobExecutionListener.afterJob()
    │   ├─ executionMode != "RECOLLECT" ✓
    │   ├─ status == COMPLETED ✓
    │   ├─ 모든 Step의 failedRecordKeys → LinkedHashSet 병합
    │   ├─ findExceededRetryKeys() → retryCount >= 3 인 키 제외
    │   └─ AutoRetryTriggerService.triggerRetryAsync(jobName, failedCount, stepId, apiKey)
    │       └─ JobParameter에 sourceStepExecutionId만 전달 (키 목록은 DB에서 조회)
    │
    ├─ [재수집 Job 시작 (executionMode=RECOLLECT, executor=AUTO_RETRY)]
    │
    ├─ RecollectionJobExecutionListener.beforeJob()
    │   ├─ last_success_date 저장 (ExecutionContext)
    │   └─ recordStart() ──→ [batch_recollection_history] INSERT (STARTED, 날짜 NULL)
    │
    ├─ ShipDetailUpdateJobConfig.shipDetailUpdateDataReader() [@StepScope 빈 초기화]
    │   └─ BatchFailedRecordRepository.findFailedRecordKeysByStepExecutionId() → DB에서 실패 키 조회
    │
    ├─ Reader.beforeFetch() [retry 모드]
    │   └─ allImoNumbers = retryRecordKeys (API 호출 없이 키 목록 직접 사용)
    │
    ├─ Reader.afterFetch(null) [retry 모드]
    │   ├─ resolveSuccessfulRetries() ──→ [batch_failed_record] UPDATE (RESOLVED)
    │   │   └─ sourceStepExecutionId 기준 (아직 원본 stepId 보유, resolve 먼저 실행)
    │   └─ 재실패 건 saveFailedRecords() [비동기]
    │       ├─ 기존 FAILED 레코드 → UPDATE (실행정보 갱신 + retryCount + 1)
    │       └─ 신규 키 → INSERT (retryCount=1)
    │
    ├─ retryModeDecider → executionMode="RECOLLECT" → "RETRY" → lastExecutionUpdateStep 스킵
    │
    ├─ RecollectionJobExecutionListener.afterJob()
    │   ├─ recordCompletion() ──→ [batch_recollection_history] UPDATE (COMPLETED/FAILED)
    │   └─ last_success_date 복원 검사 (DB 현재값 > 원본 → 스킵)
    │       └─ restoreLastSuccessDate() ──→ [batch_last_execution] UPDATE
    │
    └─ [완료]

수동 실패건 재수집 흐름 (MANUAL_RETRY)

[프론트엔드 ExecutionDetail / RecollectDetail]
    │
    ├─ "실패 건 재수집" 버튼 클릭 → 확인 다이얼로그
    │
    ├─ batchApi.retryFailedRecords(jobName, recordKeys, stepExecutionId)
    │   └─ POST /api/batch/jobs/{jobName}/execute
    │       ?sourceStepExecutionId=123
    │       &executionMode=RECOLLECT
    │       &executor=MANUAL_RETRY
    │       &reason=실패 건 수동 재수집 (N건)
    │
    ├─ BatchController.executeJob() → BatchService.executeJob()
    │   └─ jobLauncher.run(job, params)
    │
    ├─ [이후 흐름은 AUTO_RETRY와 동일]
    │   ├─ RecollectionJobExecutionListener.beforeJob() → 이력 INSERT
    │   ├─ Reader 빈 초기화 시 DB에서 실패 키 조회
    │   ├─ Reader [retry 모드] → 키 기반 재처리
    │   ├─ retryModeDecider → executionMode="RECOLLECT" → "RETRY" → Tasklet 스킵
    │   └─ RecollectionJobExecutionListener.afterJob() → 이력 UPDATE + 복원
    │
    └─ 성공 시 UI에서 새 실행 상세 화면으로 이동

수동 날짜범위 재수집 흐름 (MANUAL)

[프론트엔드 Recollects.tsx]
    │
    ├─ updateCollectionPeriod(apiKey, from, to) ──→ [batch_collection_period] UPDATE
    │
    ├─ executeJob(jobName, {executionMode, apiKey, executor, reason})
    │   └─ POST /api/batch/jobs/{jobName}/execute
    │       ?executionMode=RECOLLECT&apiKey=...&executor=MANUAL&reason=...
    │
    ├─ RecollectionJobExecutionListener.beforeJob()
    │   ├─ last_success_date 저장
    │   ├─ periodRepository.findById(apiKey) → 날짜범위 조회
    │   ├─ findOverlappingHistories() → 중복 검사
    │   └─ recordStart() ──→ [batch_recollection_history] INSERT (날짜범위 포함)
    │
    ├─ Reader [일반 모드] → API 전체 호출
    │
    ├─ retryModeDecider → "NORMAL" → lastExecutionUpdateStep 실행
    │   └─ Tasklet ──→ [batch_last_execution] UPDATE (NOW())
    │
    ├─ RecollectionJobExecutionListener.afterJob()
    │   ├─ recordCompletion() ──→ [batch_recollection_history] UPDATE
    │   └─ last_success_date 복원 ──→ [batch_last_execution] UPDATE (원래 값 복원)
    │
    └─ [완료]

3. 테이블 영향 매트릭스

테이블 AUTO_RETRY MANUAL_RETRY MANUAL(날짜) UI 일괄 RESOLVED UI 재시도 초기화
batch_failed_record UPSERT(FAILED) + UPDATE(RESOLVED) UPSERT(FAILED) + UPDATE(RESOLVED) - UPDATE(RESOLVED) UPDATE(retryCount=0)
batch_recollection_history INSERT + UPDATE INSERT + UPDATE INSERT + UPDATE - -
batch_collection_period READ (apiKeyName 조회) READ (apiKeyName 조회) READ/WRITE (날짜범위) - -
batch_last_execution READ (복원용) READ (복원용) READ → WRITE(갱신) → WRITE(복원) - -
batch_job_execution INSERT (Spring Batch 자동) INSERT (Spring Batch 자동) INSERT (Spring Batch 자동) - -

4. 실패 레코드 관리

retryCount 세마틱

retryCount는 해당 레코드가 재수집된 횟수를 나타냅니다.

시점 retryCount 의미
최초 실패 0 아직 재시도하지 않음
1차 재수집 실패 1 1번 재시도했으나 재실패
2차 재수집 실패 2 2번 재시도했으나 재실패
3차 재수집 실패 3 MAX_AUTO_RETRY_COUNT 도달 → 자동 재수집 제외

중복 방지 (Upsert 패턴)

동일 (jobName, recordKey)에 대해 FAILED 레코드는 항상 1건만 유지됩니다.

[일반 모드 저장 시]
  ├─ 기존 FAILED 레코드 없음 → INSERT (retryCount=0)
  └─ 기존 FAILED 레코드 있음 → UPDATE (jobExecutionId, stepExecutionId, errorMessage 갱신, retryCount 유지)

[재수집 모드 저장 시 (재실패)]
  ├─ 기존 FAILED 레코드 없음 → INSERT (retryCount=1)
  └─ 기존 FAILED 레코드 있음 → UPDATE (jobExecutionId, stepExecutionId, errorMessage 갱신, retryCount + 1)

중복 방지 효과:

  • 배치 실행 1 → IMO 1234 실패 → INSERT (stepExecId=100, retryCount=0)
  • 배치 실행 2 → IMO 1234 또 실패 → UPDATE (stepExecId=200, retryCount=0 유지)
  • 재수집 트리거 → stepExecId=200 기준 조회 → 1건만 재수집 (중복 재수집 방지)

동기/비동기 저장 구분

호출 시점 메서드 이유
일반 모드 afterFetch saveFailedRecordsSync (동기) RECOLLECT Job의 Reader가 DB 조회 시 커밋 완료 보장
재수집 모드 afterFetch saveFailedRecords (비동기) 후속 RECOLLECT 없으므로 비동기 OK

재시도 초과 레코드 관리

retryCount >= MAX_AUTO_RETRY_COUNT(3)에 도달한 레코드는 자동 재수집에서 제외됩니다. 사용자는 UI에서 다음 3가지 방식으로 처리할 수 있습니다.

액션 설명 API 효과
재시도 초기화 retryCount를 0으로 리셋 POST /api/batch/failed-records/reset-retry 다음 배치 실행 시 자동 재수집 대상에 다시 포함
수동 재수집 즉시 RECOLLECT Job 트리거 POST /api/batch/jobs/{jobName}/execute 선택한 키에 대해 재수집 즉시 실행
일괄 RESOLVED FAILED → RESOLVED 상태 변경 POST /api/batch/failed-records/resolve 해당 레코드 포기 (더 이상 재수집 안 함)
[재시도 초과 레코드 처리 흐름]

  retryCount >= 3 도달 → 자동 재수집 제외
    │
    ├─ [재시도 초기화] → retryCount = 0 → 다음 배치 실행에서 자동 재수집 재개
    │
    ├─ [수동 재수집] → RECOLLECT Job 즉시 실행 → 성공 시 RESOLVED, 실패 시 retryCount + 1
    │
    └─ [일괄 RESOLVED] → status = RESOLVED → 재수집 대상에서 영구 제외

UI 재시도 상태 표시

FailedRecordsToggle 테이블의 "재시도" 컬럼에 retryCount 기반 상태 배지를 표시합니다.

retryCount 배지 색상 의미
0 대기 blue 아직 재시도하지 않음, 자동 재수집 대상
1~2 재시도 N/3 amber 재수집 진행 중
>= 3 재시도 초과 red 자동 재수집 제외, 사용자 조치 필요

"재시도 초기화" 버튼은 재시도 초과 상태 레코드가 1건 이상 있을 때만 표시됩니다.


5. 무한루프 방지 구조

[삼중 가드]

Guard 1: AutoRetryJobExecutionListener
  └─ executionMode == "RECOLLECT" → 즉시 리턴 (재수집 Job에서는 자동 재수집 트리거 안 함)

Guard 2: findExceededRetryKeys()
  └─ retryCount >= 3 인 키 → mergedKeys에서 제거
  └─ 남은 키 없으면 → 재수집 자체 스킵

Guard 3: Reader afterFetch [retry 모드]
  └─ ExecutionContext에 failedRecordKeys 미기록 → 리스너가 감지하지 못함

[시나리오]
  일반 실행 → 실패 3건 → batch_failed_record INSERT (retryCount=0)
    → 자동 재수집 1차 → 재실패 → retryCount=1
    → 자동 재수집 2차 → 재실패 → retryCount=2
    → 자동 재수집 3차 → 재실패 → retryCount=3
    → 다음 정상 실행에서 같은 키 다시 실패해도 retryCount >= 3 → Guard 2에서 차단

6. 프론트엔드 UI 진입점

화면 컴포넌트/영역 기능
Executions failedRecordCount 뱃지 COMPLETED 상태 + 미해결 실패건 > 0 → amber 뱃지
ExecutionDetail FailedRecordsToggle 실패 레코드 목록 + 재시도 상태 배지 + "재시도 초기화" / "재수집 실행" / "일괄 RESOLVED" 버튼
RecollectDetail FailedRecordsToggle 재수집 상세 내 실패 레코드 + 동일 버튼
Recollects 이력 목록 + 수집기간 관리 검색/필터 + CSV 내보내기 + 재수집 실행 + 기간 편집
Dashboard 재수집 현황 위젯 통계 카드 5개 + 최근 5건 테이블

7. 주요 파일 맵

백엔드

파일 역할
ShipDetailUpdateDataReader.java API 호출 + 실패 감지 + ExecutionContext 저장
ShipDetailUpdateJobConfig.java Job/Step 구성 + retryModeDecider(executionMode 기반) + DB에서 실패 키 조회
AutoRetryJobExecutionListener.java Job 완료 후 실패 키 병합 + retryCount 필터 + 트리거
AutoRetryTriggerService.java @Async 비동기 Job 재실행 (sourceStepExecutionId만 전달)
RecollectionJobExecutionListener.java 재수집 이력 기록 + last_success_date 백업/복원
RecollectionHistoryService.java 이력 CRUD + 통계 + 수집기간 관리
BatchFailedRecordService.java 실패 레코드 Upsert(동기/비동기) + RESOLVED 처리
BatchFailedRecordRepository.java 실패 키 조회 + 벌크 UPDATE(retryCount 유지/증가/초기화) + RESOLVED
BatchService.java Job 실행 + failedRecordCount 조회
BatchController.java REST API 엔드포인트 (실행/이력/CSV/RESOLVED/재시도 초기화)

프론트엔드

파일 역할
batchApi.ts API 클라이언트 (retryFailedRecords, resolveFailedRecords, resetRetryCount, exportCSV 등)
Executions.tsx 실행 목록 + failedRecordCount amber 뱃지
ExecutionDetail.tsx 실행 상세 + FailedRecordsToggle (재수집/RESOLVED)
RecollectDetail.tsx 재수집 상세 + FailedRecordsToggle (재수집/RESOLVED)
Recollects.tsx 재수집 이력 목록 + 수집기간 관리 + CSV 내보내기
Dashboard.tsx 대시보드 재수집 현황 위젯

공유 컴포넌트

파일 사용처
components/CopyButton.tsx ApiLogSection 내부
components/DetailStatCard.tsx ExecutionDetail, RecollectDetail
components/ApiLogSection.tsx ExecutionDetail, RecollectDetail
components/InfoItem.tsx ExecutionDetail, RecollectDetail