QueryDSL 도입기
현재 프로젝트에서 모든 CRUD 처리를 쿼리메서드, JPQL로 처리했었다.
단순한 조회는 대부분 쿼리메서드로 처리하고, fetchJoin이나 조건이 여러 개인 경우 JPQL을 사용했었다.
그러나 이제 검색 조건을 만들면서 동적 쿼리를 사용하는 케이스가 생기게 되었다.
현재 검색 조건으로
1. 스크럼의 제목
2. 리더의 닉네임
이 2개의 조건이 or로 들어오게 된다.
검색 조건을 dto로 받기에 동적으로 들어오는 조건에 맞춰서 쿼리가 나가야하는데 처음에 JPQL을 사용해서 만들었더니 결국 쿼리를 2개 만들어서 어떤 검색타입으로 들어왔는지 확인해서 불러주어야 했다.
이런 불편함을 없애고자 했던 이유도 컸고, 결국 실무에서 JPA를 사용하면 동적 쿼리를 많이 다루기에 QueryDsl을 거의 필수로 사용한다는 정보를 들었다.(대체제가 없다고 하셨다.)
그리고 JPQL과 QueryDsl의 차이점은 문법 체크였다.
@Query("SELECT s FROM Scrum s JOIN FETCH s.user WHERE s.team = :team AND s.deleteDate IS NULL")
List<Scrum> findByTeamWithFetchJoinUserAndDeleteDateIsNull(@Param("team") Team team);
위는 JPQL인데 결국 쿼리를 문자열로 작성하게 되고, 컴파일에서 해당 문법 에러를 잡지 못한다.
그러나 QueryDsl은 타입-세이프-쿼리이다.
return queryFactory
.selectFrom(scrum)
.where(scrum.team.eq(team).and(scrum.deleteDate.isNull()))
.fetch();
문자열 형태가 아닌 자바의 타입 시스템을 이용하여 작성하기에 문법의 에러를 컴파일에서 잡아줄 수 있다.
우선 스프링부트 2.x랑 3.x의 QueryDsl 도입 방법이 다르다.
스프링 부트 : 3.20
QueryDsl : 5.0.0
Java : 17
gradle : 8.5
위 환경으로 진행하였습니다.
먼저 yml에 플러그인 추가.
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
이후 QueryDsl 관련 디펜던시 추가(뒤에 jakarta 필수입니다.)
// QueryDSL
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"
그리고 아래 내용을 yml 마지막에 추가해주었습니다.
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
큐클래스 위치, QueryDsl 소스 파일 생성 위치 등 설정을 해줍니다.
QueryDsl을 사용하기 위해 Config을 생성해줍니다.
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(this.entityManager);
}
}
여기서 @PersistenceContext는 @Autowired와 같은 기능을 하지만 EntityManager를 위한 특화된 어노테이션입니다.
EntityManager는 Thread-safe 해야 되기 때문에 동시성 문제가 발생하지 않도록 @PersistenceContext가 설정을 해주는 것으로만 이해하고 있습니다.
그 다음 QueryDsl을 위한 정석 Repository 생성 방법을 보겠습니다.
먼저 기존에 사용하던 Repository는 그대로 냅둡니다.(쿼리메서드, JPQL용 메서드라고 보시면 됩니다.)
@Repository
public interface ScrumRepository extends JpaRepository<Scrum, Long> {
}
이후 QueryDsl을 위한 인터페이스를 생서합니다. (도메인)Repository(Custom) 과 같은 네이밍 규칙을 따른다고 합니다.
public interface ScrumRepositoryCustom {
Boolean existsActiveScrumByUser(User user);
List<Scrum> findActiveScrumsByTeam(Team team);
Optional<Scrum> findActiveScrumByScrumId(Long scrumId);
Page<Scrum> searchScrumWithPagination(ScrumSearchCondition condition, Pageable pageable);
}
저는 안에 QueryDsl용 메서드를 정의 해두었습니다.
이후 해당 인터페이스를 구현한 Impl 클래스를 생성합니다.
@RequiredArgsConstructor
public class ScrumRepositoryImpl implements ScrumRepositoryCustom {
private final JPAQueryFactory queryFactory;
}
여기서 JPAQueryFactory를 생성자 주입으로 넣어주는 것을 볼 수 있습니다.
해당 객체는 QueryDsl을 이용해서 쿼리를 만들 때 사용하게 됩니다.
이후 기존에 만들었던 JpaRepository를 상속하고 있던 ScrumRepository에 ScrumRepositoryCustom을 상속시켜줍니다.
@Repository
public interface ScrumRepository extends JpaRepository<Scrum, Long>, ScrumRepositoryCustom {
}
그러면 하나의 Repository로 JpaRepository도 이용하고 QueryDsl용 Repository도 사용이 가능합니다!!
결국 총 2개의 인터페이스, 1개의 클래스가 완성되게 됩니다.