
JPA, QueryDSL을 사용하는 프로젝트에서 BooleanExpression을 이용해서 where절을 자주 쓰곤 했는데,
최근에 소스코드 보안점검을 해보니 '널(Null) 포인터 역참조가 발생할 수 있다'고 지적 받은 코드가 있어서
BooleanExpression에서 널 포인터 역참조를 피하려면 어떻게 해야하는지,
BooleanExpression과 BooleanBuilder는 어떤 차이가 있고 어떤 상황에서 써야하는지 알아보려고 한다.
BooleanExpression과 BooleanBuilder는 QueryDSL에서 where절의 조건을 표현하는 조건 표현식이다.
사실 query문에 where문을 직접 입력해도 된다!
query.where(qUser.age.goe(18));
하지만 이렇게 쓰지 않는 이유는 조건이 있을 수도 있고, 없을 수도 있다.
예를 들면 나이(age)는 입력했지만, 이름(name)은 입력 안할 수도 있고, 활성화 여부(active)는 전체일 수도 있다.
query.where(...) 안에 직접 쓰면 if 문으로 계속 쿼리를 만들어야 한다.
따라서 동적으로 where절을 만들어서 재사용성, 가독성, 유지보수성을 향상시키기 위해 조건 표현식을 사용한다.
BooleanBuilder where = new BooleanBuilder();
if (age != null) {
where.and(qUser.age.goe(age));
}
if (StringUtils.hasText(name)) {
where.and(qUser.name.contains(name));
}
if (!"all".equals(status)) {
where.and(qUser.status.eq(status));
}
query.where(where);
null 역참조가 발생할 수 있는 코드를 살펴보자.
BooleanExpression whereClause = Expressions.TRUE;
/**
whereClause에 붙은 수많은 조건들...
**/
/** srchClause만 따로 작성하는 이유는 검색 관련 where절만 따로 관리하려고 **/
BooleanExpression srchClause= null;
if (StringUtils.hasText(srchData)) {
srchClause= qDatas.title.eq(srchData);
}
if (srchClause!= null) {
whereClause = whereClause.and(srchClause);
}
문제점 1. Expressions.TRUE
일반적으로 BooleanExpression에서 Expressions.TRUE를 쓰면 항상 true인 조건으로 시작하기 때문에,
where절에 초기값이 null이 아니라는 점에서 안전한 시작점을 만들어준다.
하지만 코드와 같이 whereClause.and(...) 메소드에 srchClause와 같이 또 다른 BooleanExpression이 들어간다면?
srchClause에서 null 참조 에러가 발생하게 된다.
실제 BooleanExpressions.and()의 내부에서는 다음과 같이 동작한다.
public BooleanExpression and(Predicate right) {
return Expressions.booleanOperation(Ops.AND, this, right);
}
right가 null이면 내부에서 null 참조 에러가 발생하기 때문에, null 참조 에러가 발생하게 되는 것이다.
문제점 2. BooleanExpressions은 불변이다.
BooleanExpression은 불변이라 and()가 호출될 때마다 새로운 객체를 만든다.
즉, and()를 호출할 때마다 기존 식이 변경되는 것이 아니라 새로운 객체를 만드는 것이다.
BooleanExpression where = qUser.active.isTrue();
where = where.and(qUser.age.goe(18));
이 코드에서도 qUser.active.isTrue() 에서 BooleanExpression A가 생성되고,
A.and(qUser.age.goe(18)) 에서 BooleanExpresion B가 생성된다.
결과적으로 where은 BooleanExpression B를 가리키게 되고, A는 사라지지 않는다.
여기서 든 궁금증이 BooleanExpression으로 수십 개의 조건을 달면 메모리 문제가 발생할 수도 있을까?
사실 수십 개 조건 정도로는 메모리 문제가 없지만,
반복 루프로 수천~수만 개가 생성될 경우 GC에 부하를 일으킬 수 있고,
static 저장과 같이 장시간으로 누적되면 메모리 누수가 발생할 수 있다.
null 역참조가 발생할 수 있는 코드를 BooleanBuilder를 사용하여 수정할 수 있다.
BooleanBuilder whereClause = new BooleanBuilder();
if (StringUtils.hasText(srchData)) {
whereClause.and(qDatas.title.eq(srchData));
}
whereClause를 BooleanBuilder 생성자를 이용하여 만들어주고,
BooleanExpression을 사용하는 것처럼 .and()를 호출하면서 조건을 붙여주면 된다.
BooleanBuilder는 가변 객체라서 null 체크가 필요 없다.
즉, and()를 호출하더라도 객체 내부의 상태가 변화하기 때문에 null 체크를 신경쓰지 않아도 된다.
null 체크를 하지 않아도 되기 때문에 조건이 많아도 코드를 간결하게 작성할 수 있다.
그럼 null 체크를 하지 않아도 되는 BooleanBuilder를 쓰는게 항상 좋을까?
그건 아니다.
BooleanExpression은 조건 수가 적고 간단할 때,
특정 BooleanExpression을 다른 곳에서 재사용하고 싶을 때 유용하다.
BooleanExpression activeUsers = qUser.status.eq("ACTIVE");
BooleanExpression adminUsers = qUser.role.eq("ADMIN");
query.where(activeUsers.and(adminUsers)); // 바로 조합 가능
이 같은 상황의 경우 고정 조건을 만들어서 and 하기 때문에 null 발생 우려가 없다.
BooleanExpression을 사용하기 애매한 상황에서 BooleanBuilder를 사용한다.
조건 수가 많거나 동적으로 달라질 때,
검색 화면, 필터링 조건처럼 여러 조건을 순차적으로 누적해야 할 때 유용하다.
BooleanBuilder builder = new BooleanBuilder();
if (hasText(title)) builder.and(qDatas.title.eq(title));
if (hasText(status)) builder.and(qDatas.status.eq(status));
if (hasText(owner)) builder.and(qDatas.owner.eq(owner));
query.where(builder);

QueryDSL 공식 문서에서는 복합 조건식을 작성할 때 BooleanBuilder 클래스를 사용하라고 권장하지만,
개발 상황에 따라 달라질 수 있지 않을까?!
때에 따라 적절한 표현식을 사용하고, 어떤 특징이 있는지 알고 사용하는 것도 중요하다!
'Development > Java' 카테고리의 다른 글
| [QueryDSL] Expressions.dateTemplate로 SQL 구문 만들기 (1) | 2025.09.22 |
|---|---|
| [Apache POI] Iterator로 읽은 빈 셀 CellType.BLANK로 예외처리하기 (0) | 2025.09.16 |
| [Apache POI]Java에서 이미지를 포함한 Excel 파일 생성하기 (5) | 2025.08.08 |
| [SpringBoot] select 쿼리문에서 update가 일어났던 이유 (1) | 2025.06.18 |