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 필요
컬럼명 = 필드명OX
snake_case → camelCase설정으로 가능X
컬럼명과 필드명 완전히 다름XO
조인 결과 → 중첩 객체XO
1:N 관계XO

mapUnderscoreToCamelCase=true 설정을 켜면 user_nameuserName 자동 매핑이 됩니다. 하지만 조인 결과나 1:N 관계에서는 ResultMap이 필수입니다.

camelCase 자동 변환을 활성화하는 설정은 다음과 같습니다.

XML
<!-- mybatis-config.xml -->
<settings>
  <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

ResultMap 기본 구조

컬럼과 필드를 하나씩 매핑하는 가장 기본적인 형태입니다.

XML
<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) 객체를 포함하는 경우입니다.

JAVA
public class Order {
    private Long id;
    private int totalAmount;
    private User user;  // ← 1:1 관계
}
XML
<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 관계 매핑

하나의 주문에 여러 주문 항목이 있는 경우입니다.

JAVA
public class Order {
    private Long id;
    private int totalAmount;
    private List<OrderItem> items;  // ← 1:N 관계
}
XML
<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 정보는 리스트에 추가됩니다.

PLAINTEXT
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 속성으로 별도 쿼리를 지정할 수 있습니다.

XML
<!-- 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 필수조인 시 동일 컬럼명 충돌 방지
댓글 로딩 중...