[Spring Boot] sports echo 프로젝트에 장바구니 기능 구현하기
- 장바구니 기능은 로그인한 멤버가 원하는 상품들을 담아 둘 수 있는 기능이다.
- 한 명의 멤버가 여러 상품을 여러 개 담을 수 있다.
- 여러 명의 멤버는 각각 자신의 장바구니를 가질 수 있다.
우선 장바구니 ERD를 다음과 같이 설계했다.
멤버와 장바구니가 1:N
, 상품과 장바구니도 1:N
으로 설계했다. 그러나 이렇게 설계하고 막상 구현할 때는 굉장히 헷갈렸는데, 장바구니 기능을 유저 입장에서 생각했기 때문이다. 멤버가 각각 장바구니를 가지므로 1:1
이 아닌가?? 장바구니 하나에 상품을 여러 개 담을 수 있으므로 1:N
이 아니라 N:1
아닌가?? 상품도 여러 장바구니에 담길 수 있으니까 N:N
인가??
그러나 위의 설계를 그대로 채택했다. 여기서 장바구니는 장바구니라는 하나의 기능이라고 생각하기보단 멤버가 상품을 여러 개 가질 수 있고 상품도 멤버를 여러 개 가질 수 있으므로 멤버와 상품이 N:N
관계일때, 장바구니는 이를 연결해주는 중간 테이블인 것이다. 그래서 장바구니 엔티티 이름은 cart에서 member_product로 변경했다. (다만 이 이름은 추후에 약간의 혼란을 야기하였으므로 그냥 cart로 했어도 될 것 같다.)
ERD를 기반으로 엔티티를 설계하면 다음과 같다.
public class MemberProduct {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int productsQuantity;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
}
MemberProduct 엔티티는 중간 테이블이므로 member와 product와 각각 연관관계를 가진다. 또한 어떤 멤버가 어떤 상품을 몇 개 담았는지 알 수 있어야 하므로 상품 개수를 나타내는 productsQuantity 컬럼만 추가했다.
이제 본격적인 기능 구현을 해보자. 우선 상품을 장바구니에 추가하는 메서드이다.
@Transactional
public MemberProductResponseDto addCart(Long productId, MemberProductRequestDto requestDto, Member member) {
Product product = findProduct(productId);
Optional<MemberProduct> memberProduct = memberProductRepository.findByProductIdAndMemberId(
productId, member.getId());
MemberProductResponseDto responseDto;
if (memberProduct.isPresent()) {
memberProduct.get().updateQuantity(requestDto.getProductsQuantity());
responseDto = MemberProductMapper.INSTANCE.toResponseDto(memberProduct.get());
} else {
MemberProduct savedMemberProduct = MemberProductMapper.INSTANCE.toEntity(
requestDto, member, product);
memberProductRepository.save(savedMemberProduct);
responseDto = MemberProductMapper.INSTANCE.toResponseDto(savedMemberProduct);
}
return responseDto;
}
productId는 pathvariable, Member 정보는 로그인한 유저 정보를 가져오고 requestDto로 상품 수량(productsQuantity)를 받는다. 해당 멤버가 해당 상품을 장바구니에 담았던 기록이 있는지 확인 후, 기록이 있을 경우 수량을 추가해 업데이트하고 없을 경우 새로운 memberProduct 객체를 만들어 저장한다.
다음은 장바구니 조회 메서드이다. 특정 멤버의 장바구니를 조회하는 메서드의 경우 두 가지 로직을 생각해 볼 수 있었다.
- memberProduct 레포지토리에서 memberId를 통해 memberProduct 객체들을 List로 조회
- member 엔티티에 연관관계 매핑으로 존재하는 memberProductList를 get 해오는 방식
각각의 방식은 장단점이 존재한다. 1번의 경우 쿼리가 더 많이 나가지만 안정적이며 2번의 경우는 1번의 반대이다. 우선적으로 1번을 채택하였지만 추후 쿼리 튜닝을 진행하면서 실행 시간, 쿼리 개수를 포함해 로직의 안정성, 프로젝트에서의 적합성을 모두 고려하여 확정할 예정이다.
@Transactional(readOnly = true)
public List<MemberProductResponseDto> getCart(Member member) {
List<MemberProduct> memberProductList = memberProductRepository.findByMemberId(
member.getId());
return memberProductList.stream()
.map(MemberProductMapper.INSTANCE::toResponseDto)
.toList();
}
하여 1번 방식으로 구현한 장바구니 조회 메서드이다. findByMemberId 메서드를 통해 memberProduct 리스트를 조회해온다.
모두 잘 작동 하는 것을 볼 수 있다.
github : https://github.com/SportsEcho/EchoProject-BE/tree/dev1/src/main/java/com/sportsecho/memberProduct