Skip to content

Commit 898201b

Browse files
authored
[Feat] 최종 실패 인덱싱 Job 수동 재처리 지원 추가 구현
[Feat] 최종 실패 인덱싱 Job 수동 재처리 지원 추가 구현
2 parents e1bd2d2 + 74d4b09 commit 898201b

14 files changed

Lines changed: 1959 additions & 7 deletions

File tree

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
# Issue #108 최종 실패 인덱싱 Job 수동 재처리 상세 설계
2+
3+
closes #108
4+
5+
## 1. 문서 목적
6+
7+
이 문서는 이슈 [#108](https://github.com/DocGrid/backend/issues/108)의 구현 기준을 정의한다.
8+
9+
인덱싱 Job은 실패 유형이 재시도 가능하고 남은 횟수가 있을 때만 `PENDING` Queue로 재예약된다. 재시도
10+
가능 횟수를 모두 소진했거나 재시도 불가 유형으로 종료된 Job은 `FAILED`로 종결되고, 대상 Document
11+
Version은 `FAILED`, 그 Version의 Embedding Set은 `STALE`이 되어 검색에서 제외된다.
12+
13+
`FAILED` Job을 다시 처리할 경로는 현재 존재하지 않는다. Claim은 `PENDING`만, Lease 만료 복구는
14+
`PROCESSING`만 후보로 삼기 때문에 어떤 자동 경로도 `FAILED` Job을 되살리지 않는다. 외부 Embedding
15+
서버 장애나 일시적인 Storage 장애처럼 원인이 이미 해소된 뒤에도 같은 문서를 다시 인덱싱하려면 새
16+
Version을 업로드하는 방법밖에 없다.
17+
18+
이 작업은 최종 실패로 종결된 Job만 관리자가 명시적으로 Queue에 되돌릴 수 있는 수동 재처리 경로를
19+
추가한다.
20+
21+
### 1.1 성공 기준
22+
23+
- 최종 `FAILED` Job만 수동 재처리 대상이 된다.
24+
- 처리 중이거나 자동 재시도가 예정된 Job은 명시적으로 거부한다.
25+
- 최신 처리 대상 Version이 아니면 거부한다.
26+
- 현재 검색 가능한 이전 Version과 `current_version` 포인터를 보존한다.
27+
- 이전 Worker, Claim Token, Lease 등 소유권 정보를 초기화한다.
28+
- Retry Count와 기존 Attempt 이력을 삭제하지 않는다.
29+
- 수동 재처리를 새 상태 전이와 감사 Event로 남긴다.
30+
- 기존 Chunk를 무조건 삭제하지 않는다.
31+
- 중복 요청과 동시 요청이 하나의 상태 전이로 수렴한다.
32+
- Lease 만료 복구 및 자동 재시도와 경합하지 않는다.
33+
- 관리자 권한을 요구하고 Claim Token 등 민감 정보를 응답에 노출하지 않는다.
34+
35+
## 2. 범위
36+
37+
### 2.1 포함
38+
39+
- 최종 실패 Job 한 건을 다시 Queue에 넣는 관리자 API
40+
- Job, Document Version, Document의 재처리 상태 전이
41+
- 재개 지점 결정과 대상 Version Embedding 정리
42+
- 수동 재처리 감사 Event 기록
43+
44+
### 2.2 제외
45+
46+
- 재처리 대상 Job 목록·상세 조회 API (관리자 인덱싱 조회 작업에서 진행)
47+
- 자동 재시도 정책과 Backoff 계산 변경
48+
- 여러 Job을 한 번에 재처리하는 Batch API
49+
- 재처리 예약, 스케줄링, 자동 트리거
50+
- OCR 등 실패 원인 자체를 해결하는 파싱 기능
51+
52+
## 3. 현재 구조 분석
53+
54+
### 3.1 상태 모델
55+
56+
`EmbeddingJobStatus``PENDING`, `PROCESSING`, `INDEXED`, `FAILED`, `CANCELED`로 구성된다. 별도의
57+
재시도 예약 상태는 없고, 자동 재시도가 예정된 Job은 `PENDING` + 미래의 `next_retry_at`으로 표현된다.
58+
Claim Query는 `next_retry_at IS NULL OR next_retry_at <= :claimedAt` 조건을 사용하므로 예약 시각 전에는
59+
후보가 되지 않는다.
60+
61+
### 3.2 최종 실패 시점의 데이터 상태
62+
63+
`IndexingFailureTransitionService`의 최종 실패 경로는 하나의 Transaction에서 다음을 수행한다.
64+
65+
1. 대상 Version의 `ACTIVE` Embedding을 모두 `STALE`로 전환
66+
2. `document_versions.status``FAILED`로 전환
67+
3. 이전 `INDEXED` Version이 현재 검색 대상이면 Document를 그대로 두고, 아니면 `FAILED`로 전환
68+
4. `embedding_jobs.status``FAILED`로 전환하고 `failed_at`, 오류 Snapshot 기록
69+
5. 단계 실패 Event와 `FAILED` Event를 같은 시각으로 append
70+
71+
`markFailed``locked_by_worker_id`, `claim_token`, `locked_at`, `lock_expires_at`을 감사 목적으로
72+
남긴다. `document_chunks`는 삭제하지 않는다.
73+
74+
### 3.3 재개 지점 계약
75+
76+
파이프라인 각 단계는 Version 상태로 재개 지점을 판단한다.
77+
78+
| Version 상태 | 동작 |
79+
|---|---|
80+
| `UPLOADED`, `PARSING` | 원본을 다시 읽어 파싱하고 Chunk Set 저장 |
81+
| `CHUNKED` | 파싱을 생략하고 Embedding 생성 |
82+
| `EMBEDDING` | 저장된 Embedding 수에 따라 재생 또는 재작업 |
83+
84+
Chunk Set 저장과 `CHUNKED` 전이는 같은 Transaction에서 일어나므로 Chunk가 존재하면 항상 완전한
85+
Set이다. 반면 `CHUNKED` 상태에서 대상 Version·Model의 Embedding 행이 0이 아니면
86+
`DOCUMENT_EMBEDDINGS_INCONSISTENT`로 차단된다. 따라서 최종 실패가 남긴 `STALE` Embedding을 정리하지
87+
않으면 재처리 자체가 불가능하다.
88+
89+
### 3.4 검색 보호 장치
90+
91+
Vector 검색 Query는 `e.status = 'ACTIVE' AND d.status = 'INDEXED' AND d.current_version_id =
92+
e.document_version_id` 조건을 사용한다. 실패한 Version의 Embedding은 `STALE`이므로 원래 검색에 노출될
93+
수 없고, 이전 `INDEXED` Version은 `current_version_id`가 유지되는 한 계속 검색된다.
94+
95+
## 4. 설계
96+
97+
### 4.1 상태 전이 계약
98+
99+
```text
100+
사전조건: embedding_jobs.status = FAILED
101+
document_versions = 해당 문서의 최신 Version, status = FAILED
102+
documents.deleted_at IS NULL
103+
documents.status ∈ {UPLOADED, INDEXING, INDEXED, FAILED}
104+
같은 Version에 PENDING/PROCESSING Job 없음
105+
106+
전이: Job: FAILED -> PENDING, next_retry_at = NULL
107+
locked_by_worker_id, claim_token, locked_at, lock_expires_at, failed_at = NULL
108+
retry_count, max_retry_count, error_code, error_message 보존
109+
Version: FAILED -> CHUNKED (Chunk가 이미 있는 경우)
110+
-> UPLOADED (Chunk가 없는 경우)
111+
Document: 이전 INDEXED Version이 현재 검색 대상이면 변경 없음
112+
그 외에는 INDEXING
113+
Event: MANUAL_RETRY (FAILED -> PENDING) 1건 append
114+
Attempt: 변경 없음
115+
```
116+
117+
`retry_count`를 유지하므로 수동 재처리는 추가 실행 1회만 부여한다. 이번 실행이 다시 실패하면
118+
`hasRemainingRetries()`가 거짓이 되어 자동 재시도 없이 즉시 최종 실패로 종결되고, 필요하면 관리자가
119+
다시 수동 재처리를 요청한다. 이 선택은 재시도 이력을 지우지 않으면서 무한 자동 재시도를 만들지
120+
않기 위한 것이다.
121+
122+
### 4.2 Chunk와 Embedding 처리 정책
123+
124+
- Chunk는 삭제하지 않는다. 존재하면 완전한 Set이므로 파싱을 생략하고 재사용한다.
125+
- 대상 Version의 Embedding 행만 삭제한다. 최종 실패 시점에 이미 `STALE`이라 검색에 노출되지 않으며,
126+
남겨두면 Embedding 개수 불변식 검증에서 재처리가 차단된다.
127+
- 다른 Version의 Chunk와 Embedding은 조회하지도 변경하지도 않는다.
128+
129+
실패한 실행이 남긴 Vector를 다시 `ACTIVE`로 되살리는 방식은 채택하지 않았다. 완료 검증 단계에서
130+
실패한 경우 그 Vector Set이 실제로 불완전할 수 있고, 이를 판별하려면 완료 Transaction과 같은 수준의
131+
검증을 재처리 경로에 중복 구현해야 하기 때문이다.
132+
133+
### 4.3 Transaction 경계와 잠금 순서
134+
135+
`EmbeddingJobManualRetryService`는 단일 `@Transactional` 경계에서 외부 I/O 없이 동작한다.
136+
137+
1. `findByIdForUpdate`로 Job 행을 잠근다.
138+
2. Job 상태가 `FAILED`인지 확인한다.
139+
3. Version, Document를 기존 경로와 같은 순서로 잠근다.
140+
4. 재처리 대상 조건을 모두 검증한다.
141+
5. 재개 지점을 정하고 대상 Version Embedding을 삭제한다.
142+
6. Job, Version, Document 상태를 바꾸고 `MANUAL_RETRY` Event를 append한다.
143+
144+
Job 행 잠금이 Claim, 완료, 협력적 실패, Lease 복구와의 단일 직렬화 지점이다. Lease 복구는
145+
`PROCESSING` + 만료 행만, Claim은 `PENDING` 행만 후보로 삼으므로 커밋 전에는 이 Transaction과 경합하지
146+
않고, 커밋 후에는 정상 Claim 경로로 흡수된다.
147+
148+
### 4.4 API 계약
149+
150+
```text
151+
POST /admin/indexing-jobs/{jobId}/retry
152+
Request Body 없음, ADMIN 권한 필요
153+
154+
200 OK
155+
{
156+
"success": true,
157+
"data": {
158+
"jobId": 10,
159+
"status": "PENDING",
160+
"documentId": 3,
161+
"documentVersionId": 5,
162+
"documentVersionStatus": "CHUNKED",
163+
"retryCount": 3,
164+
"maxRetryCount": 3,
165+
"requeuedAt": "2026-08-06T15:00:00"
166+
}
167+
}
168+
```
169+
170+
`/admin/**``SecurityConfig`에서 이미 `hasRole("ADMIN")`으로 보호되므로 Security 설정은 변경하지
171+
않는다. 응답에는 Claim Token, 실패 원인 상세, 내부 예외 정보를 포함하지 않는다.
172+
173+
## 5. 오류 케이스
174+
175+
| 상황 | HTTP | 코드 |
176+
|---|---|---|
177+
| Job 없음 | 404 | `EMBEDDING-JOB-001` |
178+
| Job이 `PENDING`·`PROCESSING`·`INDEXED`·`CANCELED` (중복 요청 포함) | 409 | `EMBEDDING-JOB-008` |
179+
| 최신 Version이 아님 | 409 | `EMBEDDING-JOB-009` |
180+
| 삭제된 문서이거나 재처리 불가 문서 상태 | 409 | `EMBEDDING-JOB-009` |
181+
| 같은 Version에 살아 있는 Job 존재 | 409 | `EMBEDDING-JOB-009` |
182+
| Version이 `FAILED`가 아니거나 현재 Version 포인터 불일치 | 500 | `DOCUMENT-INDEXING-004` |
183+
| Job ID가 양수가 아님 | 400 | `COMMON-002` |
184+
185+
중복 요청은 멱등 재생 대신 명시적 충돌로 처리한다. 현재 Schema에는 `PENDING` Job이 자동 재시도
186+
예약인지 수동 재처리 결과인지 구분하는 식별자가 없어, 멱등 재생을 지원하려면 추가 Column이나 Event
187+
조회가 필요하기 때문이다.
188+
189+
## 6. 테스트 설계
190+
191+
### 6.1 단위 테스트
192+
193+
`EmbeddingJobManualRetryServiceTest` (Mockito)
194+
195+
- Chunk 존재 시 `CHUNKED` 재개, 미존재 시 `UPLOADED` 재개
196+
- 소유권 필드와 종료 시각 초기화, `retry_count` 보존
197+
- Claim Token 없는 `MANUAL_RETRY` Event 기록
198+
- 이전 `INDEXED` Version이 있을 때 문서 상태·포인터 보존
199+
- Job 없음, `PENDING`·`PROCESSING`·`INDEXED` 거부
200+
- 최신 Version 아님, 삭제된 문서, 살아 있는 Job 존재 거부
201+
- Version이 `FAILED`가 아닐 때 불변식 오류
202+
203+
### 6.2 Controller 테스트
204+
205+
`IndexingJobAdminControllerTest` (`@WebMvcTest`)
206+
207+
- 정상 응답 필드와 민감 정보 미노출
208+
- Job ID Validation
209+
- 정의된 오류 코드와 HTTP 상태 매핑
210+
- ADMIN 외 사용자와 미인증 요청 차단
211+
212+
### 6.3 통합 테스트
213+
214+
`EmbeddingJobManualRetryIntegrationTest` (`@Tag("integration")`, 실제 PostgreSQL)
215+
216+
- 소유권 초기화 후 즉시 Claim 후보가 되는지 확인
217+
- Chunk 유지와 대상 Version Embedding 삭제
218+
- Chunk 없는 Job의 `UPLOADED` 재개
219+
- Attempt 이력·재시도 횟수 보존과 `MANUAL_RETRY` Event 1건
220+
- 이전 `INDEXED` Version의 검색 결과와 현재 포인터 보존
221+
- 동시 요청 2건이 전이 1회 + 충돌 1회로 수렴
222+
- 자동 재시도 예정 Job 거부 시 예약 유지
223+
- 최신 Version이 아닐 때 거부하고 기존 데이터 유지
224+
225+
## 7. 커밋 분할
226+
227+
1. `feat: #108 최종 실패 Job 수동 재처리 도메인 규칙 추가`
228+
2. `feat: #108 수동 재처리 대상 Embedding 삭제 쿼리 추가`
229+
3. `feat: #108 최종 실패 Job 수동 재처리 Command Service 구현`
230+
4. `feat: #108 관리자 수동 재처리 API 추가`
231+
5. `test: #108 수동 재처리 단위·Controller 테스트 추가`
232+
6. `test: #108 수동 재처리 PostgreSQL 통합 테스트와 검증 결과 추가`
233+
7. `docs: #108 최종 실패 Job 수동 재처리 설계 문서 추가`
234+
235+
## 8. 완료 조건
236+
237+
- 최종 `FAILED` Job만 수동 재처리 가능
238+
- `PENDING`·`PROCESSING`·`INDEXED`·`CANCELED` 거부
239+
- 현재 검색 가능한 Version 유지
240+
- Attempt와 Retry 감사 이력 유지
241+
- 소유권 정보 초기화
242+
- 동시 재처리 요청이 하나의 상태 전이로 수렴
243+
- 전체 회귀 테스트 통과
244+
245+
## 9. 참고
246+
247+
- Flyway 마이그레이션 없음. `indexing_events.event_type`은 CHECK 제약이 없는 `VARCHAR(30)`이라
248+
`MANUAL_RETRY` 값을 그대로 저장할 수 있다.
249+
- `SecurityConfig` 변경 없음.
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# #108 최종 실패 인덱싱 Job 수동 재처리 검증 결과
2+
3+
## 1. 검증 정보
4+
5+
- 실행일: 2026-08-06 (Asia/Seoul)
6+
- 대상 브랜치: `feature/108`
7+
- 애플리케이션: Spring Boot 3.5.16, Java 17
8+
- 데이터베이스: 로컬 PostgreSQL 17.8 + pgvector 0.8.1 컨테이너
9+
- 검증 범위: 전체 회귀 Test와 수동 재처리 전용 단위·Controller·PostgreSQL 통합 Test
10+
- 최종 결과: 653개 통과, 실패·오류·Skip 0개
11+
12+
Test Class별 격리 Schema와 Flyway Migration을 사용했고, 기존 개발 데이터는 변경하지 않았다. DB 접속
13+
정보와 인증 값은 실행 Process 환경변수로만 주입했으며 실제 값은 기록하지 않는다.
14+
15+
## 2. 전체 회귀 검증
16+
17+
실행 명령의 환경 값은 Placeholder로 대체한다.
18+
19+
```bash
20+
DB_PORT='<local-test-port>' \
21+
DB_SSLMODE=disable \
22+
JWT_SECRET='<test-only-secret>' \
23+
MINIO_ENDPOINT='<local-test-endpoint>' \
24+
MINIO_ACCESS_KEY='<local-test-value>' \
25+
MINIO_SECRET_KEY='<local-test-value>' \
26+
MINIO_BUCKET='<local-test-bucket>' \
27+
./gradlew test
28+
```
29+
30+
결과:
31+
32+
```text
33+
BUILD SUCCESSFUL
34+
tests=653 failures=0 errors=0 skipped=0
35+
```
36+
37+
같은 명령을 `develop`(`e1bd2d2`)에서 실행한 기준 Test 수는 625개이며, 이번 작업으로 단위 13개, 통합
38+
8개, Controller 7개가 추가되어 653개가 됐다.
39+
40+
```text
41+
develop : tests=625 failures=0 errors=0
42+
feature : tests=653 failures=0 errors=0 (625 + 28)
43+
```
44+
45+
## 3. 단위 검증
46+
47+
`EmbeddingJobManualRetryServiceTest` 13개 통과.
48+
49+
| 시나리오 | 기대 | 결과 |
50+
|---|---|---|
51+
| Chunk가 있는 최종 실패 Job | `CHUNKED` 재개, 문서 `INDEXING` | 통과 |
52+
| Chunk가 없는 최종 실패 Job | `UPLOADED` 재개 | 통과 |
53+
| 재처리 후 소유권과 재시도 이력 | Worker·Token·Lease·`failed_at` 모두 `null`, `retry_count = 3` 유지, 잔여 자동 재시도 없음 | 통과 |
54+
| 감사 Event | `MANUAL_RETRY` 1건, Claim Token 미포함 | 통과 |
55+
| 이전 `INDEXED` Version 존재 | 문서 상태·현재 포인터 보존 | 통과 |
56+
| Job 없음 | `EMBEDDING_JOB_NOT_FOUND` | 통과 |
57+
| `PENDING` Job | `EMBEDDING_JOB_MANUAL_RETRY_NOT_ALLOWED` | 통과 |
58+
| `PROCESSING` Job | `EMBEDDING_JOB_MANUAL_RETRY_NOT_ALLOWED` | 통과 |
59+
| `INDEXED` Job | `EMBEDDING_JOB_MANUAL_RETRY_NOT_ALLOWED` | 통과 |
60+
| 최신 Version 아님 | `EMBEDDING_JOB_MANUAL_RETRY_TARGET_INVALID`, 상태 변경 없음 | 통과 |
61+
| 삭제된 문서 | `EMBEDDING_JOB_MANUAL_RETRY_TARGET_INVALID` | 통과 |
62+
| 같은 Version에 살아 있는 Job | `EMBEDDING_JOB_MANUAL_RETRY_TARGET_INVALID` | 통과 |
63+
| Version이 `FAILED`가 아님 | `DOCUMENT_INDEXING_FAILURE_INCONSISTENT` | 통과 |
64+
65+
## 4. Controller 계약 검증
66+
67+
`IndexingJobAdminControllerTest`의 수동 재처리 Test 7개 통과.
68+
69+
| 시나리오 | 기대 | 결과 |
70+
|---|---|---|
71+
| ADMIN 정상 요청 | 200, 재개 지점 포함, `claimToken`·`errorMessage` 미노출 | 통과 |
72+
| `jobId = 0` | 400 `COMMON-002` | 통과 |
73+
| Job 없음 | 404 `EMBEDDING-JOB-001` | 통과 |
74+
| 최종 실패 Job 아님 | 409 `EMBEDDING-JOB-008` | 통과 |
75+
| 대상 조건 불충족 | 409 `EMBEDDING-JOB-009` | 통과 |
76+
| 종료 데이터 불일치 | 500 `DOCUMENT-INDEXING-004` | 통과 |
77+
| USER 권한·미인증 | 403 | 통과 |
78+
79+
## 5. PostgreSQL 통합 검증
80+
81+
실행:
82+
83+
```bash
84+
DB_PORT='<local-test-port>' \
85+
DB_SSLMODE=disable \
86+
JWT_SECRET='<test-only-secret>' \
87+
MINIO_ENDPOINT='<local-test-endpoint>' \
88+
MINIO_ACCESS_KEY='<local-test-value>' \
89+
MINIO_SECRET_KEY='<local-test-value>' \
90+
MINIO_BUCKET='<local-test-bucket>' \
91+
./gradlew test \
92+
--tests 'com.opensource.docgrid.domain.embedding.integration.EmbeddingJobManualRetryIntegrationTest'
93+
```
94+
95+
결과:
96+
97+
```text
98+
tests=8 failures=0 errors=0 skipped=0
99+
```
100+
101+
### 5.1 재현 절차
102+
103+
각 Test는 격리 Schema에 다음 상태를 직접 구성한 뒤 실제 Service Transaction을 호출한다.
104+
105+
```text
106+
embedding_jobs status=FAILED, retry_count=3, max_retry_count=3,
107+
locked_by_worker_id / claim_token / lock_expires_at / failed_at 존재
108+
embedding_job_attempts status=FAILED (attempt_no=1)
109+
indexing_events FAILED 1건
110+
document_versions status=FAILED
111+
embeddings status=STALE (대상 Version)
112+
documents status=FAILED 또는 이전 INDEXED Version 보유
113+
```
114+
115+
### 5.2 시나리오별 결과
116+
117+
| 시나리오 | 확인 항목 | 결과 |
118+
|---|---|---|
119+
| 최종 실패 Job 재처리 | `status=PENDING`, 소유권 4개 Column과 `failed_at`·`next_retry_at` `null`, `findNextPendingForUpdate`가 즉시 해당 Job 반환 | 통과 |
120+
| Chunk 유지·Embedding 정리 | Version `CHUNKED`, `document_chunks` 1건 유지, 대상 Version `embeddings` 0건, 문서 `INDEXING` | 통과 |
121+
| Chunk 없는 Job | Version `UPLOADED` | 통과 |
122+
| 감사 이력 | `retry_count=3`, Attempt 1건 `FAILED` 유지, `FAILED` Event 1건 유지, `MANUAL_RETRY` Event 1건 추가, metadata에 Claim Token 없음 | 통과 |
123+
| 이전 검색 Version 보호 | 문서 `INDEXED` 유지, `current_version_id`가 이전 Version, 이전 Version Embedding `ACTIVE`, 재처리 전후 Vector 검색 결과가 모두 `이전 검색 본문` | 통과 |
124+
| 동시 요청 2건 | 커밋 1건, 나머지 1건 `EMBEDDING_JOB_MANUAL_RETRY_NOT_ALLOWED`, `MANUAL_RETRY` Event 정확히 1건, 대상 Embedding 0건 | 통과 |
125+
| 자동 재시도 예정 Job | 409로 거부, `next_retry_at` 유지, Version `FAILED` 유지, `MANUAL_RETRY` Event 0건 | 통과 |
126+
| 최신 Version 아님 | 409로 거부, Job `FAILED` 유지, 대상 Embedding 1건 그대로 유지 | 통과 |
127+
128+
## 6. Swagger 수동 검증
129+
130+
이번 작업에서는 Swagger 수동 검증을 수행하지 않았다. 수동 재처리 API는 `hasRole("ADMIN")`으로 보호되며
131+
검증하려면 ADMIN 계정 생성과 JWT 발급, 그리고 자동 재시도를 모두 소진한 최종 실패 Job을 실제로
132+
만들어야 한다. 이 사전 상태는 현재 로컬에서 실제 문서 업로드부터 Worker 실행까지 전 구간을 돌려야
133+
재현할 수 있어, 예정된 로컬 전체 관통 E2E 작업에서 함께 수행하는 것이 적절하다.
134+
135+
대체 검증으로 다음 두 계층을 사용했다.
136+
137+
- HTTP 계약: `@WebMvcTest` 기반 Controller Test로 상태 코드, 응답 필드, 민감 정보 미노출, ADMIN 권한
138+
차단을 확인
139+
- 실제 DB 동작: 실제 PostgreSQL 통합 Test로 상태 전이 원자성, 검색 보호, 동시 요청 수렴을 확인
140+
141+
## 7. 결론
142+
143+
- 최종 실패 Job만 수동 재처리 가능하고 나머지 상태는 모두 거부한다.
144+
- 현재 검색 가능한 Version은 재처리 전후 동일한 검색 결과를 유지한다.
145+
- Attempt와 재시도 감사 이력을 삭제하지 않는다.
146+
- 소유권 정보가 초기화되어 과거 Claim Token으로는 후속 단계를 수행할 수 없다.
147+
- 동시 재처리 요청은 하나의 상태 전이로 수렴한다.
148+
- 전체 653개 Test가 실패 없이 통과한다.

0 commit comments

Comments
 (0)