QueryDSL 적용하기

안녕하세요 Cocone M의 SUMMER 인텁십 1기 인턴 정우중입니다! ​ 저는 Cocone M에서 사용하는 모바일 명함을 타 그룹사 크루분들도 사용할 수 있도록 하는 기능을 개발했습니다.

코드를 작성해 나가던 중 그룹사 크루 분들의 정보를 검색할 수 있는 기능을 만들어야 하는 상황이 발생했습니다. 이때 상당히 복잡했지만 순수 JPQL으로 쿼리문을 짜고 있었는데 생각한대로 잘 작동하지 않아 다른 크루분과 상의를 했었습니다.

제 고민을 말씀드리니 그 상황에선 QueryDSL이라는 것을 사용하는 것이 훨씬 편할 것이라 말씀해주셔서 자리로 돌아와 공부를 하고 적용을 해봤습니다. 이때 공부한 내용과 어떻게 제 코드에 적용한 방법, 코드를 작성하며 느낀점 등을 공유해드릴까 합니다. ​

QueryDSL 설정하기

​ QueryDSL을 사용하려면 우선 설정을 진행해야 합니다. ​ 사내 홈페이지에 연관되어있는 프로젝트이기에 이미 작성된 레거시 코드가 있었고 다른 기능들에서 queryDSL을 사용하고 있어 따로 설정해 줄 필요는 없었기에 간단히 소개만 하고 넘어가겠습니다. maven과 gradle는 설정방법이 약간 다른데 이 프로젝트는 maven을 사용하였습니다. ​ 우선 의존성을 추가해 주기 위해 아래 코드를 pom.xml에 추가합니다. ​

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>4.4.0</version>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>4.4.0</version>
    <scope>provided</scope>
</dependency>

​ 다음으론 쿼리용 클래스를 생성해야 하는 QueryDSL을 위해 APT 플러그인을 설정해 줍니다. 그러면 Entity에 등록한 클래스가 접두사 Q가 붙은채로 새로 생성됩니다. QueryDSL에서쿼리를 작성하는데 Q클래스를 필요로 하는데 이는 Q 타입 클래스를 static import 하여 사용할 수 있습니다. ​ 위 설정들이 끝나면 드디어 QueryDSL을 사용할 준비가 완료되었습니다!


QueryDSL 문법

​ QueryDSL 문법에 대한 설명을 드리기 앞서 제가 작성한 코드를 공유해드리겠습니다. JPQL로 작성한 코드와 비교를 하면 좋겠지만 양해 부탁드립니다,,,^^ ​

public Page<CoconeCrewDto> getCoconeCrewList(CoconeCrewParam coconeCrewParam, Pageable pageable) {

    BooleanBuilder builder = getBooleanBuilder(coconeCrewParam);

    List<CoconeCrewDto> crews = queryFactory
						.select(coconeCrew)
            .from(coconeCrew)
            .where(builder)
            .orderBy(coconeCrew.crewName.asc(),coconeCrew.branchId.asc(),coconeCrew.coconeId.asc(),coconeCrew.jobTitle.asc(),coconeCrew.isActive.desc(),coconeCrew.isAdmin.desc())
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch()
            .stream()
            .map(CoconeCrewDto::new)
            .collect(Collectors.toList());

    Long count = queryFactory
            .select(coconeCrew.count())
            .from(coconeCrew)
            .where(builder)
            .fetchOne();

    return new PageImpl<>(crews, pageable, count);
}

  1. select절 select는 반환받을 객체를 의미합니다. 조건을 달아 특정 객체만 받을 수도 있지만 지금은 크루의 전체 정보를 받아야 하기에 엔티티를 반환했습니다.
  2. from절 엔티티에서 반환받기에 클래스 이름을 써주시면 됩니다.
  3. where절 조건절을 붙이기 위해선 사용합니다. 당장은 크루의 이름, 소속되어있는 그룹사, 직무, 재직여부, 크루정보를 관리할 수 있는 관리자 권한 5가지로 검색하는 기능을 만들었습니다. 조건을 나열할 수 도 있지만 동적쿼리를 해결하기위해 BooleanBuilder를 사용했습니다. BooleanBuilder의 코드는 아래와 같습니다. 검색할 매개변수들을 CoconeCrewParam에 넣어놓고 사용했습니다.

