728x90
ToOne 관계 (N+1 문제) , 페치 조인
지연 로딩과 조회 성능 최적화
주문(Order) 내의 ToOne 관계인 member, delivery 지연로딩 조회
Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
// = new ProxyMember()를 생성해서(하이버네이트에서) 넣어둔다.
// = new ByteBuddyInterceptor() (프록시 기술)
@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;
...
페치 조인 최적화
@GetMapping("/api/v2/simple-orders")
public ResponseEntity<Result> ordersV2() {
// order 2개
// 1 + N(2) -> 회원 N번 + 배송 N번
List<Order> orders = repository.findAllByString(new OrderSearch());
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return ResponseEntity.ok(new Result(result.size(), 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(); // getMember 까지는 프록시객체(SQL 안날라감)
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); // getDelivery(): Lazy 강제 초기화
}
}
값을 받는 DTO
결과
{
"count": 2,
"data": [
{
"orderId": 1,
"name": "userA",
"orderDate": "2023-10-02T11:39:00.157897",
"orderStatus": "ORDER",
"address": {
"city": "서울",
"street": "1",
"zipcode": "15352"
}
},
{
"orderId": 2,
"name": "userB",
"orderDate": "2023-10-02T11:39:00.204814",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
}
}
]
}
2개의 row 값 호출
한번에 잘 나온 것 같지만 콘솔창을 살펴보면..
Hibernate: select o1_0.order_id,o1_0.delivery_id,o1_0.member_id,o1_0.order_date,o1_0.status from orders o1_0 join member m1_0 on m1_0.member_id=o1_0.member_id limit ?
Hibernate: select m1_0.member_id,m1_0.city,m1_0.street,m1_0.zipcode,m1_0.name from member m1_0 where m1_0.member_id=?
Hibernate: select d1_0.delivery_id,d1_0.city,d1_0.street,d1_0.zipcode,d1_0.status from delivery d1_0 where d1_0.delivery_id=?
Hibernate: select o1_0.order_id,o1_0.delivery_id,o1_0.member_id,o1_0.order_date,o1_0.status from orders o1_0 where o1_0.delivery_id=?
Hibernate: select m1_0.member_id,m1_0.city,m1_0.street,m1_0.zipcode,m1_0.name from member m1_0 where m1_0.member_id=?
Hibernate: select d1_0.delivery_id,d1_0.city,d1_0.street,d1_0.zipcode,d1_0.status from delivery d1_0 where d1_0.delivery_id=?
각 row 마다 order -> member 1, delivery 1 -> member 2, delivery 2 호출이 됨
-> 1 + N(2) -> 회원 N번 + 배송 N번
지연로딩은 영속성 컨테스트에서 조회하므로, 이미 조회된 경우 쿼리 날리지 않음
Fetch Join
- 연관된 엔티티나 컬렉션을 한 번에 같이 조회
- 연관된 엔티티는 프록시가 아닌 실제 엔티티
- 연관된 엔티티를 사용해도 지연 로딩 발생 x
- 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 엔티티 조회 가능
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();
}
하이버네이트
Hibernate:
select
o1_0.order_id,
d1_0.delivery_id,
d1_0.city,
d1_0.street,
d1_0.zipcode,
d1_0.status,
m1_0.member_id,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.name,
o1_0.order_date,
o1_0.status
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
join
delivery d1_0
on d1_0.delivery_id=o1_0.delivery_id
SELECT o FROM Order o
member, delivery로 나가는 쿼리가 많기 때문에
미리 fetch join 을 하는 것이 좋다 (권장)
DTO로 화면에 보낼 속성에 맞춰 바로 쿼리조회
public List<SimpleOrderQueryDto> findOrderDtos() {
return em.createQuery(
"SELECT NEW com.inflearn.optimization.domain.order.dto.SimpleOrderQueryDto(" +
"o.id, m.name, o.orderDate, o.status, d.address)" +
" FROM Order o" +
" JOIN o.member m" +
" JOIN o.delivery d", SimpleOrderQueryDto.class)
.getResultList();
}
@Data
public class SimpleOrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
public SimpleOrderQueryDto(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;
}
public SimpleOrderQueryDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
}
}
new 명령어를 사용하여 DTO로 변환
페치 조인과 비교했을 때 크게 차이가 없음
API 스펙에 맞춰 DTO를 작성하기에 레포지토리 재사용성이 떨어짐
쿼리 방식 선택 권장 순서
- 우선 엔티티를 DTO로 변환
- 필요하면 페치 조인으로 성능을 최적화 - 대부분의 성능 이슈 해결
- DTO로 직접 조회하는 방법 (new)
- JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용
최적화를 위한 레포지토리 분리
OrderRepository - 순수한 엔티티 조회용도의 레포지토리
@Repository
public class OrderRepository {
재사용도 더 용이하게 가능
API 스펙용 레포지토리
@Repository
@RequiredArgsConstructor
public class SimpleOrderQueryRepository {
private final EntityManager em;
public List<SimpleOrderQueryDto> findOrderDtos() {
return em.createQuery(
"SELECT NEW com.inflearn.optimization.domain.order.simpleOrderQuery.SimpleOrderQueryDto(" +
"o.id, m.name, o.orderDate, o.status, d.address)" +
" FROM Order o" +
" JOIN o.member m" +
" JOIN o.delivery d", SimpleOrderQueryDto.class)
.getResultList();
}
}
통계용, 전용, 복잡한 조인쿼리 등
쿼리 서비스 , 쿼리 레포지토리
화면에 디펜던시
'Back-end > Spring Boot + REST API' 카테고리의 다른 글
스프링부트 API JPA 최적화 페치조인, 페이징 default_batch_fetch_size (0) | 2023.12.23 |
---|---|
스프링부트 API JPA 최적화 (N+1) 컬렉션 조회 최적화, DTO 조회 성능 향상 (0) | 2023.12.21 |
Spring Boot 시큐리티 + JWT 흐름 정리 (0) | 2022.11.23 |
Spring Boot 실행 프로세스와 Embedded Servers , CLI 실행 (0) | 2022.09.09 |
Spring boot - blog application (REST API) : AWS RDS, Elastic Beanstalk (0) | 2022.09.08 |
댓글