Spring/SpringBoot

도메인 분석 설계 - 엔티티 설계시 주의점

느리지만 꾸준하게 2022. 4. 5. 02:41

엔티티 설계시에는 가급적 Setter을 사용안하는게 좋다.

 

그리고 모든 연관관계는 지연로딩으로 설정을 해줘야 한다.

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

 

최악의 경우를 살펴보자. 

아래에서 Member Order 연관  Order OrderItem이 연관  이렇게 다 연관이 되어있다 하자.

하나를 가져오면 다 연관되어 있는 애들을 DB에서 다 끌고 가져오기 때문에 절대로 쓰면 안된다.

그래서 지연로딩으로 모든 연관관계를 설정을 해주고(fetch join) 또는 엔티티 그래프 기능을 사용한다.

  • 실무에서 모든 연관관계는 지연로딩( LAZY )으로 설정해야 한다.
  • 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용한다.
  • @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 한다.

Order와 Member을 만약 아래와 같이 세팅을 해놓는다고 하자.

 

Order을 조회하는데 fetch를 아래와 같이 작성한다고 하자.

Order을 조회할 때 member을 join을 해서 같이 가져온다.(query를 한번에)

(Many to One은 기본 Fetch가 EAGER One to One도 마찬가지 한개를 찌르는 거는 다 EAGER)

(만약 EAGER로 지정안해놓으면 연관된 쿼리가 무수히 많이 나가게 된다.)

그래서 ~ to One에서는 LAZY를 쓰면 안된다.

진행중인 프로젝트에서 command shift f를 눌러서 ~(Many, One) to One을 모두 LAZY로 바꿔줘야 한다.

// ManyToOne.java

public @interface ManyToOne {

    /** 
     * (Optional) The entity class that is the target of 
     * the association. 
     *
     * <p> Defaults to the type of the field or property 
     * that stores the association. 
     */
    Class targetEntity() default void.class;

    /**
     * (Optional) The operations that must be cascaded to 
     * the target of the association.
     *
     * <p> By default no operations are cascaded.
     */
    CascadeType[] cascade() default {};

    /** 
     * (Optional) Whether the association should be lazily 
     * loaded or must be eagerly fetched. The EAGER
     * strategy is a requirement on the persistence provider runtime that 
     * the associated entity must be eagerly fetched. The LAZY 
     * strategy is a hint to the persistence provider runtime.
     */
    FetchType fetch() default EAGER;

    /** 
     * (Optional) Whether the association is optional. If set 
     * to false then a non-null relationship must always exist.
     */
    boolean optional() default true;
}

(One to Many은 기본이 LAZY 그러므로 그냥 공란으로 놔둬도 LAZY로 인식한다.)

두개가 다르다.

// OneToMany

public @interface OneToMany {

    /**
     * (Optional) The entity class that is the target
     * of the association. Optional only if the collection
     * property is defined using Java generics.
     * Must be specified otherwise.
     *
     * <p> Defaults to the parameterized type of
     * the collection when defined using generics.
     */
    Class targetEntity() default void.class;

    /** 
     * (Optional) The operations that must be cascaded to 
     * the target of the association.
     * <p> Defaults to no operations being cascaded.
     *
     * <p> When the target collection is a {@link java.util.Map
     * java.util.Map}, the <code>cascade</code> element applies to the
     * map value.
     */
    CascadeType[] cascade() default {};

    /** (Optional) Whether the association should be lazily loaded or
     * must be eagerly fetched. The EAGER strategy is a requirement on
     * the persistence provider runtime that the associated entities
     * must be eagerly fetched.  The LAZY strategy is a hint to the
     * persistence provider runtime.
     */
    FetchType fetch() default LAZY;
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

