1. 기본 세팅

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. 화면 만들기

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>

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


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="사진없음">


실제 원래 파일명으로 저장하면 파일명이 중복되어 업로드가 되지 않을 수 있다. 그래서 해쉬를 사용해 이름의 중복을 피한다.
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 를 사용해 임의의 해시값을 파일명에 붙인다.

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. 파일 외부 폴더로 받기

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

이런 난수가 파일명 앞에 붙는다.
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="사진없음">

DB에 정상적으로 저장이 완료된다.

화면에 파일과 이름을 출력한다.
Share article