SpringBoot & JPA API설계-1

7736 단어 SpringJPAJPA

본 포스트는 김영한 님의 인프런 강좌를 수강 후에 정리한 내용입니다.

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이 조금 더 권장되느 느낌이다.

좋은 웹페이지 즐겨찾기