	// JPQL select o From order o; -> SQL로 그대로 번역이 된다. 문제가 된다.
    // select * from order
    // order을 100개를 조회했다고 하자.
    // 100개를 조회했는데 보니까 order만 날라왔다.(member가 FetchType.EAGER로 되어있네?)
    // 단건 쿼리가 100개가 날라가게 된다....
    // => n + 1 문제(첫번째 쿼리가 가져온 결과가 100개면 100(result) + 1(order)이 되어서)
    @ManyToOne(fetch = FetchType.EAGER)
    // EAGER 이름에서 알 수 있듯이 뭘 꼭 하겠다. => 즉 member을 꼭 가져오겠다라고 생각이 든다.
    @JoinColumn(name = "member_id")
    private Member member;

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

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

 

그리고 Member에 보면 아래와 같이 초기화를 할지(이게 낫다.)

private List<Order> orders = new ArrayList<>();

아래와 같이 초기화를 할 지 고민이 생길 수 있는데.

@OneToMany(mappedBy = "member")
    private List<Order> orders;

    public Member() {
        orders = new ArrayList<>();
        
    }

 

아래를 보면 member 객체를 생성을 하는데

가급적이면 위에서 private List<Order> orders = new ArrayList<>(); 이 부분을 밖으로 꺼내지도 말고(수정가능성)

변경도 하면 안된다. 객체 생성할 때 한번 넣어놓고 바꾸지 말자.(컬렉션을 필드에다 바로 초기화하는 것이 제일 좋다.)

Member member = new Member();
  System.out.println(member.getOrders().getClass());
  em.persist(team);
  System.out.println(member.getOrders().getClass());
//출력 결과
class java.util.ArrayList
// hibernate가 추적할 수 있는 본인의 PersistentBag을 이용해서 바꿔버린다.
class org.hibernate.collection.internal.PersistentBag

// 만약 set해서 collection을 바꾼다? 그러면 hibernate가 원하는 메커니즘으로 안돌아간다.

 

다음으로 테이블 & 컬럼명을 생성하는 전략을 보자.

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

member table명을 default로 아무것도 안하게 되면 어떻게 할 건지에 대한 전략이 있다.

  • 하이버네이트 기존 구현: 엔티티의 필드명을 그대로 테이블의 컬럼명으로 사용 ( SpringPhysicalNamingStrategy )
  • 스프링 부트 신규 설정 (엔티티(필드) 테이블(컬럼))
    1. 카멜 케이스 언더스코어(memberPoint member_point) 2. .() _(언더스코어)
    3. 대문자 소문자

// Membmer

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

 

Order에서 ordrItems에 데이터를 넣어두고 저장을 해준다.

public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();
    
    // persist(orderItemA)
    // persist(orderItemB)
    // persist(orderItemC)
    // 이거만 해주면 위에 3개의 persist는 지워도 된다. persist(order)

그리고 delivery의 값을 setting 해놓으면 order을 저장해 놓으면 delivery entity도 같이 persist 해준다.

@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

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

    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    // Cascade을 활용하게 되면 아래처럼 delivery의 값만 세팅해놓고 order만 persist 해주면 delivery의 부분까지 같이 persist가 된다.
    private Delivery delivery;

 

그리고 order와 member가 있으면 member가 주문을 하면 list 안에 order을 넣어줘야 한다.

order에서도 양뱡향이니까 member와 왔다갔다 할 수 있다.

// Member Class

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")
    private List<Order> orders = new ArrayList<>();
}
// Order Class

package jpabook.jpashop.domain;


import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

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

import static javax.persistence.FetchType.*;

@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

 

 

Order에서 연관관계 메서드는 아래와 같이 짜주게 된다.(양방향일때 하나의 메서드를 이용해서 묶는 방법이 효율적이다.)

//==연관관계 메서드==//
    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);
    }

 

Category에서도 연관관계 편의 메서드를 사용해서 작성한다.

// Category

package jpabook.jpashop.domain.item;

import lombok.Getter;
import lombok.Setter;

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

import static javax.persistence.FetchType.*;

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

}

엔티티 설계시 주의점을 다시 떠올리면 아래와 같다.

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

 

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

     

     

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

 

  • 테이블, 컬럼명 생성 전략

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

<출처 김영한: 실전! 스프링 부트와 JPA 활용1 - 웹 어플리케이션 개발 >

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1/dashboard

 

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

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

www.inflearn.com