- 지난 게시글
2024.09.02 - [Spring Boot] - [Spring Boot] 스프링 이미지 업로드 예제 (0) - 예시 상황, 공통 코드
2024.09.02 - [Spring Boot] - [Spring Boot] 스프링 이미지 업로드 예제 (1) - 프로젝트 내부 디렉토리에 업로드
2024.09.02 - [Spring Boot] - [Spring Boot] 스프링 이미지 업로드 예제 (2) - DB에 이미지 직접 저장
1. 설명
- 예제(2) 에서 DB에 이미지를 직접 저장함
- 실제 프로젝트 배포 시 트래픽이 많아져서 업로드 되는 이미지가 많다면 DB에 무리가 갈 수 있음 ⇒ 성능 저하 초래 가능!
- 서버 ↔ 클라이언트 간 이동해야 할 데이터 양이 많아짐 ⇒ 속도 느려질 수 있음!
- 외부 경로에 이미지를 업로드 하고 DB에는 이미지 이름만 저장하는 방식으로 해결
- 예제(1)의 정적 리소스 로드 문제점과 예제(2)의 DB 직접 저장 문제점을 모두 해결 가능하다고 판단
- 이미지 이름에 UUID 추가, 예외 조건 등은 이전과 동일하게 작성
이전 공통 코드, 예제(1),(2) 코드를 기반으로 작성하되 V3를 붙인 클래스를 생성함
2. 추가 코드
2-1. application.yml
- 이전과 동일
2-2. config > WebMvcConfig.java
- 스프링의 설정 인터페이스 (WebMvcConfigurer 인터페이스) 구현체
- 외부 경로에 업로드(저장)된 이미지를 불러오는 설정을 추가함
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/v3/img/**")
.addResourceLocations("file:///Users/anchangmin/Desktop/image_upload/");
}
}
- addResourceHandler
- 설정한 경로 (여기서는 "/v3/img/" 하위 모두) 로 이미지를 요청할 때 해당 요청을 처리할 리소스 핸들러 등록
- addResourceLocations
- 리소스 핸들러에 실제 파일이 위치한 외부 경로를 설정함
- 실제 외부 경로 앞에 file:// 을 붙여야 함!
쉽게 말해,
/v3/img/example.jpg 로 이미지 요청을 보내면
서버가 실제로 /Users/anchangmin/Desktop/image_upload/example.jpg 파일을 찾아서 반환함!
2-3. domain
- PostImagesV3
- 예제(1) 과 유사하지만 여기서는 이미지 이름만 저장 (경로는 위에서 작성한 리소스 핸들러가 해결해 줄 것!)
@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "post_images_v3")
public class PostImagesV3 {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_images_id")
private Long id;
// 다대일 양방향 연관관계
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private PostV3 post;
// 저장한 이미지 이름 (UUID + 이미지 이름)
private String imageName;
// 연관관계 편의 메서드
public void setPost(PostV3 post) {
this.post = post;
post.getPostImagesList().add(this);
}
public PostImagesV3(String imageName) {
this.imageName = imageName;
}
}
- PostV3
- 예제(1), 예제(2)와 동일
@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "post_v3")
public class PostV3 {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
private String title;
private String content;
// 일대다 양방향 연관관계
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostImagesV3> postImagesList = new ArrayList<>();
public PostV3(String title, String content) {
this.title = title;
this.content = content;
}
}
- PostV3Dto
- 예제(1), (2)와 동일
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PostV3Dto {
private String title;
private String content;
// 다수의 이미지 (리스트) 입력받기 위해 추가
private List<MultipartFile> imageList;
}
2-4. repository
- PostImagesV3Repository
- 예제(1), (2)와 동일
@Repository
public interface PostImagesV3Repository extends JpaRepository<PostImagesV3, Long> {
}
- PostV3Repository
- 예제(1), (2)와 동일
@Repository
public interface PostV3Repository extends JpaRepository<PostV3, Long> {
}
2-5. service
- PostImagesV3Service
- 예외 처리 코드, 이미지 이름에 UUID 추가 등은 예제(1), (2)와 동일
- 이미지를 외부 경로에 저장 + PostImages에는 이미지 이름 저장
@Service
@RequiredArgsConstructor
public class PostImagesV3Service {
private final PostImagesV3Repository postImagesV3Repository;
@Transactional
public void saveImage(List<MultipartFile> imageList, PostV3 post) {
// 업로드한 파일이 없다면 리턴
if(imageList == null) return;
// 사진 업로드 개수 <= 5 인지 체크
if(imageList.size() > 5) throw new IllegalArgumentException("최대 사진 업로드 개수는 5개 입니다!");
// 업로드 파일의 타입이 사진인지 체크
for (MultipartFile image : imageList) {
if(!isImageFile(image)) throw new IllegalArgumentException("사진 이외의 파일은 업로드 불가능합니다!");
}
// 각 사진에 대해 업로드와 db에 저장
for (MultipartFile image : imageList) {
// 파일이 비어있는 경우 패스
if(Objects.requireNonNull(image.getOriginalFilename()).isEmpty()) continue;
// 이미지 이름 앞에 랜덤값 추가 (중복 안되게)
String imageName = addUUID(image.getOriginalFilename());
// 이미지 업로드
try {
uploadImage(image, imageName);
} catch (IOException e) {
throw new RuntimeException("이미지 업로드를 실패했습니다!");
}
// PostImages 엔티티 저장
savePostImages(imageName, post);
}
}
// 파일이 이미지 형식인지 검사하는 메서드
private static boolean isImageFile(MultipartFile image) {
String contentType = image.getContentType();
return contentType != null && (contentType.startsWith("image/"));
}
// 이미지 이름 중복 피하기 위해 이미지 이름 앞에 UUID 추가하는 메서드
private static String addUUID(String originalFilename) {
return UUID.randomUUID().toString().replace("-", "") + "_" + originalFilename;
}
// 이미지를 외부경로에 업로드하는 메서드
private static void uploadImage(MultipartFile image, String imageName) throws IOException {
// 이미지를 실제 업로드 할 경로 (Users/anchangmin/Desktop/image_upload/ 에 imageName으로 업로드)
String uploadPath = "/Users/anchangmin/Desktop/image_upload/" + imageName;
// 업로드 경로를 Path 객체로 변환
Path path = Paths.get(uploadPath);
// 경로의 부모 디렉토리가 존재하지 않으면 생성
Files.createDirectories(path.getParent());
// 이미지 바이트를 파일에 기록
Files.write(path, image.getBytes());
}
// PostImages 엔티티 생성 및 저장하는 메서드
private void savePostImages(String imageName, PostV3 post) {
// PostImages 이미지 이름 설정함
PostImagesV3 postImages = new PostImagesV3(imageName);
// 연관관계 편의 메서드 통해서 Post 를 설정
postImages.setPost(post);
// 레파지토리에 엔티티 저장
postImagesV3Repository.save(postImages);
}
}
더보기
예제(1)과 다른 점
- 예제(1)에서는 이미지를 프로젝트 내부 경로에 저장했음
- 예제(1)의 PostImages 에 이미지 경로를 저장했지만 여기서는 이미지 이름만 저장함
- 앞서 작성했듯 리소스 핸들러가 자동으로 경로 처리!
// 이미지를 실제 업로드 할 경로 (Users/anchangmin/Desktop/image_upload/ 에 imageName으로 업로드)
String uploadPath = "/Users/anchangmin/Desktop/image_upload/" + imageName;
여기서 리소스 핸들러에 작성한 외부 경로를 넣어주면 됨 (file:// 제외)
- PostV3Service
- 예제(1), (2)와 동일
@Service
@RequiredArgsConstructor
public class PostV3Service {
private final PostV3Repository postV3Repository;
private final PostImagesV3Service postImagesV3Service;
// 모든 게시글 리턴
public List<PostV3> findAll() {
return postV3Repository.findAll();
}
// 특정 게시글 리턴
public PostV3 findById(Long postId) {
return postV3Repository.findById(postId).orElse(null);
}
// 게시글 저장
@Transactional
public void save(PostV3Dto postDto) {
PostV3 post = new PostV3(postDto.getTitle(), postDto.getContent());
// 이미지 저장 로직 추가
postImagesV3Service.saveImage(postDto.getImageList(), post);
postV3Repository.save(post);
}
}
2-6. controller
- PostV3ApiController
- 예제(1), (2)와 동일
@RestController
@RequiredArgsConstructor
@RequestMapping("/v3/api/posts")
public class PostV3ApiController {
private final PostV3Service postV3Service;
// 모든 게시글 조회
@GetMapping
public ResponseEntity<List<PostV3>> findAllPosts() {
return ResponseEntity.ok(postV3Service.findAll());
}
// 특정 게시글 단건 조회
@GetMapping("/{postId}")
public ResponseEntity<PostV3> findPost(@PathVariable("postId") Long postId) {
PostV3 post = postV3Service.findById(postId);
return post == null ? ResponseEntity.status(HttpStatus.NO_CONTENT).body(null) : ResponseEntity.ok(post);
}
// 게시글 작성
@PostMapping(consumes = "multipart/form-data")
public ResponseEntity<Void> savePost(@ModelAttribute PostV3Dto postDto) {
postV3Service.save(postDto);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
- PostV3Controller
- 예제(1), (2)와 동일
@Controller
@RequiredArgsConstructor
@RequestMapping("/v3/posts")
public class PostV3Controller {
private final PostV3Service postV3Service;
@GetMapping("/{postId}")
public String home(@PathVariable("postId") Long postId, Model model) {
model.addAttribute("post", postV3Service.findById(postId));
return "v3_home";
}
}
2-7. html
- 예제(1)과 유사하지만 이미지 이름 앞에 registry.addResourceHandler() 에 작성한 uri를 추가
- 여기서는 '/v3/img/' 를 추가
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" >
<h1>제목: [[${post.title}]]</h1>
<h1>내용: [[${post.content}]]</h1>
<div th:each="post_image : ${post.postImagesList}">
<img th:src="${'/v3/img/' + post_image.imageName}" style="width: 200px"/>
</div>
</html>
3. 실행 결과
3-1. 이미지 첨부 게시글 작성 (POST localhost:8080/v3/api/posts)
3-2. MySQL 확인
3-3. 외부 경로에 이미지 업로드 확인
3-4. 특정 게시글 조회 (http://localhost:8080/v3/posts/1)
3-5. 예외 처리 및 리소스 로드 확인
- 예제(1), (2)와 동일한 예외 처리 결과 확인 가능함
- 프로젝트가 다시 빌드되지 않더라도 정적 리소스가 외부 경로에서 로드 되는 것을 확인 가능함
'Spring Boot' 카테고리의 다른 글
[Spring Boot] 스프링 트랜잭션 (@Transactional) (0) | 2024.09.05 |
---|---|
[Spring Boot] 스프링 ChatGPT API 활용 예제 ( + postman으로 확인 ) (0) | 2024.09.04 |
[Spring Boot] 스프링 이미지 업로드 예제 (2) - DB에 이미지 직접 저장 (0) | 2024.09.02 |
[Spring Boot] 스프링 이미지 업로드 예제 (1) - 프로젝트 내부 디렉토리에 업로드 (0) | 2024.09.02 |
[Spring Boot] 스프링 이미지 업로드 예제 (0) - 예시 상황, 공통 코드 (0) | 2024.09.02 |