ResultMap — 복잡한 결과를 객체에 매핑하는 방법
DB 컬럼명이
user_name인데 자바 필드는userName이면, 결과가 null로 들어오는 경험을 해보셨나요?
MyBatis의 resultType은 컬럼명과 필드명이 정확히 일치할 때만 동작합니다. 실무에서는 네이밍 컨벤션 차이, 조인 결과, 1:N 관계 매핑 등으로 자동 매핑이 깨지는 경우가 빈번합니다.
개념 정의
ResultMap 은 SQL 결과의 컬럼과 자바 객체의 필드 사이의 매핑 규칙을 명시적으로 정의하는 MyBatis의 핵심 기능입니다. 자동 매핑이 불가능한 상황에서 어떤 컬럼이 어떤 필드로 들어가는지 직접 지정합니다.
왜 필요한가
- ** 네이밍 불일치 **: DB는
snake_case, 자바는camelCase를 쓰는 것이 일반적입니다.user_name컬럼이userName필드에 자동으로 매핑되지 않습니다. - ** 조인 결과 매핑 **: 주문(Order)과 사용자(User)를 JOIN한 결과를 하나의 객체 그래프로 변환해야 합니다.
- **1:N 관계 **: 하나의 주문에 여러 주문 항목(OrderItem)이 있을 때, 플랫한 결과를 중첩 컬렉션으로 변환해야 합니다.
자동 매핑 vs ResultMap
먼저 자동 매핑이 동작하는 조건을 정리합니다.
| 조건 | 자동 매핑 | ResultMap 필요 |
|---|---|---|
| 컬럼명 = 필드명 | O | X |
| snake_case → camelCase | 설정으로 가능 | X |
| 컬럼명과 필드명 완전히 다름 | X | O |
| 조인 결과 → 중첩 객체 | X | O |
| 1:N 관계 | X | O |
mapUnderscoreToCamelCase=true설정을 켜면user_name→userName자동 매핑이 됩니다. 하지만 조인 결과나 1:N 관계에서는 ResultMap이 필수입니다.
camelCase 자동 변환을 활성화하는 설정은 다음과 같습니다.
<!-- mybatis-config.xml -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
ResultMap 기본 구조
컬럼과 필드를 하나씩 매핑하는 가장 기본적인 형태입니다.
<resultMap id="userMap" type="User">
<id property="id" column="user_id"/> <!-- PK -->
<result property="name" column="user_name"/>
<result property="email" column="email_addr"/>
<result property="createdAt" column="reg_date"/>
</resultMap>
<select id="findById" resultMap="userMap">
SELECT user_id, user_name, email_addr, reg_date
FROM users WHERE user_id = #{id}
</select>
<id> 태그는 PK 컬럼을 지정합니다. 기능적으로는 <result>와 동일하지만, MyBatis 내부에서 객체 캐싱과 중복 제거에 PK 정보를 활용하기 때문에 반드시 구분해서 작성해야 합니다.
association — 1:1 관계 매핑
주문(Order) 안에 사용자(User) 객체를 포함하는 경우입니다.
public class Order {
private Long id;
private int totalAmount;
private User user; // ← 1:1 관계
}
<resultMap id="orderWithUser" type="Order">
<id property="id" column="order_id"/>
<result property="totalAmount" column="total_amount"/>
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<result property="email" column="email"/>
</association>
</resultMap>
<select id="findOrderWithUser" resultMap="orderWithUser">
SELECT o.id as order_id, o.total_amount,
u.id as user_id, u.name as user_name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.id = #{id}
</select>
위 쿼리에서 핵심은 alias 입니다. 두 테이블에 id 컬럼이 모두 있으므로 order_id, user_id로 구분해야 합니다. alias를 빠뜨리면 컬럼이 충돌하여 잘못된 값이 매핑됩니다.
collection — 1:N 관계 매핑
하나의 주문에 여러 주문 항목이 있는 경우입니다.
public class Order {
private Long id;
private int totalAmount;
private List<OrderItem> items; // ← 1:N 관계
}
<resultMap id="orderWithItems" type="Order">
<id property="id" column="order_id"/>
<result property="totalAmount" column="total_amount"/>
<collection property="items" ofType="OrderItem">
<id property="id" column="item_id"/>
<result property="productName" column="product_name"/>
<result property="quantity" column="quantity"/>
<result property="price" column="price"/>
</collection>
</resultMap>
MyBatis는 <id>로 지정된 PK 값을 기준으로 중복을 판단합니다. 같은 order_id를 가진 행들이 하나의 Order 객체에 모이고, 각 행의 item 정보는 리스트에 추가됩니다.
SQL 결과 (플랫) → 자바 객체 (중첩)
order_id | item_id | ... Order(id=1)
1 | 10 | ... ├─ OrderItem(id=10)
1 | 11 | ... └─ OrderItem(id=11)
2 | 12 | ... Order(id=2)
└─ OrderItem(id=12)
주의할 점
<id> 태그 누락 시 중복 객체 생성
<collection>에서 <id> 태그를 빠뜨리면 MyBatis가 행 단위로 새 객체를 생성합니다. 주문 1건에 항목 3건이면, 같은 Order 객체가 3개 만들어지고 각각 items에 1건씩만 들어갑니다. <id>가 있어야 같은 PK 값을 가진 행들을 하나의 객체로 합칩니다.
N+1 쿼리 문제
<association>과 <collection>에는 select 속성으로 별도 쿼리를 지정할 수 있습니다.
<!-- N+1 위험: 주문 100건이면 사용자 쿼리 100번 추가 실행 -->
<association property="user" column="user_id"
select="findUserById"/>
이 방식은 주문 N건마다 사용자 조회 쿼리가 추가로 실행되어 N+1 문제가 발생합니다. 성능이 중요한 쿼리에서는 JOIN을 사용한 단일 쿼리 + 인라인 ResultMap 방식이 안전합니다.
컬럼 alias 충돌
조인 쿼리에서 같은 이름의 컬럼(id, name, status 등)이 여러 테이블에 있으면, alias 없이는 어떤 테이블의 값인지 구분할 수 없습니다. ResultMap의 column 속성은 alias된 이름을 기준으로 매핑하므로, SELECT 절에서 반드시 고유한 alias를 지정해야 합니다.
정리
| 항목 | 설명 |
|---|---|
| ResultMap | 컬럼-필드 매핑 규칙을 명시적으로 정의 |
<id> | PK 매핑, 중복 제거와 캐싱에 필수 |
<association> | 1:1 관계 매핑 (N:1 조인 포함) |
<collection> | 1:N 관계 매핑, PK 기준으로 그룹핑 |
| N+1 방지 | select 속성 대신 JOIN + 인라인 ResultMap 사용 |
| alias 필수 | 조인 시 동일 컬럼명 충돌 방지 |