Backend/Spring / / 2025. 7. 2. 10:54

[Spring Boot] 파일 업로드 & 다운로드 구현

📌 단순 기능? 아니죠, 이것도 "설계"입니다.

Spring Boot에서 파일 업로드와 다운로드 기능은 흔하게 보이지만, 막상 제대로 설계하고 구현하려면 고려할 게 많습니다.
단순히 MultipartFile을 받는다고 끝이 아니며, 보안, 경로 설계, 예외처리, 응답 헤더 구성 등 세세한 이슈들이 얽혀 있습니다.


☑️ 파일 업로드: MultipartFile의 세계

Spring Boot에서는 업로드를 위해 클라이언트가 multipart/form-data 형식으로 요청을 보내야 합니다. 이를 컨트롤러에서 MultipartFile로 받아 처리합니다.

✅ 코드 예시: 업로드 컨트롤러

@PostMapping("/upload")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
    if (file.isEmpty()) {
        return ResponseEntity.badRequest().body("파일이 비어 있습니다.");
    }

    try {
        String uploadDir = "uploads/";
        String filePath = uploadDir + file.getOriginalFilename();

        File dest = new File(filePath);
        file.transferTo(dest);

        return ResponseEntity.ok("업로드 성공: " + file.getOriginalFilename());
    } catch (IOException e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                             .body("업로드 실패: " + e.getMessage());
    }
}

💡 설계 포인트

  • uploadDir은 application.yml 또는 application.properties에서 외부 설정으로 분리하는 것이 이상적입니다.
  • 중복 파일 처리는 UUID로 이름을 치환하거나, DB 연동을 통해 관리하는 방식이 일반적입니다.
  • 업로드 경로는 절대 static 리소스와 같은 공개 경로에 두지 마세요. 보안에 취약합니다.

☑️ 파일 다운로드: Content-Disposition의 마법

파일 다운로드는 단순한 링크 응답이 아닌, 명확한 헤더 구성과 파일 스트리밍 처리가 핵심입니다.

✅ 코드 예시: 다운로드 컨트롤러

@GetMapping("/download/{fileName:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
    try {
        Path filePath = Paths.get("uploads").resolve(fileName).normalize();
        Resource resource = new UrlResource(filePath.toUri());

        if (!resource.exists()) {
            return ResponseEntity.notFound().build();
        }

        String contentType = Files.probeContentType(filePath);
        contentType = contentType != null ? contentType : "application/octet-stream";

        return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType(contentType))
            .header(HttpHeaders.CONTENT_DISPOSITION,
                    "attachment; filename=\"" + resource.getFilename() + "\"")
            .body(resource);
    } catch (IOException e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

💡 설계 포인트

  • @PathVariable에서 정규식을 통해 확장자 포함을 명시 ({fileName:.+})해야 합니다.
  • Content-Disposition 헤더를 통해 다운로드를 강제할 수 있으며, UTF-8 파일명을 지원하려면 별도의 인코딩 처리 필요합니다.
  • 파일명이 노출되므로, **임의 경로 조작 방지 (.. 등)**를 필터링하거나 정규화(normalize) 필수입니다.

🔐 보안 고려 사항

항목설명
경로 탐색 공격 ../../ 같은 상대경로 접근 시 파일 유출 위험. normalize()나 화이트리스트 검증 필요
파일 크기 제한 spring.servlet.multipart.max-file-size 설정 필요
MIME 타입 검증 Files.probeContentType()과 화이트리스트 조합 사용
권한 관리 파일 업로드/다운로드는 인증된 사용자로 제한하고, 소유자 확인 로직 필수
 

📁 application.yml 설정 예시

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 15MB

🧠 마무리하며: 실무에서는 어떻게 쓰이는가?

실제 서비스에서는 업로드된 파일을 S3, FTP, 별도 정적 서버에 올리고, 메타 정보는 DB로 관리합니다.
Spring Boot 내에서만 처리하는 예시는 로컬 개발 단계나 간단한 내부 툴에서 주로 사용됩니다.

그리고 "단순 저장"이 아닌 "파일 시스템 설계"라는 관점에서 접근하면, 프로젝트의 품질이 확 달라집니다.
보안, 유지보수, 성능까지 고려한 구조가 결국 실무의 방향입니다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유