
Spring MVC에서 파일 업로드를 이해할 때 가장 먼저 잡아야 하는 것은 새로운 문법 이름이 아니다.
이전글에서 문자열, 숫자,DTO를 받는 흐름을 먼저 봤다면, 이번에는 그 요청 처리 구조가 파일까지 어떻게 확장되는지를 보면 된다.
즉, 파일 업로드는 완전히 새로운 별개 기능이 아니라,Spring MVC가 요청값을 받는 구조 위에 파일 데이터가 추가된 것이라고 이해하면 된다.
그래서 이 글은 이전글의RequestParam,ModelAttribute,DTO바인딩 흐름 다음 단계로 이어진다.
이전글에서는 컨트롤러가 요청값을 어떻게 받는지 정리했다.
이번에는 그 구조 안에서 파일이 어떤 방식으로 들어오고, 컨트롤러가 그 파일을 어떻게 확인하고 다루는지를 차례대로 정리한다.
파일도 결국 브라우저가 서버로 보내는 요청 데이터이다.
이 점은 문자열이나 숫자와 같다.
다만 파일은 단순한 글자값 하나가 아니라 이름, 크기, 실제 내용까지 함께 움직여야 하므로 처리 방식이 더 복잡해진다.
즉, 문자열 요청과 파일 요청은 완전히 다른 세계가 아니다.
둘 다 컨트롤러 메서드의 매개변수로 들어오는 요청 데이터이다.
차이는 문자열은 비교적 가볍고 단순한 값이고, 파일은 실제 데이터 덩어리까지 같이 다뤄야 한다는 점이다.
그래서 파일 업로드를 이해할 때도 이전글에서 정리한Spring MVC요청 처리 구조를 그대로 떠올리면 된다.
이전글에서는RequestParam으로 값을 하나씩 받기도 했고,DTO나VO로 값을 묶어서 받기도 했다.
파일 업로드도 이 흐름을 그대로 이어 간다.
파일 하나를 직접 받을 수도 있고, 업로드용 객체 안에 파일 필드를 넣어서 함께 받을 수도 있다.
즉, 파일 업로드를 갑자기 새 문법 암기처럼 보지 말고, 요청값 받기의 확장판으로 이해하는 것이 가장 자연스럽다.
문자열이나 숫자는 보통 한 줄 값처럼 다루면 된다.
예를 들어 이름, 나이, 주소는 값 자체가 비교적 단순하다.
브라우저가 서버로 보낼 때도 텍스트 중심으로 보내면 된다.
하지만 파일은 다르다.
파일은 단순히 “사진” 같은 값 하나만 보내는 것이 아니다.
실제로는 파일 이름도 필요하고, 파일 크기도 필요하고, 그 안에 들어 있는 실제 데이터도 같이 움직여야 한다.
즉, 파일은 텍스트 한 줄처럼 가볍게 보낼 수 있는 값이 아니다.
그래서 일반 요청값과는 다른 방식으로 다뤄야 한다.
사용자가 파일을 업로드한다는 것은 단순히 파일을 선택했다는 뜻으로 끝나지 않는다.
서버는 그 파일이 실제로 존재하는지도 확인해야 하고, 이름이 무엇인지도 알아야 하고, 필요하면 저장도 해야 한다.
즉, 파일 하나를 처리하려고 해도 그 주변에 따라오는 정보가 많다.
초보자 기준에서는 이 부분을 값 + 파일 자체 정보 + 실제 내용을 함께 받아야 하는 구조라고 이해하면 된다.
기본 개념예제
아래 예제는 파일 업로드 폼이 왜 일반 폼과 다르게 생기는지를 보여 주는 가장 작은 예제이다.
// exam01.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>upload form</title> </head> <body> <form action="/upload-basic" method="post" enctype="multipart/form-data"> <input type="file" name="file"><!-- 업로드할 파일 선택 --> <input type="submit" value="전송"><!-- 파일 업로드 요청 전송 --> </form> </body> </html>이 예제에서 먼저 눈에 들어와야 하는 것은
input type="file"이다.
이 입력칸은 일반 텍스트 입력칸과 다르게 파일 선택 창을 열어 준다.
그리고enctype="multipart/form-data"도 같이 들어가 있다.
이 설정이 있어야 브라우저가 파일 데이터를 일반 텍스트 요청처럼 보내지 않고, 파일 전송에 맞는 방식으로 보낼 수 있다.
즉, 파일 업로드는 폼 모양부터 이미 일반 요청과 다르게 시작된다고 이해하면 된다.
multipart/form-data는 파일과 일반 입력값을 여러 부분으로 나누어 보내는 전송 형식이다.
이름만 보면 어렵지만, 초보자 기준에서는 문자열 입력값과 파일 입력값을 한 번에 묶어서 보내되, 각각을 따로 구분해서 전달하는 방식이라고 이해하면 된다.
여기서 중요한 점은 2번과 역할이 다르다는 것이다.
2번에서는 왜 파일이 일반 문자열처럼 단순하지 않은지를 봤다.
여기서는 그래서 브라우저가 실제로 어떤 전송 형식을 써야 하는지를 보는 것이다.
즉, 파일이 복잡하기 때문에 전송 방식도 일반 폼과 달라진다.
브라우저는form을 전송할 때 어떤 방식으로 묶어서 보낼지 결정해야 한다.
파일이 없는 일반 폼이면 비교적 단순한 방식으로 보내도 된다.
하지만 파일이 포함되면 일반 텍스트처럼 보내면 안 된다.
그래서enctype="multipart/form-data"를 반드시 지정해야 한다.
이 설정이 있어야 브라우저가 문자열 입력값과 파일 입력값을 서로 구분해서 전송할 수 있다.
서버 입장에서도 일반 요청 파라미터처럼 파일을 보면 안 된다.
문자열은 요청값 하나로 바로 읽으면 되지만, 파일은 파일명, 비어 있는지 여부, 크기, 실제 저장 같은 처리가 함께 필요하다.
그래서Spring MVC는 파일 업로드 요청이 들어오면 이를 일반 문자열이 아니라 파일 객체로 다룰 수 있게 준비해 둔다.
그 중심에 있는 것이MultipartFile이다.
기본 개념예제
아래 예제는 문자열 입력값과 파일 입력값이 한 폼 안에서 함께 전송되는 가장 기본적인 형태이다.
// multipart.html <!doctype html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>multipart</title> </head> <body> <h1>멀티파트 학습</h1> <hr/> <form method="post" action="업로드처리주소" enctype="multipart/form-data"> 성명: <input type="text" name="stname" /> <br/> 학번: <input type="text" name="stnum" /> <br/> 과제첨부: <input type="file" name="reports" /> <br/> <input type="submit" value="제출" /> </form> </body> </html>이 예제에서 먼저 봐야 할 것은
enctype="multipart/form-data"이다.
이 설정이 있기 때문에 브라우저는stname,stnum같은 일반 입력값과reports파일 입력값을 한 번에 보내면서도, 각각을 따로 구분해서 전송할 수 있다.
또name="reports"도 중요하다.
이 이름은 나중에 서버 쪽에서 파일을 받을 때 연결 기준이 된다.
즉, 브라우저 화면에서 파일 입력칸 이름을reports로 정하면, 서버도 그 이름을 기준으로 파일 요청값을 받게 된다.
개발자 도구로 보면 무엇이 확인되는가
이 예제는 브라우저 개발자 도구의
Network창으로 확인하면 더 이해가 쉬워진다.
폼을 제출한 뒤 요청 하나를 클릭해서 보면,Headers에서는 요청 메서드가POST로 보이고,Content-Type은multipart/form-data; boundary=...형태로 나타난다.
여기서boundary는 여러 부분으로 나뉜 데이터를 구분하는 경계선이라고 이해하면 된다.
즉, 브라우저가 정말 멀티파트 형식으로 요청을 만들었다는 뜻이다.
그리고페이로드를 보면stname,stnum,reports가 같이 들어간다.
즉, 일반 입력값 두 개와 파일 입력값 한 개가 하나의 요청 안에서 함께 전송된다는 점을 눈으로 확인할 수 있다.
이게 바로multipart/form-data를 설명할 때 가장 중요한 장면이다.
원본 데이터까지 열어 보면 각 부분이 실제로 따로 나뉘어 전송되는 모습이 더 분명하게 보인다.
stname,stnum는 각각 별도 파트로 들어가고,reports부분에는filename="근로계약서.pdf"처럼 실제 파일명과Content-Type: application/pdf같은 파일 종류 정보도 함께 붙는다.
즉, 파일은 단순히 “있다” 정도로만 전송되는 것이 아니라, 일반 입력값과는 다른 파일 정보까지 함께 묶여서 전송된다는 점이 분명하게 드러난다.
그래서 이 예제는 파일 업로드가 단순히 입력칸 하나 더 추가되는 것이 아니라, 브라우저의 전송 형식 자체가 달라지는 구조라는 점을 보여 주기에 아주 좋은 예제이다.
즉,multipart/form-data는 파일만 보내는 형식이 아니라, 문자열 입력값과 파일 입력값을 함께 보내되 각각을 구분해서 전달하는 형식이라고 이해하면 된다.
Spring MVC에서 파일을 받는 가장 기본적인 방식은 컨트롤러 메서드의 매개변수 타입을MultipartFile로 지정하는 것이다.
여기서 먼저 정확히 잡아야 할 점은MultipartFile이 예약어가 아니라는 점이다.
MultipartFile은 업로드된 파일 하나를 표현하기 위해Spring MVC가 제공하는 파일 전용 타입이다.
컨트롤러 메서드의 매개변수는 요청으로 들어온 값을 받는 자리이다.
문자열 요청값을 받을 때는 매개변수 타입을String으로 지정한다.
숫자 요청값을 받을 때는 매개변수 타입을int나Integer로 지정한다.
파일 요청값을 받을 때는 매개변수 타입을MultipartFile로 지정한다.
즉,MultipartFile을 매개변수 타입으로 적는다는 것은 이 자리에는 업로드 파일을 받겠다고 선언하는 것이다.
예를 들어 문자열 요청값은 아래처럼 받을 수 있다.// StringParameterExample.java @PostMapping("/save-name") public String saveName(@RequestParam("name") String name) { System.out.println(name); // 요청으로 들어온 문자열 확인 return "result"; }
String name에서String은 타입이고,name은 메서드 안에서 사용할 변수 이름이다.
@RequestParam("name")은 요청에서name이라는 이름으로 넘어온 값을 찾으라는 뜻이다.
즉, 요청에서name값을 찾아String타입의name변수에 넣는 구조이다.
파일 요청값도 흐름은 같다.
다만 타입이 문자열용String이 아니라 파일용MultipartFile로 바뀐다.// MultipartFileParameterExample.java @PostMapping("/upload-direct") public String upload(@RequestParam("file") MultipartFile file) { System.out.println(file.getOriginalFilename()); // 요청으로 들어온 파일명 확인 return "result"; }
MultipartFile file에서MultipartFile은 타입이고,file은 변수 이름이다.
@RequestParam("file")은 요청에서file이라는 이름으로 넘어온 값을 찾으라는 뜻이다.
즉, 이 코드는 “요청에서file이라는 이름으로 넘어온 업로드 파일을 찾아서MultipartFile타입의file변수로 받겠다”는 뜻이다.
이 흐름은 아래처럼 비교하면 더 분명하다.// ParameterTypeCompare.java // 문자열 요청값을 받을 때 @RequestParam("name") String name // 파일 요청값을 받을 때 @RequestParam("file") MultipartFile file따라서 “문자열 요청값을
String으로 받듯이, 파일 요청값은MultipartFile로 받는다”는 말은 단순히 이름을 외우라는 뜻이 아니다.
요청값의 종류에 맞게 컨트롤러 매개변수 타입을 정한다는 뜻이다.
파일 입력칸과 컨트롤러 매개변수가 연결되는 흐름
파일이 컨트롤러 매개변수에 들어오려면 먼저 브라우저가 파일을 파일 업로드 형식으로 보내야 한다.
그래서 파일 업로드 폼에는enctype="multipart/form-data"가 필요하다.// uploadForm.html <form action="/upload-direct" method="post" enctype="multipart/form-data"> <input type="file" name="file"><!-- file이라는 이름으로 파일 전송 --> <input type="submit" value="업로드"><!-- 업로드 요청 전송 --> </form>이 코드에서
input type="file"은 사용자가 파일을 선택하게 해 주는 입력칸이다.
그리고name="file"은 서버로 보낼 때 이 파일 데이터의 이름을file로 정하겠다는 뜻이다.
이제 컨트롤러에서 아래처럼 받으면 된다.// UploadDirectController.java import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; @Controller public class UploadDirectController { @PostMapping("/upload-direct") public String upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { // 파일을 선택하지 않았는지 확인 System.out.println("파일이 없습니다"); return "result"; } System.out.println(file.getOriginalFilename()); // 실제 업로드 파일명 확인 return "result"; } }이 코드에서 연결 기준은
file이라는 이름이다.
브라우저의 파일 입력칸은name="file"로 파일을 보낸다.
컨트롤러는@RequestParam("file")로 요청에서file이라는 이름의 값을 찾는다.
그리고 그 값을MultipartFile타입으로 받는다.
전체 흐름은 아래처럼 이어진다.// MultipartFileBindingFlow.java // 1. 브라우저의 파일 입력칸 이름 name="file" // 2. 사용자가 선택한 파일 test.png // 3. 브라우저가 file이라는 이름으로 파일 전송 file = test.png // 4. 컨트롤러가 file이라는 이름의 요청값을 찾음 @RequestParam("file") // 5. 파일 요청값이므로 MultipartFile 타입으로 받음 MultipartFile file // 6. 메서드 안에서 file 변수로 업로드 파일 사용 file.getOriginalFilename()즉,
MultipartFile이라는 이름 자체가 자동으로 동작하는 것이 아니다.
브라우저가multipart/form-data형식으로 파일을 보내고,Spring MVC가 요청 안에서file이라는 이름의 파일 데이터를 찾은 뒤, 그 데이터를MultipartFile타입의 매개변수에 넣어 주는 구조이다.
어노테이션 없이 아래처럼 쓸 수 있는 경우도 있다.// UploadDirectControllerSimple.java @PostMapping("/upload-direct") public String upload(MultipartFile file) { System.out.println(file.getOriginalFilename()); // 실제 업로드 파일명 확인 return "result"; }이 코드는 매개변수 이름
file과 파일 입력칸의name="file"이 맞으면 동작할 수 있다.
Spring MVC가 같은 이름의 파일 데이터를 찾아MultipartFile file에 넣어 줄 수 있기 때문이다.
하지만 처음 배울 때는 이 방식보다@RequestParam("file") MultipartFile file처럼 쓰는 편이 더 좋다.
어떤 요청 이름과 어떤 매개변수가 연결되는지 바로 보이기 때문이다.
즉, 초보자 기준에서는 아래처럼 이해하면 된다.// RequestParamMultipartFileFlow.java @RequestParam("file") // 요청에서 file이라는 이름의 값을 찾음 MultipartFile // 그 값을 업로드 파일 타입으로 받음 file // 메서드 안에서 file이라는 변수명으로 사용함
파일을 직접 받는 방식과 객체 안에 넣어 받는 방식
파일을 받는 방식은 하나만 있는 것이 아니다.
가장 단순한 방식은 지금처럼 컨트롤러 매개변수에MultipartFile을 직접 적는 방식이다.
이 방식은 파일 하나를 바로 확인할 수 있어서 처음 이해하기 쉽다.
하지만 실제 화면에서는 파일만 보내는 경우보다 제목, 설명, 작성자 같은 일반 입력값과 파일을 함께 보내는 경우가 많다.
이럴 때는 업로드용VO나DTO안에MultipartFile필드를 넣어서 받을 수 있다.// UploadFileVO.java import org.springframework.web.multipart.MultipartFile; public class UploadFileVO { private String title; // 일반 문자열 요청값 private MultipartFile file; // 업로드 파일 요청값 }이 구조는 이전에 배운
DTO,VO바인딩과 비슷하다.
여러 문자열 값을 객체 하나로 묶어 받던 것처럼, 파일도 객체 안에 넣어 함께 받을 수 있다.
즉, 파일 업로드는 기존 요청값 처리 구조와 완전히 다른 기능이 아니라, 기존 요청값 처리 구조에 파일 타입이 추가된 흐름이다.
또 여러 파일을 더 직접적으로 다루고 싶을 때는MultipartRequest를 사용할 수도 있다.
MultipartRequest는 업로드 요청 전체에서 파일 목록을 꺼내는 방식에 가깝다.
다만 처음 파일 업로드를 이해할 때는MultipartFile을 먼저 잡는 것이 좋다.
왜냐하면 파일 하나가 어떻게 컨트롤러 매개변수로 들어오고, 그 안에서 파일명과 크기를 어떻게 확인하는지가 가장 직관적으로 보이기 때문이다.
이 이미지는 파일 업로드 처리 시 컨트롤러 메서드에서 사용할 수 있는 파라미터 형태를 보여 준다.
MultipartFile하나를 직접 받을 수도 있고, 여러 파일이면 배열로 받을 수도 있고, 업로드용 객체 안에 넣어서 받을 수도 있다.
기본 개념예제
아래 예제는 컨트롤러가
MultipartFile하나를 직접 받는 가장 단순한 형태이다.// UploadDirectController.java import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; @Controller public class UploadDirectController { @PostMapping("/upload-direct") public String upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { // 파일이 비어 있는지 먼저 확인 System.out.println("파일이 없습니다"); // 업로드 실패 상황 확인 return "result"; // 결과 화면 이름 반환 } System.out.println(file.getOriginalFilename()); // 업로드한 실제 파일명 확인 return "result"; // 결과 화면 이름 반환 } }// 출력결과 // 파일이 없습니다 // 또는 // test.png이 예제에서 중요한 점은 파일도 결국 컨트롤러 메서드의 매개변수로 들어온다는 것이다.
다만 문자열이 아니라 파일이기 때문에 매개변수 타입을MultipartFile로 지정한다.
그리고@RequestParam("file")을 붙여서 요청에서file이라는 이름으로 넘어온 업로드 파일을 이 매개변수에 넣겠다고 명확히 표시한다.
이 단계에서 정리할 핵심은 간단하다.
브라우저는name="file"이라는 이름으로 파일을 보내고, 컨트롤러는@RequestParam("file") MultipartFile file로 그 파일을 받는다.
이 흐름이 잡히면 다음 5번에서MultipartFile이 어떤 역할을 하는지도 자연스럽게 이어진다.
4번에서 파일 요청값이
MultipartFile매개변수에 들어오는 흐름을 봤다.
이제 5번에서는 그 매개변수 안에 들어온MultipartFile이 정확히 무엇을 의미하는지 정리한다.
MultipartFile은 업로드된 파일 하나를 자바 객체처럼 다루게 해 주는 타입이다.
서버는 화면에서 넘어온 파일을 그냥 막연한 데이터 덩어리로 직접 만지는 것이 아니다.
MultipartFile객체를 통해 업로드 파일에 접근한다.
즉,MultipartFile은 단순히 파일을 담아 두는 상자가 아니다.
업로드된 파일을 확인하고 다루고 저장하기 위한 파일 처리 도구이다.
문자열 요청값을String으로 받으면 그 안에는 문자열 값이 들어 있다.
파일 요청값을MultipartFile로 받으면 그 안에는 업로드 파일과 관련된 정보가 들어 있다.
파일은 단순한 글자값이 아니기 때문에 파일명, 크기, 비어 있는지 여부, 실제 내용 같은 정보가 함께 필요하다.
그래서MultipartFile에는 파일 처리를 위한 여러 메서드가 들어 있다.
MultipartFile 안에 들어 있다고 생각하면 되는 것
MultipartFile file이라는 매개변수 안에는 업로드 파일을 다루기 위한 정보와 기능이 들어 있다.
초보자 기준에서는 아래처럼 이해하면 된다.
- 실제 업로드한 파일 이름을 확인할 수 있다.
- 파일 입력칸의 이름을 확인할 수 있다.
- 파일을 선택했는지 확인할 수 있다.
- 파일 크기를 확인할 수 있다.
- 파일 내용을 읽을 수 있다.
- 파일을 실제 경로에 저장할 수 있다.
즉,
MultipartFile은 업로드된 파일 하나를 컨트롤러 안에서 다룰 수 있게 만들어 주는 객체이다.
기본 개념예제
아래 예제는
MultipartFile이 무엇을 대표하는지 가장 직관적으로 보여 주는 예제이다.// MultipartFileIntroController.java import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController public class MultipartFileIntroController { @PostMapping("/upload-name") public String upload(@RequestParam("file") MultipartFile file) { return file.getOriginalFilename(); // 업로드한 실제 파일명 반환 } }// 출력결과 // test.png이 코드는 업로드된 파일에서 실제 파일명만 꺼내서 바로 응답한다.
여기서file은 단순한 문자열 변수가 아니다.
업로드된 파일을 표현하는MultipartFile객체이다.
그래서file.getOriginalFilename()처럼 파일 전용 메서드를 사용할 수 있다.
이 흐름을 실제 값 기준으로 보면 아래와 같다.// MultipartFileObjectFlow.java // 1. 사용자가 선택한 파일 test.png // 2. 컨트롤러 매개변수에 들어온 객체 MultipartFile file // 3. 파일 객체에서 실제 파일명 꺼내기 file.getOriginalFilename() // 4. 결과 test.png즉,
MultipartFile을 받으면 이 객체를 통해 업로드된 파일에 접근할 수 있다.
문자열 요청값을 받으면 그 값을 바로 출력할 수 있듯이, 파일 요청값을 받으면 그 파일 객체 안의 정보를 메서드로 읽을 수 있는 것이다.
MultipartFile을 왜 객체처럼 본다고 하는가
객체는 관련된 값과 기능을 함께 가진 구조이다.
MultipartFile도 업로드 파일과 관련된 값과 기능을 함께 가지고 있다.
예를 들어 파일명만 필요하면getOriginalFilename()을 사용한다.
파일을 선택했는지 확인하려면isEmpty()를 사용한다.
파일 크기가 필요하면getSize()를 사용한다.
실제 저장이 필요하면transferTo()를 사용할 수 있다.
그래서MultipartFile file은 단순히 파일 이름 하나만 들어 있는 변수가 아니다.
파일 업로드 처리에 필요한 여러 정보를 꺼내고, 파일을 저장하는 기능까지 사용할 수 있는 객체이다.
이 단계에서 정리할 핵심은 이것이다.
4번은 파일이 컨트롤러 매개변수에 어떻게 들어오는지 설명하는 부분이고, 5번은 그렇게 들어온MultipartFile객체로 무엇을 할 수 있는지 설명하는 부분이다.
getName과 getOriginalFilename
getName()은 파라미터 이름을 반환한다.
즉,form에서name="file"처럼 지정한 이름을 확인할 때 쓰는 메서드이다.
반면getOriginalFilename()은 사용자가 실제로 업로드한 파일 이름을 반환한다.
즉, 화면에서 선택한 파일명이 무엇인지 확인할 때 쓰는 메서드이다.
초보자가 여기서 자주 헷갈리는 것은 두 메서드가 둘 다 파일 이름처럼 보인다는 점이다.
하지만 둘은 다르다.
getName()은 입력 필드 이름이고,getOriginalFilename()은 실제 업로드 파일 이름이다.
즉, 하나는 폼의 이름, 다른 하나는 실제 파일의 이름이다.
isEmpty와 getSize
isEmpty()는 업로드된 파일이 실제로 비어 있는지 확인한다.
사용자가 파일을 선택하지 않았는데 요청만 보냈다면 이 값이 중요해진다.
getSize()는 파일 크기를 반환한다.
즉, 업로드된 파일이 얼마나 큰지 확인할 수 있다.
이 두 메서드는 파일 상태를 먼저 확인하는 데 자주 쓰인다.
파일 업로드에서는 바로 저장하는 것보다 먼저 “정말 파일이 들어왔는가”, “크기는 어느 정도인가”를 보는 흐름이 중요하다.
그래서isEmpty()와getSize()는 초반에 가장 자주 보게 되는 메서드이다.
getBytes, getInputStream, transferTo
getBytes()는 파일 데이터를 바이트 배열로 가져온다.
getInputStream()은 입력 스트림으로 가져온다.
이 두 메서드는 실제 파일 데이터를 읽어서 별도 처리할 때 쓰인다.
반면transferTo()는 업로드된 파일 데이터를 지정한 파일로 저장할 때 쓴다.
처음 단계에서는getBytes()와getInputStream()를 아주 깊게 파지 않아도 된다.
지금은 “파일 내용을 직접 읽어야 할 때 쓰는 도구” 정도로 이해하면 된다.
반대로transferTo()는 파일 업로드에서 실제 저장과 연결되기 때문에 초보자 기준에서도 꼭 봐야 할 메서드이다.
즉, 업로드 파일을 실제 파일로 옮겨 저장하는 핵심 메서드라고 이해하면 된다.
기본 개념예제
아래 예제는
MultipartFile에서 가장 자주 보는 메서드 몇 개를 확인하는 예제이다.// exam05.java import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController public class MultipartFileMethodController { @PostMapping("/upload-check") public String upload(MultipartFile file) { if (file.isEmpty()) { // 파일이 없으면 더 진행하지 않음 return "파일이 없습니다"; // 파일 없음 응답 } return "필드이름:" + file.getName() // 폼의 파라미터 이름 + ", 실제파일명:" + file.getOriginalFilename() // 업로드한 실제 파일명 + ", 파일크기:" + file.getSize(); // 업로드 파일 크기 } }// 출력결과 // 필드이름:file, 실제파일명:test.png, 파일크기:2048이 예제는 파일 상태 확인 흐름을 보여 준다.
먼저isEmpty()로 파일이 있는지 확인하고, 그다음getName(),getOriginalFilename(),getSize()로 파일 정보를 읽는다.
즉, 파일 업로드의 가장 기본 흐름은 파일 존재 확인 → 파일 정보 확인이라고 이해하면 된다.
그리고 여기서transferTo()는 아직 넣지 않았다.
왜냐하면 지금 단계의 목적은 저장이 아니라,MultipartFile이 어떤 정보를 가지고 있는지 먼저 이해하는 것이기 때문이다.
실제 저장 흐름은 응용예제에서 다시 자연스럽게 연결하면 된다.
파일 업로드는 컨트롤러 코드만 보면 반쪽 이해에 머물기 쉽다.
브라우저 쪽 폼이 어떻게 생겼는지도 같이 봐야 한다.
파일 업로드 폼에서는 보통method="post",enctype="multipart/form-data",input type="file"가 함께 들어간다.
이 셋은 따로 노는 설정이 아니라, 파일 업로드를 위해 같이 움직이는 구조이다.
method="post"는 데이터를 서버로 보내겠다는 뜻이고,enctype="multipart/form-data"는 파일 업로드에 맞는 전송 형식을 지정하는 것이다.
그리고input type="file"는 사용자가 실제 파일을 선택하게 해 주는 입력칸이다.
즉, 파일 업로드는 폼 단계부터 이미 일반 입력과 다른 구조를 가진다.
실제 업로드 화면은 파일만 보내는 경우보다, 파일과 일반 입력값을 함께 보내는 경우가 더 많다.
예를 들어 제목과 첨부파일을 같이 보내거나, 설명과 이미지를 같이 보내는 식이다.
이때 브라우저는 문자열 입력값과 파일 입력값을 함께 묶어서 전송한다.
서버는 그중 문자열은 문자열대로 받고, 파일은MultipartFile로 받으면 된다.
이 흐름이 중요한 이유는 파일 업로드가 이전글의 요청값 처리와 완전히 분리된 것이 아니라는 점을 다시 보여 주기 때문이다.
즉, 문자열과 파일은 한 폼 안에서 같이 전송될 수 있고, 컨트롤러도 이를 함께 받을 수 있다.
이전글에서DTO나VO로 여러 값을 묶어서 받던 구조와 자연스럽게 연결되는 이유가 바로 여기에 있다.
기본 개념예제
아래 예제는 제목 입력값 하나와 파일 하나를 함께 보내는 가장 단순한 업로드 예제이다.
// exam06.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>upload with title</title> </head> <body> <form action="/upload-with-title" method="post" enctype="multipart/form-data"> <input type="text" name="title" placeholder="제목 입력"><!-- 일반 문자열 입력 --> <input type="file" name="file"><!-- 업로드 파일 선택 --> <input type="submit" value="전송"><!-- 파일과 문자열 함께 전송 --> </form> </body> </html>// exam06.java import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.multipart.MultipartFile; @Controller public class UploadWithTitleController { @PostMapping("/upload-with-title") public String upload(String title, MultipartFile file) { System.out.println(title); // 일반 문자열 요청값 확인 System.out.println(file.getOriginalFilename()); // 업로드 파일명 확인 return "result"; // 결과 화면 이름 반환 } }// 출력결과 // 공지사항 첨부 // notice.pdf이 예제는 파일 업로드가 이전글의 요청 처리 구조 위에 얹혀 있다는 점을 가장 분명하게 보여 준다.
title은 일반 문자열 요청값으로 받고,file은MultipartFile로 받는다.
즉, 한 폼 안에 있는 값들이라고 해도 서버에서는 각 타입에 맞게 나누어 받을 수 있다.
그래서 파일 업로드를 볼 때는 “파일만 따로 다루는 기술”이라고 생각하기보다, 기존 요청값 구조에 파일이 추가된 형태라고 이해하는 편이 훨씬 자연스럽다.
이 흐름이 잡혀 있으면 이후 응용예제에서도 어떤 부분이 새로 나온 개념이고, 어떤 부분이 이전글에서 본 요청 처리 구조의 반복인지 구분하기 쉬워진다.
이제 기본 흐름은 잡혔다.
다음부터는 실제 업로드 예제를 보면서, 위에서 정리한multipart/form-data,MultipartFile, 파일 바인딩 방식이 코드 안에서 어떻게 이어지는지 2차로 이해하면 된다.
앞에서는
multipart/form-data,MultipartFile, 파일 바인딩 방식을 먼저 정리했다.
이제부터는 그 개념들이 실제 코드 안에서 어떻게 이어지는지 응용예제로 확인한다.
응용예제에서는 파일 업로드 기능만 따로 떼어 보는 것이 아니라, 폼 전송 → 객체 바인딩 → 파일 저장 → 결과 화면 출력이 한 흐름으로 어떻게 이어지는지도 같이 본다.
즉, 여기서는 개념을 다시 외우는 것이 아니라, 실제 업로드 요청이 들어왔을 때 스프링이 파일을 어떻게 받고, 어떻게 저장하고, 결과를 어떤 화면으로 보여 주는지를 결과까지 연결해서 이해하는 것이 핵심이다.
1. 업로드용
VO로 파일 하나 저장하기 (UploadController1.java, FileVO1.java, uploadForm1.html)이 예제는 파일 하나를 업로드하는 가장 기본적인 응용 흐름이다.
하지만 단순히 파일 하나를 받는 데서 끝나지 않는다.
실제로는 업로드 폼 요청, 업로드용VO바인딩, 실제 파일명 확인, 파일 내용 읽기, 실제 경로 저장, 중복 파일 검사, 같은 화면 재사용까지 한 번에 보여 준다.
특히 이 묶음은 파일을MultipartFile매개변수로 직접 받지 않고,FileVO1안에 넣어서 받는다는 점이 중요하다.
즉, 이전글에서 보던DTO,VO바인딩 흐름이 파일 업로드에도 그대로 이어진다는 것을 보여 주는 예제이다.
이 예제가 같이 보여주는 개념
form에서enctype="multipart/form-data"를 설정해야 파일 업로드 요청이 만들어진다.input type="file"의name값과VO의 필드 이름이 같아야 파일이 정상적으로 바인딩된다.- 컨트롤러는
FileVO1 vo처럼 업로드용 객체를 매개변수로 받아 파일을 처리할 수 있다.getOriginalFilename()으로 실제 업로드 파일명을 확인할 수 있다.getBytes()로 파일 내용을 바이트 배열로 읽을 수 있다.File객체와FileOutputStream을 사용하면 업로드 파일을 실제 경로에 저장할 수 있다.- 같은 이름의 파일이 이미 있으면 저장하지 않고 메시지만 보여 줄 수 있다.
ModelAndView와Thymeleaf조건식을 이용하면 같은 화면을 재사용하면서 결과 메시지를 보여 줄 수 있다.즉, 이 예제는 파일 업로드 기능만 따로 보여 주는 것이 아니라,
폼 전송 → 객체 바인딩 → 파일 확인 → 실제 저장 → 결과 화면 출력 전체 흐름을 한 번에 보여 주는 예제이다.
코드 설명
먼저 업로드 화면부터 보면, 사용자가 파일 하나를 선택해서
/upload1로 전송하는 구조이다.// uploadForm1.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>JSP Test Example</title> </head> <body> <h2>SpringMVC FileUpload</h2> <hr> <form th:unless="${ msg }" action="/upload1" enctype="multipart/form-data" method="post"> <input type="file" name="uploadFile" /> <!-- 업로드할 파일 선택 --> <input type="submit" value="업로드"/> <!-- 업로드 요청 전송 --> </form> <h2 th:if="${ msg }" th:text="${ msg }"></h2> <!-- 결과 메시지 출력 --> </body> </html>이 코드에서 먼저 봐야 할 것은
enctype="multipart/form-data"이다.
앞에서 정리한 것처럼 파일 업로드 요청을 만들려면 이 설정이 꼭 필요하다.
그리고 파일 입력칸의 이름이uploadFile인데, 이 이름은 나중에VO안의 필드 이름과 연결된다.
즉, 폼의name값과 업로드용 객체의 필드 이름이 맞아야 스프링이 파일을 자연스럽게 넣어 줄 수 있다.
또th:unless="${ msg }"와th:if="${ msg }"도 중요하다.
처음에는msg가 없으므로 업로드 폼이 보인다.
반대로 업로드가 끝나서 메시지가 생기면 폼은 숨고, 그 자리에 결과 메시지가 출력된다.
즉, 이 화면은 입력 화면과 업로드 결과 화면을 같은 페이지에서 함께 처리하는 구조이다.
이제 업로드용 객체를 보면, 파일을MultipartFile필드 하나로 감싸고 있다.// FileVO1.java package com.example.springedu.domain; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.web.multipart.MultipartFile; @Getter @AllArgsConstructor public class FileVO1 { private MultipartFile uploadFile; // 업로드 파일이 바인딩될 필드 }여기서 핵심은
uploadFile이라는 필드 이름이다.
이 이름이 폼의name="uploadFile"과 맞기 때문에 스프링이 업로드 파일을 이 필드에 연결할 수 있다.
즉, 이 예제는 파일 업로드도 결국 이전글에서 본 객체 바인딩 구조 위에서 동작한다는 점을 보여 준다.
이제 실제 저장을 처리하는 컨트롤러를 보면 흐름이 더 분명해진다.// UploadController1.java package com.example.springedu.controller; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import com.example.springedu.domain.FileVO1; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; @Controller public class UploadController1 { @RequestMapping("/uploadForm1") public void formFile() { // 요청 경로 이름으로 화면을 찾는다 } @RequestMapping("/upload1") public ModelAndView saveFile(FileVO1 vo) { String fileName = vo.getUploadFile().getOriginalFilename(); // 실제 업로드 파일명 확인 byte[] content = null; ModelAndView mav = new ModelAndView(); mav.setViewName("uploadForm1"); // 같은 화면 재사용 try { content = vo.getUploadFile().getBytes(); // 업로드 파일 내용을 바이트 배열로 읽음 File f = new File("/Users/leeunduck/uploadtest/" + fileName); // 저장할 실제 파일 경로 생성 if (f.exists()) { // 같은 이름의 파일이 이미 있으면 mav.addObject("msg", fileName + " : 파일이 이미 존재해요!!"); } else { FileOutputStream fos = new FileOutputStream(f); // 파일 저장 스트림 생성 fos.write(content); // 실제 파일 저장 fos.close(); // 스트림 닫기 mav.addObject("msg", fileName + ": 파일이 저장되었어요!!"); } } catch (IOException e) { e.printStackTrace(); mav.addObject("msg", "오류가 발생했어요!!"); } return mav; } }이 코드에서 먼저 봐야 할 것은
saveFile(FileVO1 vo)이다.
컨트롤러가MultipartFile을 직접 받는 것이 아니라FileVO1객체를 통째로 받고 있다.
즉, 업로드 파일이VO안으로 먼저 바인딩된 다음, 컨트롤러는 그 객체에서 다시 파일을 꺼내 쓰는 구조이다.
이 지점이 바로 이전글의DTO,VO바인딩 흐름과 연결되는 부분이다.
그리고vo.getUploadFile().getOriginalFilename()으로 실제 파일명을 확인한다.
이건 앞에서 본MultipartFile메서드 중getOriginalFilename()이 실제로 어디서 쓰이는지를 보여 준다.
그다음getBytes()로 파일 내용을 바이트 배열로 읽는다.
즉, 이 예제는transferTo()대신getBytes()+FileOutputStream방식으로 저장하는 흐름을 보여 준다.
또File f = new File("/Users/leeunduck/uploadtest/" + fileName);부분도 중요하다.
이 줄은 업로드한 파일을 어디에 저장할지 실제 경로를 만드는 부분이다.
그리고f.exists()로 같은 이름의 파일이 이미 있는지 먼저 확인한다.
즉, 이 예제는 업로드 파일을 무조건 저장하지 않고, 같은 이름의 파일이 이미 있으면 저장을 막는 흐름까지 함께 보여 준다.
그래서 결과가 이렇게 나온다
이 묶음의 실행 결과를 보면, 먼저 파일을 선택하고 업로드를 진행하는 흐름이 보인다.
그리고 업로드가 끝나면 같은 화면으로 돌아오면서 결과 메시지가 출력된다.
이 장면에서 중요한 점은 화면이 다른 페이지로 바뀌지 않았다는 것이다.
같은uploadForm1.html을 다시 사용하면서,msg값이 생겼기 때문에 업로드 폼 대신 처리 결과가 출력된 것이다.
즉,Thymeleaf의th:unless와th:if가 실제로 어떻게 쓰이는지도 결과 화면에서 확인할 수 있다.
그래서 이 묶음의 전체 흐름은
업로드 폼 요청 → 업로드용VO바인딩 → 실제 파일명 확인 → 파일 내용 읽기 → 실제 경로 저장 → 같은 화면에 결과 메시지 출력
이라고 정리할 수 있다.
이 묶음에서 꼭 잡아야 하는 핵심은 이것이다.
파일 하나 업로드도 결국 폼 전송, 객체 바인딩, 파일 메서드 사용, 실제 저장, 결과 화면 출력이 모두 이어지는 흐름이라는 점이다.
2. 업로드용
VO배열로 여러 파일 저장하기 (UploadController2.java, FileVO2.java, uploadForm2.html)이 예제는 파일 하나가 아니라 여러 파일을 한 번에 업로드하는 흐름을 보여 준다.
하지만 단순히 파일 입력칸에multiple만 붙였다고 끝나는 예제가 아니다.
실제로는 여러 파일 선택, 배열 바인딩, 반복문 처리, 중복 파일 검사,transferTo()저장, 여러 결과 메시지 누적 출력까지 함께 보여 준다.
즉, 이 묶음은 앞의 1번 예제가 “파일 하나를VO로 받는 흐름”이었다면, 이번에는 여러 파일을MultipartFile[]배열로 묶어 받는 흐름으로 확장된 예제이다.
이 예제가 같이 보여주는 개념
input type="file"에multiple을 주면 여러 파일을 선택할 수 있다.- 파일 입력칸 이름과
VO안의 배열 필드 이름이 같아야 여러 파일이 정상적으로 바인딩된다.- 업로드용 객체 안에
MultipartFile[]배열을 두면 여러 파일을 한 번에 받을 수 있다.for반복문으로 업로드한 파일들을 하나씩 처리할 수 있다.transferTo()를 사용하면 각 파일을 실제 저장 경로에 바로 저장할 수 있다.- 같은 이름의 파일이 이미 있으면 그 파일만 저장을 막고 메시지를 누적할 수 있다.
- 여러 파일 처리 결과를 하나의 문자열로 누적해서 같은 화면에 보여 줄 수 있다.
즉, 이 예제는 여러 파일 업로드에서 가장 중요한
배열 바인딩 → 반복 처리 → 개별 저장 → 결과 누적 출력 흐름을 한 번에 보여 준다.
코드 설명
먼저 업로드 화면부터 보면 파일 선택칸에
multiple이 들어가 있다.// uploadForm2.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>JSP 테스트 예제</title> </head> <body> <h2>SpringMVC FileUpload(Multiple)</h2> <hr> <form th:unless="${ msg }" action="/upload2" enctype="multipart/form-data" method="post"> <input type="file" name="uploadFiles" multiple/> <!-- 여러 파일 선택 --> <input type="submit" value="파일올리기"/> <!-- 업로드 요청 전송 --> </form> <h2 th:if="${ msg }" th:utext="${ msg }"></h2> <!-- 여러 결과 메시지 출력 --> </body> </html>여기서 가장 먼저 봐야 할 것은
multiple이다.
이 속성이 있기 때문에 사용자는 파일을 여러 개 선택할 수 있다.
그리고 파일 입력칸 이름이uploadFiles인데, 이 이름은 나중에VO안의 배열 필드 이름과 연결된다.
즉, 여러 파일 업로드도 결국 입력칸 이름과 객체 필드 이름이 맞아야 바인딩이 된다는 점은 같다.
이제 업로드용 객체를 보면MultipartFile[]배열이 들어가 있다.// FileVO2.java package com.example.springedu.domain; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.web.multipart.MultipartFile; @Getter @AllArgsConstructor public class FileVO2 { private MultipartFile[] uploadFiles; // 여러 업로드 파일이 바인딩될 배열 필드 }이 객체는 앞의
FileVO1과 비슷하지만, 파일 하나가 아니라 배열을 가진다는 점이 다르다.
즉, 이번에는 파일 여러 개가 한 번에 들어올 수 있기 때문에MultipartFile하나가 아니라MultipartFile[]배열로 받아야 한다.
이제 실제 저장을 처리하는 컨트롤러를 보면 반복문 흐름이 핵심이다.// UploadController2.java package com.example.springedu.controller; import java.io.File; import java.io.IOException; import com.example.springedu.domain.FileVO2; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.ModelAndView; @Controller public class UploadController2 { @RequestMapping("/uploadForm2") public void formFile() { // 요청 경로 이름으로 화면을 찾는다 } @RequestMapping("/upload2") public ModelAndView saveFile(FileVO2 vo) { System.out.println(vo.getUploadFiles().length + "개가 업로드 됨"); // 업로드된 파일 개수 확인 String resultStr = ""; String path = "/Users/leeunduck/uploadtest/multi"; // 여러 파일 저장 경로 File isDir = new File(path); if (!isDir.isDirectory()) { isDir.mkdirs(); // 저장 폴더가 없으면 생성 } ModelAndView mav = new ModelAndView(); mav.setViewName("uploadForm2"); // 같은 화면 재사용 for (MultipartFile mfile : vo.getUploadFiles()) { // 여러 파일을 하나씩 꺼내 처리 String fileName = mfile.getOriginalFilename(); // 실제 업로드 파일명 확인 try { File f = new File(path + "/" + fileName); // 저장할 실제 파일 경로 생성 if (f.exists()) { // 같은 이름의 파일이 이미 있으면 resultStr += fileName + " : 파일이 이미 존재해요!!<br>"; } else { mfile.transferTo(f); // 실제 파일 저장 resultStr += fileName + " : 파일이 저장되었어요!!<br>"; } } catch (IOException e) { e.printStackTrace(); resultStr += fileName + " : 오류가 발생했어요!!<br>"; } } mav.addObject("msg", resultStr); // 여러 처리 결과를 한 번에 전달 return mav; } }이 코드에서 핵심은
vo.getUploadFiles()이다.
이번에는 파일 하나가 아니라 여러 파일이 배열로 바인딩되어 들어온다.
그래서 컨트롤러는 그 배열을 반복문으로 돌면서 파일 하나씩 꺼내 처리해야 한다.
즉, 이 예제는 여러 파일 업로드를 결국 “파일 하나 처리 로직을 여러 번 반복하는 구조”로 해결한다는 점을 보여 준다.
그리고 저장 방식은 앞의 1번과 다르다.
1번은getBytes()와FileOutputStream을 사용했다.
이번에는mfile.transferTo(f)를 사용한다.
즉, 이 예제는 여러 파일 업로드에서transferTo()가 실제로 어떻게 쓰이는지를 보여 준다.
또resultStr += ...부분도 중요하다.
여러 파일을 처리하면 결과도 여러 줄이 될 수 있다.
그래서 결과 문자열을 하나씩 이어 붙인 뒤, 마지막에 한 번에msg로 넘긴다.
즉, 이 예제는 여러 파일의 저장 결과를 누적해서 같은 화면에 출력하는 구조도 함께 보여 준다.
그래서 결과가 이렇게 나온다
이 묶음의 실행 결과를 보면, 여러 파일을 선택해서 업로드하는 흐름이 보인다.
그리고 업로드가 끝나면 여러 파일 처리 결과가 한 번에 메시지로 출력된다.
이 장면에서 중요한 것은 결과가 한 줄이 아니라 여러 줄로 누적되어 보인다는 점이다.
파일을 여러 개 처리했기 때문에, 각 파일의 저장 성공 또는 중복 여부가 결과 문자열에 차례대로 붙는다.
즉, 이 예제는 여러 파일 업로드에서는 결과도 파일 개수만큼 누적해서 보여 줄 수 있다는 점을 보여 준다.
그래서 이 묶음의 전체 흐름은
여러 파일 선택 →VO배열 바인딩 → 반복문으로 파일 하나씩 처리 →transferTo()저장 → 결과 문자열 누적 → 같은 화면에 한 번에 출력
이라고 정리할 수 있다.
이 묶음에서 꼭 잡아야 하는 핵심은 이것이다.
여러 파일 업로드는 파일 하나 처리 로직을 배열 바인딩과 반복문으로 확장한 구조라는 점이다.
3. MultipartRequest로 여러 파일 저장하기 (UploadController3.java, uploadForm3.html)
이 예제도 여러 파일 업로드를 처리한다.
하지만 2번과는 받는 방식이 다르다.
2번은 업로드용VO안에MultipartFile[]배열을 넣어서 받았다.
이번에는 업로드 요청 자체를MultipartRequest로 받고, 그 안에서getFiles("mfile")로 파일 목록을 꺼내는 구조이다.
즉, 이 묶음은 여러 파일 업로드를 처리할 때 배열 바인딩 방식 말고MultipartRequest방식도 사용할 수 있다는 점을 보여 주는 예제이다.
이 예제가 같이 보여주는 개념
- 여러 파일 업로드는
MultipartRequest로도 받을 수 있다.getFiles("mfile")를 사용하면 같은 이름으로 전송된 파일 목록을List<MultipartFile>로 꺼낼 수 있다.- 업로드 폼의 파일 입력칸 이름과
getFiles()안의 이름이 같아야 정상적으로 파일 목록을 얻을 수 있다.- 저장 폴더가 없으면
mkdirs()로 폴더를 먼저 만들 수 있다.- 반복문으로 파일 목록을 돌면서 각 파일을 저장할 수 있다.
transferTo()로 여러 파일을 실제 경로에 저장할 수 있다.- 각 파일 결과를 문자열로 누적해 한 번에 화면에 보여 줄 수 있다.
즉, 이 예제는 여러 파일 업로드를
요청 객체에서 직접 목록을 꺼내 처리하는 방식으로 보여 준다.
코드 설명
먼저 업로드 화면을 보면 여러 파일 선택 구조는 2번과 비슷하다.
// uploadForm3.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>JSP 테스트 예제</title> </head> <body> <h2>SpringMVC FileUpload(Multiple)-MultipartRequest</h2> <hr> <form th:unless="${ msg }" action="/upload3" enctype="multipart/form-data" method="post"> <input type="file" name="mfile" multiple/> <!-- 여러 파일 선택 --> <input type="submit" value="파일올리기"/> <!-- 업로드 요청 전송 --> </form> <h2 th:if="${ msg }" th:utext="${ msg }"></h2> <!-- 여러 결과 메시지 출력 --> </body> </html>여기서 중요한 것은 파일 입력칸 이름이
mfile이라는 점이다.
이 이름은 나중에 컨트롤러에서mreq.getFiles("mfile")로 연결된다.
즉, 이번에도 여전히 폼의 이름과 서버에서 꺼내는 이름이 정확히 맞아야 한다.
이제 실제 저장을 처리하는 컨트롤러를 보면MultipartRequest를 직접 받고 있다.// UploadController3.java package com.example.springedu.controller; import java.io.File; import java.io.IOException; import java.util.List; import jakarta.servlet.ServletContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartRequest; import org.springframework.web.servlet.ModelAndView; @Controller public class UploadController3 { @Autowired ServletContext context; @RequestMapping("/uploadForm3") public void formFile() { // 요청 경로 이름으로 화면을 찾는다 } @RequestMapping("/upload3") public ModelAndView saveFile(MultipartRequest mreq) { ModelAndView mav = new ModelAndView(); List<MultipartFile> list = mreq.getFiles("mfile"); // 같은 이름의 업로드 파일 목록 추출 System.out.println(list.size() + "개가 업로드 됨"); // 업로드된 파일 개수 확인 String resultStr = ""; String path = "/Users/leeunduck/uploadtest//multi"; // 여러 파일 저장 경로 File isDir = new File(path); if (!isDir.isDirectory()) { isDir.mkdirs(); // 저장 폴더가 없으면 생성 } mav.setViewName("uploadForm3"); // 같은 화면 재사용 for (MultipartFile mfile : list) { // 파일 목록을 하나씩 꺼내 처리 String fileName = mfile.getOriginalFilename(); // 실제 업로드 파일명 확인 try { File f = new File("/Users/leeunduck/uploadtest//multi/" + fileName); // 저장할 실제 파일 경로 생성 if (f.exists()) { // 같은 이름의 파일이 이미 있으면 resultStr += fileName + " : 파일이 이미 존재해요!!<br>"; } else { mfile.transferTo(f); // 실제 파일 저장 resultStr += fileName + " : 파일이 저장되었어요!!<br>"; } } catch (IOException e) { e.printStackTrace(); resultStr += fileName + " : 오류가 발생했어요!!<br>"; } } mav.addObject("msg", resultStr); // 여러 처리 결과를 한 번에 전달 return mav; } }이 코드에서 가장 중요한 부분은
saveFile(MultipartRequest mreq)이다.
이번에는 업로드 파일을VO배열로 받는 것이 아니라, 업로드 요청 전체를 받을 수 있는MultipartRequest를 받고 있다.
즉, 이 예제는 업로드 요청 객체에서 직접 파일 목록을 꺼내는 방식이다.
그리고mreq.getFiles("mfile")를 보면, 같은 이름mfile로 전달된 파일들을List<MultipartFile>형태로 꺼내고 있다.
즉, 여러 파일 업로드를 배열 대신 리스트로 처리하는 구조라고 이해하면 된다.
그 이후 흐름은 2번과 비슷하다.
반복문으로 파일을 하나씩 꺼내고,getOriginalFilename()으로 실제 파일명을 확인하고,transferTo()로 저장하고, 결과 문자열을 누적한다.
즉, 받는 방식은 달라도 이후 저장 흐름은 결국 파일 하나씩 반복 처리하는 구조는 같다.
또ServletContext가 주입되어 있지만, 현재 코드는 실제 저장 경로를 직접 문자열로 지정해서 사용하고 있다.
즉, 이 예제에서는ServletContext보다MultipartRequest로 파일 목록을 꺼내는 방식이 핵심이다.
그래서 결과가 이렇게 나온다
이 묶음의 실행 결과를 보면, 여러 파일을 선택해서 업로드하는 흐름이 보인다.
그리고 업로드가 끝나면 여러 파일 처리 결과가 한 번에 메시지로 출력된다.
이 장면에서 중요한 것은 2번과 결과 화면은 비슷해 보여도, 컨트롤러가 파일을 받는 방식이 다르다는 점이다.
2번은VO안의MultipartFile[]배열을 사용했다.
이번에는MultipartRequest에서getFiles("mfile")로 파일 목록을 직접 꺼냈다.
즉, 여러 파일 업로드는 결과는 비슷해도 컨트롤러가 파일을 받는 방식은 두 가지로 나뉠 수 있다는 점을 보여 준다.
그래서 이 묶음의 전체 흐름은
여러 파일 선택 →MultipartRequest로 업로드 요청 받기 →getFiles("mfile")로 파일 목록 추출 → 반복문으로 파일 하나씩 처리 →transferTo()저장 → 결과 문자열 누적 출력
이라고 정리할 수 있다.
이 묶음에서 꼭 잡아야 하는 핵심은 이것이다.
여러 파일 업로드는VO배열 방식으로도 처리할 수 있고,MultipartRequest에서 직접 파일 목록을 꺼내는 방식으로도 처리할 수 있다는 점이다.
여기서 먼저 정리해야 할 핵심
- 1번은 파일 하나를 업로드용
VO안의MultipartFile로 받아 저장하는 흐름이다.- 2번은 여러 파일을 업로드용
VO안의MultipartFile[]배열로 받아 반복 처리하는 흐름이다.- 3번은 여러 파일을
MultipartRequest로 받고,getFiles()로 파일 목록을 꺼내 처리하는 흐름이다.- 1번은 저장할 때
getBytes()와FileOutputStream을 사용했다.- 2번과 3번은 저장할 때
transferTo()를 사용했다.- 세 예제 모두 같은 이름의 파일이 이미 있으면 저장을 막고 메시지를 다르게 보여 준다.
- 세 예제 모두 결과 화면을 새로 만들기보다 같은 화면을 다시 사용하면서 메시지를 출력하는 구조를 사용한다.
즉, 파일 업로드 응용예제는 겉으로 보면 비슷해 보여도,
파일 하나인지 여러 개인지,
VO로 받을지MultipartRequest로 받을지,
어떤 메서드로 저장할지에 따라 코드 구조가 달라진다.
하지만 공통 흐름은 같다.
결국 파일 업로드는 폼 전송 → 파일 바인딩 → 실제 저장 → 결과 출력으로 이어지는 구조라고 이해하면 된다.
4. canvas에 그린 그림을 업로드하고 다시 불러오기 (UploadController4.java, canvas_memo.html, canvas_memo.js)
이 예제는 앞에서 본 일반 파일 업로드와 결이 조금 다르다.
앞의 예제들은 사용자가input type="file"로 이미 존재하는 파일을 선택해서 서버로 보냈다.
그런데 이번 예제는 브라우저 안의canvas에 직접 그린 그림을 서버로 업로드한다.
즉, 브라우저에서 만든 그림 데이터를 파일처럼 만들어 업로드하고, 다시 불러와서 복원하는 흐름을 보여 주는 예제이다.
그래서 이 묶음은 단순히 파일 업로드만 보여 주는 것이 아니라,canvas그림 생성,Blob만들기,FormData전송,MultipartFile수신, 서버 저장, 문자열 응답, 이미지 복원까지 한 번에 연결해서 보여 준다.
이 예제가 같이 보여주는 개념
canvas.toDataURL()을 사용하면 현재canvas그림을 문자열 데이터로 바꿀 수 있다.Blob과FormData를 사용하면 브라우저 안에서 만든 데이터를 파일처럼 전송할 수 있다.data.append("mfile", ...)의 이름과 컨트롤러의MultipartFile mfile이름이 같아야 정상적으로 연결된다.@RestController는 뷰를 찾지 않고 응답 본문 자체를 바로 반환한다.- 업로드 요청 결과를
"OK","FAIL"같은 짧은 문자열로 바로 응답할 수 있다.getBytes()와FileOutputStream으로 업로드 데이터를 실제 경로에 저장할 수 있다.- 이 예제의 다운로드는 일반 파일 다운로드가 아니라, 저장된 문자열 데이터를 다시 읽어 이미지로 복원하는 흐름이다.
localStorage에 저장하는save(),restore()와 서버에 저장하는upload(),downimage()는 서로 다른 기능이다.즉, 이 예제는 파일 업로드를 이미 있는 파일을 보내는 기능으로만 보지 않고, 브라우저 안에서 만든 데이터도 파일처럼 보내고 다시 복원할 수 있는 기능으로 확장해서 보여 준다.
코드 설명
먼저 화면을 보면 버튼과
canvas가 같이 있다.// canvas_memo.html <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"/> <title>Draw memo on canvas</title> </head> <body> <button onclick="save();">Save</button> <button onclick="restore();">Restore</button> <button onclick="initialize();">Clear</button> <button onclick="upload();">Upload</button> <button onclick="downimage();">DownImage</button><br><br> <button onclick="setColor('red');">RED</button> <button onclick="setColor('blue');">BLUE</button> <button onclick="setColor('green');">GREEN</button> <button onclick="setColor('yellow');">YELLOW</button> <button onclick="setColor('black');">BLACK</button><br><br> <canvas id="myCanvas" width="580" height="450"></canvas> <script src="canvas_memo.js"></script> </body> </html>이 화면에서 중요한 것은
Upload와DownImage버튼이다.
Save,Restore는 브라우저의localStorage를 사용하는 기능이고,Upload,DownImage는 서버와 통신하는 기능이다.
즉, 이 예제는 브라우저 내부 저장과 서버 저장을 같이 보여 주지만, 실제 파일 업로드와 직접 연결되는 것은Upload,DownImage쪽이다.
이제JavaScript를 보면 업로드와 복원 흐름이 더 분명해진다.// canvas_memo.js function upload() { var data = new FormData(); // 업로드용 폼 데이터 객체 생성 var myblob = new Blob([canvas.toDataURL()], {type : 'text/plain'}); // canvas 그림을 문자열로 만든 뒤 Blob으로 감쌈 data.append('mfile', myblob, "test.png"); // mfile 이름으로 서버에 보낼 데이터 추가 var xhr = new XMLHttpRequest(); xhr.onload = function() { if (xhr.responseText == "OK") alert("UPLOAD SUCCESS"); // 업로드 성공 else alert("UPLOAD FAIL"); // 업로드 실패 }; xhr.open("POST", "/canvasupload", true); // 업로드 요청 xhr.send(data); } function downimage() { var xhr = new XMLHttpRequest(); xhr.onload = function() { var img = new Image(); img.onload = function() { context.drawImage(img, 0, 0); // 받은 이미지를 다시 canvas에 그림 } img.src = xhr.responseText; // 서버가 돌려준 문자열을 이미지 소스로 사용 } xhr.open("GET", "/canvasdownload", true); // 저장된 데이터 읽기 요청 xhr.send(); }여기서 가장 중요한 줄은
canvas.toDataURL()이다.
이 메서드는 현재canvas에 그려진 그림을 문자열 형태로 바꾼다.
즉, 지금 이 예제는 진짜 이미지 파일 자체를 바로 보내는 것이 아니라, 그림을 문자열 데이터로 바꾼 뒤 그것을 업로드하는 구조이다.
그다음new Blob([canvas.toDataURL()], {type : 'text/plain'})도 중요하다.
이 코드는canvas그림 문자열을Blob으로 감싼다.
그리고data.append('mfile', myblob, "test.png")를 보면, 이Blob을mfile이라는 이름으로FormData에 넣고 있다.
즉, 브라우저 안에서 만든 데이터를 파일처럼 포장해서 서버로 보내는 구조이다.
또 여기서 초보자가 가장 헷갈리기 쉬운 부분이 있다.
파일 이름은"test.png"이지만, 실제 내용은 일반PNG바이너리 파일이 아니다.
안에 들어 있는 내용은canvas.toDataURL()이 만든 문자열 데이터이다.
즉, 확장자는.png처럼 보이지만, 실제 저장 내용은 그림을 나타내는 문자열이라는 점이 이 예제의 핵심이다.
이제 서버 쪽 코드를 보면 왜 이런 흐름이 가능한지가 보인다.// UploadController4.java package com.example.springedu.controller; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController public class UploadController4 { @RequestMapping(value="/canvasupload", produces="text/plain; charset=utf-8") public String saveFile(MultipartFile mfile) { String fileName = mfile.getOriginalFilename(); // 업로드 파일명 확인 byte[] content = null; String result = "OK"; try { content = mfile.getBytes(); // 업로드 데이터 읽기 File f = new File("/Users/leeunduck/uploadtest/" + fileName); // 저장 경로 생성 FileOutputStream fos = new FileOutputStream(f); fos.write(content); // 실제 파일 저장 fos.close(); } catch (IOException e) { e.printStackTrace(); result = "FAIL"; } return result; // 결과 문자열 직접 응답 } @RequestMapping(value="/canvasdownload", produces="text/plain; charset=utf-8") public String downloadFile() { String path = "/Users/leeunduck/uploadtest/"; char[] buffer = null; try { FileReader reader = new FileReader(path + "test.png"); // 저장된 문자열 데이터 읽기 buffer = new char[(int)(new File(path + "test.png").length())]; reader.read(buffer); reader.close(); } catch (Exception e) { e.printStackTrace(); } return new String(buffer); // 읽은 문자열을 그대로 응답 } }이 코드에서 먼저
@RestController를 봐야 한다.
이 예제는 결과 화면으로 이동하는 구조가 아니라,"OK","FAIL"같은 문자열이나 다운로드용 문자열 데이터를 응답 본문으로 바로 보내는 구조이다.
즉, 앞의 예제들처럼 같은 화면을 다시 보여 주는 방식이 아니라, 이번에는AJAX요청에 맞게 데이터 자체를 바로 응답하는 방식이다.
saveFile(MultipartFile mfile)를 보면,MultipartFile이름이mfile이다.
이 이름은JavaScript의data.append('mfile', ...)와 정확히 맞아야 한다.
즉, 이번에도 결국 브라우저에서 보내는 이름과 컨트롤러 매개변수 이름이 연결 기준이 된다.
그리고mfile.getBytes()로 업로드된 내용을 읽고,FileOutputStream으로/Users/leeunduck/uploadtest/아래에 저장한다.
이 부분은 1번 예제와 비슷하다.
다만 여기서는 사용자가 고른 일반 파일이 아니라, 브라우저가 만든canvas데이터 문자열을 저장하고 있다는 점이 다르다.
또 이 코드는 중복 검사 없이 같은 이름이면 그냥 다시 저장하기 때문에, 이전 예제들처럼 중복 메시지를 만드는 구조는 아니다.
즉, 이번 예제는 덮어쓰기 흐름에 더 가깝다.
downloadFile()도 중요하다.
여기서는FileReader로test.png를 문자처럼 읽고, 그 결과를 문자열로 반환한다.
일반적인PNG바이너리 파일이라면 이렇게 읽는 흐름이 자연스럽지 않다.
그런데 이번 예제는 저장된 내용 자체가canvas.toDataURL()문자열이기 때문에, 문자로 읽어서 다시 문자열로 돌려줘도 동작한다.
즉, 이 예제는 파일처럼 저장했지만, 실제로는 문자열 데이터를 저장하고 다시 돌려주는 구조이다.
그래서 결과가 이렇게 나온다
이 묶음의 실행 결과를 보면, 먼저
canvas에 그림을 그리고Upload버튼을 눌러 업로드하는 흐름이 보인다.
이 장면에서 중요한 것은 일반 파일 선택 창을 거치지 않았다는 점이다.
사용자가 직접 파일을 고른 것이 아니라, 브라우저 안의canvas그림이Blob과FormData를 통해 서버로 전송되었다.
즉, 이 예제는 브라우저에서 만든 데이터를 파일처럼 업로드할 수 있다는 점을 실제로 보여 준다.
그리고 업로드가 끝나면uploadtest폴더 아래에test.png가 저장된 것을 확인할 수 있다.
이 장면에서 초보자가 꼭 알아야 할 점은, 이 파일이 이름은test.png이지만 내부에는canvas.toDataURL()에서 만든 문자열 데이터가 들어 있다는 점이다.
즉, 겉보기에는 이미지 파일처럼 저장되어 있어도, 이번 예제의 핵심은 그림 데이터를 문자열로 저장하고 다시 복원하는 흐름에 있다.
그래서 이 묶음의 전체 흐름은canvas에 그림 그리기 →toDataURL()로 문자열 만들기 →Blob과FormData로 업로드 →MultipartFile로 받기 → 실제 경로 저장 → 다시 문자열로 읽기 →img.src에 넣어 복원하기라고 정리할 수 있다.
이 묶음에서 꼭 잡아야 하는 핵심은 이것이다.
파일 업로드는 이미 있는 파일만 보내는 기능이 아니라, 브라우저 안에서 만든 데이터도 파일처럼 만들어 서버에 저장하고 다시 불러올 수 있는 기능이라는 점이다.
5. 저장된 Data URL 문자열을 다시 이미지로 출력하기 (DataURLController.java, imgtag.html)
이 예제는 바로 앞의
canvas업로드 예제와 자연스럽게 이어진다.
앞 예제에서는canvas.toDataURL()로 만든 문자열 데이터를test.png라는 이름으로 저장했다.
이번 예제는 그 저장된 문자열을 다시 읽어서 브라우저로 보내고, 브라우저가 그 문자열을img태그의src에 넣어 이미지를 다시 보여 주는 흐름을 확인하는 예제이다.
즉, 이 묶음은 일반적인 파일 다운로드 예제가 아니다.
서버가 이미지 파일 자체를 바이너리로 내려주는 것이 아니라, 그림을 표현하는 문자열 데이터를 다시 보내고, 브라우저가 그 문자열을 이미지 주소처럼 해석해서 화면에 그리는 구조를 보여 준다.
이 예제가 같이 보여주는 개념
canvas.toDataURL()로 만든 문자열은 그림을 나타내는Data URL데이터이다.- 서버는 저장된
Data URL문자열을 다시 읽어서 그대로 응답할 수 있다.@RestController는 뷰를 찾지 않고 응답 본문 자체를 바로 반환한다.produces="text/plain; charset=US-ASCII"는 지금 반환값이 화면 이름이 아니라 일반 문자열이라는 뜻이다.fetch("/dataurl")로 받은 응답은 문자열이므로res.text()로 읽는다.- 브라우저는
img.src = 문자열처럼Data URL을 이미지 주소처럼 사용할 수 있다.- 즉, 이 예제는 저장된 그림 문자열 → 서버 응답 → 브라우저 이미지 복원 흐름을 보여 준다.
즉, 이 예제의 핵심은 파일처럼 저장된 문자열 데이터를 다시 읽어 와서, 브라우저가 그것을 이미지로 복원할 수 있다는 점이다.
코드 설명
먼저 서버 쪽 컨트롤러를 보면
/dataurl요청이 들어왔을 때test.png파일 내용을 문자 단위로 읽어서 그대로 문자열로 반환한다.// DataURLController.java package com.example.springedu.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.File; import java.io.FileReader; @RestController public class DataURLController { @RequestMapping(value = "/dataurl", produces="text/plain; charset=US-ASCII") public String getDataURL() { char[] content = null; File f = new File("/Users/leeunduck/uploadtest/test.png"); // 저장된 test.png 확인 if (f.exists() && f.length() > 0) { content = new char[(int)f.length()]; // 파일 길이만큼 문자 배열 준비 try (FileReader reader = new FileReader("/Users/leeunduck/uploadtest/test.png")) { reader.read(content); // 파일 내용을 문자로 읽음 } catch (Exception e) { e.printStackTrace(); } } return new String(content); // 읽은 문자열을 그대로 응답 } }여기서 가장 중요한 점은
FileReader를 사용하고 있다는 것이다.
일반적인 진짜PNG바이너리 파일이라면 이렇게 문자로 읽는 흐름이 자연스럽지 않다.
그런데 지금test.png안에는 앞 예제에서 저장한canvas.toDataURL()문자열이 들어 있으므로, 문자로 읽어서 다시 문자열로 반환하는 흐름이 가능하다.
또@RestController와produces="text/plain; charset=US-ASCII"도 중요하다.
이 설정 때문에 이 메서드는 어떤 화면으로 이동하는 것이 아니라, 파일에서 읽은 문자열 자체를 그대로 응답 본문으로 보낸다.
즉, 이 예제는 뷰 반환이 아니라 문자열 데이터 반환 구조이다.
이제 브라우저 쪽 코드를 보면,/dataurl에서 받은 문자열을 어떻게 쓰는지가 더 분명해진다.// imgtag.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>Data URL 형식 테스트!!!</h1> <img width="500" src=""> <script> window.addEventListener("load", () => { fetch("/dataurl") // 서버에 저장된 Data URL 문자열 요청 .then((res) => { return res.text(); // 응답 본문을 문자열로 읽음 }).then((content) => { alert(content); // 받은 문자열 확인 document.querySelector("img").src = content; // 문자열을 이미지 주소로 사용 }); }); </script> </body> </html>이 코드에서 핵심은
fetch("/dataurl")와res.text()이다.
서버가 돌려주는 값은JSON도 아니고HTML도 아니라 일반 문자열이기 때문에text()로 읽는다.
그리고 그 문자열을alert(content)로 한 번 확인한 뒤,document.querySelector("img").src = content;로 이미지 태그의src에 그대로 넣는다.
즉, 브라우저는 이 문자열을 그냥 글자로만 보는 것이 아니라,data:image/png;base64,...같은 형식의Data URL이면 이미지 주소처럼 해석할 수 있다.
그래서 사용자는 파일을 다시 선택하거나 업로드하지 않아도, 서버가 돌려준 문자열만으로 이미지를 다시 볼 수 있게 된다.
그래서 결과가 이렇게 나온다
먼저
/dataurl주소를 직접 열어 보면, 화면에는 아주 긴 문자열이 보인다.
이 문자열은 일반 문장이 아니라data:image/png;base64,...형태의Data URL데이터이다.
즉, 서버가 실제로 보내고 있는 것은 그림을 표현하는 문자열이라는 점을 여기서 바로 확인할 수 있다.
로 문자열 수신 →img.src에 대입 → 이미지 복원이라고 정리할 수 있다.
이 묶음에서 꼭 잡아야 하는 핵심은 이것이다.
브라우저에서 만든 그림은 문자열 형태로 저장할 수도 있고, 서버가 그 문자열을 다시 돌려주면 브라우저는 그것을 이미지로 다시 복원할 수 있다.