⌨️ 비즈니스 로직 구현 회고 (찜한 상품, 리뷰, 통합검색)
찜한 상품 쪽은 비즈니스 로직 적으로 완벽히 구현하기보다는 프런트에서 해보고 싶은 것을 같이 협의하여 작성하였다,
리뷰 또한 마찬 가지이나 이 쪽은 백엔드 적인 로직이 들어갔다.
1. 찜한 상품 구현
첫 번 째는 찜한 상품의 구현이다. 로직은 간단한 Create와 Delete만 만들었다, 이유는 프런트에서 한 버튼 (♥) 의 상태에 따라 다른 요청을 보내는 것을 구현해보고 싶다는 의견을 주었다.
예를 들어 ♡ 에서는 Post 요청을 보내 찜 상품을 Create 하고 상태를 ♥ 로 바꾼다.
반대로 ♥ 에서는 Delete 요청을 보내 찜 상품을 Delete 하고 상태를 ♡ 로 바꾼다.
위 로직을 백엔드 적으로 해결하려고 내가 생각했던 것은
둘 다 Post를 보내 해당 상품이 있는지 탐색 후 없다면 만들고 있다면 삭제하는 로직으로 구현하려 했다.
1.1. 찜한 상품의 로직
//찜 등록
@Override
public void createProductLike(Long productId, CustomPrincipal principal){
Product product = productRepository.findById(productId).orElseThrow(() -> new EntityNotFoundException("상품이 없습니다."));
Profile profile = profileRepository.getReferenceById(principal.profileId());
ProductLike productLike = productLikeRepository.findByProductIdAndProfileId(productId, principal.profileId());
if(productLike != null){
throw new BusinessLogicException(ExceptionCode.PRODUCT_LIKE_IS_EXIST);
}
else
productLikeRepository.save(ProductLike.of(profile, product));
}
//찜 삭제
@Override
public void deleteProductLike(Long productId, CustomPrincipal principal){
productLikeRepository.deleteByProductId(productId);
}
하지만 프런트에서와의 협의를 통해 위에서 설명한 대로 구현했기에 코드 자체가 굉장히 간결해졌다.
중요한 부분이 업무가 어느 한쪽에 치우치지 않고 충분히 협의를 해서 로직을 짜는 것 같다.
페이지네이션 같은 것이나 정렬 필터 등등 프론트 자체적으로 해결할 수 있는 것들도 분명히 있으니, 업무의 과중이 어느 한쪽에 치우치지 않게 밸런스를 잡는 것이 중요한 부분이라고 생각한다.
2. 마이페이지 리뷰 구현
작성 가능 한 후기와 작성한 후기는 조금 고생을 했다,
첫째로 작성이 가능한 후기로 선별하려면 우선 결제가 완료가 되었어야 했고,
두 번째로는 우리는 특별하게 같은 상품이라도 구매한 주문번호가 다르다면 리뷰를 추가로 남길 수 있게 했다, 즉 두 구매를 다른 구매로 보는 것으로 정했다.
2.1. 작성 가능한 후기의 로직
다행히도 작성 가능한 후기의 로직 중 "구매 여부"는 지난 베스트 상품에서의 로직을 통해 어느 정도 해소를 했다.
나는 이제 구매한 상품 중에 리뷰가 달려 있지 않은 주문만 선택해서 보여주면 됐다.
여기서 하나의 문제가 발생했다 선택하는 객체가 서로 달랐기 때문이다. 우선
작성 가능한 후기는 후기를 작성할 수 있는 "주문"을 보여줬고
작성한 후기는 "후기"를 보여줬기에 서로 선택하는 객체가 달랐다.
@GetMapping("/review")
public ResponseEntity getReviewList(
@AuthenticationPrincipal CustomPrincipal principal,
@RequestParam("type") String type,
@Min(0) @RequestParam(defaultValue ="0",required = false) int page,
@RequestParam(defaultValue = "4", required = false) int size
){
Pageable pageable = PageRequest.of(page, size);
if(Objects.equals(type, "nonexistent")){
Page<ProfileMyPageReviewEnableResponse> response = ordersService.readEnableReviewOrder(principal,pageable);
List<ProfileMyPageReviewEnableResponse> responseList = response.getContent();
List<Integer> barNumber = paginationService.getPaginationBarNumbers(page, response.getTotalPages());
return new ResponseEntity<>(
new PageResponseDto<>(responseList,response,barNumber), HttpStatus.OK);
}else if(Objects.equals(type, "exist"))
{
Page<ProfileMyPageReviewExistResponse> response = productCsService.readProductReview(principal, pageable);
List<ProfileMyPageReviewExistResponse> responseList = response.getContent();
List<Integer> barNumber = paginationService.getPaginationBarNumbers(page, response.getTotalPages());
return new ResponseEntity<>(
new PageResponseDto<>(responseList, response, barNumber), HttpStatus.OK);
}
return null;
}
따라서 해당 부분의 분기를 컨트롤러에서 나눴는데.. 살짝 의문스러운 부분이 많은 구현 방식이다 로직은 서비스 계층에 담고 싶은 데 서로 다른 객체를 한 컨트롤러에서 담으려니 분리가 힘들었다.
아니면 분기를 나누지 말고 컨트롤러 자체를 분리했으면 어땠을까 싶다.
우선 작성 가능한 후기의 서비스 계층은 아래와 같다
@Override
public Page<ProfileMyPageReviewEnableResponse> readEnableReviewOrder(CustomPrincipal principal, Pageable pageable) {
return orderDetailRepository.findAllByOrdersProfileIdAndProductReviewIsNull
(principal.profileId(), PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("id").descending())).map(ProfileMyPageReviewEnableResponse::from);
}
우리의 서비스 연관관계에선 결제가 완료되어야 비로소 오더디테일 테이블이 생성되기에
Page<OrderDetail> findAllByOrdersProfileIdAndProductReviewIsNull(Long profileId, Pageable pageable);
해당 부분은 간단히 JPA를 활용하여 구현했다.
2.2. 작성한 후기의 로직
이 부분은 쉽게 완료할 수 있었다. 단순히 후기를 보여주면 됐기 때문이다.
컨트롤러는 위와 같고
서비스 계층은
//profileId로 리뷰 리스트 get
@Transactional(readOnly = true)
@Override
public Page<ProfileMyPageReviewExistResponse> readProductReview(CustomPrincipal principal, Pageable pageable){
return productReviewRepository.findByProfileId(principal.profileId(), PageRequest.of(pageable.getPageNumber(),pageable.getPageSize(), Sort.by("id").descending())).map(ProfileMyPageReviewExistResponse::from);
}
위와 같이 간단히 해결 됐으며 데이터 계층 또한 마찬가지로 JPA를 활용하여 구현했다.
해당 부분에서의 고민 사항은 역시 컨트롤러 부분이다.. 이 부분을 컨트롤러를 분리를 했다면 어땠을까 싶다.
3. 통합 검색 구현
통합 검색은 정말 간단히 구현하기로 협의했다. 브랜드와, 상품 이름, 판매자 등을 통합해서 검색해 주는 것이다.
드롭다운 박스로 키워드의 카테고리를 정하지 않고 각 칼럼이 해당 키워드를 포함한다면 전부 보여주는 식으로 구현하는 것으로 협의를 했다.
3.1. 통합 검색의 로직
위에서 말했다시피 되게 간단히 구현한 검색바기에 컨트롤러 또한 단출하다
@GetMapping
public ResponseEntity getSearchProduct(
@RequestParam(required = false, value = "keyword") String keyWord,
@Min (0)@RequestParam(defaultValue = "0", required = false) int page,
@Positive @RequestParam(defaultValue = "15", required = false) int size
){
Pageable pageable = PageRequest.of(page, size);
Page<ProductResponseToPage> searchProductPage = productService.readProductWithKeyWord(keyWord, pageable).map(ProductResponseToPage::from);
List<ProductResponseToPage> productPage = searchProductPage.getContent();
List<Integer> barNumber = paginationService.getPaginationBarNumbers(page, searchProductPage.getTotalPages());
return new ResponseEntity<>(
new PageResponseDto<>(productPage,searchProductPage, barNumber), HttpStatus.OK);
}
}
서비스 계층이 조금 이상한데 우선 코드를 보고 말을 하겠다.
//통합 검색 (Name, Seller, Brand)
@Transactional(readOnly = true)
@Override
public Page<ProductDto> readProductWithKeyWord(String keyWord, Pageable pageable) {
return productRepository.findAllByNameContainingOrBrandContainingOrSellerContaining(keyWord,keyWord,keyWord, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("id").descending())).map(ProductDto::from);
하나의 키워드를 받아 Keyword, Keyword, Keyword, 로 작성했는 데 누가 봐도 이쁜 코드는 아니다.
해당 부분은 Repository를 봐보자
Page<Product> findAllByNameContainingOrBrandContainingOrSellerContaining(String keyword1,String keyword2, String keyword3, Pageable pageable);
JPA에서 제공하는 것을 활용하여 구현하려니 변수가 의미 없이 늘어난 것이 보인다.
해당 코드의 구현 방법은 JPA에서 검색하려는 칼럼마다 변수를 넣어줘야 한다 즉 A B C 칼럼에서 같은 것을 검색하려면 (x, x, x)로 넣어줘야 작동했다.
해당 부분은 그러나 쿼리문으로 고려해 보면
SELECT NAME,BRAND,SELLER FROM PRODUCT
WHERE NAME ='' OR BRAND ='' OR SELLER = ''
같은 느낌이 될 것이다. 해당 부분은 querydsl을 통해 한 개의 변수를 통해 작성하거나 GROUP BY로 그룹화 후에 구현했다면 어땠을까 싶다.
4. 피드백
- 작성 가능한 후기와, 작성한 후기 컨트롤러 분리에 대해서 고민해 보고 리팩토링
- 찜한 상품의 안정성 체크를 위해 백엔드적인 로직을 추가할 것을 고민해 보기
- 통합검색을 더욱 고성능으로 쿼리문을 리팩토링 하기 *결국 데이터가 많아지면 쿼리문의 최적화에 따라 검색 속도가 달라지므로