IT recording...

[스프링 JPA1] 1. 요구사항 분석 및 도메인 셜계 본문

Spring

[스프링 JPA1] 1. 요구사항 분석 및 도메인 셜계

I-one 2022. 2. 7. 16:32

[원문링크]

https://adorable-aspen-d23.notion.site/JPA1-1-2994f80a6bec41d2a82c48c787786bac

 

[스프링 JPA1] 1. 요구사항 분석 및 도메인 셜계

1. 요구사항 분석

adorable-aspen-d23.notion.site

김영한님의 [실전!스프링부트와JPA활용1 - 웹 어플리케이션개발] 강의를 듣고 작성한 글입니다.

 

1. 요구사항 분석

  • 회원 기능
    • 회원 등록
    • 회원 조회
  • 주문 기능
    • 상품 주문
    • 주문 내역 조회
    • 주문 취소
  • 상품 기능
    • 상품 등록
    • 상품 수정
    • 상품 조회
  • 기타 요구사항
    • 상품은 재고 관리가 필요하다.
    • 상품의 종류는 도서, 음반, 영화가 있다.
    • 상품을 카테고리로 구분할 수 있다.
    • 상품 주문시 배송 정보를 입력할 수 있다.

2. 설계

2-1. 도메인 모델 설계

  • 요구사항 분석을 바탕으로 어떤 도메인이 필요한지 살핀다.
  • 각 도메인 간 연간관계를 파악한다.
  • 회원-주문(1:N)
    • 한 회원은 여러개의 주문을 가질 수 있다.
    • 주문은 하나의 회원에 매핑된다.
  • 주문-배송(1:1)
    • 한 주문은 하나의 배송 정보를 가진다.
  • 주문-상품(M;N)
    • 하나의 주문에서 여러 상품을 선택할 수 있다.
    • 한 상품은 여러개의 주문에 매핑될 수 있다.
    • ⇒ 주문상품이라는 엔티티를 추가하여 1:N, N:1관계로 풀어낸다.

2-2. 엔티티 설계 (객체)

  • 도메인 모델과 유사한 모양을 갖는다.
  • Member
    • 임베디드 타입인 Address를 갖는다.
    • List<Order> 를 갖는다. (양방향 관계이기 때문에 표시하였지만 실무에서는 양방향 관계를 많이 사용하지 않는다. 회원이 주문을 갖는 것이 아니라, 주문이 회원을 갖는 것으로 생각하자)
  • Order
    • Enum을 활용하여 status를 표현한다.

2-3. 테이블 설계 (RDBMS)

: 설계된 객체를 토대로 RDBMS는 어떻게 설계되는지 살펴보자

  • 1:N 관계에서 항상 다 쪽에 외래키가 존재한다.
  • 외래 키가 있는 곳을 연관관계의 주인으로 정하자.
연관관계의 주인은 단순히 외래 키를 누가 관리하냐의 문제이지, 비즈니스상 우위에 있는 것이 아니다.
자동차와 바퀴가 존재할 때 (1:N) 외래키는 바퀴쪽에 존재하고, 이에 따라 바퀴를 연관관계의 주인으로 설정할 수 있다.

자동차를 연관관계의 주인으로 정하게 되면, 자동차가 관리하지 않는 바퀴 테이블의 외래 키 값이 업데이트 되므로 
관리와 유지보수가 어렵고, 추가적으로 별도의 업데이트 쿼리가 발생하는 성능 문제도 발생한다.

3. 엔티티 클래스 개발

  • 주의
    • Getter만 열고, Setter는 가급적 열지 말자
      • Setter를 막 열어두면 가까운 미래에 엔티티가 도대체 어디서 왜 변경되는지 한참 찾고 있어야 한다. 따라서 변경 지점이 명확하도록 , 변경을 위한 비즈니스 메서드를 별도로 제공해야 한다.

3-1. Member

@Entity
@Getter @Setter
public class Member {
  @Id @GeneratedValue
  @Column(name = "member_id")
  private Long id;

  private String username;

  @Embedded //내장 타입 사용
  private Address address;

  @OneToMany(mappedBy = "member") //회원과 주문은 1대다관계 //나는 연관관계 거울이에요~!  //Order테이블에 있는 member 필드에 의해 매핑된거야.
  //읽기전용이 됨. 여기다가ㅏ 세팅해도 세팅안됨, ORDER의 member에 세팅해야 함
  // mappedBy : 양방향 관계에서 관계의 주인을 설정해 주는 것, order테이블에 있는 member필드에 의해 매핑된거야 = 주인이 아니야
  private List<Order> orders = new ArrayList<>();

  public Long getId() {
      return id;
  }
}

기본 id

  • @Id
  • @GeneratedValue → Sequential하게 증가한다.
  • @Column(name = “xxx_id”) → rdbms에서 사용할 컬럼 이름을 등록한다.
    • 엔티티의 식별자로는 id를 사용해도 되지만, (→ member.id처럼 사용 가능)
    • 테이블은 타입이 없으므로 구분이 어렵다. 따라서 member_id 와 같이 명시적으로 적어준다.
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;

