MyBatis 쿼리 가이드

MyBatis XML 매퍼 작성 규칙, ANSI SQL 규칙, 페이징

마지막 수정: 2026-05

MyBatis 쿼리 가이드

기본 원칙

  • #{} 사용으로 PreparedStatement 파라미터 바인딩 (SQL Injection 방지)
  • ${} 는 동적 컬럼명/정렬 등 불가피한 경우에만, 화이트리스트 검증 필수
  • 동적 쿼리는 <if>, <where>, <choose> 태그 활용

SQL 파일 위치

/src/main/resources/mybatis/{업무명}/mapper/{업무명}Mapper.xml

예시:

src/main/resources/
└── mybatis/
    ├── user/
    │   └── mapper/
    │       └── UserMapper.xml
    └── board/
        └── mapper/
            └── BoardMapper.xml

ANSI 쿼리 규칙

규칙설명
키워드 대문자SELECT, FROM, WHERE, AND, OR
테이블명 대문자T_USER, T_BOARD
컬럼명 대문자USER_NM, REG_DT
SELECT * 금지필요한 컬럼만 명시
컬럼 콤마 위치줄바꿈 후 앞에 붙임
테이블 AliasA → B → C
JOIN 규칙INNER JOIN / LEFT JOIN 기본, RIGHT JOIN 최소화
시간SYSDATE 금지 → SYSTIMESTAMP 사용

컬럼 콤마 위치 규칙

-- 올바른 예 (콤마를 줄바꿈 후 앞에 붙임)
SELECT A.USER_SN
     , A.USER_ID
     , A.USER_NM
     , A.USER_EMAIL
  FROM T_USER A

-- 잘못된 예
SELECT A.USER_SN,
       A.USER_ID,
       A.USER_NM
  FROM T_USER A

Mapper XML 구조

<!-- /src/main/resources/mybatis/user/mapper/UserMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.{회사명}.{프로젝트명}.user.mapper.UserMapper">

    <!-- 단건 조회 -->
    <select id="get" parameterType="com.{회사명}.{프로젝트명}.user.vo.UserVo"
            resultType="com.{회사명}.{프로젝트명}.user.vo.UserVo">
        SELECT A.USER_SN
             , A.USER_ID
             , A.USER_NM
             , A.USER_EMAIL
             , A.REG_DT
          FROM T_USER A
         WHERE A.USER_SN = #{userSn}
    </select>

    <!-- 목록 조회 -->
    <select id="getList" parameterType="com.{회사명}.{프로젝트명}.user.vo.UserVo"
            resultType="com.{회사명}.{프로젝트명}.user.vo.UserVo">
        SELECT A.USER_SN
             , A.USER_ID
             , A.USER_NM
             , A.USER_EMAIL
          FROM T_USER A
        <where>
            <if test="searchKeyword != null and searchKeyword != ''">
              AND A.USER_NM LIKE '%' || #{searchKeyword} || '%'
            </if>
        </where>
        ORDER BY A.USER_SN DESC
        OFFSET #{offset} ROWS FETCH NEXT #{rowPerPage} ROWS ONLY
    </select>

    <!-- 전체 수 -->
    <select id="getTotalCount" parameterType="com.{회사명}.{프로젝트명}.user.vo.UserVo"
            resultType="int">
        SELECT COUNT(*)
          FROM T_USER A
        <where>
            <if test="searchKeyword != null and searchKeyword != ''">
              AND A.USER_NM LIKE '%' || #{searchKeyword} || '%'
            </if>
        </where>
    </select>

    <!-- 등록 -->
    <insert id="regist" parameterType="com.{회사명}.{프로젝트명}.user.vo.UserVo">
        INSERT INTO T_USER (
              USER_ID
            , USER_NM
            , USER_EMAIL
            , REG_DT
        ) VALUES (
              #{userId}
            , #{userNm}
            , #{userEmail}
            , SYSTIMESTAMP
        )
    </insert>

    <!-- 수정 -->
    <update id="update" parameterType="com.{회사명}.{프로젝트명}.user.vo.UserVo">
        UPDATE T_USER
           SET USER_NM    = #{userNm}
             , USER_EMAIL = #{userEmail}
             , UPD_DT     = SYSTIMESTAMP
         WHERE USER_SN = #{userSn}
    </update>

    <!-- 삭제 -->
    <delete id="delete" parameterType="com.{회사명}.{프로젝트명}.user.vo.UserVo">
        DELETE FROM T_USER
         WHERE USER_SN = #{userSn}
    </delete>

