Post

QueryDSL에서 where 다중 파라미터를 이용해 null을 처리하는 방법

GET-P 프로젝트를 진행하던 중, 프론트엔드에서 요청된 쿼리스트링에 따라 필터링을 하는 기능을 구현해야 됐었다. 해당 기능을 구현하는데 있어서 겪은 경험에 대해 정리해보았다.

다음 코드는 GET-P 플랫폼에 등록된 프로젝트 중 인자로 넘어온 프로젝트 상태, 지원 마감일에 따라 데이터베이스로 쿼리를 보내는 findFiltteredProjects 메소드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public List<Project> findFilteredProjects(ProjectStatus projectStatus,
            Long applicationDeadlineOffset) {
    BooleanBuilder booleanBuilder = new BooleanBuilder();
    if (projectStatus != null) {
        booleanBuilder.and(project.status.eq(projectStatus));
    }
    if (applicationDeadlineOffset != null) {
        LocalDate applicationDeadline = LocalDate.now().plusDays(applicationDeadlineOffset);
        booleanBuilder.and(project.applicationDeadline.loe(applicationDeadline));
    }
    JPAQuery<Project> query = queryFactory.selectFrom(project).where(booleanBuilder);
    return query.fetch();
}

처음에는 위와 같이 BooleanBuilder를 이용해 각 변수가 가지고 있는 값에 따라 동적으로 쿼리문을 만들어 내었다. 그러나 다만, 각 변수가 가지고 있는 값을 확인을 해야 하기 때문에 if 문이 반복될 수 밖에 없었다. 이는 코드의 가독성을 해치고, 메소드가 한 가지 일을 하는 것이 아닌 여러 가지 일을 하게 된다.

위 문제는 QueryDSL의 where 문의 다중 파라미터를 이용해 해결할 수 있다. where 문에 null이 들어가게 되면 해당 변수에 대한 쿼리는 무시된다는 것을 이용해, null 값을 확인하는 로직을 메소드로 분리하고 마지막 selectFromwhere 문에 메소드들의 결과값을 파라미터로 넘기는 것이다.

먼저 projectStatusnull 인지 아닌지 판단하는 로직을 보자.

1
2
3
if (projectStatus != null) {
    booleanBuilder.and(project.status.eq(projectStatus));
}

위 코드 중 project.status.eq(projectStatus)는 데이터베이스에 저장된 Project 엔티티들 중 projectStatus와 같은 값을 가지고 있는 Project 만을 가져올 수 있도록 하는 status = :projectStatus라는 불린 표현식을 만들어 낸다. 정리하자면, 위 코드는 status = :projectStatus라는 BooleanExpression을 만들어내어 BooleanBuilder에 담는 일을 한다.

이는 다음과 같이 변경할 수 있다.

if 문을 메소드로 분리해 projectStatusnull인 경우 null을 반환하고, 그렇지 않은 경우 BooleanExpression을 반환하도록 하는 메소드인 projectStatusEq 메소드를 선언한다.

1
2
3
private BooleanExpression projectStatusEq(ProjectStatus projectStatus) {
    return projectStatus != null ? project.status.eq(projectStatus) : null;
}

단순한 null 검사 만을 하기 때문에 삼항 연산자를 사용했지만, 추가적인 로직이 필요한 경우 삼항 연산자는 피해야 한다고 생각한다.

두 번째는 지원 마감일에 대한 쿼리인데, 현재 날짜를 기준으로 지원 마감일이 applicationDeadlineOffset과 같거나 적게 남은 Project를 검색하는 쿼리다.

1
2
3
4
if (applicationDeadlineOffset != null) {
    LocalDate applicationDeadline = LocalDate.now().plusDays(applicationDeadlineOffset);
    booleanBuilder.and(project.applicationDeadline.loe(applicationDeadline));
}

이 또한 비슷한 방법으로 applicationDeadlineLoe 라는 메소드를 선언한다.

1
2
3
4
5
6
7
private BooleanExpression applicationDeadlineLoe(Long applicationDeadlineOffset) {
    if (applicationDeadlineOffset == null) {
        return null;
    }
    LocalDate applicationDeadline = LocalDate.now().plusDays(applicationDeadlineOffset);
    return project.applicationDeadline.loe(applicationDeadline);
}

그리고 이들을 queryFactory.selectFromwhere 문의 파라미터로 넘긴다.

1
2
JPAQuery<Project> query = queryFactory.selectFrom(project).where(
            projectStatusEq(projectStatus), applicationDeadlineLoe(applicationDeadlineOffset));

이를 최종적으로 정리하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private BooleanExpression projectStatusEq(ProjectStatus projectStatus) {
    return projectStatus != null ? project.status.eq(projectStatus) : null;
}

private BooleanExpression applicationDeadlineLoe(Long applicationDeadlineOffset) {
    if (applicationDeadlineOffset == null) {
        return null;
    }
    LocalDate applicationDeadline = LocalDate.now().plusDays(applicationDeadlineOffset);
    return project.applicationDeadline.loe(applicationDeadline);
}

public List<Project> findFilteredProjects(ProjectStatus projectStatus,
        Long applicationDeadlineOffset) {
    JPAQuery<Project> query = queryFactory.selectFrom(project).where(
            projectStatusEq(projectStatus), applicationDeadlineLoe(applicationDeadlineOffset));
    return query.fetch();
}

처음 코드에 비해 가독성이 훨씬 좋아졌고, findFiltteredProjects 메소드가 한 가지 일만 하게 되었다. 또한 다른 메소드에서 해당 변수들에 대한 쿼리가 있는 경우 메소드를 재활용할 수 있게 되었다.

This post is licensed under CC BY 4.0 by the author.