웹 개발을 하다 보면 파일 업로드 기능을 구현해야 하는 경우가 많습니다. Spring Framework에서는 이를 위해 MultipartFile 인터페이스를 제공하는데, 오늘은 이에 대해 자세히 알아보겠습니다.
MultipartFile은 Spring Framework에서 제공하는 인터페이스로, HTTP 멀티파트 요청으로 업로드된 파일을 처리하기 위한 객체입니다.
웹에서 파일을 업로드할 때 사용하는 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>
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; // 파일을 지정 위치에 저장
}
@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";
}
transferTo(File dest) 메서드는 업로드된 파일을 실제 디스크에 저장하는 핵심 메서드입니다.
// 간단한 사용법
MultipartFile file = ...;
File destFile = new File("/path/to/save/filename.pdf");
file.transferTo(destFile);
내부에서 일어나는 일:
@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() 등) 사용 불가실제 프로덕션 환경에서 사용할 수 있는 안전한 파일 업로드 예시를 보겠습니다.
@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;
}
}
여러 파일을 동시에 업로드하는 경우의 처리 방법입니다.
@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편에서는 더 고급 기능들을 다룰 예정입니다:
파일 업로드는 웹 애플리케이션의 핵심 기능 중 하나입니다. 다음 편에서는 제공해주신 실제 코드를 분석하며 더 실용적인 내용을 다뤄보겠습니다!