꾸준히하자아자

카카오 쇼핑하기 클론프로젝트 #3 본문

프로젝트/카카오 쇼핑하기 web

카카오 쇼핑하기 클론프로젝트 #3

성장하고픈개발자 2023. 8. 9. 01:30
728x90
728x90

매주 프로젝트 진행상황을 블로그에 업데이트 하려고 했지만...

4주차 까지는 스프링부트에 적응하면서 과제 하느라 바빴고 쏟아지는 새로운 개념들을 익히기 바빴다..ㅎ ㅎ

 

저번 주에 드디어 6주간의 프로젝트가 끝나고 뭘 해야 좋을 지 고민하다가...

새로운 프로젝트를 하는 것 보단 기존 프로젝트의 부족한 기능을 추가하거나 코드를 리팩토링 하는 시간을 갖는 게 좋을것 같다고 생각했다.

 

기존 프로젝트엔 "장바구니 조회" , "장바구니 추가" , "장바구니 수량 수정 기능"만 구현했다.

따라서 "장바구니 옵션 삭제" 기능을 추가해봤다.

제대로 잘 구현했는지 확신할 수 없지만ㅜ..ㅜ 일단 스스로 구현한 내용을 바탕으로 적어보겠다.

사진은 프론트 UI인데 각 옵션마다 삭제할 수 있는 x 버튼을 추가했다고 가정하자.


x 버튼을 눌렀을 때, /carts/update 경로로 DELETE 요청이 들어오고

Request Header에는 Bearer Token이 들어가고

 

Request Body (예시)

{
	"cartId" : 3
}

 

Response Body (예시)

