JPA/Querydsl

[Querydsl] SpringBoot 3 Querydsl 적용해보기 (Dto반환)

Kyle H 2023. 6. 21. 23:41

"스프링 부트와 AWS로 혼자 구현하는 웹서비스" 라는 도서를 읽던 중에 Querydsl 실제 적용 내용이 없어서, Querydsl 강의 들은 내용도 정리 할 겸 블로그에 글을 남겨보려고 합니다.

 

해당 프로젝트에서 사용한 버전은 다음과 같습니다.

- SpringBoot 3.1.0

- hibernate 6.2.2

- querydsl 5.0.0

버전이 바뀌면 적용 방법이 달라질 수 있습니다.

 

1. build.gradle 셋팅

먼저 build.gradle에서 querydsl 의존성을 추가합니다. 

기본적으로 data-jpa의 의존성은 있다고 가정합니다.

dependencies {
	...

    //Querydsl 추가
    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"
}

다음으로 Q타입이 Gradle의 JavaCompile을 통해 만들 수 있는 설정과, Q타입이 생성될 위치를 지정해줍니다.

def querydslSrcDir = 'src/main/generated'
clean {
    delete file('src/main/generated')
}
tasks.withType(JavaCompile) {
    options.generatedSourceOutputDirectory = file(querydslSrcDir)
}

 

위 설정을 적용하면, gradle을 통해 clean 할 때 Q타입이 지워지고, JavaCompile을 하면서 Q타입을 새로 만듭니다. 

Gradle을 새로고침하여 라이브러리가 제대로 들어왔는지 확인합니다.


2. Q타입 생성

스프링부트 애프리케이션을 실행하면 컴파일 시점에 Q타입이 자동으로 생성됩니다.

Q타입만 갱신하고 싶을 경우, 위에서 만든 설정을 활용합니다.

우측 Gradle에서 Tasks/other/compileJava를 실행해봅니다.

우측 main/generated 아래 Posts 엔티티의 Q타입 QPosts가 제대로 생성되었습니다.

이제 이 Q타입을 활용하여 querydsl 쿼리를 작성해봅니다.


3. Querydsl 적용하기

3.1 인터페이스 생성

먼저 Spring data jpa를 활용한 PostsRepository는 기본적으로 인터페이스입니다. Spring data jpa에서 다양한 메서드를 제공하여 주지만 개발자가 직접 만든 로직을 추가하고 싶을 땐 PostsRepository에 Custom한 Repository를 추가해줘야합니다.

public interface PostsRepositoryCustom {

    List<PostsDto> findAllDesc();
}

위와 같이 임의의 이름으로 새로운 인터페이스를 정의하고, 구현하고자 하는 메서드의 시그니처를 작성합니다. 

여기서는 Posts 엔티티의 Dto를 리스트로 만들어 id의 내림차순으로 반환해보려고 합니다.

 

3.2 Dto 생성

리포지토리 구현에 앞서 PostsDto를 만들어줍니다.

import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;

@Data
public class PostsDto {

    private Long postsId;
    private String title;
    private String content;
    private String author;

    @QueryProjection
    public PostsDto(Long postsId, String title, String content, String author) {
        this.postsId = postsId;
        this.title = title;
        this.content = content;
        this.author = author;
    }
}

단순 예제로 Posts 엔티티의 값들을 가져와 PostsDto를 만들어주었습니다. 

주목할 점은 생성자에 @QueryProjection 이란 애노테이션을 붙였다는 점입니다. 

이 애노테이션은, 뒤에서 작성할 querydsl에서 Dto를 반환하는데 편리함을 제공합니다. 

대신 Dto가 querydsl 패키지에 의존한다는 단점이 있습니다.\

 

3.3 인터페이스 구현

import com.querydsl.jpa.impl.JPAQueryFactory;
import com.tech.book.springboot.web.dto.PostsDto;
import com.tech.book.springboot.web.dto.QPostsDto;
import jakarta.persistence.EntityManager;

import java.util.List;

