[Spring] 이미지 파일 업로드

류재성's avatar
Mar 04, 2024
[Spring] 이미지 파일 업로드
 

1. 기본 세팅

 
notion image
 
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-mustache'
 
세팅할 때 mustache 선택해도 된다.
 
application.yml
server: servlet: encoding: charset: utf-8 force: true spring: mustache: servlet: expose-session-attributes: true expose-request-attributes: true datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:test;MODE=MySQL username: sa password: h2: console: enabled: true jpa: hibernate: ddl-auto: create show-sql: true properties: hibernate: format_sql: true
 

2. 화면 만들기

 
notion image
 
index.mustache
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>메인페이지</h1> </body> </html>
 
uploadForm.mustache
 
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>사진등록페이지</h1> <hr> <ul> <li> <a href="/">메인페이지</a> </li> <li> <a href="/uploadForm">사진등록페이지</a> </li> <li> <a href="/uploadCheck">사진확인페이지</a> </li> </ul> <form action="/upload" method="post" enctype="multipart/form-data"> // multipart/form-data 다양한 타입을 한꺼번에 보낼 수 있음. <input type="text" name="title" placeholder="사진제목..."> <input type="file" name="imgFile"> //사진 업로드 태그. <button>사진업로드</button> </form> </body> </html>
 
uploadCheck.mustache
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>사진확인페이지</h1> <hr> <ul> <li> <a href="/">메인페이지</a> </li> <li> <a href="/uploadForm">사진등록페이지</a> </li> <li> <a href="/uploadCheck">사진확인페이지</a> </li> </ul> <img src="#" width="500" height="500" alt="사진없음"> </body> </html>
 
notion image
 
 

