2024.09.02 - [Spring Boot] - [Spring Boot] 스프링 이미지 업로드 예제 (0) - 예시 상황, 공통 코드
1. 설명
- (로컬 환경) 이전 방법( https://blogan99.tistory.com/138?category=1148702 )으로 이미지 저장 시 문제점
- resources > static > image_upload에 이미지가 업로드 되더라도 인텔리제이에서 프로젝트가 다시 빌드되지 않으면 정적 리소스를 로드할 수 없기 때문에 이미지를 불러오지 못함
- devtools로 자동 빌드하더라도 시간이 걸림
- 프로젝트 배포 시에는 해결 불가능함
- DB에 직접 이미지 정보를 저장하는 방식으로 해결
- 이미지 이름에 UUID 추가, 예외 조건 등은 이전과 동일하게 작성
이전 공통 코드, 예제(1) 코드를 기반으로 작성하되 V2를 붙인 클래스를 생성함
2. 추가 코드
2-1. application.yml
- 이전과 동일
2-2. domain
- PostImagesV2
- 이미지 이름 필드와 이미지 정보 필드를 생성
@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "post_images_v2")
public class PostImagesV2 {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_images_id")
private Long id;
// 다대일 양방향 연관관계
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private PostV2 post;
// 저장한 이미지 이름
private String imageName;
// 이미지 정보
@Lob
@Column(columnDefinition = "MEDIUMBLOB")
private byte[] imageData;
// Base64 문자열 추가
public String getBase64Image() {
return Base64.getEncoder().encodeToString(this.imageData);
}
// 연관관계 편의 메서드
public void setPost(PostV2 post) {
this.post = post;
post.getPostImagesList().add(this);
}
public PostImagesV2(String imageName, byte[] imageData) {
this.imageName = imageName;
this.imageData = imageData;
}
}
더보기
예제(1)과 다른 점
- 예제(1)에서는 이미지 경로를 저장 ↔ 이미지 이름과 이미지 정보를 직접 저장해서 DB에서 꺼내오는 방식
- @Lob : 해당 필드가 Large Object임을 나타냄 (이미지, 비디오, 텍스트 같은 큰 데이터 저장 시 사용)
- columnDefinition : DB상 열 속성을 정의
- "BLOB" : 최대 65KB의 이진 데이터 저장 가능
- "MEDIUMBLOB" : 최대 16MB의 이진 데이터 저장 가능
- "LONGBLOB" : 최대 4GB의 이진 데이터 저장 가능
- columnDefinition : DB상 열 속성을 정의
// 저장한 이미지 이름
private String imageName;
// 이미지 정보
@Lob
@Column(columnDefinition = "MEDIUMBLOB")
private byte[] imageData;
// Base64 문자열 추가
public String getBase64Image() {
return Base64.getEncoder().encodeToString(this.imageData);
}
- PostV2
- 예제(1)과 동일
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "post_v2")
public class PostV2 {
@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<PostImagesV2> postImagesList = new ArrayList<>();
public PostV2(String title, String content) {
this.title = title;
this.content = content;
}
}
- PostV2Dto
- 예제(1)과 동일
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PostV2Dto {
private String title;
private String content;
// 다수의 이미지 (리스트) 입력받기 위해 추가
private List<MultipartFile> imageList;
}
2-3. repository
- PostImagesV2Repository
- 예제(1)과 동일
@Repository
public interface PostImagesV2Repository extends JpaRepository<PostImagesV2, Long> {
}
- PostV2Repository
- 예제(1)과 동일
@Repository
public interface PostV2Repository extends JpaRepository<PostV2, Long> {
}
2-4. service
- PostImagesV2Service
- 예외 처리 코드, 이미지 이름에 UUID 추가 등은 예제(1)과 동일
- PostImages 생성 시 image.getBytes() 통해 이미지의 정보를 추가하는 코드 추가됨
@Service
@RequiredArgsConstructor
public class PostImagesV2Service {
private final PostImagesV2Repository postImagesV2Repository;
@Transactional
public void saveImage(List<MultipartFile> imageList, PostV2 post) {
// 업로드한 파일이 없다면 리턴
if(imageList == null) return;
// 사진 업로드 개수 <= 5 인지 체크
if(imageList.size() > 5) throw new IllegalArgumentException("최대 사진 업로드 개수는 5개 입니다!");
// 업로드 파일의 타입이 사진인지 체크
for (MultipartFile image : imageList) {
if(!isImageFile(image)) throw new IllegalArgumentException("사진 이외의 파일은 업로드 불가능합니다!");
}
try {
// 각 사진에 대해 db에 저장
for (MultipartFile image : imageList) {
// 파일이 비어있는 경우 패스
if(Objects.requireNonNull(image.getOriginalFilename()).isEmpty()) continue;
// 이미지 이름 앞에 랜덤값 추가 (중복 안되게)
String imageName = addUUID(image.getOriginalFilename());
// PostImages 엔티티 생성, Post와 연관관계 설정
PostImagesV2 postImages = new PostImagesV2(imageName, image.getBytes());
postImages.setPost(post);
// 생성한 PostImages 엔티티 저장
postImagesV2Repository.save(postImages);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 파일이 이미지 형식인지 검사하는 메서드
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;
}
}
- PostV2Service
- 예제(1)과 동일
@Service
@RequiredArgsConstructor
public class PostV2Service {
private final PostV2Repository postV2Repository;
private final PostImagesV2Service postImagesV2Service;
// 모든 게시글 리턴
public List<PostV2> findAll() {
return postV2Repository.findAll();
}
// 특정 게시글 리턴
public PostV2 findById(Long postId) {
return postV2Repository.findById(postId).orElse(null);
}
// 게시글 저장
@Transactional
public void save(PostV2Dto postDto) {
PostV2 post = new PostV2(postDto.getTitle(), postDto.getContent());
// 이미지 저장 로직 추가
postImagesV2Service.saveImage(postDto.getImageList(), post);
postV2Repository.save(post);
}
}
2-5. controller
- PostV2ApiController
- 예제(1)과 동일
@RestController
@RequiredArgsConstructor
@RequestMapping("/v2/api/posts")
public class PostV2ApiController {
private final PostV2Service postV2Service;
// 모든 게시글 조회
@GetMapping
public ResponseEntity<List<PostV2>> findAllPosts() {
return ResponseEntity.ok(postV2Service.findAll());
}
// 특정 게시글 단건 조회
@GetMapping("/{postId}")
public ResponseEntity<PostV2> findPost(@PathVariable("postId") Long postId) {
PostV2 post = postV2Service.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 PostV2Dto postDto) {
postV2Service.save(postDto);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
- PostV2Controller
- 예제(1)과 동일
@Controller
@RequiredArgsConstructor
@RequestMapping("/v2/posts")
public class PostV2Controller {
private final PostV2Service postV2Service;
@GetMapping("/{postId}")
public String home(@PathVariable("postId") Long postId, Model model) {
model.addAttribute("post", postV2Service.findById(postId));
return "v2_home";
}
}
2-6. html
- 예제(1)의 PostImages의 이미지 경로를 통해 이미지 출력 방법과는 다름
- PostImages 엔티티에 추가한 getBase64Image() 통해 이미지 출력
<!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="'data:image/jpeg;base64,' + ${post_image.base64Image}" style="width: 200px"/>
</div>
</html>
3. 실행 결과
3-1. 이미지 첨부 게시글 작성 (POST localhost:8080/v2/api/posts)
3-2. MySQL 확인
- 예제(1)과는 달리 BLOB 타입의 이미지 정보가 저장됨을 확인 가능
3-3. 특정 게시글 조회 (http://localhost:8080/v2/posts/1)
3-4. 예외 처리 확인
- 예제(1)과 동일한 처리 결과 확인 가능함
'Spring Boot' 카테고리의 다른 글
[Spring Boot] 스프링 ChatGPT API 활용 예제 ( + postman으로 확인 ) (0) | 2024.09.04 |
---|---|
[Spring Boot] 스프링 이미지 업로드 예제 (3) - 외부 경로에 업로드 (0) | 2024.09.03 |
[Spring Boot] 스프링 이미지 업로드 예제 (1) - 프로젝트 내부 디렉토리에 업로드 (0) | 2024.09.02 |
[Spring Boot] 스프링 이미지 업로드 예제 (0) - 예시 상황, 공통 코드 (0) | 2024.09.02 |
[Spring Boot] MapStruct 라이브러리로 엔티티 ↔ DTO 변환 자동으로 매핑하기 (예제 코드) (0) | 2024.08.14 |