Skip to content

fix(file): 파일 저장 rollback 보상과 Worker Link 업로드 멱등성을 보강 - #185

Draft
krestar wants to merge 11 commits into
mainfrom
fix/172-file-storage-rollback-idempotency
Draft

fix(file): 파일 저장 rollback 보상과 Worker Link 업로드 멱등성을 보강#185
krestar wants to merge 11 commits into
mainfrom
fix/172-file-storage-rollback-idempotency

Conversation

@krestar

@krestar krestar commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Draft / 머지 전 확인 필요

구현과 최종 HEAD의 PostgreSQL 전체 테스트를 완료했습니다.
V48~V50 반영 순서와 머지 직전 migration 번호를 확인한 뒤 Ready로 전환합니다.

왜 필요한가

파일 저장소 쓰기는 DB 트랜잭션 밖에서 발생하므로, 파일 저장 이후 DB 작업이 실패하면 DB에는 참조가 없지만 물리 파일만 남을 수 있었습니다.
반대로 로컬 파일을 최종 경로에 직접 복사하면 쓰기 실패 시 불완전한 최종 파일이 노출될 가능성도 있었습니다.

Worker Link 문서 업로드는 재시도나 동시 요청에서 같은 논리 요청이 중복 실행될 수 있었습니다.
기존 clientRequestId는 클라이언트가 선택적으로 보내는 multipart 필드였고, 같은 키의 요청 내용이 달라졌는지 판별하거나 동시 요청을 하나의 결과로 수렴시키는 계약이 부족했습니다.

일반 파일 업로드와 Worker Link 업로드를 함께 수정한 이유는 두 경로 모두 비트랜잭션 파일 저장과 DB 트랜잭션 사이의 일관성 경계를 공유하기 때문입니다.
공통 저장 primitive와 rollback 보상 정리를 먼저 보강하고, Worker Link 경로에만 API 수준의 멱등성 계약과 동시성 제어를 추가했습니다.

Closes #172

무엇이 바뀌나

1. 로컬 파일 저장을 원자화하고 멱등 삭제를 지원

  • storage root와 같은 파일시스템의 임시 파일에 먼저 기록합니다.
  • 선언된 파일 크기와 실제 기록된 크기가 다르면 최종 반영 전에 실패시킵니다.
  • 기록이 끝난 파일은 ATOMIC_MOVE로만 최종 경로에 반영합니다.
  • atomic move를 지원하지 않는 환경에서는 일반 move로 fallback하지 않고 fail-closed 합니다.
  • 저장 실패 시 임시 파일을 정리하고, 정리 실패는 원래 예외의 suppressed exception으로 보존합니다.
  • FileStorage.deleteIfExists()를 추가해 rollback 보상 정리를 반복 호출해도 안전하게 했습니다.

2. DB rollback 시 생성된 파일을 보상 정리

  • 파일 저장 전에 트랜잭션 완료 상태를 관찰하도록 등록합니다.
  • store() 성공 직후에만 보상 정리를 활성화해, 생성 key가 기존 정상 파일과 충돌하여 저장에 실패한 경우 그 기존 파일을 삭제하지 않습니다.
  • 파일 저장 후 DB 작업이 rollback되거나 commit 과정에서 실패하면 생성된 파일을 삭제합니다.
  • 정상 commit에서는 파일을 유지합니다.
  • 트랜잭션 결과가 UNKNOWN이거나 보상 삭제가 실패하면 원래 실패 원인을 덮지 않고 운영 로그로 남깁니다.
  • 일반 파일 업로드와 Worker Link 문서 업로드가 같은 보상 정리 방식을 사용합니다.