import static com.tech.book.springboot.domain.posts.QPosts.posts;

public class PostsRepositoryImpl implements PostsRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public PostsRepositoryImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    @Override
    public List<PostsDto> findAllDesc() {
        return queryFactory
                .select(new QPostsDto(
                        posts.id,
                        posts.title,
                        posts.content,
                        posts.author
                ))
                .from(posts)
                .orderBy(posts.id.desc())
                .fetch();
    }

}

3.1에서 만든 인터페이스를 구현해주는 단계입니다.

여기서 한가지 주의해야 할 점은, 인터페이스는 임의의 이름으로 만들어도 되지만, 구현체는 추가하려는 대상 Repository명 + Impl이 되어야 합니다. 여기선 PostsRepository에 기능을 추가하므로 PostsRepositoryImpl로 생성해 주었습니다.

 

querydsl의 쿼리를 만들기 위한 JPAQueryFactory를 생성자에서 EntityManager를 통해 만들어줍니다. 

JPAQueryFactory를 어떻게 만드느냐는 빈으로 주입 받는 방법도 있고 여러가지입니다. 

여기서는 단순하게 명시적인 방법을 사용했습니다.

 

위에서 만든 메서드를 구현할 때 querydsl의 문법이 드디어 사용됩니다. Q타입의 static 필드인 QPosts.posts를 static import하여 쿼리를 깔끔하게 작성할 수 있습니다. 

반환값을 Posts가 아닌 PostsDto를 사용하기 위해 PostsDto의 생성자를 사용합니다. 

아까 Dto를 만들때 @QueryProjection란 애노테이션을 사용한 것이 빛을 발하는 시간입니다.

일반 JPQL을 사용할땐 Dto를 반환하려면 반환타입의 모든 패키지명을 명시해주었어야 했습니다. 

 

querydsl의 Projection을 사용하는 방법도 있지만 이 방법이 코드 자체는 가장 깔끔하다고 느껴집니다. 

 

3.4 Repository에 적용하기

마지막으로 이 PostsRepository에 extends 항목으로 이 Custom Repository를 추가해줍니다.

public interface PostsRepository extends JpaRepository<Posts, Long>, PostsRepositoryCustom {
}

4. 테스트 

그럼 작성한 쿼리가 제대로 동작하는지 테스트를 작성해보겠습니다.

@SpringBootTest
class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

    @AfterEach
    public void cleanup() {
        postsRepository.deleteAll();
    }
    
    ...
    
    @Test
    @DisplayName("Querydsl_적용")
    public void applyQuerydsl() throws Exception {
        // given
        String title1 = "title1";
        String content1 = "content1";
        String author1 = "author1";

        postsRepository.save(Posts.builder()
                .title(title1)
                .content(content1)
                .author(author1)
                .build());

        String title2 = "title2";
        String content2 = "content2";
        String author2 = "author2";

        postsRepository.save(Posts.builder()
                .title(title2)
                .content(content2)
                .author(author2)
                .build());

        // when
        List<PostsDto> postsList = postsRepository.findAllDesc();

        // then
        assertThat(postsList.get(0).getTitle()).isEqualTo(title2);
        assertThat(postsList.get(1).getTitle()).isEqualTo(title1);
    }
 }

Repository는 그대로 PostsRepository를 사용합니다.

한번 테스트를 돌려보겠습니다.

 

테스트도 성공하고, JPQL과 쿼리도 예상한대로 잘 실행되는 것을 볼 수 있습니다.

 

Querydsl을 통해 SQL도 java코드로 짤 수 있다는 점이 정말 재밌는 것 같습니다.

컴파일 시점에 쿼리 문법 오류도 찾을 수 있으니 사용하지 않을 이유가 없다고 느껴집니다.

 

긴 글 읽어주셔서 감사합니다~!

'JPA > Querydsl' 카테고리의 다른 글

[Querydsl] Querydsl with Kotlin (Kotlin으로 querydsl 사용하기)  (0) 2023.09.07