도메인 분석 설계

김슬기·2022년 10월 7일
0

요구사항 분석

  • API설계를 위해 필요한 기능들을 분석해야한다.
  • 회원기능
    • 회원 가입
    • 회원 목록 조회
  • 상품기능
    • 상품 등록
    • 상품 수정
    • 상품 목록 조회
  • 주문 기능
    • 상품 주문
    • 주문 내역조회
    • 주문 취소
  • 세부사항
    • 상품은 재고관리 필요
    • 상품의 종류는 도서, 음반,영화 있음
    • 상품을 카테고리 별로 구분해야함
    • 상품 주문시 배송 정보 입력할 수 있게 하기

도메인 모델과 테이블 설계

  • 다대다 관계같은 경우에는 관계형 DB에서는 물론이고 엔티티에도 거의 사용하지않음

    • 관리어렵..
    • 그래서 이 다대다관계를 1:N ,N:1두개를 혼합하여 풀어서 작성한다
    • 그것이 주문상품…
  • 여기서 *은 N, 즉, 다 를 의미

  • 주소(Address): 값 타입(임베디드 타입)이다. 회원과 배송(Delivery)에서 사용한다.

    • 주소에 필요한 정보들을 가지고있는 내장타입
  • OrderItem은 다대다관계를 풀어내는 용도

    • 하지만 사실 그냥 다대다로 써도 되지만 추가적으로 필요한 정보들을 기입하기 위해 필수적
      • 가격정보
      • 수량정보
  • 그래서 Order가 OrderItem을 리스트로 가지고있다.

  • Delivery에서의 주소는 배송지 정보 (Member의 주소와는 다를수도 있으므로)

  • Category-Item은 ManyToMany를 설정할 수 있게함 (예제이므로 설명하기위해)

    • 실무에선 ManyToMany, 다대다는 쓰지않는다.
    • 일대다,다대일 이렇게 풀어서 사용해야한다.
    • 아래에서 구현할떈 그렇게 함
  • 주문, 멤버 사이 양방향으로 엔티티를 갖는데 이건 좋지않다.
    - 단방향으로 설계해야함.
    - 예제니깐 넘어가쟈
    - 사실해결하려면 Order안에 멤버를 냄기고 멤버에서 orders지우기
    - 접근할때 Order안에서 멤버를 타고 접근 (전부 탐색)하면될듯?

  • 아이템은 그냥 한개에 몰아서 표현한것 뿐

    • DTYPE으로 구분
  • 카테고리도 다대다 풀어서 쓰기

    • 객체는 다대다관계가 가능하다.
    • 하지만 관계혀형데이터베이스에선 불가능
  • 연관관계 매핑 분석

    • fk가 있는곳을 연관관계의 주인으로 정하는것이 좋다.
      • 여기서 주인이란 단순히 외래 키를 누가 관리하냐이다.
      • 일대다 관계에서 항상 다쪽에 외래 키가 있다.
      • 그래서 ex) Orders.member_id로 접근
    • 카테고리와 상품을 @ManyToMany를 써서 매핑하는데 실무에선 쓰지말기. 지금은 예제니깐~

엔티티 클래스 개발

  • 예제에서는 설명을 쉽게하기 위해 엔티티 클래스에 Getter, Setter를 모두 열고, 최대한 단순하게 설계
  • 실무에서는 가급적 Getter는 열어두고, Setter는 꼭 필요한 경우에만 사용하는 것을 추천
    • Setter를 열어두면 값을 바꾸게 되는경우가 생길 수있으므로 최대한 닫아놓는게 좋다.
  • 도메인 패키지 안에 엔티티 개발
  • 회원 엔티티
package jpabook.jpashop.domain;

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;

    @OneToMany(mappedBy = "member") //양방향이므로 연관관계의 주인이 아님을 설정 (order테이블안에 있는 member속성으로 접근)
    private List<Order> orders = new ArrayList<>();
}
  • 엔티티의 식별자는 id 를 사용하고 PK 컬럼명은 member_id 를 사용했다.
  • 엔티티는 타입(여기서는Member )이 있으므로 id 필드만으로 쉽게 구분할 수 있다.
  • 테이블은 타입이 없으므로 구분이 어렵다.
  • 그리고 테이블은 관례상 테이블명 + id 를 많이 사용한다. 참고로 객체에서 id 대신에 memberId 를 사용해도 ok
  • Address
package jpabook.jpashop.domain;
import lombok.Getter;
import javax.persistence.Embeddable;
@Embeddable
@Getter
public class Address {
    private String city;
    private String street;
    private String zipcode;
    protected Address() { // jpa 스펙 상 기본생성자가 있긴해야함 프로텍티드인 이유는 상속할일이 없지만 그냥 다들 암묵적으로 사용(안전)
    }
    public Address(String city, String street, String zipcode) { //세터대신 사용  (값을 변경하면 안되기때문에 초기값을 그대로들고가도록 설계)
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}
  • Order
package jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders") //테이블 이름을 바꿔줌
@Getter @Setter
public class Order {
    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "member_id") //매핑에 사용할 이름 지정
    //양방향이므로 연관관계의 주인으로 설정 (아무설정 안한것이 주인)
    private Member member;

		@OneToMany(mappedBy = "order", cascade = CascadeType.ALL) //order이 저장 혹은 삭제될때 orderlist에 있는 아이템들도 같이 진행
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne
    @JoinColumn(name = "delivery_id",cascade = CascadeType.ALL)
    private Delivery delivery;

