관리 메뉴

거니의 velog

(44) 장바구니 API 만들기 2 본문

SpringBoot_React 풀스택 프로젝트

(44) 장바구니 API 만들기 2

Unlimited00 2024. 3. 8. 15:15

3. Repository의 설정

* Cart와 CartItem 엔티티 객체를 처리할 Repository를 생성한다. repository 패키지에 Cart를 처리하는 CartRepository, CartItemRepository 인터페이스를 생성한다.


(1) CartRepository

* CartRepository는 기본적으로 JpaRepository가 제공하는 기능들을 사용자의 이메일을 통해서 Cart를 알아내는 기능을 추가해 준다.

package com.unlimited.mallapi.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.unlimited.mallapi.domain.Cart;

/**
 * 장바구니 엔터티에 대한 데이터베이스 조회를 위한 JpaRepository입니다.
 *
 * - JpaRepository를 상속하여 기본적인 CRUD(Create, Read, Update, Delete) 기능을 사용할 수 있습니다.
 * - 특별한 쿼리 메서드나 JPQL(Querydsl)을 사용하여 추가적인 데이터베이스 조회가 가능합니다.
 */
public interface CartRepository extends JpaRepository<Cart, Long> {

    /**
     * 회원 이메일에 해당하는 장바구니를 조회하는 메서드입니다.
     *
     * @param email 조회할 회원의 이메일
     * @return 회원 이메일에 해당하는 장바구니 (Optional로 감싸여 있음)
     */
    @Query("select cart from Cart cart where cart.owner.email = :email")
    public Optional<Cart> getCartOfMember(@Param("email") String email);

}

(2) CartItemRepository

* CartItemRepository는 여러 엔티티를 JPQL로 조인처리해서 원하는 기능들을 구현해야 한다.

* CartItemRepository에는 다음과 같은 기능들을 추가한다.

1. 특정한 사용자의 이메일을 통해서 해당 사용자의 모든 장바구니 아이템을 조회하는 기능
   - 로그인했을 때 사용자가 담은 모든 장바구니 아이템 조회 시에 사용
   
2. 사용자의 이메일과 상품번호로 해당 장바구니 아이템을 알아내는 기능
   - 새로운 상품을 장바구니에 담고자 할 때 기존 장바구니 아이템인지 확인하기 위해서 필요
   
3. 장바구니 아이템이 속한 장바구니의 번호를 알아내는 기능
   - 해당 아이템을 삭제한 후 해당 아이템이 속해 있는 장바구니의 모든 아이템을 알아내기 위해 필요

4. 특정한 장바구니의 번호만으로 해당 장바구니의 모든 장바구니 아이템을 조회하는 기능
   - 특정한 장바구니 아이템을 삭제한 후에 해당 장바구니 아이템이 속해 있는 장바구니의 모든 장바구니
     아이템을 조회할 때 필요

* CartItemRepository에서 가장 중요한 기능은 여러 엔티티들을 조인처리해서 CartItemListDTO 타입의 객체로 조회하는 기능이다.

package com.unlimited.mallapi.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.unlimited.mallapi.domain.CartItem;
import com.unlimited.mallapi.dto.CartItemListDTO;

/**
 * 장바구니 상품 엔터티에 대한 데이터베이스 조회를 위한 JpaRepository입니다.
 *
 * - JpaRepository를 상속하여 기본적인 CRUD(Create, Read, Update, Delete) 기능을 사용할 수 있습니다.
 * - 특별한 쿼리 메서드나 JPQL(Querydsl)을 사용하여 추가적인 데이터베이스 조회가 가능합니다.
 */
public interface CartItemRepository extends JpaRepository<CartItem, Long> {

    /**
     * 회원 이메일에 해당하는 장바구니 상품 목록을 조회하는 메서드입니다.
     *
     * @param email 조회할 회원의 이메일
     * @return 회원 이메일에 해당하는 장바구니 상품 목록
     */
    @Query("select " +
            " new com.unlimited.mallapi.dto.CartItemListDTO(ci.cino, ci.qty, p.pno, p.pname, p.price, pi.fileName) "
            +
            " from " +
            "   CartItem ci inner join Cart mc on ci.cart = mc " +
            "   left join Product p on ci.product = p " +
            "   left join p.imageList pi" +
            " where " +
            "   mc.owner.email = :email and pi.ord = 0 " +
            " order by ci desc ")
    public List<CartItemListDTO> getItemsOfCartDTOByEmail(@Param("email") String email);

    /**
     * 특정 회원의 특정 상품에 대한 장바구니 상품을 조회하는 메서드입니다.
     *
     * @param email 조회할 회원의 이메일
     * @param pno   조회할 상품의 고유 식별 번호
     * @return 특정 회원의 특정 상품에 대한 장바구니 상품 (Optional로 감싸여 있음)
     */
    @Query("select" +
            " ci " +
            " from " +
            "   CartItem ci inner join Cart c on ci.cart = c " +
            " where " +
            "   c.owner.email = :email and ci.product.pno = :pno")
    public CartItem getItemOfPno(@Param("email") String email, @Param("pno") Long pno);

    /**
     * 특정 장바구니 상품에 대한 장바구니의 고유 식별 번호를 조회하는 메서드입니다.
     *
     * @param cino 조회할 장바구니 상품의 고유 식별 번호
     * @return 특정 장바구니 상품에 대한 장바구니의 고유 식별 번호
     */
    @Query("select " +
            "  c.cno " +
            "from " +
            "  Cart c inner join CartItem ci on ci.cart = c " +
            " where " +
            "  ci.cino = :cino")
    public Long getCartFromItem(@Param("cino") Long cino);

    /**
     * 특정 장바구니에 속한 장바구니 상품 목록을 조회하는 메서드입니다.
     *
     * @param cno 조회할 장바구니의 고유 식별 번호
     * @return 특정 장바구니에 속한 장바구니 상품 목록
     */
    @Query("select new com.unlimited.mallapi.dto.CartItemListDTO(ci.cino,  ci.qty,  p.pno, p.pname, p.price , pi.fileName )  "
            +
            " from " +
            "   CartItem ci inner join Cart mc on ci.cart = mc " +
            "   left join Product p on ci.product = p " +
            "   left join p.imageList pi" +
            " where " +
            "  mc.cno = :cno and pi.ord = 0 " +
            " order by ci desc ")
    public List<CartItemListDTO> getItemsOfCartDTOByCart(@Param("cno") Long cno);

}

* 인터페이스에 선언된 getItemsOfCartDTOByEmail() 이나 getItemsOfCartDTOByCart() 는 Projection을 이용해서 직접 CartItemListDTO 타입의 객체들을 반환하는 방식으로 작성한다.


[장바구니 아이템 추가 테스트]

* 장바구니에 새로운 장바구니 아이템을 추가하는 경우에 전달되는 데이터는 다음과 같다.

- 사용자의 이메일(email)

- 현재 상품의 번호

- 수량(qty)

* 장바구니 아이템 추가는 우선 이메일을 이용해서 현재 사용자의 장바구니에 해당 상품으로 만들어진 장바구니 아이템이 있는지를 확인해야 한다. 만일 해당 장바구니 아이템이 있다면 수량(qty)만 변경해서 저장하고 그렇지 않다면 새로운 장바구니 아이템을 만들어서 저장해야 한다. 이 때 장바구니 자체가 없을 수도 있었기 때문에 장바구니를 체크하는 로직이 중간에 필요하다.

* 테스트 코드를 위해 CartRepositoryTests 를 작성한다.

* 가장 먼저 화면에서 특정한 상품을 선택해서 장바구니에 추가하는 경우를 테스트한다.

package com.unlimited.mallapi.repository;

import java.util.*;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;

