객체 그래프 탐색과 Q 클래스의 Path, 그리고 QueryDSL의 관계

김신영·2025년 6월 24일
0

QueryDSL을 사용하다 보면 자주 마주치는 Q클래스, Path, 객체 그래프 탐색이라는 개념들. 처음엔 다소 낯설 수 있지만, 이를 제대로 이해하면 QueryDSL의 핵심 원리를 꿰뚫어볼 수 있다.


1. 객체 그래프

객체 그래프 (Object Graph) 는 말 그대로 "객체들 간의 참조 관계를 그래프로 표현한 것"이다.

예를 들어 아래와 같은 엔티티 구조가 있다고 할 때:

@Entity
public class Todo {
    @Id
    private Long id;

    private String title;
    private String comment;

    @ManyToOne
    private User user;
}

@Entity
public class User {
    @Id
    private Long id;

    private String name;
}

이 구조는 다음과 같은 객체 그래프로 표현된다:

Todo
 ├── title
 ├── comment
 └── User
       └── name

이 객체 그래프를 따라 우리는 todo.user.name 과 같이 연관된 객체의 필드까지 탐색할 수 있다.


2. Q 클래스와 Path

QueryDSL은 빌드 시점에 각 엔티티에 대해 Q클래스를 생성한다.
예: TodoQTodo, UserQUser

public class QTodo extends EntityPathBase<Todo> {
    public static final QTodo todo = new QTodo("todo");
    
    public final StringPath title = createString("title");
    public final StringPath comment = createString("comment");
    
  
    public final QUser user;
    
  
    public QTodo(String variable) {
        super(Todo.class, forVariable(variable));
    }
}

여기서 중요한 구조

  • title, comment 필드는 각각 StringPath 타입의 인스턴스 변수로 선언됨
  • StringPath는 QueryDSL의 Path<String> 인터페이스를 구현한 클래스이며,
    내부적으로는 PathImpl<String>를 필드로 포함하여 공통 로직을 제공한다.
  • Path 인터페이스를 구현한 다양한 타입이 존재한다:
필드 타입Q 클래스의 Path 타입
StringStringPath
LongNumberPath<Long>
DateDateTimePath<...>
UserQUser (연관 객체, 또 다른 Q 클래스)

이러한 Path 객체들은 QueryDSL 내부에서 경로(Path) 로 사용된다.
예: todo.user.nameQTodo → QUser → StringPath

따라서 Path는 객체 그래프의 각 간선(edge) 에 해당한다고 볼 수 있고,
Q 클래스는 각 필드를 Path 객체로 래핑한 정적인 경로 모음이라 할 수 있다.


3. QueryDSL과 객체 그래프 탐색

QueryDSL은 Q 클래스에 정의된 Path들을 활용해 SQL 쿼리를 타입 안전하게 조립한다.

예를 들어 다음과 같은 코드:

queryFactory.selectFrom(todo)
    .join(todo.user, user)
    .where(user.name.eq("kim"))
    .fetch();

이 코드는 실제로는 다음과 같은 경로를 따라 객체 그래프를 탐색한 결과다:

QTodo.todo.user.name → Todo.user.name

QuerDSL의 장점:

타입 안전성

  • 컴파일 타임에 필드명, 타입 등을 검증할 수 있다.

IDE 자동완성

  • todo.user.userType.name처럼 경로가 객체의 참조처럼 이어지기 때문에
    IDE에서 점(.) 연산을 통해 자동완성이 가능하다.
  • 반면, JPQL에서는 "t.user.userType.name"처럼 문자열 기반이라 자동완성이 불가능하다.

지연 로딩(fetch) 제어 가능

  • fetchJoin()을 통해 연관된 객체를 명시적으로 fetch할 수 있다.
queryFactory
    .selectFrom(todo)
    .join(todo.user, user).fetchJoin()
    .join(user.userType, userType).fetchJoin();
  • 어떤 객체를 join할지, fetch할지를 객체 경로 기반으로 선언할 수 있다.
    (즉, 객체 참조를 따라가듯이 자연스럽게 Path를 따라 구성)

