[Spring Boot2][2] 2. 도메인 분석 설계

sorzzzzy·2021년 10월 10일
0

Spring Boot - RoadMap 2

목록 보기
13/26
post-thumbnail

🏷 요구사항 분석

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.memberORDERS.MEMBER_ID 외래 키와 매핑한다.

2️⃣ 주문상품과 주문

  • 다대일 양방향 관계이다.
  • 외래 키가 주문상품에 있으므로 주문상품이 연관관계의 주인이다.
  • 그러므로 OrderItem.orderORDER_ITEM.ORDER_ID 외래 키와 매핑한다.

3️⃣ 주문상품과 상품

  • 다대일 단방향 관계이다.
  • OrderItem.itemORDER_ITEM.ITEM_ID 외래 키와 매핑한다.

4️⃣ 주문과 배송
= 일대일 양방향 관계이다.

  • Order.deliveryORDERS.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 문제에서 안전!


profile
Backend Developer

0개의 댓글