JiwonDev

Spring ํŒŒ์ผ ์—…๋กœ๋“œ, ๋‹ค์šด๋กœ๋“œ

by JiwonDev

์ฐธ๊ณ ๋กœ ๋ฐฑ์—”๋“œ์—์„œ ํŒŒ์ผ์„ ์—…๋กœ๋“œ/๋‹ค์šด๋กœ๋“œ ํ•  ๋•Œ,

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—๋Š” ํŒŒ์ผ ๊ฒฝ๋กœ (๋ณดํ†ต ๊ธฐ๋ณธ root๋ฅผ ์ œ์™ธํ•œ ์ƒ์„ธ ๊ฒฝ๋กœ)๋ฅผ ์ €์žฅํ•ด๋‘๊ณ 

ํŒŒ์ผ์€ ๋‹ค๋ฅธ ๊ฒฝ๋กœ์— ๋”ฐ๋กœ ์ €์žฅํ•˜๊ฑฐ๋‚˜, ๋ณ„๋„์˜ ์ปจํ…์ธ ์šฉ ์„œ๋ฒ„์— ๊ด€๋ฆฌํ•œ๋‹ค.

๊ธ€ ๋งˆ์ง€๋ง‰์— ์‹ค์ œ ์ฝ”๋“œ๋Š” ์–ด๋–ป๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š”์ง€ ์„ค๋ช…ํ•˜๋„๋ก ํ•˜๊ฒ ๋‹ค.

 

 

# HTTP์—์„œ๋Š” ์–ด๋–ป๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š”๊ฐ€

HTTP์—์„œ ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ Body์— ๋„ฃ์–ด ์ „์†กํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๋ฐฉ๋ฒ•์€ POST - Form ๋ฐฉ์‹์ด๋‹ค.

ํผ์„ ์ „์†กํ•  ๋•Œ HTTP ํ—ค๋”์— content-type = application/x-www-form-urlencoded๋กœ ์ง€์ •ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๋™์ž‘ํ•œ๋‹ค. 

 โžก ์ด ๋ฐฉ์‹์€ ๋ชจ๋“  ๋ฉ”์‹œ์ง€๋ฅผ String์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์„œ ์ „์†กํ•œ๋‹ค. 

HTTP ๋ฉ”์‹œ์ง€๋Š” Header๋‚˜ Body๋‚˜ ๊ฒฐ๊ตญ ํ…์ŠคํŠธ๋กœ ์ด๋ฃจ์–ด์ง„ ๋ฐ์ดํ„ฐ์ด๋‹ค.

 

@ 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"๋„ ๊ฐ™์ด ์ง€์ •ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

์ด๋ฏธ์ง€, ์‚ฌ์ง„ ๊ฐ™์€ ์ด์ง„ ๋ฐ์ดํ„ฐ๋„ ๊ฒฐ๊ตญ 2์ง„์ˆ˜๋กœ ์ด๋ฃจ์–ด์ง„ ๊ฐ’์ด๋‹ค. HTTP body์— ๋‹ด์•„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

 

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";
}

์ฐธ๊ณ ๋กœ ํ•ด๋‹น ๋กœ๊ทธ๋Š” logging.level.org.apache.coyote.http11=debug ๋กœ ํ•ด์•ผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
์œ„์— ๋งํ•œ multi-part ๋ฐฉ์‹์œผ๋กœ, ์ด๋ฏธ์ง€ํŒŒ์ผ(์ด์ง„ํŒŒ์ผ)์„ ์ „์†ก๋ฐ›๊ณ  ์žˆ๋‹ค.

 

์„œ๋ธ”๋ฆฟ์—์„œ๋Š” 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 ์š”์ฒญ์„ ํ•  ์ˆ˜ ์žˆ๋Š” ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ ๋‹ค.

HTML์˜ &amp;amp;amp;lt;input type="file"&amp;amp;amp;gt; ํƒœ๊ทธ๋ฅผ ์ด์šฉํ•˜๋ฉด ์†์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

 

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 ํ—ค๋”๋ฅผ ๊ฐ€์ง„๋‹ค.

 

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