- 자동 재수집 리스너(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>
206 lines
9.3 KiB
Markdown
206 lines
9.3 KiB
Markdown
# 재수집 프로세스 분석 및 테스트 시나리오
|
|
|
|
## 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 |
|
|
|
|
---
|