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인 다른 연관 관계가 있다면, 그것도 함께 즉시 로딩됩니다.
}
fetchgraph와 loadgraph의 핵심 차이점
| jakarta.persistence.fetchgraph | jakarta.persistence.loadgraph | |
| 목적 | 명시된 연관 관계만 EAGER로, 나머지는 모두 LAZY. | 명시된 연관 관계는 EAGER로, 나머지는 원래 FetchType 유지. |
| 기본 동작 | 엔티티의 기본 FetchType 설정을 무시. |
엔티티의 기본 FetchType 설정을 존중. |
| 활용 | 꼭 필요한 데이터만 가져와서 최적화를 극대화할 때. | 기존 설정에 추가적인 EAGER 로딩이 필요할 때. |
5. Hibernate와 EntityGraph 힌트의 연관성 (중요!)
간혹 "엔티티 그래프 힌트가 DB 쿼리와는 연관이 없다"는 오해가 있을 수 있지만, 이는 사실이 아닙니다.
Hibernate와 같은 JPA 구현체는 EntityGraph 힌트를 보고 연관 엔티티를 가져올 페치 전략을 결정합니다. 그리고 이 페치 전략은 최종적으로 데이터베이스로 전송되는 SQL 쿼리의 형태에 직접적인 영향을 미칩니다.
예를 들어, fetchgraph나 loadgraph 힌트로 특정 연관 관계를 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 |