XML ๋งคํผ ๋ฌธ๋ฒ, ๋์ SQL ํ๊ทธ, ResultMap ์ฐ๊ด๊ด๊ณ ๋งคํ ๋ฐ ์ค๋ฌด ์ต์ ํ ๋ ํผ๋ฐ์ค
## 1. XML ๋งคํผ ๊ธฐ๋ณธ ์ ์ธ & ๊ตฌ์กฐ (Mapper DTD)
```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">
<!-- namespace: ์ฐ๊ฒฐํ ๋งคํผ ์ธํฐํ์ด์ค์ ํ ํจํค์ง ๊ฒฝ๋ก -->
<mapper namespace="com.example.project.mapper.UserMapper">
<!-- ํ๊ทธ์ id๋ Java ๋งคํผ ์ธํฐํ์ด์ค์ ๋ฉ์๋ ์ด๋ฆ๊ณผ 1:1 ์ผ์นํด์ผ ํจ -->
<select id="findById" parameterType="long" resultType="com.example.project.dto.UserDto">
SELECT id, username, email, created_at
FROM users
WHERE id = #{id}
</select>
</mapper>
```
## 2. ๊ธฐ๋ณธ CRUD ๊ตฌ๋ฌธ (SELECT, INSERT, UPDATE, DELETE)
```xml
<!-- 1. SELECT: ๋จ์ผ/๋ค์ค ํ ์กฐํ -->
<select id="findByEmail" parameterType="string" resultType="UserDto">
SELECT id, username, email, status FROM users WHERE email = #{email}
</select>
<!-- 2. INSERT: ์๋ ์ฆ๊ฐ(Auto-Increment) PK ํค ์ฑ๋ฒ (useGeneratedKeys) -->
<insert id="insertUser" parameterType="UserDto" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (username, email, password, status, created_at)
VALUES (#{username}, #{email}, #{password}, 'ACTIVE', NOW())
</insert>
<!-- Oracle ์ํ์ค ๊ธฐ๋ฐ PK ์ฌ์ /์ฌํ ์ฑ๋ฒ (selectKey) -->
<insert id="insertUserOracle" parameterType="UserDto">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
SELECT seq_users.NEXTVAL FROM dual
</selectKey>
INSERT INTO users (id, username, email) VALUES (#{id}, #{username}, #{email})
</insert>
<!-- 3. UPDATE: ์์ -->
<update id="updateLoginTime" parameterType="long">
UPDATE users SET last_login_at = NOW() WHERE id = #{id}
</update>
<!-- 4. DELETE: ์ญ์ -->
<delete id="deleteById" parameterType="long">
DELETE FROM users WHERE id = #{id}
</delete>
```
## 3. ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ (`#` vs `$`)
```xml
<!-- #{param}: PreparedStatement ํ๋ผ๋ฏธํฐ ์นํ (?) -->
<!-- ๊ฐ์ ์๋์ผ๋ก ๋ฐ์ดํ๊ฐ ๋ถ์ผ๋ฉฐ SQL Injection์ ์๋ฒฝ ์ฐจ๋จ (๊ธฐ๋ณธ์ ์ผ๋ก ๋ฐ๋์ ์ฌ์ฉ) -->
SELECT * FROM users WHERE username = #{username}
<!-- ${param}: ๋ฌธ์์ด ์ง์ ์นํ (Statement ๋ฐฉ์) -->
<!-- SQL Injection ์ทจ์ฝ์ ์ด ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก ํ
์ด๋ธ๋ช
/์ปฌ๋ผ๋ช
๋์ ๋ณ๊ฒฝ ์์๋ง ์ ํ์ ์ฌ์ฉ -->
SELECT * FROM ${tableName} ORDER BY ${sortColumn} ${sortDirection}
<!-- ๋ค์ค ํ๋ผ๋ฏธํฐ ๋งคํ (@Param ์ด๋
ธํ
์ด์
ํ์ฉ) -->
<!-- Java: List<User> search(@Param("name") String name, @Param("role") String role); -->
SELECT * FROM users
WHERE username = #{name} AND role = #{role}
<!-- NULL ํ๋ผ๋ฏธํฐ ํ์ฉ (ํด๋น ์ปฌ๋ผ์ JDBC ํ์
๋ช
์ ๊ถ์ฅ) -->
INSERT INTO users (username, phone)
VALUES (#{username}, #{phone, jdbcType=VARCHAR})
```
## 4. ๋์ SQL: ์กฐ๊ฑด ๋ถ๊ธฐ (`if`, `choose`)
```xml
<!-- 1. <if> ํ๊ทธ: ๋จ์ผ ์กฐ๊ฑด ๋ถ๊ธฐ -->
<select id="searchUsers" parameterType="UserSearchDto" resultType="UserDto">
SELECT * FROM users
WHERE 1 = 1
<if test="keyword != null and keyword != ''">
AND (username LIKE CONCAT('%', #{keyword}, '%') OR email LIKE CONCAT('%', #{keyword}, '%'))
</if>
<!-- ๋ฌธ์์ด ๋น๊ต ์ฃผ์: ๋จ์ผ ๋ฌธ์ 'Y' ๋น๊ต ์ char ํ๋ณํ ์๋ฌ ๋ฐฉ์ง -->
<if test='status == "ACTIVE"'>
AND status = 'ACTIVE'
</if>
</select>
<!-- 2. <choose>, <when>, <otherwise>: ๋ค์ค ์กฐ๊ฑด ๋ถ๊ธฐ (if-else if-else) -->
<select id="findUsersOrdered" resultType="UserDto">
SELECT * FROM users
ORDER BY
<choose>
<when test="sortBy == 'name'">username ASC</when>
<when test="sortBy == 'date'">created_at DESC</when>
<when test="sortBy == 'views'">view_count DESC</when>
<otherwise>id DESC</otherwise>
</choose>
</select>
```
## 5. ๋์ SQL: ๊ตฌ๋ฌธ ์ ๋ฆฌ (`where`, `set`, `trim`)
```xml
<!-- 1. <where> ํ๊ทธ: 1=1 ์ ๊ฑฐ, ๋งจ ์์ ๋ถํ์ํ AND/OR ์๋ ์ ๊ฑฐ, ํ์ ์กฐ๊ฑด ์์ผ๋ฉด WHERE ์์ฒด ์๋ต -->
<select id="findActiveUsers" resultType="UserDto">
SELECT * FROM users
<where>
<if test="status != null">AND status = #{status}</if>
<if test="minAge != null">AND age >= #{minAge}</if>
</where>
</select>
<!-- 2. <set> ํ๊ทธ: UPDATE ์ ๋ง์ง๋ง ์ฝค๋ง(,) ์๋ ์ ๊ฑฐ ๋ฐ SET ํค์๋ ์ฝ์
-->
<update id="updateSelective" parameterType="UserDto">
UPDATE users
<set>
<if test="username != null">username = #{username},</if>
<if test="email != null">email = #{email},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>
<!-- 3. <trim> ํ๊ทธ: ์ปค์คํ
์ ๋/์ ๋ฏธ์ฌ ์ ์ด -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="email != null">AND email = #{email}</if>
</trim>
```
## 6. ๋์ SQL: ์ปฌ๋ ์
๋ฐ๋ณต (`foreach`)
```xml
<!-- 1. IN ์ ๋ค์ค ์กฐํ (List/Array ํ๋ผ๋ฏธํฐ) -->
<!-- Java: List<User> findByIds(@Param("idList") List<Long> idList); -->
SELECT * FROM users
WHERE id IN
<foreach item="id" collection="idList" open="(" separator="," close=")">
#{id}
</foreach>
<!-- ์ถ๋ ฅ ์์: WHERE id IN (1, 2, 5, 8) -->
<!-- 2. ๋ค์ค ํ ์ผ๊ด ์ฝ์
(Bulk INSERT / MySQL) -->
INSERT INTO users (username, email, role) VALUES
<foreach item="item" collection="list" separator=",">
(#{item.username}, #{item.email}, #{item.role})
</foreach>
<!-- 3. Map ํค-๊ฐ ์ํ -->
<foreach collection="filterMap" index="key" item="val" separator="AND">
${key} = #{val}
</foreach>
```
## 7. ๊ฒฐ๊ณผ ๋งคํ (ResultMap & CamelCase)
```xml
<!-- 1. DB ์ปฌ๋ผ๊ณผ Java ๊ฐ์ฒด ํ๋๋ช
์ด ๋ค๋ฅผ ๋์ ๋งคํ ์ ์ -->
<resultMap id="UserDetailResultMap" type="UserDetailDto">
<id property="userId" column="user_id" />
<result property="userName" column="user_nm" />
<result property="userEmail" column="email_addr" />
<result property="registeredAt" column="reg_dt" />
</resultMap>
<select id="getDetail" resultMap="UserDetailResultMap">
SELECT user_id, user_nm, email_addr, reg_dt FROM users WHERE user_id = #{id}
</select>
<!-- ํ: Spring Boot ์ค์ ์์ map-underscore-to-camel-case: true ์ -->
<!-- user_name -> userName ์๋ ๋ณํ๋๋ฏ๋ก ๋จ์ ํ๋๋ ResultMap ๋ถํ์ -->
```
## 8. ์ฐ๊ด ๊ด๊ณ ๋งคํ (1:1 association, 1:N collection)
```xml
<!-- 1:1 ๊ด๊ณ (์ฃผ๋ฌธ -> ์ฃผ๋ฌธ์ ์ ๋ณด: association) & 1:N ๊ด๊ณ (์ฃผ๋ฌธ -> ์ฃผ๋ฌธ ์ํ ๋ชฉ๋ก: collection) -->
<resultMap id="OrderWithItemsMap" type="OrderDto">
<id property="orderId" column="order_id" />
<result property="orderDate" column="order_date" />
<!-- 1:1 ์ฐ๊ด ๋งคํ -->
<association property="buyer" javaType="UserDto">
<id property="id" column="buyer_id" />
<result property="username" column="buyer_name" />
</association>
<!-- 1:N ์ปฌ๋ ์
๋งคํ -->
<collection property="items" ofType="OrderItemDto">
<id property="orderItemId" column="order_item_id" />
<result property="productName" column="product_name" />
<result property="quantity" column="quantity" />
<result property="price" column="price" />
</collection>
</resultMap>
<select id="findOrderFullDetail" resultMap="OrderWithItemsMap">
SELECT o.order_id, o.order_date,
u.id as buyer_id, u.username as buyer_name,
i.order_item_id, i.product_name, i.quantity, i.price
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items i ON o.order_id = i.order_id
WHERE o.order_id = #{orderId}
</select>
```
## 9. SQL ์กฐ๊ฐ ๋ชจ๋ํ & ์ฌ์ฌ์ฉ (`sql`, `include`, `bind`)
```xml
<!-- 1. <sql> ๊ณตํต ์ปฌ๋ผ ์กฐ๊ฐ ์ ์ -->
<sql id="commonUserColumns">
u.id, u.username, u.email, u.status, u.created_at
</sql>
<!-- 2. <include> ์กฐ๊ฐ ์ฝ์
-->
<select id="findAll" resultType="UserDto">
SELECT <include refid="commonUserColumns" />
FROM users u
WHERE u.status = 'ACTIVE'
</select>
<!-- 3. <bind> ๋ณ์ ์์ฑ (LIKE ๊ฒ์์ด ์ ์ฒ๋ฆฌ) -->
<select id="findByKeyword" resultType="UserDto">
<bind name="pattern" value="'%' + keyword + '%'" />
SELECT * FROM users
WHERE username LIKE #{pattern} OR email LIKE #{pattern}
</select>
```
## 10. ํน์ ๋ฌธ์ ์ฒ๋ฆฌ (CDATA) & ํ์ด์ง
```xml
<!-- ๋ถ๋ฑํธ(<, >, <=, >=) ๋ฐ ํน์๋ฌธ์ XML ํ์ฑ ๋ฐฉ์ง (CDATA ํ์ ์ฌ์ฉ) -->
<select id="findBetweenDates" resultType="LogDto">
SELECT * FROM access_logs
<![CDATA[
WHERE log_time >= #{startDate}
AND log_time < #{endDate}
AND response_status >= 400
]]>
</select>
<!-- ์ฃผ์ DB ํ์ด์ง ์ฟผ๋ฆฌ ํจํด -->
<!-- MySQL / MariaDB -->
SELECT * FROM users ORDER BY id DESC LIMIT #{pageSize} OFFSET #{offset};
<!-- Oracle 12c+ -->
SELECT * FROM users ORDER BY id DESC
OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY;
```
## 11. Spring Boot ์ค์ & ์ค๋ฌด ๊ถ์ฅ ์ต์
```yaml
# application.yml ์ค๋ฌด ์ถ์ฒ ์ค์
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.project.dto, com.example.project.entity
configuration:
# ์นด๋ฉ ์ผ์ด์ค ์๋ ๋งคํ (user_id -> userId)
map-underscore-to-camel-case: true
# null ํ๋ผ๋ฏธํฐ ์ฝ์
์ ์๋ฌ ๋ฐฉ์ง (๊ธฐ๋ณธ NULL ํ์ฉ)
jdbc-type-for-null: 'NULL'
# SQL ์ฟผ๋ฆฌ ์คํ ํ์์์ (์ด ๋จ์)
default-statement-timeout: 25
# ์ง์ฐ ๋ก๋ฉ ํ์ฑํ
lazy-loading-enabled: true
# ์ฟผ๋ฆฌ ์บ์ ์ฌ์ฉ ์ฌ๋ถ
cache-enabled: false
# MyBatis ์คํ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ ๋ก๊ทธ ์ถ๋ ฅ
logging:
level:
com.example.project.mapper: DEBUG
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์