JPA QueryDSL에서 쿼리 힌트(Query Hint)

1. JPA 쿼리 힌트(Query Hint)란?

JPA 쿼리 힌트는 데이터베이스 시스템이나 Hibernate와 같은 JPA 구현체에게 쿼리 실행 방식을 제어하거나 최적화하기 위한 "팁"을 제공하는 기능입니다. 이름처럼 강제 사항은 아니지만, 대부분의 경우 JPA 구현체는 이 힌트를 존중하여 쿼리 실행 계획을 수립합니다.

주로 다음과 같은 목적으로 사용됩니다.

  • 성능 최적화: 캐싱 제어, 읽기 전용 쿼리 설정, 페치 사이즈(Fetch Size) 조절 등
  • 리소스 제어: 쿼리 타임아웃, 락 타임아웃 설정
  • 디버깅/추적: SQL 쿼리에 주석 삽입

2. QueryDSL에서 쿼리 힌트 적용하기

QueryDSL은 결국 JPA의 EntityManager를 통해 쿼리를 실행하므로, JPA의 힌트 메커니즘을 그대로 활용합니다. QueryDSL 환경에서 힌트를 적용하는 주요 방법은 두 가지입니다.

2.1. Spring Data JPA @QueryHints 어노테이션 사용 (권장!)

Spring Data JPA를 사용하고 QuerydslPredicateExecutor를 통해 QueryDSL을 통합하여 사용한다면, Repository 인터페이스의 메서드에 @QueryHints 어노테이션을 사용하여 쿼리 힌트를 적용하는 것이 가장 간편하고 권장되는 방법입니다.

이 어노테이션은 Spring Data JPA가 제공하는 기능이지만, QuerydslPredicateExecutor가 제공하는 QueryDSL 기반의 메서드(예: findAll(Predicate predicate))에도 완벽하게 적용되어, 해당 QueryDSL 쿼리가 실행될 때 힌트가 JPA 구현체로 전달됩니다.

import org.springframework.data.jpa.repository.QueryHints;
import jakarta.persistence.QueryHint; // 또는 org.hibernate.jpa.QueryHint
import com.querydsl.core.types.Predicate;

public interface MyEntityRepository extends JpaRepository<MyEntity, Long>, QuerydslPredicateExecutor<MyEntity> {

    // QuerydslPredicateExecutor의 findAll 메서드를 오버라이딩하여 @QueryHints 적용
    @Override
    @QueryHints(value = {
        @QueryHint(name = "org.hibernate.readOnly", value = "true"), // 읽기 전용 쿼리
        @QueryHint(name = "org.hibernate.fetchSize", value = "1000") // Fetch Size 설정
    })
    List<MyEntity> findAll(Predicate predicate); // QueryDSL Predicate를 받아 쿼리 실행

    // 특정 조건의 엔티티를 조회하는 QueryDSL 기반 메서드에 타임아웃 힌트 적용
    @QueryHints(@QueryHint(name = "jakarta.persistence.query.timeout", value = "2000"))
    List<MyEntity> findAllByComplexCondition(Predicate predicate);
}

org.hibernate.fetchSize 힌트는 JDBC 드라이버에게 한 번에 가져올 결과 집합의 크기를 알려주어 대용량 데이터 조회 시 네트워크 오버헤드를 줄이는 데 도움이 됩니다.

2.2. JPAQueryFactory를 통한 프로그램 방식 적용

Spring Data JPA를 사용하지 않거나, 더 세밀하게 JPA Query 객체에 접근하여 힌트를 제어해야 할 경우, JPAQueryFactory를 통해 직접 setHint() 메서드를 호출할 수 있습니다.

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import com.querydsl.core.types.Predicate;

// ... (생략)

@Autowired
private JPAQueryFactory queryFactory;

@Autowired
private EntityManager entityManager;

