본문 바로가기
세미나

[기술교육] JPA 트러블 슈팅 - 1편

by 성건희 2023. 3. 14.
반응형

사내에서 기술교육을 진행하여 정리하였다.

하이버네이트 버전

Java Persistence 2.2 (하이버네이트 5.3+)

  • stream query results
  • @Repeatable 어노테이션

Jakarta Persistence 3.1 (하이버네이트 6.1+)

  • UUID 를 타입으로 쓸 수 있다.
  • JPQL / Criteria API 의 확장 - 날짜, 숫자 함수 추가 등

하이버네이트 는 2가지 버전을 운영중이다.

2023 03 14 기준 최신 버전
* Hibernate 5.6
* Hibernate 6.1 -> 자카르타를 쓰려면 이걸 써야함

로그

쿼리가 나가면 바인딩 값이 ? 로 떠서 볼 수가 없는데
BasicBinder=trace 옵션을 application.yml 에 추가하면 바인딩 된 값을 볼 수 있다.

JPA

@Transient : 특정 필드를 컬럼에 맵핑하지 않을 경우에 지정

복합 Key

방법 1) @IdClass
방법 2) @EmbeddedId / @Embeddable

복합 Key Class 제약조건

  • public class
  • public 기본(no-arg) 생성자
  • Serializable 인터페이스 구현
  • equals(), hashCode() 메서드 정의

복합키 @JoinColumn 대신 @MapsId 를 사용하자

Fetch 전략

  • FetchType.EAGER
  • FetchType.LAZY

-ToOne 의 기본 전략은 EAGER 로 되어있어서 일반적인 상황에서 최적화를 하기 위해서는 ToOne 전략을 모두 LAZY 로 바꿔라

영속성 전이 (cascade)

연관관계에 있는 엔티티도 같이 저장할 수 있다.
이게 없으면 각각 insert 해야하는 불편함이 있음.

단점

엔티티 2개의 라이프사이클이 똑같을 수 없다.

ex) 업무와 댓글 -> 업무를 지울 때 댓글을 지워야할까?
하나의 업무에 댓글이 엄청 많이 달렸다. 업무를 지우면 댓글 지우는데 한세월… 성능이 엄청 안좋음
(댓글은 나중에 배치로 따로 지우거나 하는게 나을 수 있음)

insert 는 일반적인 상황에서 상관없지만, remove 시에는 성능을 고려해서 영속성 전이를 사용해야한다.

  • CascadeType.ALL: 모든 Cascade를 적용 (성능 문제로인해 실무에서 사용 X)
  • CascadeType.PERSIST: 엔티티를 영속화할 때, 연관된 엔티티도 함께 유지
  • CascadeType.MERGE: 엔티티 상태를 병합(Merge)할 때, 연관된 엔티티도 모두 병합
  • CascadeType.REMOVE: 엔티티를 제거할 때, 연관된 엔티티도 모두 제거
  • CascadeType.DETACH: 부모 엔티티를 detach() 수행하면, 연관 엔티티도 detach()상태가 되어 변경 사항 반영 X
  • CascadeType.REFRESH: 상위 엔티티를 새로고침(Refresh)할 때, 연관된 엔티티도 모두 새로고침

JPA 와 DB 간 패러다임 불일치

방향성

  • 단방향 (unidirectional)
  • 양방향 (bidirectional)

DB는 외래키에 방향이 없다.
자바는 방향이 있다.

  • 양방향 연관 관계에서 주인은 외래 키 (FK) 가 있는 곳
  • 연관 관계의 주인이 아닌 경우, mappedBy 속성으로 연관 관계의 주인을 지정한다.

단방향에 비해 양방향은 복잡하고 양방향 연관관계를 맵핑하려면 객체를 양쪽 모두에서 관리해야함
따라서 일단든 단방향 방식으로 사용하고 꼭 필요하다면 양방향으로 사용하자.

연관관계

일대다

