[SpringDataJPA] QueryDSL 적용해보기

Dev_ch·2023년 5월 27일
0

이번 포스팅은 QueryDSL을 적용하고 어떻게 활용하는지, 어떤 부분에서 이점을 얻는지 알아보도록 하자

1. build.gradl

해당 포스팅은 SpringBoot 2.7.1 버전을 사용했다.

buildscript {
	ext {
		queryDslVersion = "5.0.0"
	}
}

plugins {
	...
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

dependencies {
	...
	implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
	annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}"
}

def querydslDir = "$buildDir/generated/querydsl"
querydsl {
	jpa = true
	querydslSourcesDir = querydslDir
}
sourceSets {
	main.java.srcDir querydslDir
}
configurations {
	querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
	options.annotationProcessorPath = configurations.querydsl
}

QueyrDSL을 적용하기에 앞서 build.gradle에 추가해줘야 한다. 특히 querydslDir 패키지에 맞춰 Q클래스들이 생성되니 경로를 잘 확인해주도록 하자.

적용이 완료됐다면 gradle 탭에서 해당 패키지로 이동해 compileQuerydsl을 실행해주면, 위에 설정했던 경로대로 QueryDSL 전용 Entity 클래스가 생성되었을 것 이다.

2. 들어가기에 앞서

여기서 고민해봐야할 문제가 있다. 서비스에 요구하는 쿼리 수준이 Spring Data JPA에서 쉽게 해결되지 않는다거나 동적쿼리를 필요로 한다거나 여러 JPQL이 사용되냐 등등 현재 프로젝트에 꼭 필요하냐에 대해 생각해봐야한다.

서비스나 프로젝트의 규모가 크거나, 기업과 같은 실무환경에서는 복잡하고 어려운 연산의 쿼리가 필요해지기에 QueryDSL은 분명 이점을 가져오겠지만 현재 진행하고 있는 서비스나 프로젝트가 그러한 쿼리들이 필요로 하지 않는다면 Spring Data JPA로 충분히 해결될 것 이다.

Spring Data JPA는 메서드를 통해 DB에서 데이터를 가져오는 것은 물론 성능 또한 최적화가 직접 작성하는 QueryDSL 보다 훨씬 더 나은 성능과 편리함을 가져다 주기에 꼭 필요해진 환경이 아니라면 QueryDSL을 무리해서 적용시킬 필요는 없다. 라고 먼저 말하고 싶다.

물론, 앞서 말했듯 기업이나 실제 비즈니스에서는 활용도가 높을 것 이다.

3. Spring Data JPA + QueryDSL 활용해보기

1. 추가되는 클래스

Spring Data JPA를 활용하면서 QueryDSL을 같이 활용하는 방법은 아래와 같다.

ContentRepository는 기존에 Spring Data JPA이고, QueryDSL 적용을 위해 새로 작성한 ContentCustomRepository와 ContentRepositoryImpl을 보도록 하자.

ContentCustomRepository의 경우 네이밍에 크게 상관을 가지지 않아도 되지만, 실제로 QueryDSL의 기능이 구현될 ContentRepositoryImpl은 Impl을 뒤에 꼭 붙여주도록 하자.

2. ContentCustomRepository 상속

기존에 사용하던 Spring Data JPA의 인터페이스에 JPARepository와 추가적으로 생성한 contentCustomRepository 까지 상속을 받아주도록 하자.

3. contentCustomRepository에 사용자 정의 인터페이스 구현

아래의 쿼리는 Spring Data JPA로도 충분히 구현 가능하다 🤔

public interface ContentCustomRepository {
    Page<Content> searchMyCommentPosts(Long userId, PageRequest pageRequest);
    Long countDuplicateLocation(String location, Long userId);
}

위와같이 사용할 Repository를 정의해주고 ContentRepositoryImpl에 실제 쿼리를 구현하자.

4. ContentRepositoryImpl 구현하기

public class ContentRepositoryImpl implements ContentCustomRepository {

    private final JPAQueryFactory queryFactory;

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