public List<MyEntity> findEntitiesWithProgrammaticHint(Predicate predicate) {
    QMyEntity myEntity = QMyEntity.myEntity;

    Query jpaQuery = queryFactory
        .selectFrom(myEntity)
        .where(predicate) // QueryDSL Predicate 사용
        .createQuery(); // QueryDSL 쿼리를 JPA Query 객체로 변환

    jpaQuery.setHint("jakarta.persistence.query.timeout", 3000); // 3초 타임아웃 설정
    jpaQuery.setHint("org.hibernate.comment", "Finding entities by programmatic hint"); // SQL 주석 추가

    return jpaQuery.getResultList();
}

3. 엔티티 그래프(Entity Graph) 이해하기

쿼리 힌트 중 가장 강력하고 자주 사용되는 것이 바로 엔티티 그래프입니다. JPA 2.1부터 도입된 이 기능은 쿼리 시점에 연관 엔티티를 어떻게 로딩할지(즉시 로딩 vs 지연 로딩) 동적으로 제어하여 N+1 문제를 해결하고 성능을 최적화하는 핵심 메커니즘입니다.

왜 엔티티 그래프가 필요할까요?

JPA의 기본 연관 관계 로딩 방식(EAGER, LAZY)은 정적입니다. 하지만 애플리케이션의 특정 기능에서는 EAGER가 필요하고, 다른 기능에서는 LAZY가 필요할 수 있습니다. 엔티티 그래프는 이러한 정적인 설정을 보완하여, 쿼리별로 연관 관계의 페치 전략을 유연하게 변경할 수 있도록 해줍니다.

엔티티 그래프 정의하기

엔티티 그래프는 주로 @NamedEntityGraph 어노테이션을 사용하여 엔티티 클래스에 정의합니다.

import jakarta.persistence.*;
import java.util.List;

@Entity
@NamedEntityGraph(
    name = "board-with-member-comments", // 그래프 이름
    attributeNodes = {
        @NamedAttributeNode("member"), // Board와 연관된 Member를 포함
        @NamedAttributeNode(value = "comments", // Board와 연관된 Comments를 포함
                            subgraph = "comments-subgraph") // Comments 내부의 연관 관계도 정의 가능
    },
    subgraphs = @NamedSubgraph(
        name = "comments-subgraph",
        attributeNodes = @NamedAttributeNode("author") // Comment와 연관된 Author를 포함
    )
)
public class Board {
    @Id @GeneratedValue private Long id;
    private String title;

    @ManyToOne(fetch = FetchType.LAZY) // 실제 코드는 LAZY여도, 그래프 사용 시 EAGER로 로딩 가능
    private Member member;

    @OneToMany(mappedBy = "board", fetch = FetchType.LAZY)
    private List<Comment> comments;

    // ... (생략)
}

@Entity
public class Comment {
    @Id @GeneratedValue private Long id;
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    private Member author; // 댓글 작성자

    // ... (생략)
}

4. fetchgraph vs loadgraph (엔티티 그래프 힌트)

정의된 엔티티 그래프는 jakarta.persistence.fetchgraph 또는 jakarta.persistence.loadgraph 힌트와 함께 사용됩니다. 이 둘의 차이점을 명확히 이해하는 것이 중요합니다.

4.1. jakarta.persistence.fetchgraph

  • 목적: 정의된 엔티티 그래프에 **명시적으로 지정된 연관 관계만 즉시 로딩(Eager Fetching)**하고, **나머지 연관 관계는 모두 지연 로딩(Lazy Fetching)**으로 처리합니다.
  • 특징: 엔티티의 기본 FetchType 설정(EAGER/LAZY)을 무시하고, 오직 그래프에 명시된 것만 EAGER로 가져옵니다. "이것들만 EAGER로 가져와. 나머지는 필요 없어!"라는 강력한 의미를 가집니다.
  • 활용: 필요한 데이터만 정확히 조회하여 N+1 문제 등을 해결하고 쿼리 성능을 최적화하는 데 매우 유용합니다. 불필요한 연관 데이터 로딩을 방지합니다.

적용 예시 (QueryDSL + Spring Data JPA):

import org.springframework.data.jpa.repository.EntityGraph; // Spring Data JPA의 @EntityGraph
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import com.querydsl.core.types.Predicate;
import java.util.List;