private BooleanBuilder getBooleanBuilder(CoconeCrewParam coconeCrewParam) {
    BooleanBuilder builder = new BooleanBuilder();

    //크루 이름 검색
    if (StringUtils.isNotEmpty(coconeCrewParam.getCrewName())) {
        builder.and(coconeCrew.crewName.contains(coconeCrewParam.getCrewName()));
    }

    //그룹사 검색
    if (StringUtils.isNotEmpty(coconeCrewParam.getBranchId())) {
        builder.and(coconeCrew.branchId.eq(coconeCrewParam.getBranchId()));
    }

    //직무 검색
    if (StringUtils.isNotEmpty(coconeCrewParam.getJobTitle())) {
        builder.and(coconeCrew.jobTitle.contains(coconeCrewParam.getJobTitle()));
    }

    //재직 여부로 검색
    if (StringUtils.isNotEmpty(coconeCrewParam.getIsActive())) {
        builder.and(coconeCrew.isActive.eq(coconeCrewParam.getIsActive().equalsIgnoreCase("on")));
    }

    //관리자 권한 여부로 검색
    if (StringUtils.isNotEmpty(coconeCrewParam.getIsAdmin())) {
        builder.and(coconeCrew.isAdmin.eq(coconeCrewParam.getIsAdmin().equalsIgnoreCase("on")));
    }

    return builder;
}

​ 4. orderBy절 정렬하는 방법을 정하는 절입니다. 크루 이름, 그룹사 이름, 회사 Id에 따라서 오름차순으로 정렬을 했고, 권한 관련된 부분은 보유 여부에 따라 “ON”, “OFF”로 나누어 주었기에 보유한 사람이 상단에 위치하도록 내림차순으로 정렬했습니다. 여러 조건을 한 번에 작성할 수 있어서 매우 간편했습니다.

```java
.orderBy(coconeCrew.crewName.asc(),coconeCrew.branchId.asc(),coconeCrew.coconeId.asc(),coconeCrew.jobTitle.asc(),coconeCrew.isActive.desc(),coconeCrew.isAdmin.desc())
```
  1. offset절, limit절 리스트로 반환하는 것이 아니라 페이지로 반환하려고 설정해주는 절입니다. 리스트로 반환하려고 한다면 작성하지 않아도 되는 코드입니다.
  2. fetch절 결과를 반환하는 절입니다. fetch(), fetchOne(), fetchFirst(), fetchResult(), fetchCount() 중 조건에 맞게 사용하면 됩니다. 위 프로젝트에선 일치하는 결과가 없어도 빈 리스트를 반환하도록 fetch()를 사용하였습니다.
  3. stream절, map절, collect절 결과가 엔티티로 반환이 되는데 이를 DTO로 변환하기 위해 사용한 절입니다.

​ 하단에 count는 결과값을 페이지로 만들기 위해 추가해준 단입니다.


마치며

​ 이렇게 간단하게 제가 프로젝트에서 사용한 QueryDSL에 대해 설명을 드렸습니다. 처음엔 상당히 복잡하다고 생각했는데 하나씩 차근차근 적용하다보니 기존에 사용하던 JPQL보다 훨씬 간편하고 재밌다는 느낌이 들더군요!

JPQL로 쿼리문을 작성할때는 런타임 에러가 발생해서 오류가 있을 경우 실행 후 찾아야 했지만 QueryDSL의 경우 오류가 있을 경우 컴파일 에러가 발생해서 수정이 훨씬 간편하다는 점이 가장 편리했던 것 같습니다. 앞으로 간단한 코드라도 QueryDSL을 사용하면 정말 좋을 것 같습니다.
감사합니다!