본문으로 건너뛰기

파일 서비스

이 페이지는 파일 메타데이터, object storage 업로드/다운로드 흐름, 재사용 가능한 파일 링크, 다운로드 이력, 교재 폴더를 담당하는 테넌트 범위 모듈 lumie-backend/modules/file의 레퍼런스입니다.

소스 경로

경로역할
lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/web/{FileController,TextbookFolderController}.java공개 HTTP 인터페이스
lumie-backend/modules/file/src/main/java/com/lumie/file/application/service/{FileCommandService,FileQueryService,FileMetadataLookupService}.java업로드, 다운로드, 권한 확인, 메타데이터 로직
lumie-backend/modules/file/src/main/java/com/lumie/file/application/service/{TextbookFolderCommandService,TextbookFolderQueryService}.java폴더 트리 명령과 조회
lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/internal/FileServiceAdapter.java다른 모듈이 사용하는 공개 프로세스 내부 file API
lumie-backend/modules/file/src/main/java/com/lumie/file/domain/entity/{FileMetadata,FileLink,FileDownload,TextbookFolder}.java주요 file aggregate
lumie-backend/modules/file/src/main/java/com/lumie/file/domain/vo/FilePath.javaMinIO 기반 파일 storage를 위한 object key builder
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sqlRLS와 함께 file_metadata, textbook_folders를 기본 생성
lumie-backend/app/src/main/resources/db/migration/public/{V54__create_file_links,V63__create_file_download_table,V64__add_download_count_to_file_download}.sql재사용 링크와 다운로드 이력용 후속 table

공개 인터페이스

엔드포인트목적
POST /v1/files/upload백엔드를 통한 직접 프록시 업로드
POST /v1/files/presigned-upload메타데이터와 presigned object-storage 업로드 URL 생성
POST /v1/files/presigned-download권한 있는 파일에 대한 presigned object-storage 다운로드 URL 생성
POST /v1/files스토리지에 object가 존재한 뒤 presigned 업로드를 완료 처리
GET /v1/files/{fileId}, GET /v1/files, DELETE /v1/files/{fileId}메타데이터 조회, 목록, 삭제
GET /v1/files/{fileId}/download백엔드를 통해 권한 있는 다운로드 스트리밍
GET /v1/files/{fileId}/downloadersstaff 사용자에게 student 이름을 포함한 다운로드 이력 반환
GET /v1/files/public/{fileId}명시적으로 허용된 entity type만 위한 익명 공개 파일 전달
POST /v1/textbook-folders, GET /v1/textbook-folders, GET /v1/textbook-folders/{id}, GET /v1/textbook-folders/{id}/path, PATCH /v1/textbook-folders/{id}, DELETE /v1/textbook-folders/{id}교재 폴더 트리 관리

내부 인터페이스와 의존성

인터페이스역할
lumie-backend/libs/internal-api/src/main/java/com/lumie/file/api/FileService.java파일 삭제, 링크 삭제, 재사용 파일 링크 교체를 위한 공개 프로세스 내부 계약
StoragePort직접 업로드, presigned URL, object 삭제에 쓰는 object-storage 추상화
ContentServiceFileMetadataLookupService가 announcement와 Q&A에 대한 student 다운로드 권한을 확인할 때 사용
StudentServicegetFileDownloaders(...)가 다운로드 이력의 student 이름을 해석할 때 사용

집계와 테이블

집계참고
FileMetadatastorage object key와 upload-completed flag를 가진 테넌트 소유 파일 행
FileLink기존 source file을 다른 entity에 연결하는 재사용 attachment 링크
FileDownload사용자별 다운로드 이력과 횟수
TextbookFolder최대 깊이 5인 테넌트 폴더 트리

런타임 흐름

계약 참고 사항

익명 공개 다운로드 인터페이스는 의도적으로 매우 좁게 제한됩니다.

// lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/web/FileController.java
private static final java.util.Set<EntityType> PUBLIC_ALLOWED_TYPES =
java.util.Set.of(EntityType.HOMEPAGE_ASSET);

이 route는 임의의 테넌트 파일이 아니라, 홈페이지 이미지처럼 명시적으로 공개된 asset만을 위한 것입니다.

Storage identity는 표시 파일명과 분리됩니다. FilePath.toObjectKey()는 MinIO object key를 tenant/entityType/fileId/filename으로 만들고, FileMetadata.originalFilename은 사용자에게 보이는 파일명을 보존합니다. fileId가 각 object key를 unique하게 만들기 때문에 같은 이름 업로드는 허용됩니다.

예시 계약

이 예시는 FileController, PresignedUploadRequest, PresignedUploadResponse, RegisterUploadRequest, FileMetadataResponse에서 직접 가져왔습니다.

Presigned 업로드

POST /v1/files/presigned-upload
Content-Type: application/json

{
"entityType": "HOMEPAGE_ASSET",
"entityId": 15,
"filename": "hero.png",
"contentType": "image/png",
"fileSize": 24576
}
HTTP/1.1 201 Created

{
"fileId": "<uuid>",
"uploadUrl": "https://<object-store>/...",
"expiresInSeconds": <seconds>
}

업로드 완료

POST /v1/filesFileCommandService.registerUploadCompleted(...)가 object 존재를 확인한 뒤에만 presigned 업로드를 완료 처리합니다.

POST /v1/files
Content-Type: application/json

{
"fileId": "<uuid>"
}
HTTP/1.1 200 OK

{
"id": "<uuid>",
"entityType": "HOMEPAGE_ASSET",
"entityId": 15,
"folderId": null,
"originalFilename": "hero.png",
"contentType": "image/png",
"fileSize": 24576,
"uploadCompleted": true,
"createdAt": "<timestamp>"
}

실패, 재시도, 관측성

  • POST /v1/files/upload는 1 MiB를 초과하는 파일을 거부하고, 호출자에게 presigned-upload 흐름으로 전환하라고 안내합니다.
  • 직접 업로드가 Idempotency-Key를 사용하면, 컨트롤러는 파일 바이트와 메타데이터의 fingerprint를 계산합니다. 다른 내용으로 같은 key를 재사용하면 idempotency conflict가 됩니다.
  • FileMetadataLookupService는 visible announcement 또는 Q&A에 연결된 파일에 대해서만 student 다운로드를 허용합니다. staff 사용자는 이 콘텐츠별 검사를 우회합니다.
  • deleteFile(...)file_links에서 아직 참조 중인 파일을 삭제하지 않습니다.
  • FileServiceAdapter는 호출자의 DB 트랜잭션이 커밋된 뒤에만 MinIO 정리를 수행하지만, 교재 폴더의 재귀 삭제는 트랜잭션 중간에 object를 직접 삭제합니다. 실패 후 storage와 database 간 drift를 조사할 때 이 비대칭이 중요합니다.
  • getFileDownloaders(...)는 staff 전용이며, student 모듈을 통해 user record를 더 이상 해석할 수 없으면 "삭제된 학생"을 반환합니다.
  • metadata는 존재하지만 다운로드가 MinIO NoSuchKey로 실패하면, 권한이나 폴더 query를 바꾸기 전에 object-key 충돌 troubleshooting guide를 사용합니다.

검증

cd lumie-backend
./gradlew :modules:file:test
./gradlew :modules:file:test --tests '*FileController*'
./gradlew :modules:file:test --tests '*TextbookFolder*'

예상 성공 신호:

  • Gradle이 BUILD SUCCESSFUL로 종료되고, file 모듈 테스트가 여전히 컨트롤러 수준 업로드와 폴더 흐름을 다룹니다.
  • PresignedUploadResponse가 여전히 fileId, uploadUrl, expiresInSeconds를 노출하고, FileMetadataResponse.uploadCompletedregisterUploadCompleted(...)true로 바뀝니다.

관련 페이지