김영한 개발자님의 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의를 수강하고 중요한 점이나 인상깊었던 점들을 요약, 정리했습니다.
@PostMapping("/api/v1/members") // 요청 값으로 Member 엔티티를 직접 받는다.
public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member)
{
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@NotEmpty
등)을 위한 로직이 추가됨@PostMapping("/api/v2/members")
public CreateMemberResponse saveMemberV2(@RequestBody @Valid
CreateMemberRequest request) {
Member member = new Member();
member.setName(request.getName());
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberRequest {
private String name;
}
실무에서는 엔티티를 API 스펙에 노출하면 안됨
@GetMapping("/api/v1/members")
public List<Member> membersV1() {
return memberService.findMembers();
}
@GetMapping("/api/v2/members")
public Result membersV2() {
List<Member> findMembers = memberService.findMembers();
// 엔티티 -> DTO 변환
List<MemberDto> collect = findMembers.stream()
.map(m -> new MemberDto(m.getName()))
.collect(Collectors.toList());
return new Result(collect);
}
@Data // 컬렉션을 감싸서 향후 필요한 필드를 추가할 수 있다.
@AllArgsConstructor
static class Result<T> {
private T data;
}
@Data
@AllArgsConstructor
static class MemberDto {
private String name;
}
엔티티가 변해도 API 스펙이 변경되지 않는다.
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();
}
위와 같이 페치 조인으로 성능을 최적화하면 대부분의 성능 이슈가 해결됨
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로 직접 조회시 SELECT 절에서 원하는 데이터를 직접 선택하므로 DB 애플리케이션 네트웍 용량 최적화.
하지만 단점은 API 스펙에 맞춘 코드가 리포지토리에 들어가 재사용성이 떨어진다.
먼저 row수를 증가시키지 않는 ToOne(OneToOne
, ManyToOne
) 관계를 모두 페치조인 하고, 컬렉션은 지연 로딩으로 조회
// ToOne 관계만 우선 모두 페치 조인으로 최적화
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();
}
@GetMapping("/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(toList());
return result;
}
컬렉션 필드나 엔티티 클래스에 @BatchSize
를 적용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회
# application.yml 설정 파일에도 가능
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
마찬가지로 한계 돌파를 이용한다.
하지만 컬렉션마다 쿼리를 날려 1+N이 되므로 일대다 관계인 컬렉션은 IN 절을 활용해서 메모리에 미리 조회해서 최적화할 수 있다.
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;
}
private List<Long> toOrderIds(List<OrderQueryDto> result) {
return result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
}
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" + // in절을 활용하여 쿼리 1번
" where oi.order.id in :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList();
return orderItems.stream()
.collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
}
hibernate.default_batch_fetch_size
, @BatchSize
로 최적화 / 필요 없을시 페치 조인 사용엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용
DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate