SpringBoot & JPA API설계-1
본 포스트는 김영한 님의 인프런 강좌를 수강 후에 정리한 내용입니다.
API설계에 쓰일 엔티티의 관계만을 나타내는 간략한 ERD
주문 조회 API - v1
엔티티를 직접 노출
@GetMapping("/api/v1/simple-orders") public List<Order> ordersV1() { List<Order> orders = orderRepository.findAllByString(new OrderSearch()); for (Order order : orders) { order.getMember().getName(); // Lazy 강제 초기 order.getDelivery().getAddress(); // Lazy 강제 초기화 } return orders; }
- 엔티티를 조회하고 조회된 엔티티 리스트를 그대로 리턴
- 만약 엔티티가 변경된다면? API 스펙 자체가 바뀌게 됨
- 무한순환참조 방지를 위해 양방향 관계의 경우 엔티티에 @JsonIgnore설정
- 절대 엔티티를 직접 노출(리턴)하지 말자.
Tip
리턴시 제네릭 타입의 Wrapper클래스 사용
@AllArgsConstructor @Data static class OrderDtoWrapper<T> { private int orderCount; private T data; }
- 위와 같은 제네릭 타입으로 리턴 값을 감싼다면 Json입장에서 {data: [...] }의 형태가 되기 때문에 합계, 카운팅, 평균 등을 Json에 추가하기 용이하다.
예시
주문 조회 API - v2
엔티티를 DTO로 변환하여 노출
@GetMapping("/api/v2/simple-orders") public OrderDtoWrapper<List<OrderDto>> ordersV2() { List<Order> orders = orderRepository.findAllByString(new OrderSearch()); List<OrderDto> collect = orders.stream() .map(m -> new OrderDto(m)) .collect(Collectors.toList()); return new OrderDtoWrapper<List<OrderDto>>(collect.size(), collect); } }
- DTO로 변환하여 노출하므로 엔티티와 API스펙의 의존을 제거
- 또한 엔티티에서 노출시키고 싶은 데이터만 노출시킬 수 있다는 장점
- But N+1문제 발생
- N+1문제? (N명의 멤버가 N개의 주문, 배송정보도 N개)
- Order를 조회시 N명의 멤버(Member)와 N개의 배송정보(Delivery)가 있다고 가정
- Order를 한 번 조회했을 때, 한 개 Order내 의 N명의 멤버를 조회하기 위해 N번의 쿼리가 추가로 나간다.
- 또한 배송정보를 조회하기 위해 N번의 쿼리가 추가로 나간다.
- 2개 Order조회시 멤버2번 배송2번 총 5개 쿼리가 나가게 된다.
- N+1문제가 발생한 쿼리
주문 조회 API - v3
Fetch Join으로 N+1문제 해결
- Order와 연관관계에 있는 엔티티를 모두 한방 쿼리로 조회
- Lazy로 설정되어 있어도 무시하고 즉시 조회
때문에 N+1문제가 발생하지 않는다.API Controller, Repository 코드
@GetMapping("/api/v2/simple-orders") public OrderDtoWrapper<List<OrderDto>> ordersV2() { List<Order> orders = orderRepository.findAllByString(new OrderSearch()); List<OrderDto> collect = orders.stream() .map(m -> new OrderDto(m)) .collect(Collectors.toList()); return new OrderDtoWrapper<List<OrderDto>>(collect.size(), collect); }
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(); }
- Fetch Join을 적용한 쿼리
- Order와 연관관계에 있는 모든 엔티티를 Join하여 한 번에 가져온다.
주문 조회 API - v4
JPA에서 DTO를 직접 조회
- JPA에서 DTO타입으로 직접 조회하여 N+1문제를 해결하고, 엔티티의 필요한 필드만을 조회하여 select절을 줄이는 것을 목적으로 한다.
- API Controller 코드
@GetMapping("/api/v4/simple-orders") public List<OrderSimpleQueryDto> orderV4() { return orderSimpleQueryRepository.findOrderDtos(); }
- Repository 코드
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 코드
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; } }
- 쿼리
- Fetch Join보다 select절이 확실히 줄었다.
- 하지만 작성해야 하는 코드는 간단한 예제임에도 불구하고 많이 늘었났다.
- 늘어난 만큼 성능의 향상이 있는가??
기대하는 만큼의 성능향상을 기대하기는 어렵다고 한다.
여러 trade-off가 있겠지만 Fetch Join이 조금 더 권장되느 느낌이다.
Author And Source
이 문제에 관하여(SpringBoot & JPA API설계-1), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@dhk22/SpringBoot-JPA-API설계-1저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)