Spring Data JPA 쓰니까 쿼리가 이상해졌어!
아니 이걸 왜 다 따로 불러와?
Spring Data JPA 를 이용해 사용자의 예약 목록을 조회하는 페이지에 접근했더니 다음과 같은 쿼리들이 발생했다.(스압 주의)
발생한 SQL
자신의 예약 정보를 조회하기 위해서는 그저 예약, 회원, 예약 시간, 테마 테이블을 모두 join 해서 한번에 쿼리를 작성하면 된다. 비록 쿼리가 복잡해지긴 하겠지만, 저 많은 쿼리를 다 따로 데이터베이스에 전송하면서 생기는 오버헤드보다는 아마 나을 것이다.
fetch type
JPA 에서는 @ManyToOne 과 같이 연관관계를 설정하는 어노테이션에 fetch 옵션을 제공해준다. 이 옵션은 FetchType.LAZY 와 FetchType.EAGER 중 하나로 설정할 수 있다.
LAZY 옵션은 연관 데이터가 실제로 필요한 시점에 조회해와라! 라는 옵션이고, EAGER 옵션은 연관관계를 무조건 조회해와라! 라는 옵션이다. LAZY 옵션은 연관관계의 id 값만 조회해오고, 실제로 연관 데이터가 필요할 때 이 id를 이용해 select 쿼리를 이용해 데이터를 조회한다. 반면, EAGER 옵션은 어차피 모든 연관 데이터를 조회할 것이기 때문에 처음부터 모든 데이터를 조회해온다.
이 옵션은 EntityManager.find 를 호출할 때 적용된다.
Spring Data JPA 너는 참 재밌는 친구구나
!
JPA에서는 보다 객체에 가까운 형태의 언어인 JPQL 을 지원한다. JPQL은 연관관계를 이용하여 쿼리를 표현하기 때문에 SQL보다 훨씬 깔끔하고 읽기 좋다. 이를 통해 보다 쉽게 쿼리를 작성하고, JPA 의 구현체가 이 JPQL 을 적절히 번역하여 SQL 로 실행시킨다. JPQL 명령어중 fetch join 이 있는데, 이를 사용하여 쿼리를 작성하면 모든 연관 데이터가 한번에 조회된다.
Query Method
JPQL은 기존의 SQL에 비해 편리하지만 여전히 쿼리를 작성해야 한다. Spring Data JPA에서는 이를 한층 더 편하게 사용할 수 있도록 Query Method 라는 기능을 제공한다. 적절한 규칙을 지켜서 메서드 이름을 정한다면, 그 규칙에 따라 생성된 JPQL이 실행되는 구조다.
@Query
Spring Data JPA에서 제공하는 어노테이션으로, 레퍼지토리의 메서드가 동작할 때 실행할 JPQL을 설정하는 어노테이션이다. 이 어노테이션에 fetch join을 사용한 쿼리를 작성한다면, 연관 데이터를 한번에 조회할 수 있다.
쿼리 메서드기능보다 우선하여 적용된다. 즉, 쿼리 메서드 작명 규칙에 따른 메서드더라도, @Query 를 사용하여 JPQL을 적용한 경우 쿼리 메서드 기능은 무시된다.
@EntityGraph
N + 1 문제가 발생하는 것은 단순히 fetch join을 하지 않기 때문이다. 따라서, @Query 를 이용해 fetch join 쿼리를 작성한다면 N + 1 문제를 해결할 수 있다. 다만, 쿼리 메서드 기능을 이용할 수 없다는 것이 큰 단점이다.
이를 해결하기 위해 @EntityGraph 를 사용할 수 있다. 한번에 조회하고 싶은 연관 객체를 설정해 줄 수 있다.
예시
@Entity
public class Reservation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Member reservationMember;
private LocalDate date;
@ManyToOne(fetch = FetchType.LAZY)
private ReservationTime time;
@ManyToOne(fetch = FetchType.LAZY)
private Theme theme;
protected Reservation() {
}
}
Java
복사
이 클래스는 방탈출 시스템의 예약을 저장하는 엔티티다. Member, ReservationTime, Theme와 연관 관계를 맺고 있다.
public interface JpaReservationRepository extends JpaRepository<Reservation, Long> {
@EntityGraph(attributePaths = {"reservationMember", "time", "theme"})
List<Reservation> findAllByReservationMember_Id(long memberId);
}
Java
복사
이때, 레퍼지토리에서 메서드 위에 @EntityGraph 어노테이션을 사용해, 특정 연관 객체를 한번에 조회하게 할 수 있다. 위 코드는 모든 연관관계를 한번에 조회하라는 것이다. 이를 사용했을 때 발생하는 쿼리다.
select
r1_0.id,
r1_0.create_at,
r1_0.date,
rm1_0.id,
rm1_0.create_at,
rm1_0.email,
rm1_0.password,
rm1_0.name,
rm1_0.role,
t1_0.id,
t1_0.create_at,
t1_0.description,
t1_0.name,
t1_0.thumbnail,
t2_0.id,
t2_0.create_at,
t2_0.start_at
from
reservation r1_0
left join
member rm1_0
on rm1_0.id=r1_0.reservation_member_id
left join
theme t1_0
on t1_0.id=r1_0.theme_id
left join
reservation_time t2_0
on t2_0.id=r1_0.time_id
where
(
r1_0.deleted = FALSE
)
and rm1_0.id=?
SQL
복사
기존에 있던 수많은 쿼리가 모두 사라지고 한번의 쿼리로 조회되었다.