Spring/Spring MVC

파일 업로드

민철킹 2021. 8. 12. 18:41

일반적으로 HTML Form을 통해 파일 업로드 기능을 구현한다.

 

먼저 HTML Form 전송 방식 두 가지를 알아보자.

- application/x-www-form-urlencoded

- multipart/form-data

 

application/x-www-form-urlencoded

이 방식은 HTML Form을 서버로 전송하는 가장 기본적인 방법이다.

별도의 "enctype"옵션이 없으면 웹 브라우저는 요청 HTTP 헤더에 아래와 같이 추가한다.

Content-Type : application/x-www-form-urlencoded

Form에 입력한 데이터를 HTTP Body에 문자로 &로 구분하여 전송하게된다.

 

파일 업로드를 위해서는 파일은 문자가 아니라 바이너리 데이터를 전송해야하는데 문자를 전송하는 이 방식은 파일을 전송하기 어렵다. 또한 Form을 전송할 때 파일만 전송하는 것이 아니라는 점때문에 문제가 있다.즉, 문자와 바이너리 데이터를 함께 전송해야한다.

 

이를 위해서 HTTP는 "multipart/form-data"라는 전송 방식을 제공한다.

 

multipart/form-data

Form 태그에 별도의 enctype="multipart/form-data"를 지정해야한다.

 

이 방식을 사용하면 다른 종류의 여러 파일과 Form내용을 함께 전송할 수 있다.

 

각각의 항목이 구분되어 있고 부가 정보가 있는 "Content-Disposition"이라는 항목별 헤더가 추가되었다.

Form의 일반 데이터는 각 항목별로 문자로 전송되고, 파일은 파일 이름과 Content-Type이 추가되고 바이너리 데이터가 전송된다.(Content-Type은 브라우저가 자동으로 생성해줌)각각의 Part로 나누어져 전송되는 구조이다.

 


Servlet과 파일 업로드

 

HTML Form으로 상품명(text), 파일(file)이 전송하여 컨트롤러에서 받고 로그로 출력해보자.

upload-form.html

Servlet을 사용할 것이기 때문에 HttpServletRequest를 파라미터로 받는다.

HttpServletRequest에는 getParts라는 메서드가 존재하는데 이것이 바로 각각의 항목으로 구분된 Form-data이다.

각각의 항목이 Part이고 이를 담은 컬렉션이 Parts!!

또한 HTTP 메세지를 서버에서 로그로 확인하기 위해 application.properties에 다음과 같이 입력한다.

logging.level.org.apache.coyote.http11=debug

 

그 후, HTML Form에 "Spring"이라는 텍스트와 png파일을 전송하였다.서버에 남은 로그를 살펴보자.

먼저 HTTP HeaderContent-Type에는 multipart/form-data, boundary에는 랜덤값이 들어가있는 것을 확인할 수 있따.

동일한 boundary값으로 각각의 항목으로 구분되어 넘어온 것을 확인할 수 있다.

이것은 HTTP Message이고 직접 서버에 남긴 로그를 보면

request가 일반적으로 사용하는 HttpServletRequest가 아닌 StandardMultipartHttpServletRequest이다.

또한, parts를 보면 컬렉션안에 두개의 Part가 있는데 이것이 바로 HTML Form에서 넘긴 두개의 항목(텍스트, 파일)이다.

parts안에 담긴 것을 개발자가 꺼내어 DB에 저장하거나 사용할 수 있다.

 

참고로 멀티파트를 사용할 때 옵션으로 업로드 사이즈를 제한할 수 있다.
용량이 큰 파일이 업로드되지 못하도록 제한할 때 유용하다. 사이즈를 넘으면 SizeLimitExceededException이 발생한다. 
application.properties에서 설정할 수 있는데 아래와 같다.

spring.servlet.multipart.max-file-size는 파일 하나의 최대 크기를 제한한다.(기본 1MB)
spring.servlet.multipart.max-request-size는 멀티파트 요청 하나에 있는 여러 파일을 합을 제한할 수 있다.(기본 10MB)

이 옵션은 true가 기본인데 false로 변경하게되면 RequestStandardMultipartHttpServletRequest가 아닌 일반적으로 사용하는 RequestFacade(톰캣의 경우)로 사용된다. 따라서, multipart와 관련된 처리를 아예 막을 수 있다.
텍스트와 파일을 넘기더라도 위에서와 같이 getParameter, getParts를 사용해 값을 가져올 수가 없게된다.
즉, 서블릿 컨테이너는 multipart와 관련된 처리를 아예하지 않게 된다.