4. 마무리 정리

개념설명
객체 그래프객체 간의 참조 관계를 그래프 형태로 표현한 구조 (예: Todo → User → name)
Q 클래스엔티티를 기반으로 QueryDSL이 생성한 쿼리용 클래스. 각 필드를 Path 객체로 래핑
Path쿼리에서 사용할 수 있도록 필드를 감싼 객체. StringPath, NumberPath 등으로 타입화
QueryDSLQ 클래스에 정의된 Path들을 조합해 객체 그래프를 따라가며 쿼리를 타입 안전하게 구성하는 DSL

요약

Q 클래스는 컴파일 타임에 생성되며, 각 필드를 Path 객체로 래핑한 정적 구조이다.
QueryDSL은 이 Path 객체들을 통해 객체 그래프를 타입 안전하게 탐색하고,
SQL 쿼리를 객체 참조처럼 구성할 수 있도록 돕는 DSL(Domain-Specific Language)이다.
이 구조를 이해하면 QueryDSL을 단순한 SQL 빌더가 아닌,
객체 지향적 쿼리 언어로 활용하는 핵심 원리를 파악할 수 있다.








참고 : QueryDSL 작동 구조 요약도

[Java 코드]
queryFactory
.select(todo.user.name)
.from(todo)
.join(todo.user, user)
.where(user.name.eq("kim"))
.fetch();

     │
     ▼

[Q 클래스 기반 Path 조립]
todo → QTodo
└── user → QUser
└── name → StringPath
→ 각 필드는 컴파일 타임에 생성된 Path 객체이며, 객체 그래프의 경로를 타입 안전하게 표현함

     │
     ▼

[QueryDSL이 SQL 문자열로 변환]
SELECT u.name
FROM todo t
JOIN user u ON t.user_id = u.id
WHERE u.name = 'kim'
→ QueryDSL이 Path 정보를 기반으로 실제 SQL을 조립함

     │
     ▼

[JPA가 SQL 실행 → 결과 매핑]

  • QueryDSL이 생성한 JPQL 기반 쿼리를 EntityManager가 실행 요청하고,
    Hibernate가 이를 실제 SQL로 변환하여 DB에 전달함.

  • DB에서 반환된 ResultSet은 Hibernate에 의해 Java 객체로 매핑됨

  • 이 때 select(todo.user.name)은 user 테이블의 name 컬럼만 조회하며,
    해당 값은 문자열 타입으로 매핑되어 Java의 String으로 반환됨

         │
         ▼

    [Java 결과로 반환]
    → QueryDSL은 select 대상에 따라 반환 타입(T)을 결정하고,
    fetch 메서드에 따라 단건 또는 다건 결과를 반환함

예)

  • select(todo.user.name).fetch() → List
  • select(todo.user.name).fetchOne() → String
  • select(todo).fetch() → List
  • select(new QDto(...)).fetchFirst() → Dto
  • select(todo.title, todo.weather).fetch() → List

참고 : Path 객체 내부 정보

구성 요소설명
type필드의 자바 타입. 예: String.class, Long.class
metadataPathMetadata 객체. 필드명·전체 경로·별칭(alias) 등을 보유
parent상위 Path 객체 (e.g. todo.user.name에서 name.parent는 user)
root루트 Path (예: QTodo.todo) — 전체 객체 그래프의 출발점
annotatedElement
(선택)
원본 자바 필드나 메서드를 나타내는 리플렉션 객체. 코드 생성 도구에서 사용하고 일반 사용자 코드에서는 거의 사용되지 않음

예시
StringPath title = new StringPath(QTodo.todo, "title");
type = String.class, parent = QTodo.todo, root = QTodo.todo, metadata = "todo.title"

2개의 댓글

comment-user-thumbnail
2025년 6월 25일

좋은 글 정독했습니다 잘 봤습니다 이대로 글 쓰면 좋은 것 같ㅡ니

1개의 답글