지연 로딩과 조회 성능 최적화

9899 단어 SpringJPAJPA

주문 + 배송정보 + 회원을 조회하는 API를 만들자.
지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해보자.

지금부터 하는 설명은 정말 중요하다고 언급
잘 안들으면 인생 허비스

V1 - 엔티티 직접 노출

음... 이렇게 쓰면 엔티티가 외부에 노출되는데, 이때 Member -> Order -> Member -> ... 무한루프를 돌게 된다.
그러므로 엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭 한곳을 @JsonIgnore처리해야 한다.

하지만, 그냥 DTO를 쓰는 것이 훨씬 좋다.
@JsonIgnore를 사용해도, LAZY 설정을 했다면, 해당 객체를 프록시로 생성하기 때문에 이것과 관련된 바이트예외가 던져진다. 이를 해결하기 위해
강의에서 Hibernate5Modle을 사용해서 더 수월하게 보내는 방법을 설명했지만, 그냥 DTO로 따로 보내는것이 훨씬 유지보수에 좋아 보인다.

V2 - 엔티티 DTO 변환

이 부분의 문제는
반복문을 돌면서
Order 하나 찾는 쿼리를 동작하면서
order.getMember().getName();
order.getDelivery().getAddress();
이 부분을 돌때마다 쿼리를 돌려야한다.
즉, 하나의 상품을 돌때 쿼리를 1 + 2번 날려야한다.
두개의 상품은 쿼리를 1 + 4번 날려야한다.
10개면 1 + 20번 날려야한다.

이게 N + 1 문제이다.

그러므로 성능에 문제가 발생할 수 있다는 것.

V3 - 페치 조인 최적화

이렇게 할 경우, 쿼리를 처음 한 번만 실행하게 된다.

V2

2022-03-17 09:18:13.346 DEBUG 72007 --- [nio-8080-exec-5] 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_ 
    inner join
        member member1_ 
            on order0_.member_id=member1_.member_id limit ?
Hibernate: 
    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_ 
    inner join
        member member1_ 
            on order0_.member_id=member1_.member_id limit ?
2022-03-17 09:18:13.396 DEBUG 72007 --- [nio-8080-exec-5] 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=?
Hibernate: 
    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=?
2022-03-17 09:18:13.403 DEBUG 72007 --- [nio-8080-exec-5] 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=?
Hibernate: 
    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=?
2022-03-17 09:18:13.408 DEBUG 72007 --- [nio-8080-exec-5] 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=?
Hibernate: 
    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=?
2022-03-17 09:18:13.414 DEBUG 72007 --- [nio-8080-exec-5] 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=?
Hibernate: 
    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=?

V3

2022-03-17 09:19:05.854 DEBUG 72007 --- [nio-8080-exec-6] org.hibernate.SQL                        : 
    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
Hibernate: 
    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

한 번에 전부 조회하여 넣어주기 때문에 쿼리를 한 번만 사용하는 것이다.
그로 인해 성능도 굉장히 빨라지게 된다.

하지만, 모든 데이터를 찍어야하는 단점이 존재한다.

V4 - JPA에서 DTO 바로 조회

2022-03-17 09:38:13.036 DEBUG 73510 --- [nio-8080-exec-5] org.hibernate.SQL                        : 
    select
        order0_.order_id as col_0_0_,
        member1_.name as col_1_0_,
        order0_.order_date as col_2_0_,
        order0_.status as col_3_0_,
        delivery2_.city as col_4_0_,
        delivery2_.street as col_4_1_,
        delivery2_.zipcode as col_4_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
Hibernate: 
    select
        order0_.order_id as col_0_0_,
        member1_.name as col_1_0_,
        order0_.order_date as col_2_0_,
        order0_.status as col_3_0_,
        delivery2_.city as col_4_0_,
        delivery2_.street as col_4_1_,
        delivery2_.zipcode as col_4_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에 넣어주는 방식이다.
더 최적화되었지만, V3이 좋은지 V4가 좋은지는 가리기가 애매하다.

V3의 경우에는 fetch join로 인해 결과를 만들었고, 이 데이터들은 재사용성이 가능하다. 단점은 한번에 많은 데이터를 가져온다는 점이다.

V4의 경우에는 딱 원하는 데이터만 가져오기 때문에 성능성에서는 좋다는 장점이 있다. 하지만, 이 Dto 전용이므로, 재사용을 할 수 없고, 코드도 지져분하다는 단점을 가지고 있다.

또, V3의 경우에는 Order엔티티를 가져오기 때문에 계층이 유지되지만, V4에서 리포지토리에 다른 Dto를 사용한다는 것 자체가 논리적으로 계층이 깨진다는 것을 볼 수 있다.

강사의 개인적인 의견으로는 V3V4는 사실상 성능차이가 크지 않기 때문에 V3를 선호하는 것 같다. 요즘에는 네트워크도 좋다는 점과, select가 많이 들어간다고 해서 성능이 많이 저하되지는 않고, 그 밑의 from이나 다른 명령어들이 성능을 먹는 것이라고 말씀하심.

만약 V4를 사용하고 싶다면,
해당 메서드를 OrderSimpleQueryRepository 를 통해 리포지토리를 하나 만드는 것이 좋다고 하심.

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

좋은 웹페이지 즐겨찾기