해당 옵션이 true일 때는 Spring의 DispatcherServlet에서 MultipartResolver를 실행하여 일반적인 HttpServletRequest를 MultipartHttpServletRequest로 변환해서 반환한다.

Spring의 기본 MultipartResolver는 MultipartHttpServletRequest를 구현한 StandardMultipartHttpServletRequest를 반환한다.

결론적으로 이 옵션을 true로 하면 컨트롤러에서 StandardMultipartHttpServletRequest를 파라미터로 주입받을 수 있어 멀티파트와 관련된 여러 처리를 지원받을 수 있다!!!

 

클라이언트로부터 파일을 받아 실제 서버(내 컴퓨터)에 업로드해보자. 보통은 AWS S3를 많이 사용함.

 

파일이 업로드될 폴더 생성
application.properties

application.properties에 파일이 저장될 경로를 설정해둔다.

==> 이렇게하니 스프링 부트 내부에서 사용하는 톰캣 경로와 같이 합쳐져버려 오류가 발생하여 절대 경로로 설정하였다.

변경

저장해둔 값은 Spring이 제공하는 @Value 애노테이션을 사용해 컨트롤러로 값을 가져와 사용하면된다.

 

먼저 Controller에서 Part의 정보를 로그로 찍어보자.

메서드 명들이 이미 직관적이기 때문에 어떤 기능을 하는지는 추측가능할 것이다.

각 Part의 Header 정보들을 로그로 찍는 내부 반복문과 전송된 파일의 이름을 가져오는 getSubmittedFileName(),

Part의 size를 알려주는 getSize(), 데이터를 읽기위한 getInputStream()이 있다. Stream이기 때문에 Spring이 제공하는 StreamUtils를 통해 String으로 변환한 후에 로그로 찍어주었다.

 

서버를 실행시켜 실제 로그들을 살펴보도록 하겠다.먼저 일반 text이다.

다음은 파일이다.

Stream -> String 변환

바이너리 데이터를 문자열로 변환하여 위와 같이 알수없는(?) 깨진 문자들이 출력되었다.

 

파일 저장

파일 저장은 위와 같이 제출된 파일이 있는지를 확인한 후 위에서 가져온 폴더명과 제출된 fileName을 합쳐 경로를 만들고

Part가 제공하는 write메서드로 해당 경로에 파일을 저장하도록 하였다.

파일 업로드 완료

파일 전송 후 해당 폴더로 들어가보면 파일이 저장되어있는 것을 확인할 수 있다. 사진 뿐 아니라 텍스트 파일도 정상적으로 전송되어 저장되는 것을 확인할 수 있었다.

 

서블릿이 제공하는 Part는 편하지만 HttpServletRequest를 사용해야하고 추가로 파일 부분만 구분하려면 여러 줄의 코드를 넣어야한다.

 

 

Spring은 이러한 부분을 편리하게 제공하고있다.

Spring과 파일 업로드

Spring을 사용하게되면 @RequestParam을 통해 MultipartFile을 바로 위와 같이 받을 수 있다.

또한 많은 편의 메서드가 제공된다.

transferTo를 통해 해당 파일을 지정된 경로에 저장하였다.

 

실제 실행시켜 확인해보자.

이번에는 귀여운 Gradle의 코끼리 로고를 저장하였음.

Servlet을 사용했을 때보다 훨씬 더 코드도 간결하고 편리하게 파일 업로드 기능을 사용할 수 있게되었다.

 

@ModelAttribute에서도 MultipartFile을 동일하게 사용할 수 있다!!

 


업로드 기능을 활용한 예제를 만들어보자.

 

상품이름, 첨부파일 하나, 이미지 파일 여러개를 업로드하고 첨부파일은 다운로드가 가능하게, 이미지는 웹 브라우저에서 확인할 수 있는 서비스를 만들어보겠다.

 

도메인

Item
UploadFile

하나의 첨부파일과 여러장의 이미지 파일이 업로드 될 것이기 때문에 위와 같이 Item을 만들었다.

또한 클라이언트에서 동일한 이름의 파일이 들어올 수 있기 때문에 덮어씌워지는 것을 방지하기위해 storeFileName이라는 필드를 만들고 이 필드에 UUID와 같은 식별 값을 넣어 파일명이 같더라도 구분할 수 있도록 하겠다.

