본문으로 바로가기

프로젝트의 전체 소스 코드는 이곳에서 확인하실 수 있습니다.

1. RDBMS에 데이터를 저장하는것이 아닌 Redis에 사용자가 추가한 상품을 저장하는 방법

해당 방법에는 몇 가지 문제점이 있었다. 먼저 RDBMS에 저장하지 않고 Redis에 캐시 개념으로 사용할 경우 데이터가 유실될 가능성이 있다. 물론 데이터 유실을 방지하기 위해 Redis 내 데이터들을 영속성으로 저장하는 방법이 존재하지만 Redis의 영속성 기능으로 인한 장애 발생 가능성이 굉장히 크기 때문에 사용할 수 없었다.

또한 레디스는 항상 메모리 관리에 신경을 써야 하기 때문에 TTL을 설정해야 하는데, Time-out을 설정할 경우 시간이 지나면 사용자가 추가한 CART에서 상품이 삭제되어버린다. (이 문제 또한 요구사항마다 다르지만 본 프로젝트에서는 CART에 담은 아이템을 지속적으로 유지하도록 구현할 예정이다.)

이러한 이유로 In-memory DB인 Redis를 CART로 사용하기에는 다소 무리가 있다고 생각돼 해당 방법은 배제하였다.

2. 값 타입 컬렉션 사용

두 번째 방법으로 USER(사용자)마다 값 타입 컬렉션으로 상품 목록을 저장하는 방법이다.

JPA에서 값 타입 컬렉션을 사용할때 @ElementCollection , @CollectionTable 어노테이션을 사용하여 아래와 같이 구현해야 한다.

@ElementCollection
@CollectionTable(name = "CART", joinColumn = 
     @JoinColumn(name = "USER_ID")
)
private Set<Product> wishList = new HashSet<>();

하지만 이러한 구현 방식에는 단점이 존재한다.

wishList라는 값 타입 컬렉션에 변경 사항이 발생하게 되면 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. 엄청난 비효율이다.

 

따라서 JPA에서는 이러한 비효율적인 값 타입 컬렉션의 대안으로 일대다 관계영속성 전이 + 고아 객체 제거옵션을 추가하여 값 타입 컬렉션 처럼 사용하고 있다.

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "USER_ID")
private Set<CartItem> wishList = nwe HashSet<>();

해당 방식으로 구현할때는 CartItem이라는 중간 테이블을 생성해서 사용해야 한다. 기존에 상품으로 사용하던 엔티티인 Product를 그대로 사용하게 되면orphanRemoval = true 옵션으로 인해 해당 사용자가 탈퇴시 사용자가 wishList에 담아놓은 상품까지 함께 삭제되기 때문이다.

3. 사용자와 장바구니의 일대일 매핑

설명하기에 앞서 이번 프로젝트에서는 해당 방식으로 장바구니를 구현하였다.

 

사용자(USER)와 장바구니(CART)의 연관관계는 프로젝트의 요구사항에 따라 변경될 수 있다. 다음과 같은 두 개의 요구사항이 있다고 가정해보자.

요구사항 1 : 회원 가입을 진행하고 로그인을 하고난 이후에 장바구니 기능을 사용할 수 있다.

✏️ 하나의 회원마다 장바구니가 정해져 있어야 하기 때문에 해당 요구사항은 일대일 매핑으로 충분하다.

요구사항2 : 로그인을 하지 않아도 장바구니 이용이 가능하다. 즉 비회원 주문이 가능하다.

✏️ 이러한 경우라면 사용자와 장바구니의 연관관계는 일대다 또는 다대다 관계를 고려해봐야 한다.

 

현재 진행하는 프로젝트는 간단한 상품조회를 제외하고는 대부분 회원제로 운영되고 있다. 따라서 요구사항1과 동일한 방식으로 사용자와 장바구니의 일대일 매핑을 통해 구현하였다.

 

1. User

@Entity
public class User extends UserBase {
    //... 생략

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CART_ID")
    private Cart cart;

}

OneToOne 매핑을 처리할때 요구사항에 따라 FetchType을 LAZY로 변경해줘야 한다. 해당 프로젝트에서는 USER정보를 불러올때 CART에 대한 정보까지 함께 가져올 필요가 없기 떄문에 LAZY로 설정했다.