{
    "success": true,
    "response": {
        "carts": [
            {
                "cartId": 4,
                "optionId": 1,
                "optionName": "01. 슬라이딩 지퍼백 크리스마스에디션 4종",
                "quantity": 10,
                "price": 100000
            },
            {
                "cartId": 5,
                "optionId": 2,
                "optionName": "02. 슬라이딩 지퍼백 플라워에디션 5종",
                "quantity": 10,
                "price": 109000
            }
        ],
        "totalPrice": 209000
    },

라고 가정하자.

예시 응답 Body에는 cartId가 3인 옵션이 삭제된 후의 장바구니를 나타내었다.


장바구니 옵션 삭제 로직을 구현할 때 고민했던 점 

1. 요청Body에 (장바구니 수량 수정 로직과 같이) 리스트 형태로 넘겨줄지 말지 고민했다.

{
    "cartId": 1
}
{
    "cartId": 2
} //or 하나만?
  • 장바구니 수정 로직, 장바구니 추가 로직을 구현할 때도 리스트 형태로 넘겨줬었다..,, 
  • 화면 설계서를 보면 동시에 두 옵션의 수량을 수정하는 것이 아닌 하나하나 수량을 수정하는데 굳이 리스트로 넘겨줘야하나? 라는 생각이 들었다.
  • 프론트가 요청Body를 백에게 넘겨주는 작동원리를 몰라서 이런 의문이 드는 것 같다.
  • 마찬가지로 삭제 요청도 사용자가 하나하나 처리하므로 굳이 리스트 형태로 넘겨주지 않아도 될 것 같다고 생각했다.
  • 물론 리스트 형태로 넘기는 게 정답일 수도 있지만! 그냥 단일 객체로 넘기는 형태로 작성하기로 했다.

2. 화면 UI내에서 발생할 수 없는 요청Body에 대한 예외처리도 하는 게 좋다. (추후에 공부)

  • 처음엔 클라이언트는 보통 화면 UI 내에서 조작하기 때문에 들어올 수 없는 예외처리는 필요 없다고 생각했다.
  • 하지만 잘못된 경로로 이상한 요청이 들어올 수 있기 때문에 최대한 모든 예외를 잡아야 한다.
  • 명백히 잘못 들어온 경우라면 다양한 사이드 이팩트 방지를 위해 예외처리해 주는게 맞다고 한다!
  • 보안문제, 데이터 무결성 유지, 서버 자원 보호 등의 이유도 있다.

3. 마지막에 남은 하나의 레코드를 삭제하려고 할 때 삭제가 되지 않는 에러 발생 

처음에 작성한 서비스 계층 로직

@Transactional
    public CartResponse.DeleteDTO delete(CartRequest.DeleteDTO requestDTO, User sessionUser) {
        List<Cart> cartList = cartJPARepository.findAllByUserId(sessionUser.getId());

        if(cartList.isEmpty()){
            throw new Exception404("장바구니가 비어있습니다.");
        }

        HashSet<Integer> set = new HashSet<>();
        for(Cart cart : cartList){
            set.add(cart.getId());
        }
        if(!set.contains(requestDTO.getCartId())){
            throw new Exception400("장바구니에 없는 상품은 삭제할 수 없습니다.");
        }


        for(Cart cart : cartList){
            if(requestDTO.getCartId() == cart.getId()){
                cartJPARepository.deleteById(requestDTO.getCartId());
                cartList.remove(cart);
            }
        }
		return new CartResponse.DeleteDTO(cartList);
    }
  • user의 장바구니 정보를 가져오고, 장바구니가 비었다면 예외처리를 한다.
  • 만약에 요청Body로 존재하지 않는 cartId 가 들어온다면 예외처리를 한다.
  • for 루프를 돌면서 요청Body로 들어온 cartId와 DB에 존재하는 cartId가 같다면 delete하고 cartList에도 삭제한다.
    • But 문제 발생….!!
    • 삭제를 하나하나 하다가 장바구니에 마지막 하나의 상품이 남고 삭제 시도를 했을 때 500 에러가 발생했다.
    • cartJPARepository.deleteById(requestDTO.getCartId())를 호출한 후에 cartList 에서 해당 요소를 제거하는 것은 데이터베이스와의 동기화 문제를 야기할 수 있다고 한다.
    • 삭제 작업은 데이터베이스에서 먼저 수행되도록 보장해야 한다.
    • 따라서 삭제가 완료된 후 리스트에서 제거하는 로직으로 변경해야 한다.

변경 후

for (Cart cart : cartList) {
            if (requestDTO.getCartId() == cart.getId()) {
                cartJPARepository.deleteById(requestDTO.getCartId());
                break;  // 삭제 후 반복문 종료
            }
        }
        
cartList.removeIf(cart -> cart.getId() == requestDTO.getCartId());

테이블 확인

만약에 user_id가 2인 사용자로 로그인 한 상태라고 가정했을 때 모든 경우의 응답을 유도해보겠다.


장바구니에 없는 상품을 삭제하려고 시도했을 때


cartId가 4인 상품은 본인 장바구니에 있는 상품이 아니므로 삭제 불가능


cartId가 1인 상품 삭제


cartId가 3인 상품 삭제


cartId가 2인 상품 삭제 -> 장바구니에 담겨있던 상품들을 모두 삭제해서 장바구니가 비어있다.


장바구니가 비어있는 상태에서 상품을 삭제하려고 시도했을 경우


삭제 시 실행된 쿼리문

@Query("select c from Cart c join fetch c.option o join fetch o.product where c.user.id =:userId")
    List<Cart> findAllByUserId(int userId);

불필요한 select쿼리를 방지하기 위해 product 테이블과 option 테이블을 fetch join 하여 한번에 값을 가져왔다.

Hibernate: 
    select
        cart0_.id as id1_0_0_,
        option1_.id as id1_4_1_,
        product2_.id as id1_6_2_,
        cart0_.option_id as option_i4_0_0_,
        cart0_.price as price2_0_0_,
        cart0_.quantity as quantity3_0_0_,
        cart0_.user_id as user_id5_0_0_,
        option1_.option_name as option_n2_4_1_,
        option1_.price as price3_4_1_,
        option1_.product_id as product_4_4_1_,
        product2_.description as descript2_6_2_,
        product2_.image as image3_6_2_,
        product2_.price as price4_6_2_,
        product2_.product_name as product_5_6_2_ 
    from
        cart_tb cart0_ 
    inner join
        option_tb option1_ 
            on cart0_.option_id=option1_.id 
    inner join
        product_tb product2_ 
            on option1_.product_id=product2_.id 
    where
        cart0_.user_id=?
Hibernate: 
    delete 
    from
        cart_tb 
    where
        id=?

 

728x90
728x90