지연 로딩 @ManyToOne, @OneToOne 연관관계에 대한 성능 최적화 내용이다. @OneToMany 연관관계는 다음 포스트에서 정리할 예정이다.
예제 도메인
@Entity
@Table(name = "orders")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
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;
}
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
@Id
@GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "order_id ")
private Order order;
private int orderPrice;
private int count;
}
@Entity
public class Delivery {
@Id
@GeneratedValue
@Column(name = "delivery_id")
private Long id;
@OneToOne(mappedBy = "delivery", fetch = LAZY)
private Order order;
@Embedded
private Address address;
@Enumerated(EnumType.STRING)
private DeliveryStatus status;
}
엔티티 직접 노출
엔티티를 API의 응답 값으로 반환하는 방법이다.
@GetMapping("/api/v1/simple-orders")
public List<Order> orderV1() {
List<Order> orders = orderRepository.findAll();
return orders;
}
Jackson 라이브러리 StackOverFlow 예외 발생
엔티티를 json으로 변환하는 과정에서 양방향 연관관계로 설정되어 있으면 서로를 계속 참조하게 되어 무한 루프에 빠지게 된다.
양방향 연관관계의 한쪽에 @JsonIgnore를 적용하여 해당 예외를 방지할 수 있다.
@JsonIgnore
@Enumerated(EnumType.STRING)
private DeliveryStatus status;
Jackson 라이브러리의 직렬화 실패
지연 로딩으로 반환된 프록시 객체가 아직 초기화되지 않은 상태에서 직렬화를 시도하므로 예외가 발생한다.
Hibernate5Module을 추가하면 초기화되지 않은 지연 객체에 대한 직렬화를 방지할 수 있다.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
@Bean
Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
지연 로딩을 강제 초기화 값을 가져오도록 할 수 있다.
public List<Order> orderV1() {
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
order.getMember().getName();
order.getDelivery().getAddress();
}
return orders;
}
엔티티를 직접 노출하면 안되는 이유
- 엔티티가 변경되면 API 스펙도 함께 변경된다.
- 불필요한 데이터까지 반환하게 된다.
- 엔티티에 의존관계 없이 독립적으로 API의 스펙이 결정되어야 한다.
엔티티를 DTO로 변환
엔티티를 API 스펙에 맞는 DTO로 변환하여 반환하는 방법이다.
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> orderV2() {
List<Order> orders = orderRepository.findAll();
return orders.stream()
.map(SimpleOrderDto::new)
.collect(Collectors.toList());
}
private class SimpleOrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
public SimpleOrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
}
}
엔티티를 DTO로 변환하는 과정에서 프록시 초기화가 일어나므로 지연 로딩에 의한 N + 1 문제가 발생한다. (Order 조회 1번, Member 지연 로딩 조회 N번, Delivery 지연 로딩 조회 N번)
엔티티를 DTO로 변환 - 페치 조인 최적화
지연 로딩에 의한 N + 1 문제를 방지하기 위해 페치 조인을 사용하는 방법이다.
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();
}
페치 조인 사용하면 엔티티들을 쿼리 1번에 가져오므로 성능 향상을 기대할 수 있다.
JPA에서 DTO로 바로 조회
JPA에서 API 스펙에 맞는 DTO로 바로 조회하는 방법이다.
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> orderV4() {
return orderSimpleQueryRepository.findOrderDtos();
}
public List<OrderSimpleQueryDto> findOrderDtos() {
return em.createQuery(
"select new me.study.jpashop2.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();
}
API 스펙에 밀접한 쿼리므로 재사용성이 낮고 메인 기능들과 연관성이 낮은 경우가 많다. 그러므로 쿼리용 Repository를 별도의 클래스로 분리하고, 패키지도 분리하는 것이 좋다.
필요한 필드만 DB에서 가져오기 때문에 네트워크 사용량을 줄일 수 있으나, Repository의 재사용성이 낮고 개발 복잡도가 올라가는 단점이 있다.
정리
- 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
- 필요하면 페치 조인으로 성능을 최적화 한다. (대부분의 성능 이슈가 해결된다.)
- 그래도 안되면 DTO를 직접 조회하는 방법을 사용한다.
- 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.
'Spring' 카테고리의 다른 글
OSIV와 성능 최적화 (0) | 2022.03.25 |
---|---|
JPA 지연 로딩과 조회 성능 최적화 (2) (0) | 2022.03.07 |
Log4j에서 SLF4J + Logback 으로 전환하기 (0) | 2022.02.03 |
@Transactional의 propagation(전파옵션) (0) | 2022.01.24 |
댓글