File 모듈
File 모듈은 Lumie 플랫폼의 파일 관리를 담당합니다(modules/file). MinIO 오브젝트 스토리지와 연동하여 파일 업로드, 다운로드, 메타데이터 관리 기능을 제공합니다. lumie-backend.jar에 포함되어 단일 Spring Boot 애플리케이션으로 배포됩니다.
주요 기능
파일 업로드
- 직접 업로드: 멀티파트 폼을 통한 즉시 업로드
- Presigned URL 업로드: 클라이언트가 직접 스토리지에 업로드
파일 다운로드
- 스트리밍 다운로드: 서버를 통한 파일 스트리밍
- Presigned URL 다운로드: 클라이언트가 직접 스토리지에서 다운로드
메타데이터 관리
- 파일 정보 저장 및 조회
- 엔티티별 파일 연결 관리
- 업로드 상태 추적
API 엔드포인트
파일 업로드
직접 업로드
POST /v1/files/upload
Content-Type: multipart/form-data
entityType: EXAM_PAPER
entityId: 123
file: [파일 데이터]
응답:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"entityType": "EXAM_PAPER",
"entityId": 123,
"originalFilename": "exam.pdf",
"contentType": "application/pdf",
"fileSize": 1048576,
"uploadCompleted": true,
"createdAt": "2024-01-15T10:30:00Z"
}
Presigned URL 생성
POST /v1/files/presigned-upload
Content-Type: application/json
{
"entityType": "EXAM_PAPER",
"entityId": 123,
"filename": "exam.pdf",
"contentType": "application/pdf",
"fileSize": 1048576
}
응답:
{
"fileId": "550e8400-e29b-41d4-a716-446655440000",
"uploadUrl": "https://minio.example.com/bucket/path?signature=...",
"expiresInSeconds": 3600
}
업로드 완료 등록
POST /v1/files/register-upload
Content-Type: application/json
{
"fileId": "550e8400-e29b-41d4-a716-446655440000"
}
파일 다운로드
직접 다운로드
GET /v1/files/{fileId}/download
응답:
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 1048576
Content-Disposition: attachment; filename="exam.pdf"
[파일 데이터]
Presigned URL 생성
POST /v1/files/presigned-download
Content-Type: application/json
{
"fileId": "550e8400-e29b-41d4-a716-446655440000"
}
응답:
{
"fileId": "550e8400-e29b-41d4-a716-446655440000",
"downloadUrl": "https://minio.example.com/bucket/path?signature=...",
"filename": "exam.pdf",
"contentType": "application/pdf",
"expiresInSeconds": 3600
}
메타데이터 조회
파일 정보 조회
GET /v1/files/{fileId}
엔티티별 파일 목록
GET /v1/files/entity/{entityType}/{entityId}
파일 삭제
DELETE /v1/files/{fileId}
도메인 모델
FileMetadata 엔티티
@Entity
@Table(name = "file_metadata")
public class FileMetadata extends BaseEntity {
@Id
private UUID id;
@Enumerated(EnumType.STRING)
private EntityType entityType;
private Long entityId;
private String originalFilename;
private String storedFilename;
private String contentType;
private Long fileSize;
private String objectKey;
private boolean uploadCompleted;
}
EntityType 열거형
파일이 연결될 수 있는 엔티티 타입을 정의합니다:
public enum EntityType {
EXAM_PAPER("exam-papers"),
STUDENT_ANSWER("student-answers"),
ACADEMY_LOGO("academy-logos"),
USER_PROFILE("user-profiles");
private final String pathSegment;
}
FilePath 값 객체
MinIO 오브젝트 키 생성을 담당합니다:
public record FilePath(EntityType entityType, UUID fileId, String filename) {
public String toObjectKey() {
String tenantSlug = TenantContextHolder.getRequiredTenant();
String tenantId = extractTenantId(tenantSlug);
return tenantId + "/" + entityType.getPathSegment() + "/" + filename;
}
}
스토리지 구조
MinIO 버킷 구조
lumie/
├── c704d223/ # 테넌트 ID
│ ├── exam-papers/
│ │ ├── exam_001.pdf
│ │ └── exam_002.pdf
│ ├── student-answers/
│ │ ├── answer_001.jpg
│ │ └── answer_002.jpg
│ └── academy-logos/
│ └── logo.png
└── a1b2c3d4/ # 다른 테넌트
└── ...
파일명 생성 규칙
- 저장 파일명:
UUID + 확장자(예:550e8400-e29b-41d4-a716-446655440000.pdf) - 오브젝트 키:
{tenantId}/{entityType}/{storedFilename}
멀티테넌시
RLS 기반 데이터 격리
file_metadata 테이블은 tenant_id 컬럼과 RLS 정책으로 테넌트별로 격리됩니다. 별도 스키마를 사용하지 않습니다.
컨텍스트 관리
JwtAuthenticationFilter가 JWT를 검증하고 TenantContextHolder와 UserContextHolder에 테넌트·사용자 정보를 설정합니다. 이후 RequestContextFilter(libs/common)가 SecurityContext에서 해당 정보를 확인하고 MDC에 로깅 컨텍스트를 설정합니다.
JWT가 없는 요청의 경우 RequestContextFilter는 X-Tenant-Slug, X-Tenant-Id, X-User-Id, X-User-Role 헤더에서 폴백으로 컨텍스트를 추출합니다.
자세한 흐름은 멀티테넌시 문서의 컨텍스트 전파 섹션을 참고하세요.
모듈 간 연동
Tenant Service 연동
TenantService를 주입받아 in-process 메서드 호출로 테넌트 유효성을 검증합니다:
@Component
@RequiredArgsConstructor
public class TenantInternalAdapter implements ValidateTenantPort {
private final TenantService tenantService;
@Override
public boolean validateTenant(String slug) {
var result = tenantService.validateTenant(slug);
return result.valid();
}
}
설정
file-svc는 독립적인 설정 파일이 없습니다. 모든 설정은 app/src/main/resources/application.yaml에서 통합 관리됩니다.
# app/src/main/resources/application.yaml (관련 설정)
minio:
endpoint: ${MINIO_ENDPOINT:http://localhost:9000}
external-endpoint: ${MINIO_EXTERNAL_ENDPOINT:http://localhost:9000}
access-key: ${MINIO_ACCESS_KEY:minioadmin}
secret-key: ${MINIO_SECRET_KEY:minioadmin}
bucket-name: ${MINIO_BUCKET_NAME:lumie-files}
spring:
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
file-size-threshold: 5MB
예외 처리
도메인 예외
public enum FileErrorCode implements ErrorCode {
// 400 Bad Request
INVALID_FILE_TYPE("FILE_001", "지원하지 않는 파일 형식입니다", 400),
FILE_SIZE_EXCEEDED("FILE_002", "파일 크기가 제한을 초과했습니다", 400),
// 404 Not Found
FILE_NOT_FOUND("FILE_003", "파일을 찾을 수 없습니다", 404),
// 500 Internal Server Error
STORAGE_ERROR("FILE_004", "스토리지 오류가 발생했습니다", 500),
PRESIGNED_URL_GENERATION_FAILED("FILE_005", "Presigned URL 생성에 실패했습니다", 500);
}
글로벌 예외 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
ErrorCode errorCode = ex.getErrorCode();
return ResponseEntity
.status(errorCode.getStatus())
.body(new ErrorResponse(
errorCode.getCode(),
ex.getMessage(),
LocalDateTime.now()
));
}
}
보안
인증/인가
- Spring Security
JwtAuthenticationFilter를 통한 JWT 검증 TenantContextHolder에서 테넌트 정보 추출- 인증된 요청만 파일 업로드/다운로드 허용
파일 접근 제어
- 테넌트별 파일 격리
- 엔티티 소유권 검증 (향후 구현 예정)
모니터링
헬스체크
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
endpoint:
health:
show-details: always
probes:
enabled: true
로깅
구조화된 JSON 로그를 통한 추적:
{
"timestamp": "2024-01-15T10:30:00.000Z",
"level": "INFO",
"logger": "com.lumie.file.application.service.FileCommandService",
"message": "Uploaded file: 550e8400-e29b-41d4-a716-446655440000",
"tenantSlug": "inst-c704d223",
"tenantId": "1",
"userId": "123"
}
성능 최적화
연결 풀 설정
spring:
datasource:
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
Presigned URL 활용
대용량 파일의 경우 Presigned URL을 사용하여 서버 부하를 줄입니다:
- 업로드: 클라이언트 → MinIO 직접 업로드
- 다운로드: 클라이언트 → MinIO 직접 다운로드
- 만료 시간: 1시간 (3600초)
배포
file-svc는 독립적으로 배포되지 않습니다. lumie-backend.jar에 포함되어 단일 Spring Boot 애플리케이션으로 배포됩니다.
- 네임스페이스:
lumie-backend - Replica: 3
- 포트: 8080 (HTTP only)
빌드 및 배포 방법은 인프라 문서를 참고하세요.