프로그래밍/Spring

[Spring boot / JPA] 2. 도메인 분석 설계

daykim 2023. 6. 29. 17:14
 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강

www.inflearn.com

 

목차

  • 요구사항 분석
  • 도메인 모델과 테이블 설계
  • 엔티티 클래스 개발1
  • 엔티티 클래스 개발2
  • 엔티티 설계시 주의점

 

요구사항 분석


간단한 쇼핑몰을 만들것이다.

기능 목록

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

 

 

도메인 모델과 테이블 설계


  • 회원, 주문, 상품의 관계
    • 회원은 여러 상품을 주문할 수 있다.
    • 한 번 주문할 때 여러 상품을 선택할 수 있으므로 주문과 상품은 다대다 관계다.
    • 하지만, 이런 다대다 관계는 관계형 데이터베이스는 물론이고, 엔티티에서도 거의 사용하지 않는다.
      -> 운영에선 사용하면 안 된다! 기본편에 설명해주셨다 한다.
    • 다라서 그림처럼 엔티티를 추가해 다대다 관계를 일대다, 다대일 관계로 풀어냈다.
  • 상품 분류
    • 상품은 도서, 음반, 영화로 구분되는데, 상품이라는 공통 속성을 사용하므로, 상속 구조로 표현했다.

 

회원 엔티티 분석

  • 주문(Order)의 주문 상태(status)는 열거형으로 주문(ORDER), 취소(CANCEL)을 표현할 수 있다.
  • 상품(Item)의 종류는 도서, 음반, 영화가 있는데, 각각은 사용하는 속성이 다르다.
  • 카테고리(Category)는 계층구조로 parent, child로 부모, 자식 카테고리를 연결한다.
  • 참고
    실무에선 회원이 주문을 참조하지 않고, 주문이 회원을 참조하는 것으로 충분하다.
    여기선 일대다, 다대다 양방향 연관관계를 설명하기 위해 추가했다.
    member랑 order를 동급으로 생각해라. 주문을 생성할 때 회원이 필요하다고 생각해라.
    조회도 주문내역이 필요하면, order에서 필터링 조건에 member가 들어가는 식으루

 

회원 테이블 분석

  • 관계형 데이터베이스는 다대다 관계가 안 된다. 따라서 카테고리처럼 중간에 매핑 테이블을 둔다.
  • 회원 엔티티의 Address 임베디드 타입 정보가 회원 테이블에 그대로 들어갔다.
    + Delivery도 마찬가지다.
  • ITEM은 앨범, 도서, 영화 타입을 통합해 하나의 테이블로 만들었다.
    -> DTYPE 칼럼으로 타입 구분
  • ODERS인 것은, 데이터베이스가 order by를 예약어로 잡고 있는 경우가 많아 관례상 사용한다.

 

연관 관계 매핑 분석

  • 회원과 주문
    • 일대다, 다대일의 양방향 관계다.
    • 따라서 연관관계의 주인을 정해야하는데, 외래키가 있는 주문을 연관관계 주인으로 정하는 것이 좋다.
    • 그러므로 Order.member를 ORDERS.MEMBER_ID 외래키가 매핑한다.
  • 주문상품과 주문
    • 다대일 양방향 관계다.
    • 외래키가 주문상품에 있으므로, 주문상품이 연관관계의 주인이다.
    • 그러므로 OrderItem.order를 ORDER_ITEM.ORDER_ID 외래키와 매핑한다.
  • 주문 상품과 상품
    • 다대일 단방향 관계다.
    • OrderItem.item을 ORDER_ITEM.ITEM_ID 외래키와 매핑한다.
  • 주문과 배송
    • 일대일 양방향 관계다.
    • Order.delivery를 ORDERS.DELIVERY_ID 외래키와 매핑한다.
  • 카테고리와 상품
    • @ManyToMany를 사용하기 위해서 매핑한다.
    • (실무에선 @ManyToMany는 사용하지 말자. 여기서 다대다 관계를 예제로 보여주려 추가한것일 뿐이다.)

 

 

엔티티 클래스 개발1


  • 실무에선 가급적 Getter는 열어두고, Setter는 꼭 필요한 경우에만 사용하는 것을 추천한다.

 

회원 엔티티 / 주문 엔티티

  • @Embeddable : 어딘가 내장될 수 있다.
  • @Embedded : 내장타입이다.
  • @ManyToOne / @OneToMany
  • @JoinColumn
  • Member와 Order는 양방향 관계다.
    • 9:30
    • 멤버는 Orders를 리스트로 가지고 있고, 오더도 멤버를 가지고 있다. -> 양방향 참조
    • 그럼 JPA는 어디서 값을 확인해야 하는가? 혼란이 온다.
    • JPA에선 둘 중 하나만 하도록 약속했는데 ,연관관계 주인이 있는 곳이다.
    • 연관관계 주인은 FK가 가까운 곳으로 한다. 이 경우 Order의 member다.

 

