inblog logo
|
{CODE-RYU};
    SPIRNG

    [Spring] 이미지 파일 업로드

    류재성's avatar
    류재성
    Mar 04, 2024
    [Spring] 이미지 파일 업로드
    Contents
    1. 기본 세팅2. 화면 만들기3. DTO 만들기4. 컨트롤5. 파일 폴더에 저장6. 파일명 중복 피하기6. 파일 경로 DB에 저장7. 파일 외부 폴더로 받기8. 데이터 화면에 출력
     

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

    RSS·Powered by Inblog