본문 바로가기
Spring Data

Spring Boot Data Jpa 프로젝트에 Querydsl 적용하기

by 향로 (기억보단 기록을) 2018. 12. 31.
반응형

안녕하세요?
이번 시간에는 Spring Boot Data Jpa 프로젝트에 Querydsl을 적용하는 방법을 소개 드리겠습니다.

모든 코드는 Github에 있습니다.

Spring Data Jpa를 써보신 분들은 아시겠지만, 기본으로 제공해주는 @Query로는 다양한 조회 기능을 사용하기에 한계가 있습니다.

그래서 이 문제를 해결하기 위해 정적 타입을 지원하는 조회 프레임워크를 사용하는데요.
Querydsl은 Jooq와 함게 가장 유명한 조회 프레임워크입니다.

이번 포스팅에서는 Spring Boot Data Jpa에서 Querydsl을 어떻게 설정하는지를 이야기합니다.
Querydsl의 장점 혹은 왜 써야하는지 등의 내용은 담지 않습니다.

이건 나중에 한번 각잡고 작성해서 공유드리겠습니다 :)

개발환경은 다음과 같습니다.

  • IntelliJ
  • Spring Boot 2.1.1
  • Gradle
  • Lombok

IntelliJ가 아닌 이클립스라도 크게 문제는 없습니다만, IntelliJ라면 더욱 편하게 진행하실 수 있습니다.

그럼 이제 시작해보겠습니다.

1. Gradle 설정

Gradle Multi Module이 아닌 단일 모듈로 작업을 시작합니다.

만약 Gradle Multi Module에서 어떻게 사용하는지 궁금하신 분들은 제 개인프로젝트를 참고해보세요

먼저 스프링부트와 Gradle로 프로젝트를 생성합니다.

gradle1

그리고 build.gradle을 열어 아래와 같이 Querydsl 관련 설정을 추가합니다.

먼저 Querydsl 플러그인 설정을 먼저합니다.

gradle2

buildscript {
    ext {
        5...
        querydslPluginVersion = '1.0.10' // 플러그인 버전
    }
    repositories {
        ...
        maven { url "https://plugins.gradle.org/m2/" } // 플러그인 저장소
    }
    dependencies {
        ...
        classpath("gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:${querydslPluginVersion}") // querydsl 플러그인 의존성 등록
    }
}

위 플러그인이 있어야만 Querydsl의 도메인 모델인 QClass들이 생성됩니다.
QClass 들이 Querydsl의 중심이기 때문에 꼭 있어야합니다.

자세한건 이후 실습때 경험해보실 수 있습니다.

그리고 Querydsl을 사용할 수 있는 라이브러리를 추가합니다.

gradle3

dependencies {
    compile("com.querydsl:querydsl-jpa") // querydsl
    compile("com.querydsl:querydsl-apt") // querydsl
    ...
}

의존성까지 다 추가되셨다면 Gradle에 Querydsl의 도메인인 QClass 생성을 위한 Task를 추가합니다.

즉, 위에서 설정한 Plugin을 사용하는 Task 추가입니다.

// querydsl 적용
apply plugin: "com.ewerk.gradle.plugins.querydsl" // Plugin 적용
def querydslSrcDir = 'src/main/generated' // QClass 생성 위치

querydsl {
    library = "com.querydsl:querydsl-apt"
    jpa = true
    querydslSourcesDir = querydslSrcDir
}

sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', querydslSrcDir]
        }
    }
}

위 설정값을 모두 합친 코드는 아래와 같습니다.

