multipart-file upload 관련 HTTP 요청을 처리할 수 있는 서버 애플리케이션을 만들어볼 것이다.
이번 튜토리얼에서 활용
가장 큰 특징은 순수 HTML 구조를 유지하는 view template이라는 것이다.
이번 application은 MVC 구조를 따른다. 여기서 View 부분에 Thymeleaf를 활용하는 형태.
이번 application은 multi-part된 file을 HTTP를 통해 받은 다음에 이를 저장할 것이다.
Java의 경우 MVC의 Controller 부분 구현에 대해서 Servlet
이라는 class를 활용하며, 이 Servlet
은 Servlet Container
에 의해 관리가 된다.
Servlet
의 환경설정, Servlet
의 유효시간, Servlet
이 무슨 url에서 온 request를 처리할지 등을 다 설정하는 환경설정 관련 파일이 바로 web.xml
이다.
예전에는 Servlet
을 프로그래밍하는데... 먼저 해당 interface를 implement한 HTTPServlet
같은 class를 extend
해가지고 구현을 했다. 그리고 환경 설정 같은 경우 직접 web.xml
의 조작을 필요로 했다.
Spring의 등장 이후, web.xml의 구축은 굳이 우리가 할 필요 없고 수정사항만 application.properties
에 넣으면 되며, Servlet
interface의 구현 class들을 직접 활용하는 대신 @RESTController
, 혹은 그냥 @Controller
을 활용해가지고 프로그래밍 하는 것도 가능하게 되었다.
위처럼 Spring에서 만든 annotation인 @Controller
들의 경우에는 Spring에서 bean으로 취급을 한다. 하지만 이 @Controller
들이 활용하는, 즉 @Controller
아래에서 MVC controller의 controller역할을 하는 Servlet Container은 Spring에서 Bean으로 취급을 하지 않는다. 이 점 유의.
하지만 Spring에서 자동으로 servlet container을 auto-configure 및 실행하는 것은 맞으며, 앞에 말했듯 이 servlet container 관련 설정을 application.properties
에서 하는 것이 가능하다.
결론은, multi-part file upload를 위해 우리가 추가적으로 고려해야 할 환경 관련 설정은 하나도 없다. 그냥 dependency만 처음에 initializr로 잘 넣으면 된다. (web, thymeleaf)
굳이 multi-part upload에 대한 세부사항을 조절하고 싶은 경우, MultiPartConfigElement
class를 활용하면 된다. Spring에서 MultiPartConfigElement를 활용해 관련 환경 설정을 하는 방법 아니면 servlet의 multipart
운용을 어떻게 할지를 application.properties
에서 설정해도 되며 이 튜토리얼에서는 이 방식을 통해 최대 업로드 파일 크기 및 최대 요청 파일 크기를 제한해 뒀다.
listUploadedFiles
method 관련MVC의 Model에 해당하는 interface다.
MVC model을 알고 있다면, View가 여기서 data를 확인해가지고 user에게 어떻게 전시할지를 업데이트함을 알고 있을 것이다. 이 Model 역할을 하는 애들에 관한 interface가 Model
interface라고 생각하면 된다.
listUploadFiles
에서 해당 interface 부류의 한 객체에 특정 attribute를 추가하고 있는걸 볼 수 있다. 이는 특정 view가 참고할 data를 추가하는 것으로 볼 수 있다. 이 부분에 대해 더 자세히 알아보려면 몇가지를 더 설명해야 한다.
앞의 listUploadFiles
에서 사용하는 class다.
UriComponentsBuilder
을 만드는데 사용되는 class다. 여기서 UriComponentsBuilder
이란 UriComponents
를 만드는 builder이다.
data들을 특정 collection으로 aggregate할 때 쓰임.
여기서는 List
를 만드는데 사용.
listUploadedFiles
의 역할한 문장으로 코드가 이루어져있는데, storageService
의 loadAll
의 결과물에 대해서 FileUploadController
class에 있는 serveFile
이라는 method를 전부 적용해가지고 UriComponent
가 element로 있는 List
를 만든 다음에 Model
type인 model에 files
라는 attribute로 집어넣고 있다. 여기서 fromMethodName
의 세번째 parameter의 argument는 serveFile
에 전달할 parameter이며, loadAll()
의 결과물에 전처리를 해가지고 전달을 하고 있다.
참고로 storageService
의 type은 StorageService
로 이 예제에서 직접 정의한 interface인데, 그걸 잠깐 확인해보면 loadAll
이 Java에 기본으로 있는 class인 Path
에 대한 Stream
을 만든다는 것이다. 이 점과 loadAll
과 listUploadFiles
라는 메서드의 이름, 그리고 listUploadFiles
가 @GetMapping("/")
으로 되어 있는 것을 통해 유추할 수 있는게 뭐냐면 현재 storage에 전달되어 있는 파일들의 경로를 전부 추출해서 Stream
형태로 전달한다는 것이다.
그리고 이 Stream
에 mapping
을 적용해가지고 각 Path
에 대한 UriComponentBuilder
을 만들어 List
에 저장하고, 그것을 model
에다가 files
라는 attribute로 저장하는 것이다.
이후 uploadForm
이라는 String
을 반환하는 것을 볼 수 있다. 이건 사실 렌더링할 View template를 지칭하는 것이다. 해당 이름의 template은 프로젝트의 resources
디렉토리에 html 확장자로 저장되어 있는걸 볼 수 있으며 Thymeleaf를 활용하는 것도 확인 가능.
그 html 파일을 자세히 살펴보면 다음과 같은 영역이 있다.
<div>
<ul>
<li th:each="file : ${files}">
<a th:href="${file}" th:text="${file}" />
</li>
</ul>
</div>
이는 thymeleaf iterator을 활용하는 html 구문이다. files
라는 attribute가 iterate 대상이고, 하는것은 대충 업로드 된 파일들이 무엇인지를 전부 출력.
즉 listUploadedFiles
는 그냥 /
에 대해 요청이 왔을 때, 업로드 된 파일들이 무엇인지 그 경로를 전부 출력하는데 사용되는 method인 것이다.
@RequestBody
, @ResponseBody
는 JSON을 Java 객체로, Java 객체를 JSON으로 변환할 때 사용되는 annotation이다.
이 때 좀 구체적으로 보낼 response에 대해 설정을 하고 싶을 때 사용되는게 바로 ResponseEntity
class이다. Header, Body에 status code까지 추가가 가능하다.
예시의 경우, 특정 resource에 대해 요청이 왔을 때 이 파일이 존재하면 그 파일을 전달하고, 없으면 오류를 반환하는 method인 serveFile
에서 활용되었다.
URL의 path부분에 있는 경로를 변수로 사용하겠다는 annotation
구체적으로 path의 어느 부분을 사용할지는 Mapping
부분의 {}
로 둘러싸인 이름 부분을 통해 정한다. 즉 코드의 경우 /files
다음의 부분을 변수로 사용하겠다는 것을 뜻함.
serveFile
method에서 사용중.
Spring에서 제공하는 Interface로, low-level data를 나타내는 추상화된 녀석이다.
Java에도 비슷한 역할을 하는 URL
이라는 class가 있지만 별로여가지고, 일반적인 low-level-resource를 접근하는데 필요한 기능들 및 존재 여부에 대한 기능들을 implement하는 틀을 만든게 바로 이 interface이다.
여기서는 그냥 파일을 Resource
로 취급 하고 있다.
서버에서 multipart-file upload를 지원해야 하다보니 관련 요청을 받을텐데, 그럴려면 multipart-file을 나타내는 class가 필요할거고 그게 바로
말 그대로, multi-part file을 나타내는 class다.
임시로 디스크나 메모리에 저장되어 있는데, 이를 구체적으로 어디에 넣을지 추가 조작을 우리가 해야 한다.
interface로, Model
interface를 확장한 interface이다.
관련 유용한 글 1(PRG pattern). 해당 interface가 Post/Redirect/Get(PRG) pattern이랑 관련되어 있는 interface라 이에 대해 잘 모르면 좌측 글 참고바람.
관련 유용한 글 2. RedirectAttribute
가 PRG pattern에서 어떻게 사용되는지에 대한 글이다.
위 두 글을 읽으면 이 class에 대해서 충분히 알 수 있고, 또 handleFileUpload
가 무슨 일을 하는지도 알 수 있다. 간단히 말하자면 storageService
의 store
을 활용해 file의 저장을 한 다음, redirectAttribute
를 통해 redirection 후에 있을 GET parameter을 미리 저장하고 "redirect:/"
을 retur해 해당 위치에 대한 GET 요청을 하도록 유도해 PRG pattern까지 구현.
Controller
에서 발생한 exception을 처리하는 method임을 지정하는 annotation이다.
여기서는 자체 정의한 exception인 StorageFileNotFoundException
을 처리하는 method를 지정하기 위해 사용했다. 참고로 모든 StorageException
을 처리하는 것은 아니라는 점 유의. 파일 저장을 관리하는 storageService
측에서 어떤 파일을 찾는데 실패했을 때 나오는 exception만 처리 하며 이 경우 파일을 못찾았다는 response를 보낸다.
StorageService
라는 자체 class는 @Service
annotation을 달고 있다.
@Component
를 포함하고 있는 annotation이다. 즉 bean으로 취급된다.
그럼 구체적으로 무슨 역할인가? 일단 이론적으론 DDD 패턴에서 business model을 담당하는 class임을 의미한다. Spring의 경우 service layer에 속할 bean이라는 것을 표기할 때 이 annotation을 사용한다.
@Component
대비 특별한 기능은 없다. 보통 business logic에 속하는 녀석이다 + @Transaction
annotation을 활용해 Transaction management에 용이하다를 표기하는 정도?
StorageProperties
라는 class가 @ConfigurationProperties
가 붙어있고, main application에 @EnableConfigurationProperties
가 붙어있는데, 전자는 configuration bean들을 enable하는데 사용되며, 후자는 해당 class의 configuration을 application.properties
등을 통해 설정하는 것이 가능하다는 것을 의미한다.
이 때 application.properties
에서 사용해야 하는 이름이 @ConfigurationProperties
에 주어지는데 여기서는 그게 storage
다. 이를 활용해 StorageProperties
의 location
값을 바꾸는 것이 가능하다.
뭔가 많이 복잡해보이지만, 구현이 그런 것이고 결국 크게 보면 multipart-file에 대한 upload 지원 및 현재 업로드 된 파일들이 무엇인지를 웹페이지에 전시하는것 말고 딱히 하는 것은 없다.
이 때 server이 운용되는 시스템의 filesystem을 직접적으로 활용하고 있는데, 실제로는 이러는 것이 좋지 않고 관계형 DB, 혹은 NoSQL DB, 혹은 그냥 임시 저장소에다가 저장을 해두는 것이 좋다.
https://github.com/baekrang256/Backend-Practice/tree/main/SpringTutorial/uploadingfiles