(XXToOne의 경우 FetchType.EAGER가 JPA가 제공하는 기본 스펙이기 때문에 상황에 따라 프록시 객체를 가져오도록 LAZY로 설정을 변경해야 한다.)

 

2. Cart와 Product

다음으로 Cart(위시리스트)와 Product(상품)의 연관관계를 고민해볼 차례다.

처음에는 Cart와 Product의 관계를 1:N(일대다)로 놓고 구현을 진행했었다. 구현 내용은 아래와 같다.

@Entity
public class Cart {

    //...생략

    @OneToMany
    @JoinColumn(name = "CART_ID")
    private Set<Product> wishList = new HashSet<>();

} 
@Entity
public class Product extends BaseTimeEntity {

    //...생략

    @ManyToOne
    @JoinColumn(name = "CART_ID", insertable = false, updatable = false)
    private Cart cart;
}

하지만 Cart와 Product의 연관관계를 1:N으로 설정했을때 문제점이 발견되었다.

바로 아래와 같이 Product에 특정 CART_ID가 종속되는 문제점이 발생했다.

만약 이렇게 구현하면 User AProduct A라는 상품을 카트에 추가하고, 이후 User BProduct A라는 상품을 추가하게 될 경우 User A는 카트에서 Product A 상품을 조회할 수 없게 된다. 따라서 1:N의 관계로는 원하는 장바구니 기능을 구현해낼 수 없었다.


위시리스트와 상품의 연관관계를 다시 설정해야 했다.

Cart(위시리스트)에는 여러 Product(상품)을 담을 수 있고, Product(상품) 또한 여러 Cart(위시리스트)에 담길 수 있다. 따라서 다대다(N:M) 관계로 설정하여 구현을 진행했다.

 

JPA에서 ManyToMany 관계를 공식 스펙으로 제공하고 있지만 해당 방식은 실무에서는 거의 사용하지 않는 방식이라고 한다. 따라서 다대다 매핑을 사용할 경우 CartProduct라는 중간 테이블을 생성하여 일대다 , 다대일로 풀어서 사용해야 한다.

 

Cart.java

@Entity
public class Cart {

    //...생략


    @OneToMany(mappedBy = "cart")
    private Set<CartProduct> wishList = new HashSet<>();
    }
}

CartProduct.java

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CartProduct {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "CART_ID")
    private Cart cart;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;

}

이렇게 CartProduct라는 테이블을 생성함으로써 사용자의 Cart(위시리스트)에는 Product가 직접 등록되는 것이 아닌 CartProduct가 등록되는 것이다. 따라서 Product가 특정 CART에 종속되는 문제를 해결할 수 있게 되었다.

 

Product.java

Product의 입장에서 CartProduct를 조회할 일이 없기 때문에 따로 양방향 매핑을 설정할 필요가 없다.

 


정리

해당 프로젝트는 장바구니를 구현하는 여러가지 방법 중 사용자와 카트를 일대일 관계로, 카트와 상품을 다대다의 관계로 바라보고 구현하였다. 처음에는 정말 간단하게 구현할 수 다고 생각했지만 JPA 연관관계 매핑에대한 이해도가 부족하여 생각보다 시간이 오래걸렸다. 여기서 구현한 방법이 반드시 정답은 아니다. 간단하게 사용자 도메인에 상품 List 또는 Set을 추가하여 값타입 컬렉션으로 추가하는 방식으로도 구현이 가능하다. 본인의 판단 하에 최적의 방법을 선택하여 구현하도록 하자.

 

또한 본 프로젝트는 일반 쇼핑몰과 달리 신발 경매를 서비스를 주 목적으로 하기 때문에 위시리스트에 상품을 중복되게 추가하여 수량을 늘리는 시스템은 의미가 없다고 생각하여 구현하지 않았다. 만약 본인이 진행하는 프로젝트에 카트에 저장된 상품마다 수량이 적용되어야 한다면 CartProduct 에 Count 필드를 생성하고 그에 알맞는 비즈니스 로직을 구현하도록 하자.