Study/Java&Spring

SpringBoot 조회 API

kdhoooon 2022. 1. 4. 16:32

상황

Order 는 Member, Delivery 와 양방향 연관관계에 있는 엔티티다.

Order를 조회하는 api를 개발할 것이다.

 

1. 엔티티를 노출하면서 조회하는 가장 기본적인 방식 사용

엔티티를 노출한 상태에서 조회 했을 때 생기는 문제

  • 양방향 관계에 있는 것들이 계속 조회를 하면서 무한 루프를 돌게 된다.

 

예시코드

@GetMapping("/api/v1/simple-orders")
  public List<Order> ordersV1(){
  List<Order> all = orderRepository.findAllByString(new OrderSearch());
  return all;
}

위와 같이 엔티티를 그대로 노출하면서 조회를 하였다.

 

여기서 Order 는

Order -> Member (ManyToOne)

Order -> Delivery (OneToOne)

으로 양방향 연관관계에 있다.

 

무한루프를 도는 오류

위 사진과 같이 무한루프를 돌게 되었다.

이를 해결하기 위해서는 양방향 연관관계에 있는 메서드에 @JsonIgnore 어노테이션을 사용해서 막을 수 는 있다. (하지만 이렇게 했을 경우 또다른 문제가 발생했다)

 

위 오류는 Spring에서는 프록시를 기본으로 사용하기 때문에 지연로딩을 설정한 상태에서 생기는 오류다.

지연로딩시에 ByteBuddyInterceptor 위치에 가짜 프록시객체를 생성해서 넣어둔다.
그리고 해당 객체를 사용할 때 DB에서 값을 가져오는 방식을 사용한다.
하지만 루프를 도는 상황에서는 프록시객체 상태이기 때문에 값을 불러올수 없는 오류다.

이 문제도 해결하기 위해서 Hibernate5Module을 이용할 수 있다.

build.gradle 파일에 모듈 설치를 위해 아래 코드를 적어준다.

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

Application 클래스에 아래 빈을 등록해준다.

@Bean
Hibernate5Module hibernate5Module(){
  Hibernate5Module hibernate5Module = new Hibernate5Module();
  hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true);
  return hibernate5Module;
}

코드설명을 하자면, Hibernate5Module.Feature.FORCE_LAZY_LOADING 을 하여 강제로 지연로딩에 있는 값을 모두 불러오는 것이다.

강제로 로딩을 하지 않으면 null 값으로 지연로딩에 있는 값은 가져오지 않는 상태로 나오게 된다.

이렇게 하면 엔티티를 직접 노출하여도 무한루프 도는 오류를 해결할 수 있다.

 

또는

 

프록시 상태로 있는 객체를 불러서 강제로딩할 수 있다. ( 이 방법은 Hibernate5Module을 사용하는 방식보다는 Api 스펙에 맞는 값만 불러올 수 있다는 장점이 있다.)

 

 

★★★★중요★★★★

하지만, 엔티티를 직접 노출하는 것은 단점이 너무 많고 유지보수가 어려워 질 수 있기 때문에, 간단한 서비스가 아니면 추천하지 않는 방식이다.

위와 같은 오류를 방지하기 위해서 DTO로 변환해서 반환하는 것이 좋은 방법이다.

 

 

2. DTO 를 사용하여 무한루프 도는 문제 해결

static class OrderDto{
  private Long orderId;
  private String name;
  private LocalDateTime orderDate;
  private OrderStatus orderStatus;
  private Address address;

  public SimpleOrderDto(Order order){
  orderId = order.getId();
  name = order.getMember().getName();
  orderDate = order.getOrderDate();
  orderStatus = order.getStatus();
  address = order.getDelivery().getAddress();
  }
}

위와 같이 OrderDto 클레스를 선언하여 api 스펙에 필요한 데이터만 불러오는 방식을 사용하였다.

 

결과적으로 문제는 없었다.

하지만, 이렇게 할 경우에도 성능에서 이슈가 발생한다.

order.getMember().getName(), order.getDelivery().getAddress() 이 두가지 코드에서 성능 저하가 일어난다고 볼 수 있다.

 

Order 의 값을 가져오기위해 Order, Member, Delivery 총 세가지의 테이블을 조회해야하는 것이다.

이럴경우 한개만 조회하면 문제가 되지 않지만, 여러개 order를 조회 할경우는 n + 1 문제가 발생한다.

 

n + 1 문제란?

1개의 값을 조회하기 위해서 n 개의 쿼리가 추가로 발생하는 경우

 

3. Fetch Join 을 사용하여 n + 1 문제 해결

JPA에 있는 join fetch 기능을 이용해서 query 한번에 Order, Member, Delivery 테이블을 조회하는 방법이다.

 

<예시코드>

public List<Order> findAll() {
    return em.createQuery(
            "select o from Order o" +
                    " join fetch o.member m" +
                    " join fetch o.delivery d", Order.class
    ).getResultList();
}

위와 같이 코드가 있다고 하면,

Order 의 Member 값과 Delivery값을 join fetch 를 사용하여 모두 가져올 수 있다.

 

사용방식은 좀 더 공부해보면 좋을 것 같다.

 

하지만 이 코드에도 단점이 있다.

select 에서 모든 엔티티를 끌고온다는 단점이 있다.

이를 최적화 하는 방법을 알아보자.

 

 

4. JPA에서 바로 DTO로 조회하는 방식으로 최적화

 

해당 api에 필요한 값들을 불러와서 DTO에 바로 넣어주는 방식으로 필요없는 select 문을 줄여 성능을 최적화 할 수 있었다.

 

<예시코드>

public List<OrderSimpleQueryDto> findOrderDtos(){
    return em.createQuery(
            "select new jpabook.japshop.repository.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();
}

 

★★★★중요★★★★

 

4번째 경우가 성능에서는 우세하지만 무조건적으로 사용하기는 어렵다.

이유는,

3번의 방법은 해당 조회 말고도 다른 곳에서도 사용할 수 있게 유연하게 변경이 가능하다.

4번의 api 스팩에 맞게 이미 repository가 고정되는 방식이므로 속도에서는 빠르나 해당 api 말고 다른곳에서 사용이 어렵다. 변경에 딱딱하다는 단점이 있다.(respository 순수성이 유지되지 않는 방식, 새로운 api 스펙이 필요하면 또 새롭게 코드를 작성해야 한다, 코드도 복잡하다)

 

3번보다 4번이 성능이 우세하지만 4번의 장점(3번보다 4번이 속도가 크게 차이나지는 않다)보다는 단점이 더욱 크기 때문에, 3번을 사용하는 것이 보통의 경우에는 3번까지의 튜닝을 권장한다.(단, traffic이 너무 큰 api 라면 4번의 경우가 우세하므로 고려해볼만한 방법이다.)

 

따라서 정말 성능 최적화를 위해 필요하다면, 필요에 맞게 따로 package로 모아서 관리하는 것이 좋다.(Repositroy 는 최대한 순수하게 유지하기 위해서)

 

 

5. JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용

  • 직접 네이티브 SQL이나 JDBC에서 제공하는 Template를 이용해서 API에 최적화 된 코드를 제공하는 방식이다.

'Study > Java&Spring' 카테고리의 다른 글

JPA Flush(플러시)  (0) 2022.01.17
JPA Fetch Join 튜닝  (0) 2022.01.15
@Transactional Annotation 과 AOP 그리고 CGLib 와 Dynamic Proxy(JDK Proxy)  (0) 2022.01.03
JPA 정의와 사용  (0) 2021.12.30
Spring MVC  (0) 2021.12.26