본문으로 건너뛰기

파일 서비스

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

소스 경로

Path역할
lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/web/{FileController,TextbookFolderController}.java공개 HTTP surface
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/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

공개 인터페이스

Endpoint목적
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}교재 폴더 트리 관리

내부 인터페이스와 의존성

Surface역할
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 이름을 해석할 때 사용

집계와 테이블

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

런타임 흐름

계약 참고 사항

익명 공개 다운로드 surface는 의도적으로 매우 좁게 제한됩니다.

// 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는 임의의 tenant 파일이 아니라, 홈페이지 이미지처럼 명시적으로 공개된 asset만을 위한 것입니다.

예시 계약

이 예시는 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 트랜잭션이 commit된 뒤에만 MinIO 정리를 수행하지만, 교재 폴더의 재귀 삭제는 트랜잭션 중간에 object를 직접 삭제합니다. 실패 후 storage와 database 간 drift를 조사할 때 이 비대칭이 중요합니다.
  • getFileDownloaders(...)는 staff 전용이며, student 모듈을 통해 user record를 더 이상 해석할 수 없으면 "삭제된 학생"을 반환합니다.

검증

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

예상 성공 신호:

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

관련 페이지