이번에는 스프링 부트에서 이미지를 저장하는 방법을 알아보고자 한다.
사실 해당 내용을 공부하기 전 내가 궁금했던 부분은 아래 두가지였다.
- Q1. html form에서 저장된 이미지는, 어떠한 형태로 스프링 부트에게 전달되는가
- Q2. 전달받은 이미지는 DB에 어떻게 저장되는가
해당 내용만 먼저 간단하게 살펴보자면
- A1.
스프링 부트에서는MultipartFile
클래스를 제공해주고, form 형식으로 파일이 날아오면, 자동으로MultipartFile
형태로 전환해준다.
추후MultipartFile.transferTo()
메소드를 이용하여 자바의File
타입으로 간단하게 전환할 수 있다. - A2.
보통 DB에 이미지 파일을 저장하는 방법은 아래 두가지가 있다.- 이미지 데이터 자체를 DB에 저장(이진값 형태로)
- 이미지는 로컬 폴더에 따로 저장 후, 이미지가 저장된 경로를 DB에 저장
해당 포스트에서도 2번 방법으로 이미지를 저장하는 방법을 다룬다.
그럼 이제부터 전체 과정을 살펴보자
01. 이미지 저장 전체 로직
이미지 저장 로직은 위 이미지와 같다.
사실 대부분의 부분이 일반적인 스프링의 웹 계층 구조와 유사하고, ImageHandler
부분을 유의하여 보면 될 것 같다.
- 사용자는 이미지 파일을 POST 요청으로 서버에 전송한다. 이때 데이터 타입은
multipart/form-data
형식으로 전송한다. - 컨트롤러는 해당 요청을 받는다. 이때 저장할 이미지를
MultipartFile
형태로 받아와 서비스 계층에 넘겨준다 - 서비스는 먼저 전달받은 이미지를 서버 PC에 저장한다. 이때 파일 저장 과정은
ImageHandler
클래스에게 위임한다.ImageHandler
는 이미지를 저장한 후 이미지가 저장된 경로를 리턴한다. - 서비스는 이미지 저장 후 리턴받은 이미지 저장 경로를 레퍼지토리 계층에 넘겨준다.
이때 이미지 경로는Entity
형태로 전달된다. - 레퍼지토리는 전달받은 경로를 DB에 저장한다.
전체 로직은 위와 같다.
한번 실제 코드로 살펴보자
02. 코드 구현
✨ Entity 구현 (Image)
- Image.java
package com.example.ImageTest.entity;
@Entity
@Getter
@Setter
public class Image {
@Id
@NotNull
String path="";
}
먼저 Entity
타입을 구현해준다.
본 포스팅에서는 빠른 구현을 위해 이미지가 저장된 경로만 속성값으로 가지지만, 실제로 서비스를 구현할 때는 별도의 아이디, 원본 이미지 이름, 저장된 이미지 경로 를 모두 속성값으로 가지는 것을 추천한다.
✨ Repository 구현 (ImageRepository)
- ImageRepository.java
package com.example.ImageTest.repository;
@Repository
public interface ImageRepository extends JpaRepository<Image, String> {
}
해당 포스팅에서는 JPA를 사용해 빠르게 구현하였다.
✨ ImageHandler 구현(ImageHandler)
- ImageHandler.java
package com.example.ImageTest.ImageHandler;
public class ImageHandler {
public String save(MultipartFile image) throws IOException {
String fileName = getOriginName(image);
String fullPathName = "C:\\Users\\gkwns\\hajunFolder\\SpringBoot\\Spring-Boot-Start\\" +
"09\_ImageTest\\src\\main\\resources\\static\\imgs\\" + fileName;
image.transferTo(new File(fullPathName));
return fullPathName;
}
}
private String getOriginName(MultipartFile image){
return image.getOriginalFilename();
}
}
ImageHandler
는 서버안에 실제로 이미지를 저장하는 부분을 담당한다.
getOriginName()
: 전달받은 이미지 파일의 이름을 반환한다.save()
: 전달받은 이미지를 저장한다.MultipartFile
의transferTo()
메소드를 호출하여 파일을 저장한다.
이미지를 저장할 때 주의할 점이 몇가지 있다.
- 절대 경로 사용하기
스프링에서는 파일을 저장할 때 절대 경로의 사용을 권장한다.
때문에 해당 예제에서도 저장되는 경로를 절대 경로로 설정해 주었다. - 독립되는 파일 이름 사용하기
만약 저장하려는 파일과 같은 이름을 가진 파일이 기존에 존재할 경우, 해당 파일은 덮어 씌어진다.
이를 위해 새로 저장하는 파일의 이름은 각자의 방법대로 기존 파일과 구별되는 이름임을 보장해 주어야 한다.
해당 예제에서는 빠른 구현을 위해, 원본 이미지 이름 그대로 저장했으나, 이는 절대 좋은 방법이 아니다.
실제 서비스를 구현할때는 꼭!!! 구별되는 이름을 새로 생성해주자.
✨ Service 구현 (ImageService)
- ImageService.java
package com.example.ImageTest.Service;
@Service
public class ImageService {
ImageHandler imageHandler = new ImageHandler();
@Autowired
ImageRepository imageRepository;
public boolean saveImage(MultipartFile image) throws IOException {
String path = imageHandler.save(image);
Image imageEntity = new Image();
imageEntity.setPath(path);
imageRepository.save(imageEntity);
return true;
}
public Optional<String> findOne(){
List<Image> list = imageRepository.findAll();
if (list.isEmpty()) return Optional.empty();
return Optional.of(list.getFirst().getPath());
}
}
각 메소드의 역할은 아래와 같다.
saveImage()
: 이미지를 저장한다.
먼저ImageHandler
를 호출하여 서버에 이미지 저장 후,ImageRepository
를 호출하여 저장된 이미지의 경로를 DB에 저장한다.findOne()
: DB에 저장되어있는 이미지중 아무거나 하나를 가져온다.
(해당 예제에서는 빠른 구현을 위해, 여러 이미지를 저장하는 경우는 고려하지 않는다,,,)
만약 저장된 이미지가 없을 경우 비어있는 Optional 객체를 리턴한다.
✨ Controller 구현 (ImageController)
- ImageController.java
package com.example.ImageTest.controller;
@Controller
public class ImageController {
@Autowired
ImageService imageService;
@GetMapping("/upload")
public String getUploadPage(){
return "upload";
}
@GetMapping("/view")
public String getViewPage(Model model){
Optional<String> imgPath = imageService.findOne();
if (imgPath.isPresent()){
String[] filenames = imgPath.get().split("\\\\");
model.addAttribute("img", filenames[filenames.length-1]);
}
return "view";
}
@ResponseBody
@GetMapping("/images/{filename}")
public Resource showImage(@PathVariable String filename) throws MalformedURLException {
return new UrlResource("file:" + "C:\\Users\\gkwns\\hajunFolder\\SpringBoot\\Spring-Boot-Start\\" +
"09\_ImageTest\\src\\main\\resources\\static\\imgs\\" + filename);
}
@PostMapping("/save")
public String postSaveFile(@RequestParam MultipartFile image) throws IOException {
if(!image.isEmpty()){
imageService.saveImage(image);
}
return "redirect:/view";
}
}
각 주소에 대하여 비즈니스 로직 실행 후 view를 리턴한다.
/upload
: 이미지를 업로드하는 페이지/view
: 저장된 이미지를 확인하는 페이지, DB에서 이미지가 저장된 경로를 가져와 모델에 추가한다./images/{filename}
: 이미지 리소스 파일을 리턴하는 페이지/save
: 이미지 저장 요청을 받는 URL, 해당 URL에 POST 요청이 날아오면 저장 로직을 실행한다.
여기서 주의 깊게 볼 부분은 /images/{filename}/
경로이다.
스프링 부트에서 외부의 리소스를 불러올때는 Resource
타입의 객체로 반환해 줘야한다.
이를 위해 이미지 파일의 리소스 객체를 반환하는 URI를 먼저 만들고, 실제로 결과를 확인하는 페이지에서는 해당 URI를 통해 리소스 객체를 가져와야 한다.
- 여기서 리소스 객체를 생성할 때는 file: 뒤에 실제 파일 경로를 붙여주면 된다.
✨ View 구현
- upload.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/save" method="post" enctype="multipart/form-data">
<input type="file" name="image">
<button type="submit">업로드</button>
</form>
</body>
</html>
이미지를 업로드하는 페이지.
이미지 파일을 선택 후 /save
경로로 요청을 전송한다.
이때 데이터 인코딩 형식은 `multipart/form-data'로 설정한다.
- view.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<img th:src="'/images/'+${img}" th:if="${img} != null">
<div th:unless="${img} != null">이미지 없음</div>
</body>
</html>
저장된 이미지를 확인하는 페이지.
타임리프로 구현했다.
만약 이미지가 1개 이상 저장되어 있으면 해당 이미지를 표시하고, 그렇지 않으면 이미지 없음이라고 표시한다.
<img>
태그의 src
태그은 앞전에 정의한 Resource 객체를 반환하는 주소를 참조한다.
03. 실행 결과
✨ 이미지 업로드 전
이미지 저장 이전에는 db와 이미지 저장 폴더가 비어있다.
✨ 이미지 업로드
✨ 이미지 업로드 결과
이미지 저장 이후 DB와 폴더에 정상적으로 이미지가 저장된 모습을 확인 할 수 있다
'Back End > Spring && Spring Boot' 카테고리의 다른 글
[Spring Data JPA] Repository 메소드 작성 규칙 (1) | 2024.06.07 |
---|---|
[Spring/JPA] JPA 개념 (0) | 2024.06.06 |
[Spring Security] 세션 관련 설정하기 - 세션 만료 시간, 최대 세션 갯수, 세션 고정 설정 (0) | 2024.06.04 |
[Spring Security] 로그인 과정 살펴보기 (0) | 2024.06.03 |
[Spring Security] 회원 가입 로직 (0) | 2024.06.02 |