[Spring Boot2][2] 2. 도메인 분석 설계
🏷 요구사항 분석
1️⃣ 회원 기능
- 회원 등록
- 회원 조회
2️⃣ 상품 기능
- 상품 등록
- 상품 수정
- 상품 조회
3️⃣ 주문 기능
- 상품 주문
- 주문 내역 조회
- 주문 취소
4️⃣ 기타 요구사항
- 상품은 재고 관리가 필요하다.
- 상품의 종류는 도서, 음반, 영화가 있다.
- 상품을 카테고리로 구분할 수 있다.
- 상품 주문시 배송 정보를 입력할 수 있다.
🏷 도메인 모델과 테이블 설계
✔️ 도메인 모델과 테이블 설계
1️⃣ 회원, 주문, 상품의 관계
- 회원은 여러 상품을 주문할 수 있다.
- 그리고 한 번 주문할 때 여러 상품을 선택할 수 있으므로 주문과 상품은 다대다 관계다.
- 하지만 이런 다대다 관계는 관계형 데이터베이스는 물론이고 엔티 티에서도 거의 사용하지 않는다.
- 따라서 그림처럼 주문상품이라는 엔티티를 추가해서 다대다 관계를 일대다, 다대일 관계로 풀어냈다😉
2️⃣ 상품 분류
- 상품은 도서, 음반, 영화로 구분되는데 상품이라는 공통 속성을 사용하므로 상속 구조로 표현했다.
✔️ 회원 엔티티 분석
1️⃣ 회원(Member)
- 이름과 임베디드 타입인 주소, 그리고 주문 리스트를 가진다.
2️⃣ 주문(Order)
- 한 번 주문시 여러 상품을 주문할 수 있으므로 주문과 주문상품은 일대다 관계이다.
- 주문은 상품을 주문한 회원과 배송 정보, 주문 날짜, 주문 상태를 가지고 있다.
주문 상태는 열거형을 사용했는데 주문,ㅍ취소를 표현할 수 있다.
3️⃣ 주문상품(OrderItem)
- 주문한 상품 정보와 주문 금액, 주문 수량 정보를 가지고 있다.
4️⃣ 상품(Item)
- 이름, 가격, 재고수량을 가지고 있다.
- 상품의 종류로는 도서, 음반, 영화가 있는데 각각은 사용하는 속성이 조금씩 다르다.
5️⃣ 배송(Delivery)
- 주문시 하나의 배송 정보를 생성하고 주문과 배송은 일대일 관계이다.
6️⃣ 카테고리(Category)
- 상품과 다대다 관계를 맺는다.
- parent, child로 부모, 자식 카테고리를 연결한다.
7️⃣ 주소(Address)
- 값 타입(임베디드 타입)이다.
- 회원과 배송에서 사용한다.
✔️ 회원 테이블 분석
1️⃣ MEMBER
- 회원 엔티티의 Address 임베디드 타입 정보가 회원 테이블에 그대로 들어갔다.
- 이것은 DELIVERY 테이블도 마찬가지다.
2️⃣ ITEM
- 앨범, 도서, 영화 타입을 통합해서 하나의 테이블로 만들었다(싱글 테이블 전략!).
- DTYPE 컬럼으로 타입을 구분한다.
✔️ 연관관계 매핑 분석
1️⃣ 회원과 주문
- 일대다, 다대일의 양방향 관계이다!
- 연관관계의 주인을 정해야 하는데, 외래 키가 있는 주문을 연관관계의 주인으로 정하는 것이 좋다!
(테이블에서는 **무조건 '다'에 외래키가 존재하게 되어있음!) - 그러므로
Order.member
를ORDERS.MEMBER_ID
외래 키와 매핑한다.
2️⃣ 주문상품과 주문
- 다대일 양방향 관계이다.
- 외래 키가 주문상품에 있으므로 주문상품이 연관관계의 주인이다.
- 그러므로
OrderItem.order
를ORDER_ITEM.ORDER_ID
외래 키와 매핑한다.
3️⃣ 주문상품과 상품
- 다대일 단방향 관계이다.
OrderItem.item
을ORDER_ITEM.ITEM_ID
외래 키와 매핑한다.
4️⃣ 주문과 배송
= 일대일 양방향 관계이다.
Order.delivery
를ORDERS.DELIVERY_ID
외래 키와 매핑한다.
5️⃣ 카테고리와 상품
@ManyToMany
를 사용해서 매핑한다.
(실무에서@ManyToMany
는 사용하지 말자. 여기서는 다대다 관계를 예제로 보여주기 위해 추가했을 뿐!)
🏷 엔티티 클래스 개발
📌 참고
- 예제에서는 설명을 쉽게하기 위해 엔티티 클래스에
Getter
,Setter
를 모두 열고, 최대한 단순하게 설계- 실무에서는 가급적
Getter
는 열어두고,Setter
는 꼭 필요한 경우에만 사용하는 것을 추천!!
✔️ Member
- 회원 엔티티
package jpabook.jpashop.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
@JsonIgnore
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
✔️ Order
- 주문 엔티티
package jpabook.jpashop.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.BatchSize;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import static javax.persistence.FetchType.*;
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
@JsonIgnore
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@JsonIgnore
@OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate; //주문시간
// Enum 타입은 @Enumerated(EnumType.STRING) 꼭 필요! -> 문자열로 출력
@Enumerated(EnumType.STRING)
private OrderStatus status; //주문상태 [ORDER, CANCEL]
// 양방향 일 때 -> 연관관계 편의 메서드
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery) {
this.delivery = delivery;
delivery.setOrder(this);
}
}
✔️ OrderItem
- 주문 상품 엔티티
package jpabook.jpashop.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jpabook.jpashop.domain.item.Item;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import static javax.persistence.FetchType.*;
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
@Id @GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "item_id")
private Item item;
@JsonIgnore
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "order_id")
private Order order;
private int orderPrice; //주문 가격
private int count; //주문 수량
//==생성 메서드==//
public static OrderItem createOrderItem(Item item, int orderPrice, int count) {
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPrice(orderPrice);
orderItem.setCount(count);
item.removeStock(count);
return orderItem;
}
}
✔️ Item
- 상품 엔티티
package jpabook.jpashop.domain.item;
import jpabook.jpashop.domain.Category;
import jpabook.jpashop.exception.NotEnoughStockException;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.BatchSize;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
// 싱글 테이블 전략
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
// 구별할 타입을 정해줌
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int stockQuantity;
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();
}
✔️ Delivery
- 배송 엔티티
package jpabook.jpashop.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import static javax.persistence.FetchType.*;
@Entity
@Getter @Setter
public class Delivery {
@Id @GeneratedValue
@Column(name = "delivery_id")
private Long id;
@JsonIgnore
@OneToOne(mappedBy = "delivery", fetch = LAZY)
private Order order;
@Embedded
private Address address;
// Enum 타입은 @Enumerated(EnumType.STRING) 꼭 필요! -> 문자열로 출력
@Enumerated(EnumType.STRING)
private DeliveryStatus status; //READY, COMP
}
✔️ Category
- 카테고리 엔티티
package jpabook.jpashop.domain;
import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
import static javax.persistence.FetchType.*;
@Entity
@Getter @Setter
public class Category {
@Id @GeneratedValue
@Column(name = "category_id")
private Long id;
private String name;
@ManyToMany
// 중간 테이블 역할
@JoinTable(name = "category_item",
joinColumns = @JoinColumn(name = "category_id"),
inverseJoinColumns = @JoinColumn(name = "item_id"))
private List<Item> items = new ArrayList<>();
// 내 부모 - 부모는 하나임
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
// 내 자식 - 자식은 여러명을 가질 수 있음
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
//==연관관계 메서드==//
public void addChildCategory(Category child) {
this.child.add(child);
child.setParent(this);
}
}
📌 실무에서는
@ManyToMany
를 사용하지 말자
@ManyToMany
는 편리한 것 같지만, 중간 테이블(CATEGORY_ITEM
)에 컬럼을 추가할 수 없고, 세밀하게 쿼리를 실행하기 어렵기 때문에 실무에서 사용하기에는 한계가 있다.- 중간 엔티티(
CategoryItem
)를 만들고@ManyToOne
,@OneToMany
로 매핑해서 사용하자!- 정리하면 다대다 매핑을 일대다, 다대일 매핑으로 풀어 내서 사용하자!!
✔️ Address
- 주소 값 타입
package jpabook.jpashop.domain;
import lombok.Getter;
import javax.persistence.Embeddable;
// JPA의 내장타입 : 어디든 내장될 수 있다는 뜻
@Embeddable
@Getter
public class Address {
private String city;
private String street;
private String zipcode;
protected Address() {
}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
}
📌 값 타입은 변경 불가능하게 설계해야 한다!
@Setter
를 제거하고, 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스를 만들자!- JPA 스펙상 엔티티나 임베디드 타입(
@Embeddable
)은 자바 기본 생성자(default constructor
)를public
또는protected
로 설정해야 한다.public
으로 두는것 보다는protected
로 설정하는 것이 그나마 더 안전하다!
🏷 엔티티 설계시 주의점
1️⃣ 엔티티에는 가급적 Setter를 사용하지 말자
Setter
가 모두 열려있다면, 변경 포인트가 너무 많아서 유지보수가 어렵다☹️- 나중에 리펙토링으로
Setter
제거하자!
2️⃣ 모든 연관관계는 지연로딩으로 설정하자
- 즉시로딩(
EAGER
)은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다.
특히 JPQL을 실행할 때 N+1 문제가 자주 발생한다. - 실무에서 모든 연관관계는 지연로딩(LAZY`)으로 설정해야 한다.
- 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용하자!
@XToOne(OneToOne, ManyToOne)
관계는 기본이 즉시 로딩 이므로 직접 지연 로딩으로 설정해야 한다!
3️⃣ 컬렉션은 필드에서 초기화하자
private List<Order> orders = new ArrayList<>();
- 컬렉션은 필드에서 바로 초기화 하는 것이 안전하다.
- null 문제에서 안전!
Author And Source
이 문제에 관하여([Spring Boot2][2] 2. 도메인 분석 설계), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@sorzzzzy/Spring-Boot22-2.-도메인-분석-설계저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)