주문상품 엔티티

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
  • 상속관계 전략
  • SINGLE_TABLE : 한 테이블에 모두 하겠다. -> 테이블 분석에  ITEM 테이블
@Enumerated(EnumType.STRING)
  • EnumType.ORDINAL 로 하게되면 숫자로 정의되는데, 중간에 값을 투입하면, 처음과 정의가 달라진다.
  • 따라서 STRING으로 설정해야 한다.

 

Order와 Delivery

  • 1대 1 관계이므로, FK를 어디에 둬도 상관없다.
  • 강사님은 주로 Access를 많이 하는 곳에 둔다.
  • 따라서 연관관계 주인은 FK가 가까운 Order에 잡으면 된다.

 

 

엔티티 클래스 개발2


카테고리 엔티티

  • 실무에선 @ManyToMany를 사용하지 말자.
    편리한 것 같지만, 중간 테이블에 컬럼을 추가할 수 없고, 세밀하게 쿼리를 실행하기 어렵다.
    따라서 실무에서 사용하기에는 한계가 있다.
  • 중간 엔티티를 만들고, @ManyToOne, @OneToMany로 매핑해서 사용하자.
    정리하면 다대다 매핑을 일대다, 다대일 매핑으로 풀어내서 사용하자.

 

  • 테이블 생성이 잘 되었다.
  • ++ FK는 시스템에 따라 선택이다.
    • 실시간 트래픽이 중요하다면, 정합성보다 잘 서비스되는 것이 더 중요하다면, FK 빼고 인덱스만 잘 잡아주면 된다. 
    • 금융처럼 중요하고, 데이터가 맞아야 된다면, FK를 진지하게 고민해야 한다.

 

주소 값 타입

  • 값 타입은 변경 불가능하게 설계해야한다.
  • 따라서 @Setter 제거하고, 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스를 만든다.
  • JPA 스펙상 엔티티나 임베디드 타입은 자바 기본 생성자(default constructor)를 public 또는 protected로 설정해야한다.
  • public으로 두는 것 보단, protected로 설정하는 것이 그나마 더 안전하다.
  • JPA가 이런 제약(기본 생성자가 있어야 한다.)을 두는 이유는, JPA 구현 라이브러리가 객체를 생성할 때 리플렉션 같은 기술을 사용할 수 있도록 지원해야 하기 때문니다.

 

 

엔티티 설계시 주의점


엔티티에는 가급적 Setter를 사용하지 말자.

  • Setter가 모두 열려있다. 변경 포인트가 너무 많아서, 유지보수가 어렵다.

*** 모든 연관관계는 지연로딩으로 설정해야 한다.

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

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

  • 컬렉션은 필드에서 바로 초기화 하는 것이 안전하다.
    • null 문제에서 안전하다.
  • 하이버네이트는 엔티티를 영속화 할 때, 컬랙션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다.
    • 만약 getOrders() 처럼 임의의 메서드에서 컬력션을 잘못 생성하면, 하이버네이트 내부 메커니즘에 문제가 발생할 수 있다.
// 이렇게 하지 말고,
private List<Order> orders;
public Member() {
    orders = new ArrayList<>();
}

// 이렇게 해라.
private List<Order> orders = new ArrayList<>();

// null 문제에서 안전해진다.
// 초기화 신경쓸 필요가 없다.

 

테이블, 컬럼명 생성 전략

  • 스프링 부트에서 하이버네이트 기본 매핑 전략을 변경해서 실제 테이블 필드명은 다름

스프링 부트 신규 설정 (엔티티(필드) -> 테이블(컬럼))

  1. 카멜 케이스 -> _
  2. . -> _
  3. 대문자 -> 소문자

 


casecade

  • persist를 각각 할 필요없이 하나만 해주면, 하위 엔티티로 모든 작업을 전파한다.
  • Order에 persist를 해주면, delivery와 orderItems 모두 적용된다.

 

연관관계 (편의) 메서드

  • 양방향 연관관계에서 양쪽 모두 관계를 맺어줄 때 사용한다.
  • 아래 코드와 같이 한 번에 양방향 관계를 설정하는 메서드다.
  • 코드를 간결하게 구현할 수 있고, 원자적으로 묶을 수 있어 안전하다.
//==연관관계 (편의) 메서드==//
public void addChildCategory(Category child) {
    this.child.add(child);
    child.setParent(this);
}

// origin
public static void main(String args[]) {
    Member member = Member();
    Order order = new Order();
    
    member.getOrders().add(order);
    order.setMember(member);
}

 

 

참고자료