public interface BoardRepository extends JpaRepository<Board, Long>, QuerydslPredicateExecutor<Board> {

    // QueryDSL Predicate를 사용하는 findAll 메서드에 fetchgraph 적용
    @Override
    @EntityGraph(value = "board-with-member-comments", type = EntityGraph.EntityGraphType.FETCH)
    List<Board> findAll(Predicate predicate);
    // 결과: board.member와 board.comments.author는 즉시 로딩됩니다.
    // Board의 다른 EAGER 관계가 있더라도, 그래프에 없으면 LAZY로 처리됩니다.
}

4.2. jakarta.persistence.loadgraph

  • 목적: 정의된 엔티티 그래프에 **명시적으로 지정된 연관 관계는 즉시 로딩(Eager Fetching)**하고, 나머지 연관 관계는 해당 연관 관계의 FetchType 설정(EAGER 또는 LAZY)을 그대로 따릅니다.
  • 특징: 엔티티의 기본 FetchType 설정을 존중합니다. "이것들을 EAGER로 가져와. 그리고 원래 EAGER인 것들도 가져와!"와 같습니다.
  • 활용: 기존의 FetchType 설정에 더해, 특정 쿼리에서 추가적으로 필요한 연관 관계를 EAGER로 로딩하고 싶을 때 사용됩니다.

적용 예시 (QueryDSL + Spring Data JPA):

import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import com.querydsl.core.types.Predicate;
import java.util.List;

public interface BoardRepository extends JpaRepository<Board, Long>, QuerydslPredicateExecutor<Board> {

    // 특정 메서드에 loadgraph 적용
    @EntityGraph(value = "board-with-member-comments", type = EntityGraph.EntityGraphType.LOAD)
    List<Board> findByTitleContaining(String title, Predicate predicate); // QueryDSL Predicate와 함께 사용 가능
    // 결과: board.member와 board.comments.author는 즉시 로딩됩니다.
    // 만약 Board에 기본 FetchType.EAGER인 다른 연관 관계가 있다면, 그것도 함께 즉시 로딩됩니다.
}

fetchgraphloadgraph의 핵심 차이점

  jakarta.persistence.fetchgraph jakarta.persistence.loadgraph
목적 명시된 연관 관계만 EAGER로, 나머지는 모두 LAZY. 명시된 연관 관계는 EAGER로, 나머지는 원래 FetchType 유지.
기본 동작 엔티티의 기본 FetchType 설정을 무시. 엔티티의 기본 FetchType 설정을 존중.
활용 꼭 필요한 데이터만 가져와서 최적화를 극대화할 때. 기존 설정에 추가적인 EAGER 로딩이 필요할 때.

5. Hibernate와 EntityGraph 힌트의 연관성 (중요!)

간혹 "엔티티 그래프 힌트가 DB 쿼리와는 연관이 없다"는 오해가 있을 수 있지만, 이는 사실이 아닙니다.

Hibernate와 같은 JPA 구현체는 EntityGraph 힌트를 보고 연관 엔티티를 가져올 페치 전략을 결정합니다. 그리고 이 페치 전략은 최종적으로 데이터베이스로 전송되는 SQL 쿼리의 형태에 직접적인 영향을 미칩니다.

예를 들어, fetchgraphloadgraph 힌트로 특정 연관 관계를 EAGER로 로딩하라고 지시하면, Hibernate는 대부분의 경우 N+1 문제를 방지하기 위해 LEFT JOIN 또는 INNER JOIN을 포함하는 SQL 쿼리를 생성하여 데이터베이스에 전송합니다.

즉, 엔티티 그래프 힌트는 Hibernate가 DB 쿼리를 생성할 때 JOIN 여부 및 방식을 결정하는 중요한 지표가 됩니다.

 

'Java > Spring' 카테고리의 다른 글

JPA에서 Optimistic Lock과 Pessimistic Lock  (0) 2019.07.08
RestTemplate  (2) 2019.07.07
Spring AOP, Proxy  (0) 2019.07.04