파일 업로드
- 김영한님의 스프링 MVC 2편 강의를 통해 파일 업로드의 원리(
multipart/form-data)를 이해하고, 서블릿과 스프링이 제공하는 각각의 업로드 처리 방식을 비교하며, 실제 웹 애플리케이션에서 파일 업로드와 다운로드를 구현하는 방법을 정리함
파일 업로드 소개
HTML Form 전송 방식
-
HTML Form을 통한 파일 업로드를 이해하려면 두 가지 전송 방식의 차이를 이해해야 함
application/x-www-form-urlencodedmultipart/form-data
application/x-www-form-urlencoded 방식
- 개념
- HTML 폼 데이터를 서버로 전송하는 가장 기본적인 방법
- Form 태그에 별도의 enctype 옵션이 없으면 기본값
-
HTTP 메시지 헤더
1
Content-Type: application/x-www-form-urlencoded -
HTTP Body
1
username=kim&age=20
- 특징
- 폼에 입력한 항목을 문자로 전송
&로 구분- URL 인코딩 처리

- 파일 업로드의 문제점
- 파일은 문자가 아닌 바이너리 데이터임
- 문자 전송 방식으로 파일 전송 어려움
- 이름, 나이(문자) + 첨부파일(바이너리)을 동시에 전송해야 함
- 문자와 바이너리를 동시에 전송 불가능함
multipart/form-data 방식
- 개념
- 문자와 바이너리를 동시에 전송하기 위한 방식
- 여러 종류의 파일과 폼 내용을 함께 전송 가능
-
Form 태그 설정
1
<form enctype="multipart/form-data">
-
HTTP 메시지 구조
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
POST /upload HTTP/1.1 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary ------WebKitFormBoundary Content-Disposition: form-data; name="username" kim ------WebKitFormBoundary Content-Disposition: form-data; name="age" 20 ------WebKitFormBoundary Content-Disposition: form-data; name="file"; filename="test.png" Content-Type: image/png 바이너리 데이터... ------WebKitFormBoundary--
- 구조 분석
- 각 항목이
boundary로 구분됨 - 각 항목마다
Content-Disposition헤더가 추가됨 - 파일은 파일명과
Content-Type이 추가됨 - 일반 데이터는 문자, 파일은 바이너리로 전송됨
- 각 항목이

두 방식 비교
| 구분 | application/x-www-form-urlencoded | multipart/form-data |
|---|---|---|
| 용도 | 일반 폼 데이터 | 파일 업로드 + 폼 데이터 |
| 데이터 형식 | 문자만 가능 | 문자 + 바이너리 |
| 인코딩 | URL 인코딩 | Part별 인코딩 |
| 구분자 | & | boundary |
| 파일 업로드 | 불가능 | 가능 |
| 복잡도 | 단순 | 복잡 |
서블릿과 파일 업로드
ServletUploadControllerV1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@Controller
@RequestMapping("/servlet/v1")
public class ServletUploadControllerV1 {
@PostMapping("/upload")
public String saveFileV1(HttpServletRequest request)
throws ServletException, IOException {
log.info("request={}", request);
String itemName = request.getParameter("itemName");
log.info("itemName={}", itemName);
Collection<Part> parts = request.getParts();
log.info("parts={}", parts);
return "upload-form";
}
}
-
request.getParts()- multipart/form-data 전송 방식에서 각각 나누어진 부분을 받아서 확인함
멀티파트 설정 옵션
-
업로드 사이즈 제한
1 2
spring.servlet.multipart.max-file-size=1MB spring.servlet.multipart.max-request-size=10MB
max-file-size- 파일 하나의 최대 사이즈 (기본 1MB)
max-request-size- 멀티파트 요청 하나에 여러 파일 업로드 시 전체 합 (기본 10MB)
- 사이즈 초과 시
SizeLimitExceededException발생
-
멀티파트 기능 끄기
1
spring.servlet.multipart.enabled=false
- 서블릿 컨테이너가 멀티파트 처리를 하지 않음
request.getParameter("itemName")-> nullrequest.getParts()-> 빈 배열
-
멀티파트 기능 켜기 (기본값)
1
spring.servlet.multipart.enabled=true
HttpServletRequest->StandardMultipartHttpServletRequest로 변경됨-
복잡한 멀티파트 요청을 처리해서 제공함