buildscript {
    ext {
        springBootVersion = '2.1.1.RELEASE'
        querydslPluginVersion = '1.0.10'
    }
    repositories {
        mavenCentral()
        maven { url "https://plugins.gradle.org/m2/" } // plugin 저장소
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:${querydslPluginVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.jojoldu.blogcode'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile("com.querydsl:querydsl-jpa") // querydsl
    compile("com.querydsl:querydsl-apt") // querydsl

    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-web')

    runtimeOnly('com.h2database:h2')
    compile('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

// querydsl 적용
apply plugin: "com.ewerk.gradle.plugins.querydsl"
def querydslSrcDir = 'src/main/generated'

querydsl {
    library = "com.querydsl:querydsl-apt"
    jpa = true
    querydslSourcesDir = querydslSrcDir
}

sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', querydslSrcDir]
        }
    }
}

자 여기까지가 기존의 Querydsl & Gradle 설정 방법이였는데요.
최근 Gradle 5.0 이상 & IntelliJ 2020 이상부터는 제대로 작동하지 않는 경우가 있습니다.
이때 어떻게 해야할지 소개드립니다.

Gradle 5.0 이상 & IntelliJ 2020.x 사용시

위 설정들은 Gradle 4 & IntelliJ 2019 버전 기준입니다.
최근 Gradle 버전이 계속 증가하면서 Querydsl의 Gradle Plugin이 해당 버전을 못쫓아가는 경우가 계속 발생하는데요.
그러다보니 계속해서 QClass 생성 방법이 변경되다보니 Gradle & IntelliJ가 업데이트 될 때마다 새로운 설정 방법이 필요하게 됩니다.

이로 인해서 최근엔 Gradle의 Annotation processor 을 사용하는 방법을 많이 사용하고 계십니다.
저 역시 최근 프로젝트에서는 Annotation processor 으로 설정하고 있습니다.

해당 설정들에 대한 상세한 설명은 허니몬님의 블로그글을 꼭 정독해보시길 추천드립니다.

아래는 Gradle Plugin이 필요 없는 설정 (build.gradle) 코드입니다.

즉, com.ewerk.gradle.plugins.querydsl 플러그인 사용하지 않습니다.

plugins {
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
}
...

apply plugin: "io.spring.dependency-management"
dependencies {
    compile("com.querydsl:querydsl-core") // querydsl
    compile("com.querydsl:querydsl-jpa") // querydsl
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // querydsl JPAAnnotationProcessor 사용 지정
    annotationProcessor("jakarta.persistence:jakarta.persistence-api")
    annotationProcessor("jakarta.annotation:jakarta.annotation-api")

}

def generated='src/main/generated'
sourceSets {
    main.java.srcDirs += [ generated ]
}

tasks.withType(JavaCompile) {
    options.annotationProcessorGeneratedSourcesDirectory = file(generated)
}

clean.doLast {
    file(generated).deleteDir()
}

이것 이외에는 별도의 설정이 필요 없습니다.
설정 하시고 나면 이후부터는 Gradle Project (View -> Tool Windows -> Gradle Project)을 열어 Tasks -> other -> compileJava를 실행시키시면 src/main/generated에 Q클래스들이 생성됩니다.

gradle-task

2. Java Config & 기본 사용법

먼저 Java 설정을 진행합니다.

2-1. Java Config

설정값을 모아둔 패키지에 QuerydslConfiguration을 생성합니다.

config1

@Configuration
public class QuerydslConfiguration {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

위 설정으로 이 프로젝트에서는 어느 곳에서나 JPAQueryFactory를 주입 받아 Querydsl을 사용할 수 있게 됩니다.

그럼 한번 간단하게 사용해볼까요?

2-2. 기본적인 사용법

먼저 테스트로 사용할 Entity를 하나 생성해보겠습니다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Academy {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String address;

    @Builder
    public Academy(String name, String address) {
        this.name = name;
        this.address = address;
    }
}

그리고 테스트로 데이터를 넣고, 검증할 Repository도 하나 생성합니다.

public interface AcademyRepository extends JpaRepository<Academy, Long> {
}

자 그럼 여기서 Querydsl Repository를 하나 생성하겠습니다.
클래스명은 AcademyRepositorySupport입니다.

@Repository
public class AcademyRepositorySupport extends QuerydslRepositorySupport {
    private final JPAQueryFactory queryFactory;

    public AcademyRepositorySupport(JPAQueryFactory queryFactory) {
        super(Academy.class);
        this.queryFactory = queryFactory;
    }

    public List<Academy> findByName(String name) {
        return queryFactory
                .selectFrom(academy)
                .where(academy.name.eq(name))
                .fetch();
    }

}
  • 설정으로 Bean 등록된 queryFactory를 생성자 인잭션으로 주입 받아 사용합니다.

이 코드를 생성하다보면 다음과 같은 오류 메세지가 나옵니다.

querydsl1

이건 Querydsl의 QClass인 academy를 사용하고 싶은데 아직 찾을수 없다는 뜻인데요.
QClass를 생성해보겠습니다.

IntelliJ의 Gradle View를 열어서 Tasks -> other -> compileQuerydsl를 더블클릭으로 실행합니다.
(이 Task가 위 Gradle 설정에서 등록한 Task입니다.)

querydsl2

그럼 아래와 같이 Build가 진행되는데요.
성공으로 끝나면 현재 프로젝트에 있는 모든 Entity의 QClass가 생성됩니다.

querydsl3

build.gradle에서 설정한 위치 (src/main/generated/) 을 보시면 아래와 같이 QClass가 생성된 것을 확인할 수 있습니다.

querydsl4

자 그럼 이제 클래스가 생성되었으니 아까 전 코드의 academy를 import 합니다.
해당 코드에서 option+enter를 사용해 Import를 진행하시면 됩니다.

querydsl5

그럼 아래와 같이 Import가 된 것을 확인할 수 있습니다.

querydsl6

나머지 코드를 완성합니다.

querydsl7

컴파일 에러가 나지 않는 상태가 되셨다면, 테스트 코드로 이 메소드가 정상작동하는지 테스트해보겠습니다.

2-3. 기본 사용법 테스트

Querydsl이 정상작동했다면 findByName이라는 메소드가 정상작동하겠죠?

아래와 같이 테스트 코드를 작성해서 검증해보겠습니다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class BasicTest {

    @Autowired
    private AcademyRepository academyRepository;

    @Autowired
    private AcademyRepositorySupport academyRepositorySupport;

    @After
    public void tearDown() throws Exception {
        academyRepository.deleteAllInBatch();
    }

    @Test
    public void querydsl_기본_기능_확인() {
        //given
        String name = "jojoldu";
        String address = "jojoldu@gmail.com";
        academyRepository.save(new Academy(name, address));

        //when
        List<Academy> result = academyRepositorySupport.findByName(name);

        //then
        assertThat(result.size(), is(1));
        assertThat(result.get(0).getAddress(), is(address));
    }
}

코드는 간단합니다.
1개의 Academy 데이터를 넣고, Querydsl로 만든 findByName메소드로 조회시 정상적으로 결과가 나오는지 확인합니다.

test1

정상적으로 Queyrdsl이 설정된 것을 확인할 수 있습니다!
하지만!
여기서 끝이 아니라, 한 단계 더 나아가보겠습니다.

3. Spring Data Jpa Custom Repository 적용

위와 같은 방식으로도 Querydsl을 사용할 수 있지만, 한가지 단점이 있는데요.

항상 2개의 Repository를 의존성으로 받아야한다는 것입니다.

Querydsl의 Custom Repository와 JpaRepository를 상속한 Repository가 기능을 나눠가졌기 때문인데요.

이를 해결하기 위해 Spring Data Jpa에서는 Custom Repository를 JpaRepository 상속 클래스에서 사용할 수 있도록 기능을 지원합니다.

전체적인 그림은 아래와 같습니다.

diagram

Spring Data 공식 문서을 참고하시면 Custom Repository 내용이 나오니 자세히 읽어보시면 됩니다.

위와 같이 구성하면 AcademyRepository에서 AcademyRepositoryImpl 의 코드도 사용할 수 있습니다.

일종의 공식이라고 보시면 되는데요, Custom이 붙은 인터페이스를 상속한 Impl 클래스의 코드는 Custom 인터페이스를 상속한 JpaRepository에서도 사용할 수 있습니다.

CustomImpl만 외우셔두 됩니다.

자 그럼 진행해보겠습니다.
먼저 AcademyRepository 와 같은 위치에 AcademyRepositoryCustom 인터페이스와 AcademyRepositoryImpl 클래스를 생성합니다.

custom1

그리고 AcademyRepositoryCustom 인터페이스와 AcademyRepositoryImpl 클래스에 다음과 같은 코드를 추가합니다.

public interface AcademyRepositoryCustom {
    List<Academy> findByName(String name);
}

클래스는 기존에 있던 Support 클래스 코드를 참고해서 구현합니다.

@RequiredArgsConstructor
public class AcademyRepositoryImpl implements AcademyRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @Override
    public List<Academy> findByName(String name) {
        return queryFactory.selectFrom(academy)
                .where(academy.name.eq(name))
                .fetch();
    }
}

다른 블로그를 보시면 QuerydslSupport 상속 코드도 추가하는데, 페이징이 필요한게 아니라면 안하셔도 됩니다
결국 JPAQueryFactory를 통해서 작동하는거라서요

그리고 이 코드를 AcademyRepository에서 쓸수 있게 상속 구조로 변경하겠습니다.

public interface AcademyRepository extends JpaRepository<Academy, Long>, AcademyRepositoryCustom {
}

그럼 이 코드가 정상작동하는지 테스트 해볼까요?

custom2

@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomTest {

    @Autowired
    private AcademyRepository academyRepository;

    @After
    public void tearDown() throws Exception {
        academyRepository.deleteAllInBatch();
    }

    @Test
    public void querydsl_Custom설정_기능_확인() {
        //given
        String name = "jojoldu";
        String address = "jojoldu@gmail.com";
        academyRepository.save(new Academy(name, address));

        //when
        List<Academy> result = academyRepository.findByName(name);

        //then
        assertThat(result.size(), is(1));
        assertThat(result.get(0).getAddress(), is(address));
    }
}

테스트 코드를 실행해보면!

custom3

이렇게 성공적으로 기능이 작동하는 것을 확인할 수 있습니다.
위 코드가 잘 적용된거죠?

4. 상속/구현 없는 Repository

마지막은 제가 가장 선호하는 방식인 Querydsl만으로 Repository를 구성하는 방법입니다.
아래처럼 JPAQueryFactory 만 있으면 Querdsl을 사용할 수 있습니다.

@RequiredArgsConstructor
@Repository 
public class AcademyQueryRepository {
    private final JPAQueryFactory queryFactory;

    public List<Academy> findByName(String name) {
        return queryFactory.selectFrom(academy)
                .where(academy.name.eq(name))
                .fetch();
    }
}
  • 최소한의 Bean 등록을 위해 @Repository를 선언합니다.
  • 별도의 상속(extends) / 구현(implements) 없이 JPAQueryFactory 만 있으면 됩니다.
  • 특정 Entity 만 사용해야한다는 제약도 없습니다.

잘 작동하는지 한번 확인해볼까요?

아래 테스트 코드를 수행해보면?

@Autowired
private AcademyQueryRepository academyQueryRepository;

@Test
public void querydsl_기본_기능_확인2() {
    //given
    String name = "jojoldu";
    String address = "jojoldu@gmail.com";
    academyRepository.save(new Academy(name, address));

    //when
    List<Academy> result = academyQueryRepository.findByName(name);

    //then
    assertThat(result.size(), is(1));
    assertThat(result.get(0).getAddress(), is(address));
}

테스트가 성공하는 것을 확인할 수 있습니다.

test2

Core 기능이 아닌 어드민/API 등에서 특정 Entity를 메인으로 확정할 수 없는 경우와 자주 변경되는 기능에 대해서는 위처럼 선언해서 사용하시면 심플하게 변경에 대응할 수 있습니다.ㄴ

5. 주의 사항

Querydsl의 QClass를 담는 src/main/generated는 자동생성되는 파일들의 디렉토리이니 무조건 .gitignore에 추가하셔야 합니다.

gitignore

반응형