JPQL을 사용할 때는 join 타입을 꼭 명시하자
우아한테크코스

JPQL을 사용할 때는 join 타입을 꼭 명시하자

상황

Spring Data JPA 사용시 @Query 애노테이션으로 JPQL을 사용하면서 문득 이상한 점을 발견했다.
어떤 쿼리가 나가는지 확인했을 때 내부적으로 cross join을 사용한 것이다!

평소에 inner join이나 left join 정도만 알고 있었던 터라, cross join이 무엇인지 잘 몰랐다.
cross join이 무엇인지 알아보고, 이것을 어떻게 해결할 수 있는지 알아보자.

테이블 결합 방식

교차결합(Cross Join)

교차결합(Cross Join)은 카티전곱(Cartesian product)라고도 불리며 곱집합으로 계산된다.
곱집합은 미드 시리즈와 비슷하다. 시즌1부터 6까지 각 10편씩 있다고 가정하면, 시즌1-1, 시즌1-2, … 시즌 6-10 까지 총 60편(6x10)이다.
SELECT문을 사용할 때 FROM 테이블을 2개 이상을 지정하면 이들은 곱집합으로 계산된다.

    SELECT * FROM 시즌, 편;

교차 결합 방식은 결합한 테이블의 행이 비효율적으로 많이 생성되기 때문에 잘 사용하지 않는다.
만약 각각 행이 20만개, 20만개인 테이블을 교차결합한다면 총 400억개의 행이 생성될 것이다!
그래서 결합 방법으로는 교차결합보다는 내부결합이 자주 사용된다.

 

우리는 보통 테이블을 결합할 때 결합조건을 명시하여 원하는 조건의 행만 가져온다.

 

이렇게 교차 결합으로 계산된 곱집합에서 원하는 조건만을 검색하는 것을 내부결합(Inner Join)이라고 부른다.
일반적으로 내부결합을 사용할 때는 FROM절에 복수의 테이블을 적지 않고, INNER JOIN이라는 키워드를 사용한다.

    SELECT * FROM 테이블명1 INNER JOIN 테이블명2 ON 결합조건

JPQL에서는 join 타입을 꼭 명시하자

실제로 프로젝트에서 cross join이 발생했던 JPQL 코드이다.

    @Query("SELECT r FROM Reservation AS r "
                + "WHERE r.schedule.coach.id = :coachId "
                + "AND r.reservationStatus NOT IN :status")
    List<Reservation> findAllByCoachIdAndStatusNot(Long coachId, ReservationStatus status);

JPQL은 엔티티 기준으로 쿼리를 만들 수 있다.
그래서 무의식적으로 자동완성이 제공하는 기능에 따라 엔티티 내의 참조값을 통해 내가 원하는 결합조건을 설정할 수 있다.
이렇게 WHERE 절을 사용해서 결합조건을 설정하는 방법을 세타조인이라고 한다.
세타조인을 이용하면 전혀 관계없는 엔티티도 조인할 수 있다.

조인이 성능상 차지하는 부분은 크다. 우리는 cross join이 왜 비효율적인지 알아보았다.
그래서 JPQL을 사용할 때 INNER JOIN을 꼭 명시해야 한다.

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

INNER JOIN 방식으로 SELECT문이 나갔음을 확인할 수 있었다.

출처