MultipartResolver
- 동작 원리
spring.servlet.multipart.enabled=true설정DispatcherServlet에서MultipartResolver실행- 멀티파트 요청인 경우
HttpServletRequest를MultipartHttpServletRequest로 변환
파일 저장과 Part 주요 메서드
- Part 주요 메서드
part.getSubmittedFileName()- 클라이언트가 전달한 파일명
part.getInputStream()- Part의 전송 데이터를 읽을 수 있음
part.write(...)- Part를 통해 전송된 데이터를 저장
part.getName()- 파트 이름
part.getHeaderNames()- 헤더 이름들
part.getSize()- Part body 사이즈
스프링과 파일 업로드
MultipartFile 인터페이스
- 특징
- 스프링이 제공하는 멀티파트 파일 지원 인터페이스
- 서블릿의
Part보다 훨씬 편리하게 사용 가능
SpringUploadController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
@Controller
@RequestMapping("/spring")
public class SpringUploadController {
@Value("${file.dir}")
private String fileDir;
@PostMapping("/upload")
public String saveFile(@RequestParam String itemName,
@RequestParam MultipartFile file,
HttpServletRequest request) throws IOException {
if (!file.isEmpty()) {
String fullPath = fileDir + file.getOriginalFilename();
file.transferTo(new File(fullPath));
}
return "upload-form";
}
}
@RequestParam MultipartFile file- HTML Form의 name에 맞춰
@RequestParam을 적용하면 됨 @ModelAttribute에서도 동일하게 사용 가능함
MultipartFile 주요 메서드
| 메서드 | 설명 |
|---|---|
file.getOriginalFilename() |
업로드 파일명 |
file.transferTo(...) |
파일 저장 |
file.isEmpty() |
파일이 비어있는지 확인 |
file.getSize() |
파일 크기 |
file.getBytes() |
파일 내용을 byte[]로 반환 |
file.getInputStream() |
파일 내용을 InputStream으로 반환 |
예제로 구현하는 파일 업로드, 다운로드
요구사항
- 상품 관리
- 상품 이름
- 첨부파일 하나
- 이미지 파일 여러 개
- 기능
- 첨부파일 업로드/다운로드
- 업로드한 이미지를 웹 브라우저에서 확인
구현
-
Item - 상품 도메인
-
ItemRepository - 상품 저장소
-
UploadFile - 업로드 파일 정보
1 2 3 4 5 6 7 8 9 10
@Data public class UploadFile { private String uploadFileName; // 고객이 업로드한 파일명 private String storeFileName; // 서버 내부에서 관리하는 파일명 public UploadFile(String uploadFileName, String storeFileName) { this.uploadFileName = uploadFileName; this.storeFileName = storeFileName; } }
-
파일명 분리 이유
- 고객이 업로드한 파일명으로 서버에 저장하면 충돌이 발생할 수 있음
- 서버에서는 UUID 등을 사용하여 유일한 이름으로 저장해야 함

-
FileStore - 파일 저장 처리
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
@Component public class FileStore { @Value("${file.dir}") private String fileDir; public String getFullPath(String filename) { return fileDir + filename; } public UploadFile storeFile(MultipartFile multipartFile) throws IOException { if (multipartFile.isEmpty()) { return null; } String originalFilename = multipartFile.getOriginalFilename(); String storeFileName = createStoreFileName(originalFilename); multipartFile.transferTo(new File(getFullPath(storeFileName))); return new UploadFile(originalFilename, storeFileName); } // UUID로 서버 내부 파일명 생성 (확장자 유지) private String createStoreFileName(String originalFilename) { String ext = extractExt(originalFilename); String uuid = UUID.randomUUID().toString(); return uuid + "." + ext; } private String extractExt(String originalFilename) { int pos = originalFilename.lastIndexOf("."); return originalFilename.substring(pos + 1); } }
storeFile()- 업로드된 파일을 서버에 저장하고 업로드 파일 정보를 반환함
createStoreFileName()- 서버 내부에서 관리하는 파일명은 유일한 이름을 사용할 수 있도록 UUID를 사용해서 저장함
extractExt()- 확장자를 별도로 추출해서 서버 내부에서 관리하는 파일명에도 붙여줌
-
ItemForm - 상품 저장용 폼
1 2 3 4 5 6 7
@Data public class ItemForm { private Long itemId; private String itemName; private List<MultipartFile> imageFiles; // 다중 업로드 private MultipartFile attachFile; }
List<MultipartFile> imageFiles- 이미지 다중 업로드
MultipartFile attachFile- 단일 첨부파일
@ModelAttribute에서 사용 가능
-
ItemController - 다운로드 처리
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
@GetMapping("/attach/{itemId}") public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException { Item item = itemRepository.findById(itemId); String storeFileName = item.getAttachFile().getStoreFileName(); String uploadFileName = item.getAttachFile().getUploadFileName(); UrlResource resource = new UrlResource("file:" + fileStore.getFullPath(storeFileName)); // 한글 파일명 깨짐 방지 String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8); // 다운로드를 위한 헤더 설정 String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\""; return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition) .body(resource); }
-
Content-Disposition 헤더
- 이 헤더가 없으면 브라우저는 파일을 다운로드하지 않고 내용을 보여줌 (이미지 등)
attachment; filename="..."설정을 통해 다운로드 창이 뜨도록 할 수 있음
연습 문제
-
일반적인 HTML 폼 전송 방식(‘x-www-form-urlencoded’)과 파일 업로드 시 사용하는 방식(‘multipart/form-data’)의 주요 차이점은 무엇일까요?
a. 전자는 텍스트만, 후자는 바이너리 데이터를 포함할 수 있습니다.
x-www-form-urlencoded는 텍스트 기반 키-값 형식으로 전송하지만,multipart/form-data는 여러 파트로 나누어 텍스트와 바이너리 데이터를 함께 보낼 수 있기 때문임
-
HTML 폼을 통해 파일을 업로드하려면
<form>태그의enctype속성을 무엇으로 지정해야 할까요?a.
multipart/form-data- 파일을 포함한 폼 데이터를 전송하기 위해 HTTP 명세에서 정의한 표준 방식이 바로
multipart/form-data이기 때문임 - 다른 옵션들은 파일 전송에 적합하지 않음
- 파일을 포함한 폼 데이터를 전송하기 위해 HTTP 명세에서 정의한 표준 방식이 바로
-
Spring Boot 서블릿 환경에서
spring.servlet.multipart.enabled설정을false로 했을 때 발생하는 주요 결과는 무엇인가요?a. 멀티파트 요청 처리가 비활성화됩니다.
- 이 설정을
false로 하면 서블릿 컨테이너가 멀티파트 요청을 처리하지 않도록 설정됨 - 따라서 파일 데이터 등을 정상적으로 받을 수 없게 됨
- 이 설정을
-
스프링에서 파일 업로드를 처리할 때, 컨트롤러 메서드의 파라미터로 업로드된 파일을 편리하게 받기 위해 주로 어떤 인터페이스를 사용할까요?
a.
org.springframework.web.multipart.MultipartFile- 스프링은 개발자가 파일 업로드를 쉽게 다룰 수 있도록
MultipartFile인터페이스를 제공함 - 파일 정보와 내용을 편리하게 얻을 수 있음
- 스프링은 개발자가 파일 업로드를 쉽게 다룰 수 있도록
-
사용자가 업로드한 파일을 서버에 저장할 때, 원본 파일 이름 대신 서버에서 생성한 고유한 이름으로 저장하는 주된 이유는 무엇일까요?
a. 같은 이름의 파일 충돌을 방지하기 위해
- 사용자들이 같은 이름의 파일을 올릴 때 서로 덮어쓰거나 예상치 못한 문제가 생길 수 있음
- 고유한 이름은 충돌을 막아줌
요약 정리
- 파일 전송 방식 비교
application/x-www-form-urlencoded- 문자와 바이너리 동시 전송 불가능
multipart/form-data- Boundary를 사용하여 문자와 바이너리를 구분해 동시 전송 가능
- 서블릿과 스프링 파일 업로드 비교
- 서블릿
HttpServletRequest의getParts()를 사용하여 각 Part를 수동으로 처리해야 함
- 스프링
MultipartFile인터페이스를 제공하여 매우 편리하게 파일 업로드 처리 가능
- 서블릿
- 멀티파트 리졸버 (MultipartResolver)
DispatcherServlet에서 멀티파트 요청을 감지하고MultipartHttpServletRequest로 변환하여 처리를 지원함
- 파일 저장
- 파일명 분리
- 고객이 업로드한 파일명과 서버에 저장할 파일명을 분리해야 함
- UUID 사용
- 파일명 충돌 방지를 위해 서버 저장 파일명은 UUID를 사용
- 파일명 분리
- 파일 다운로드
- Content-Disposition 헤더
attachment; filename="..."설정을 통해 브라우저가 파일을 다운로드하도록 지정
- 파일명 인코딩
- 한글 등비영어권 파일명 깨짐 방지를 위해
UriUtils.encode사용 필수
- 한글 등비영어권 파일명 깨짐 방지를 위해
- Content-Disposition 헤더