44. 사진 업로드 샘플링 V3 (+UUID 롤링)

박선규's avatar
Mar 04, 2024
44. 사진 업로드 샘플링 V3 (+UUID 롤링)

Pic 테이블 생성

package com.mtcoding.fileapp.pic; import jakarta.persistence.*; import lombok.Data; @Data @Table(name = "pic_tb") @Entity public class Pic { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String title; private String imgFilename; //파일 이름(파일 경로) }
💡
파일 자체는 하드디스크에 저장되고, 파일의 경로만 저장!
 

PicRepository

package com.example.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(); } public Pic findById(int id){ Query query = em.createNativeQuery("select * from pic_tb where id = ?", Pic.class); query.setParameter(1, id); return (Pic) query.getSingleResult(); } }
💡
title과 imgFilename을 DB에 저장
 

UUID - 롤링 (32자의 랜덤 수)

💡
롤링이란? 데이터나 숫자가 주기적이거나 연속적으로 변화하는 과정
notion image
package com.mtcoding.fileapp.util; 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
UUID의 해쉬값은 충돌날 확률이 아주아주아주극그극ㄱ극극 낮다. (1632승) 이런게 파일의 해시값으로 들어가면 어떻게 될까? -> 파일명을 계속 바꿔치기 함! HELLO.PNG 이라는 이름의 파일이 수만개가 동일하게 들어와도 앞에 해시값이 다 다를 것. -> 다 다른 파일로 인식(?)하여 충돌날 확률이 X
 

PicController

package com.example.fileapp.pic; import lombok.RequiredArgsConstructor; 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; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.UUID; @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. 파일저장 위치 설정해서 파일을 저장 //리팩토링 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:/"; } @GetMapping("/") public String index(){ return "index"; } @GetMapping("/uploadForm") public String uploadForm(){ return "uploadForm"; } @GetMapping("/uploadCheck") public String uploadCheck(){ return "uploadCheck"; } }
💡
롤링해서 저장을 하고, 롤링한 값이 db에 들어갈 것이다!
* 생성된 UUID 문자열과 원본 파일 이름 사이에 언더스코어(_)를 추가하여, 두 문자열을 구분 * imgFile.getOriginalFilename() 파일 업로드를 처리할 때 파일의 원본 이름을 가져오는 메서드 imgFile은 업로드된 파일을 나타내는 객체이며, getOriginalFilename() 메서드는 업로드된 파일의 원래 이름을 문자열로 반환. ex) 사용자가 photo.jpg라는 이름의 이미지 파일을 업로드했다면, 이 메서드는 "photo.jpg"라는 문자열을 반환 즉, 업로드된 파일에 대해 고유한 이름을 생성. (생성된 파일 이름은 UUID_원본파일이름 형태) f47ac10b-58cc-4372-a567-0e02b2c3d479_photo.jpg 식으로 저장된다. 이 방식은 파일 이름 충돌을 방지하고, 서버에 파일을 저장할 때 각 파일에 대해 유일한 식별자를 제공하는 효과적인 방법!
 

 
notion image
💡
DB에 저장되는 건 realFileName 나중에 이런 건 메소드를 따로 빼서 재사용 하는게 좋다
 

근데 DB에 들어오지 않는다

notion image
💡
static에 넣지말고 외부 upload 폴더를 생성하고, 여기에다가 넣어주자 → Path imgPath = Paths.get("./upload/" + realFileName); 로 변경!

PicController

@PostMapping("/upload") public String upload(PicRequest.UploadDTO requestDTO) { System.out.println(requestDTO.getTitle()); String title = requestDTO.getTitle(); MultipartFile imgFile = requestDTO.getImgFile(); String imgFilename = UUID.randomUUID() + "_" + imgFile.getOriginalFilename(); Path imgPath = Paths.get("./upload/"+imgFilename); try { Files.write(imgPath, imgFile.getBytes()); picRepository.insert(title, imgFilename); } catch (IOException e) { throw new RuntimeException(e); } return "redirect:/"; }
 

static이 아닌 다른 외부 폴더 개방 → WebMvcConfig 로 설정을 해줘야 함