</mapper>

시간 필드 타입 매핑

<!-- OffsetDateTime 타입 매핑 -->
<result property="regDt"
        column="REG_DT"
        javaType="java.time.OffsetDateTime"
        jdbcType="TIMESTAMP_WITH_TIMEZONE"/>
  • Java 타입: java.time.OffsetDateTime
  • DB 타입: TIMESTAMP WITH TIME ZONE
  • SYSDATE 사용 금지 → SYSTIMESTAMP 사용

페이징

Oracle 기준 페이징:

OFFSET #{offset} ROWS FETCH NEXT #{rowPerPage} ROWS ONLY

GeneralVOoffset, rowPerPage 필드를 자동으로 사용한다.


JOIN 규칙

-- INNER JOIN (기본)
SELECT A.USER_SN
     , A.USER_NM
     , B.DEPT_NM
  FROM T_USER A
 INNER JOIN T_DEPT B ON A.DEPT_SN = B.DEPT_SN

-- LEFT JOIN
SELECT A.USER_SN
     , A.USER_NM
     , B.DEPT_NM
  FROM T_USER A
  LEFT JOIN T_DEPT B ON A.DEPT_SN = B.DEPT_SN
 WHERE A.USE_YN = 'Y'

Mapper 인터페이스

// com.{회사명}.{프로젝트명}.user.mapper.UserMapper
@Mapper
public interface UserMapper {
    UserVo get(UserVo vo);
    List<UserVo> getList(UserVo vo);
    int getTotalCount(UserVo vo);
    void regist(UserVo vo);
    void update(UserVo vo);
    void delete(UserVo vo);
}

동적 쿼리 태그

<!-- <where>: WHERE 키워드 자동 처리 (불필요한 AND 제거) -->
<where>
    <if test="status != null">AND STATUS = #{status}</if>
    <if test="keyword != null">AND USER_NM LIKE '%' || #{keyword} || '%'</if>
</where>

<!-- <choose>: switch-case -->
<choose>
    <when test="type == 'A'">AND STATUS = 'ACTIVE'</when>
    <when test="type == 'I'">AND STATUS = 'INACTIVE'</when>
    <otherwise>AND STATUS IS NOT NULL</otherwise>
</choose>

<!-- <foreach>: IN 절 -->
<foreach collection="snList" item="sn" open="(" separator="," close=")">
    #{sn}
</foreach>

application.yml MyBatis 설정

mybatis:
  mapper-locations: classpath:mybatis/**/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true   # SNAKE_CASE → camelCase 자동 변환
    default-fetch-size: 100
    default-statement-timeout: 30

체크리스트

  • [ ] SQL 파일 위치: /src/main/resources/mybatis/{업무명}/mapper/
  • [ ] 키워드/테이블명/컬럼명 대문자
  • [ ] SELECT * 금지 — 필요한 컬럼만 명시
  • [ ] 컬럼 콤마는 줄바꿈 후 앞에 붙임
  • [ ] 테이블 Alias: A → B → C
  • [ ] SYSDATE 금지 → SYSTIMESTAMP 사용
  • [ ] 시간 필드: javaType=java.time.OffsetDateTime, jdbcType=TIMESTAMP_WITH_TIMEZONE
  • [ ] 페이징: OFFSET #{offset} ROWS FETCH NEXT #{rowPerPage} ROWS ONLY
  • [ ] #{} 파라미터 바인딩 사용