리팩토링 TODO - 결합도 낮추기
주말동안 고민에 대해 생각을 정리했는데, 일단 마감기한이 정해져있으니 굴러간다면 추후 리팩토링 사항으로 남겨놓기로 했다.
어차피 security에서 권한 확인 작업 다 하니까 굳이 Review, OwnerReview에서 권한을 확인하는 과정은 필요없다.
그래도 주문내역에 있는 사장님 userId가 맞는지 확인은 필요하다고 함.
카테고리별 가게 목록 조회
이거 도전기능에 있는 건데 그냥 후다닥 queryDSL로 만들었다.
조건은 카테고리는 무조건 선택하되, 가게 이름은 일부 포함만 되면 다 조회하는 걸로 queryDSL을 짰다.
@Repository
public class StoreRepositoryImpl extends QuerydslRepositorySupport implements StoreRepositoryCustom {
private final JPAQueryFactory queryFactory;
public StoreRepositoryImpl(JPAQueryFactory queryFactory) {
super(Store.class);
this.queryFactory = queryFactory;
}
@Override
public Page<Store> findStoresByCategory(UUID categoryId, String storeName, String sortBy, boolean isAsc, Pageable pageable) {
QStore store = QStore.store;
QCategory category = QCategory.category;
BooleanBuilder builder = new BooleanBuilder();
// 카테고리별 가게 목록 조회
if (categoryId != null) {
builder.and(category.id.eq(categoryId));
}
// storeName을 포함하는 카테고리별 가게 목록 조회
if (storeName != null && !storeName.isEmpty()) {
builder.and(store.name.containsIgnoreCase(storeName));
}
// 정렬
OrderSpecifier<?> orderSpecifier = getOrderSpecifier(sortBy, isAsc);
// 쿼리 생성
JPAQuery<Store> query = queryFactory.selectFrom(store)
.join(store.category, category)
.where(builder)
.orderBy(orderSpecifier);
// 페이징 적용 후 조회
List<Store> results = getQuerydsl().applyPagination(pageable, query).fetch();
// 전체 데이터 개수 조회 (NullPointerException 방지)
long total = Optional.ofNullable(
queryFactory.select(store.count())
.from(store)
.join(store.category, category)
.where(builder)
.fetchOne()
).orElse(0L);
return new PageImpl<>(results, pageable, total);
}
private OrderSpecifier<?> getOrderSpecifier(String sortBy, boolean isAsc) {
QStore store = QStore.store;
// 기본 이름 사전순 정렬
if (sortBy == null || sortBy.isEmpty()) {
sortBy = "name";
}
if ("updatedAt".equalsIgnoreCase(sortBy)) {
return isAsc ? store.updatedAt.asc() : store.updatedAt.desc();
} else if ("createdAt".equalsIgnoreCase(sortBy)) {
return isAsc ? store.createdAt.asc() : store.createdAt.desc();
} else {
return isAsc ? store.name.asc() : store.name.desc();
}
}
근데 Store API는 내 파트가 아니라 충돌이 엄청났다...ㅜㅜ
+) 여담인데,
일요일에 팀원들끼리 모여서 미루고 미루던 테스트를 해봤는데 이상하게 AuthorizationFilter에서 잘 들어가던 userDetails.getUser()가
request한 컨트롤러만 가면 갑자기 null로 뜨는 문제가 있었다.
팀장님과 열심히 디버깅을 걸며 대체 어디서 null 처리가 되는지 거의 2시간 이상 찾았는데...
알고보니 Merge 과정에서 유저 담당이 아닌 팀원의 이름으로 된 UserDetailsImpl이 하나 더 있었던 것이다.
UserDetailsImpl이 2개였던 것....
--> 결론 : Merge가 이렇게나 중요하다~~
+) 그래서 내가 만든 리뷰 테스트를 했는데 안됨. 분명히 User 넣기 전까지는 됐는데!!!!
난 너무 억울했는데 블로그에도 @Valid 넣은 포스트맨 테스트 캡쳐가 없어서 더 억울해졌다.
근데 시간도 너무 늦었고 그냥 내일 튜터님한테 여쭤보자고 하고 자려고 했는데 넘 억울해서 계속 다시 봤는데...
Order 테이블에 userId가 안 들어가서 예외처리 당한 거였다. 이후에 한 테스트 다 잘되서 완전 신나서 잠 깨서 개늦게 잤음...ㅋ
------- 여기까지는 주말에 한 것!
soft-delete
실무에 가면 데이터는 일종의 자산이기 때문에 유저 정보면 null 처리로 업데이트는 해도 절대 함부로 삭제하지는 않는다고 한다.
이걸 왜 생각하게 되었냐면...
<나의 생각 flow...>
1. Postman으로 테스트하다보니 기존 리뷰가 있는 경우 어떻게 할지에 대한 코드를 넣지 않음.
2. 그냥 isDeleted=false인 리뷰 하나 더 만들면 안되나? 했지만 Order랑 Review는 1:1이기 때문에 duplicate 났다.
3. 그럼 기존에 있는 isDeleted=true에 있는 거 false로 만들고, requestDto 새로 받아 업데이트 해줄까?
--> 괜찮은데? 라고 생각해서 만들고 테스트까지 마침. 그런데 팀장님이랑 이야기해보니...
4. 3번처럼 하면 그냥 DB에서 물리 삭제하고 새로 만드는 거랑 결과 똑같지 않나?
--> 이게 문제의 시작이었다...
왜인지 reviewRepository.delete(existingReview)가 실행을 안 하는 것이다...
// 리뷰 만들기
@Override
@Transactional
public ReviewResponseDto addReview(CreateReviewRequestDto requestDto, User user) throws IOException {
UUID orderId = UUID.fromString(requestDto.getOrderId());
...
// 기존 리뷰가 있는 경우, 삭제된 리뷰인지 확인 후 복구
Review existingReview = reviewRepository.findByOrder_Id(orderId);
if (existingReview == null) {
if (existingReview.isDeleted()) { // 리뷰가 삭제된 상태라면
// 기존 리뷰의 이미지 파일이 비어 있지 않다면
if (existingReview.getImage() != null && !existingReview.getImage().isEmpty()) {
s3Service.deleteFile(existingReview.getImage()); // 이미지 파일 삭제
}
// soft delete할 때 store 평점 관련 이미 처리해서 store 신경 안 써도 ok
reviewRepository.delete(existingReview);
} else {
throw new BadRequestException("리뷰가 이미 존재합니다.");
}
}
// 파일이 존재하면 S3에 파일 업로드
String imageUrl = null;
if (requestDto.getFile() != null && !requestDto.getFile().isEmpty()) {
imageUrl = s3Service.uploadFile(requestDto.getFile());
}
Review review = reviewRepository.save(new Review(requestDto, imageUrl, order));
// 리뷰 생성시 가게 총 평점 합계, 총 리뷰 개수 저장
Store store = order.getStore();
store.updateReviewStats(review.getGrade());
storeRepository.save(store);
return new ReviewResponseDto(review);
}
리뷰를 삭제하려니 연관관계때문에 주문까지 우루루 삭제되려고 함...
긴 시도(🐶오래 걸렸음)끝에 결국 ㅇㄱㅇ튜터님께서 해결해주셨다.
사실 이렇게 물리 삭제를 하고 새로 만드는 방법은 soft-delete를 하는 의미가 없는 짓이라고 하셨다.
차라리 Order:Review = 1:N으로 하고 orderId로 리뷰를 찾는 거에 AndIsDeletedFalse를 붙인 메서드를 만든 다음,
만약 existingReview != null이면 예외처리를 하고, null이면 리뷰를 새로 하나 만들어 기존에 soft-delete한 데이터를 보관하는 방법을 추천해주셨다.
오, 근데 그건 그거고 난 이게 왜 안되는지 일단 궁금합니다.
라고 말씀드리니 지금 저렇게 하면 delete(), save()가 쓰기 지연 저장소에 함께 있다 같이 처리가 되서 그런 거라고 하셨다.
그래서 delete() 구문 아래 flush() 처리를 해주면 delete()가 먼저 처리되고 이후 save()가 될 거라고 하셨다.
근데 안됨. 왜?
나는 여기서 바보같은 실수를 했다.
// 리뷰 삭제
@Override
@Transactional
public void removeReview(String reviewId, User user) {
Review review = reviewRepository.findById(UUID.fromString(reviewId)).orElseThrow(() ->
new NotFoundException("해당 리뷰는 존재하지 않습니다."));
// 리뷰 작성자 == 주문자 == 로그인 유저인지 확인하기
if (review.getOrder().getUser().getId() != user.getId()) {
throw new ForbiddenException("해당 리뷰의 작성자가 아닙니다.");
}
Store store = review.getOrder().getStore();
// 저장된 이미지 파일이 있는 경우 이미지 파일 삭제
if (review.getImage() != null && !review.getImage().isEmpty()) {
s3Service.deleteFile(review.getImage());
}
review.markAsDeleted(); // review.isDeleted=true
store.removeReviewStats(review.getGrade()); // 가게 평점에서 해당 review 제외
reviewRepository.save(review);
storeRepository.save(store);
}
뭐냐하면...
이미지 파일은 soft-delete로 리뷰 삭제할 때 이미 S3 Bucket에서 삭제했다.
그런데 DB에 image에 null 처리 되는 코드를 넣지 않아서 DB는 S3 Bucket에서 삭제됐든 말든 URL 주소를 고대로 가지고 있던 것이다.
However, 나는 existingReveiw.getImage != null이면 이미지 파일 삭제하라고 했네?
S3에서 이미 삭제된 걸 어떻게 찾겠냐!!!!!!!!!!!!!!!!!!!
그렇게 해결...
어쨌든 궁금증은 해결되서 ㄱㅇ튜터님이 추천하는 방식으로 코드를 다시 짜고, soft-delete의 의도를 지키고자 리뷰 삭제시 이미지 파일을 삭제하지 않기로 했다. 그래서 주문:리뷰, 리뷰:사장님의리뷰 다 1:N으로 만들었음.
(사장님의 리뷰 수정중에 리뷰를 @ManyToOne 해도 안되서 왜 이래???개빡치네;했는데 내가 unique=true 쳐걸어놓고 보고도 몰랐음.)
오늘도 테스트코드를 시도조차 하지못한 나란 잣식,,, 리뷰만 테스트코드 만들어보려고 한다...ㅎ
'TIL' 카테고리의 다른 글
TIL12. 1차 배달 플랫폼 프로젝트 회고 (0) | 2025.02.26 |
---|---|
TIL11. 테스트의 늪 (0) | 2025.02.25 |
TIL9. 리뷰 검색 및 정렬, QueryDSL (0) | 2025.02.21 |
TIL8. AI API (0) | 2025.02.20 |
TIL7. S3 Bucket 적용, DTO Validation (0) | 2025.02.19 |