JPA 성능 최적화 (지연 로딩, 조회 성능 최적화)
지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결
Version 1 (Entity를 직접 노출) (실무 사용 X)
- order 와 member 는 many to one 관계
- order와 deliver는 one to one 관계
@Entity
@Table(name = "orders")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
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", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate;
...
에러 1
- 모든 order를 조회하는 findAll을 하였을 경우 member와 order에서 양방향 관계에서 무한 루프에 빠질 수 있다.
해결1
- 따라서 양방향 관계에서 한쪽은
@jsonIgnore
처리를 해서 하나를 끊어준다.
에러2
- order를 조회할때 member는 지연로딩 전략을 사용했을 때 결과를 가져올때 member는 프록시 객체(임시 객체)를 생성하여 대신 주입한다. 그때 json생성 라이브러리가 순수한 member객체가 아니기 때문에 에러가 발생할 수 있다.
해결2
- jackson dataType hibernate 5module을 gradle 또는 maven에 의존을 설정하고
@Bean
으로 Hibernate5Module을 생성하여 사용하면 에러 해결
※ 위와 같이 Entity를 그대로 사용하여 반환할 경우 api 의 스펙이 변경될 때 구조를 전부 바꿔야 하는 상황이 올 수 있으며, 스펙이 노출되어 좋지 않다. 또한, 성능적으로도 좋지 않다.
Version2 (Dto 사용)
- DTO 클래스를 생성하여 생성자에 entity를 받아서 dto로 전환하여 사용하게끔 구성
- 실제 Entity가 노출되지 않고, Json 어노테이션을 사용하지 않고 원하는 키값으로 리턴 가능
문제점
- lazy 호출에 의한 쿼리가 너무 많이 호출됨
- 영속성 컨텍스트에 값이 있더라도 최악의 경우 n + 1의 쿼리 호출함
- 많은 결과값(조회 결과)가 있을 수록 쿼리 호출의 수가 감당이 안된다.
- 심지어 EAGER로 설정했을 경우 예상치 못한 쿼리 호출(양방향일 경우 더 예측 불가능)
Version3 (DTO + 성능 최적화)
- lazy 호출에 대한 쿼리가 너무 많이 처리되기 때문에 이것을 최적화
- fetch join을 활용하여 객체 그래프를 한번에 가져온다.
- 기존의 n + 1 쿼리 호출을 한번에 가져온다.
@Repository
public class OrderRepository {
private final EntityManager em;
public OrderRepository(EntityManager em) {
this.em = em;
}
public List<Order> findAllWithMemberDelivery() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.getResultList();
}
}
Version4 (결과를 바로 DTO로 매핑)
- repository의 createQuery에 반환 타입을 바로 dto로 받는다.
- 내가 원하는 select의 컬럼 값 들을 선택해서 받을 수 있다.
- version3와 효율에 대한 우열 가리기 어렵다. (select의 속도가 빠르다. 다만, 재사용성이 매우 떨어진다. 화면에 의존적인 조회)
해결법
- 패키지를 repository 하위에 별도 생성하여 쿼리용을 별도로 추출하여 관리
- 이유 : 원래의 repository는 Entity를 조회하여 재사용을 할 수 있고 Entity로 가공을 하도록 하고, 별도로 추출한 repository는 화면에 의존적이지만 속도는 빠르게 할 수 있도록 하게하여 유지보수성을 크게 향상.
-
그래도 성능이 안나올 경우 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template를 사용하여 SQL을 직접 사용한다.
- 매핑 받을 DTO 생성
@Data public class OrderSimpleQueryDto { private Long orderId; private String name; private LocalDateTime orderDate; //주문시간 private OrderStatus orderStatus; private Address address; public OrderSimpleQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) { this.orderId = orderId; this.name = name; this.orderDate = orderDate; this.orderStatus = orderStatus; this.address = address; } }
- Repository의 createQuery에서 바로 DTO로 맵핑하여 반환
@Repository @RequiredArgsConstructor public class OrderSimpleQueryRepository { private final EntityManager em; public List<OrderSimpleQueryDto> findOrderDtos() { return em.createQuery( "select new jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" + " from Order o" + " join o.member m" + " join o.delivery d", OrderSimpleQueryDto.class) .getResultList(); } }