import com.unlimited.mallapi.domain.Cart;
import com.unlimited.mallapi.domain.CartItem;
import com.unlimited.mallapi.domain.Member;
import com.unlimited.mallapi.domain.Product;

import lombok.extern.log4j.Log4j2;

/**
 * CartRepository 및 CartItemRepository 테스트 클래스입니다.
 *
 * @SpringBootTest: 스프링 부트 테스트를 위한 어노테이션
 * @Log4j2: Lombok을 이용한 로깅을 위한 어노테이션
 */
@SpringBootTest
@Log4j2
public class CartRepositoryTests {

    @Autowired
    private CartRepository cartRepository;

    @Autowired
    private CartItemRepository cartItemRepository;

    /**
     * 상품을 장바구니에 추가하는 테스트 메서드입니다.
     *
     * @Transactional: 메서드 실행이 끝난 후에 롤백되도록 하는 어노테이션
     * @Commit: 롤백되지 않도록 하는 어노테이션
     * @Test: JUnit 테스트 메서드를 지정하는 어노테이션
     */
    @Transactional(rollbackFor = Exception.class)
    @Commit
    @Test
    public void testInsertByProduct() {
        log.info("test1-----------------------");

        String email = "user1@aaa.com";
        Long pno = 5L;
        int qty = 2;

        // 상품 번호와 회원 이메일을 이용하여 해당 상품의 장바구니 상품을 조회합니다.
        CartItem cartItem = cartItemRepository.getItemOfPno(email, pno);

        // 조회된 장바구니 상품이 존재하면 수량을 변경하고 저장합니다.
        if (cartItem != null) {
            cartItem.changeQty(qty);
            cartItemRepository.save(cartItem);
            return;
        }

        // 회원 이메일로 해당 회원의 장바구니를 조회합니다.
        Optional<Cart> result = cartRepository.getCartOfMember(email);

        Cart cart = null;

        // 장바구니가 존재하지 않으면 새로운 장바구니를 생성합니다.
        if (result.isEmpty()) {
            log.info("MemberCart is not exist!!");
            Member member = Member.builder().email(email).build();
            Cart tempCart = Cart.builder().owner(member).build();
            cart = cartRepository.save(tempCart);
        } else {
            cart = result.get();
        }
        log.info(cart);
        if (cartItem == null) {
            // 상품 정보를 이용하여 새로운 장바구니 상품을 생성합니다.
            Product product = Product.builder().pno(pno).build();
            cartItem = CartItem.builder().product(product).cart(cart).qty(qty).build();
        }
        cartItemRepository.save(cartItem);
    }

}

* 테스트 코드를 실행하면 아래와 같은 테이블 생성 DDL이 실행되고 인덱스나 PK 등이 설정된다.

테이블 생성
조건 설정

* 장바구니 테이블(tbl_cart)에는 아무 것도 없었기 때문에 새로운 장바구니가 생성된 후에 새로운 장바구니 아이템이 추가된다.

장바구니 생성
새로운 장바구니 아이템 생성

* 테스트의 결과로는 tbl_cart에 하나의 데이터가 생성되고, tbl_cart_item에도 하나의 데이터가 추가된다.

* 테스트 코드를 다시 한 번 실행해 보면, 이미 해당 사용자의 장바구니에 해당 상품에 대한 장바구니 아이템이 존재하기 때문에 update가 실행되는 것을 확인할 수 있다. 기존의 코드에서 상품의 수량을 아래와 같이 변경하고 테스트를 실행하면 마지막에는 update문이 실행되는 것을 확인할 수 있다(기존과 동일한 경우 update가 실행되지 않으므로 주의).

// 사용자가 전송하는 정보
String email = "user1@aaa.com";
Long pno = 5L;
int qty = 5;


[장바구니 아이템 수정 테스트]

