이번 실습에서는 Order Controller에 집중되어 있던 비즈니스 로직을 Service 계층으로 분리하였다.
기존에는 Controller에서 상품 조회, 주문 생성, 주문 저장, 예외 처리를 모두 처리하고 있었고, 이를 Service로 이동시키고 Controller는 요청과 응답만 담당하도록 수정하였다.
계층을 나눈 기준은 다음과 같다.
이렇게 분리함으로써 각 계층의 역할이 명확해지고, 코드의 가독성과 유지보수성이 향상된다.
etc)자바에서 '패키지는 소문자, 클래스는 대문자'가 국룰
Windows에서는 대소문자를 구분하지 않지만, Linux/Mac에서는 대소문자를 구분하기 때문에 운영체제 문제가 발생할 수 있음(배포하면 오류나는 경우) => 애초에 소문자만 쓰자~
단순히 기능을 구현하는 것보다, 게층을 나누는 기준을 이해하는 것이 중요하다고 느꼈다. Controller에 로직이 많아질수록 코드가 복잡해지고 재사용이 어려워지기 때문에, Service로 분리하는 구조가 유지보수에 훨씬 유리하다는 것을 경험할 수 있었다.
리뷰 및 평점 파트 및 Area 관련 도메인을 맡았다.
오늘 리뷰 삭제를 구현하고 테스트하면서 Soft delete 관련 트러블을 마주했다.
리뷰 삭제는 물리 삭제가 아닌 Soft delete 방식으로 처리했다. 즉, 리뷰를 삭제해도 DB row가 실제로 사라지는 것이 아니라 is_delete = true 로 상태만 변경된다.
그런데 리뷰는 주문과 1:1 관계로 설계되어 있었고, order_id에 unique 제약이 걸려있었다.
@Table(
name = "p_reviews",
uniqueConstraints = {
@UniqueConstraint(name = "uk_review_order_id", columnNames = "order_id")
}
)
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false, unique = true)
private OrderEntity order;
이 상태에서 리뷰를 삭제한 뒤 같은 주문으로 다시 리뷰를 작성하려고 하자, 애플리케이션 로직상으로는 '삭제된 리뷰이므로 없는 리뷰처럼 처리'하려고 했지만 DB에는 기존 row가 그대로 남아있기 때문에 order_id unique 제약에 걸려 다시 insert가 불가능한 문제가 발생했다.
uk_review_order_id 고유 제약 조건 위반
(order_id)=... 키가 이미 있습니다
로 삭제 후 재작성 시 에러 발생
이 문제를 해결하기 위해 두 가지 방향을 고민했다.
Soft Delete된 리뷰 row를 그대로 유지
같은 주문에는 평생 하나의 리뷰만 허용
구현은 단순하지만 사용자 경험이 좋지 않음
삭제된 리뷰가 있어도 다시 리뷰 작성 가능
다만 Soft Delete와 unique 제약을 같이 쓰는 구조에서는 추가 설계가 필요함
멘토님과 상의하고, 배민이나 쿠팡이츠 등 정책을 살펴보았을 때, 삭제 후 재작성을 허용하는 것이 더 자연스럽다고 판단하였다.
서비스 로직에서 is deleted 검사 -> 리뷰 존재안하되 false면 생성, 리뷰 존재하되 false면 수정, true면 생성불가로 구현하였다.
이미 존재하는 review row를 수정하는 방식으로 재작성을 허용하였다.
ReviewEntity에 다음 로직 추가
public void restoreReview(Integer rating, String content, UUID updatedBy) {
this.rating = rating;
this.content = content;
super.restore();
this.markUpdatedBy(updatedBy);
}
@Transactional
public ResCreateReviewDtoV1 createReview(UUID orderId, UUID loginUserId, ReqCreateReviewDtoV1 request) {
UserEntity loginUser = userRepository.findById(loginUserId)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
OrderEntity order = orderRepository.findByOrderIdAndIsDeletedFalse(orderId)
.orElseThrow(() -> new IllegalArgumentException("주문을 찾을 수 없습니다."));
validateCreatePermission(order, loginUserId);
// 활성 리뷰가 있으면 새 리뷰 생성 불가
if (reviewRepository.existsByOrder_OrderIdAndIsDeletedFalse(orderId)) {
throw new IllegalStateException("이미 해당 주문에 대한 리뷰가 존재합니다.");
}
// soft delete된 리뷰가 있으면 복구해서 재사용
ReviewEntity deletedReview = reviewRepository.findByOrder_OrderIdAndIsDeletedTrue(orderId)
.orElse(null);
if (deletedReview != null) {
deletedReview.restoreReview(request.getRating(), request.getContent(), loginUserId);
return ResCreateReviewDtoV1.builder()
.reviewId(deletedReview.getReviewId())
.orderId(deletedReview.getOrder().getOrderId())
.storeId(deletedReview.getStore().getStoreId())
.rating(deletedReview.getRating())
.content(deletedReview.getContent())
.createdAt(deletedReview.getCreatedAt())
.build();
}
// 리뷰가 전혀 없을 때만 새로 생성
StoreEntity store = storeRepository.findById(order.getStoreId())
.orElseThrow(() -> new IllegalArgumentException("가게를 찾을 수 없습니다."));
ReviewEntity review = ReviewEntity.builder()
.order(order)
.user(loginUser)
.store(store)
.rating(request.getRating())
.content(request.getContent())
.build();
review.markCreatedBy(loginUserId);
ReviewEntity saved = reviewRepository.save(review);
return ResCreateReviewDtoV1.builder()
.reviewId(saved.getReviewId())
.orderId(saved.getOrder().getOrderId())
.storeId(saved.getStore().getStoreId())
.rating(saved.getRating())
.content(saved.getContent())
.createdAt(saved.getCreatedAt())
.build();
}
이 방식으로 수정한 뒤에는 다음 흐름이 가능해졌다.
리뷰 최초 작성 → 정상 생성
리뷰 삭제 → is_deleted = true
같은 주문으로 다시 리뷰 작성 → 기존 리뷰 row를 복구하여 재작성 처리
즉, DB의 unique 제약은 유지하면서도 사용자 입장에서는 리뷰 재작성 기능이 동작하게 만들 수 있었다.
애플리케이션 로직에서만 없는 것처럼 보일 뿐, DB 관점에서는 row가 그대로 존재한다.
따라서 unique 제약이나 FK 제약과 반드시 함께 고려해야 한다.
“삭제 후 재작성 허용”이라는 정책을 정했다면,
DB도 그 정책에 맞게 설계되어야 하고,
서비스 로직도 그에 맞게 분기되어야 한다.
서비스에서 is_deleted = false만 검사해도,
DB unique 제약이 그대로라면 insert 단계에서 여전히 실패할 수 있다.
따라서 이번처럼 복구 방식 또는 조건부 unique index 같은 구조적 해결이 필요하다.