본문 바로가기

개발기술/Web Dev

파일서빙 전략, 표준 그리고 구현

파일 서빙 전략

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