* 일반적인 장바구니 화면을 생각해보면 특정한 장바구니 아이템의 수량을 조정하는 경우가 많다. 이 경우에는 이미 장바구니 아이템 고유의 PK가 있는 상황이므로 이를 이용해서 수정할 수 있다.

    /**
     * 장바구니 상품의 수량을 업데이트하는 테스트 메서드입니다.
     *
     * @Test: JUnit 테스트 메서드를 지정하는 어노테이션
     * @Commit: 테스트 실행 후 롤백되지 않도록 하는 어노테이션
     */
    @Test
    @Commit
    public void tesstUpdateByCino() {
        Long cino = 1L; // 업데이트할 장바구니 상품의 고유 식별 번호
        int qty = 10; // 업데이트할 상품 수량

        // 고유 식별 번호로 장바구니 상품을 조회합니다.
        Optional<CartItem> result = cartItemRepository.findById(cino);
        CartItem cartItem = result.orElseThrow();

        // 조회된 장바구니 상품의 수량을 업데이트합니다.
        cartItem.changeQty(qty);

        // 업데이트된 장바구니 상품을 저장합니다.
        cartItemRepository.save(cartItem);
    }


[현재 사용자의 장바구니 아이템 목록 테스트]

* 사용자가 로그인을 했다면 사용자의 장바구니 아이템 목록을 볼 수 있는 기능을 테스트한다.

    /**
     * 특정 회원의 장바구니 상품 목록을 조회하는 테스트 메서드입니다.
     *
     * @Test: JUnit 테스트 메서드를 지정하는 어노테이션
     */
    @Test
    public void testListOfMember() {
        String email = "user1@aaa.com"; // 조회할 회원의 이메일

        // 회원 이메일을 이용하여 장바구니 상품 목록을 조회합니다.
        List<CartItemListDTO> cartItemList = cartItemRepository.getItemsOfCartDTOByEmail(email);

        // 조회된 장바구니 상품 목록을 출력합니다.
        for (CartItemListDTO dto : cartItemList) {
            log.info(dto);
        }
    }

* 테스트 코드를 실행해 보면 여러 테이블이 조인으로 처리되어 한 번에 목록 데이터를 구하고 이를 DTO로 변환한 것을 볼 수 있다.


[장바구니 아이템 삭제와 목록 조회]

* 장바구니 아이템을 삭제하는 기능 자체는 JpaRepository의 기능을 그대로 이용하겠지만 주의해야 하는 부분은 해당 장바구니 아이템이 삭제된 후에 다시 해당 아이템이 있었던 장바구니의 모든 장바구니 아이템 목록을 반환해야만 한다. 때문에 해당 장바구니 아이템을 삭제하기 전에 해당 장바구니의 번호를 구해두고 삭제 후에 이를 이용해서 장바구니 아이템 목록을 구하는 방식으로 구현한다.

* 테스트 코드에서는 임시로 실제 장바구니 아이템의 삭제 부분은 주석으로 처리하고 장바구니 아이템 목록을 구해본다(코드에서 deleteById()).

    /**
     * 장바구니 상품을 삭제한 후 해당 장바구니의 상품 목록을 조회하는 테스트 메서드입니다.
     *
     * @Test: JUnit 테스트 메서드를 지정하는 어노테이션
     */
    @Test
    public void testDeleteThenList() {
        Long cino = 1L; // 삭제할 장바구니 상품의 고유 식별 번호

        // 삭제할 장바구니 상품의 소속 장바구니의 고유 식별 번호를 조회합니다.
        Long cno = cartItemRepository.getCartFromItem(cino);

        // 삭제할 때 사용한 코드
        // cartItemRepository.deleteById(cino);

        // 삭제 후 장바구니의 상품 목록을 조회합니다.
        List<CartItemListDTO> cartItemList = cartItemRepository.getItemsOfCartDTOByCart(cno);

        // 조회된 장바구니 상품 목록을 출력합니다.
        for (CartItemListDTO dto : cartItemList) {
            log.info(dto);
        }
    }

* 테스트 코드를 실행하면 아래와 같이 특정한 장바구니의 모든 장바구니 아이템들을 조회하는 것을 확인할 수 있다.