2025. 3. 14. 11:57ㆍDevelop/Spring
처음엔 JDBC Template 그 다음 MyBatis 그 다음 JPA로 쿼리를 작성해왔다.
하지만 JPA의 메서드 작성규칙만으로는 어렵고 복잡한 쿼리를 작성하기 어렵다.
그래서 JPQL로 쿼리를 더 자세하게 쓰기 시작했다.
하지만 JPQL은 문자열로 쿼리를 작성한다는 단점이 있고 개발자가 오타를 낸다면 버그를 발생한다는 문제점이 있다.
그 해결책으로 나온것이 바로 QueryDSL!
QueryDSL이란?
QueryDSL은 하이버네이트 쿼리 언어(HQL: Hibernate Query Language)의 쿼리를 타입에 안전하게 생성 및 관리해주는 프레임워크다.
QueryDSL은 정적 타입을 이용하여 SQL과 같은 쿼리를 생성할 수 있게 해준다.
자바 백엔드 기술은 Spring Boot와 Spring Data JPA를 함께 사용한다. 하지만 복잡한 쿼리, 동적 쿼리를 구현하는 데 있어 한계가 있다. 이러한 문제점을 해결할 수 있는 것이 QueryDSL이다.
JPA를 이용한 코드
@Query("SELECT t FROM Todo t " +
"LEFT JOIN t.user " +
"WHERE t.id = :todoId")
Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);
JPQL을 이용해서 User필드를 바로가져오기 때문에 N+1문제를 해결하는 모습이다. 하지만 문자열안에 쿼리가 작성되어 있어서 오타를 낼 수 있는 상황이다.
QueryDSL을 이용한 코드
@Override
public Optional<Todo> findByIdWithUser(Long todoId) {
QTodo todo = QTodo.todo;
QUser user = QUser.user;
Todo result = jpaQueryFactory
.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(todo.id.eq(todoId))
.fetchOne();
return Optional.ofNullable(result);
}
QueryDSL을 이용하면 selectFrom, where과 같은 메서드를 이용해서 타입 안전하게 쿼리를 작성할 수 있다.
QueryDSL의 장점
- 문자가 아닌 코드로 쿼리를 작성할 수 있어 컴파일 시점에 문법 오류를 확인할 수 있다.
- 인텔리제이와 같은 IDE의 자동 완성 기능의 도움을 받을 수 있다.
- 복잡한 쿼리나 동적 쿼리 작성이 편리하다.
- 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.
- JPQL 문법과 유사한 형태로 작성할 수 있어 쉽게 적응할 수 있다.
그럼 QueryDSL을 어떻게 적용하는가??
먼저 의존성 추가하기
// 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"
- querydsl-jpa → QueryDSL의 JPA 연동
- querydsl-apt → QueryDSL 코드 자동 생성
- jakarta.annotation-api → QueryDSL 코드 생성 시 필요한 Jakarta 어노테이션
- jakarta.persistence-api → JPA 어노테이션 제공
다시 빌드 후에 디렉토리의 build -> generated -> sources -> annotaionProcessor -> ... -> 각 Entity별로 Q클래스가 생성되었는지 확인한다.
그 다음 Custom 리포지토리 인터페이스를 만든다. Custom suffix를 가지도록 인터페이스명을 작성해준다.
public interface TodoRepositoryCustom {
Optional<Todo> findByIdWithUser(Long todoId);
}
인터페이스를 만들었으면 내가 작성할 QueryDSL 메서드를 작성해준다.
이제 이 Custom repository를 implement 하는 CustomRepositoryImpl을 만들어준다.
@RequiredArgsConstructor
public class TodoRepositoryImpl implements TodoRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
@Override
public Optional<Todo> findByIdWithUser(Long todoId) {
QTodo todo = QTodo.todo;
QUser user = QUser.user;
Todo result = jpaQueryFactory
.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(todo.id.eq(todoId))
.fetchOne();
return Optional.ofNullable(result);
}
}
Impl 클래스에서는 이전에 Custom repository 인터페이스에서 명세해놓았던 메서드를 override한다. findByIdWithUser를 명세해 놓았으니 그 메서드를 구현한다.
private final JPAQueryFactory jpaQueryFactory;
이 필드는 JPAQueryFactory를 생성자를 통해 주입받은 것이다. 그래서 @RequiredArgsConstructor와 함께 사용한다.
JPAQueryFactory는 말그대로 쿼리공장, QueryDSL에서 SQL 쿼리를 생성하는 핵심 객체이다.
@Override
public Optional<Todo> findByIdWithUser(Long todoId) {
이 쿼리의 실행결과가 null일 수도 있으니 Optional로 감싸준다.
QTodo todo = QTodo.todo;
QUser user = QUser.user;
QueryDSL에서는 엔티티를 사용할 때 QClass(QueryDSL 전용 클래스)를 활용해야 한다.
QTodo와 QUser는 각각 Todo와 User 엔티티를 QueryDSL에서 사용할 수 있도록 변환한 객체이다. 이전에 봤듯 Q클래스는 빌드하면 자동으로 생성된다.
Todo result = jpaQueryFactory
.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(todo.id.eq(todoId))
.fetchOne();
return Optional.ofNullable(result);
JPAQueryFactory 메서드들로 쿼리를 작성한다.
- selectFrom(todo) : Todo 엔티티 테이블에서 select 해오겠다는 의미.
- .leftJoin(todo.user, user).fetchJoin() : Todo 엔티티와 연관된 User 엔티티를 LEFT JOIN으로 함께 가져오겠다는 의미. 또한 fetchJoin()을 사용해서 N+1 문제를 해결
- .where(todo.id.eq(todoId)) : todo.id가 todoId와 같은 데이터를 찾는다는 조건.
- .fetchOne(); : 조회된 결과가 단일 객체일 때 사용. 만약 여러 개의 결과가 나올 수 있다면 .fetchOne()이 아니라 .fetch()를 사용해야 한다.
- return Optional.ofNullable(result); : result가 null일 수도 있기 때문에, Optional.ofNullable()로 감싸서 반환한다. 그럼 null일 경우 Optional.empty()를 반환하여 NullPointerException 방지 역할을 한다.
이제 QueryDSL로 쿼리 메서드를 구현을 했으니 어떻게 사용할까?
public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryCustom {
그 전에 계속 사용하고 있었던 JpaRepository를 상속받는 TodoRepository에 TodoRepositoryCustom도 같이 상속받아서 쓰면 된다.
아니 자바에서는 다중상속 지원 안한다면서 이거 다중상속 아닌가요? 하고 생각할 수 있겠지만 (내가 그랬다) TodoRepository, JpaRepository, TodoRepositoryCustom 모두 인터페이스이기 때문에 다중 상속이 가능하다.
인터페이스는 추상 클래스보다 더 추상적이므로 여러 인터페이스를 상속받는 다중 상속을 지원한다.
@Transactional(readOnly = true)
public TodoResponse getTodo(long todoId) {
Optional<Todo> foundTodo = todoRepository.findByIdWithUser(todoId);
그럼 이제 이렇게 서비스 단에서 todoRepository에서 바로 findByIdWithUser를 사용할 수 있게 된다!
QueryDSL 처음엔 의존성을 추가하고, 뭘 implement하고 상속받으라는 건지 도통 모르겠어서 어떻게 쓰는거야!! 했는데 이렇게 쭉 정리해보니 이제 쓰는 법을 알겠다. 이제 QueryDSL이용해서 타입안전하게 쿼리를 작성해보자~!
'Develop > Spring' 카테고리의 다른 글
스프링에서 예외를 처리하기 (0) | 2025.03.21 |
---|---|
QueryDSL의 BooleanExpression, Projections, Pagination (0) | 2025.03.14 |
AOP? (0) | 2025.03.12 |
테스트 코드 (0) | 2025.03.12 |
Proxy가 도대체 뭔데 (0) | 2025.03.11 |