주문 + 배송정보 + 회원을 조회하는 API
- 지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결
- JPA를 활용하여 개발을 진행할 때 성능을 높일 수 있는 방법 학습
Order
Order -> Member
Order -> Delivery
domain Order
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
Member ManyToOne 관계 > ToOneDelivery는 OneToOne 관계 > ToOneOrderItem은 OneToMany 관계 > ToManyOrderSimpleApiController
package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAll(new OrderSearch());
return all;
}
}


Domain Member
@Entity
@Getter @Setter
public class Member {
...
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
@Entity
@Getter @Setter
public class Member {
...
@JsonIgnore
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
@Entity
@Getter @Setter
public class Delivery {
...
@JsonIgnore
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
private Order order;
...
}
@Entity
@Table(name = "order_item")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
...
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
...
}

public class Order {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member = new ByteBuddyInterceptor();
ByteBuddyInterceptor 객체를 new로 가져오게 되는 것Hibernate5Module 등록이 필요함build.gradle
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
JpashopApplication에 아래 코드 추가
@Bean
Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
build.gradle
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta'
JpashopApplication
package jpabook.jpashop;
import com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule;
...
@SpringBootApplication
public class JpashopApplication {
public static void main(String[] args) {
SpringApplication.run(JpashopApplication.class, args);
}
@Bean
Hibernate5JakartaModule hibernate5Module() {
return new Hibernate5JakartaModule();
}
}

@Bean
Hibernate5JakartaModule hibernate5Module() {
Hibernate5JakartaModule hibernate5JakartaModule = new Hibernate5JakartaModule();
// 강제 지연로딩 설정
hibernate5JakartaModule.configure(Hibernate5JakartaModule.Feature.FORCE_LAZY_LOADING, true);
return new Hibernate5JakartaModule();
}
OrderSimpleApiController
package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
/**
* V1. 엔티티 직접 노출
* - Hibernate5Module 모듈 등록, LAZY=null 처리
* - 양방향 관계 문제 발생 -> @JsonIgnore
*/
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAll(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); //Lazy 강제 초기화
order.getDelivery().getAddress(); //Lazy 강제 초기화
}
return all;
}
}
force lazy load를 끄고 원하는 것만 골라서 출력하는 방식order.getMember()까지는 가짜 프록시 객체를 가져오지만, getName()을 하게 되면 실제 name을 가지고 와야 하기 때문에 Lazy가 강제 초기화 되면서 Member Query를 날려서 jpa가 해당 데이터를 다 끌고 오게 됨
OrderSimpleApiController
package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
...
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
List<Order> orders = orderRepository.findAll(new OrderSearch());
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(toList());
return result;
}
@Data
static 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();
}
}
}
SimpleOrderDto 구성SimpleOrdeDto로 변환한 후 collect로 list 형태로 변환하여 반환
public SimpleOrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName(); // LAZY 초기화
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // LAZY 초기화
}


