해당 포스팅에서는 상품 구매 시 옵션(사이즈, 색상)을 선택하여 주문할 수 있도록 기능을 구현한 과정에 대해 간략하게 정리해 보고자 한다.
상품의 옵션 기능을 개발하기 위해서는 가장 먼저 고려할 사항은 테이블 설계다.
필요한 테이블은 상품, 사이즈, 그리고 재고 테이블이다.
이전에는 상품 테이블에서 상품의 재고 수를 관리했지만 옵션을 추가하면서 각 옵션별 재고 수가 다르기 때문에 별도의 재고 테이블이 필요하게 되었다.
먼저, 상품 테이블과 옵션 테이블의 관계를 고려해 보면, 하나의 상품에는 여러 옵션이 있을 수 있고, 하나의 옵션에는 여러 상품이 포함될 수 있다. 이러한 다대다 관계를 표현하기 위해 중간 테이블을 사용하여 관계를 설정했다.
재고 테이블의 경우에는 하나의 상품에는 여러 옵션별 재고가 존재하기 때문에 재고 테이블을 연관 관계의 주인으로 하여 상품 테이블과 다대일로 매핑했다.
관계형 데이터베이스는 설계 원칙과 데이터 무결성을 유지하기 위해 다대다 관계를 직접 지원하지 않는다. 관계형 DB는 테이블 간의 관계를 명확하게 정의하고, 각 테이블은 데이터의 중복을 최소화하면서 정규화된 형태로 저장된다. 다대다 관계는 이러한 정규화된 형태에 부합하지 않기 때문에 직접 지원되지 않는다. 대신, 중간 테이블을 통해 관계를 나타내고, 각 테이블 간의 관계를 일대다 혹은 다대일 관계로 나누어 데이터를 저장하게 된다.
반면 JPA에서는 다대다 관계를 직접 지원하며, 매핑을 통해 객체 간의 다대다 관계를 표현할 수 있다. 하지만 복잡성과 유지보수의 어려움으로 인해 실무에서는 잘 사용되지 않는다고 한다.
따라서 중간 테이블을 사용하는 방식으로 테이블을 설계했다.
색상 테이블, 사이즈 테이블, 상품/색상 중간 테이블, 상품/사이즈의 중간 테이블, 그리고 재고 테이블 등 총 5개의 테이블이 새롭게 생성되었다.
@Entity
public class Product {
'''
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ProductColor> colors = new ArrayList<>();
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ProductSize> sizes = new ArrayList<>();
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Inventory> inventories = new ArrayList<>();
}
@Entity
public class Color {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "color_id")
private Long id;
private String name;
private String code;
}
@Entity
public class Size {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "size_id")
private Long id;
private String name;
}
@Entity
public class ProductColor {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_color_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "color_id")
private Color color;
}
@Entity
public class ProductSize {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_size_id")
private Long id;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
@ManyToOne
@JoinColumn(name = "size_id")
private Size size;
}
@Entity
public class Inventory {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "inventory_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "size_id")
private Size size;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "color_id")
private Color color;
private int quantity;
}