JPA 성능 개선기 3. 페치조인
우아한테크코스

JPA 성능 개선기 3. 페치조인

상황

코치가 크루 한명의 면담 히스토리를 조회할 때 crew_id, reservation_status 조건에 맞는 모든 면담을 조회한다.

문제점

조회한 면담을 DTO로 변환해 필요한 정보를 응답값으로 넘겨줄 때 SELECT 쿼리가 의도치않게 발생하는 문제가 발생했다.
하나의 면담마다 SELECT 쿼리가 2개씩 추가로 발생하는 N+2 문제가 발생한 것이다.

코드

Service

    public List<CoachFindCrewHistoryResponse> findCrewHistoryByCoach(Long crewId) {
        validateCrewId(crewId);
        List<Reservation> reservations =
                reservationRepository.findAllByCrewIdAndReservationStatus(crewId, DONE);

        List<CoachFindCrewHistoryResponse> response = new ArrayList<>();
        for (Reservation reservation : reservations) {
            List<Sheet> sheets = sheetRepository.findAllByReservationIdOrderByNumber(reservation.getId());
            response.add(CoachFindCrewHistoryResponse.from(reservation, sheets));
        }

        return response;
    }

Repository

    @Query("SELECT r FROM Reservation AS r "
            + "INNER JOIN r.crew AS c "
            + "ON c.id = :crewId "
            + "INNER JOIN r.schedule AS s "
            + "WHERE r.reservationStatus = :status "
            + "ORDER BY s.localDateTime DESC")
    List<Reservation> findAllByCrewIdAndReservationStatus(Long crewId, ReservationStatus status);

해결방안

해결법은 간단하다. INNER JOIN 대신 FETCH JOIN을 사용하면 된다.
이 문제는 연관관계로 매핑된 필드의 기본전략을 모두 지연로딩으로 설정했기 때문에 발생한 문제다.

 

지연로딩으로 연관관계를 설정하면 INNER JOIN 필드 값을 가져오더라도 실제 엔티티가 아닌 프록시를 가져온다.
따라서 필드내 id가 아닌 다른 값을 사용하려고 하면 그때 실제 엔티티를 가져오기 위해 SELECT 쿼리가 날라가게 된다.

 

페치 조인을 사용하면 프록시가 아닌 실제 엔티티를 모두 가져와 해당 필드에 채워넣게 되므로, 필드 내 id가 아닌 값을 꺼내쓰더라도 SELECT 쿼리가 추가적으로 날라가지 않는다.

코드

Repository

    public static CoachFindCrewHistoryResponse from(Reservation reservation, List<Sheet> sheets) {
        Schedule schedule = reservation.getSchedule();
        Coach coach = schedule.getCoach();
        if (WRITING.equals(reservation.getSheetStatus())) {
            return new CoachFindCrewHistoryResponse(reservation.getId(), coach.getName(), coach.getImage(),
                    schedule.getLocalDateTime(), SheetDto.generateEmptySheet(sheets));
        }
        return new CoachFindCrewHistoryResponse(reservation.getId(), coach.getName(), coach.getImage(),
                schedule.getLocalDateTime(), SheetDto.from(sheets));
    }

실제 쿼리

추가로 생각해볼 점

페치조인 사용시 주의할 점

그렇다면 엔티티 내 필드 값을 모두 가져오고 싶을 때 무조건 페치조인을 사용하면 해결되는 걸까?
@ManyToOne 매핑만을 사용했다면 상관없겠지만, @OneToMany 매핑을 사용하게 되면 페치조인 사용시 주의해야 한다.

 

구체적으로 다음 2가지 상황에서 문제가 발생할 수 있다.

 

1. 하나의 엔티티 내 @OneToMany 연관관계를 가진 필드를 2개이상 모두 가져올 경우

 

이 경우 카티전 곱이 발생한다.
다음과 같은 엔티티를 생각해보자.

public class Coach {
    //생략

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "schedule_id")
    List<Schedule> schedules;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "reservation_id")
    List<Reservation> reservations;
}

위 엔티티에서 페치조인을 사용해 schedules, reservations을 모두 들고 온다면 Coach X Schedule X Reservation 테이블의 삼중곱이 일어나 어마어마한 row가 만들어질 것이다.
따라서 하나의 엔티티 내 @OneToMany 필드 2개 이상을 페치조인으로 갖고오면 안된다.

 

2. 페치조인과 페이지네이션을 같이 할 경우

 

DB를 조회하기 전까지 페이지를 모르기 때문에 메모리로 모두 가져온 다음 페이지네이션을 한다.
이 상황에 대해서는 테코블 글을 참고하면 좋을 것 같다.
JPA에서 Fetch Join과 Pagination을 함께 사용할때 주의하자