내장타입 (Value)

**@Embedded** //내장 타입 사용
private Address address;
**@Embeddable** //jpa의 내장타입, 어디엔가 내장이 될 수 있다.
@Getter
public class Address {
    private String city;
    private String street;
    private String zipcode;

    protected Address(){ //JPA스펙 상 protected로 만들자
    }

    public Address(String city, String street, String zipcode) { //값타입 생성자에서만 변경 가능하게 하기
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}

연관관계 매핑 - 주인 지정하기

  • 1:N의 N쪽이 보통 연관관계의 주인이다.
  • 주인 → @JoinColumn(name = “slave_id”)
  • slave → @OneToMany(mappedBy = “owner”)
    • 읽기 전용이 된다. (나는 연관관계 거울이에요~!)
<--Member-->
@OneToMany(mappedBy = "member") //읽기전용 , Order의 member 필드에 연관된다. 
private List<Order> orders = new ArrayList<>();

<--Order-->
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id") //member 테이블의 member_id와 조인한다.
private Member member; //주문 회원

3-2. Order

@Entity
@Table(name="orders") //테이블 이름 orders로 설정 (예약어때문)
@Getter @Setter
public class Order {
    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY) //order와 member는 다대1 관계
    @JoinColumn(name="member_id") //forign key 지정
    private Member member;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id") //order-delivery에서 order를 주인으로 봄
    private Delivery delivery;

    private LocalDateTime orderDate; //주문시간

    @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);
    }
}

@xxxToOne 에서의 지연 로딩 설정

  • OneToOne, ManyToOne
    • 기본이 즉시로딩(EAGER)이므로 지연 로딩으로 설정해야 한다.
  • 즉시로딩은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다.
  • JPQL실행 시 N+1 문제가 발생한다.
  • 실무에서는 모든 xxxToOne 관계를 LAZY 지연 로딩으로 설정하자
**@ManyToOne(fetch = FetchType.LAZY)** //order와 member는 다대1 관계
@JoinColumn(name="member_id") //forign key 지정
private Member member;

연관관계 메서드

  • 양방향 연관관계를 가질때, 객체에서는 양쪽 객체에 모두 데이터를 넣는다. (db에서는 면 되지만)
  • 둘 다 업데이트를 해야 하는데 어느 하나를 잊을 수도 있으므로 묶어서 관리하는 연관관계 메서드를 만든다.
  • 연관관계의 주도권을 가지고 있는 쪽이 보유한다.
//==연관관계 메서드==//
public void setMember(Member member){
    this.member = member; //order꺼
    member.getOrders().add(this); //member꺼
}

public void addOrderItem(OrderItem orderItem){
    orderItems.add(orderItem); //order꺼
    orderItem.setOrder(this); //orderItem꺼
}

public void setDelivery(Delivery delivery){
    this.delivery = delivery; //order꺼
    delivery.setOrder(this); //delivery꺼
}

Enum 클래스

  • 항상 string으로 지정해주어야 한다.
@Enumerated(EnumType.STRING)
private OrderStatus status; //주문상태 [ORDER, CANCEL]
public enum OrderStatus {
 ORDER, CANCEL
}

3-3. OrderItem

@Entity
@Getter @Setter
public class OrderItem {
    @Id @GeneratedValue
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne(fetch = FetchType.LAZY) //orderItem이 다 관계
    @JoinColumn(name = "order_id")
    private Order order;

    private int oderPrice; //주문 당시 가격
    private int count; //주문 당시 수량
}

3-4. Item

@Entity
**@Inheritance(strategy = InheritanceType.SINGLE_TABLE)** //상속할때 //Joined,tableper,singleTable이 있음
**@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<>();

}
@Entity
@Getter
@Setter
**@DiscriminatorValue("B")**
public class Book extends Item{
    private String author;
    private String isbn;
}

3-5. Delivery

@Entity
@Getter
@Setter
public class Delivery {
  @Id
  @GeneratedValue
  @Column(name = "delivery_id")
  private Long id;

  @OneToOne(fetch = FetchType.LAZY, mappedBy = "delivery")
  private Order order;

  @Embedded
  private Address address;

  @Enumerated(EnumType.STRING) //enum쓸 때 string을 써야 순서가 바뀌더라도 꼬이지 않음
  private DeliveryStatus status; //READY, COMP
}

3-6. Category

@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 = FetchType.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를 사용하지 말자
    • 중간 테이블에 컬럼을 추가할 수 없고, 세밀하게 쿼리 실행이 어렵다.
  • 대신 중간 엔티티(CategoryItem)을 만들고, @ManyToOne, @OneToMany 로 매핑해서 사용하자.
  • ex) 주문 - 주문상품 - 상품

3-7. 기타 주의사항

  • 컬렉션은 필드에서 초기화 하자.
    • Null에서 안전할 수 있다.
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = **new ArrayList<>();**
Comments