여러 테이블이 연관관계를 가지고 있어서
한번 조회할 때 조회대상이 되는 row수가 수백, 수천개가 되었을 때
전체를 모두 조회하는 상황 자체가 성능적으로 불리할 때가 있다.
(조회를 다 하더라도 페이징 처리를 해야해서 정렬기능이 들어가는 것도 성능적 손해)
이 문제를 해결하는 것이 "QueryDSL" 기능!!

QueryDSL
Entity 의 매핑정보를 활용하여 쿼리에 적합하도록 쿼리 전용 클래스(Q클래스)로 재구성해주는 기술
자동으로 재구성 해준다!??
- 여기에 JPAQueryFactory 을 통한 Q클래스를 활용할 수 있는 기능들을 제공합니다.
- 그럼, JPAQueryFactory 는 뭘까요?
- 재구성한 Q클래스를 통해 문자열이 아닌 객체 또는 함수로 쿼리를 작성하고 실행하게 해주는 기술 입니다.
JPAQueryFactory jqf = new JPAQueryFactory(em);
QUser user = QUser.user;
List<Person> userList = jpf
.selectFrom(user)
.where(person.username.eq(username)
.and(person.password.eq(password))
.fetch();
잘보면 함수들이 엮여있는 것이 쿼리문처럼 보이는데
이렇게 사용할 수 있는 객체를 Q객체라 부르고
이 Q객체를 만들어내는 것이 쿼리DSL 기술이다.
위의 특징을 구현하기 위해서 쿼리DSL은 두가지 기술을 사용한다.

1. Q타입클래스
2. 메소드 체이닝 방식
앞에서 잠깐 언급했는데, 클라이언트는 JPQL을 실행하여 원하는 데이터가 담긴 엔티티 객체( 혹은 일반객체 )를 반환 받으려 한다.
원하는 엔티티 정보를 QueryDSL에 넘겨야 하는데, 이때 사용되는 클래스가 Q타입클래스이다.
Q타입클래스는 컴파일 과정에서 이미 생성되어 있는 엔티티를 토대로 자동으로 만들어 진다.
만약 Member라는 클래스가 있으면
따로 QMember라는 Q클래스를 사용자가 지정해주지않아도
QMember qMember = QMember.member;
이런식으로 사용이 가능하다.
※ 참고로 저렇게 Q클래스를 생성하는 방법을 기본인스턴스 사용한다고함.
그 외의 방법도있음!!
QMember qMember = new QMember("m"); //별칭 직접 지정 방식인데, 같은 테이블을 조인하는 경우가 아니면 기본 인스턴스를 사용하는 것이 보통이라고 한다
왜 다른 클래스를 사용하는가??
QueryDSL은 JPA와 분리되어 있는 프레임워크이다. 엔티티를 직접 사용하면 JPA에 종속되므로 Q타입클래스 같은 QueryDSL 전용 클래스를 만들어 사용한다.
JPAQueryFactory jqf = new JPAQueryFactory(em);
QUser user = QUser.user;
List<Person> userList = jpf
.selectFrom(user)
.where(person.username.eq(username)
.and(person.password.eq(password))
.fetch();
위에서 나온 QuertDSL의 예시인데
보면알다싶이 select 메소드, from 메소드, (select user from user => selectFrom(user) 가능)
leftJoin 메소드, where메소드가 '.' 연산자로 체인처럼 연결되어 있음을 확인할 수 있다.
빌더 패턴은 데이터 세팅을 select, from, leftjoin, where 순으로 할 수 있도록 제공한다.
빌더 패턴이 뭐더랑
빌더 패턴은 클라이언트가 SentenceBuilder에게 Sentence 객체를 요청하는 구조이다. 요청하는 형태가 굉장히 독특하다. 메소드 체이닝 방식(method chaining)으로 객체를 요청한다.일전에 responseDto들을 세팅할 때 Builder를 많이 사용했을 텐데
SentenceBuilder의 메소드를 '.' 연산자로 호출하고 다시 다른 메소드를 '.' 연산자로 호출하는 체인(Chain) 구조로 이루어져 있음을 알 수 있다.
=> 이런 구조가 가능한 이유는 메소드의 반환타입을 하나로 통일했기 때문이다.빌더 패턴의 목적은 데이터 세팅이다.
아까 위에서 참조로 올렸던 빌더패턴의 구조에 대해서 그림을 보면 그대로 QueryDSL에 적용할 수 있다.

역할이 엄청길긴한데 일단 아주 간단하게 요약만 해봤다.
QuertDSL 동작 순서
- 문제 : 개발자(클라이언트)가 문자열로 JPQL을 작성하면 타입안정성 체크가 어렵고 동적쿼리를 직관적으로 생성하지 못한다.
- 해결점 : 이런 문제를 해결하기 위해
QueryDSL프레임워크를 사용한다.QueryDSL은 JPQL 생성및실행 업무를 담당하는 프레임워크로, 개발자는 JPQL 생성에 필요한 데이터만 설정하면 된다.- JPQL 데이터 설정을 유연하게 하기 위해, QueryDSL은 빌드패턴 구조로 이루어져 있다.
JPAQueryFactory는 빌더패턴의Director객체로, 클라이언트가 약속된 순서 데이터를 설정할 수 있도록 도와준다.AbstractJPAQuery는 빌더패턴의Builder객체로, 메소드 체이닝 방식으로 데이터 설정을 가능하게 만든다.QueryMixin은Builder가 데이터 설정을 단순하게 할 수 있도록,AbstractJPAQuery와QueryMetaData사이에서 중간 다리 역할을 한다.
개발자는 JPAQueryFactory 생성 + Q타입 클래스 기반 쿼리 작성만 하면 되고,
나머지 내부 구조(AbstractJPAQuery, QueryMixin, QueryMetadata)는 QueryDSL이 자동으로 처리해줌.
좀따 과제하면서 쿼리DSL쓰는거잇던데 실습하러오겠음
이론에 대한 내용 출처
https://coding-business.tistory.com/100
https://lordofkangs.tistory.com/466 << 여기 시리즈가 아주 도움이많이됨
https://samori.tistory.com/58

findByIdWithUser를 QueryDSL로 변경하는 것이다.
사실 다른 과제에서 EntityManager를 선언해놓은 클래스가 있기 때문에 재사용을 위해 거기서 함수를 호출하고자한다!!!
// 9. QueryDSL 적용을 위한 의존성 (SpringBoot3.0 부터는 jakarta 사용해야함)
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
필요한 의존성 추가해주고
QTodo qtodo = QTodo.todo 에서 QTodo가 안만들어짐


Rebuild Project 하니깐 된다
여튼 돌아와서
package org.example.expert.domain.todo.repository;
@Repository
@RequiredArgsConstructor
public class TodoSearchRepositoryImpl implements TodoSearchRepository {
private final EntityManager entityManager;
private final JPAQueryFactory jpaQueryFactory;
@Override
public Page<Todo> getTodos(String weather, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable) {
...
return new PageImpl<>(result, pageable, result.size());
}
@Override
public Todo getTodo(long todoId) {
QTodo qTodo = QTodo.todo;
Todo todo = jpaQueryFactory.selectFrom(qTodo)
.where(qTodo.id.eq(todoId))
.fetchOne();
if(todo == null){
throw new InvalidRequestException("Todo not found");
}
return todo;
}
}
추가해줬따!
나중에 QueryDSL 써서 검색도 하던데 그거생각해서 JPAQueryFactory 를 밖으로 빼내줬음.
이제 테스트코드쓰러간다
현재 코드에 문제가 잇다
@Override
public Todo getTodo(long todoId) {
QTodo qTodo = QTodo.todo;
Todo todo = jpaQueryFactory.selectFrom(qTodo)
.where(qTodo.id.eq(todoId))
.fetchOne();
if(todo == null){
throw new InvalidRequestException("Todo not found");
}
return todo;
}

Todo 엔티티가 연관된 엔티티를 LAZY 로딩으로 가졌기 때문에, 이후에 해당 연관 필드에 접근하면 N+1 문제가 생길 수 있음!!
왜 이런가??
예를 들어 todo.getUser().getEmail() 과 같이 연관 필드에 계속 접근하게 되면
접글할때마다 쿼리가 발생함!!!!!!!!
이전에 N+1 문제 해결했던 방안으로
.leftJoin(qTodo.user).fetchJoin()
이걸 from 절 뒤에 붙여주면된다.
@Override
public Todo getTodo(long todoId) {
QTodo qTodo = QTodo.todo;
Todo todo = jpaQueryFactory.selectFrom(qTodo)
.leftJoin(qTodo.user).fetchJoin()
.where(qTodo.id.eq(todoId))
.fetchOne();
if(todo == null){
throw new InvalidRequestException("Todo not found");
}
return todo;
}
굿
경험이 잘 녹아있는 좋은 글이네요! 배우고 갑니다.