- uploadFileName : 클라이언트가 업로드한 파일명

- storeFileName : 서버 내부에서 관리하는 파일명

 

Repository

 

간단히 HashMap에 Item을 저장하고 id를 통해 조회하는 Repository를 만들었다.

 

다음으로 멀티파트 파일을 서버에 저장하는 역할의 클래스를 만든다.

extractExt 메서드는 originalFilename에서 확장자를 추출한다.

createStoreFileName 메서드는 UUID를 생성하고 확장자와 결합해 UUID + 확장자로 서버 내부에서 관리할 파일명을 만들어준다.

 

추가로 여러장의 이미지 파일또한 업로드될 것이기 때문에 위에서만든 storeFile를 사용하는 메서드를 만들었다.

단순히 List를 돌면서 해당 MultipartFile이 비어있지 않으면 uploadFile로 만들고 List에 저장해준다.

 

폼 객체

데이터를 주고 받을 Form객체를 하나 생성하였다.

 

컨트롤러

@ModelAttribute를 사용해 빈 Form객체를 View로 넘겨준다.

item-form.html

input 태그의 multiple 옵션을 위와같이 설정하면 한번에 여러개의 파일을 선택할 수 있게된다.

이제는 Post방식으로 넘어오는 Form데이터를 받고 저장하는 컨트롤러를 만들겠다.

@ModelAttribute를 통해 Form객체에 담긴 데이터를 꺼내어 저장한다.

 

웹 브라우저에서 저장된 이미지파일들을 볼 수 있도록 하기 위해 위와 같이 itemId를 담아서 redirect함

실제 실행시켜보면 지정된 폴더에 UUID+확장자로 파일이 저장되어있는 것을 확인할 수 있다.

 

redirect되어 웹 브라우저에 이미지 파일을 띄우기 위한 컨트롤러이다.

item-view.html

위와 같이 파일들을 선택하고 제출버튼을 누르면?

UUID+확장자로 폴더에 저장이 되었고

다운로드 받을 수 있는 링크와 이미지 파일들이 웹 브라우저에 출력된 것을 확인할 수 있다.

다운로드 링크를 클릭하면 /attach/{id}로 이동하는데 이를 위한 별도의 컨트롤러를 따로 만들어야한다.

또한, 현재 image가 404로 정상출력이 되지 않고 있는데 이또한 마찬가지로 지정된 경로에서 이미지 파일을 가져와야하는데 이를 위한 컨트롤러도 만들어주어야한다.

 

먼저, 이미지 출력을 위한 컨트롤러부터 만들어보겠다.

여기에는 다양한 방식이 존재하므로 추후에 다른 방식도 찾아보자.

넘어온 파일명을 통해 fullPath를 만들고 UrlResourse가 그 경로에 실제 접근하여 파일을 가지고온다.

url에 "file:"가 붙으면 실제 내부 파일에 접근한다.

업로드를 하면 실제 해당 폴더에 아래와 같이 파일들이 저장된다.

랜더링 시점에 UrlResource를 통해 이미지를 가져오고 출력하게 되는 것이다.

이는 보안에 취약하기 때문에 보안을 위해서는 여러가지 체크로직을 넣는 것이 좋다.

 

 

첨부파일을 다운로드하는 컨트롤러를 만들어보자.

header를 설정하지 않으면 다운로드가 아닌 단순히 해당 파일을 열어서 보여주기만한다.

contentDisposition을 설정하는 것은 규약이므로 다운로드를 위해선 꼭 위와 같이 header를 설정해줘야한다.

contentDispositionattachmentfilename을 함께 주면 Body에 오는 값을 다운로드 받으라는 의미이다.

다운로드 가능!

이제 첨부파일을 다운로드 받을 수 있게 되었다.

추가로 텍스트 파일을 다운로드 할 때는 글자가 깨지는 것을 방지하기위해 Spring이 제공하는 UriUtils의 encode메서드를 활용하면 편리하게 인코딩이 가능하다.

인코딩

 

반응형

'Spring > Spring MVC' 카테고리의 다른 글

Spring Type Converter  (0) 2021.08.03
API 예외 처리  (0) 2021.07.28
예외 처리, 오류 페이지  (0) 2021.07.24
필터, 인터셉터  (0) 2021.07.21
쿠키, 세션  (0) 2021.07.17