동적 SQL — if, choose, foreach로 조건부 쿼리 만들기
검색 조건이 5개인데, 사용자가 어떤 조건을 입력할지 모를 때 SQL을 어떻게 작성하시나요?
if문을 자바 코드에서 분기해서 SQL 문자열을 이어 붙이면 유지보수가 금방 지옥이 됩니다. MyBatis는 XML 안에서 조건부 SQL을 선언적으로 작성할 수 있는 동적 SQL 기능을 제공합니다.
개념 정의
동적 SQL 은 실행 시점의 파라미터 값에 따라 SQL의 일부를 포함하거나 제외하는 기능입니다.
MyBatis는 <if>, <choose>, <where>, <foreach> 등의 XML 태그로 이를 구현합니다.
왜 필요한가
- **조건부 검색 **: 관리자 페이지에서 이름, 상태, 날짜 등 여러 필터가 선택적으로 입력됩니다. 입력되지 않은 조건은 쿼리에서 빠져야 합니다.
- **IN 절 처리 **: "선택된 ID 목록을 삭제해주세요" 같은 요구사항에서 리스트의 크기가 매번 달라집니다.
- ** 정렬 조건 변경 **: 사용자가 "이름순", "최신순" 등 정렬 기준을 선택하면 ORDER BY 절이 바뀌어야 합니다.
핵심 태그 설명
<if> — 조건이 참일 때만 포함
가장 기본적인 동적 SQL 태그입니다. test 속성의 OGNL 표현식이 참이면 내부 SQL이 포함됩니다.
<select id="findUsers" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</select>
위 쿼리에서 WHERE 1=1은 첫 번째 조건 앞에 AND를 붙이기 위한 트릭입니다. 하지만 이 방식보다 <where> 태그를 사용하는 것이 깔끔합니다.
<where> — WHERE 절을 자동으로 관리
<where> 태그는 내부에 조건이 하나라도 있으면 WHERE을 붙이고, 첫 번째 조건의 불필요한 AND/OR을 자동으로 제거합니다.
<select id="findUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
| 입력 조건 | 생성되는 SQL |
|---|---|
| name만 입력 | WHERE name LIKE '%홍%' (AND 자동 제거) |
| name + status | WHERE name LIKE '%홍%' AND status = 'ACTIVE' |
| 아무것도 없음 | SELECT * FROM users (WHERE 자체 생략) |
<where>태그가 첫 번째 AND/OR을 자동으로 제거 합니다.WHERE 1=1트릭은 더 이상 필요 없습니다.
<choose> — if-else 분기
여러 조건 중 하나만 선택해야 할 때 사용합니다. Java의 switch-case와 비슷합니다.
<select id="findByPriority" resultType="User">
SELECT * FROM users
<where>
<choose>
<when test="searchType == 'name'">
AND name = #{keyword}
</when>
<when test="searchType == 'email'">
AND email = #{keyword}
</when>
<otherwise>
AND status = 'ACTIVE'
</otherwise>
</choose>
</where>
</select>
<when> 조건을 위에서 아래로 평가하다가 처음 참인 것 하나만 실행합니다. 모두 거짓이면 <otherwise>가 실행됩니다.
<foreach> — 컬렉션 순회
리스트나 배열을 SQL의 IN 절이나 다건 INSERT에 활용합니다.
<select id="findByIds" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach collection="ids" item="id"
open="(" separator="," close=")">
#{id}
</foreach>
</select>
ids가 [1, 2, 3]이면 WHERE id IN (1, 2, 3)이 생성됩니다. open, separator, close가 괄호와 쉼표를 자동으로 처리합니다.
실전 예제 — 관리자 검색 쿼리
여러 태그를 조합한 실무 수준의 검색 쿼리를 보겠습니다.
<select id="searchOrders" resultType="Order">
SELECT o.id, o.user_id, o.total_amount, o.status
FROM orders o
<where>
<if test="userId != null">
AND o.user_id = #{userId}
</if>
<if test="startDate != null and endDate != null">
AND o.created_at BETWEEN #{startDate} AND #{endDate}
</if>
<if test="statuses != null and statuses.size() > 0">
AND o.status IN
<foreach collection="statuses" item="s"
open="(" separator="," close=")">
#{s}
</foreach>
</if>
</where>
ORDER BY o.created_at DESC
LIMIT #{offset}, #{limit}
</select>
이 쿼리에서 핵심은 모든 조건이 선택적 이라는 점입니다. 사용자가 아무 조건도 입력하지 않으면 전체 주문이 조회되고, 조건을 추가할수록 필터링됩니다.
주의할 점
OGNL 표현식의 함정 — 문자열 비교
<if> 태그의 test 속성은 OGNL 표현식을 사용합니다. 문자열 비교 시 작은따옴표로 감싸야 합니다.
<!-- 올바른 방법 -->
<if test="status == 'ACTIVE'">...</if>
<if test='status == "ACTIVE"'>...</if>
<!-- 잘못된 방법: 컴파일 에러 -->
<if test="status == "ACTIVE"">...</if>
큰따옴표 안에 큰따옴표를 쓰면 XML 파싱 에러가 납니다. 작은따옴표로 감싸거나, 바깥 속성을 작은따옴표로 변경해야 합니다.
foreach에 빈 리스트를 넘기면 SQL 에러
<foreach>에 빈 리스트가 들어가면 IN ()이 되어 SQL 문법 에러가 발생합니다. 반드시 <if>로 빈 리스트 체크를 감싸야 합니다.
<!-- 안전한 방법 -->
<if test="ids != null and ids.size() > 0">
AND id IN
<foreach collection="ids" item="id"
open="(" separator="," close=")">
#{id}
</foreach>
</if>
<set> 태그를 빠뜨리는 실수
UPDATE 문에서 동적으로 필드를 갱신할 때 <set> 태그를 사용하면 마지막 쉼표를 자동으로 제거합니다. <where>와 같은 원리입니다.
<update id="updateUser" parameterType="User">
UPDATE users
<set>
<if test="name != null">name = #{name},</if>
<if test="email != null">email = #{email},</if>
</set>
WHERE id = #{id}
</update>
정리
| 태그 | 용도 | 핵심 포인트 |
|---|---|---|
<if> | 조건부 SQL 포함 | OGNL 표현식, null 체크 필수 |
<where> | WHERE 절 자동 관리 | 첫 AND/OR 제거, 조건 없으면 WHERE 생략 |
<choose> | if-else 분기 | 첫 번째 참인 <when>만 실행 |
<foreach> | 컬렉션 순회 | IN 절, 다건 INSERT에 사용. 빈 리스트 주의 |
<set> | UPDATE 동적 필드 | 마지막 쉼표 자동 제거 |