이전 글

ORM - Hibernate - JPA - QueryDSL을 알아보자

QueryDSL에 대해서 어떤 것인지, 어떠한 구조를 가지고 ORM이 진행되는지를 알아보았다.

작은 서비스를 가정하고, 구현한 상태에서 QueryDSL을 적용하여 간단히 사용해보자.


셋업

plugins {
    id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10' // Q 클래스 생성 플러그인
    // ...
}

def querydslDir = "$buildDir/generated/querydsl" // 생성된 Q 클래스가 저장될 위치를 정의한다.

// queryDSL 자체에 대한 config
querydsl { 
    jpa = true // jpa = true로 설정하여 JPA 애너테이션을 인식
    querydslSourcesDir = querydslDir // querydslSourcesDir를 querydslDir로 설정하여 Q 클래스에 대한 output 디렉토리를 지정
}

sourceSets {
    main.java.srcDir querydslDir // Q 클래스 등이 저장되는 querydslDir를 소스 코드가 저장되는 디렉토리로 등록
		// 이 덕에 Gradle이 자동으로 QueryDSL 코드를 컴파일할 수 있다.
}

configurations {
    querydsl.extendsFrom compileClasspath // querydsl 클래스 경로를 컴파일 클래스 경로에 상속
}

// 내부적으로 Querydsl은 Java의 Annotation Processor를 이용한다.
// 컴파일러는 이 경로에 위치한 모든 JAR 파일들을 검색하여 annotation processor를 찾는다. 
// 그리고 이를 실행하여 소스 코드에서 정의한 어노테이션들을 처리한다.
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

dependencies {
		//...
		implementation "com.querydsl:querydsl-codegen:5.0.0" // QueryDSL 코드 생성
		implementation "com.querydsl:querydsl-jpa:5.0.0" // JPA 지원
		implementation "com.querydsl:querydsl-apt:5.0.0" // Annotation Process Tool
		//...
}

// -proc:only: 이 옵션은 컴파일러의 어노테이션 처리만 수행하고, 클래스를 실제로 컴파일하지 않는 것을 의미한다.
// -processor: 이 옵션은 어노테이션 프로세서를 지정하는데, QueryDSL의 어노테이션 프로세서와 Lombok의 어노테이션 프로세서를 지정하여 명시적으로 설정한다.
project.afterEvaluate {
    project.tasks.compileQuerydsl.options.compilerArgs = [
            "-proc:only",
            "-processor", project.querydsl.processors() +
                    ',lombok.launch.AnnotationProcessorHider$AnnotationProcessor'
    ]
}

// Java 컴파일 작업이 compileQuerydsl 작업에 의존하도록 설정한다.
// compileQuerydsl 작업이 먼저 수행된 후에 compileJava 작업이 수행되게끔 순서를 정해주는 것인데,
// QueryDSL Q-Type 소스 생성 작업이 Java 컴파일 작업 전에 먼저 이뤄지도록 한다.
// 위 afterEvaluate와 같이 사용하면, Lombok 어노테이션 프로세스와 충돌하지 않는다.
tasks.named('compileJava') {
    dependsOn tasks.named('compileQuerydsl')
}

자세한 설명은 gradle에 주석으로 달아놓았다.


Repository

public interface BookRepositoryCustom {

	List<Book> findAllBooks();
}

public interface BookRepository extends JpaRepository<Book, Long>, BookRepositoryCustom {
}

@Repository
@RequiredArgsConstructor
public class BookRepositoryImpl implements BookRepositoryCustom {

	private final JPAQueryFactory queryFactory;

	public List<Book> findAllBooks() { // findAll 쓰면 된다. 그냥 예제임
		QBook book = QBook.book;
		
		return queryFactory
				.selectFrom(book)
				.fetch();
	}
}

JpaRepository는 인터페이스로서 동작하고, 빌드를 하더라도 그 구현체(implement)가 없기 때문에(대신, 런타임에 프록시를 이용해 동적으로 구현한다), Interface로서 사용하게 되어 있다.

스크린샷 2023-07-19 오후 8.12.07.png

BookRepositoryCustom은 사용자 정의 쿼리 메소드를 선언하는 인터페이스로, 이를 상속받은 BookRepositoryImpl은 실제 QueryDSL 쿼리를 작성하여 실행하는 구현체다.

이를 통해서 우리는 JpaRepository를 상속받은 BookRepository 인터페이스에서 BookRepositoryCustom을 상속받는 것으로 Spring Data JPA의 기능과 QueryDSL(RepositoryImpl)의 기능을 동시에 활용할 수 있다.

스프링은 JpaRepository 인터페이스를 상속받는 Repository 인터페이스를 찾으면, 이를 구현하는 프록시 객체를 생성하여 직접 기본 제공 기능을 구현한다.