notion image
package com.example.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()); } }
.addResourceHandelr(/upload/**), upload/** 같은 패턴이 들어오면 모두 아래의 ./upload 폴더에 들어가게끔 설정 (/upload로 시작하는 모든 요청이 이 핸들러에 의해 처리) .addResourceLocations("file:./upload") 웹에 이 경로를 열어줄게 하고 설정하는 것. 여기서 정적 리소스를 찾는다. (이 핸들러가 처리하는 요청에 대한 리소스가 저장된 위치를 지정) .setCachePeriod(60 * 60) 브라우저 캐싱을 위한 시간을 설정. 여기서는 60초(1분) * 60 = 3600초, 즉 1시간 동안 캐싱하도록 설정 .resourceChain(true) 리소스 체인 활성화. 리소스의 위치를 찾고 내용을 최적화하는데 사용 .addResolver(new PathResourceResolver()): PathResourceResolver 요청된 리소스의 경로를 해석하는 리졸버. 요청된 리소스의 실제 경로를 찾을 수 있다.
💡
/upload로 시작하는 모든 요청을 프로젝트 루트의 upload 폴더에 있는 정적 리소스로 매핑하고, 해당 리소스들을 1시간 동안 캐싱하며, 리소스 요청 경로를 해석하기 위해 PathResourceResolver를 사용하도록 설정
 

uploadCheck에도 경로 설정

notion image
만약 c드라이브라면 file:c:\\upload 이런 식으로... 외부 폴더 설정...

만약

notion image
이렇게 되면 static에 있는 a 폴더에 저장한다
 

저장 됐을거다 그럼 이제

uploadCheck.mustache

<h1>제목 : {{pic.title}}</h1> <img src="/upload/{{pic.imgFilename}}" width="500" height="500" alt="사진없음">

PicController

@GetMapping("/uploadCheck") public String uploadCheck(HttpServletRequest request){ Pic pic = picRepository.findById(1); request.setAttribute("pic", pic); return "uploadCheck"; }
💡
설정해주자! 그럼 끝!
 

static 에서 외부 폴더로 옮긴 이유

h2에서 리로드가 한번 더 되서 초기화가 되서 그렇습니다 DB에서 안나와서 MVCconfig를 썼던거는 DB에 안나와서
💡
웹에 개방된 폴더가 static과 upload폴더가 되었다.
 

 
💡
multipart/form-data 로 사진 받고, WebMvcConfig으로 사진이 저장되는 경로를 바꿀 수 있다. …정도만 알아두자!
 

화면 / DB 확인

notion image
notion image
notion image
notion image
 
 
 

전체 코드

config

package com.example.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()); } }
 

Pic

package com.example.fileapp.pic; import jakarta.persistence.*; import lombok.Data; @Data @Table(name = "pic_tb") @Entity public class Pic { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String title; private String imgFilename; // 파일 패스 }
 

PicController

package com.example.fileapp.pic; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; 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; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.UUID; @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:/"; } @GetMapping("/uploadCheck") public String uploadCheck(HttpServletRequest request){ Pic pic = picRepository.findById(1); request.setAttribute("pic", pic); return "uploadCheck"; } @GetMapping("/") public String index(){ return "index"; } @GetMapping("/uploadForm") public String uploadForm(){ return "uploadForm"; } }
 

PicRepository

package com.example.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(); } public Pic findById(int id){ Query query = em.createNativeQuery("select * from pic_tb where id = ?", Pic.class); query.setParameter(1, id); return (Pic) query.getSingleResult(); } }
 

PicRequest

package com.example.fileapp.pic; import lombok.Data; import org.springframework.web.multipart.MultipartFile; public class PicRequest { @Data public static class UploadDTO{ private String title; private MultipartFile imgFile; } }
 

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> <h1>제목 : {{pic.title}}</h1> <img src="/upload/{{pic.imgFilename}}" width="500" height="500" alt="사진없음"> </body> </html>
 

uploadForm

<!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"> <input type="text" name="title" placeholder="사진제목..."> <input type="file" name="imgFile" > <button>사진업로드</button> </form> </body> </html>
Share article

p4rksk