resultMap: 컬럼과 객체를 잇는 다리
MyBatis가 조회 결과 컬럼을 객체 프로퍼티로 잇는 방식, snake_case↔camelCase 자동 매핑의 원리와 한계, resultMap 명시 매핑과 조용한 null 함정을 정리한다.
조회 쿼리의 결과는 컬럼명을 가진 행의 집합이다. 이걸 자바 객체로 받으려면 컬럼 이름과 객체의 프로퍼티 이름을 이어주는 무언가가 필요하다. MyBatis는 이를 자동 매핑과 resultMap 두 갈래로 처리한다. 둘의 경계를 모르면, 분명히 조회됐는데 객체 필드가 null인 당혹스러운 버그를 만난다.
핵심 개념: 자동 매핑과 그 한계
MyBatis는 결과 컬럼명과 객체 setter(프로퍼티)명을 대소문자 무시하고 일치시켜 자동 매핑한다. 그런데 DB는 created_at(snake_case), 자바는 createdAt(camelCase)가 관례라 이름이 어긋난다. 이때 설정 mapUnderscoreToCamelCase=true를 켜면 MyBatis가 created_at을 createdAt으로 변환해 매핑한다.
1
2
3
4
<!-- mybatis-config.xml -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
자동 매핑의 한계는 분명하다. (1) 조인 결과를 중첩 객체로 묶지 못한다(평면 컬럼만). (2) 컬럼명과 프로퍼티명이 규칙적으로 대응하지 않으면 매핑이 안 된다. (3) 타입 핸들러를 컬럼별로 지정할 수 없다. 이 한계를 넘으려면 resultMap을 명시한다.
resultMap: 명시적 매핑
resultMap은 컬럼과 프로퍼티의 대응을 직접 선언한다. 별칭(alias)으로 컬럼명을 맞추는 방법도 있지만, resultMap은 중첩 객체 매핑까지 표현할 수 있어 더 강력하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<resultMap id="userMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="name"/>
<result column="email" property="email"/>
<!-- 1:1 연관 객체 매핑 -->
<association property="dept" javaType="Dept">
<id column="dept_id" property="id"/>
<result column="dept_name" property="name"/>
</association>
<!-- 1:N 컬렉션 매핑 -->
<collection property="orders" ofType="Order">
<id column="order_id" property="id"/>
<result column="amount" property="amount"/>
</collection>
</resultMap>
<select id="findUser" resultMap="userMap">
SELECT u.id, u.user_name, u.email,
d.dept_id, d.dept_name,
o.order_id, o.amount
FROM users u
LEFT JOIN dept d ON d.dept_id = u.dept_id
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.id = #{id}
</select>
<id>는 단순 매핑이 아니다. MyBatis는 <id> 컬럼 값으로 행이 같은 엔티티인지 식별한다. 조인으로 한 사용자가 주문 수만큼 중복 행으로 나와도, 같은 id면 하나의 User로 묶고 orders 컬렉션에 주문만 누적한다. <id>를 빠뜨리면 같은 사용자가 여러 객체로 쪼개진다.
운영 함정: 조용한 null
가장 흔한 버그는 매핑 누락이 예외 없이 null이 되는 것이다. 쿼리에 user_name을 SELECT 했는데 자동 매핑 대상 프로퍼티가 name이고 underscore 변환이 꺼져 있으면, MyBatis는 대응을 못 찾아도 에러를 내지 않고 그냥 그 필드를 비워둔다. “쿼리는 맞는데 객체만 비어 있다”의 정체가 이것이다.
이를 빨리 잡으려면 설정 autoMappingUnknownColumnBehavior를 WARNING(또는 FAILING)으로 둔다. 매핑되지 않은 컬럼이 있을 때 로그/예외로 알려준다.
1
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
또 하나, 조인 매핑에서 컬럼명 충돌. users.id와 orders.id가 둘 다 id로 나오면 어느 쪽이 매핑될지 모호하다. SELECT에서 별칭(u.id AS user_id)으로 구분하고 resultMap도 그 별칭에 맞춘다.
면접 한 줄 Q&A
- Q. snake_case 컬럼을 camelCase로 받으려면? A.
mapUnderscoreToCamelCase=true로 자동 변환하거나, resultMap/별칭으로 명시 매핑한다. - Q. resultMap의
<id>역할은? A. 행이 같은 엔티티인지 식별하는 키다. 조인으로 중복된 행을 하나의 객체로 묶고 컬렉션을 누적할 때 기준이 된다.