@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
// OOrder -> SQL 1번 실행 -> 결과 주문수 2개
List<Order> orders = orderRepository.findAll(new OrderSearch());
// 루프 2번 Member, Delivery 각각 2번 실행
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(toList());
return result;
}
@Data
static class SimpleOrderDto {
...
public SimpleOrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName(); // LAZY 초기화 - Member 조회
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // LAZY 초기화 - Delivery 조회
}
}
Order 1 + Member 2 + Delivery 2LAZY로 설정해놓는 것이 좋고, 성능 튜닝은 fetch join을 이용해서 해야 함
- 쿼리가 총
1 + N + N번 실행(v1과 쿼리수 결과는 같음)
order조회 1번(order 조회 결과 수가 N이 됨)order->member지연 로딩 조회 N번order->delivery지연 로딩 조회 N번- 예) order의 결과가 4개면 최악의 경우
1 + 4 + 4번 실행됨(최악의 경우)
- 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략함
지연로딩으로 인해 데이터베이스 쿼리가 너무 많이 호출되는 문제가 발생함
지연로딩은 N번 수행되면, N번을 DB 쿼리가 아니라 영속성 컨텍스트를 찔러봄
memberId를 가지고 있으면, 그 경우에는 N번이 아니고 1번이 됨userA가 주문을 여러번 수행한 경우, 처음에는 영속성 컨텍스트를 확인해 값이 없어서 DB 쿼리를 날려서 데이터를 가져오겠지만 그 이후에는 영속성 컨텍스트에 있는 값을 그대로 사용함jpql에서 성능 최적화를 위해 제공하는 조인의 종류
fetch join은 연관된 엔티티나 컬렉션을 한 번에 같이 조회할 수 있는 기능
JOIN FETCH 명령어로 사용이 가능함
조회의 주체가 되는 엔티티 외에도 fetch join이 걸린 연관 엔티티도 함께 select하여 모두 영속화함
fetch join이 걸린 엔티티를 모두 영속화하기 때문에, FetchType이 Lazy인 엔티티를 참조하더라도 이미 영속성 컨텍스트에 들어가 있는 상태임 N + 1 문제가 해결됨OrderRepository
package jpabook.jpashop.repository;
...
@Repository
@RequiredArgsConstructor
public class OrderRepository {
private final EntityManager 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();
}
}
조회의 주체가 되는 Order 엔티티 외에도 fetch join이 걸린 Member, Delivery 엔티티도 함께 select하여 모두 영속화함
fetch join으로 order -> member, order -> delivery는 이미 조회된 상태이므로 지연로딩X
OrderSimpleApiController
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithMemberDelivery();
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(toList());
return result;
}


fetch join을 사용해서 쿼리 1번에 조회fetch join을 활용하면 성능 튜닝이 확실하게 됨OrderSimpleApiController
...
public class OrderSimpleApiController {
...
@GetMapping("/api/v4/simple-orders")
public List<SimpleOrderDto> ordersV4() {
return orderRepository.findOrderDtos();
}
}
OrderSimpleQueryDto
@Data
public class OrderSimpleQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
public OrderSimpleQueryDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
}
}
Controller 내부의 SimpleOrderDto를 사용하지 않고, 별도의 OrderSimpleQueryDto를 정의하여 Repository에서 해당 객체를 DTO로 사용할 수 있도록 해야 함Controller 내부의 SimpleOrderDto를 바라볼 경우 Repository가 Controller를 바라보는 상황이 될 수 있음OrderSimpleQueryRepository
package jpabook.jpashop.repository.Order.simplequery;
...
@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {
private final EntityManager em;
public List<OrderSimpleQueryDto> findOrderDtos() {
return em.createQuery("select o from Order o" +
" join o.member m" +
" join o.delivery d", OrderSimpleQueryDto.class)
.getResultList();
}
}
OrderRepository는 순수하게 entity를 조회하는 용도로만 사용하고, api 스펙을 그대로 정의하는 v4의 경우에는 별도의 패키지로 분리하여 작성해주는 것이 좋음package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderSimpleQueryRepository orderSimpleQueryRepository;
/**
* V4. JPA에서 DTO로 바로 조회
* @return List<OrderSimpleQueryDto>
*/
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderSimpleQueryRepository.findOrderDtos();
}
}
OrderSimpleQueryDto
package jpabook.jpashop.repository.Order.simplequery;
...
@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;
}
}
OrderSimpleQueryRepository
package jpabook.jpashop.repository.Order.simplequery;
...
@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();
}
}
OrderSimpleQueryRepository의 findOrderDtos()에서 jpql을 작성할 때, select 절에서 new operation을 사용해야 함
new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환함
일반적인 SQL을 사용할 때처럼 원하는 값을 선택해서 조회
SELECT 절에서 원하는 데이터를 직접 선택하므로 DB -> 애플리케이션 네트워크 용량 최적화
리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점이 있음
V3와 V4의 수행 결과를 비교하기 위해, V3를 먼저 수행해봄




- 우선 엔티티를
DTO로 변환하는 방법을 선택(V2방식)- 필요하면
fetch join으로 성능을 최적화(V3방식) > 대부분의 성능 이슈가 해결됨- 그래도 안되면
DTO로 직접 조회하는 방법 사용(V4방식)- 최후의 방법은 JPA가 제공하는
네이티브 SQL이나스프링 JDBC Template을 사용해서 SQL을 직접 사용
em.createNativeQuery(SQL, 결과 클래스);