[스프링/Spring] MultipartFile 기본 개념과 파일 업로드

dongbrown·2025년 7월 21일

Spring

목록 보기
22/23

웹 개발을 하다 보면 파일 업로드 기능을 구현해야 하는 경우가 많습니다. Spring Framework에서는 이를 위해 MultipartFile 인터페이스를 제공하는데, 오늘은 이에 대해 자세히 알아보겠습니다.

1. MultipartFile이란?

MultipartFile은 Spring Framework에서 제공하는 인터페이스로, HTTP 멀티파트 요청으로 업로드된 파일을 처리하기 위한 객체입니다.

1.1 멀티파트 요청이란?

웹에서 파일을 업로드할 때 사용하는 Content-Type: multipart/form-data 형식의 HTTP 요청을 말합니다.

<!-- HTML 폼 예시 -->
<form method="post" enctype="multipart/form-data" action="/upload">
    <input type="text" name="title" placeholder="제목">
    <input type="file" name="uploadFile">
    <button type="submit">업로드</button>
</form>

1.2 MultipartFile의 주요 특징

  • 메타데이터 포함: 원본 파일명, 크기, MIME 타입 등의 정보
  • 임시 저장: 메모리나 임시 디스크 공간에 저장
  • 안전한 처리: Spring이 자동으로 리소스 관리

2. MultipartFile 주요 메서드

public interface MultipartFile extends InputStreamSource {
    String getName();                    // 폼 필드명
    String getOriginalFilename();        // 원본 파일명
    String getContentType();            // MIME 타입
    boolean isEmpty();                  // 파일이 비어있는지 확인
    long getSize();                     // 파일 크기(바이트)
    byte[] getBytes() throws IOException;        // 파일 내용을 바이트 배열로
    InputStream getInputStream() throws IOException; // 파일 내용을 스트림으로
    void transferTo(File dest) throws IOException;   // 파일을 지정 위치에 저장
}

2.1 실제 사용 예시

