스프링부트 JPA 웹 앱 구축 - 엔티티 설계 시 팁과 주의사항

agugu95·2020년 8월 4일
0

상속이 있는 엔티티의 계층 관계 구조 매핑

실제로 DB에서 클래스의 상속이라는 관계는 존재하지 않음
다만 테이블 전략이라는 것을 통해 매핑할 수 있음

상속 테이블 전략

Joined

  • 가장 이상적인 상속 구현
  • 정규화와 참조 무결성을 만족한다면 객체와 가장 비슷함

Single Table

  • 서비스 규모가 크지 않을 때 때려박는 방법
  • 쿼리가 간단하고 쉽고 빠름

Per Class

  • 비추천
    Joined와 Single Table을 적절히 사용

자세한 정리

ORM 표준 JPA 프로그래밍 7장

지연 로딩의 사용

  • JPA는 fetch에 EAGER(즉시로딩)과 LAZH(지연로딩)을 지원
  • 연관 관계에는 반드시 지연로딩을 사용

왜 지연 로딩을 사용해야하는가?


이런 연관 관계를 가지고 있을 때 Order를 조회한다고 하면
만약 EAGER(즉시로딩) 설정 시

  • JPQL은 이 코드를 select o FROM oreder o;로 변경하여 실행
  • SQL select * from order; 실행

이게 만약 100번 이루어지면 member도 100번 조회되야 하기에 쿼리가 100번 날라감
첫 쿼리가 가져온 결과가 100개라면 n + 1을 100으로 치환 --> 100 + 1

Order를 조회하는 시점에 반드시 Member가 존재해야하기 때문에 Order 조회 결과가 100개라면 이에 따라 Member 조회 쿼리가 100개 날라감
즉시 로딩은 JPA 성능 저하의 매우 크리티컬한 요소이기 때문에 절대 사용하면 안됨

지연 로딩 수정

  • 기본적으로 x to one 관계는 EAGER이므로 LAZY로 수정

컬렉션 필드 초기화

Member member = new Member();
System.out.println(member.getOrders().getClass());
em.persist(team);
System.out.println(member.getOrders().getClass());

//출력 결과
class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag
  • 필드 초기화는 필드 선언 시점에 하고 필드를 건드리지 않는 것이 좋음
  • 하이버네이트가 JPA 기능들을 제공할 수 있는 방법은 컬렉션을 추적, 관리할 수 있는건
    데이터가 영속화 되는 순간 하이버네이트는 자체 컬렉션으로 데이터를 매핑하기 때문

테이블과 컬럼 이름

스프링부트는 기본적으로 SpringPhysicalNamingStrategy라는 전략을 사용
DBA는 보통 언더스코어를 사용하기에 편리하게 맞출 수 있음

  • camelCase -> under_score

  • . -> _

  • UpperCase -> lower_case

    • orderDate -> order_date

스프링부트의 기본 전략

  • 논리적 이름

    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 상태 전파

@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {

    @Id
    @GeneratedValue
    @Column(name = "order_id")
    private String 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")
    private Delivery delivery;
  • 기본적으로 연관 관계는 어떤 상태도 전파하지 않음
  • 엔티티 상태는 persist, remove, detached 등이 있음
    특히나 persist같은 엔티티 상태 변경은 모든 엔티티에 대해서 이루어져야 함
    em.persist(orderItem1)
    em.persist(orderItem2)
    ....
    em.persist(order)
  • 하지만 cascade를 통하면 상태 전파가 가능
    em.persist(order)만 사용하더라도 그 엔티티 클래스에 소속 된 모든 엔티티들은 상태 변경

casacde 질문들

  • cascade 막 달아도 되는가?

cascade 옵션은 단순히 생각하면, 그냥 persist() 호출을 줄여줄 수 있기 때문에, 유용해 보이지만, 반대로 생각하면, Order 엔티티를 저장할 때, 연관된 어떤 엔티티들이 함께 저장될까? 를 계속 코드를 보며 추적해야 합니다.

따라서 질문하신 것 처럼 어디까지는 cascade로 함께 저장하고, 어디까지는 함께 저장하면 안될까? 하는 명확한 기준이 필요합니다.

그래서 이런 기준을 잡기 애매한 경우에는 사실 사용하지 않는 것이 좋습니다.

  • cascade를 다는 기준은?

통상적으로 권장하는 cascade 범위는, 완전히 개인 소유하는 엔티티일 때, 예를 들어서 게시판과 첨부파일이 있을 때 첨부파일은 게시판 엔티티만 참조하므로, 개인 소유 입니다. 이런 경우에는 사용해도 됩니다. 그럼 반대로 개인 소유하지 않는 엔티티는 무엇일까요? 예를 들어서, 회원, 상품 등등이 있습니다.

이 예제에서 Order -> OrderItem을 개인소유 하기 때문에 cascade를 사용했습니다. 그런데 Order 입장에서 Delivery는 좀 애매합니다. 여기서는 프로젝트 규모가 작기 때문에 매우 단순하게 표현했지만, 실무에서 프로젝트 규모가 커지면, Delivery로 여러곳에서 참조될 수 있습니다. 그러면 사용하면 안됩니다.

추가로 도메인 주도 설계(DDD)의 Aggregate Root 개념을 이해하고, 프로젝트 적용하면 여기에 맞추어 cascade 옵션을 더 잘 활용할 수 있습니다.

  • cascade는 양방향 중 어느 쪽에 달아야 하는가?

제어가 더 많은 쪽

정리

  1. 완전 개인 소유인 경우에 사용할 수 있다.

  2. DDD의 Aggregate Root와 어울린다.

  3. 애매하면 사용하지 않는다.

링크

cascade를 이용한 연관 관계 엔티티 상태 전파

엔티티 연관 관계 편의 메소드

원래 양방향 추가의 경우

Member member = new Member();
Order order = new Order();
member.getOrders().add(order);
order.setMember(member);

이렇게 해줘야하는데 이런 걸 원자적인 한 코드로 묶어주는 메소드가 연관 관계 메소드
이런 메소드는 컨트롤러가 가지고 있는게 좋음

셀프 연관 관계의 경우

// 연관 관계 메소드
    public void addChildCategory(Category child) { // 셀프 연관 관계
        this.child.add(child); // 자기 자신을 넣기
        child.setParent(this); // 부모 설정
    }
    
    // 연관 관계 메소드
    public void setPerent(Category perent) { // 셀프 연관 관계
        this.perent = perent; // 자기 자신을 넣기
        perent.getChild.add(this); // 자식 설정
    }

두 메소드는 동일하게 작동함
자기 자신에 부모를 설정하는 것
부모 자기 자신에 자식을 설정하는 것

다른 경우


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

0개의 댓글