snp-batch-validation/docs/recollection-process.md
HYOJIN 2bc2f1fc32 feat(recollection): 자동 재수집 및 재수집 프로세스 전면 개선 (#30)
- 자동 재수집 리스너(AutoRetryJobExecutionListener) 및 비동기 트리거 서비스 추가
- 실패 레코드 최대 재시도 횟수(3회) 제한으로 무한 루프 방지
- 전용 스레드 풀(autoRetryExecutor) 분리
- last_success_date 복원 시 경합 조건 보호
- 재수집 이력 N+1 쿼리 해결 (벌크 조회)
- 실패 레코드 일괄 RESOLVED 처리 API 추가
- 재수집 이력 CSV 내보내기 API 추가 (UTF-8 BOM)
- 프론트엔드 공유 컴포넌트 추출 (StatCard, CopyButton, ApiLogSection, InfoItem)
- 대시보드 재수집 통계 위젯 추가
- 실행 이력 미해결 건수 COMPLETED 상태만 표시

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 17:28:23 +09:00

9.3 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
retryRecordKeys 있음 (콤마 구분) 있음 (콤마 구분) 없음
sourceStepExecutionId 있음 있음 없음
apiKey 있음 (ExecutionContext) 없음 (resolveApiKey fallback) 있음 (파라미터)
Reader 모드 retry (키 기반) retry (키 기반) 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.saveFailedRecords() ──→ [batch_failed_record] INSERT (FAILED)
    │   └─ finally: ExecutionContext에 failedRecordKeys/stepId/apiKey 저장
    │
    ├─ [Job COMPLETED]
    │
    ├─ AutoRetryJobExecutionListener.afterJob()
    │   ├─ executionMode != "RECOLLECT" ✓
    │   ├─ status == COMPLETED ✓
    │   ├─ 모든 Step의 failedRecordKeys → LinkedHashSet 병합
    │   ├─ findExceededRetryKeys() → retryCount >= 3 인 키 제외
    │   └─ AutoRetryTriggerService.triggerRetryAsync() [autoRetryExecutor 스레드]
    │
    ├─ [재수집 Job 시작 (executionMode=RECOLLECT, executor=AUTO_RETRY)]
    │
    ├─ RecollectionJobExecutionListener.beforeJob()
    │   ├─ last_success_date 저장 (ExecutionContext)
    │   └─ recordStart() ──→ [batch_recollection_history] INSERT (STARTED, 날짜 NULL)
    │
    ├─ Reader.beforeFetch() [retry 모드]
    │   └─ allImoNumbers = retryRecordKeys (API 호출 없이 키 목록 직접 사용)
    │
    ├─ Reader.afterFetch(null) [retry 모드]
    │   ├─ resolveSuccessfulRetries() ──→ [batch_failed_record] UPDATE (RESOLVED)
    │   └─ 재실패 건 saveFailedRecords() ──→ [batch_failed_record] INSERT (FAILED, 새 레코드)
    │
    ├─ retryModeDecider → "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
    │       ?retryRecordKeys=IMO1,IMO2
    │       &sourceStepExecutionId=123
    │       &executionMode=RECOLLECT
    │       &executor=MANUAL_RETRY
    │       &reason=실패 건 수동 재수집 (N건)
    │
    ├─ BatchController.executeJob() → BatchService.executeJob()
    │   └─ jobLauncher.run(job, params)
    │
    ├─ [이후 흐름은 AUTO_RETRY와 동일]
    │   ├─ RecollectionJobExecutionListener.beforeJob() → 이력 INSERT
    │   ├─ Reader [retry 모드] → 키 기반 재처리
    │   ├─ retryModeDecider → "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
batch_failed_record INSERT(FAILED) + UPDATE(RESOLVED) INSERT(FAILED) + UPDATE(RESOLVED) - UPDATE(RESOLVED)
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. 무한루프 방지 구조

[이중 가드]

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

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

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

5. 프론트엔드 UI 진입점

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

6. 주요 파일 맵

백엔드

파일 역할
ShipDetailUpdateDataReader.java API 호출 + 실패 감지 + ExecutionContext 저장
ShipDetailUpdateJobConfig.java Job/Step 구성 + retryModeDecider + 리스너 등록
AutoRetryJobExecutionListener.java Job 완료 후 실패 키 병합 + retryCount 필터 + 트리거
AutoRetryTriggerService.java @Async 비동기 Job 재실행 (autoRetryExecutor)
RecollectionJobExecutionListener.java 재수집 이력 기록 + last_success_date 백업/복원
RecollectionHistoryService.java 이력 CRUD + 통계 + 수집기간 관리
BatchFailedRecordService.java 실패 레코드 저장/RESOLVED 처리
BatchService.java Job 실행 + failedRecordCount 조회
BatchController.java REST API 엔드포인트 (실행/이력/CSV/RESOLVED)

프론트엔드

파일 역할
batchApi.ts API 클라이언트 (retryFailedRecords, resolveFailedRecords, 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