파일 업로드 소개
일반적으로 사용하는 HTML Form을 통한 파일 업로드를 이해하려면
먼저 폼을 전송하는 다음 두 가지 방식의 차이를 이해해야 한다.
HTML 폼 전송 방식
- application/x-www-form-urlencoded (일반적으로 form 전송하는 방식)
- multipart/form-data
application/x-www-form-urlencoded 방식
application/x-www-form-urlencoded 방식은 HTML 폼 데이터를 서버로 전송하는 가장 기본적인 방법이다.
Form 태그에 별도의 enctype 옵션이 없으면 웹 브라우저는 요청 HTTP 메시지의 헤더에 다음 내용을 추가한다.
Content-Type: application/x-www-form-urlencoded
그리고 폼에 입력한 전송할 항목을 HTTP Body에 문자로 username=kim&age=20 와 같이 & 로 구분해서 전송한다.
파일을 업로드 하려면 파일은 문자가 아니라 바이너리 데이터를 전송해야 한다.
문자를 전송하는 이 방식으로 파일을 전송하기는 어렵다.
그리고 또 한가지 문제가 더 있는데, 보통 폼을 전송할 때 파일만 전송하는 것이 아니라는 점이다.
다음 예를 보자.
- 이름
- 나이
- 첨부파일
여기에서 이름과 나이도 전송해야 하고, 첨부파일도 함께 전송해야 한다.
문제는 이름과 나이는 문자로 전송하고, 첨부파일은 바이너리로 전송해야 한다는 점이다.
여기에서 문제가 발생한다. 문자와 바이너리를 동시에 전송해야 하는 상황이다.
이 문제를 해결하기 위해 HTTP는 multipart/form-data 라는 전송 방식을 제공한다.
multipart/form-data 방식
이 방식을 사용하려면 Form 태그에 별도의 enctype="multipart/form-data" 를 지정해야 한다.
multipart/form-data 방식은 다른 종류의 여러 파일과 폼의 내용 함께 전송할 수 있다. (그래서 이름이 multipart 이다.)
폼의 입력 결과로 생성된 HTTP 메시지를 보면 각각의 전송 항목이 구분이 되어있다.
ContentDisposition이라는 항목별 헤더가 추가되어 있고 여기에 부가 정보가 있다.
예제에서는 username , age , file1 이 각각 분리되어 있고, (boundary로 지정된 문자를 통해서 값들이 구분이 된다.)
폼의 일반 데이터는 각 항목별로 문자가 전송되고,
파일의 경우 파일 이름과 Content-Type이 추가되고 바이너리 데이터가 전송된다.
(filename을 읽어서 ContentType이 뭔지 브라우저가 자동으로 만들어준다. ( image/png로 되어있는걸 볼수있다))
multipart/form-data 는 이렇게 각각의 항목을 구분해서, 한번에 전송하는 것이다
Part
multipart/form-data는 application/x-www-form-urlencoded 와 비교해서
매우 복잡하고 각각의 부분( Part )로 나누어져 있다.
그렇다면 이렇게 복잡한 HTTP 메시지를 서버에서 어떻게 사용할 수 있을까?
(application/x-www-form-urlencoded의 ContentType으로 오는 Http요청메시지보다 훨씬 복잡해보인다.)
참고
multipart/form-data 와 폼 데이터 전송에 대한 더 자세한 내용은 모든 개발자를 위한 HTTP 웹 기본 지식 강의를 참고하자.
서블릿과 파일 업로드1
먼저 서블릿을 통한 파일 업로드를 코드와 함께 알아보자.
ServletUploadControllerV1
multipart/form-data 방식으로 넘어오는 request와 parts들에 대해 로그를 찍어본다.
@Slf4j
@Controller
@RequestMapping("/servlet/v1")
public class ServletUploadControllerV1 {
@GetMapping("/upload")
public String newFile() {
return "upload-form";
}
@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";
}
}
Collection<Part> parts = request.getParts();
를 이용해서 받는 part란 위쪽에서 봤던 그림에서 boundary로 나누어진 각각의 부분을 말한다.
뷰 템플릿
<form th:action method="post" enctype="multipart/form-data">
<ul>
<li>상품명 <input type="text" name="itemName"></li>
<li>파일<input type="file" name="file" ></li>
</ul>
<input type="submit"/>
</form>
보면 form 태그에
enctype="multipart/form-data"
다음과 같은 속성이 들어가있다. (multipart/form-data 방식을 사용하기 위함)
Application.properties
logging.level.org.apache.coyote.http11=debug
이 옵션을 사용하면 HTTP 요청 메시지를 로그로 확인할 수 있다.
결과
상품이름과, 파일을 첨부해서 제출하기를 누르면
개발자 도구에서 ContentType에서 multipart/form-data와 boundary를 확인할 수 있다.
그리고
application.properties에서
logging.level.org.apache.coyote.http11=debug
이렇게 설정해둔 덕에 http 요청에 대한 정보를 로그에서 볼수 있는데
메시지바디에 내용을 확인할 수 있다. boundary 문자로 part가 각각 구분되어있는것을 볼 수 있다.
아래 part에서는 이미지의 바이너리 데이터도 확인할 수 있었다.
그리고 핸들러메소드에서 로그를 찍은걸 보면
HttpServletRequest에 대한 정보
들어온 아이템이름에 대한 정보
HttpServletRequest의 Parts 에 대한 정보를 확인할 수 있다.
멀티파트 사용 옵션
업로드 사이즈 제한
application.properties
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
큰 파일을 무제한 업로드하게 둘 수는 없으므로 업로드 사이즈를 제한할 수 있다.
사이즈를 넘으면 예외( SizeLimitExceededException )가 발생한다.
max-file-size : 파일 하나의 최대 사이즈, 기본 1MB
max-request-size : 멀티파트 요청 하나에 여러 파일을 업로드 할 수 있는데, 그 전체 합이다. 기본 10MB
spring.servlet.multipart.enabled 끄기
application.properties
spring.servlet.multipart.enabled=false
이렇게 설정하고 다시 테스트해보면
로그에서
다음과 같이 나온다. 아이템이름도 null로 되어있고, parts도 비어있다.
분명히
이렇게 http요청 메시지 바디에 멀티파트폼데이터형식으로 들어온것을 확인했다.
하지만 HttpServletRequest로는 들어오지 않은것!
결론
spring.servlet.multipart.enabled 옵션을 끄면 서블릿 컨테이너는 멀티파트와 관련된 처리를 하지 않는다.
(false로 해두면 멀티파트폼데이터 형식으로 http요청을 해도 값을 받지 못한다.)
spring.servlet.multipart.enabled=true (기본 true) 기본값은 true이다.
이 옵션을 켜면 스프링 부트는 서블릿 컨테이너에게 멀티파트 데이터를 처리하라고 설정한다. 참고로 기본 값은 true 이다.
true로 바꾸고 로그를 다시 보면
HttpServletRequest 객체가
RequestFacade --> StandardMultipartHttpServletRequest 로 변한 것을 확인할 수 있다.
참고
spring.servlet.multipart.enabled 옵션을 켜면
스프링의 DispatcherServlet에서 멀티파트 리졸버( MultipartResolver )를 실행한다.
멀티파트 리졸버는 멀티파트 요청인 경우 서블릿 컨테이너가 전달하는 일반적인 HttpServletRequest를 MultipartHttpServletRequest 로 변환해서 반환한다.
MultipartHttpServletRequest 는 HttpServletRequest 의 자식 인터페이스이고,
멀티파트와 관련된 추가 기능을 제공한다.
스프링이 제공하는 기본 멀티파트 리졸버는 MultipartHttpServletRequest 인터페이스를 구현한 StandardMultipartHttpServletRequest 를 반환한다.
이제 컨트롤러에서 HttpServletRequest 대신에 MultipartHttpServletRequest 를 주입받을 수 있는데,
public String saveFileV1(MultipartHttpServletRequest request) throws ServletException, IOException {
이것을 사용하면 멀티파트와 관련된 여러가지 처리를 편리하게 할 수 있다.
그런데 이후 강의에서 설명할 MultipartFile 이라는 것을 사용하는 것이 더 편하기 때문에
MultipartHttpServletRequest 를 잘 사용하지는 않는다. 더 자세한 내용은 MultipartResolver 를 검색해보자.
서블릿과 파일 업로드2
서블릿이 제공하는 Part 에 대해 알아보고 실제 파일도 서버에 업로드 해보자
먼저 파일을 업로드를 하려면 실제 파일이 저장되는 경로가 필요하다.
해당 경로에 실제 폴더를 만들어두자. 그리고 다음에 만들어진 경로를 입력해두자.
application.properties
file.dir = C:/fileTest/
주의
1. 꼭 해당 경로에 실제 폴더를 미리 만들어두자.
2. application.properties 에서 설정할 때 마지막에 / (슬래시)가 포함된 것에 주의하자.
3. 경로에 한글이 포함되어있으면 오류가 발생한다.
ServletUploadControllerV2
@Slf4j
@Controller
@RequestMapping("/servlet/v2")
public class ServletUploadControllerV2 {
@Value("${file.dir}") // @Value는 Spring 꺼를 사용해야한다 , @Value를 통해 appilcation.* 에 지정한 값을 가져올 수 있다.
private String fileDir; // 가져온 값은 여기에 저장됨.
@GetMapping("/upload")
public String newFile() {
return "upload-form";
}
@PostMapping("/upload")
public String saveFileV2(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);
for (Part part : parts) {
log.info("=== PART ===");
log.info("name={}", part.getName());
Collection<String> headerNames = part.getHeaderNames();// 각각의 파트마다 가지고있는 헤더를 출력해본다.
for (String headerName : headerNames) {
log.info("header {} : {}", headerName, part.getHeader(headerName));
}
//http 구조안의 part안에도 각각의 헤더와 바디를 가지고있다. (Content-Disposition은 모든 part에도 가지고있는 헤더)
//이미지인경우는 part안에 Content-Type이라는 헤더를 하나 더 가지게 된다.
//편의메소드
// Content-Disposition이라는 헤더의 내용을 보면 Content-Disposition : form-data; name="itemName" 이런식이다.
// Content-Disposition이라는 헤더안의 name이라는 속성에는 어떤값이 들어갔는지 코드로 구현해서 찾는건 쉽지않은일이라서 편의메소드를 제공한다.
log.info("submittedFileName = {} ", part.getSubmittedFileName());
log.info("size = {} ", part.getSize()); //part body size
//데이터 읽기 (각 Part에 있는 바디데이터를 읽는다)
InputStream inputStream = part.getInputStream(); // part는 inputStream을 받을수있게 제공한다
String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); //StreamUtils를 이용해서 인풋스트림내용을 String으로 변환한다
log.info("body = {} ", body);
}
return "upload-form";
}
}
application.properties에 저장한 값을 @Value를 이용해서 가져오는 부분과
멀티파트폼데이터 형식으로 들어오는 각각의 part에 대해 정보를 로그로 찍어보는 메소드를 추가하였다.
결과
각 part에는 Content-Disposition라는 헤더가 기본적으로 존재하고 그 헤더안에는 form의 name이 있고 이미지라면 파일이름까지 저장되어있다.
이미지가 저장되어있는 part는 사이즈가 무려 28979라서 body의 데이터는 더 있지만 생략해서 캡쳐했다.
멀티파트 형식은 전송 데이터를 하나하나 각각 부분( Part )으로 나누어 전송한다.
parts 에는 이렇게 나누어진 데이터가 각각 담긴다.
서블릿이 제공하는 Part 는 멀티파트 형식을 편리하게 읽을 수 있는 다양한 메서드를 제공한다.
part.getSubmittedFileName() : 클라이언트가 전달한 파일명
part.getInputStream(): Part의 전송 데이터를 읽을 수 있다.
파일저장하는 부분 추가
//파일 저장하기
if (StringUtils.hasText(part.getSubmittedFileName())) { //해당 part에 파일네임이 있는가 -> 바디에 이미지 바이너리 데이터가 저장된 part인가 확인
String fullPath = fileDir + part.getSubmittedFileName(); //저장할 디렉토리 명 + 파일명을 합쳐서 어느곳에 어떤이름으로 저장될지에 대한 Path를 만들어준다.
log.info("파일 저장 fullPath = {}", fullPath);
part.write(fullPath); // part는 write(경로)라는 메소드를 지원한다. 경로를 넣어서 간편하게 파일을 저장하면 된다.
}
part.write(...): Part를 통해 전송된 데이터를 저장할 수 있다.
결과
핸들러메소드가 잘동작하여 로그도 확인되었고, 파일도 잘저장된걸 확인할 수 있었다. (잘 열림)
메모장으로 아무 텍스트파일을 만들고 테스트해본다.
파일이라면 잘 동작하는것을 확인할 수 있다. (이미지만 되는게 아님)
참고
큰 용량의 파일을 업로드를 테스트 할 때는 로그가 너무 많이 남아서 다음 옵션을 끄는 것이 좋다.
logging.level.org.apache.coyote.http11=debug
part의 body를 로그로 출력하는것도 너무 로그가 많이남으니까 주석처리 해둔다.
서블릿이 제공하는 Part 는 편하기는 하지만 HttpServletRequest 를 사용해야 하고,
추가로 파일 부분만 구분하려면 여러가지 코드를 넣어야 한다.
(위에서는 HttpServletRequest에서 모든 part를 꺼내고, 반복문을 돌려서 파일이름을 가지고있는 part에 대해 처리했다.)
이번에는 스프링이 이 부분을 얼마나 편리하게 제공하는지 확인해보자.
스프링과 파일 업로드
스프링은 MultipartFile이라는 인터페이스로 멀티파트 파일을 매우 편리하게 지원한다
SpringUploadController
@Slf4j
@Controller
@RequestMapping("/spring")
public class SpringUploadController {
@Value("${file.dir}") // @Value는 Spring 꺼를 사용해야한다 , @Value를 통해 appilcation.* 에 지정한 값을 가져올 수 있다.
private String fileDir; // 가져온 값은 여기에 저장됨.
@GetMapping("/upload")
public String newFile() {
return "upload-form";
}
@PostMapping("/upload")
public String saveFile(
@RequestParam String itemName, //첫번째 part인 itemName
@RequestParam MultipartFile file, //두번째 part인 파일
HttpServletRequest request) throws IOException {
log.info("request = {} ", request); // HttpServletRequest는 로그를 찍기위해 가져옴
log.info("itemName = {}", itemName);
log.info("multipartFile = {}", file);
if (!file.isEmpty()) {
String fullPath =
fileDir + file.getOriginalFilename();//MultipartFile의 getOriginalFilename()를 이용해서 파일의 이름을 가져올 수 있다.
log.info("파일 저장 fullPath = {}", fullPath);
file.transferTo(new File(fullPath)); // MultipartFile의 transferTo()를 이용해서 파일을 저장한다.
}
return "upload-form";
}
}
코드를 보면 스프링 답게 딱 필요한 부분의 코드만 작성하면 된다.
@RequestParam MultipartFile file
업로드하는 HTML Form의 name에 맞추어 @RequestParam 을 적용하면 된다.
추가로 @ModelAttribute 에서도 MultipartFile 을 동일하게 사용할 수 있다.
(ArgumentResolver가 멀티파트폼데이터의 내부에서 파일관련 part를 찾아 처리해주기 때문에
@RequestParam 이든 @ModelAttribute를 쓰던 MultiPartFile로 잘 변환시켜 넣어준다.)
결과
MultipartFile을 보면
스프링이 제공하는 기본 멀티파트 리졸버인 MultipartHttpServletRequest 인터페이스를 구현한 StandardMultipartHttpServletRequest클래스 내부에 있는 static 클래스인 StandardMultipartFile를 확인할 수 있다.
그리고 파일 또한 잘저장된것을 확인할 수 있었다.
MultipartFile 주요 메서드
file.getOriginalFilename() : 업로드 파일 명
file.transferTo(...) : 파일 저장
예제로 구현하는 파일 업로드, 다운로드
실제 파일이나 이미지를 업로드, 다운로드 할 때는 몇가지 고려할 점이 있는데, 구체적인 예제로 알아보자
요구사항
상품
- 상품 이름
- 첨부 파일 한개
- 이미지 파일 여러개
상품은 상품이름 , 파일 한개, 이미지 파일 여러개를 저장할 수 있어야한다.
첨부파일을 업로드 다운로드 할 수 있다.
업로드한 이미지를 웹 브라우저에서 확인할 수 있다.
위의 요구사항을 구현해보자.
Item
@Data
public class Item {
private Long id;
private String itemName;
private UploadFile attachFile;
private List<UploadFile> imageFiles;
}
UploadFile
@Data
public class UploadFile {
private String uploadFileName; // 업로드되는 파일이름
private String storeFileName; // 서버에 저장될 파일 이름 (업로드 되는 파일명이 겹칠 수 있으므로)
public UploadFile(String uploadFileName, String storeFileName) {
this.uploadFileName = uploadFileName;
this.storeFileName = storeFileName;
}
}
uploadFileName : 고객이 업로드한 파일명
storeFileName : 서버 내부에서 관리하는 파일명
고객이 업로드한 파일명으로 서버 내부에 파일을 저장하면 안된다.
왜냐하면 서로 다른 고객이 같은 파일이름을 업로드 하는 경우 기존 파일 이름과 충돌이 날 수 있다.
서버에서는 저장할 파일명이 겹치지 않도록 내부에서 관리하는 별도의 파일명이 필요하다
(UUID같은걸 붙여줘서 고유하게 만들고 서버에 저장해준다)
ItemRepository
@Repository
public class ItemRepository {
private final Map<Long, Item> store = new HashMap<>();
private Long sequence = 0L;
public Item save(Item item) {
item.setId(++sequence);
store.put(item.getId(), item);
return item;
}
public Item findById(Long id) {
return store.get(id);
}
}
FileStore - 파일 저장과 관련된 업무 처리
@Component
public class FileStore {
@Value("${file.dir}")
private String fileDir;
public String getFullPath(String filename) { //파일이름을 받아서 FullPath를 만들어주는 메소드
return fileDir + filename;
}
public List<UploadFile> storeFiles(List<MultipartFile> multipartFiles) throws IOException { //여러개의 이미지를 한번에 저장하기 위한 메소드
List<UploadFile> storeFileResult = new ArrayList<>();
for (MultipartFile multipartFile : multipartFiles) {
if (!multipartFile.isEmpty()) {
storeFileResult.add(storeFile(multipartFile));
}
}
return storeFileResult;
}
public UploadFile storeFile(MultipartFile multipartFile) throws IOException { // 파일을 서버에 저장하는 메소드
// MultipartFile로 받은후 서버에 저장하고, UploadFile 객체를 만들어 반환
if (multipartFile.isEmpty()) {
return null;
}
String originalFilename = multipartFile.getOriginalFilename(); //파일의 이름가져온다 (확장자가 포함되어있다.) ex) dog.png
String storeFileName = createStoreFileName(originalFilename); //서버에 저장될 파일명 만들기
multipartFile.transferTo(new File(getFullPath(storeFileName))); // 파일을 서버에 저장한다 , getFullPath메소드를 이용해서 PullPath를 만들어줌
//File은 객체를 생성할때 Path를 파라미터로 받는다.
return new UploadFile(originalFilename, storeFileName);
}
private static String createStoreFileName(String originalFilename) {//서버에 저장될 파일명 만드는 메소드
//서버에 저장될 파일명은 고유해야하므로 UUID를 붙여서 만들어준다.
String uuid = UUID.randomUUID().toString();
//확장자는 붙여줘야 구분하기 편하니까 originalFilename에서 확장자를 가져온다.
String ext = extractExt(originalFilename);
// uuid + "." + 확장자
return uuid + "." + ext; // 서버에 저장될 파일이름 생성
}
private static String extractExt(String originalFilename) { //파일이름에서 확장자 부분만 반환하는 메소드
int index = originalFilename.lastIndexOf("."); // 파일이름중 마지막 .의 인덱스를 가져온다.
return originalFilename.substring(index + 1); // . 위치 뒷부분을 subString으로 가져온다.
}
}
멀티파트 파일을 서버에 저장하는 역할을 담당한다.
( 멀티파트 파일을 이용해서 서버에 저장하는 작업이 필요한 모든 컨트롤러에서 일일이 구현하는것보다는 클래스를 새로생성하고 스프링빈으로 등록한후 구현해놓은다음 필요한 컨트롤러에서 주입받아 사용하는것이 더 깔끔하다)
createStoreFileName() : 서버 내부에서 관리하는 파일명은 유일한 이름을 생성하는 UUID 를 사용해서 충돌하지 않도록 한다.
extractExt() : 확장자를 별도로 추출해서 서버 내부에서 관리하는 파일명에도 붙여준다.
예를 들어서 고객이 a.png 라는 이름으로 업로드 하면 51041c62-86e4-4274-801d-614a7d994edb.png 와 같이 저장한다.
ItemForm (DTO)
@Data
public class ItemForm {
private Long ItemId;
private String itemName;
private MultipartFile attachFile;
private List<MultipartFile> imageFiles;
}
상품 저장용 폼이다.
List <MultipartFile> imageFiles : 이미지를 다중 업로드 하기 위해 MultipartFile 를 사용했다.
MultipartFile attachFile : 멀티파트는 @ModelAttribute 에서 사용할 수 있다.
ItemController
@Slf4j
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemRepository itemRepository;
private final FileStore fileStore;
@GetMapping("/items/new")
public String newItem(@ModelAttribute ItemForm form) {
return "item-form";
}
@PostMapping("/items/new")
public String saveItem(@ModelAttribute ItemForm form, RedirectAttributes redirectAttributes)
throws IOException {
UploadFile atachFile = fileStore.storeFile(form.getAttachFile()); // 하나 전달되는 일반파일 저장
List<UploadFile> storeImageFiles = fileStore.storeFiles(form.getImageFiles()); // 여러개 전달되는 이미지 파일들 저장
// Inline Variable (리팩토링) 단축키 Ctrl+Alt+N
//위의까지 진행하면 폴더에 파일 저장까지는 된다, 이제 데이터베이스 (Map)에 저장한다.
//데이터베이스에는 실제 파일들이 저장되는것이 아닌, (실제파일명,저장된파일명)에 대한 정보를 데이터베이스에 저장
// (이미지파일은 S3에 올리고 URL링크를 데이터베이스에 저장하는것처럼 데이터베이스에는 실제 파일자체를 저장하지않는다.)
// 경로또한 fullPath를 저장하진않고, 지정된 Path 그 이후의 경로만 저장한다고 한다.
Item item = new Item();
item.setItemName(form.getItemName());
item.setAttachFile(atachFile);
item.setImageFiles(storeImageFiles);
itemRepository.save(item);
redirectAttributes.addAttribute("itemId", item.getId());
return "redirect:/items/{itemId}";
}
}
지금은 파일을 업로드하는 기능까지만 존재한다.( Storage에 파일들을 저장하고, 데이터베이스에는 파일정보를 저장하는)
실제 파일들은 s3, NAS 등 storage에 저장하고, 저장한뒤 경로만 데이터베이스에 저장하는 식으로 한다고 한다.
경로도 전체경로보다는 상대경로로 저장한다고한다.
아이템 등록 뷰 템플릿
<form th:action method="post" enctype="multipart/form-data">
<ul>
<li>상품명 <input type="text" name="itemName"></li>
<li>첨부파일<input type="file" name="attachFile" ></li>
<li>이미지 파일들<input type="file" multiple="multiple" name="imageFiles" ></li>
</ul>
<input type="submit"/>
</form>
input 태그의 multiple이라는 속성을 이용하면 파일이 여러개 선택가능하다.
결과확인
업로드한 파일 보여주기
@GetMapping("/items/{id}")
public String items(@PathVariable Long id, Model model) {
Item item = itemRepository.findById(id);
model.addAttribute("item", item);
return "item-view";
}
아이템 조회 뷰 템플릿
<div class="container">
<div class="py-5 text-center">
<h2>상품 조회</h2>
</div>
상품명: <span th:text="${item.itemName}">상품명</span><br/>
첨부파일: <a th:if="${item.attachFile}" th:href="|/attach/${item.id}|" th:text="${item.getAttachFile().getUploadFileName()}" /><br/>
<img th:each="imageFile : ${item.imageFiles}" th:src="|/images/${imageFile.getStoreFileName()}|" width="300" height="300"/>
</div>
첨부파일이 있으면 해당 파일을 가져올 수 있는 링크를 남기고 업로드한 파일명을 보여준다.
이미지파일이 있으면 해당 이미지파일들의 경로를 이용해서 이미지를 보여준다. (th:each로 루프를 돌아서)
결과
이미지를 보면 다 엑박이 뜨고있다.
그 이유는 뷰 템플릿에서 이미지 경로를 (/images/저장된 파일이름)로 지정해서
request했는데 아무런 기능을 아직 안만들었기 때문.
경로 == URL
파일탐색기도 경로를 reqeust해서 디렉토리를 이동하는것이고
웹브라우저에서도 경로를 주면 해당 파일을 열거나 ( 정적 Html 열듯이)
웹브라우저에 디렉토리 경로를 주면 파일탐색기처럼 동작한다.
업로드한 파일 보여주기(2) - 이미지 보여주기
@ResponseBody
@GetMapping("/images/{filename}")
public Resource downloadImage(@PathVariable String filename)
throws MalformedURLException// Spring의 Resource를 선택해야한다.
{
// UrlResource의 파라미터인 path는 "file:C:/fileTest/2d878edf-458a-4238-8155-548bf3db840b.jpg" 이런식으로 구성된다.
// 경로를 가지고 UrlResource로 이미지 파일을 읽어서 @ResponseBody 로 이미지 바이너리를 반환한다.
return new UrlResource("file:" + fileStore.getFullPath(filename));
}
결과
파일 다운로드 하기
@GetMapping("/attach/{itemId}")
public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId)
throws MalformedURLException {//@ResponseBody를 안붙이고 ResponseEntity를 사용해도된다. ResponseEntity는 알아서 변환시켜주니까
Item item = itemRepository.findById(itemId);
String storeFileName = item.getAttachFile().getStoreFileName();
String uploadFileName = item.getAttachFile().getUploadFileName(); //다운로드를 받을때 실제 파일명이 나와야하므로
UrlResource resource = new UrlResource("file:" + fileStore.getFullPath(storeFileName));
log.info("uploadFileName = {} ", uploadFileName);
String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8); // 파일이름같은건 깨지기 쉬우므로 인코딩을해줘야한다.
String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\""; // \"는 쌍따음표를 문자로 사용하기 위함
//header 설정을 해주지않으면 파일의 내용이 보이게 된다. ( text파일이면 그안에 적힌내용, 이미지파일이면 바이너리코드가 보임)
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
}
@GetMapping("/attach/{itemId}") : 파일을 다운로드 할 때 실행한다.
예제를 더 단순화 할 수 있지만,
파일 다운로드 시 권한 체크같은 복잡한 상황까지 가정한다 생각하고 이미지 id 를 요청하도록 했다.
파일 다운로드시에는 고객이 업로드한 파일 이름으로 다운로드 하는게 좋다.
이때는 Content-Disposition 헤더에 attachment; filename="업로드 파일명" 값을 주면 된다.
(filename을 이용해서 다운로드되는 파일명을 지정해준다)
결과
저 링크를 누르면
http://localhost:8080/attach/1이 request되고
response의 Content-Disposition 헤더에는 아까 설정해둔 값들이 남아있다.
웹브라우저는 attachment를 보고 다운로드를 하게된다 ( 파일 경로는 UrlResource를 이용하여 찾아서 다운로드)
'인프런 > 스프링 MVC 2편' 카테고리의 다른 글
20) Formatter(포맷터) (0) | 2023.02.12 |
---|---|
19) 스프링 타입 컨버터 (0) | 2023.02.12 |
18) API 예외처리 (3) (0) | 2023.02.12 |
17) API 예외 처리(2) (0) | 2023.02.10 |
16) API 예외 처리 (0) | 2023.02.09 |
댓글