의도와 별개로 나는 insert 만 했는데 update 쿼리가 발생하는 문제가 있다. (연관관계된 엔티티의 id 값을 update)
다에 해당하는 레코드가 많을수록 update 쿼리는 더 많이 발생한다.

영속성 전이를 사용한다면 update 쿼리가 안나가지 않을까?

  • 영속성 전이를 사용해도 동일하게 update 가 발생한다.

그럼 복합키를 사용한다면 update 쿼리가 안나가지 않을까?

  • 동일하게 update 가 발생한다.

해결

  • 일대다 양방향 관리로 변경한다.

N + 1 문제

단건 조회의 경우에는 연관관계에 해당하도록 지연, 즉시 로딩으로 가져온다.
하지만 findAll 같이 다건 조회의 경우에는 연관된 녀석 말고 주 인 녀석만 findAll 쿼리를 날린다. (1)
그 후 어? 연관관계가 필요하네? 하고 연관된 N 개를 조회하는 쿼리를 레코드 개수만큼 N개 날린다.. (N)

LAZY 로딩으로 바꾼다고 하더라도 위 문제는 해결되지 않는다.

해결

  • Fetch Join
    • JPQL : join fetch
    • Querydsl : fetchJoin()
    • -> N 에 해당하는 부분을 join 하는 방법
    • fetch 를 안쓰면 주 테이블에 대한 select 컬럼만 가져온다.
    • fetch 를 쓰면 join 에 참여한 모든 select 컬럼을 가져온다. 그 후 연관관계의 그림을 보고 값을 채워준다. 그러므로 필요한 데이터가 모두 포함된 상태이므로 select 쿼리를 추가로 날릴 필요가 없다. (N 문제 해결) 단 쿼리가 뚱뚱해지니 무거움.
  • Entity Graph
  • 그 외
    • 하이버네이트 @BatchSize
    • 하이버네이트 @Fetch(FetchMode.SUBSELECT) : IN 절 안에 쿼리를 넣는 방법
    • -> 쿼리가 너무 많이 발생되니까 묶어서 실행하자는 방법

N + 1 을 해결하려다 성능이 더 나빠지는 경우가 있다.
-> 여러 테이블을 Join 하니까 쿼리가 더 무거워짐.
이런 경우는 실행 계획을 떠서 N + 1 방식의 성능과 비교해서 트러블 슈팅을 해야한다.

Querydsl

JPQL 은 text 기반이라 실제 실행을 시켜봐야 에러가 발생하는지 확인이 가능하다는 단점이 있다.
그걸 해결하고자 나온게 Criteria API 인데, JPQL 을 생성하는 빌더 클래스로 나와서 동적 쿼리 작성이 쉽지만.. 사용하기가 너무 어렵고 복잡하다.
요걸 쉽게 만들어서 나온게 Querydsl 이다.

@NoRepositoryBean

Spring Data Jpa 에서는 repository 를 찾을 때, @Repository 가 없어도 된다.
보통 해당 어노테이션은 querydsl 의 Custom 인터페이스에 선언한다. (custom 인터페이스는 단순히 선언 용도이므로)
나는 Repository 빈 후보에서 빼줘’라는 의미

@NoRepositoryBean
public interface OrderRepositoryCustom {
    List<Order> getOrdersWithAssociations();

}
public class OrderRepositoryImpl extends QuerydslRepositorySupport
        implements OrderRepositoryCustom {
    public OrderRepositoryImpl() {
        super(Order.class);
    }

    @Override
    public List<Order> getOrdersWithAssociations() {
        QOrder order = QOrder.order;
        QCustomer customer = QCustomer.customer;
        QOrderItem orderItem = QOrderItem.orderItem;
        QItem item = QItem.item;

        return from(order)
                .innerJoin(order.customer, customer).fetchJoin()
                .leftJoin(order.orderItems, orderItem).fetchJoin()
                .innerJoin(orderItem.item, item).fetchJoin()
                .fetch();
    }

}
반응형

댓글