본문 바로가기

JPA

동적인 쿼리를 만들어줘! QueryDSL 입문기

최종 프로젝트 Triplan을 개발하면서 QueryDSL을 사용하게 되었습니다. QueryDSL을 어떤 상황에서 왜 사용했는지 그래서 뭐가 좋아졌는지 정리해보도록 하겠습니다.

문제 상황

서비스 요구 사항 : 검색 조건(도시, 여행 테마, 검색어)에 맞는 게시글을 조회한다.

 

Triplan 프로젝트에 위와 같은 서비스 요구사항이 있었습니다. 여기서 문제 상황은 도시와 여행 테마, 검색어 모두 조건이 있을 수도 없을 수도 있다는 점 때문에 발생하였습니다.

 

도시는 전체 또는 특정한 도시(서울, 부산 등등)를 입력받았고, 여행 테마 또한 전체 또는 특정한 테마(액티비티, 맛집 투어 등등)를 입력받았습니다. 도시나 테마만 입력받고 전체 게시글을 조회할 수 있으므로 검색어가 아예 없는 경우도 고려해야 했습니다.

 

저희는 우선 프로그래머스 데브코스를 하면서 배운 것은 JPA, Spring Data JPA였기 때문에 우선 이 기술로 요구사항을 구현해보려 하였습니다. 우선 여행 테마 조건은 뒤로 미뤄두고 도시와 검색어 조건을 만족하는 코드는 다음과 같았습니다.

if (city == City.ALL) {
    // 지정된 특정 도시 없음
    if (!search.isEmpty()) {
        // 검색어 없음
        List<SchedulePost> recentPosts = schedulePostRepository.findAllByOrderByCreatedDateDesc(PageRequest.of(pageIndex, PAGE_SIZE));
        return convertToSchedulePostResponseList(recentPosts);
    } else {
        // 검색어 있음
        List<SchedulePost> recentPosts = schedulePostRepository.findAllByTitleOrContentContainingOrderByCreatedDateDesc(search, search, PageRequest.of(pageIndex, PAGE_SIZE));
        return convertToSchedulePostResponseList(recentPosts);
    }
} else {
    // 지정된 특정 도시 있음
    if (!search.isEmpty()) {    
        // 검색어 없음
        List<SchedulePost> recentPosts = schedulePostRepository.findAllByCityOrderByCreatedDateDesc(city, PageRequest.of(pageIndex, PAGE_SIZE));
        return convertToSchedulePostResponseList(recentPosts);
    } else {
        // 검색어 있음
        List<SchedulePost> recentPosts = schedulePostRepository.findAllByCityAndTitleOrContentContainingOrderByCreatedDateDesc(city, search, search, PageRequest.of(pageIndex, PAGE_SIZE));
        return convertToSchedulePostResponseList(recentPosts);
    }
}

어우 1초만 봐도 숨이 턱 막히는 코드를 만들어버렸습니다.

 

조건에 따라 Qeury문의 WHERE 절이 달라지기 때문에 검색 조건 별로 Query에 대응하는 모든 메서드를 만들어줘야만 했습니다.

 

경우의 수를 생각해보면 검색어 존재 유무, 도시 조건 존재 유무만 해도 2*2 로 분기가 4가지로 나눠집니다. 여기서 여행 테마까지 추가된다면 *2를 해서 8가지 경우의 수가 됩니다. 거기다가 다른 요구사항인 정렬 조건(조회순, 좋아요 순, 최신순)까지 추가된다면 *3을 해서 24개의 메서드를 만들어야 합니다.

 

위처럼 if else로 분기가 많이 나눠지게 되는 모양을 보는 순간 ‘아 이 방법은 아니구나’라는 생각이 딱 들었습니다. 그래서 ‘동적인 Query를 만들 수 있는 방법이 무엇이 있을까?’라는 생각으로 검색을 해보니 QueryDSL이라는 기술이 있었습니다.


QueryDSL

QueryDSL이란 JPQL을 코드로 작성할 수 있도록 도와주는 빌더입니다. 빌더로 코드를 작성하기 때문에 Spring Data JPA로 해결하지 못하는 복잡한 쿼리/동적 쿼리를 해결할 수 있습니다. pom파일 설정 등의 내용은 생략하고 코드가 어떻게 변하는지 살펴보겠습니다.

 

QueryDSL을 사용하려면 구현 코드를 만들어야 하는데, Spring Data JPA는 인터페이스로 동작하기 때문에 사용자 정의 Repository가 필요합니다.

 

 

먼저, QueryDSL을 담당할 Custom 인터페이스와 구현클래스를 만들어줍니다. 그리고 기존에 Spring Data JPA에서 사용하던 SchedulePostRepository에 CustomSchedulePostRepository를 extends 해줍니다.

 

이제 QueryDSL을 코드를 작성해줍니다.

public List<SchedulePost> search(String search, City city, Theme theme) {
    return queryFactory
            .select(schedulePost)
            .from(schedulePost)
            .join(schedulePost.schedule, schedule).fetchJoin()
            .join(schedule.scheduleThemes, scheduleTheme)
            .where(
                    isEqualToTheme(theme),
                    isEqualToCity(city),
                    isContainedInTitleOrContent(search)
            )
            .fetch();
}

private BooleanExpression isEqualToTheme(Theme theme) {
    return theme == Theme.ALL ? null : scheduleTheme.theme.eq(theme);
}

private BooleanExpression isEqualToCity(City city) {
    return city == City.ALL ? null : schedulePost.city.eq(city);
}

private BooleanExpression isContainedInTitleOrContent(String search) {
    return isContainedInContent(search)
            .or(isContainedInTitle(search));
}

private BooleanExpression isContainedInContent(String search) {
    return schedulePost.content.contains(search);
}

private BooleanExpression isContainedInTitle(String search) {
    return schedulePost.title.contains(search);
}

우선 search 메소드를 보면 SQL Query를 빌더 형식으로 만들어주는 것을 볼 수 있습니다. 여기서 주목해야 할 곳은 where() 부분입니다.

 

기존에 Query Method를 여러 개를 만들어줬던 이유는 쿼리의 “WHERE” 문에 조건이 올 수도 안 올 수도 있기 때문이었습니다. 하지만 QueryDSL을 썼을 때, where 조건에서 null 값은 무시됩니다. 따라서, 조건이 있을 때와 없을 때 쿼리를 각각 만들어줬던 기존의 방식과 달리 QueryDSL을 사용하면 하나의 메서드로 처리할 수 있게 됩니다.

 

또한, where절에 파라미터로 검색조건을 추가하면 AND 조건이 추가됩니다. 덕분에 검색 조건의 갯수도 신경 쓰지 않고 하나의 메서드로 처리할 수 있습니다. 추가로 isEqualToTheme()와 같이 조건을 메서드로 분리하면 재사용을 할 수도 있고, isContainedIntitleOrContent()와 같이 조립을 해서 사용할 수 있다는 장점이 있습니다.

 

그리고 문자열 검색의 경우는 실제로 ElasticSearch와 같은 검색 엔진을 사용한다고 하는데, 이번 프로젝트에서는 사용하기에 시간이 많이 부족해서 부득이하게 contains()로 구현하였습니다.


정리

  • QueryDSL을 사용하면 문자가 아닌 코드로 작성하기 때문에 컴파일 시점에 문법 오류를 발견할 수 있다.
  • 동적 쿼리를 작성하기 편하다.
  • 제약 조건을 조립할 수도 있고 재사용할 수도 있다.