3. DTO 만들기

 
package shop.mtcoding.fileapp.pic; import lombok.Data; import org.springframework.web.multipart.MultipartFile; public class PicRequest { @Data public static class UploadDTO{ private String imgTitle; // 키 값이 title private MultipartFile imgFile; // 이미지 파일 } }
 

4. 컨트롤

 
package shop.mtcoding.fileapp.pic; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.multipart.MultipartFile; @Controller public class PicController { @PostMapping("/upload") public String upload(PicRequest.UploadDTO requestDTO){ System.out.printf(requestDTO.getTitle()); MultipartFile imgFile = requestDTO.getImgFile(); // 사진은 바이트 System.out.printf(imgFile.getContentType()); System.out.printf(imgFile.getOriginalFilename()); return "redirect:/"; } @GetMapping("/") public String index(){ return "index"; } @GetMapping("/uploadForm") public String uploadForm(){ return "uploadForm"; } @GetMapping("/uploadCheck") public String uploadCheck(){ return "uploadCheck"; } }
 
 
notion image
 
notion image
 
 

5. 파일 폴더에 저장

 
💡
서버에는 사진의 경로만 저장. 파일은 하드디스크에 저장한다. 저장 폴더는 static 폴더에 저장한다. 그래야 외부에서 파일에 접근할 수 있다.
 
@PostMapping("/upload") public String upload(picRequest.uploadDTO requestDTO){ String title = requestDTO.getTitle(); MultipartFile imgFile = requestDTO.getImgFile(); // 사진은 바이트 String imgFileName = imgFile.getOriginalFilename(); //db에는 파일 경로 저장 Path imgPath = Paths.get("./src/main/resources/static/upload/"+imgFileName); // "/" 은 최상위 폴더, "./" 은 프로젝트 폴터 //Path imgPath = Paths.get("a/"+imgFileName); // a 라는 폴더 내부에 저장. try { Files.write(imgPath,imgFile.getBytes()); } catch (IOException e) { throw new RuntimeException(e); } return "redirect:/"; }
 
 
uploadCheck.mustache
<img src="/upload/down.jpeg" width="500" height="500" alt="사진없음">
notion image
notion image
 
💡
실제 원래 파일명으로 저장하면 파일명이 중복되어 업로드가 되지 않을 수 있다. 그래서 해쉬를 사용해 이름의 중복을 피한다.
 
 

6. 파일명 중복 피하기

 
@PostMapping("/upload") public String upload(picRequest.uploadDTO requestDTO){ // 1. 데이터 전달받음. String title = requestDTO.getTitle(); MultipartFile imgFile = requestDTO.getImgFile(); // 사진은 바이트 // 2. 파일 저장 위치 설정해서 파일 저장 String imgFileName = imgFile.getOriginalFilename(); //db에는 파일 경로 저장 String realFileName = UUID.randomUUID()+""+imgFileName ; // 중복을 막기 위해 해시값을 추가 Path imgPath = Paths.get("./src/main/resources/static/upload/"+realFileName); // "/" 은 최상위 폴더, "./" 은 프로젝트 폴터 // Path imgPath = Paths.get("a/"+imgFileName); // a 라는 폴더 내부에 저장. try { Files.write(imgPath,imgFile.getBytes()); // 3. db에 저장(title, imgFileName) } catch (IOException e) { throw new RuntimeException(e); } return "redirect:/"; }
 
💡
UUID 를 사용해 임의의 해시값을 파일명에 붙인다.
 
notion image
 

6. 파일 경로 DB에 저장

 
package shop.mtcoding.fileapp.pic; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Repository public class PicRepository { private final EntityManager em; @Transactional public void insert(String title, String imgFilename){ Query query = em.createNativeQuery("insert into pic_tb(title, img_filename) values(?,?)"); query.setParameter(1, title); query.setParameter(2, imgFilename); query.executeUpdate(); } }
 
DB에 파일 제목과 이미지 파일이 저장된 주소를 입력한다.
 
 
@RequiredArgsConstructor @Controller public class PicController { private final PicRepository picRepository; @PostMapping("/upload") public String upload(PicRequest.UploadDTO requestDTO) { // 1. 데이터 전달 받고 String title = requestDTO.getTitle(); MultipartFile imgFile = requestDTO.getImgFile(); // 2. 파일저장 위치 설정해서 파일을 저장 (UUID 붙여서 롤링) String imgFilename = UUID.randomUUID() + "_" + imgFile.getOriginalFilename(); Path imgPath = Paths.get("./src/main/resources/static/upload/" + imgFilename); try { Files.write(imgPath, imgFile.getBytes()); // 3. DB에 저장 (title, realFileName) picRepository.insert(title, imgFilename); } catch (IOException e) { throw new RuntimeException(e); } return "redirect:/"; }
 
💡
폴더가 static 내부에 있어서 사진은 DB에 저장이 되지 않는다. 그래서 폴더를 외부로 옮겨야겠다.
 
 

7. 파일 외부 폴더로 받기

 
notion image
 
static 외부에 폴더를 만든다.
 
config/WebMvcConfig
package shop.mtcoding.fileapp.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.resource.PathResourceResolver; @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { WebMvcConfigurer.super.addResourceHandlers(registry); registry .addResourceHandler("/upload/**") //이 메서드를 오버라이드해서 원하는 정적 리소스 핸들러를 추가 .addResourceLocations("file:./upload/") .setCachePeriod(60 * 60) // 초 단위 => 한시간 .resourceChain(true) .addResolver(new PathResourceResolver()); } }
 
스프링에서 외부 폴더를 접근할 수 있는 코드를 설정한다.
 
💡
Spring MVC 설정이며 특히 정적 리소스에 대한 설정을 담당 addResourceHandlers(ResourceHandlerRegistry registry) : 이 메서드를 오버라이드해서 원하는 정적 리소스 핸들러를 추가
addResourceHandler("/upload/**") : '/upload/' URL 패턴으로 시작하는 모든 요청을 처리. '**'는 모든 하위 경로를 포함한다.
addResourceLocations("file:./upload/") : '/upload/' URL 패턴으로 들어온 요청이 참조하는 리소스가 위치한 경로를 설정 .setCachePeriod(60 * 60) : 브라우저 측에서 캐싱할 시간을 초 단위로 설정. 여기서는 한 시간동안 캐싱하도록 설정 resourceChain(true).addResolver(new PathResourceResolver()) : 리소스 체인을 활성화하고, PathResourceResolver를 추가. 이는 '/upload/' 경로 아래에서 요청된 리소스를 찾을 수 있도록 도와준다. '/upload/'로 시작하는 모든 HTTP 요청을 프로젝트의 'upload' 디렉토리로 매핑하며, 해당 리소스들은 한 시간 동안 캐싱한다.
 
@RequiredArgsConstructor @Controller public class PicController { private final PicRepository picRepository; @PostMapping("/upload") public String upload(PicRequest.UploadDTO requestDTO) { // 1. 데이터 전달 받고 String title = requestDTO.getTitle(); MultipartFile imgFile = requestDTO.getImgFile(); // 2. 파일저장 위치 설정해서 파일을 저장 (UUID 붙여서 롤링) String imgFilename = UUID.randomUUID() + "_" + imgFile.getOriginalFilename(); Path imgPath = Paths.get("./upload/" + imgFilename); try { Files.write(imgPath, imgFile.getBytes()); // 3. DB에 저장 (title, realFileName) picRepository.insert(title, imgFilename); } catch (IOException e) { throw new RuntimeException(e); } return "redirect:/"; }
 
컨트롤러의 경로를 재설정 한다.
 
💡
UUID(Universally Unique Identifier) 는 네트워크 상에서 고유성이 보장되는 ID를 만들기 위한 표준 규약이다. 128비트의 숫자이며, 32자리의 16진수로 표현한다.
 
test/java/shop.metacoding.fileapp/example/UUIDTest
package shop.mtcoding.fileapp.example; import org.junit.jupiter.api.Test; import java.util.UUID; public class UUIDTest { @Test public void rolling_test(){ UUID uuid = UUID.randomUUID(); String value = uuid.toString(); System.out.println(value); } }
 
notion image
 
이런 난수가 파일명 앞에 붙는다.

8. 데이터 화면에 출력

 
@PostMapping("/upload") public String upload(PicRequest.UploadDTO requestDTO) { // 1. 데이터 전달 받고 String title = requestDTO.getTitle(); MultipartFile imgFile = requestDTO.getImgFile(); // 2. 파일저장 위치 설정해서 파일을 저장 (UUID 붙여서 롤링) String imgFilename = UUID.randomUUID() + "_" + imgFile.getOriginalFilename(); Path imgPath = Paths.get("./upload/" + imgFilename); try { Files.write(imgPath, imgFile.getBytes()); // 3. DB에 저장 (title, realFileName) picRepository.insert(title, imgFilename); } catch (IOException e) { throw new RuntimeException(e); } return "redirect:/"; } @GetMapping("/uploadCheck") public String uploadCheck(HttpServletRequest request){ Pic pic = picRepository.findById(1); request.setAttribute("pic", pic); return "uploadCheck"; }
 
uploadCheck.mustache
<h1>제목 : {{pic.title}}</h1> <img src="/upload/{{pic.imgFilename}}" width="500" height="500" alt="사진없음">
 
 
notion image
 
DB에 정상적으로 저장이 완료된다.
 
notion image
 
화면에 파일과 이름을 출력한다.
Share article

{CODE-RYU};