본문으로 건너뛰기

파일 다운로드 Object Key 충돌

특정 파일 하나만 500으로 다운로드에 실패하지만 파일 메타데이터 행은 남아 있고 주변 파일 다운로드는 정상일 때 이 가이드를 사용합니다.

이 장애는 file 모듈의 backend-to-object-storage 경계에 있습니다. MinIO 호출 전에 403 또는 404로 끝나야 하는 권한 문제와는 다릅니다.

증상

  • GET /v1/files/{fileId}/downloadapplication/problem+json과 함께 500을 반환합니다.
  • 응답에 backend x-request-id가 있고, backend 로그에 FILE_501 또는 Storage operation failed가 보입니다.
  • 파일은 GET /v1/files에 표시되고 upload_completedtrue입니다.
  • 보통 같은 이름의 파일을 업로드한 뒤 다른 같은 이름의 파일을 삭제한 직후, 특정 파일 하나만 실패합니다.
  • MinIO가 file_metadata.object_key에 대해 NoSuchKey를 반환합니다.

알려진 장애 이력

날짜장애 모드지속 fix
2026-07-17관측된 production 예시: joossameng 교재 파일 exam_review.pdf가 메타데이터 id 10e28afa-87c1-47ee-847d-8940d85058ff로 다운로드됐지만, MinIO가 joossameng/textbook/exam_review.pdf에 대해 NoSuchKey를 반환했습니다. 조사 결과 새 업로드 뒤 같은 이름의 기존 파일을 삭제하면서 공유 object key가 삭제된 것으로 확인됐습니다.FilePath.toObjectKey()가 이제 fileId를 포함하므로, 표시 파일명이 같아도 tenant/entityType/fileId/filename 형태의 서로 다른 object key가 생성됩니다.

현재 런타임 계약

표면소스 경로필수 동작
다운로드 진입점lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/web/FileController.javaGET /v1/files/{fileId}/download는 권한 있는 object를 스트리밍하고 로그인 사용자에 대해 다운로드 이력을 기록합니다.
메타데이터 조회lumie-backend/modules/file/src/main/java/com/lumie/file/application/service/FileMetadataLookupService.javastorage 접근 전에 완료되고 권한이 확인된 FileMetadata 행만 해석합니다.
다운로드 서비스lumie-backend/modules/file/src/main/java/com/lumie/file/application/service/FileQueryService.javafile_metadata.object_key를 읽고 StoragePort.getObject(...)를 호출합니다.
Storage adapterlumie-backend/modules/file/src/main/java/com/lumie/file/adapter/out/storage/MinioStorageAdapter.javaMinIO 읽기 실패를 FILE_501로 매핑합니다.
Object key builderlumie-backend/modules/file/src/main/java/com/lumie/file/domain/vo/FilePath.javaobject key를 tenant/entityType/fileId/filename으로 만듭니다.
충돌 테스트lumie-backend/modules/file/src/test/java/com/lumie/file/domain/vo/FilePathTest.java같은 tenant와 entity type 아래의 두 exam_review.pdf 업로드가 서로 다른 object key를 생성함을 증명합니다.

표시 파일명은 FileMetadata.originalFilename에 그대로 남습니다. storage를 보호하려고 표시 이름을 전역 unique하게 만들지 마세요. storage unique성은 object_key가 책임져야 합니다.

진단

  1. 실패한 브라우저 요청의 x-request-id에서 시작합니다.
  2. backend 로그에서 해당 request id를 검색하고, 장애가 FileMetadataLookupService 권한 확인이 아니라 MinioStorageAdapter.getObject(...)인지 확인합니다.
  3. tenant context 아래에서 영향받은 file_metadata 행을 읽고 id, entity_type, folder_id, original_filename, file_size, object_key, upload_completed, created_at, updated_at을 확보합니다.
  4. 같은 legacy key 형태 또는 표시 파일명을 공유한 다른 행을 검색합니다. 이 representative diagnostic query는 FileMetadata가 매핑하고 V18__rls_baseline.sql이 생성한 field를 사용합니다.
SET app.tenant_id = '<tenant-id>';

SELECT id, entity_type, folder_id, original_filename, file_size,
object_key, upload_completed, created_at, updated_at
FROM file_metadata
WHERE object_key = '<object-key>'
OR original_filename = '<filename>'
ORDER BY created_at;
  1. MinIO object가 없는지 확인합니다. 로그에서는 NoSuchKey로 보이고, versioned bucket이면 x-amz-delete-marker: true도 보일 수 있습니다.
  2. 교체 업로드 뒤 같은 이름의 파일이 삭제됐는지 확인합니다. FileCommandService.deleteFile(...)fileMetadata.getObjectKey()로 object를 삭제하므로, legacy 중복 key에서는 기존 metadata 삭제가 새 물리 object를 지울 수 있습니다.

2026-07-17 관측 예시의 runtime log signature는 다음과 같았습니다.

Failed to get object: joossameng/textbook/exam_review.pdf
ErrorResponse(code = NoSuchKey, message = The specified key does not exist.)
x-amz-delete-marker: true
Business exception [FILE_501]: Storage operation failed

수정

  • 누락된 object에 복구 가능한 MinIO version이 있으면, 영향받은 object_key의 object version을 복구합니다.
  • 복구 가능한 object version이 없으면, tenant에게 파일을 다시 업로드하게 하고 깨진 metadata 행은 일반 product flow로 제거하거나 교체합니다.
  • 여러 metadata 행이 같은 object key를 가리키게 수리하지 마세요.
  • unique성을 강제하려고 original_filename을 바꾸지 마세요. 사용자는 같은 이름의 파일을 업로드할 수 있어야 하며, object key가 unique성을 가져야 합니다.
  • 코드 경로가 여전히 tenant/entityType/filename을 만든다면, 같은 이름 업로드를 더 받기 전에 FilePath.toObjectKey() fix를 배포합니다.

검증 증거

  • 영향받은 GET /v1/files/{fileId}/download200을 반환하고 기대한 bytes를 스트리밍합니다.
  • backend 로그에 영향받은 key의 NoSuchKey가 더 이상 보이지 않습니다.
  • 새 같은 이름 업로드는 key에 fileId가 포함되어 서로 다른 file_metadata.object_key 값을 만듭니다.
  • 같은 이름 파일 하나를 삭제해도 다른 파일의 object가 삭제되지 않습니다.
  • lumie-backend에서 ./gradlew :modules:file:test --tests com.lumie.file.domain.vo.FilePathTest가 통과합니다.

예방

  • direct 업로드와 presigned 업로드 흐름 모두에서 FilePath.toObjectKey()tenant/entityType/fileId/filename 계약으로 유지합니다.
  • file 모듈을 refactor할 때 FilePathTest 또는 동등한 충돌 테스트를 유지합니다.
  • object_key는 storage identity, original_filename은 표시 메타데이터로 취급합니다.
  • 파일 drift를 조사할 때 권한이나 폴더 query를 바꾸기 전에 database metadata와 MinIO object 존재를 비교합니다.
  • UI에 파일명 제한을 추가하기보다 object-key builder에서 storage identity를 고칩니다.

관련 페이지