Querydsl 필요성
- 프로젝트를 진행하면서 공간 상세 유형이 추가로 선택할 때마다 검색 조건에 추가하고 싶었다. JPQL로 이를 구현하고 싶었지만, 상세 타입이 하나만 있거나 아예 없거나 이 두가지 경우만 처리할 수 있었다.
- 세부 사항이 추가될 때마다 서비스 로직에서 분기 처리를 할 수 있겠지만 굉장히 비효율적이다. Querydsl을 사용하면 조건에 따른 동적 쿼리를 손쉽게 처리할 수 있어 적용한 경험을 공유하려고 한다!
public interface SpaceRepository extends JpaRepository<Space, Long> {
@Query("SELECT s FROM Space s " +
"WHERE s.spaceType = :spaceType " +
"AND s.realEstate.address.sido = :sido " +
"AND s.realEstate.address.sigungu = :sigungu " +
"AND s.maxCapacity >= :minCapacity " +
"AND (:detailedType IS NULL OR :detailedType MEMBER OF s.detailedTypes) " +
"AND s.isDeleted = false")
Page<Space> findByCriteria(@Param("spaceType") SpaceType spaceType,
@Param("sido") String sido,
@Param("sigungu") String sigungu,
@Param("minCapacity") int minCapacity,
@Param("detailedType") DetailedType detailedType,
Pageable pageable);
}
Querydsl 왜 사용하는가?
- 위의 예시처럼 조건에 따른 동적 쿼리를 작성해야 할 때 유용하다.
- 타입 안전성(Type-Safety)으로 컴파일 오류를 알려준다.
- SQL 쿼리를 자바 코드로 직접 작성할 수 있어서 가독성이 향상된다.
- IDE가 자동완성을 제공해서 개발 생산성이 증가한다.
Querydsl 의존성 (SpringBoot 3.2)
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")
Querydsl Q-type
- 위의 의존성을 주입하여 빌드하면 아래 경로에 Q-type클래스가 생성된다.
- Q-type은 엔티티 클래스에 대한 타입 안전한 참조를 제공한다. 쿼리를 구성할 때 문자열이 아닌 jvm이 타입을 검증할 수 있는 객체를 제공하는 것이다.
- 클래스 속성과 구조를 설명해 주는 메타데이터 역할을 한다.
@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer")
public class QAddress extends BeanPath<Address> {
private static final long serialVersionUID = -279459010L;
public static final QAddress address = new QAddress("address");
public final StringPath dong = createString("dong");
public final StringPath jibunAddress = createString("jibunAddress");
public final StringPath roadAddress = createString("roadAddress");
public final StringPath sido = createString("sido");
public final StringPath sigungu = createString("sigungu");
public QAddress(String variable) {
super(Address.class, forVariable(variable));
}
public QAddress(Path<? extends Address> path) {
super(path.getType(), path.getMetadata());
}
public QAddress(PathMetadata metadata) {
super(Address.class, metadata);
}
}
Querydsl 환경 설정
1. Spring Bean 등록(JPAQueryFactory)
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
- @PersistenceContext EntityManager 의존성 주입을 요청한다.
- JPAQueryFactory는 Querydsl 핵심 클래스로 Type-Safe한 쿼리를 생성하고 실행한다. 생성자로 EntityManager를 전달하면 애플리케이션 어디서나 JPAQueryFactory를 주입받아 사용할 수 있게 된다.
2. 사용자 정의 쿼리 메서드 인터페이스
public interface CustomSpaceRepository {
Page<Space> findBySpaceTypeInSeoulQuerydsl(SpaceType spaceType, Pageable pageable);
Page<Space> findByCriteriaQuerydsl(SpaceType spaceType, String sido, String sigungu, int minCapacity, Set<DetailedType> detailedTypes, Pageable pageable);
}
3. 사용자 정의 쿼리 메서드 구체클래스
@Repository
@RequiredArgsConstructor
public class CustomSpaceRepositoryImpl implements CustomSpaceRepository {
private final JPAQueryFactory jpaQueryFactory;
@Override
public Page<Space> findBySpaceTypeInSeoulQuerydsl(SpaceType spaceType, Pageable pageable) {}
@Override
public Page<Space> findByCriteriaQuerydsl(SpaceType spaceType, String sido, String sigungu, int minCapacity, Set<DetailedType> detailedTypes, Pageable pageable) {}
}
4. Jpa Repository 확장
- 사용자 정의 쿼리 메서드 인터페이스를 상속하여 spaceRepository에서 Querydsl 쿼리를 사용할 수 있다.
public interface SpaceRepository extends JpaRepository<Space, Long>, CustomSpaceRepository{}
Querydsl 쿼리
@Override
public Page<Space> findByCriteriaQuerydsl(SpaceType spaceType, String sido, String sigungu, int minCapacity, Set<DetailedType> detailedTypes, Pageable pageable) {
BooleanExpression conditions = space.spaceType.eq(spaceType)
.and(space.realEstate.address.sido.eq(sido))
.and(space.realEstate.address.sigungu.eq(sigungu))
.and(space.maxCapacity.goe(minCapacity))
.and(space.isDeleted.isFalse());
if (detailedTypes != null && !detailedTypes.isEmpty()) {
conditions = conditions.and(space.detailedTypes.any().in(detailedTypes));
}
List<Space> spaces = jpaQueryFactory.selectFrom(space)
.where(conditions)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long total = jpaQueryFactory
.select(space.count())
.from(space)
.where(conditions)
.fetchOne();
long totalCount = total != null ? total : 0;
return new PageImpl<>(spaces, pageable, totalCount);
}
- Querydsl은 문법이 굉장히 직관적이라 처음 봐도 어떤 일이 벌어지고 있는지 유추가 가능하다.
- where 조건
- 비교 연산자
- .eq(): eqals
- .ne(): not equals
- .lt(): less than
- .le(): less than or equals
- .gt(): greater than
- .ge(): greater than or equals
- 논리 연산자
- 널 체크
- 문자열 연산
- .startsWith(), .endsWith(), .contains(), .like()
- 컬렉션 처리
- .isEmpty(), .isNotEmpty()
- .any(): 엔티티 컬렉션 속성 중 하나라도 주어진 조건을 만족하는 지 검사
- .all(): 엔티티 컬렉션 속성이 모두 주어진 조건을 만족하는 지 검사
- .none(): 엔티티 컬렉션 속성 중 어떤 것도 주어진 조건을 만족하지 않는지 검사
- .selectCase(): 조건에 따라 다른 결과 반환
- .groupBy(), .having()
- 서브쿼리
참고자료