8w8u8
이미지 업로드 기능 작업 중 생긴 여러 문제들.. 본문
Image is abstract; cannot be instantiated
추상클래스는 인스턴스화가 불가하다!
@Getter
// @Builder 이게 문제였음!
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@DiscriminatorColumn
@Inheritance(strategy = InheritanceType.JOINED)
@Entity
public abstract class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
protected String originalName;
protected String storedName;
protected String imgPath;
private final static String[] extensionArr={"jpg","jpeg","bmp","gif","png"};
public void setImgPath(String imgPath) {
this.imgPath = imgPath;
}
// 이미지 파일의 확장자를 추출하는 메서드
public String extractExtension(String originalName) {
int index = originalName.lastIndexOf('.');
return originalName.substring(index + 1);
}
// 저장할 파일 이름을 생성하는 메서드
public String generateStoreName(String originalName) {
String extension = extractExtension(originalName);
if (!checkValidation(extension))
throw new RuntimeException(extension + " 은 지원하지 않는 확장자입니다.");
return UUID.randomUUID() + "." + extension;
}
public boolean checkValidation(String extension) {
return Arrays.stream(extensionArr).anyMatch(value -> value.equals(extension));
}
reviewImg의 review_id가 null로 저장되던 문제
save()의 잘못된 위치가 원인
Review 객체가 데이터베이스에 먼저 저장되어야 review의 id가 할당되고, 이후에 ReviewImg 객체들이 해당 review의 id를 참조할 수 있다.
public Review registerReview(Long productId, ReviewRequestDto dto, List<MultipartFile> files) throws IOException {
Product product = productService.findProductById(productId);
Member member = memberService.findMemberById(dto.getMemberId());
Review review = dto.toEntity(product, member);
reviewRepository.save(review); // 해결
for (MultipartFile multipartFile : files) {
if (multipartFile != null && !multipartFile.isEmpty()) {
String originalName = multipartFile.getOriginalFilename();
String storedName = UUID.randomUUID() + "-" + originalName;
String imgPath = imgService.saveImage(multipartFile, storedName);
ReviewImg reviewImg = ReviewImg.builder()
.originalName(originalName)
.storedName(storedName)
.imgPath(imgPath)
.review(review)
.build();
review.addReviewImage(reviewImg);
reviewImgRepository.save(reviewImg);
}
}
// reviewRepository.save(review); 원인
return review;
}
해결
CascadeType.REMOVE vs @OnDelete(action = OnDeleteAction.CASCADE) : 무엇을 사용해야할까?
둘 다 부모 엔티티가 삭제될 때 연관된 자식 엔티티도 삭제하는 기능을 수행하는데,
CascadeType.REMOVE
는 JPA 수준에서@OnDelete(action = OnDeleteAction.CASCADE)
는 데이터베이스 수준에서 처리된다.
CascadeType.REMOVE
CascadeType.REMOVE
는 JPA 수준에서 부모 엔티티가 삭제될 때 연관된 자식 엔티티도 함께 삭제되도록 설정한다.- 장점
- 데이터베이스 독립성 : 다양한 데이터베이스에서 동일하게 동작하고, 데이터베이스 변경 시 문제 발생이 적다.
- JPA에서 프로그램적으로 의존성을 관리하므로 운영자가 실수로 의존성을 가진 레코드를 삭제하는 일을 줄일 수 있다.
- 단점
- 자식 엔티티 개수만큼 DELETE 쿼리가 생성되므로 대규모 데이터 삭제 시 성능 부담이 크다.
- @OneToMany 관계에 어노테이션을 추가해야 하므로 양방향 의존 관계를 설정해야 할 수 있다.
- 애플리케이션 로직에서 엔티티 상태를 세밀하게 관리해야 하는 경우 유리하다.
- e.g. 블로그 포스트와 댓글처럼 애플리케이션 내부에서 여러 상태 변화가 자주 발생하는 경우.
@OnDelete(action = OnDeleteAction.CASCADE)
@OnDelete(action = OnDeleteAction.CASCADE)
는 데이터베이스 수준에서 부모 엔티티가 삭제될 때 연관된 자식 엔티티도 함께 삭제되도록 설정한다.- 장점
- 단일 DELETE 쿼리로 연쇄 삭제를 처리하여 성능이 뛰어나다.
- 단점
- 특정 데이터베이스에 의존적이며, 데이터베이스 변경 시 설정을 다시 해야할 수도 있다.
- 데이터베이스 성능이 중요하고 대규모 데이터 삭제 작업이 필요한 경우 유리하다.
- e.g. 주문과 주문 항목처럼 데이터베이스 수준에서 무결성을 유지하면서 대량의 데이터를 효율적으로 삭제해야 하는 경우.
그래서 나는...
리뷰와 리뷰이미지 사이의 관계에서, 나는 @OnDelete(action = OnDeleteAction.CASCADE)
를 사용했다.
@Entity
public class ReviewImg extends Image {
// FK
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id", updatable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Review review;
'back-end' 카테고리의 다른 글
Docker CI/CD (0) | 2025.07.05 |
---|---|
CI/CD 파이프라인 구축 (GitHub Actions + AWS Codedeploy + EC2 + RDS + S3) (0) | 2025.07.05 |
NullPointerException:Cannot invoke 000.findAll()" because "this.000Repository" is null 에러 해결 (0) | 2025.07.05 |
MYSQL 3819 에러 해결 : ERROR 3819 (HY000): Check constraint 'item_chk_1' is violated (0) | 2025.07.05 |
판매제품을 direct하게 연결하면 안될까? (0) | 2025.07.05 |