목표
이전 포스팅에서 조회한 주문내역에서 추가로 주문한 상품 정보인 OrderItem과 Item을 조회하고, 일대다 관계 조회 시 최적화하는 방법을 알아보자.
V1. 엔티티 직접 노출
기본 조회
package jpabook.jpashop.api;
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
/**
* V1. 엔티티 직접 노출
* - Hibernate5Module 모듈 등록, LAZY=null 처리
* - 양방향 관계 문제 발생 -> @JsonIgnore
*/
@GetMapping(value = "/api/v1/orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName();
order.getDelivery().getAddress();
// 프록시 강제 초기화
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName());
}
return all;
}
}
- Hibernate5Module의 지연로딩 설정이 OFF로 되어있기 때문에 지연로딩 항목을 강제 초기화한다.
- 강제 초기화하면 Hibernate5Module설정에 의해 엔티티를 JSON으로 생성한다.
- 양방향 연관관계면 무한 루프에 걸리지 않게 한곳에
@JsonIgnore
를 선언해야 한다.
V1 마무리
지연로딩으로 설정된 항목에 대해 추가적인 설정이 필요하며 강제 초기화를 해야 결과가 표시된다. 가장 중요한 것은 엔티티를 직접 노출하기 때문에 좋은 방법이 아니다.
V2. 엔티티를 DTO로 변환
프록시 강제 초기화 이전
package jpabook.jpashop.api;
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
/**
* V2. 엔티티를 DTO로 변환 (fetch join 미사용)
* - 트랜잭션 안에서 지연로딩 필요
*/
@GetMapping(value="/api/v2/orders")
public List<OrderDTO> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDTO> result = orders.stream()
.map(o -> new OrderDTO(o))
.collect(Collectors.toList());
return result;
}
@Data
static class OrderDTO {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItem> orderItems;
public OrderDTO(Order order) {
this.orderId = order.getId();
this.name = order.getMember().getName();
this.orderDate = order.getOrderDate();
this.orderStatus = order.getStatus();
this.address = order.getDelivery().getAddress();
this.orderItems = order.getOrderItems();
}
}
}
- 값 타입은 변할일이 없기 때문에 노출해도 상관없다.
프록시 강제 초기화 이후
@Data
static class OrderDTO {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItem> orderItems;
public OrderDTO(Order order) {
this.orderId = order.getId();
this.name = order.getMember().getName();
this.orderDate = order.getOrderDate();
this.orderStatus = order.getStatus();
this.address = order.getDelivery().getAddress();
// 프록시 강제 초기화
order.getOrderItems().stream().forEach(o -> o.getItem().getName());
}
}
- 지연로딩 항목을 강제 초기화 하여 데이터 조회를 한다.
프록시 강제 초기화를 통해 OrderItem 정보도 조회되지만 엔티티 정보가 노출되는 위험성이 있다.
OrderItemDTO 이용
package jpabook.jpashop.api;
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
/**
* V2. 엔티티를 DTO로 변환 (fetch join 미사용)
* - 트랜잭션 안에서 지연로딩 필요
*/
@GetMapping(value="/api/v2/orders")
public List<OrderDTO> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDTO> result = orders.stream()
.map(o -> new OrderDTO(o))
.collect(Collectors.toList());
return result;
}
@Data
static class OrderDTO {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDTO> orderItems;
public OrderDTO(Order order) {
this.orderId = order.getId();
this.name = order.getMember().getName();
this.orderDate = order.getOrderDate();
this.orderStatus = order.getStatus();
this.address = order.getDelivery().getAddress();
// OrderItemDTO 사용
this.orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDTO(orderItem))
.collect(Collectors.toList());
}
}
@Data
static class OrderItemDTO {
private String itemName;
private int orderPrice;
private int count;
public OrderItemDTO(OrderItem orderItem) {
this.itemName = orderItem.getItem().getName();
this.orderPrice = orderItem.getOrderPrice();
this.count = orderItem.getCount();
}
}
}
- OrderItem을 강제 초기화를 하여 가져왔어도 엔티티 정보가 그대로 노출되기 때문에 OrderDTO 내 OrderItem에 대해서도 OrderItemDTO를 만들어 사용해야 한다.
V2 마무리
지연로딩으로 인해 너무나 많은 SQL이 실행되는 단점이 있다. 추가로 DTO로 변환하여 사용하라는 것은 단순히 엔티티를 DTO로 감싸는 것이 아니라 엔티티에 대한 의존을 완전히 끊으라는 것이다.
V3. 엔티티를 DTO로 변환 - 페치조인 최적화
// OrderApiController.java
@GetMapping(value = "/api/v3/orders")
public List<OrderDTO> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDTO> result = orders.stream()
.map(o -> new OrderDTO(o))
.collect(Collectors.toList());
return result;
}
// OrderRepository.java
/**
* fetch join 사용
* @return
*/
public List<Order> findAllWithItem() {
List<Order> result = em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item i", Order.class)
.getResultList();
return result;
}
- 페치 조인 사용으로 SQL이 1번만 실행된다.
DISTINCT를 사용하여 중복 데이터 제거
/**
* fetch join 사용
* @return
*/
public List<Order> findAllWithItem() {
List<Order> result = em.createQuery(
"select distinct o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item i", Order.class)
.getResultList();
return result;
}
- DISTINCT
(1) SQL의 DISTINCT와 동일
(2) 루트 엔티티가 중복인 경우 중복을 걸러주는 기능 - Order와 OrderItem은 일대다 관계이기 때문에 Order에 DISTINCT를 사용하여 중복 Order를 제거한다.
V3 마무리
페치조인을 사용하여 실행 SQL을 줄일 수 있는 장점이 있지만 일대다 관계를 페치조인하는 순간 데이터가 1 x N으로 뻥튀기 되기 때문에 페이징이 불가능하다. 또한, 하이버네이트가 경고 로그를 남기면서 모든 데이터를 DB에서 읽어와 메모리에서 페이징하기 때문에 시스템상 매우 위험하다. 추가적으로 컬렉션 페치조인은 1개만 사용할 수 있다. 둘 이상 사용할 경우 1 x N x M으로 데이터가 뻥튀기 되기 때문에 데이터 정합에 문제가 발생한다.
V3.1. 엔티티를 DTO로 변환 - 페이징과 한계 돌파
컬렉션을 페치조인하면 페이징 불가
- 컬렉션을 페치조인하면 일대다 조인이 발생하므로 데이터가 예측할 수 없이 증가한다.
- 일대다에서 일(1)을 기준으로 페이징을 해야하는 것이 맞는데, 데이터는 다(N)를 기준으로 ROW가 생성된다.
- 하이버네이트가 경고 로그를 남기고 모든 DB의 데이터를 조회하여 메모리에서 페이징하기 때문에 시스템상 매우 위험하다.
페이징과 컬렉션 엔티티 조회하는 방법
- XToOne(
@OneToOne
,@ManyToOne
) 관계를 모두 페치조인한다.
(XToOne 관계는 ROW수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.) - 컬렉션은 지연로딩으로 조회한다.
- 지연로딩 성능 최적화를 위해
hibernate.default_batch_fetch_size
,@BatchSize
를 적용한다.hibernate.default_batch_fetch_size
:application.yml
에 설정(전역설정)@BatchSize
: 개별 설정
해당 옵션을 사용하면 컬렉션이나 프록시 객체를 한꺼번에 설정한 size만큼 IN 쿼리로 조회한다.
소스 구현
// OrderRepository.java
/**
* fetch join + paging
* @param offset
* @param limit
* @return
*/
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
List<Order> result = em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class
).setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
return result;
}
// OrderApiController.java
/**
* V3.1 엔티티를 DTO로 변환 - 페이징과 한계 돌파
* ToOne 관계만 우선 모두 페치 조인으로 최적화
* 컬렉션 관계는 hibernate.default_batch_fetch_size, @BatchSize로 최적화
*/
@GetMapping(value = "/api/v3.1/orders")
public List<OrderDTO> ordersV3_page(
@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "100") int limit
) {
List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);
List<OrderDTO> result = orders.stream()
.map(o -> new OrderDTO(o))
.collect(Collectors.toList());
return result;
}
// application.yml
spring:
jpa:
properties:
hibernate:
'[default_batch_fetch_size]': 100
- 최적화 옵션
-application.yml
내properties.hibernate.default_batch_fetch_size
로 설정하여 전역으로 사용하거나@BatchSize
어노테이션을 개별로 적용하여 최적화 할 수 있다.
-@BatchSize
어노테이션은 컬렉션은 컬렉션 필드에 엔티티는 엔티티 클래스에 적용해야 한다.
동작 결과
최적화 결과
- 쿼리 호출수가 1 + N + M에서 1 + 1 + 1로 최적화 됐다. 왜냐하면 IN절을 사용하기 때문이다.
- 조인보다 DB데이터 전송량이 중복 데이터가 적기 때문에 최적화 된다.
- 페치조인 방식보다는 쿼리 호출수가 많지만 데이터 전송량이 적다.
- 페치조인은 페이징이 불가능하지만 이 방법은 페이징이 가능하다.
※ 참고로 default_batch_fetch_size
의 크기는 0 ~ 1000을 권장한다. 왜냐하면 지정한 크기를 사용하여 SQL IN절을 사용하는데 데이터베이스에 따라 IN절 파라미터를 1000개로 제한하기도 하기 때문이다. 결과적으로 1000으로 설정하는 것이 성능상 가장 좋지만 데이터베이스 입장에서 한번에 1000개를 애플리케이션으로 불러오기 때문에 데이터베이스에 순간 부하가 증가할 수 있어 문제가 발생할 수 있기 때문에 데이터베이스와 애플리케이션이 순간 부하를 어느정도 견딜 수 있는지를 확인하여 설정하는 것이 좋다. (애플리케이션은 설정값에 상관 없이 전체 데이터를 로딩해야 하므로 메모리 사용량은 같다.)
V3.1 마무리
XToOne관계는 페치조인을 해도 페이징에 영향을 주지 않는다. 따라서 XToOne 관계는 페치조인으로 쿼리수를 줄여서 해결하고 나머지는 최적화 옵션을 사용하여 페이징 및 최적화를 하는 것을 권장한다.
V4. JPA에서 DTO 직접 조회
XToOne관계와 XToMany관계 별도 조회 후 값 셋팅
OrderApiController
@GetMapping(value = "/api/v4/orders")
public List<OrderQueryDTO> ordersV4() {
return orderQueryRepository.findOrderQueryDTOs();
}
OrderQueryRepository
package jpabook.jpashop.repository.order.query;
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
/**
* JPA에서 DTO 직접 조회
* XToOne 관계는 한번에 조회하고
* 컬렉션은 별도로 조회하여 루트 조회한 결과에 셋팅
* Query : 루트 1번, 컬렉션 N번
* 단건 조회에서 많이 사용하는 방식
* @return
*/
public List<OrderQueryDTO> findOrderQueryDTOs() {
// root 조회 (XToOne 관계 한번에 조회)
List<OrderQueryDTO> result = this.findOrders();
// 루프 돌면서 컬렉션 조회(추가 쿼리 실행)
result.forEach(o -> {
List<OrderItemQueryDTO> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
/**
* XToOne 관계 조회
* Order, Member, Delivery
* @return
*/
private List<OrderQueryDTO> findOrders() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderQueryDTO(o.id, m.name, o.orderDate, o.status, d.address)" +
" from Order o " +
" join o.member m " +
" join o.delivery d ", OrderQueryDTO.class)
.getResultList();
}
/**
* 컬렉션 조회
* @param orderId
* @return
*/
private List<OrderItemQueryDTO> findOrderItems(Long orderId) {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDTO(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi " +
" join oi.item i " +
" where oi.order.id = :orderId", OrderItemQueryDTO.class)
.setParameter("orderId", orderId)
.getResultList();
}
}
- 일반 Repository는 엔티티를 조회하는 용도로 사용되고 특정 패키지 내 Repository는 특정 기능에 맞춤으로 구성하여 사용된다.
- OrderApiController 내 DTO를 참조하지 않은 이유는 Repository에서 Controller를 참조하는 순환관계가 되기 때문이다.
- new 키워드는 SQL처럼 사용해야 해서 생성자에 컬렉션을 넣을 수 없다.
OrderQueryDTO
package jpabook.jpashop.repository.order.query;
@Data
public class OrderQueryDTO {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDTO> orderItems;
public OrderQueryDTO(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;
}
}
OrderItemQueryDTO
package jpabook.jpashop.repository.order.query;
@Data
public class OrderItemQueryDTO {
@JsonIgnore
private Long orderId;
private String itemName;
private int orderPrice;
private int count;
public OrderItemQueryDTO(Long orderId, String itemName, int orderPrice, int count) {
this.orderId = orderId;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}
- 결과에 표시하지 않을 항목에
@JsonIgnore
어노테이션을 사용한다.
V4 마무리
Order + Member + Delivery를 조회하는 루트 SQL 1번, OrderItem + Item을 조회하는 컬렉션 SQL 2번이 실행되면서 N+1문제가 발생한다. ROW수가 증가하지 않는 XToOne관계를 먼저 조회하고 XToMany관계를 조회하여 XToOne 관계의 조회 결과에 셋팅하여 결과를 표시하면 중복 데이터가 발생하지 않는다.
V5. JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화
파라미터 비교를 IN절로 구성
// OrderApiController.java
@GetMapping(value = "/api/v5/orders")
public List<OrderQueryDTO> ordersV5() {
return orderQueryRepository.findAllByDTO_optimization();
}
// OrderQueryRepository.java
/**
* 컬렉션 조회 최적화
* @return
*/
public List<OrderQueryDTO> findAllByDTO_optimization() {
List<OrderQueryDTO> result = findOrders();
Map<Long, List<OrderItemQueryDTO>> orderItemMap = findOrderItemMap(toOrderIds(result));
// 주문상품 셋팅
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
/**
* XToOne 관계 조회
* Order, Member, Delivery
* @return
*/
private List<OrderQueryDTO> findOrders() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderQueryDTO(o.id, m.name, o.orderDate, o.status, d.address)" +
" from Order o " +
" join o.member m " +
" join o.delivery d ", OrderQueryDTO.class
).getResultList();
}
/**
* 주문번호 조회
* @param result
* @return
*/
private List<Long> toOrderIds(List<OrderQueryDTO> result) {
return result.stream().map(o -> o.getOrderId()).collect(Collectors.toList());
}
/**
* 주문상품 컬렉션(Map) 조회
* @param orderIds
* @return
*/
private Map<Long, List<OrderItemQueryDTO>> findOrderItemMap(List<Long> orderIds) {
List<OrderItemQueryDTO> orderItems = em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDTO(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi " +
" join oi.item i " +
" where oi.order.id in :orderIds", OrderItemQueryDTO.class
).setParameter("orderIds", orderIds)
.getResultList();
return orderItems.stream().collect(Collectors.groupingBy(OrderItemQueryDTO::getOrderId));
}
- Map을 사용하여 매칭 성능 향샹(key, value로 구성)
- XToOne 관계를 먼저 조회하고 얻은 결과에서 OrderId를 List로 구성하여 컬렉션 식별자로 사용한다.
V5 마무리
V4에서 발생한 N+1문제를 파라미터 비교를 IN절로 변경하여 구성함으로써 최종 실행쿼리가 1 + 1로 최적화되었다.
컬렉션 데이터에 대한 조회는 비교할 파라미터를 List로 구성하고 IN절로 변경하여 구현하면 된다.
V6. JPA에서 DTO 직접 조회 - 플렛 데이터 최적화
컬렉션도 같이 조회
// OrderApiController.java
@GetMapping("/api/v6/orders")
public List<OrderQueryDTO> ordersV6() {
List<OrderFlatDTO> flats = orderQueryRepository.findAllByDto_flat();
return flats.stream().collect(
groupingBy(o -> new OrderQueryDTO(o.getOrderId(), o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()), Collectors.mapping(o -> new OrderItemQueryDTO(o.getOrderId(), o.getItemName(), o.getOrderPrice(), o.getCount()), toList()))
).entrySet().stream().map(e -> new OrderQueryDTO(e.getKey().getOrderId(), e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(), e.getKey().getAddress(), e.getValue())).collect(toList());
}
// OrderQueryDTO.java
// 생성자 추가
public OrderQueryDTO(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address, List<OrderItemQueryDTO> orderItems) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.orderItems = orderItems;
}
// OrderQueryRepository.java
public List<OrderFlatDTO> findAllByDto_flat() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderFlatDTO(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
" from Order o" +
" join o.member m" +
" join o.delivery d" +
" join o.orderItems oi" +
" join oi.item i", OrderFlatDTO.class
).getResultList();
}
- OrderFlatDTO 타입으로 조회한 결과를 stream을 이용하여 OrderItemQueryDTO를 넣어서 OrderQueryDTO 형태로 리턴하는 로직이다.
OrderFlatDTO
package jpabook.jpashop.repository.order.query;
@Data
public class OrderFlatDTO {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private Address address;
private OrderStatus orderStatus;
private String itemName;
private int orderPrice;
private int count;
public OrderFlatDTO(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address, String itemName, int orderPrice, int count) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}
V6 마무리
최종 실행 쿼리는 1번이지만 컬렉션 데이터로 인해 중복 데이터가 발생하고 애플리케이션에서 추가 작업(stream을 사용한 분해 등)이 필요하고 페이징이 불가능하다.
정리
엔티티 조회
- V1 : 엔티티를 그대로 반환하기 때문에 정보 노출 문제가 발생한다.
- V2 : 엔티티 조회 후 DTO로 변환한다.
- V1, V2 : 조인이 들어갈 경우 성능이 잘 안나온다.
- V3 : 페치조인으로 쿼리수를 최적화 하지만 컬렉션 페치조인 시 페이징이 불가능하다.
- V3.1 : XToOne 관계는 페치조인을 사용하고 컬렉션은 페치조인 대신 지연로딩을 유지하고
hibernate.default_batch_fetch_size
또는@BatchSize
를 사용하여 페이징 및 최적화한다.
DTO 직접 조회
- V4 : new 키워드를 사용하며 컬렉션 사용이 불가능하다.
- V5 : 일대다 관계인 컬렉션은 IN절을 사용하여 메모리에 미리 조회해서 최적화한다.
- V6 : 조인 결과를 그대로 조회 후 애플리케이션에서 원하는 모양으로 직접 변환한다.
권장 순서
- 엔티티 조회 방식으로 우선 접근
- 페치조인으로 쿼리수 최적화
- 컬렉션 최적화 (페이징 필요시hibernate.default_batch_fetch_size
또는@BatchSize
사용 페이징 필요 없을 경우 페치조인 사용) - 엔티티 조회방식으로 해결이 안되면 DTO 조회 방식 사용
- DTO 조회 방식으로 해결이 안되면 NativeSQL 또는 스프링 Template 사용
※ 참고 1
엔티티 조회 방식은 페치조인이나 hibernate.default_batch_fetch_size
, @BatchSize
와 같이 코드를 거의 수정하지 않고 옵션만 변경해서 다양한 성능 최적화를 시도할 수 있다. 반면 DTO를 직접 조회하는 방식은 성능을 최적화하거나 성능 최적화 방식 변경 시 많은 코드를 변경해야 한다.
실무에서는 엔티티 조회 방식으로하여 페치조인이나 옵션을 넣어 성능 이슈가 어느정도 해결되지만 이것으로도 해결이 안된다면 DTO를 직접 조회하는 방식을 사용하는 것 이전에 캐시를 사용하여 해결하는 것을 추천한다. 왜냐하면 DTO 방식을 사용한다해도 해결된다는 보장이 없기 때문이다.
※ 주의
엔티티는 직접 캐싱을 하면 안된다. 영속성 컨텍스트에 의해 관리되고 상태가 있기 때문에 캐시에 올라가게 되면 지워지지 않아 영속성 컨텍스트의 관리기능과 꼬일 수 있다. 그렇기 때문에 캐시해야 하는 것을 DTO로 변환하여 DTO를 캐싱해야 한다.
※ 참고 2
개발자는 성능 최적화와 코드 복잡도 사이에서 줄타기를 해야 한다. 보통 성능 최적화는 단순한 코드를 복잡한 코드로 몰고간다. 엔티티 조회 방식의 장점은 JPA가 많은 부분을 최적화 해주기 때문에 단순한 코드를 유지하면서 성능을 최적화 할 수 있지만 DTO 조회 방식은 SQL을 직접 다루는 것과 유사하기 때문에 성능 최적화와 코드 복잡도 사이에서 줄타기를 해야 한다.
DTO 조회 방식의 선택지
DTO 조회 방식도 각각의 장단점이 있다.
- V4, V5, V6에서 단순하게 쿼리가 1번 실행된다해서 V6가 항상 좋은 방법은 아니다.
- V4는 코드가 단순하고 유지보수가 용이하여 특정 데이터 한건만 조회하면 성능이 잘 나온다.
- V5는 코드가 복잡하지만 쿼리 실행수가 1+1번만 실행되고 페이징도 가능하며 성능도 V4보다 훨씬 좋다.
- V6는 V4, V5와 다른 방식으로 쿼리는 1번만 실행되지만 중복 데이터가 발생하여 성능도 V5와 큰 차이도 없고 페이징이 불가능하며 코드가 매우 복잡하다.
실무에서는 데이터를 한번에 전송하기보단 끊어서 전송하기 때문에 V5를 많이 사용하거나 엔티티 조회 방식의 옵션 설정을 많이 이용한다.
'Dev > JPA' 카테고리의 다른 글
[JPA] JPA 활용 II - API 개발 고급 (실무 필수 최적화) (0) | 2021.10.20 |
---|---|
[JPA] JPA 활용 II - API 개발 고급 (지연로딩과 조회성능 최적화) (0) | 2021.10.04 |
[JPA] JPA 활용 II - API 개발 고급 (준비) (0) | 2021.09.29 |
[JPA] JPA 활용 II - API 개발 기본 (0) | 2021.09.29 |
[JPA] JPA 활용 II - 강좌 소개 (0) | 2021.09.29 |
댓글