검색 조건이 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이 포함됩니다.

XML
<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을 자동으로 제거합니다.

XML
<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 + statusWHERE name LIKE '%홍%' AND status = 'ACTIVE'
아무것도 없음SELECT * FROM users (WHERE 자체 생략)

<where> 태그가 첫 번째 AND/OR을 자동으로 제거 합니다. WHERE 1=1 트릭은 더 이상 필요 없습니다.

<choose> — if-else 분기

여러 조건 중 하나만 선택해야 할 때 사용합니다. Java의 switch-case와 비슷합니다.

XML
<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에 활용합니다.

XML
<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가 괄호와 쉼표를 자동으로 처리합니다.

실전 예제 — 관리자 검색 쿼리

여러 태그를 조합한 실무 수준의 검색 쿼리를 보겠습니다.

XML
<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 표현식을 사용합니다. 문자열 비교 시 작은따옴표로 감싸야 합니다.

XML
<!-- 올바른 방법 -->
<if test="status == 'ACTIVE'">...</if>
<if test='status == "ACTIVE"'>...</if>

<!-- 잘못된 방법: 컴파일 에러 -->
<if test="status == "ACTIVE"">...</if>

큰따옴표 안에 큰따옴표를 쓰면 XML 파싱 에러가 납니다. 작은따옴표로 감싸거나, 바깥 속성을 작은따옴표로 변경해야 합니다.

foreach에 빈 리스트를 넘기면 SQL 에러

<foreach>에 빈 리스트가 들어가면 IN ()이 되어 SQL 문법 에러가 발생합니다. 반드시 <if>로 빈 리스트 체크를 감싸야 합니다.

XML
<!-- 안전한 방법 -->
<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>와 같은 원리입니다.

XML
<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 동적 필드마지막 쉼표 자동 제거
댓글 로딩 중...