본문 바로가기
Spring/JPA

[SpringBoot] JPA 페치 조인 최적화

by 진꿈청 2024. 1. 26.

1. 특정 조회 작업 시 엔티티를 그냥 반환하는 경우

 

@GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2(){
        //ORDER 2개
        //N + 1 -> 1 + 회원 N + 배송 N
        List<Order> orders = orderRepository.findAll(new OrderSearch());
        List<SimpleOrderDto> collect = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());

        return collect;
    }


문제점 1

만약 엔티티 설정에 지연 로딩으로 설정하여 놓았다면, 해당 엔티티와 연관된 엔티티들에는 프록시가 들어간다.

하지만, 기본적으로 이 프록시 객체를 Json으로 생성하는 것에는 무리가 있다.

또한, @JsonIgnore 어노테이션을 사용하지 않으면 양방향 관계의 경우 무한 호출이 된다.

 

문제점2

엔티티를 그대로 반환하면 나중에 엔티티가 변경되었을 때 관련 API 명세를 다시 작성해야 한다.

여러 부서가 사용하는 API라면 당연히 큰 문제가 발생.

 

문제점3

하나의 쿼리에 다른 쿼리가 너무 많이 나가게 된다. -> N + 1 문제 발생.

 

 

 

2. 엔티티를 DTO로 변환해서 반환하는 경우.

 

    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2(){
        //ORDER 2개
        //N + 1 -> 1 + 회원 N + 배송 N
        List<Order> orders = orderRepository.findAll(new OrderSearch());
        List<SimpleOrderDto> collect = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());

        return collect;
    }

    @Data
    static class SimpleOrderDto {
        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(); //LAZY 초기화
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress(); //LAZY 초기화
        }
    }

이 방법은 기존에 있던 API 명세에 큰 영향을 주지 않게 되어 1문제점2가 보완된다.

하지만, 문제점1, 문제점3 문제는 남아있다.

 

 

 

3. 엔티티를 DTO로 변환 및 페치 조인으로 최적화 한 뒤 반환하는 경우.

 

    @GetMapping("/api/v3/simple-orders")
    public List<SimpleOrderDto> ordersV3(){
        List<Order> orders = orderRepository.findAllWithMemberDelivery();
        List<SimpleOrderDto> collect = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());

        return collect;
    }
    
    @Data
    static class SimpleOrderDto {
        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(); //LAZY 초기화
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress(); //LAZY 초기화
        }
    }
    
    // querydsl를 활용한 fetch join
    @Override
    public List<Order> findAllWithMemberDelivery() {
        QOrder order = QOrder.order;
        QMember member = QMember.member;
        QDelivery delivery = QDelivery.delivery;

        return jpaQueryFactory
                .select(order)
                .from(order)
                .join(order.member, member)
                .fetchJoin()
                .join(order.delivery, delivery)
                .fetchJoin()
                .fetch();
    }

 

이 방법은 읽어올 때 JPA의 fetch join을 활용하는 방법이다. 

fetch join을 활용하면 엔티티에 지연 로딩으로 설정되어 있어도,

프록시 객체가 아닌 실제 정보를 갖고오게 된다.

 

따라서, 조회 쿼리 1개에 대해 N개의 쿼리가 나갔던 기존의 방법에서,

페치 조인 사용시 1개의 쿼리만 나가는 놀라운 현상이 발생한다.

 

JPA에서 지원하는 fetch join을 적극 활용하도록 하자.

 

 

본 내용은 김영한 강사님의 인프런 JPA 강의를 토대로 작성되었습니다.