ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • API 개발과 성능 최적화
    김영한(인프런 강의)/실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 2021. 2. 6. 13:57
    반응형

    코드

    github.com/rlawls1991/Study_JPA_API

     

     

     

    지연 로딩의 문제점

    버전코드

    github.com/rlawls1991/Study_JPA_API/commit/61b3534a503114dc6554cbf6433de3e4c1b2ddff

     

     

    아래 컨트롤러를 보자 이 코드는 모든 주문들의 List을 가져오는 Controller API이다.

    @RestController
    @RequiredArgsConstructor
    public class OrderSimpleApiController {
        private final OrderRepository orderRepository;
        /**
         * V1. 엔티티 직접 노출
         * - Hibernate5Module 모듈 등록, LAZY=null 처리
         * - 양방향 관계 문제 발생 -> @JsonIgnore
         */
        @GetMapping("/api/v1/simple-orders")
        public List<Order> ordersV1() {
            List<Order> all = orderRepository.findAllByString(new OrderSearch());
            return all;
        }
    }

    보기에는 아무런 문제가 없어보이지만 실행시키게 된다면 아래와 같은 문제가 발생이된다.

    오류

    무슨 오류일까? 코드를 한번 보자

    Order에는 Member와 관계를 맺고 있고, Member는 Order와 관계를 맺고있다. 이렇게 된다면 서로가 서로를 바라보는 즉 무한로딩에 빠지게 된다.

    Order
    Member

    첫번째 해결방안

    @JsonIgonre 사용

    한쪽의 Entitiy에 걸어주어 무한루프에 빠지지 않게 해준다.

    하지만 다음과 같은 오류가 발생이 된다. 

    이 오류가 어떤 오류인지 알아보자.

     

    우선 member라는 객체는 new를 사용해서 객체를 생성하지 않았고, DB에서 데이터를 가지고 있지 있지 않다. 그 이유는 지연 로딩이다.

    지연로딩
    @OneToMany는 기본적으로 지연로딩이다.
    지연로딩이 설정되어있는 객체는 실제 select 할때 데이터를 가져오는 형태

    Member의 지연로딩

     

    이 지연로딩을 해결할 수 있는 방법은 아래와 같다.

    Hibernate5Module 사용

    Hibernate5Module에 대한 의존성 추가 후 Bean으로 등록해준다. 

    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
    @Bean
    public Hibernate5Module hibernate5Module(){
        Hibernate5Module hibernate5Module = new Hibernate5Module();
        /*
        강제 지연 로딩!
        엔티티에 관련된 모든 데이터 가져온다.
        hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING,
                true);
        */
        return hibernate5Module;
    }

    Hibernate5Module에 설정 중 지연로딩을 바로 실행시켜주는 로직이 있는데 위와같다. 하지만 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다. 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워 진다.

    항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용

     

     

    엔티티를 DTO로 변환

    위의 결과가 문제가 엔티티에 대한 정보를 바로 노출하게 된다는 것이다. 이를 방지하기 위해서는 DTO를 사용해야 한다. 그렇지만 아래와같은 문제점이 발생이 된다.

    • 쿼리가 총 1 + N + N번 실행된다. (v1과 쿼리수 결과는 같다.)
      • order 조회 1번(order 조회 결과 수가 N이 된다.)
      • order -> member 지연 로딩 조회 N 번
      • order -> delivery 지연 로딩 조회 N 번
      • 예) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다.
        • (최악의 경우) 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.

     

    현재 존재하는 데이터로 조회시

    Order1회 조회 1번할 시 member 2명, delivery2개를 하기 때문에 1 + 2 + 2 회 총 5회가 실행이 된다.

    2021-02-06 15:35:28.161 DEBUG 3524 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
        select
            order0_.order_id as order_id1_6_,
            order0_.delivery_id as delivery4_6_,
            order0_.member_id as member_i5_6_,
            order0_.order_date as order_da2_6_,
            order0_.status as status3_6_ 
        from
            orders order0_
    2021-02-06 15:35:28.185 DEBUG 3524 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
        select
            member0_.member_id as member_i1_4_0_,
            member0_.city as city2_4_0_,
            member0_.street as street3_4_0_,
            member0_.zipcode as zipcode4_4_0_,
            member0_.name as name5_4_0_ 
        from
            member member0_ 
        where
            member0_.member_id=?
    2021-02-06 15:35:28.186 TRACE 3524 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
    2021-02-06 15:35:28.192 DEBUG 3524 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
        select
            delivery0_.delivery_id as delivery1_2_0_,
            delivery0_.city as city2_2_0_,
            delivery0_.street as street3_2_0_,
            delivery0_.zipcode as zipcode4_2_0_,
            delivery0_.status as status5_2_0_ 
        from
            delivery delivery0_ 
        where
            delivery0_.delivery_id=?
    2021-02-06 15:35:28.192 TRACE 3524 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [5]
    2021-02-06 15:35:28.193 DEBUG 3524 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
        select
            member0_.member_id as member_i1_4_0_,
            member0_.city as city2_4_0_,
            member0_.street as street3_4_0_,
            member0_.zipcode as zipcode4_4_0_,
            member0_.name as name5_4_0_ 
        from
            member member0_ 
        where
            member0_.member_id=?
    2021-02-06 15:35:28.194 TRACE 3524 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [8]
    2021-02-06 15:35:28.194 DEBUG 3524 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
        select
            delivery0_.delivery_id as delivery1_2_0_,
            delivery0_.city as city2_2_0_,
            delivery0_.street as street3_2_0_,
            delivery0_.zipcode as zipcode4_2_0_,
            delivery0_.status as status5_2_0_ 
        from
            delivery delivery0_ 
        where
            delivery0_.delivery_id=?
    2021-02-06 15:35:28.194 TRACE 3524 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [12]
    

     

    엔티티를 DTO로 변환 - 페치 조인 최적화

    엔티티를 페치 조인(fetch join)을 사용해서 쿼리 1번에 조회

    페치 조인으로 order -> member , order -> delivery 는 이미 조회 된 상태 이므로 지연로딩X

    /**
     * V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용O)
     * - fetch join으로 쿼리 1번 호출
     * 참고: fetch join에 대한 자세한 내용은 JPA 기본편 참고(정말 중요함)
     */
    @GetMapping("/api/v3/simple-orders")
    public List<SimpleOrderDto> ordersV3() {
        List<Order> orders = orderRepository.findAllWithMemberDelivery();
        List<SimpleOrderDto> result = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());
        return result;
    }
    
    
    //OrderRepository 추가 코드
    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();
    }

     

      select
            order0_.order_id as order_id1_6_0_,
            member1_.member_id as member_i1_4_1_,
            delivery2_.delivery_id as delivery1_2_2_,
            order0_.delivery_id as delivery4_6_0_,
            order0_.member_id as member_i5_6_0_,
            order0_.order_date as order_da2_6_0_,
            order0_.status as status3_6_0_,
            member1_.city as city2_4_1_,
            member1_.street as street3_4_1_,
            member1_.zipcode as zipcode4_4_1_,
            member1_.name as name5_4_1_,
            delivery2_.city as city2_2_2_,
            delivery2_.street as street3_2_2_,
            delivery2_.zipcode as zipcode4_2_2_,
            delivery2_.status as status5_2_2_ 
        from
            orders order0_ 
        inner join
            member member1_ 
                on order0_.member_id=member1_.member_id 
        inner join
            delivery delivery2_ 
                on order0_.delivery_id=delivery2_.delivery_id

     

     

    엔티티를 DTO로 변환 : JPA에서 DTO로 바로 조회

    @Repository
    @RequiredArgsConstructor
    public class OrderSimpleQueryRepository {
        private final EntityManager em;
    
        public List<OrderSimpleQueryDto> findOrderDtos() {
            return em.createQuery(
                    "select new com.study.domain.dto.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();
        }
    }

    특징

    • 일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회
    • new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
    • SELECT 절에서 원하는 데이터를 직접 선택하므로 DB 애플리케이션 네트웍 용량 최적화(생각보다 미비)
    • 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

     

    페치조인과 JPA에서 DTO로 바로 조회 정리

    엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두가지 방법은 각각 장단점이 있다. 둘중 상황에 따라서 더 나은 방법을 선택하면 된다. 엔티티로 조회하면 리포지토리 재사용성도 좋고, 개발도 단순해진다. 

     

    쿼리 방식 선택 권장 순서

    1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
    2. 필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다.
    3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
    4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접

     

    반응형
Designed by Tistory.