3. Worker Link 문서 업로드에 멱등성 계약 적용

  • Idempotency-Key 헤더를 필수 canonical 식별자로 사용합니다.
  • 문서 종류, 파일명, MIME type, 크기, 파일 checksum을 기반으로 요청 해시를 계산합니다.
  • 같은 Worker Link에서 같은 키와 같은 요청을 재시도하면 기존 upload_id를 반환합니다.
  • 같은 키를 다른 내용에 재사용하면 409 IDEMPOTENCY_CONFLICT를 반환합니다.
  • Worker Link 행 잠금으로 같은 링크의 동시 요청을 직렬화하고, DB 행과 물리 파일이 하나의 결과로 수렴하도록 했습니다.
  • legacy multipart clientRequestId는 선택·deprecated 입력으로 유지하되 멱등성 판단에는 사용하지 않습니다.
  • 신규 레코드의 legacy 식별자는 canonical:<stored_file_id> 형식으로 저장해 해시 식별자와의 충돌을 방지합니다.
  • 문서 MIME 허용 범위는 기존과 동일하게 JPEG, PNG, WEBP, PDF를 유지합니다. PASSPORT_COPY는 문서 종류이며 PDF 전용 제한이 아닙니다.
  • documentType은 서버의 지원 enum을 OpenAPI에 명시하고, 알 수 없는 값은 파일 생성 전에 400 VALIDATION_FAILED로 거절합니다.

4. V51 멱등성 해시 스키마 추가

  • worker_document_upload_idempotency에 다음 nullable 컬럼을 추가합니다.
    • idempotency_key_hash
    • request_hash
  • canonical 행에 대한 unique/check constraint를 추가합니다.
  • 기존 legacy 행은 두 컬럼이 모두 NULL인 상태로 계속 읽을 수 있습니다.
  • 기존 데이터를 삭제하거나 강제 backfill하지 않습니다.

5. 회귀·PostgreSQL 동시성 테스트와 운영 문서 보강

  • source read 실패, size mismatch, finalize 실패, 기존 최종 파일 보호, 반복 삭제를 검증합니다.
  • rollback/commit 성공/commit 실패/결과 UNKNOWN에서 파일 보상 동작을 검증합니다.
  • 실제 PostgreSQL 16과 HTTP 요청으로 Worker Link 동시 업로드가 DB 행 1개와 물리 파일 1개로 수렴하는지 검증합니다.
  • legacy 식별자 전환 충돌과 PostgreSQL 외래 키를 고려한 테스트 정리 순서를 검증합니다.
  • 배포 전후 확인 및 고아 파일 복구 절차를 문서화합니다.

검증

자동 테스트

  • 최종 HEAD를 전용 PostgreSQL 16 테스트 DB에서 .\gradlew.bat clean test
    • 136개 test suite
    • 622개 test
    • skipped 0 / failures 0 / errors 0
    • BUILD SUCCESSFUL
  • Flyway V1~V51 적용 및 validate
  • WorkerLinkDocumentPostgreSqlIntegrationTest
  • DemoSeedPostgreSqlApplicationIntegrationTest
  • WorkerLinkSecurityIntegrationTest
  • 기존 파일 충돌 시 rollback 보상이 기존 정상 파일을 삭제하지 않는 회귀 테스트
  • documentType 거절 및 OpenAPI enum 계약 테스트

수동 테스트

  • 정상 WEBP 문서 업로드
    • 물리 파일 1개 생성
    • 원본과 저장 파일의 SHA-256 일치
    • 임시 파일 0개
  • 같은 Idempotency-Key와 같은 파일을 순차 재시도
    • 같은 upload_id 반환
  • 같은 Idempotency-Key와 같은 파일을 2개 요청으로 동시 업로드
    • 응답 2개가 같은 upload_id로 수렴
    • DB 행 1개, 물리 파일 1개, 임시 파일 0개
  • 같은 Idempotency-Key에 다른 파일을 업로드
    • 409 IDEMPOTENCY_CONFLICT
    • 추가 물리 파일 0개
  • DB canonical 행 확인
    • client_request_id = canonical:<stored_file_id>
    • stored_file_id = storage_key = upload_id
    • key/request hash 저장 확인

보안·개인정보

  • 원본 Idempotency-Key는 DB에 저장하지 않고 SHA-256 해시만 저장합니다.
  • 업로드 응답과 오류 응답에 storage 절대 경로를 노출하지 않습니다.
  • 기존 Worker Link 토큰 검증, 만료, 문서 형식·크기 검증은 유지합니다.
  • path traversal 방어와 storage root 경계 검증을 유지합니다.
  • 로그에는 원본 파일 내용이나 원본 멱등성 키를 남기지 않습니다.

API·DB·운영 영향

API

  • POST /api/v1/public/worker-links/{token}/documents
    • Idempotency-Key 헤더가 필수입니다.
    • 같은 키/같은 요청은 기존 성공 응답을 반환합니다.
    • 같은 키/다른 요청은 409 IDEMPOTENCY_CONFLICT를 반환합니다.
    • 성공 응답 스키마는 변경하지 않습니다.
  • 일반 파일 업로드 API에는 새 멱등성 계약을 추가하지 않았으며 기존 요청·응답 계약을 유지합니다.

DB

  • 신규 Flyway 마이그레이션: V51__add_worker_document_upload_idempotency_hashes.sql
  • nullable 컬럼과 constraint를 추가하는 additive migration입니다.
  • 이전 애플리케이션 버전은 새 컬럼을 사용하지 않아도 기동할 수 있으므로, 애플리케이션 rollback 시 V51을 되돌리지 않고 유지합니다.
  • 다만 신규 레코드의 client_request_idcanonical:<stored_file_id>이므로, 구버전 서버는 신버전에서 성공한 요청을 기존 clientRequestId로 찾지 못할 수 있습니다. 이는 schema 호환과 별개인 의미적 rollback 한계입니다.
  • rollback 가능 기간에는 Client가 Idempotency-Key와 multipart clientRequestId를 함께 전송하는 현재 동작을 유지하고, 구버전 rollback 후 신버전 성공 요청을 자동 재시도하지 않습니다.
  • 이미 적용된 Flyway 파일을 수정하거나 삭제하지 않습니다.

infra 및 배포 관련 전달 사항

  • #174의 V48~V50을 먼저 main에 반영한 뒤 이 PR을 최신 main에 맞춥니다.
  • #176에 중복 포함된 V47~V50의 정리 방향을 관련 담당자와 확인합니다. #176이 현재 상태 그대로 이 PR보다 먼저 머지되는 것을 전제로 하지 않습니다.
  • 이 PR 머지 직전에 main의 다음 migration 번호를 다시 조회합니다. 다른 PR이 V51을 먼저 사용했다면 이 PR migration을 다음 번호로 변경합니다.
  • 실제 파일 볼륨에서 atomic move가 지원되는지 배포 전 smoke test가 필요합니다. 미지원 환경에서는 불완전한 파일을 노출하지 않고 업로드가 실패합니다.
  • V51 unique constraint 적용 전 대상 테이블 규모와 기존 canonical 중복 데이터가 없는지 확인하고, 운영 DB recovery point를 확보합니다.
  • 배포 후 정상 업로드, 같은 키 재시도, 임시 파일 0개를 확인합니다.
  • 애플리케이션 rollback이 필요하면 이전 이미지로 되돌리되 V51 스키마는 유지하고, 위 의미적 멱등성 한계에 따라 자동 재시도를 막고 DB·파일 volume을 대조합니다.

화면 또는 응답 예시

같은 키와 같은 요청을 재시도하면 최초 응답과 같은 upload_id를 반환합니다.

{
  "upload_id": "<same-upload-id>",
  "file_name": "passport.webp",
  "size": 247114,
  "expires_at": "<expires-at>"
}

같은 키에 다른 요청 내용을 사용하면 다음 오류를 반환합니다.

{
  "status": 409,
  "code": "IDEMPOTENCY_CONFLICT",
  "message": "같은 Idempotency-Key가 다른 문서 업로드 요청에 이미 사용되었습니다."
}

머지 체크리스트

  • #174의 V48~V50 migration을 먼저 main에 반영
  • #176에 중복 포함된 V47~V50 정리 방향 확인
  • 최신 main 반영 및 충돌 해결
  • V51 번호 중복 여부 재확인
  • 최종 HEAD를 깨끗한 PostgreSQL 테스트 DB에서 전체 테스트
  • 필수 PR checks 통과
  • infra 담당자에게 파일 볼륨 atomic move 및 V51 사전 점검 항목 전달

- 임시 파일에 완전히 기록한 뒤 atomic move로 저장을 확정한다.
- 저장 실패 시 이번 요청이 만든 임시 산출물을 정리한다.
- storage key 기반 멱등 삭제 계약과 단위 테스트를 추가한다.
- 트랜잭션 완료 상태에 따라 요청 소유 파일을 보상 정리한다.
- 상위 트랜잭션 rollback과 commit 실패 경로까지 관찰한다.
- cleanup 실패와 불명확한 transaction 상태를 안전하게 기록한다.
- 일반 파일 이동 fallback을 제거해 불완전한 최종 파일 노출을 방지한다.
- 원자적 이동이 불가능하면 저장을 실패시키고 임시 파일을 정리한다.
- 기존 파일 보호 테스트 이름을 실제 검증 범위에 맞게 명확히 한다.
- canonical Idempotency-Key hash와 request hash 저장 컬럼을 추가한다.
- Worker Link 범위에서 canonical key의 유일성을 DB 제약으로 보장한다.
- 기존 clientRequestId 기반 레코드와 이전 애플리케이션의 호환성을 유지한다.
- 레거시 insert와 hash 무결성 제약을 PostgreSQL migration 테스트로 검증한다.
- Idempotency-Key를 필수 canonical key로 검증하고 해시만 저장한다.
- 정규화된 요청 정보와 파일 체크섬으로 재시도 및 충돌을 판별한다.
- Worker Link 행 잠금으로 동시 업로드를 직렬화한다.
- 업로드 파일에 트랜잭션 롤백 보상을 적용한다.
- clientRequestId를 선택·deprecated 필드로 전환하고 API 계약 테스트를 보강한다.
- 동일 Worker Link 문서 업로드의 실제 HTTP 경쟁을 PostgreSQL 16에서 재현한다
- 멱등 요청이 하나의 DB 행과 로컬 파일로 수렴하는지 반복 검증한다
- 실제 파일 저장 후 트랜잭션 롤백 시 DB와 파일이 함께 정리되는지 검증한다
- cleanup 실패와 UNKNOWN 결과의 운영 대응 및 배포 Smoke 절차를 문서화한다
- canonical 업로드의 legacy 호환 식별자를 서버 생성 파일 UUID로 분리한다.
- 기존 client_request_id가 Idempotency-Key 해시와 같아도 PK 충돌하지 않게 한다.
- legacy 행과 canonical 행이 함께 존재하는 전환 시나리오를 회귀 테스트로 검증한다.
- worker_document를 task와 stored_file보다 먼저 삭제한다.
- 테스트 실행 순서와 관계없이 FK 제약을 준수하도록 fixture 초기화를 수정한다.
@krestar
krestar marked this pull request as draft August 15, 2026 15:26
krestar and others added 3 commits August 16, 2026 00:26
- 파일 저장 성공 후에만 롤백 cleanup을 활성화한다.
- storage key 충돌로 저장에 실패한 경우 기존 파일을 삭제하지 않는다.
- 일반 파일 및 Worker Link 업로드의 파일 ownership 회귀 테스트를 추가한다.
- 지원하는 documentType을 OpenAPI에 명시하고 잘못된 유형을 파일 생성 전에 거절한다.
- V51 적용 후 구버전 서버로 롤백할 때 canonical 멱등성 결과를 재사용하지 못하는 한계를 문서화한다.
- 롤백 가능 기간에는 clientRequestId 전송을 유지하도록 운영 절차를 보강한다.
@krestar krestar added area:server Spring Boot API·도메인·DB·tenant·Task Workflow 영역; Prompt·모델·Provider 구현 제외 area:infra Server Dockerfile·DB 설정·CI hook·배포 가능성 영역; 통합 인프라 운영은 infra 저장소와 조율 priority:P1 핵심 작업 다음으로 처리할 중요 작업 status:in-review 구현을 마치고 리뷰 또는 병합을 기다리는 작업 labels Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:infra Server Dockerfile·DB 설정·CI hook·배포 가능성 영역; 통합 인프라 운영은 infra 저장소와 조율 area:server Spring Boot API·도메인·DB·tenant·Task Workflow 영역; Prompt·모델·Provider 구현 제외 priority:P1 핵심 작업 다음으로 처리할 중요 작업 status:in-review 구현을 마치고 리뷰 또는 병합을 기다리는 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[File][Reliability] FileStorage rollback 보상과 Worker Link 업로드 멱등성 보강

1 participant