[실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 도메인 분석 설계

이재표·2023년 10월 28일
0

요구사항 분석

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

도메인 모델과 테이블 설계


관계

  • 회원과 주문은 일대다 양방향 관계이다.
  • 주문과 상품은 다대다 관계이기 때문에 중간테이블을 임의로 만들어줬다.
  • 상품과 카테고리도 다대다 관계로 중간테이블을 만들어줘야하지만, jpa의 모든 관계를 보기위해 중간테이블을 만들지 않았다.(물론 DB테이블이 만들어질때 중간테이블이 만들어진다.)
  • 주문과 배송은 일대일 양방향 관계이다.

    객체지향에서 실수하는것이 멤버와 오더가 있으면 회원이 주문을 하니까 회원에 오더 리스트를 두고 쓰면 되겠다같이 회원에 오더가 속하는 것처럼 생각하는데, 시스템에서 멤버랑 오더를 동급으로 두고 생각하는 것이 맞다. 회원을 통해 주문이 아닌 주문을 생성할때 회원이 필요한것뿐, 주문 내역을 볼때도 회원을 통해 오더를 불러오는것이 아닌 오더를 불러올때 멤버를 필터링 조건으로 넣어주는 것이다.

엔티티 클래스 개발

멤버와 주문관계

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

    private String username;
    @Embedded
    private Address address;
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}

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

멤버와 주문은 다대일 양방향 관계이다. 객체입장에서는 변경되야할 필드가 두가지 이지만 DB는 FK하나만 변경되어야하기때문에 두 연관관계에서 주인(FK)를 정해줘야하고, 무조건적으로 다쪽이 주인이 되어야한다. 때문에 Order엔티티에 @JoinColumn을 통해 주인을 지정해주고 Member에는 mappedBy를 통해 변경을 받는 객체라는 것을 적어준다.

주인 엔티티에서만 값을 변경이 가능하다. 주인이 아닌 엔티티에서는 조회만 가능하다.

@Getter @Setter
@Entity
public class Member {
    ...
    @Embedded
    private Address address;
    ...
}
-----------------------------
@Embeddable
@Getter
public class Address {
    private String city;
    private String strret;
    private String zipcode;
}

임베디드 타입을 통해 같은 성격의 필드를 하나의 클래스로 묶어 사용할수 있다.

Embedded 값타입은 변경이 불가능해야한다. 따라서 @Setter를 제거하고, 인스턴스 생성시 모든 값을 초기화하여 변경 불가능하게 만들어주자!! 이때 JPA스펙상 엔티티나 임베디드 타입은 기본 생성자를 만들어줘야하는데, 이때 public이나 protected로 설정해주자! 이때 생성자를 막 생성하여 인스턴스를 만드는 경우가 있을수도 있는데, 따로 인스턴스 생성 메서드가 있거나 제약해줘야할시 public보단 protected로 설정하여 제약을 걸어주자!!
==> JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수
있도록 지원해야 하기 때문이다.

enum타입

enum타입을 통해 특정 필드의 상태를 여러개로 열거할수 있는데, 이때 EnumType.STRING으로 옵션을 걸어주지 않는다면 추후 ENUM에 다른 상태가 추가된 경우 장애가 날수 있기때문에 무조건 EnumType.STRING으로 옵션을 걸어줘야한다.

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

    @OneToOne
    private Order order;
    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private DeliveryStatus status;
}
-----------------------------
public enum DeliveryStatus {
    READY,COMP
}

테이블 전략

특정 테이블을 상속받아 사용하는 경우 다음과 같이 @Inheritance 어노테이션을 통해 어느 테이블전략을 사용할지 명시해준다. 여기서는 싱글테이블 전략을 이용한다. @DiscriminatorColumn을 통해 테이블의 타입을 나눌때 어떻게 나눌지를 명시해주는데, 이때 dtype이라는 필드를 생성하여 해당 필드를 통해 테이블을 구분하는데, Book의 경우 dtype이 B이면 Book이라는 것을 구분하도록 하였다.

@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<>();
}
-------------------------------------
@Entity
@DiscriminatorValue("B")
@Getter
@Setter
public class Book extends Item{
    private String author;
    private String isbn;
}

자기자신과의 연관관계

자기 자신 엔티티만을 가지고 부모와 자식을 나타내야하는 경우도 있을수 있다. 예로들어 카테고리 안에 카테고리가 속할수 있기 때문이다. 이때 단순히 위의 카테고리와 카테고리 리스트를 선언하여 양방향을 걸어주면된다.

public class Category {
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Category parent;

    @OneToMany(mappedBy = "parent")
    private List<Category> child = new ArrayList<>();

다대다 중간테이블

다대다 관계에서는 중간 테이블을 임의로 만들어주지 않으면 DB생성시에 중간테이블을 만들기 때문에 설정해주는 것이 필요하다. 이때 @JoinTable을 통해 중간테이블에 대해 설정해준다. 두개의 fk가 들어가있기때문에 fk에 대한 두 옵션이 들어간다.

다대다 관계의 경우 자동으로 만들어지는 테이블 외의 다른 필드를 넣어줄수도, 테이블을 튜닝할수도 없기 때문에 지양해야한다.

@Entity
@Getter
@Setter
public class Category {
    ...
    @ManyToMany
    @JoinTable(name = "category_item",
            joinColumns = @JoinColumn(name = "category_id"),
            inverseJoinColumns = @JoinColumn(name = "item_id"))
    private List<Item> items = new ArrayList<>();
	...
}

@Getter?? @Setter??

Getter와 Setter 모두 사용하지 않는것이 가장 이상적이지만 엔티티의 데이터는 조회할 일이 너무 많으므로, Getter의 경우 모두 열어두는 것이 편리하다. Getter의 경우 단순 조회메서드이기 때문에 호출 하는 것 만으로 어떤 일이 발생하지는 않지만 Setter를 호출하면 데이터가 변한다. Setter를 막 열어두면 엔티티의 값이 변경되었지만 왜 변경되는지 추적하기 점점 힘들어진다. 그래서 엔티티를 변경할 때는 Setter 대신에 변경 지점이 명확하도록 변경을 위한 비즈니스 메서드를 별도로 제공해야 한다.

엔티티 설계시 주의점

모든 연관관계는 지연로딩으로 설정!

즉시로딩( EAGER )은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다. 특히 JPQL을 실행할 때 N+1
문제가 자주 발생한다. 실무에서 모든 연관관계는 지연로딩( LAZY )으로 설정해야 한다. 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용한다. @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 한
다.

컬렉션은 필드에서 초기화 하자.

List<String> list=new ArrayList<>();

컬렉션은 필드에서 바로 초기화 하는 것이 null문제에서 안전하다.

0개의 댓글