@PostMapping("/upload")
public String handleFileUpload(@RequestParam("uploadFile") MultipartFile file) {
    if (!file.isEmpty()) {
        System.out.println("파일명: " + file.getOriginalFilename());
        System.out.println("파일 크기: " + file.getSize() + " bytes");
        System.out.println("MIME 타입: " + file.getContentType());

        // 파일 저장
        try {
            file.transferTo(new File("/uploads/" + file.getOriginalFilename()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return "success";
}

3. transferTo() 메서드 심화 이해

transferTo(File dest) 메서드는 업로드된 파일을 실제 디스크에 저장하는 핵심 메서드입니다.

3.1 내부 동작 원리

// 간단한 사용법
MultipartFile file = ...;
File destFile = new File("/path/to/save/filename.pdf");
file.transferTo(destFile);

내부에서 일어나는 일:

  1. 메모리 저장된 경우: 메모리의 데이터를 직접 파일에 쓰기
  2. 임시 파일 저장된 경우: 임시 파일을 목적지로 이동 (더 효율적)

3.2 transferTo() 사용시 주의사항

@Service
public class FileService {

    public void saveFile(MultipartFile file) throws IOException {
        File saveDir = new File("/uploads");

        // 1. 디렉토리가 없으면 생성
        if (!saveDir.exists()) {
            saveDir.mkdirs();
        }

        // 2. 파일명 중복 체크 및 처리
        String fileName = file.getOriginalFilename();
        File destFile = new File(saveDir, fileName);

        if (destFile.exists()) {
            // 중복 파일명 처리 로직
            fileName = System.currentTimeMillis() + "_" + fileName;
            destFile = new File(saveDir, fileName);
        }

        // 3. 파일 저장 (한 번만 호출 가능!)
        file.transferTo(destFile);

        // 4. transferTo() 이후에는 더 이상 파일에 접근 불가
        // file.getBytes(); // 예외 발생!
    }
}

⚠️ 중요한 제약사항:

  • transferTo()한 번만 호출 가능
  • 호출 후에는 다른 메서드(getBytes(), getInputStream() 등) 사용 불가
  • 목적지 디렉토리가 존재하지 않으면 예외 발생

4. 실전 예시: 안전한 파일 업로드

실제 프로덕션 환경에서 사용할 수 있는 안전한 파일 업로드 예시를 보겠습니다.

@RestController
@RequestMapping("/api/files")
public class FileUploadController {

    @Value("${app.upload.path:/tmp/uploads}")
    private String uploadPath;

    @PostMapping("/upload")
    public ResponseEntity<?> uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "category", defaultValue = "general") String category) {

        try {
            // 1. 기본 검증
            if (file.isEmpty()) {
                return ResponseEntity.badRequest().body("파일이 비어있습니다.");
            }

            // 2. 파일 크기 검증 (10MB 제한)
            if (file.getSize() > 10 * 1024 * 1024) {
                return ResponseEntity.badRequest().body("파일 크기가 10MB를 초과합니다.");
            }

            // 3. 파일 확장자 검증
            String originalFileName = file.getOriginalFilename();
            if (!isValidFileExtension(originalFileName)) {
                return ResponseEntity.badRequest().body("허용되지 않은 파일 형식입니다.");
            }

            // 4. 안전한 파일명 생성
            String safeFileName = generateSafeFileName(originalFileName);

            // 5. 업로드 디렉토리 생성
            Path uploadDir = Paths.get(uploadPath, category);
            Files.createDirectories(uploadDir);

            // 6. 파일 저장
            Path filePath = uploadDir.resolve(safeFileName);
            file.transferTo(filePath.toFile());

            // 7. 응답 데이터 생성
            Map<String, Object> response = new HashMap<>();
            response.put("originalFileName", originalFileName);
            response.put("storedFileName", safeFileName);
            response.put("filePath", filePath.toString());
            response.put("fileSize", file.getSize());
            response.put("contentType", file.getContentType());

            return ResponseEntity.ok(response);

        } catch (IOException e) {
            return ResponseEntity.status(500).body("파일 저장 중 오류가 발생했습니다.");
        }
    }

    private boolean isValidFileExtension(String fileName) {
        if (fileName == null) return false;

        String extension = fileName.toLowerCase();
        return extension.endsWith(".jpg") ||
               extension.endsWith(".jpeg") ||
               extension.endsWith(".png") ||
               extension.endsWith(".pdf") ||
               extension.endsWith(".doc") ||
               extension.endsWith(".docx");
    }

    private String generateSafeFileName(String originalFileName) {
        // UUID + 타임스탬프로 안전한 파일명 생성
        String extension = "";
        if (originalFileName != null && originalFileName.contains(".")) {
            extension = originalFileName.substring(originalFileName.lastIndexOf("."));
        }
        return UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + extension;
    }
}

5. 다중 파일 업로드

여러 파일을 동시에 업로드하는 경우의 처리 방법입니다.

@PostMapping("/upload/multiple")
public ResponseEntity<?> uploadMultipleFiles(
        @RequestParam("files") List<MultipartFile> files) {

    List<Map<String, Object>> results = new ArrayList<>();

    for (MultipartFile file : files) {
        if (file.isEmpty()) continue;

        try {
            // 각 파일별로 저장 처리
            String fileName = generateSafeFileName(file.getOriginalFilename());
            Path filePath = Paths.get(uploadPath, fileName);
            file.transferTo(filePath.toFile());

            Map<String, Object> fileInfo = new HashMap<>();
            fileInfo.put("originalFileName", file.getOriginalFilename());
            fileInfo.put("storedFileName", fileName);
            fileInfo.put("fileSize", file.getSize());
            fileInfo.put("status", "SUCCESS");

            results.add(fileInfo);

        } catch (IOException e) {
            Map<String, Object> errorInfo = new HashMap<>();
            errorInfo.put("originalFileName", file.getOriginalFilename());
            errorInfo.put("status", "FAILED");
            errorInfo.put("error", e.getMessage());

            results.add(errorInfo);
        }
    }

    return ResponseEntity.ok(results);
}

다음 편 예고

2편에서는 더 고급 기능들을 다룰 예정입니다:

  • 실제 비디오 처리 서비스 구현 분석
  • Files.copy() vs transferTo() 성능 비교
  • 대용량 파일 처리 전략
  • 파일 메타데이터 추출
  • 보안 강화 방법

파일 업로드는 웹 애플리케이션의 핵심 기능 중 하나입니다. 다음 편에서는 제공해주신 실제 코드를 분석하며 더 실용적인 내용을 다뤄보겠습니다!

0개의 댓글