파일 서빙 전략
1. 주체별 분류
누가 실제로 클라이언트에 파일을 보내주는가?
분류 | 설명 | 예시 |
Java 직접 응답 | Java에서 InputStream → OutputStream으로 전달 | 바이트 스트림 방식 |
Nginx 정적 서빙 | Nginx가 파일 직접 서빙 | /files/image.jpg |
Java + Nginx | Java가 인증 등 처리 후 Nginx에 서빙 위임 (X-Accel-Redirect 헤더 사용) | X-Accel-Redirect |
CDN | 글로벌 캐시 서버가 등록된 파일을 대신해서 전송해주는 전송 대행 서비스 | CloudFront, Cloudflare |
주체별 분류 : Java 직접 응답
- 바이트 스트림 방식 (Java 등에서 직접 처리)
- Java (Spring 등)에서 InputStream을 통해 파일을 직접 읽어서, OutputStream에 바이트로 써주는 방식입니다.
- 장점
- 동적으로 처리 가능하여 세밀한 제어 가능
- 다운로드 전에 동적으로 파일 생성 후 응답 가능 → 예: Excel, PDF 등을 동적으로 만들어서 응답 가능
- 별도의 웹서버 설정 없이 바로 Spring에서 구현 가능
- 동적으로 처리 가능하여 세밀한 제어 가능
- 단점
- Java 서버의 리소스를 소모 (CPU, I/O, 메모리)
- 대용량 파일일 경우 성능 저하 또는 OutOfMemory 우려
@GetMapping("/download")
public void download(HttpServletResponse response) throws IOException {
File file = new File("path/to/file.mp4");
try (InputStream in = new FileInputStream(file);
OutputStream out = response.getOutputStream()) {
response.setContentType("video/mp4");
response.setHeader("Content-Disposition", "attachment; filename=\"file.mp4\"");
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
}
주체별 분류 : Nginx를 통한 Static 파일 서빙
- Java는 파일 경로(URL)만 내려주고, Nginx가 직접 파일을 클라이언트에게 전송합니다.
- 장점
- 빠름, 동시 접속 처리에 강함 (Nginx는 정적 파일 처리에 특화)
- 별도 서버 리소스를 거의 사용하지 않음
- 단점
- 동적 제어가 어려움 (권한 검사, 다운로드 카운트 등)
- 보안 제어는 Java가 아니라 Nginx/파일 시스템 수준에서 해야 함
Spring에서는 URL만 응답:
@GetMapping("/file-url")
public String fileUrl() {
return "http://yourdomain.com/files/video.mp4";
}
Nginx 설정
location /files/ {
alias /var/www/files/;
}
주체별 분류 : Java + Nginx 위임 방식 (X-Accel-Redirect)
Java가 먼저 권한 검사 등 비즈니스 로직을 처리한 후, 클라이언트에게 직접 파일을 전송하는 대신, Nginx에게 "이 파일 대신 보내줘"라고 위임하는 방식이야.
✅ 장점
- Java에서 보안 체크(로그인, 권한 등) 가능
- Nginx의 빠른 정적 파일 처리 성능 활용 가능
- Java 메모리/CPU 사용 없이 파일 전송 처리 가능
❌ 단점
- X-Accel-Redirect를 위한 Nginx 설정 필요
- 직접 파일 응답하지 않기 때문에 로깅/카운팅 등은 Java에서 추가 처리 필요
Java 예시 (Spring)
@GetMapping("/secure-download")
public ResponseEntity<Void> download(HttpServletResponse response) {
// 인증/권한 체크 후
response.setHeader("X-Accel-Redirect", "/internal/files/video.mp4");
response.setHeader("Content-Disposition", "attachment; filename=\"video.mp4\"");
return ResponseEntity.ok().build();
}
Nginx 설정 예시
location /internal/files/ {
internal;
alias /var/www/files/;
}
internal; 설정은 직접 접근 금지, 오직 X-Accel-Redirect로만 접근 가능하게 함
2. 파일 위치 기준
어디에 저장되어 있는가?
분류 | 설명 | 예시 |
서버 로컬 저장소 | 서버 디스크에 파일 있음 | /var/www/files/ |
NAS / NFS 공유 저장소 | 다수 서버 간 공유 스토리지 | SMB, NFS |
Cloud Object Storage | 클라우드 버킷 | AWS S3, GCS |
환경 | 일반적인 선택 | 이유 |
개인/스타트업/내부 서비스 | Nginx (클라우드에 설치된 서버) | 빠르고 직접 관리 가능, 비용 없음 |
중대형 서비스, 다국적 서비스 | S3 + CDN (CloudFront 등) | 확장성, 전세계 속도, 캐싱, 보안 등에서 탁월 |
사내 전용 웹, 관리자 포털 | Nginx (VM 또는 EC2 내부) | 외부에 공개되지 않으므로 보안·성능 크게 요구되지 않음 |
파일서빙 전략이 백엔드 관점에서 유의미한가?
1️⃣ API 설계에 직접적인 영향을 줘요
- 단순 GET /file/{id}로 끝날 일이 아님
- Java가 직접 줄지, Presigned URL 줄지, Nginx로 넘길지 결정해야 함
2️⃣ 보안 정책 설계에 영향
- 정적 파일 직접 노출 vs 인증된 사용자만 접근
- X-Accel-Redirect, signed URL 등 도입 여부가 여기서 갈림
3️⃣ 서버 운영/배포 전략에 영향
- 파일을 로컬에 둘지, 공유 저장소 쓸지, S3에 둘지 결정
- 이건 곧 백엔드 인프라 구조 설계의 일부
4️⃣ 성능/확장성 고려에 핵심
- Java가 파일 계속 읽으면 GC, I/O 병목 생김
- 대규모 서비스에서 무조건 CDN, S3 고려해야 함
파일 서빙 표준
파일 서빙 표준 : 파일 메타데이터를 DB에 저장
파일시스템 구조는 파일 자체는 파일 시스템(NAS, S3 등)에 저장하고, DB에는 해당 파일의 메타데이터 + 저장된 파일의 경로(URL)를 저장하는 구조
1. 🔐 실제 저장 위치 (예: 리눅스 디렉토리 or S3)
/var/www/files/123_company_report.pdf
2. 🗃️ DB에 저장되는 정보 예시
컬럼명 | 예시 |
report_id | 123 |
company_id | 77 |
file_name | company_report_q1.pdf |
file_path | /internal-files/123_company_report.pdf |
uploaded_by | user123 |
created_at | 2024-04-15 |
→ 📌 DB에는 실제 파일 내용은 없고,
→ 파일이 저장된 경로(URL 또는 internal path)를 저장합니다.
3. 메타데이터 저장이유
- 정책 바뀌어서 경로가 /2024/회사명/보고서.pdf → /보고서자료/회사id-파일id.pdf로 바뀌는 경우
- 디렉토리 이동, 리네이밍, 마이그레이션 등으로 실제 경로가 바뀔 수 있음
- 규칙으로 계산하면 못 찾음, but DB에 저장돼 있으면 무조건 찾을 수 있음
- 경로를 계산해서 파일을 찾으면, DB 필터링, 정렬, 페이징 등 기본 기능을 못 씀
- "해당 회사의 최근 1개월 보고서 목록 보여줘" → 경로로 못 함
- "파일명 기준 정렬" → 경로로 못 함
- "부서별 통계 내기" → 불가능
4, 파일저장과 메타데이터의 일관성문제
- 파일저장은 성공하였지만 DB저장은 실패하는 경우가 있고 그 경우 파일은 고아상태가됨
- 파일 저장은 되돌릴 수 없으니, 실패 시 수동 롤백 처리를 한다.
try {
// 1. 파일 저장
String path = fileStorage.save(file); // → 파일은 이미 저장됨
// 2. 트랜잭션 시작
fileMetadataRepository.save(new FileEntity(path, ...)); // → DB 실패 가능성 있음
} catch (Exception e) {
fileStorage.delete(path); // ← 수동으로 삭제 (정합성 회복)
throw e;
}
- 서버의 장애가 발생하여 catch문까지도 못가면 고아파일이 생성됨
- 주기적 청소 배치 (Cron) : /tmp/uploads/ 아래 파일 중, 1시간 이상된 파일 중 DB에 없는 것을 주기적으로 삭제
파일 서빙 표준 : 파일 저장 경로와 URL 경로 분리
“파일 저장 경로”와 “파일 접근 URL”은 실제로는 별도로 관리되는 게 일반적이고 정석입니다.
개념 | 설명 | 예시 |
📁 파일 저장 경로 | OS 파일 시스템의 경로. Java가 직접 접근하고 저장 | /var/www/files/floorplan/uuid.png |
🌐 파일 접근 URL | 클라이언트가 브라우저 등으로 요청하는 주소 (Nginx가 서빙) | /static/floorplan/uuid.png |
이유 | 설명 |
💡 보안/구조 분리 | 저장 경로는 민감할 수 있고 노출되면 위험할 수 있음 |
🔄 유연한 URL 변경 가능 | URL 구조는 바뀔 수 있어도 저장 경로는 그대로 유지 가능 |
🔧 Nginx 등으로 추상화 가능 | URL은 Nginx 설정에 따라 언제든 경로 매핑 가능 |
📦 S3, NAS 등 외부 저장소 대응 | 저장 위치는 클라우드, URL은 CDN일 수 있음 |
실제 저장 경로 (Java 입장에서)
Path uploadDir = Paths.get(flowPlanPath, companyNo.toString());
// 예: /var/www/files/floorplan/39/floorplan_39.png
외부에 제공하는 논리 URL
String fileUrl = "/files/floorplan/" + companyNo + "/" + fileName;
// 예: /files/floorplan/39/floorplan_39.png
Nginx에서 반드시 필요한 설정
location /files/ {
alias /var/www/files/;
}
항목 | 설명 |
/files/ | 외부에서 접근할 논리 URL prefix |
/var/www/files/ | 실제 파일이 저장된 경로 (flowPlanPath와 연결됨) |
4. 파일처리 서버 고정을 위해 고정 라우팅 처리
파일시스템에 관련된 요청은 파일시스템에 접근이 되지 않는 서버에서 요청을 받아도 의미가 없다. 그러므로 파일관련된 서버를 별도로 할당하고 그 서버에서만 처리하도록 하는 고정라우팅 방식을 도입할 수 있다.
특정 API 요청 (예: 파일 다운로드) 에 대해서만 특정 서버(Nginx 1번 서버 등)만 응답하도록 분기하는 것은 실무에서 "고정 라우팅" 또는 "정적 콘텐츠 핀포인트 처리"라고 불립니다.
[Client] ─▶ /api/... → 로드밸런서 → 모든 서버에 분산
─▶ /files/download → 로드밸런서 → 특정 서버로만 전달
로드밸런서(또는 Nginx 앞단)에서 경로 분기
예: AWS ALB, Nginx, HAProxy, Kubernetes Ingress 등
# 예시: 특정 URI는 특정 서버로 보냄
map $request_uri $file_server {
default backend_all;
~^/files/ backend_file; # 파일 요청은 파일 서버로만
}
upstream backend_all {
server app1;
server app2;
server app3;
}
upstream backend_file {
server file-server-only; # 오직 이 서버만 파일 보유
}
server {
location / {
proxy_pass http://$file_server;
}
}
✅ 장점
항목 | 설명 |
✅ 확장성 보장 | 파일 요청만 특정 서버로 집중 → 나머지 서버는 로직 처리 전담 |
✅ 복제 불필요 | 파일 서버 1대만 파일 갖고 있으면 됨 |
✅ 캐시 및 정적 최적화 집중 가능 | 특정 서버만 정적 최적화, SSD 고속 구성 등 가능 |
⚠️ 단점 / 주의할 점
항목 | 설명 |
❗ 파일 서버 죽으면 요청 실패 | 고가용성 고려 필요 (backup 서버, 헬스체크 등) |
❗ 네트워크 경로 조건이 복잡해질 수 있음 | 로드밸런서/프록시 설정이 정교해야 함 |
❗ 파일 처리 로직은 반드시 해당 서버에서만 동작해야 함 | Java → local save → 해당 서버에만 남음 |
5. 파일 업로드 구현
파일 업로드도 본질적으로는 일반적인 HTTP POST 요청입니다. 하지만 header와 body 포맷이 다르고, Java (Spring 등)는 이를 위해 Multipart 처리 방식을 제공합니다.
항목 | HTTP Multipart (Spring, REST API) | FTP |
📦 편의성 | 클라이언트(웹, 앱, Postman)에서 바로 사용 가능 | 별도 FTP 클라이언트 필요 |
🔐 보안 | HTTPS 기반 → 암호화 + 인증 편리 | FTP는 평문 전송 (FTPS/SFTP 필요) |
🧩 API 통합성 | 기존 REST API 흐름과 통합 가능 (권한, DB 저장 등) | 단순 파일 송수신만 가능 |
🚀 배포/운영 호환 | nginx, 웹서버, API 서버와 자연스럽게 연동 | 별도 포트, 데몬, 서비스 운영 필요 |
🌐 브라우저 호환성 | <form enctype="multipart/form-data"> | 웹 브라우저에서 불가능 |
클라이언트의 파일 업로드 요청 (Multipart)
POST /api/files/upload
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="abc.pdf"
Content-Type: application/pdf
(binary file content)
------WebKitFormBoundary7MA4YWxkTrZu0gW--
Spring 서버 내 동작방식
내부적으로는 CommonsMultipartResolver 또는 StandardServletMultipartResolver가 multipart body를 파싱해서 MultipartFile 객체로 만들어줍니다. 이 과정에서 Content-Type이 multipart/form-data가 아니거나 형식이 틀리면,
Spring 또는 서블릿 컨테이너가 400 Bad Request로 자동 차단해 줍니다
@PostMapping("/upload")
public ResponseEntity<?> upload(@RequestParam MultipartFile file) {
// ✅ Spring이 multipart/form-data를 자동 파싱
// ✅ file.getOriginalFilename(), file.getBytes() 등으로 사용 가능
}
'개발기술 > Web Dev' 카테고리의 다른 글
Static File Upload (0) | 2025.04.12 |
---|---|
Tomat And Netty (0) | 2025.04.06 |
NGNIX 사용 (0) | 2025.03.07 |
RESTFUL API 설계 (0) | 2024.12.15 |
Interception , Filter (0) | 2024.09.05 |