1. 알고자 하는 것
회사에서 동기가 MyBatis로 작성한 쿼리 중, in절에 대해 empty list가 넘어올 때 Syntax 오류가 발생하여 empty list에 대한 방어 코드 작성 후 다시 배포했었던 상황이 있었다.
QueryDsl로 작성한 쿼리에 대해서는 in절에 대해서 별다른 문제가 없었던 것으로 기억해서, QueryDsl의 in절의 내부 동작과 함께 in절에서의 여러 케이스에 대한 결과를 비교해보고자 한다.
- QueryDsl에서 in query를 사용할 때 List에 empty list가 올 때의 결과
- QueryDsl에서 in query를 사용할 때 List에 null이 올 때의 결과
- QueryDsl에서 in query의 내부 동작
2. 알게된 것
- QueryDsl에서 in절을 테스트 하기 위해 사용한 쿼리는 다음과 같다. (전체 소스코드는 하단 링크 참조)
public List<String> selectMemberNameList(List<Long> memberIdList) {
return queryFactory
.select(member.name)
.from(member)
.where(member.id.in(memberIdList)).fetch();
}
QueryDsl에서 in query를 사용할 때 List에 empty list가 올 때의 결과
- in절에 사용될 List를 비어있는 list로 주었다.
public List<String> findMemberNameListByEmptyList() {
return memberRepository.selectMemberNameList(Collections.emptyList());
}
- 다음과 같이 Exception 없이 정상적으로 비어있는 결과를 반환한다.
// 20230917151622
// http://localhost:8080/members/empty
[
]
- 실행된 쿼리는 다음과 같이 where 1=2로 변경되어 수행되었다. 이로 인해 문제 없이 비어있는 결과를 반환할 수 있었다.
Hibernate: select m1_0.name from member m1_0 where 1=2
QueryDsl에서 in query를 사용할 때 List에 null이 올 때의 결과
- in절에 사용될 List를 null로 주었다.
public List<String> findMemberNameListByNull() {
return memberRepository.selectMemberNameList(null);
}
- empty list와는 다르게, NullPointerException이 발생하였다.
java.lang.NullPointerException: Cannot invoke "java.util.Collection.size()" because "right" is null
at com.querydsl.core.types.dsl.SimpleExpression.in(SimpleExpression.java:192) ~[querydsl-core-5.0.0.jar:na]
- 객체가 null이어서 Collection Type의 size()를 수행할 수 없어 NullPointerException이 발생하였다.
QueryDsl에서 in query의 내부 동작
- in절에 대한 QueryDsl의 내부 동작은 다음과 같이 정의되어 있다.
public BooleanExpression in(Collection<? extends T> right) {
if (right.size() == 1) {
return eq(right.iterator().next());
} else {
return Expressions.booleanOperation(Ops.IN, mixin, ConstantImpl.create(right));
}
}
- in절에 들어오는 list의 사이즈가 1 or else에 따라 동작이 달라짐을 확인할 수 있다.
- empty list의 경우에는 size() 메서드가 동작하기 때문에 오류가 발생하지 않았다.
- null의 경우에는 객체가 null이므로 right.size()를 수행할 때 NPE가 발생했다.
- empty list의 경우, where 1=2로 치환되었는데, 이는 hibernate에서 메서드를 기반으로 SQL을 구성할 때 다음과 같은 동작을 수행하기 때문이다.
public String toFragmentString() {
if ( values.size() == 0 ) {
return "1=2";
...
}
- list의 size가 0일 경우 where 절 뒤에 오는 문자열을 "1=2"로 치환해 모든 조건을 충족하지 못하도록 처리한다.
- 이로 인해 QueryDsl의 in절에서 empty list에 대해서는 정상적으로 결과를 반환할 수 있었다.
3. 정리
- QueryDsl에서 in절에 empty list가 들어올 때에는 where 1=2로 치환하여 정상적으로 비어있는 결과가 반환된다.
- QueryDsl에서 in절에 null이 들어올 때에는 QueryDsl의 in 메서드 내부 동작에서 right.size()을 수행할 때 NPE가 발생한다.
- QueryDsl을 사용하고, in절에 null이 들어오지 않도록 @RequestBody(required=true)와 같이 input에 대한 validation을 수행하도록 하자.
- 만약 null이 들어올 수 있도록 해야만 한다면, 다음과 같이 비즈니스 로직에서 적절한 validation을 수행하도록 하자.
public List<String> findMemberNameListByNullValidation(List<Long> memberIdList) {
if (memberIdList == null) {
// Custom Exception Throw 등의 Validation 처리
// 코드 흐름에 맞춘 validation 처리!
return Collections.emptyList();
}
return memberRepository.selectMemberNameList(memberIdList);
}
전체 소스코드 : https://github.com/HunSeongPark/blog-study/tree/main/querydsl-in-empty-list
'QueryDsl' 카테고리의 다른 글
QueryDsl에서 결과를 가져오는 메서드, fetchFirst()와 fetchOne()의 차이 (0) | 2023.09.20 |
---|---|
No-Offset 적용을 통한 무한 스크롤 방식의 페이징 쿼리 성능 개선 (0) | 2023.05.17 |
exists 메서드 성능 개선 - count vs exists (0) | 2023.05.01 |
order by 조건 설정 - 현재 기준 n일 미만 내 작성된 게시글 상위에 정렬 (0) | 2023.01.29 |