    @Override
    public Page<Content> searchMyCommentPosts(Long userId, PageRequest pageRequest) {
        List<Content> contents = queryFactory
                .select(content1)
                .from(content1)
                .innerJoin(content1.comments, comment)
                .where(comment.user.id.eq(userId))
                .orderBy(content1.createdAt.desc())
                .offset(pageRequest.getOffset())
                .limit(pageRequest.getPageSize())
                .fetch();

        JPAQuery<Long> countQuery = queryFactory
                .select(content1.count())
                .from(content1)
                .innerJoin(content1.comments, comment)
                .where(comment.user.id.eq(userId));

        return PageableExecutionUtils.getPage(contents, pageRequest, countQuery::fetchOne);
    }
    
        @Override
    public Long countDuplicateLocation(String location, Long userId) {
        return queryFactory
                .select(content1.count())
                .from(content1)
                .innerJoin(content1.group, group)
                .where(
                        group.id.in(getMyGroupIds(userId)),
                        content1.deletedYn.isFalse(),
                        content1.location.eq(location))
                .fetchOne();
    }
 }

QueryDSL을 사용하기 위해 JPAQueryFactory를 EntityManager를 통해 생성해주자.

초반에 생성해둔 Q클래스를 이용하여 queryFactory의 쿼리를 작성하면 되는데, 만약 Entity이름이 Content 였다면, QContent라는 이름으로 생성됐을 것 이고 내부에서 자동으로 생성자를 호출해주는데 이를 import하여 content1로 표시가 된 것 이다. group도 마찬가지.

사실 SQL과 JPA를 잘 활용한다면, QueryDSL을 큰 어려움 없이 작성할 것 이다. 그러니까 JPA라는 기초가 잘 되어있어야 QueryDSL의 활용도를 높일 수 있을 것 이다.


QueryDSL을 통해 복잡하거나 동적 쿼리 등등 이러한 것들을 작성하여 활용할 수 있겠지만, 쿼리가 너무 복잡해진다면 분리를 하는 것도 좋을 것 이고, QueryDSL을 사용하기 전에 Spring Data JPA를 통해 해결할 수 있지 않을까? 라는 고민도 해보는 것이 좋다.

필자의 경우 성능을 중요시하게 여기다보니, QueryDSL을 적용해보고 성능 테스트를 거쳐보면서 무조건 적용하는 것이 아닌 정말 효율적일까에 대해 먼저 고민해보고 QueryDSL을 사용해보자는 생각이 많이 들었다.

또한, JPA, SQL에 지식에 한계를 느끼기도 해서 역시 기초가 가장 중요하다는 것을 다시 한번 깨달았다. JPA에 대해 좀 더 깊게 공부를 해서 QueryDSL을 잘 다뤄보도록 하자 😉

2023.08.31 추가

1. QueryDSL 5.0.0 버전 설정

QueryDSL 5.0.0 이상 의존성 추가 하는 방법은 아래와 같다.

build.gradle

dependencies {
	.
    .
    .
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

위에서 작성했던 스크립트를 제외하고 해당 의존성만 추가해도 된다. 패키지 변경으로 인해 jakarta 관련 어노테이션 프로레서도 같이 꼭 의존성에 추가해주도록 하자.

2. QueryDSL Repository 단위 테스트에서 사용하는법

@DataJpaTest를 이용해 슬라이싱 테스트를 진행할때 JpaQueryFactory가 persistenceLayer가 아니다보니 빈 등록이 되지않아서 테스트를 돌릴 수 없는데 이때 따로 빈으로 등록해줄 수 있다.

테스트 패키지에서


@TestConfiguration
@RequiredArgsConstructor
public class TestQuerydslConfig {

    private final EntityManager entityManager;

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

    @Bean
    public UserRepositoryImpl userRepositoryImpl() {
        return new UserRepositoryImpl(queryFactory());
    }
}

위와 같은 클래스를 만들어주자. JPAQueryFactory를 슬라이싱 테스트에서도 작동할 수 있게 빈으로 등록하는 과정을 클래스화 시킨 것 이다.

@DataJpaTest
@ActiveProfiles("test")
@Import(TestQuerydslConfig.class)
class UserRepositoryTest {
...
}

@Import를 통해 작성한 클래스를 통해 JPAQueryFactory를 Bean으로 등록해주면 된다.

profile
내가 몰입하는 과정을 담은 곳

0개의 댓글