DTO를 활용하면 엔티티와 프레젠테이션 계층을 위한 로직을 분리할 수 있다.
엔티티와 API 스펙을 명확하게 분리할 수 있다.
엔티티가 변해도 API 스펙이 변하지 않아 기능 변경시 수정할 코드가 줄어든다.
반면 엔티티를 파라미터로 받거나 반환값으로 사용하면 엔티티에 API 검증을 위한 로직이 추가되어
순수한 엔티티가 만들어지지 않는다.
실무에서 하나의 엔티티를 위한 API가 다양하게 만들어지는데 각각의 API를 위한 모든 요청 요구사항을 담기 어렵다.
페치 조인을 사용하지 않으면 지연로딩 설정으로 인해 쿼리를 N번 호출한다.
@Data
class SimpleOrderDto {
private Long orderId;
private String name;
private Address address;
public SimpleOrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName(); // 오더가 N개 조회되면 각각의 멤버를 구하는 쿼리 N개가 나간다
address = order.getDelivery().getAddress(); // 오더가 N개 조회되면 각각의 딜리버리를 구하는 쿼리 N개가 나간다
}
}
일반적으로 조회하면 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();
}
엔티티를 페치 조인으로 쿼리 한번에 조회. 페치 조인으로 멤버와 딜리버리는 이미 조회 된 상태이므로 지연로딩 발생 안함
레포지토리에서 DTO를 사용해서 직접 쿼리한다 JPQL의 경우 new 키워드를 사용하자
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();
}
}
엔티티를 DTO로 변환하는 방법을 택한 후 페치 조인으로 성능을 최적화 하자
그래도 안되면 DTO로 직접 조회하자
최후의 방법으로 네이티브 SQL이나 스프링 JDBC Templete를 사용하자
컬렉션 조회도 마찬가지로 N+1 문제가 발생한다.
XToOne 관계와 마찬가지로 N+1 문제가 발생할 수 있는 컬렉션에 JPQL 쿼리를 따로 작성한다.
public List<Order> findAllWithItem() {
return 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();
}
distinct를 사용한 이유는 일대다 조인이 있으므로 데이터베이스의 row 수가 증가한다. 그래서 같은 엔티티가 조회되므로 이를 방지하기 위해 사용한다.
컬렉션 페치 조인을 사용하면 페이징이 불가능하다는 단점이 있고 두개 이상 페치 조인을 사용하면 데이터가 부정확하게 조회될 수 있다.
다대일, 일대일 관계를 모두 페치조인 한다. ToOne 관계는 row 수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다
컬렉션은 지연 로딩으로 조회하는데, 지연 로딩 성능 최적화를 위해hibernate.default_batch_fetch_size, @BatchSize를 적용한다
컬렉션이나 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회해서 N+1 문제를 해결한다
size의 크기는 적당한 사이즈를 골라야 한다. 숫자가 높을수록 순간 부하가 커지기 때문에 100~1000 사이를 선택하자
레포지토리 예시
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
컨트롤러 예시
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(toList());
return result;
}
2-2에서와 같이 ToOne 코드를 모두 한번에 조회한다
나머지 컬렉션은 MAP을 활용해 조인을 사용해서 조회한다. IN절 활용
루트를 돌면서 반환할 DTO에 추가한다(추가 쿼리 실행X)
예)
public List<OrderQueryDto> findAllByDto_optimization() {
//루트 조회(toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders();
//orderItem 컬렉션을 MAP 한방에 조회
Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));
//루프를 돌면서 컬렉션 추가(추가 쿼리 실행X)
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
엔티티 조회 방식으로 접근하자
단건 조회일 경우 또는 단순한 쿼리일 경우 N+1 문제를 생각하지 말고 단순하게 코딩하자
페이징이 필요하면 hibernate.default_batch_fetch_size, @BatchSize로 최적화
페이징이 필요 없으면 페치 조인을 사용한다
OSIV는 Open Session In View의 약자로 true 또는 false로 설정할 수 있다.
true일 경우 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다.
하지만 너무 오랫동안 커넥션 리소스를 사용하기 때문에 커넥션이 모자랄 수 있다는 단점이 있다.
false일 경우 트랜잭션이 끝날 때 영속성 컨텍스트를 닫고 DB 커넥션도 반환한다. 커넥션 리소스를 낭비하지 않는다.
하지만 모든 지연로딩을 트랜잭션 안에서 처리해야 해서 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해 두어야 한다.
따라서 OSIV를 끄고 커맨드와 쿼리를 분리하면 성능을 챙길 수 있다.
서비스 계층에서 트랜잭션을 유지하므로 컨트롤러로 넘어가기 전에 DB와의 일을 끝내야 한다.
커맨드는 XXXService 라는 이름으로 핵심 비즈니스 로직(명령)을 작성하고
쿼리는 XXXQueryService 라는 이름으로 화면이나 API에 맞춘 읽기 전용 서비스 로직을 작성한다.
출처 : 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화