    private LocalDateTime orderDate; // 시간 분까지 다있음 주문시간
    @Enumerated(EnumType.STRING)
    private OrderStatus status; // 주문상태 ORDER, CANCEL
}
  • OrderItem
package jpabook.jpashop.domain;

import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

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

    @ManyToOne
    @JoinColumn(name="item_id")
    private Item item;
    @ManyToOne
    @JoinColumn(name = "order_id")
    private Order order;//하나의 주문에 여러개의 아이음

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

}
  • OrderStatus
package jpabook.jpashop.domain;

public enum OrderStatus {
ORDER,CANCEL
}
  • Item
package jpabook.jpashop.domain.item;

import jpabook.jpashop.domain.Category;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)//상속 전략을 잡아줘야함 (Joined 는 가장 정교하게,SINGLE_TABLE은 한테이블에 다 떄려박음
@DiscriminatorColumn(name = "dtype") //구분할때 예를들어 book이면 어쩔거냐~? 그내용을 상속받는 각각의 클래스에 적는다 @DiscriminatorValue로 받고
@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<>();

}
  • Album
package jpabook.jpashop.domain.item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@DiscriminatorValue("A")
@Getter
@Setter
public class Album extends Item{  //extend => 상속
    @Id
    @GeneratedValue
    @Column(name= "member_id")
    private String artist;
    private String etc;
}
  • Book
package jpabook.jpashop.domain.item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@DiscriminatorValue("B")
@Getter
@Setter
public class Book extends Item{
    @Id
    @GeneratedValue
    @Column(name= "member_id")
    private String author;
    private String isbn;
}
  • Movie
package jpabook.jpashop.domain.item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@DiscriminatorValue("M")
@Getter
@Setter
public class Movie extends Item{

    @Id
    @GeneratedValue
    @Column(name= "member_id")
    private String directer;
    private String actor;
}
  • Delivery
package jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

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

    @OneToOne(mappedBy = "delivery")
    private Order order;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)//절대  EnumType.ORDINARY로 쓰지않기//중간에 하나 다른값이 들어온다면 순서가 밀려서 망해버린다.
    private DeliveryStatus status; //READY, COMP

}
  • DeliveryStatus
package jpabook.jpashop.domain;

public enum DeliveryStatus {
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;
@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<>();//자식은 여러갤 가질수 있다. (하위계층 카테고리는 많을수있으므로)
}

엔티티 설계시 주의점

  • Setter가급적 쓰지말기
    • 변경포인트가 너무 많아지면 유지보수 어려움
  • 모든연관관계는 지연로딩으로 설정하기
    • 즉시로딩(Eager)은 예측어렵, 어떤 SQL실행될지 추적어렵,특히 JPQL실행할떄 N+1문제가 자주 발생
    • 모든 연관관계를 지연로딩(LAZY)로 설정해주기
    • 연관된 엔티티를 함께 조회해야하면, fetch join 또는 엔티티 그래프 기능 사용
    • XtoOne관계는 기본이 EAGER 상태이므로 무조건 바꿔줘야함
      • XtoMANY는 LAZY임
    • fetch = FetchType.LAZY이것을 애노테이션 옆에 괄호로 넣기
  • 컬렉션(음…테이블배열?)은 필드에서 초기화
    • 선언과 초기화를 나누지않는다.( 바로초기화하는것이 안전
      • 널문제에서 안전
      • 하이버네이트는 엔티티를 영속화(persist == 저장) 할 때, 컬랙션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다.
      • 만약 getOrders() 처럼 임의의 메서드에서 컬력션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문제가 발생할 수 있다.
      • 필드레벨에서 생성하는 것이 가장 안전하고, 코드도 간결하다.
  • 테이블명 컬럼명 설정전략
    • 스프링 부트 신규 설정 (엔티티(필드) 테이블(컬럼))
      • 띄어쓰기 →언더스코어( _ )
      • .(점) → 언더스코어
      • 대문자 → 소문자
    • 논리명(명시적으로 테이블, 컬럼명을지정해주지 않을시 적용하는 방법),물리명 적용 (모든 논리명에 다 적용되는 방법) 은 한번 찾아보기
  • CASCADE
    • 한 엔티티가 저장이나 삭제될 때 연관된 엔티티를 어떻게 처리할 지에 대한 옵션
    • cascade = CASCADETYPE.ALL하면연관관계의 주인의 처리를 따라감
  • 연관관계 메서드(양방향일떄, 단방향일떈 ㄴㄴ)
    • 두 엔티티를 생성하고 저장할때 서로 따로 생성했다면, 그 둘의 관계를 엮어줘야한다.

    • (Order클래스)만약 이 코드의 member.getOrders().add(this); 이 부분이없다면

      public void setMember(Member member){
          this.member = member;
          member.getOrders().add(this);
      }
    • 여기서 이렇게 두개 다 연결해주어ㅑ한다. 하지만 위의 코드가 있다면 member.getOrders().add(order);이녀석은 생략해도된다.

      public static void main(String[] args){
          Member member = new Member();
          Order order = new Order();
      
          member.getOrders().add(order);
          order.setMember(member);
      }
    • 나머지 연관관계도 다 설정 (양뱡향만~~~)

profile
낭만그리고김슬기

0개의 댓글