JPA로 엔티티 설계 시 주의사항

오렌지·2023년 2월 3일

JPA 활용 1편

목록 보기
4/9
post-thumbnail

이번 포스팅은 김영한 이사님의 스프링 부트 JPA 활용 1편을 듣고 제작되었습니다.

주의 사항

1. Getter는 모두 열어두고 Setter는 모두 닫아두자.

일반적으로 모든 엔티티에 대해 Getter는 열어두고 Setter는 닫아두고 진행하다가 필요할때만 여는 것이 좋다. 왜냐하면 Setter를 남발하면 추후에 값의 변경에 대한 추적을 할때 굉장히 힘들어질 수 있다.

2. 테이블의 pk명은 명확하게 하자.

pk 컬럼명은 일반적으로 id로 그냥 쓰기도 하는데 실제론 테이블은 타입이 없으므로 id라는 정보만으로 어느 테이블의 pk인지 구문하기 어렵다. 때문에 실무에선 pk이름을 관례상 테이블명 + id로 많이 사용한다고 한다.

3. @ManyToMany를 통한 다대다 관계는 절대 사용하지 말자.

이렇게 생성할 경우 중간테이블에 컬럼을 추가할 수 없기 때문에 자원 손실이 커지고 요구사항 변경시에도 수정하기 어렵다.

4. 값타입은 변경에 닫혀있어야 한다.

값타입은 한번 생성되면 프로그램 내부에서 변경될 일이 거의 없다. 따라서 생성자를 통해 초기에 한번만 세팅해주고 Setter는 지워주자.

@Embeddable
@Getter
@AllArgsConstructor
public class Address {
    private String city;
    private String street;
    private String zipcode;

    protected Address() {
        
    }
}

기본 생성자는 protected로 생성하는 것이 그나마 안전하다. 기본 생성자가 있어야 되는 이유는 JPA 구현 라이브러리가 리플랙션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다.

5. 모든 연관관계는 지연로딩으로 설정!

이 이유에 대한 구체적인 설명은 이 글에 잘 정리되어있다. 지연 로딩즉시 로딩에 대한 간단한 설명은 다음과 같다.

  • 지연 로딩 (Lazy-Loading) : 데이터를 조회할 때 필요한 시점에만 연관된 데이터를 가져옴.
  • 즉시 로딩 (Eager-Loading) : 데이터를 조회할 때 한번에 연관된 데이터를 모두 가져옴.

쉽게 말해 지연 로딩은 데이터가 필요한 시점에만 그 데이터에 대한 쿼리를 날려가져오고 즉시 로딩은 하나의 데이터를 가져오더라도 모든 연관된 데이터를 가져온다는 말이다.

어떨때 특히 문제가 되냐면 JPQL을 이용해서 데이터를 조회하는 쿼리를 짤 때 문제가 된다고 한다.

예를 들어 member를 통해 order를 조회할 때 select o from order o 라는 JPQL문장이 select * from order로 실행되어 필요한 order만 가져오는 것이 아닌 모든 order를 가져오게 된다고 한다.

이것이 N+1문제라고 하며 JPA의 성능저하의 주 요인이 된다고 한다.

따라서 모든 연관관계는 지연로딩으로 설정해야 하며 연관된 엔티티를 DB에서 함꼐 조회할 떄는 fetch join이나 엔티티 그래프를 사용해야된다.

주의할 점으로 JPA에서 ManyToOne, OneToOne은 디폴트로 EAGER가 적용되어있으므로 우리가 직점 LAZY로 바꿔주어야 한다.

참고

IntelliJ에서는 crtl + shift + f를 누르면 이렇게 공통된 코드를 일괄적으로 찾을 수 있다. 강의에서는 이 기능을 통해서 XtoOne관계를 모두 LAZY로 변경해주었다.

이것에 대해선 스프링JPA 기본편에서도 다루고 있다고 하여 추후에 다시 다뤄보고자 한다.

6. 컬렉션은 필드에서 초기화해주자.

컬렉션은 필드에서 바로 초기화 하는 것이 안전하다. 또한 persist로 엔티티를 영속화하는 순간 Hibernate가 컬렉션을 한번 감싸서 Hibernate가 제공하는 내장 컬렉션으로 바뀐다. 컬렉션을 필드에서 바로 초기화해주고 추후에도 코드에서 변경하는 일은 없어야 한다.

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

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

  1. 카멜 케이스 -> 언더스코어(memberPoint member_point)
  2. .(점) -> _(언더스코어)
  3. 대문자 -> 소문자

예를 들어 orderDate라는 필드명은 컬럼명으로 바뀔 때 order_date로 바뀐다. 필요한 경우 생성 전략을 바꿀 수 있는데 이것에 대해선 교안을 찾아보자.

8. cascade 옵션

cascade = CascadeType.ALL옵션을 사용하면 연관관계에 있는 엔티티를 모두 persist해준다. 코드로 보면 다음과 같다.

@Entity
@Table(name="orders")
@Getter @Setter
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")
    private Delivery delivery;

    private LocalDateTime orderDate; //주문 시간

    @Enumerated(EnumType.STRING)
    private OrderStatus status; //주문 상태[ORDER, CANCEL]
}

Order를 살펴보면 orderItemsdelivery에 대해서 CascadeType.ALL옵션이 적용되어 있다.

이 옵션이 적용되어 있으면 Orderpersist할 때 orderItemsdelivery를 따로 persist할 필요 없이 Orderperist해줘도 영속화가 된다.

9. 연관관계 편의 메서드

양방향 연관관계의 경우 연관관계의 주인이 누가 됐건 두 엔티티에 모두 값이 세팅이 되어 있어야한다. 그러한 부분을 메서드로 묶으면 비지니스 로직을 짤 떄 훨씬 수월하다. 코드를 보자.

@Entity
@Table(name="orders")
@Getter @Setter
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")
    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 addOderItem(OrderItem orderItem){
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

    public void setDelivery(Delivery delivery){
        this.delivery = delivery;
        delivery.setOrder(this);
    }
}

이렇게 밑에 setMemberaddOrderItem과 같이 클래스 단에서 값을 세팅해주는 로직을 메소드로 만들어 사용하면 실수도 줄어들고 비지니스로직을 짤 때도 훨씬 수월하다.

두 개의 엔티티 중에 접근 빈도가 더 높은 엔티티에 연관관계 편의 메서드를 설정해놓으면 구현 시 편하게 사용할 수 있다.

연관관계 편의 메서드에 대해서는 기본편에서 자세히 다뤄져있다고 하여 그때 구체적으로 다뤄볼 예정이다.

총정리

이렇게 해서 엔티티 설계시 주의 사항에 대해서 자세히 알아보았다. 모든 주의 사항들이 매우 중요한 것 같아 앞으로 설계를 할 떄 반드시 기억해두고 있을 필요성이 있을 것이라 생각한다.

특히 지연로딩에 관해서는 개념을 잘못 알고 있던 부분이 있었는데 이 부분에 대해서 잘 정확한 원리를 이해하고 지연로딩을 사용해야 된다는 것을 깨달을 수 있었다.

사실 여기에서 다뤄지고 있는 내용들중 대부분은 기본편에서 자세히 다뤄지고 있는 것 같은데 N+1문제라던가 연관관계 편의 메서드에 대해서는 따로 포스팅을 하여 내용을 정리해볼 것이다..

이렇게 해서 도메인 설계 및 구현이 끝났는데 지금까지의 과정만 하더라도 상당히 유익했었다. 앞으로 배울 내용들이 기대가 된다.

profile
안녕하세요

0개의 댓글