Spring Data

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

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

안녕하세요?
이번 시간에는 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

반응형