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

shin·2023년 8월 13일
0

1. 엔티티에는 가급적 Setter 사용 지양

  • Setter가 모두 열려있으면, 변경 포인트가 너무 많아서 유지보수가 어려워짐

2. ✨모든 연관관계는 지연로딩으로 설정✨

  • 즉시로딩(EAGER)은 예측이 어렵고 어떤 SQL이 실행될지 추적하기가 어려움

    • 연관된 엔티티를 함께 로딩하는 방법
  • 특히 JPQL을 실행할 때 N+1 문제가 자주 발생함

    public class Order {
    
      @ManyToOne(fetch = FetchType.EAGER)
      @JoinColumn(name = "member_id")
      private Member member;
    • order와 member 엔티티의 연관관계가 즉시로딩으로 설정되어 있으면, order를 조회하는 시점에 member를 꼭 가져와야 함

    • JPQL select o from oder o; -> SQL select * from order

    • order를 가져오는 요청 수행 후에 만약 100개의 order가 존재하면 그 100개의 order에 대한 member를 총 100번의 조회를 통해 가져와야 하는 N+1 문제가 발생함

  • 실무에서 모든 연관관계는 지연로딩(LAZY)로 설정해야 함

  • 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용함

  • @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 함

@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")
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate; //주문시간

    @Enumerated(EnumType.STRING)
    private OrderStatus status; //주문상태

}

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

  • 컬렉션은 필드에서 바로 초기화 하는 것이 안전함
  • null 문제에서 안전함
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
ember 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
  • 하이버네이트는 엔티티를 영속화할 때, 컬렉션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경함

  • 만약 getOrders()처럼 임의의 메서드에서 컬렉션을 잘못 생성하면 하이버네이트 내부 메커니즘에서 문제가 발생할 수 있음

    • 컬렉션을 수정하면 하이버네이트가 영속화한 엔티티를 관리하기 위해 변경한 PersistentBag과 같은 내장 컬렉션에서 다시 원래 컬렉션으로 변경이 되게 됨
    • 변경이 되면 원래 동작해야 하는 동작대로 수행이 안돼서 문제가 발생할 수 있기 때문에, 컬렉션은 가급적이면 수정하지 않는 것이 좋음
  • 따라서 필드 레벨에서 생성하는 것이 가장 안전하고 코드도 간결함


4. 테이블, 컬럼명 생성 전략

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

1) 하이버네이트 기존 구현

  • 엔티티의 필드명을 그대로 테이블의 컬럼명으로 사용
  • SpringPhysicalNamingStrategy

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

(1) 카멜 케이스 -> 언더스코어

  • memberPoint -> member_point
  • 앞선 예시에서 order entity의 orderDate(주문시간) 컬럼의 경우 테이블 생성시 컬럼명이 order_date로 지정되어 생성됨
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
    ...
    private LocalDateTime orderDate; //주문시간
    ...

}

(2) 그 외 네이밍 규칙

3) 적용 2단계

(1) 논리명 생성

  • 명시적으로 컬럼, 테이블명을 직접 적지 않으면 ImplicitNamingStrategy 사용
  • spring.jpa.hibernate.naming.implicit-strategy
    : 테이블이나 컬럼명을 명시하지 않을 대 논리명 적용

(2) 물리명 적용

  • spring.jpa.hibernate.naming.physical-strategy
    : 모든 논리명에 적용됨, 실제 테이블에 적용
    (username -> usernm 등으로 회사 룰로 바꿀 수 있음)
  • 테이블명이 적혀있든 적혀있지 않든 적용됨

4) 스프링 부트 기본 설정

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


5. Cascade(영속성 전이) 설정

  • Cascade(영속성전이) : 특정 엔티티를 영속 상태로 만들면 연관된 엔티티도 함께 영속 상태로 전이되는 것

    • 특정 엔티티에 대해 작업을 수행하면 관련 엔티티도 동일한 작업이 수행됨
  • Cascade 설정 전

persist(orderItemA)
persist(orderItemA)
persist(orderItemA)
persist(order)
  • Cascade 설정 후
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
  ...
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();
    ...
}
persist(order)
  • 기본적으로 엔티티는 persist를 저장하고 싶으면 각자 수행해줘야 함
  • 그러나 Cascade 설정을 해놓으면 order를 persist하면 orderItem도 같이 persist됨


6. 연관관계 메서드 정의

  • JPA에서 양방향 연관 관계를 설정했을 때 연관 관계의 주인의 반대쪽 필드는 mappedby를 설정하여 읽기 전용으로 쓰임

  • 연관관계의 주인 쪽에만 연관관계를 설정하면 객체 지향적이지 않고, DB에서 데이터를 조회하기 전까지는 연관관계 주인의 쪽에만 값이 세팅되어 반대쪽 엔티티에서는 값을 출력해보면 데이터가 나오지 않음

  • 따라서 연관관계 편의 메서드를 사용하면 코드의 중복을 방지하고 양방향 연관 관계의 매핑에서 한쪽만 연관 관계가 설정되는 것을 방지할 수 있음

  • 연관관계 편의 메서드 사용 전

public static void main(String[] args) {
	Member member = new Member();
	Order order = new Order();

	member.getOrders().add(order);
	order.setMember(member);
}
  • 연관관계 편의 메서드 사용 후
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {

    ...

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

    ...

    // 연관관계 메서드
    public void setMember(Member member){
        this.member = member;
        member.getOrders().add(this);
    }

}
public static void main(String[] args) {
	Member member = new Member();
	Order order = new Order();

	order.setMember(member);
}


강의 : 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발

profile
Backend development

1개의 댓글

comment-user-thumbnail
2023년 8월 13일

많은 것을 배웠습니다, 감사합니다.

답글 달기