Spring ํ์ผ ์ ๋ก๋, ๋ค์ด๋ก๋
by JiwonDev์ฐธ๊ณ ๋ก ๋ฐฑ์๋์์ ํ์ผ์ ์ ๋ก๋/๋ค์ด๋ก๋ ํ ๋,
๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ ํ์ผ ๊ฒฝ๋ก (๋ณดํต ๊ธฐ๋ณธ root๋ฅผ ์ ์ธํ ์์ธ ๊ฒฝ๋ก)๋ฅผ ์ ์ฅํด๋๊ณ
ํ์ผ์ ๋ค๋ฅธ ๊ฒฝ๋ก์ ๋ฐ๋ก ์ ์ฅํ๊ฑฐ๋, ๋ณ๋์ ์ปจํ ์ธ ์ฉ ์๋ฒ์ ๊ด๋ฆฌํ๋ค.
๊ธ ๋ง์ง๋ง์ ์ค์ ์ฝ๋๋ ์ด๋ป๊ฒ ๋ง๋ค ์ ์๋์ง ์ค๋ช ํ๋๋ก ํ๊ฒ ๋ค.
# HTTP์์๋ ์ด๋ป๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๋๊ฐ
HTTP์์ ํ ์คํธ ๋ฐ์ดํฐ๋ฅผ Body์ ๋ฃ์ด ์ ์กํ ๋ ์ฌ์ฉํ๋ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ๋ฐฉ๋ฒ์ POST - Form ๋ฐฉ์์ด๋ค.
ํผ์ ์ ์กํ ๋ HTTP ํค๋์ content-type = application/x-www-form-urlencoded๋ก ์ง์ ํ๋ฉด ์๋์ ๊ฐ์ด ๋์ํ๋ค.
โก ์ด ๋ฐฉ์์ ๋ชจ๋ ๋ฉ์์ง๋ฅผ String์ผ๋ก ์ฒ๋ฆฌํด์ ์ ์กํ๋ค.
@ HTTP์์ ์ฒ๋ฆฌํด์ค๋ค๋๊ฒ ๋ฌด์จ ์๋ฏธ์ฃ ?
์น ๋ธ๋ผ์ฐ์ ๋ HTTP ํ์ค ์คํ(์ธํฐํ์ด์ค)๋ฅผ ๊ตฌํํ ํ๋ก๊ทธ๋จ์ด๋ค. ์ฆ HTTP์ ์ ํ์๋ ๊ธฐ๋ฅ์ ์น ๋ธ๋ผ์ฐ์ ๊ฐ ๊ตฌํํ๊ณ ์ฐ๋ฆฌ๊ฐ ์ฌ์ฉํ ์ ์๊ฒ HTTP ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํด์ค๋ค.
์์ฆ์ ์น ๋ธ๋ผ์ฐ์ ๋ง๊ณ ๋ ๋ค์ํ ๊ณณ์์ HTTP๋ฅผ ์ด์ฉํด ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๋๋ค. ์ด๋ ํด๋ผ์ด์ธํธ ,์๋ฒ๊ฐ HTTP ์คํ์ ๊ตฌํํ๊ณ HTTP ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ๊ธฐ๋ฅ์ ์ ๊ณตํด์ฃผ๊ธฐ ๋๋ฌธ์ด๋ค.
# HTTP์ Content-type ํค๋
HTTP์์ ์ฌ์ฉํ๋ Content-type ํค๋๋ [HTTP Message Body]์ ๋ค์ด๊ฐ๋ ๋ฐ์ดํฐ ํ์ ์ ๋ช ์ํ๋ค.
Content-type์๋ MIME(Multipurpose Internet Mail Extensions) ์ ๋ช ์๋ ํ์ ์ค ํ๋๋ฅผ ์ฌ์ฉํ์ฌ ๋ฉ์์ง๋ฅผ ์ธ์ฝ๋ฉํ๋ค. ๊ทธ ์ค ๋ฐ์ดํฐ๋ฅผ ์ ์กํ ๋ ์ฌ์ฉํ๋ ๋ํ์ ์ธ ํ์ ์ด multipart(์ฌ๋ฌํ์ผ๊ณผ ํผ์ ํจ๊ป ์ ์กํ๋ ๋ฐฉ์) ์ด๋ค.
- multipart/mixed : ์ด๋ค ๋ฐ์ดํฐ๊ฐ ์ฒจ๋ถ๋ ํ ์คํธ ๋ฉ์์ง
- multipart/alternative : ๋์ฒดํ ์ ์๋ ๋ฐ์ดํฐ ํ์ ์ ํจ๊ป ๋ณด๋ธ ๋ฉ์์ง (text + HTML ๋ฑ)
- multipart/form-data : ํน์ํ ๋ฌธ๋ฒ์ผ๋ก ์ฌ๋ฌ ๋ฐ์ดํฐ๋ฅผ ํจ๊ป ์ ์กํ๋ค.
@ multipart/form-data
๋ณดํต ๋ฐ์ดํฐ๋ฅผ ์ ์กํ ๋์๋ content-type = multipart/form-data ๋ฅผ ๋ง์ด ์ฌ์ฉํ๋ค.
์ด๋ฅผ ์ด์ฉํ๋ฉด ์๋ก ๋ค๋ฅธ ํ์์ ๋ฐ์ดํฐ (์ด๋ฏธ์ง, ์์, HTML Form)์ ํ๋ฒ์ ๋ฌถ์ด์ ์ ์กํ ์ ์๋ค.
โก ์ฐธ๊ณ ๋ก ์ด ๋ฐฉ์์ ์ฌ์ฉํ๋ ค๋ฉด enctype = "multipart/form-data"๋ ๊ฐ์ด ์ง์ ํด์ค์ผ ํ๋ค.
multipart/form-data๋ ๊ตฌ๋ถ์(boundary)๋ฅผ ์ด์ฉํด์ ์ฌ๋ฌ๊ฐ์ง ํ์ ์ ๋ฐ์ดํฐ๋ฅผ ํํธ๋ณ๋ก ๊ตฌ๋ถํ๋ค.
์ค์ ์์ฑ๋ HTTP Reqeust, Response ๋ฉ์์ง
ํด๋ผ์ด์ธํธ์์ test.txt, number.txt ํ์ผ์ ์ ์กํ๋ ๊ฒฝ์ฐ
Content-Type: multipart/form-data; boundary=Uee--r1_eDOWu7FpA0LJdLwCMLJQapQGu
## HTTP Request Data
POST /file/upload HTTP/1.1[\r][\n]
Content-Length: 344[\r][\n]
Content-Type: multipart/form-data; boundary=Uee--r1_eDOWu7FpA0LJdLwCMLJQapQGu[\r][\n]
Host: localhost:8080[\r][\n]
Connection: Keep-Alive[\r][\n]
User-Agent: Apache-HttpClient/4.3.4 (java 1.5)[\r][\n]
Accept-Encoding: gzip,deflate[\r][\n]
[\r][\n]
--Uee--r1_eDOWu7FpA0LJdLwCMLJQapQGu[\r][\n]
Content-Disposition: form-data; name=files; filename=test.txt[\r][\n]
Content-Type: application/octet-stream[\r][\n]
[\r][\n]
aaaa๋ฐ์ดํฐ๋ฐ์ดํฐ๋ฐ์ดํฐ( ํ์ผ๋ช
์ test.txt์ด์์. )์ด๊ฑด ๋ฐ์ดํฐ
[\r][\n]
--Uee--r1_eDOWu7FpA0LJdLwCMLJQapQGu[\r][\n]
Content-Disposition: form-data; name=files; filename=number.txt[\r][\n]
Content-Type: application/octet-stream[\r][\n]
[\r][\n]
111123154251252 (ํ์ผ๋ช
์ number.txt์ด์์) 12412904
[\r][\n]
--Uee--r1_eDOWu7FpA0LJdLwCMLJQapQGu--[\r][\n]
์๋ฒ์์ HTTP 200์๋ต์ด ์จ ๊ฒฝ์ฐ
## HTTP Response Data
HTTPHTTP/1.1 200 OK[\r][\n]
Server: Apache-Coyote/1.1[\r][\n]
Accept-Charset: big5, big5-hkscs, euc-jp, euc-kr...[\r][\n]
Content-Type: text/html;charset=UTF-8[\r][\n]
Content-Length: 7[\r][\n]
Date: Mon, 30 Jun 2014 01:28:19 GMT[\r][\n]
[\r][\n]
SUCCESS
# ์๋ธ๋ฆฟ์์์ ํ์ผ์ ๋ก๋
๐งจ ์ด ๋ด์ฉ์ ์ค์ ๋ก ์ฌ์ฉํ์ง๋ ์๋ ๊ธฐ๋ฅ์ด๋ค. ๋งํธํ๊ฒ ์ฝ์ด๋ณด์
๊ฐ๋จํ๋ค. HttpServletRequest์ ์๋ getParts()๋ฅผ ์ด์ฉํ๋ฉด ๋๋ค. ์ด๋ ์๋ธ๋ฆฟ ์คํ 3.0๋ถํฐ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ด๋ค.
@PostMapping("/upload")
public String saveFileV1(HttpServletRequest request) throws
ServletException, IOException {
// request.getParts() ๋ฅผ ์ด์ฉํ๋ฉด, multipart/form-data ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ์ ์๋ค.
// ์ด๋ Part ๊ฐ์ฒด๋ก ๋๋์ด์ ธ์ ์ ์ก๋๋ค.
Collection<Part> parts = request.getParts(); // import javax.servlet.http.Part;
return "upload-form";
}
์๋ธ๋ฆฟ์์๋ HttpRequest ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ฉด, ์ด๋ฅผ ์์ํ RequestFacade๋ฅผ ํตํด ๋ฉํฐํํธ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํด์ค๋ค.
ํ์ง๋ง ์คํ๋ง์์๋ ์ด๋ฅผ ์ฌ์ฉํ์ง ์๋๋ค. ํด๋น ๊ธฐ๋ฅ์ Off ์์ผ๋๊ณ , ๋ฐ๋ก ์คํ๋ง ์ ์ฉ ๊ฐ์ฒด๋ก ์ถ์ํ ์์ผ๋์๋ค.
๋ง์ฝ ์๋ธ๋ฆฟ ์ปจํ
์ด๋์์ ์ ๊ณตํ๋ ๋ฉํฐํํธ ์ค์ ์ ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด, ์๋์ ๊ฐ์ด ๋ณ๊ฒฝํ ์ ์๋ค.
๋ฌผ๋ก spring.multipart.enabled = false๋ก ํด๋ฒ๋ฆฌ๋ฉด, ์คํ๋ง Controller์์ ์ฌ์ฉํ๊ธฐ ๋ฒ๊ฑฐ๋ก์์ง๋ค.
spring.servlet.multipart.max-file-size= 1MB // ๊ธฐ๋ณธ๊ฐ, ํ์ผ ํ๋์ max size
spring.servlet.multipart.max-request-size= 10MB // ๊ธฐ๋ณธ๊ฐ, ํ ์์ฒญ์ ์ ์ฒดํ์ผ max size
/*
multipart๋ ์ผ๋ฐ์ ์ธ ์์ฒญ๋ณด๋ค ์ฒ๋ฆฌ๊ณผ์ ์ด ๋งค์ฐ ๋ณต์กํ๋ค.
๊ทธ๋์ ์๋ธ๋ฆฟ ์ปจํ
์ด๋์์ ๋ฉํฐํํธ ์ฒ๋ฆฌ๋ฅผ Off ํด๋๊ณ , ์คํ๋ง์์ ๊ด๋ จ๋ ์ฒ๋ฆฌ๋ฅผ ๋ฐ๋ก ํ๋ค.
๋ง์ฝ ์๋ธ๋ฆฟ์์ ์ฒ๋ฆฌํ๋๋ก ๋ง๋ค๊ณ ์ถ๋ค๋ฉด multipart.enabled= false๋ก ์ค์ ํด์ฃผ๋ฉด ๋๋ค.
*/
spring.servlet.multipart.enabled= true // ๊ธฐ๋ณธ๊ฐ
์ฐธ๊ณ ๋ก ์คํ๋ง์ ๋์คํจ์ณ ์๋ธ๋ฆฟ์์ MultipartResolver๋ฅผ ํตํด ์ ์ ํ HttpServletRequest ๊ตฌํ์ฒด๋ก ๋ณ๊ฒฝํด์ ์ฌ์ฉํ๋ค.
๊ธฐ๋ณธ ๊ตฌํ์ฒด๋ StandardMultipartHttpServeltRequest์ด๋ฉฐ ์ด๋ฅผ ํตํด ๋ฉํฐํํธ ๊ฐ์ ์ฒ๋ฆฌํด์ค๋ค.
๋ฌผ๋ก ์ฌ์ฉํ๊ธฐ ํธํ๊ฒ ์ถ์ํํ๊ฒ์ผ ๋ฟ, ๊ฒฐ๊ตญ ์ต์ข ์ ์ผ๋ก๋ ์๋ธ๋ฆฟ์ผ๋ก ํ์ผ์ ์ ์ก๋ฐ๋๊ฑด ๋์ผํ๋ค.
์ด ๊ณผ์ ์ DispatcherServlet.doDispatcher() ์ ๊ตฌํ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ฝ๊ฒ ์ดํดํ ์ ์๋ค.
์๋ธ๋ฆฟ ์ปจํ ์ด๋(ํฐ์บฃ)์์ ๋ฐ์ HttpRequest ๊ฐ์ฒด๋ฅผ ์๋์ ๊ฐ์ด ์ค์ ํด์ค๋ค.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// Multipart ํํ์ ์์ฒญ์ด๋ผ๋ฉด HttpRequest ๊ตฌํ์ฒด๋ฅผ ๋ฐ๊ฟ์ค๋ค.
processedRequest = checkMultipart(request); // ํด๋น ๋ฉ์๋ ๋ด๋ถ์์ multipartResolver ์ฌ์ฉ
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
... ์ดํ ์ฝ๋ ์๋ต ...
๐ ํ์ผ ์ ๋ก๋ ํด๋ณด๊ธฐ
1. multipart ํ์์ผ๋ก POST ์์ฒญ์ ํ ์ ์๋ ํ์ด์ง๋ฅผ ๋ง๋ ๋ค.
2. ์๋ฒ์ ํ์ผ์ด ์ ๋ก๋๋ ๊ฒฝ๋ก๋ฅผ ์ง์ ํ๋ค.
# application.properties
file.dir = /Users/jiwon/my/files # ์ํ๋ ๊ฒฝ๋ก๋ฅผ ์ค์ ํ๋ค.
@Controller
@RequestMapping("/servlet/v2")
public class ServletUploadControllerV2 {
@Value("${file.dir}")
private String fileDir; // ๋ฌผ๋ก ๊ทธ๋ฅ String ์ผ๋ก ๊ฒฝ๋ก๋ฅผ ๋ฐ์๋ฃ๋ ๋ฐฉ๋ฒ๋ ์๋ค.
}
3. HttpServletRequest.getParts() ๋ฅผ ํตํด Collection<Part>๊ฐ์ฒด๋ฅผ ํตํด ๋ฉํฐํํธ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋๋ค.
@GetMapping("/upload") // ํ์ผ ์
๋ก๋ ํ์ด์ง ์ ๊ณต
public String getUploadForm() {
return "upload-form";
}
@PostMapping("/upload")
public String uploadFile(HttpServletRequest request) throws
ServletException, IOException {
String itemName = request.getParameter("itemName");
Collection<Part> parts = request.getParts();
for (Part part : parts) {
// ๊ฐ Part ์ ์ด๋ฆ
log.info("name={}", part.getName());
// ๊ฐ Part ์ ํค๋๊ฐ
Collection<String> headerNames = part.getHeaderNames();
for (String headerName : headerNames) {
log.info("header {}: {}", headerName, part.getHeader(headerName));
}
// ๊ฐ Part ์ ํธ์ ๋ฉ์๋ (size, FileName ๋ฑ)
//content-disposition; filename
log.info("submittedFileName={}", part.getSubmittedFileName());
log.info("size={}", part.getSize()); //part body size
// ๊ฐ Part ์ ๋ฐ์ดํฐ ๋ฐ๋
InputStream inputStream = part.getInputStream();
String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("body={}", body);
// Part ์์ ๊บผ๋ธ ๋ฐ์ดํฐ๋ฅผ ๋ด๊ฐ ์ํ๋ ํ์ผ๋ก ์ ์ฅํ๊ธฐ
if (StringUtils.hasText(part.getSubmittedFileName())) {
String fullPath = fileDir + part.getSubmittedFileName();
log.info("ํ์ผ ์ ์ฅ fullPath={}", fullPath);
part.write(fullPath);
}
}
return "upload-form"; // ํ์ผ ์
๋ก๋ ์๋ฃํ, ๋ค์ GET ํ์ด์ง๋ก
}
์ง์ ํด๋ณด๋ฉด ์๊ฒ ์ง๋ง, ์ด๋ ๊ฒ ํ๋ํ๋ ์ฒ๋ฆฌํ๋๊ฑด ๋งค์ฐ ๋ฒ๊ฑฐ๋กญ๋ค.
๐ ์คํ๋ง์์ ํ์ผ์ฒ๋ฆฌ, MultipartFile ์ธํฐํ์ด์ค
์คํ๋ง์์๋ MultipartFile ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ๋ฉด ํ์ผ์ฒ๋ฆฌ๋ฅผ ๋งค์ฐ ์ฝ๊ฒ ํ ์ ์๋ค.
๋ฑํ ์ค๋ช ์ด ํ์์๋ค. @RequestParam ์ผ๋ก HTML Form ์ด๋ฆ์ ๋ฐ๊ฑฐ๋, DTO์ ๋ด์ @ModelAttribute๋ฅผ ์จ๋ ๋๋ค.
@PostMapping("/upload")
public String uploadFile(@RequestParam MultipartFile file) throws IOException {
// ์
๋ก๋ํ๋ HTML Form ์ด๋ฆ(file)์ ๋ง์ถฐ์ ์ฌ์ฉํ๋ฉด ๋๋ค. ์ด๋ฆ์ ๋ชจ๋ฅธ๋ค๋ฉด @ModelAttribute ๋ก ๋ฐ์ ์๋ ์๋ค.
log.info("multipartFile={}", file);
if (!file.isEmpty()) {
String fullPath = fileDir + file.getOriginalFilename();
log.info("ํ์ผ ์ ์ฅ fullPath={}", fullPath);
file.transferTo(new File(fullPath)); // java.io.File ๋ก ๋ณํ
}
return "upload-form";
}
์ฐธ๊ณ ๋ก Java.io.File ๊ฐ์ฒด๋ ์๋์ ๊ฐ์ด ์ฌ์ฉํ ์ ์๋ค.
๐ ๋ณต์กํ ํ์ผ์ ๋ก๋ ์์
1. ๋๋ฉ์ธ ๊ตฌํ
์๋ฅผ ๋ค์ด, ์ผํ๋ชฐ ํ๋งค์ ํ์ด์ง๊ฐ ์๊ณ
[์ํ ์ด๋ฆ] [์ฒจ๋ถํ์ผ 1๊ฐ] [์ด๋ฏธ์ง ํ์ผ ์ฌ๋ฌ๊ฐ]๋ฅผ ํ๋ฒ์ ์ ๋ก๋ ํด์ผํ๋ค๊ณ ๊ฐ์ ํด๋ณด์.
@Data /* Item ๋๋ฉ์ธ ๊ตฌํ */
public class Item {
private Long id;
private String itemName; // ์ํ ์ด๋ฆ
private UploadFile attachFile; // ์ฒจ๋ถํ์ผ 1๊ฐ
private List<UploadFile> imageFiles; // ์ด๋ฏธ์งํ์ผ ์ฌ๋ฌ๊ฐ
}
@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);
}
}
์ฐธ๊ณ ๋ก ํ์ผ๋ช ์ String์ผ๋ก ๊ด๋ฆฌํด๋ ๋์ง๋ง, ๊ด๋ฆฌํ๊ธฐ ํธํ๊ฒ UploadFile ๊ฐ์ฒด๋ฅผ ์ถ๊ฐ์ ์ผ๋ก ๋ง๋ค์๋ค.
@Data
public class UploadFile {
private String uploadFileName; // ๊ณ ๊ฐ์ด ์
๋ก๋ํ ํ์ผ ๋ช
private String storeFileName; // ์๋ฒ์ ์ ์ฅ๋๋ ํ์ผ ๋ช
(ํ์ผ์ด๋ฆ์ด ๊ฒน์น์ง์๊ฒ ์ฒ๋ฆฌ)
public UploadFile(String uploadFileName, String storeFileName) {
this.uploadFileName = uploadFileName;
this.storeFileName = storeFileName;
}
}
2. ํ์ผ์ ์๋ฒ์ ์ ์ฅํ FileStore ๊ฐ์ฒด ๊ตฌํ
์ปจํธ๋กค๋ฌ์์ MultiPart ๋ฐ์ดํฐ๋ฅผ ์ง์ ์ ์ฅํด๋ ๋์ง๋ง, ์ฑ ์์ ๋ถ๋ฆฌํ์ฌ FileStore ๊ฐ์ฒด๋ฅผ ๋ฐ๋ก ๋ง๋ค์.
@Component
public class FileStore {
@Value("${file.dir}")
private String fileDir;
// ํ์ผ ์ ์ฅ
public UploadFile storeFile(MultipartFile file) throws IOException {
if (file.isEmpty()) {
return null;
}
String originalFilename = file.getOriginalFilename();
String storeFileName = createStoreFileName(originalFilename);
file.transferTo(new File(getFullPath(storeFileName)));
// ์ ์ฅ๋ UploadFile ์ด๋ฆ์ ๋ง๋ค์ด ๋ฐํ
return new UploadFile(originalFilename, storeFileName);
}
// storFile์ ์ด์ฉํ ํ์ผ ์ฌ๋ฌ ๊ฐ ์ ์ฅ
public List<UploadFile> storeFiles(List<MultipartFile> files)
throws IOException {
List<UploadFile> storeFileResult = new ArrayList<>();
for (MultipartFile multipartFile : imageFiles) {
if (!imageFile.isEmpty()) {
storeFileResult.add(storeFile(multipartFile));
}
}
return storeFileResult;
}
// ์ ์ฅํ ๊ฒฝ๋ก ์ง์
private String getFullPath(String filename) {
return fileDir + filename;
}
// ์๋ฒ์ ์ ์ฅํ ๊ณ ์ ํ ํ์ผ๋ช
์์ฑ
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);
}
}
3. ์ปจํธ๋กค๋ฌ ๊ตฌํ
FileStore๋ฅผ ๋ณ๋๋ก ๋ง๋ค์์ผ๋ฏ๋ก, ์ปจํธ๋กค๋ฌ๋ ๊ตฌํ์ ๊ฐ๋จํ๋ค. ๊ทธ๋ฅ MultipartFile์ ๋ฐ์์ ์ ๋ฌํด์ฃผ๋ฉด ๋์ด๋ค.
@Data // Item_Upload_Request (DTO)
public class ItemForm {
private Long itemId;
private String itemName;
private List<MultipartFile> imageFiles;
private MultipartFile attachFile;
}
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemRepository itemRepository;
private final FileStore fileStore;
...
}
์ปจํธ๋กค๋ฌ์์ ์ฌ์ฉํ ํ์ผ์ ๋ก๋/๋ค์ด๋ก๋ HTML ํผ์ ๋ง๋ค์ด์ฃผ์.
์ฐธ๊ณ ๋ก <input>์ผ๋ก ์ฌ๋ฌ๊ฐ์ ํ์ผ์ ๋ฑ๋กํ๊ณ ์ถ๋ค๋ฉด, <input type="file" multiple="multiple"> ๋ก ์ค์ ํ ์ ์๋ค.
์ด๋ ์คํ๋ง ์ปจํ
์ด๋์์ List<MultipartFile> files ๋ก ์ฝ๊ฒ ๋ฐ์ ์ ์๋ค.
<!DOCTYPE HTML> <!-- uploadForm.HTML -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>์ํ ๋ฑ๋ก</h2>
</div>
<form enctype="multipart/form-data" method="post" th:action>
<ul>
<li>์ํ๋ช
<input name="itemName" type="text"></li>
<li>์ฒจ๋ถํ์ผ<input name="attachFile" type="file"></li>
<li>์ด๋ฏธ์ง ํ์ผ๋ค<input multiple="multiple" name="imageFiles"
type="file"></li>
</ul>
<input type="submit"/>
</form>
</div> <!-- /container -->
</body>
</html>
<!DOCTYPE HTML> <!-- upload-view.HTML -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>์ํ ์กฐํ</h2>
</div>
์ํ๋ช
: <span th:text="${item.itemName}">์ํ๋ช
</span>
<br/>
์ฒจ๋ถํ์ผ: <a th:href="|/attach/${item.id}|"
th:if="${item.attachFile}"
th:text="${item.getAttachFile().getUploadFileName()}"/>
<br/>
<img height="300"
th:each="imageFile : ${item.imageFiles}"
th:src="|/images/${imageFile.getStoreFileName()}|" width="300"/>
</div> <!-- /container -->
</body>
</html>
๊ทธ๋ฆฌ๊ณ ์๋์ ๊ฐ์ API๋ฅผ ์ ๊ณตํด์ฃผ๋ฉด ๋๋ค.
- Get ("/items/new") - ์ ๋ก๋์ฉ ํผ์ ์ ๊ณตํด์ค๋ค.
@GetMapping("/items/new")
public String newItem(@ModelAttribute ItemForm form) {
return "item-form";
}
- Post ("/items/new") - ํผ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ์๋ฒ์ ์ ์ฅํ๊ณ , ์ ๋ก๋ ํ๋ฉด์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ์ํจ๋ค.
@PostMapping("/items/new")
public String saveItem(@ModelAttribute ItemForm form, RedirectAttributes redirectAttributes)
throws IOException {
// ์๋ฒ์ ํ์ผ ์ ์ฅ
UploadFile attachFile = fileStore.storeFile(form.getAttachFile());
List<UploadFile> storeImageFiles = fileStore.storeFiles(form.getImageFiles());
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์ Item ๋๋ฉ์ธ ์ ์ฅ
Item item = new Item();
item.setItemName(form.getItemName());
item.setAttachFile(attachFile);
item.setImageFiles(storeImageFiles);
itemRepository.save(item);
// ๋ฆฌ๋ค์ด๋ ํธ (POST -> GET)
redirectAttributes.addAttribute("itemId", item.getId());
return "redirect:/items/{itemId}";
}
- Get ("/items/{ id }") - ์ ์ฅ๋ ์ํ์ ๋ณด์ฌ์ค๋ค
@GetMapping("/items/{id}")
public String items(@PathVariable Long id, Model model) {
Item item = itemRepository.findById(id);
model.addAttribute("item", item);
return "item-view";
}
- Get ("/imags/{ filename }") - <img> ํ๊ทธ๋ก ์ด๋ฏธ์ง๋ฅผ ์กฐํํ ๋ ์ฌ์ฉํ๋ค. ์ด๋ฏธ์ง ๋ฐ์ด๋๋ฆฌ๋ฅผ ๋ฐํํ๋ค.
์ด๋ ์คํ๋ง์ด ์ ๊ณตํด์ฃผ๋ UrlResource ๊ฐ์ฒด๋ฅผ ์ด์ฉํ๋ฉด ํ์ผ์ ์ฝ๊ฒ ๋ฐํํ ์ ์๋ค.
@ResponseBody // ๋ฐ์ด๋๋ฆฌ๋ฅผ ์ง์ HTTP Body๋ก ๋ฐํํ ๊ฑฐ๋ผ, @ResponseBody ์ฌ์ฉ
@GetMapping("/images/{filename}")
public Resource downloadImage(@PathVariable String filename) throws MalformedURLException {
return new UrlResource("file:" + fileStore.getFullPath(filename));
}
- Get ("/attach/{ itemId }") - ์ฒจ๋ถํ์ผ์ ๋ค์ด๋ก๋ ํ๋ค. ํด๋ผ์ด์ธํธ๊ฐ ๋ค์ด ํ์ผ๋ช ์ ์ ํ๋๋ก ๋ง๋๋๊ฒ ์ข๋ค.
@GetMapping("/attach/{itemId}")
public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException {
Item item = itemRepository.findById(itemId);
// HTTP content-disposition Header ์ค์ , ๋ธ๋ผ์ฐ์ ์์ ํด๋น ๋ฐ์ดํฐ๋ฅผ ๋ค์ด๋ก๋ํจ
// ex) Content-Disposition: attachment; filename="hello.jpg"
String uploadFileName = item.getAttachFile().getUploadFileName();
String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";
// ํ์ผ ๊ฒฝ๋ก๋ฅผ ์ฐพ์์ UrlResource ์์ฑ
String storeFileName = item.getAttachFile().getStoreFileName();
UrlResource resource = new UrlResource("file:" + fileStore.getFullPath(storeFileName));
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
}
Content Disposition ํค๋๋ฅผ ๋ฃ์ด์ฃผ๋ฉด, ๋ธ๋ผ์ฐ์ ์์ ๋ฆฌ์์ค์์ ์ธ์ํด์ ๋ค์ด๋ก๋ ํด์ค๋ค. (์๋๋ฉด URL ๋งํฌ๋ก ์ธ์ํจ)
์ฐธ๊ณ ๋ก ๋ฉํฐํํธ ๋ฐ์ดํฐ๋ ์๋์ ๊ฐ์ HTTP ํค๋๋ฅผ ๊ฐ์ง๋ค.
'๐ฑ Spring Framework > Spring MVC' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋ก๊ทธ ์ถ์ ๊ธฐ๋ก ์์๋ณด๋ ๋์์ธํจํด (0) | 2022.02.10 |
---|---|
Spring ํ์ ์ปจ๋ฒํฐ (Converter, Formatter) (0) | 2021.08.31 |
Spring ์์ธ์ฒ๋ฆฌ - API, @ExceptionHandler (0) | 2021.08.30 |
Spring ์์ธ์ฒ๋ฆฌ - ์ค๋ฅํ์ด์ง(404,500) (0) | 2021.08.30 |
Spring Login#2 ํํฐ, ์ธํฐ์ ํฐ, ArugmentResolver (0) | 2021.08.30 |
๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev