안녕하딤니카? 재영입니다
이제 슬슬 개인 프로젝트가 마무리되고있는데요 (5번째글인데? 벌써?
이 글 내지 다음 글까지 프로젝트 개발하다가 만난 이슈 정리를 하고 마감하지 않을까... 싶습니다
오늘은 JPA 의 CascadeType 설정을 잘못해서 리뷰 기능 중 리뷰 삭제가 정상적으로 진행되지 않던 문제를 해결한 건을 작성해보겠습니다
그럼~~~ 레 쭈 고

1. 문제 상황
저는 지금 Spring Boot (버전 3.2.3) 와 JPA 를 사용해서 쇼핑몰 프로젝트를 개발하고 있습니다.
쇼핑몰 프로젝트 중 사용자가 구매한 상품의 리뷰 관련 기능을 개발하고 있었습니다...
(리뷰 작성 / 리뷰 수정 / 리뷰 삭제 / 리뷰 조회 / 리뷰 관리 / 상품 상세페이지 하단에 ajax 로 리뷰 리스트 및 리뷰 상세정보 조회)
전반적인 리뷰 기능을 개발한 다음, 로컬 환경에서 프로젝트를 실행해서 기능 테스트를 진행하던 중
리뷰 삭제 시 첨부한 리뷰 이미지는 삭제되는데 정작 리뷰가 삭제 안 되는 이슈가 발생했습니다.
(리뷰 이미지 로직과 리뷰 로직을 분리해서 개발했고, 리뷰 이미지 로직을 리뷰 로직에 끼워넣었는데, 리뷰 이미지 로직은 정상 동작 하는데, 리뷰 로직 중 삭제 로직만 문제가 있었음)
2. 원인 분석
암튼 처음에는 리뷰의 비즈니스 로직의 문제인줄 알고 그쪽에 디버그 포인트를 찍어서 테스트를 진행했는데,
리뷰 이미지 삭제 로직은 정상 동작하고, 리뷰 이미지의 delete 쿼리도 정상적으로 나가는 반면,
이상하게 리뷰 delete 쿼리만 안 나가는 겁니다
그리고 리뷰가 삭제되었다고 catch 에도 안 걸리는 문제가 발생하는걸 확인했습니다.
(비즈니스 로직이나 컨트롤러 로직에 문제였으면 catch 에 걸어둔 예외처리에 걸렸어야하는데, 예외처리에도 안 걸림)
그래서 쿼리 문제면 리포지토리 쪽 문제인가 싶어서
리포지토리에 적어둔 쿼리를 데이터베이스에 그대로 실행시켜봤는데 이상하게 데이터베이스에는 별 다른 문제가 없었습니다.
잉? 싶어서 OrderItem 엔티티와 Review 엔티티를 살펴봤습니다. (OrderItem 와 Review 은 일대일(OneToOne) 관계로 매핑)
@Entity
@Table(name = "order_item")
@Getter
@Setter
public class OrderItem extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@OneToOne(mappedBy = "orderItem", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Review review;
private int orderPrice; // 주문가격
private int count; // 수량
@Enumerated(EnumType.STRING)
private ReviewStatus reviewStatus = ReviewStatus.NOT_REVIEWED;
}
보시면, OrderItem 의 review 필드에 CascadeType 옵션 설정을 해둔 것을 확인했고,
이 옵션때문에 JPA 가 연관된 엔티티를 삭제하는 방식에 문제가 발생한 것을 확인했습니다.
처음에 엔티티 설계를 할 때, 주문 상품 삭제 시 Review 도 함께 삭제하면 되겠구나~ 싶었는데
보통 주문 상품은 주문 엔티티와 관련있지, Review 는 OrderItem 과 연관관계만 맺으면 되고 CascadeType 사용은 안 해야 되는데... 좀 안일하게 생각했던 것이 문제였습니다... 🥲
3. 해결 방법
문제를 해결하기 위해 OrderItem 엔티티에서 cascade 옵션을 제거했습니다.
이로 인해 연관된 Review 엔티티의 삭제가 OrderItem 엔티티의 생명주기에 의존하지 않게 되었습니다.
@Entity
@Table(name = "order_item")
@Getter
@Setter
public class OrderItem extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@OneToOne(mappedBy = "orderItem", fetch = FetchType.LAZY) // cascade 옵션 제거
private Review review;
private int orderPrice; // 주문가격
private int count; // 수량
@Enumerated(EnumType.STRING)
private ReviewStatus reviewStatus = ReviewStatus.NOT_REVIEWED;
}
그리고, ReviewService 에서 리뷰 삭제 로직을 수정하여 리뷰 엔티티를 명시적으로 삭제하도록 했습니다.
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class ReviewService {
private final ReviewRepository reviewRepository;
private final ReviewImgService reviewImgService;
// 리뷰 삭제
public void deleteReview(Long reviewId) {
Review review = reviewRepository.findById(reviewId)
.orElseThrow(() -> new EntityNotFoundException("Review not found with ID: " + reviewId));
reviewImgService.deleteImagesByReviewId(reviewId); // ReviewImgService를 사용하여 이미지 삭제
// 리뷰 작성 상태 업데이트
OrderItem orderItem = review.getOrderItem();
if (orderItem != null) {
orderItem.setReviewStatus(ReviewStatus.NOT_REVIEWED);
orderItem.setReview(null); // OrderItem과의 연관관계 해제
orderItemRepository.save(orderItem);
}
reviewRepository.delete(review);
// 리뷰가 실제로 삭제되었는지 확인
if (reviewRepository.findById(reviewId).isPresent()) {
throw new IllegalStateException("리뷰 삭제에 실패했습니다.");
} else {
log.info("Review successfully deleted");
}
}
}
이와 같이 OrderItem 과 Review 엔티티 간의 cascade 설정을 적절히 조정하고, 리뷰 이미지를 삭제한 후 리뷰 엔티티 자체를 명시적으로 삭제함으로써 문제를 해결할 수 있었습니다... 휴~
4. JPA CascadeType 옵션
4-1. CascadeType 의 정의와 이점
CascadeType 은 JPA 에서 엔티티 간의 관계를 맺을 때, 관련된 엔티티에 대해 특정 작업 (저장, 삭제 등) 이 자동으로 전파되도록 설정할 수 있는 옵션입니다.
이를 통해 부모 엔티티의 상태 변화가 자식 엔티티로 자동으로 전파되며, 코드의 가독성과 유지보수성을 향상시킬 수 있습니다.
CascadeType 의 주요 이점을 정리하자면, 아래와 같습니다.
1) 자동 전파
부모 엔티티에서 수행한 작업이 자동으로 자식 엔티티로 전파되므로, 일일이 자식 엔티티에 대해 동일한 작업을 수행할 필요가 없습니다.
2) 코드 간소화
코드에서 반복적인 작업을 줄일 수 있어서 간결하고 명확한 코드를 작성할 수 있습니다.
3) 트랜잭션 관리
연관된 엔티티들이 하나의 트랜잭션 내에서 함께 처리되므로 데이터의 일관성을 유지할 수 있습니다.
4-2. CascadeType 를 사용하면 좋은 경우
제 프로젝트에서 CascadeType 옵션을 사용하는 엔티티는 Order 와 OrderItem 엔티티입니다.
이 엔티티는 Order 항목이 삭제되거나 변경되면, OrderItem 엔티티에도 변경이 필요하기 때문에, Order 엔티티의 orderItems 필드에 CascadeType.All 옵션을 사용해서 엔티티 간의 변경을 한꺼번에 처리합니다.
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> orderItems = new ArrayList<>();
}
@Entity
@Table(name = "order_item")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
}
Order 엔티티가 저장(PERSIST), 병합(MERGE), 삭제(REMOVE) 될 때, 연관된 OrderItem 도 함께 저장, 병합, 삭제됩니다.
그리고 추가로 orphanRemoval = true 옵션을 사용했는데, 이 옵션은 부모 엔티티에서 자식 엔티티가 더 이상 참조되지 않으면 자동으로 삭제되도록 합니다.
저는 Order 와 OrderItem 연관관계에서만 CascadeType 옵션을 사용해줬는데, 게시글과 댓글 관계에서도 CascadeType 옵션을 사용할 수 있을 것 같습니다. (게시글이 저장될 때, 연관된 댓글도 함께 저장, 게시글이 삭제될 때, 연된된 댓글도 함께 삭제)
그래서 CascadeType 언제 사용하면 좋은지 옵션별로 아래에 정리해보도록 하겠습니다.
1) 단순한 부모 - 자식 관계
부모 엔티티가 존재할 때만 자식 엔티티가 의미 있는 경우 (CascadeType.REMOVE 사용)
Order 와 OrderItem 관계에서 Order 가 삭제되면 해당 OrderItem 도 함께 삭제
2) 연관된 엔티티를 함께 저장할 때
부모 엔티티와 함께 자식 엔티티도 저장되어야 하는 경우 (CascadeType.PERSIST 사용)
새로운 Order 을 저장할 때 여러 OrderItem 을 함께 저장
3) 엔티티 병합 시 연관된 엔티티도 함께 병합할 때
부모 엔티티가 수정될 때 자식 엔티티도 함께 수정되어야 하는 경우 (CascadeType.MERGE 사용)
Order 와 OrderItem 관계에서 Order 가 변경될 때 OrderItem 도 함께 변경
4) 엔티티를 새로 고침할 때 자식 엔티티도 새로 고침이 필요할 때
부모 엔티티가 새로 고침될 때 자식 엔티티도 함께 새로 고침이 필요할 경우 (CascadeType.REFRESH 사용)
5) 엔티티를 분리할 때 자식 엔티티도 분리가 필요할 때
부모 엔티티가 영속성 컨텍스트에서 분리될 때, 자식 엔티티도 함께 분리해야 하는 경우 (CascadeType.DETACH 사용)
6) CascadeType 의 모든 옵션을 포함하여 자동으로 전파가 필요할 때
위에 정리한 5가지 CascadeType 옵션을 포함하여, 부모 엔티티의 작업이 자식 엔티티의 작업에 자동으로 전파되어야 하는 경우 (CascadeType.ALL 사용)
4-3. CascadeType 사용 시 주의사항
부모 엔티티와 자식 엔티티의 작업에 영향을 주는 CascadeType 옵션을 사용할 때에는, 예기치 못한 작업이 진행될 수 있기 때문에 CascadeType 옵션을 신중하게 사용해야 합니다.
CascadeType 사용 시 주의사항을 아래에 정리해보겠습니다.
1) 데이터 무결성
잘못된 CascadeType 설정으로 인해, 예상치 못한 데이터 삭제나 업데이트가 발생할 수 있습니다.
특히 CascadeType.REMOVE 는 매우 신중하게 사용해야 합니다.
2) 성능
과도한 CascadeType 사용은 성능 저하를 일으킬 수 있습니다. 필요하지 않은 경우에는 설정을 자제해야 합니다.
3) 복잡한 연관관계
복잡한 엔티티 관계에서는 CascadeType 이 예상치 못한 동작을 일으킬 수 있으므로, 관계의 깊이나 복잡성을 고려하여 신중하게 설정해야 합니다.
4) 비즈니스 로직
CascadeType 설정의 자동 전파로 인해 비즈니스 로직이 예상치 못하게 동작할 수 있습니다. 항상 전파 설정을 이해하고 비즈니스 로직에 맞게 사용해야 합니다.
5) 명시적 관리
모든 작업을 자동으로 처리하는 것보다는, 필요한 경우, 명시적으로 처리하는 것이 더 안전할 수 있습니다.
4-4. CascadeType 을 사용하지 말아야 하는 경우
위에서 CascadeType 을 사용하면 좋은 경우, 사용 예시, 사용 시 주의사항을 정리했는데요,
이제 사용하면 안 되는 경우를 정리해보겠습니다. 주의사항과 어느정도 일맥상통하지만, 아래에 CascadeType 를 사용하면 안 되는 경우를 다시 정리해보겠습니다!
1) 독립적인 엔티티
부모 엔티티와 자식 엔티티가 독립적으로 관리되어야 할 때는 CascadeType 을 사용하지 않는 것이 좋습니다.
2) 복잡한 관계
너무 복잡한 관계에서 CascadeType 을 사용하면 예기치 않은 동작이 발생할 수 있습니다.
3) 데이터 일관성
특정 작업에서만 자식 엔티티가 전파되어야 하는 경우에는 CascadeType 을 사용하지 않고 명시적으로 작업을 수행하는 것이 좋습니다.
이와 같은 기준을 통해 CascadeType 을 적절히 사용하면 JPA 엔티티 간의 관계를 효과적으로 관리할 수 있습니다.
5. 정리
JPA 를 사용하면서 엔티티 간의 연관 관계를 설정할 때, CascadeType 옵션을 신중하게 사용해야 합니다.
잘못된 CascadeType 설정은 예기치 않은 동작을 유발할 수 있으며, 연관된 엔티티의 생명주기를 명확히 이해하고, 필요한 경우 명시적으로 삭제하는 것이 중요합니다.
이런 문제를 해결하면서 JPA 의 엔티티 관리에 대해 더 깊이 이해하게 되었습니다. 앞으로도 유사한 문제가 발생할 때 신속하게 대응할 수 있을... 것입니다... (제발
이번에도 삽질 한 번 하고 득도하는 개린이가 되었습니다 ㅜ 휴!
🍀
좋아하는 것을 계속 좋아하세요!
반드시 행복해집니다
[Github] https://github.com/chujaeyeong
[E-mail] chujy1224@gmail.com
'Project' 카테고리의 다른 글
| 커스텀 어노테이션과 정규식을 사용해서 비밀번호를 검증하기 - 쇼핑몰 프로젝트 07 (1) | 2024.06.12 |
|---|---|
| Swagger 도구를 사용한 API 문서 작성 방법과 사용 시 이점 - 쇼핑몰 프로젝트 06 (1) | 2024.06.09 |
| Principal 객체를 활용하여 인증된 사용자의 정보를 반환하기 - 쇼핑몰 프로젝트 04 (3) | 2024.05.27 |
| 여러 유형의 회원을 하나의 엔티티로 관리하기 - 쇼핑몰 개인 프로젝트 03 (0) | 2024.05.27 |
| Spring Boot 3.x + Spring Security 6 + OAuth2 소셜 로그인의 흐름 - 쇼핑몰 개인 프로젝트 02 (1) | 2024.05.16 |