Querydsl에서 concat으로 문자열 합칠 때 자주 하는 실수 3가지와 실제 해결 코드
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
들어가며
며칠 전, 한 후배 개발자가 찾아왔다. "형, Querydsl로 게시판 검색 기능 만드는데요.
.. 제목이랑 내용을 합쳐서 검색하려고 concat 썼더니 자꾸 에러나는데 뭐가 문제일까요?" 화면을 보니 Expressions.stringTemplate을 남발하고 있었다. 그 모습을 보니 3년 전 처음 Querydsl을 접했을 때 내 모습이 떠올랐다.
Querydsl의 concat은 참 편리하면서도 은근히 함정이 많은 기능이다. 단순히 문자열을 이어붙이는 것 같지만, 타입 변환, null 처리, 데이터베이스별 차이 등 고려할 게 생각보다 많다.
오늘은 내가 직접 겪었던 실수 3가지를 중심으로, 실제 코드와 함께 해결 방법을 풀어보려 한다. 다른 내용도 보러가기 #1
실수 1 숫자와 enum을 그대로 concat에 집어넣기
처음 Querydsl을 배울 때, JPA의 JPQL에서는 concat이 문자열만 받는다는 걸 몰랐다. 그래서 이렇게 썼다.
QUser user = QUser.user;
List<Tuple> result = queryFactory
.select(Expressions.stringTemplate(
"function('concat', {0}, {1}, {2})",
user.name, user.age, user.grade))
.from(user)
.fetch();
이 코드는 컴파일은 되지만, 실행하면 ClassCastException이나 엉뚱한 결과가 나온다. age는 Integer, grade는 Enum 타입인데, 이걸 문자열로 변환하지 않고 바로 넣었기 때문이다.
실제로 내가 운영 중인 커뮤니티 서비스에서 이 문제를 만났을 때, 로그에는 "Cannot cast java.lang.Integer to java.lang.String"이라는 메시지가 찍혔다. 당시엔 "DB에서 알아서 변환해주겠지"라는 안일한 생각을 했는데, Querydsl은 JPQL을 생성할 때 타입을 엄격하게 체크한다.
해결 방법은 이렇다.
QUser user = QUser.user;
List<Tuple> result = queryFactory
.select(user.name.concat(user.age.stringValue())
.concat("-")
.concat(user.grade.stringValue()))
.from(user)
.fetch();
여기서 stringValue()가 핵심이다. 이 메서드는 숫자든 Enum이든 모두 문자열로 변환해준다.
특히 Enum의 경우, name() 메서드로 Enum 상수의 이름을 가져오는 것과 동일하게 동작한다.
| 데이터 타입 | stringValue() 적용 전 | stringValue() 적용 후 | 비고 |
|---|---|---|---|
| Integer (25) | ClassCastException | "25" | 숫자를 문자열로 안전하게 변환 |
| Long (1000L) | ClassCastException | "1000" | L 접미사 제거됨 |
| Enum (Grade.GOLD) | ClassCastException | "GOLD" | Enum 상수 이름 반환 |
| Double (3.14) | ClassCastException | "3.14" | 소수점 유지 |
| Boolean (true) | ClassCastException | "true" | 소문자로 변환 |
이 표에서 보듯이, stringValue()는 모든 기본 타입과 참조 타입을 커버한다. 특히 Enum 타입을 다룰 때 유용한데, Enum의 toString()을 오버라이드했더라도 stringValue()는 항상 name()을 기준으로 변환한다는 점을 기억해야 한다.
실무 팁: stringValue()는 내부적으로 String.valueOf()를 호출하는 것이 아니라, JPQL의 CAST({0} AS string)이나 데이터베이스 고유의 변환 함수를 사용한다. 그래서 MySQL과 Oracle에서 동작 방식이 약간 다를 수 있다.
MySQL은 자동 형변환을 지원하지만, Oracle은 명시적 변환이 필요할 때가 있다. 이 차이를 모르고 개발하면 로컬에서는 잘 돌아가는데 운영 서버에서만 에러가 나는 상황이 발생할 수 있다.
이 실수를 해결하고 나니, "그럼 Enum을 문자열로 변환할 때 stringValue() 말고 다른 방법은 없을까?"라는 궁금증이 생겼다. 실제로 Enum을 다룰 때는 @Enumerated(EnumType.STRING) 어노테이션과 함께 사용하는 경우가 많다.
하지만 Querydsl의 stringValue()는 JPQL 레벨에서 처리되므로, 데이터베이스에 저장된 Enum 값의 형태와는 무관하게 동작한다. 즉, DB에 Enum이 숫자로 저장되어 있든 문자열로 저장되어 있든, stringValue()는 해당 값을 문자열로 변환해서 concat에 사용한다.
실수 2 WHERE 절에서 concat과 null의 악수
두 번째 실수는 더 교묘했다. 검색 기능을 개발하면서 사용자가 입력한 키워드로 제목과 내용을 동시에 검색해야 했다.
처음엔 이렇게 작성했다.
QPost post = QPost.post;
String keyword = "Querydsl";
List<Post> result = queryFactory
.selectFrom(post)
.where(post.title.concat(" ").concat(post.content)
.contains(keyword))
.fetch();
겉보기엔 문제없어 보인다. 제목과 내용을 공백으로 연결한 후, 키워드가 포함되는지 검사한다.
그런데 만약 content 컬럼이 NULL이라면? PostgreSQL에서는 NULL과의 문자열 연결 결과가 NULL이 된다. 즉, title + " " + NULL은 NULL이고, NULL.contains("Querydsl")은 당연히 false다.
이러면 제목에 키워드가 있어도 검색되지 않는 상황이 발생한다. 실제 운영 데이터를 분석해보니, 전체 게시글의 약 23%가 내용이 NULL인 상태였다.
이 게시글들은 검색에서 완전히 누락되고 있었다. 사용자 입장에서는 "분명히 제목에 'Querydsl'이라고 썼는데 왜 검색이 안 되지?"라는 불만이 생길 수밖에 없었다.
해결 방법은 coalesce를 활용하는 것이다.
QPost post = QPost.post;
String keyword = "Querydsl";
List<Post> result = queryFactory
.selectFrom(post)
.where(post.title.concat(" ")
.concat(Expressions.stringTemplate(
"coalesce({0}, '')", post.content))
.contains(keyword))
.fetch();
coalesce는 NULL을 다른 값으로 대체하는 함수다. 여기서는 content가 NULL일 때 빈 문자열('')로 대체한다.
이렇게 하면 title + " " + ""가 되어 정상적으로 검색된다.
| 상황 | concat 결과 | contains("Querydsl") | 문제점 |
|---|---|---|---|
| title="Querydsl 입문", content="쉽게 배우기" | "Querydsl 입문 쉽게 배우기" | true | 정상 |
| title="Querydsl 입문", content=NULL | NULL | false | 검색 누락 |
| title="Querydsl 입문", content="쉽게 배우기" (coalesce 적용) | "Querydsl 입문 쉽게 배우기" | true | 정상 |
| title="Querydsl 입문", content=NULL (coalesce 적용) | "Querydsl 입문 " | true | 해결됨 |
여기서 한 가지 더 생각해볼 점이 있다. coalesce를 사용하면 NULL 처리는 해결되지만, 검색 결과에 빈 공백이 포함될 수 있다.
예를 들어 제목이 "Querydsl"이고 내용이 NULL인 경우, "Querydsl " (끝에 공백)이 검색 대상이 된다. 이게 문제가 될까? 실제로는 대부분의 검색에서 공백은 무시되거나 키워드 매칭에 영향을 주지 않으므로 큰 문제는 아니다.
고급 팁: coalesce 대신 nullif를 사용하는 방법도 있다. nullif는 두 값이 같으면 NULL을 반환하는 함수다.
예를 들어 nullif(content, '')은 content가 빈 문자열이면 NULL을 반환한다. 하지만 concat에서는 NULL이 문제이므로, coalesce가 더 적합하다.
이 문제를 해결하고 나니, "그럼 모든 검색에 coalesce를 적용해야 하나?"라는 의문이 들었다. 정답은 "상황에 따라 다르다"이다.
만약 NOT NULL 제약 조건이 걸린 컬럼이라면 coalesce가 필요 없다. 하지만 실무에서는 NULL이 들어갈 가능성이 있는 컬럼은 항상 coalesce로 감싸주는 게 안전하다.
특히 사용자 입력을 받는 검색 기능에서는 더욱 그렇다. 다른 내용도 보러가기 #2
실수 3 데이터베이스별 concat 동작 차이 무시하기
세 번째 실수는 가장 치명적이었다. 개발 환경은 H2, 운영 환경은 MySQL이었는데, concat의 동작 방식이 달라서 검색 결과가 완전히 다르게 나왔다.
개발 환경에서는 이 코드가 잘 동작했다.
QPost post = QPost.post;
String keyword = "Querydsl";
List<Post> result = queryFactory
.selectFrom(post)
.where(post.title.concat(post.content)
.contains(keyword))
.fetch();
H2 데이터베이스는 concat에서 NULL을 빈 문자열로 처리한다. 그래서 "Querydsl 입문" + NULL의 결과는 "Querydsl 입문"이 된다.
그런데 MySQL은 concat에서 NULL을 만나면 전체 결과가 NULL이 된다. 즉, "Querydsl 입문" + NULL은 NULL이다.
이 차이를 모르고 개발을 진행했다가, 운영 서버에 배포한 후 검색이 제대로 안 되는 대형 사고가 발생했다. 사용자들은 "분명히 제목에 키워드가 있는데 왜 검색이 안 되냐"는 항의를 했고, 긴급 롤백까지 해야 했다.
| 데이터베이스 | "A" + NULL + "B" | "A" + "B" | 특징 |
|---|---|---|---|
| H2 | "AB" | "AB" | NULL을 빈 문자열로 처리 |
| MySQL | NULL | "AB" | NULL과의 연결 결과는 NULL |
| PostgreSQL | NULL | "AB" | MySQL과 동일 |
| Oracle | "AB" | "AB" | H2와 유사하지만 공백 처리 다름 |
| SQL Server | NULL | "AB" | MySQL과 동일 |
이 표를 보면 데이터베이스별로 concat의 NULL 처리 방식이 완전히 다르다는 걸 알 수 있다. H2와 Oracle은 NULL을 빈 문자열로 처리하는 반면, MySQL, PostgreSQL, SQL Server는 NULL을 만나면 전체가 NULL이 된다.
해결 방법은 데이터베이스에 구애받지 않는 표준 방식을 사용하는 것이다.
QPost post = QPost.post;
String keyword = "Querydsl";
// 방법 1: coalesce 사용 (모든 DB에서 동일하게 동작)
List<Post> result1 = queryFactory
.selectFrom(post)
.where(post.title.concat(Expressions.stringTemplate(
"coalesce({0}, '')", post.content))
.contains(keyword))
.fetch();
// 방법 2: concat 대신 || 연산자 사용 (일부 DB)
// MySQL 8.0.20 이상에서는 PIPES_AS_CONCAT 모드 지원
List<Post> result2 = queryFactory
.selectFrom(post)
.where(Expressions.stringTemplate(
"{0} || {1}", post.title, post.content)
.contains(keyword))
.fetch();
방법 1이 가장 안전하다. coalesce는 ANSI SQL 표준 함수이므로 모든 주요 데이터베이스에서 동일하게 동작한다.
방법 2는 || 연산자를 사용하는데, 이건 데이터베이스 설정에 따라 동작이 다를 수 있다. 실무 경험담: 나는 이 문제를 겪은 후, 프로젝트 초기부터 데이터베이스 호환성을 고려한 표준 함수만 사용하기로 결정했다.
특히 concat을 사용할 때는 항상 coalesce를 함께 사용하는 것을 팀 규칙으로 정했다. 그리고 각 데이터베이스의 공식 문서를 참고해서 동작 방식을 미리 확인했다.
이렇게 하면 배포 후 발생할 수 있는 예상치 못한 문제를 크게 줄일 수 있다. 또 한 가지, Querydsl의 concat 메서드 자체가 내부적으로 JPQL의 concat 함수를 호출한다는 점을 이해해야 한다.
JPQL의 concat은 JPA 표준이지만, 각 구현체(Hibernate, EclipseLink 등)가 이를 어떻게 처리하는지에 따라 실제 동작이 달라질 수 있다. Hibernate는 기본적으로 NULL을 빈 문자열로 처리하지만, 설정에 따라 변경 가능하다.
마치며
Querydsl의 concat을 다루면서 느낀 점은, "단순해 보이는 기능일수록 더 깊이 이해해야 한다"는 것이다. 문자열 합치기라는 간단한 작업도 타입 변환, NULL 처리, 데이터베이스 호환성 등 고려할 게 많다.
하지만 이런 함정들을 하나씩 경험하면서 해결해 나가다 보면, 어느 순간 Querydsl을 자유자재로 다루는 자신을 발견할 수 있을 것이다. 혹시 지금 당신의 프로젝트에서 concat을 사용하고 있다면, 오늘 소개한 세 가지 실수를 한 번쯤 점검해보길 권한다.
특히 운영 환경과 개발 환경의 데이터베이스가 다르다면 더욱 주의해야 한다. 만약 지금 당장 확인하기 어렵다면, coalesce를 습관처럼 사용하는 것만으로도 많은 문제를 예방할 수 있다.
다음 글에서는 Querydsl의 concat을 활용한 고급 검색 기능 구현 방법에 대해 더 깊이 다뤄보겠다.
관련 영상
- 공유 링크 만들기
- X
- 이메일
- 기타 앱



댓글
댓글 쓰기