체크박스 여러 개로 거르는 필터를 쿼리로 옮기기
체크박스 다중 선택 필터를 IN 절 동적 SQL로 옮기고, 선택 0개·전체 선택의 의미를 백엔드가 정의하는 법.
그 주엔 한 화면에서 종류를 체크박스로 여러 개 골라 목록을 거르는 필터를 다뤘다. UI는 단순하다. 체크박스 몇 개를 켜면 그에 해당하는 행만 남는다. 그런데 이걸 쿼리로 옮기는 순간, UI에서는 자명했던 두 경계가 백엔드의 설계 결정으로 바뀐다. 하나도 안 골랐을 때와 전부 골랐을 때 무엇을 보여줄 것인가. 이 질문에 답을 정하지 않으면 필터는 동작은 하되 사용자가 기대한 결과를 주지 못한다.
다중 선택은 IN 절이다
다중 선택 필터의 본질은 “이 컬럼 값이 선택한 집합 중 하나에 속하는가”다. 이건 정확히 SQL의 IN 절이다.
1
2
SELECT * FROM orders
WHERE status IN ('PAID', 'SHIPPED', 'DONE');
IN (a, b, c)은 옵티마이저에게 status = a OR status = b OR status = c와 동치로 풀린다. 인덱스가 status에 있으면 각 값에 대한 인덱스 탐색을 모아 결과를 합친다(엔진에 따라 index range scan 혹은 여러 번의 seek). 선택 개수가 늘면 후보 행이 늘고, 어느 임계를 넘으면 옵티마이저가 인덱스를 버리고 풀 스캔으로 전환한다 — 즉 선택 개수에 따라 실행계획이 변동한다. 사용자가 7개 중 6개를 켰다면, 차라리 “선택 안 한 1개를 제외”하는 게 빠를 수도 있다는 뜻이다.
두 경계의 의미를 정한다
코드로 옮기기 전에 정책부터 박는다.
- 0개 선택 → 보통 “필터 미적용 = 전체”로 본다. 빈 결과로 두면 화면이 텅 비어 사용자가 당황한다. 단, 의도가 “아무것도 안 보겠다”라면 빈 결과가 맞다. 도메인이 정한다.
- 전체 선택 →
IN에 모든 값을 넣는 대신 조건절 자체를 생략한다. 결과는 같지만 옵티마이저가 깔끔한 계획을 세운다.
MyBatis <foreach>로 IN을 만들 때 가장 흔한 사고는 빈 리스트다.
1
2
3
4
5
6
7
8
9
10
11
<select id="search" resultType="Order">
SELECT * FROM orders
<where>
<if test="statuses != null and statuses.size() > 0">
AND status IN
<foreach collection="statuses" item="s" open="(" separator="," close=")">
#{s}
</foreach>
</if>
</where>
</select>
statuses가 빈 리스트인데 size > 0 가드 없이 <foreach>로 들어가면 status IN ()이라는 문법 오류 SQL이 만들어진다. DB가 거부한다. 그래서 null 체크와 size() > 0 체크를 함께 둔 뒤, 조건이 없으면 절을 통째로 빼 “전체”로 떨어지게 한다. 0개=전체 정책이 자연스럽게 이 가드에 녹는다.
운영 함정
IN 절 길이 폭주. 사용자가 수백 개를 선택할 수 있는 화면이라면 IN 리스트가 수백 개 바인딩 변수로 불어난다. 일부 드라이버·DB는 IN 항목 수에 상한이 있고, 길이가 길어질수록 파싱·계획 수립 비용이 커진다. 항목이 많으면 임시 테이블에 담아 조인하는 편이 안정적이다.
바인딩 변수 폭증으로 인한 계획 캐시 오염. IN 개수가 매번 다르면 (IN (?), IN (?,?), IN (?,?,?) …) 옵티마이저 입장에선 서로 다른 SQL 문이 된다. 계획 캐시가 변형마다 늘어 메모리를 먹는다. 항목 수를 일정 구간으로 패딩하거나 배열 바인딩을 쓰는 기법으로 변형 수를 줄인다.
핵심 요약
- 다중 선택 필터 =
IN절. 선택 개수가 실행계획을 흔든다. - 0개와 전체의 의미는 코드가 아니라 도메인이 정한다. 보통 0개=전체, 전체=조건 생략.
<foreach>앞엔 반드시null && size() > 0가드.IN ()는 문법 오류다.
면접 한 줄 Q&A Q. 다중 선택 필터에서 아무것도 안 골랐을 때 백엔드는 어떻게 처리하나? A. “전체”로 볼지 “빈 결과”로 볼지를 도메인이 먼저 정한다. 전체라면 조건절을 생략하고, 빈 결과라면 빈 리스트를 거부 조건으로 변환한다. 무가드로
IN ()을 만들면 SQL 문법 오류로 깨진다.