[스프링부트와 JPA활용 1] 2. 도메인 분석 설계

jada·2024년 1월 30일
0

Spring 스터디

목록 보기
16/35

도메인 모델과 테이블 설계

회원, 주문, 상품의 관계: 회원은 여러 상품을 주문할 수 있다. 그리고 한 번 주문할 때 여러 상품을 선택할 수 있으므로 주문과 상품은 다대다 관계다. 하지만 이런 다대다 관계는 관계형 데이터베이스는 물론이고 엔티티에서도 거의 사용하지 않는다. 따라서 그림처럼 주문상품이라는 엔티티를 추가해서 다대다 관계를 일대다, 다대일 관계로 풀어냈다.

상품 분류: 상품은 도서, 음반, 영화로 구분되는데 상품이라는 공통 속성을 사용하므로 상속 구조로 표현했다.

엔티티 클래스 개발

회원 엔티티

package jpabook.jpashop.domain;
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")
   private List<Order> orders = new ArrayList<>();
}

주문 엔티티

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(fetch = FetchType.LAZY)
   @JoinColumn(name = "member_id")
   private Member member; //주문 회원
   @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
   private List<OrderItem> orderItems = new ArrayList<>();
   @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
   @JoinColumn(name = "delivery_id")
   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);
   }
}

주의할 것

  • 다대다 매핑을 지양하자! (실무에서는 @ManyToMany를 사용하지 말자)

    • @ManyToMany는 중간테이블에 컬럼을 추가할 수 없고, 세밀하게 쿼리를 실행하기 어렵기 때문에 실무에서 사용하기에는 한계가 있다. 중간 엔티티를 만들고 @ManyToOne, @OneToMany로 매핑해서 사용하자. 정리하면 다대다 매핑을 일대다, 다대일 매핑으로 풀어내서 사용하자.
  • 값 타입은 기본적으로 immutable(불변)하도록 설계해야 한다. 즉 값이 변경되지 않도록 해야 한다. 따라서 생성 시에만 값을 세팅할 수 있도록 해야 한다. -> 따라서 @Getter만 만들고, @Setter는 만들지 않도록 한다.

    • 값 타입 클래스 만들 때 기본 생성자 없으면 오류 나는 이유?
    • JPA 기본 스펙이 값 타입 같은 것들을 JPA가 생성할 때, 리플렉션이나 프록시같은 기술을 써야하는데, 그러려면 기본 생성자가 필요하다. (따라서 protected로 기본생성자 만들어놓는 것이 좋다.)

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

엔티티 설계시 주의점

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

  • Setter가 모두 열려있다. 변경 포인트가 너무 많아서, 유지보수가 어렵다. 나중에 리펙토링으로 Setter 제거

모든 연관관계는 지연로딩으로 설정! (중요!!!)

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

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

  • null 문제에서 안전하다.
  • 하이버네이트는 엔티티를 영속화 할 때, 컬랙션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다. 만약 getOrders() 처럼 임의의 메서드에서 컬력션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문제가 발생
    할 수 있다. 따라서 필드레벨에서 생성하는 것이 가장 안전하고, 코드도 간결하다.
Member member = new Member();
System.out.println(member.getOrders().getClass());
em.persist(member);
System.out.println(member.getOrders().getClass());
//출력 결과
class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag

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

하이버네이트 기존 구현: 엔티티의 필드명을 그대로 테이블의 컬럼명으로 사용 ( SpringPhysicalNamingStrategy )

스프링 부트 신규 설정 (엔티티(필드) 테이블(컬럼))
1. 카멜 케이스 언더스코어(memberPoint memberpoint)
2. .(점)
(언더스코어)
3. 대문자 소문자

적용 2 단계
1. 논리명 생성: 명시적으로 컬럼, 테이블명을 직접 적지 않으면 ImplicitNamingStrategy 사용
spring.jpa.hibernate.naming.implicit-strategy : 테이블이나, 컬럼명을 명시하지 않을 때 논리명 적
용,
2. 물리명 적용:
spring.jpa.hibernate.naming.physical-strategy : 모든 논리명에 적용됨, 실제 테이블에 적용
(username usernm 등으로 회사 룰로 바꿀 수 있음)

스프링 부트 기본 설정

spring.jpa.hibernate.naming.implicit-strategy: 
org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
spring.jpa.hibernate.naming.physical-strategy: 
org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy

Cascade 설정
저장시, 엔티티 당 각각 persist를 호출해줘야 함

  • 예 ) persist(orderItemA); persist(orderItemB); persist(orderItemC); persist(order);

But, cascade를 사용하면 persist(order);만 해주면 됨. cascade는 persist를 전파한다. 따라서 orderItems 객체의 값 세팅해놓으면, order를 저장할 때 orderItems 엔티티도 같이 persist해준다.

(삭제도 마찬가지로 전파됨!)

연관관계 편의 메서드

  • 양방향 연관관계일 때, 양쪽 세팅하는 것을 원자적으로 한 메서드로 처리하므로 사용하는 것이 좋다!
profile
꾸준히 발전하는 개발자가 되자 !

0개의 댓글