// Developer · Debugging Reports

뉴스도 디버깅이 필요해

마주친 오류를 재현하고, 원인을 추적하고, 기록합니다. Java · Spring · DB · 실무 트러블슈팅.

debug — today.log
$ debug --trace "오늘의 뉴스"
> 헤드라인 파싱 중...
> 원인 추적: 근거 누락 · 출처 불명
> 패치 적용: 사실 확인 + 출처 보강
✓ RESOLVED
TOTAL
TODAY
YESTERDAY

Categories

웹개발/Mybatis

MyBatis resultType resultMap 차이, JOIN과 1:N 매핑에서 선택 기준

자바를잡아2026. 7. 9. 09:00
반응형

MyBatis에서 단순 조회는 resultType으로 충분해 보이지만, JOIN을 붙인 순간 필드가 null로 들어오거나 같은 부모 객체가 여러 번 생기는 문제가 나온다. 이때 먼저 확인할 것은 조회 결과 구조가 단순 객체인지, 중첩 객체나 리스트인지다.

결론부터 말하면 단일 테이블의 평평한 결과는 resultType, 컬럼명이 다르거나 JOIN 결과를 객체 안 객체 또는 리스트로 묶어야 하면 resultMap을 쓰는 편이 맞다. 특히 1:N 조회에서는 collectionid 태그를 제대로 잡지 않으면 중복 행을 객체 구조로 묶기 어렵다.

핵심 결론: resultType은 자동 매핑이고, resultMap은 결과를 어떻게 객체로 조립할지 직접 정의하는 매핑 설계도다.

확인 순서: 단일 테이블인가 → 컬럼명과 필드명이 맞는가 → JOIN이 있는가 → 1:1인가 1:N인가 → 부모/자식 id 컬럼이 명확한가.

기본 차이는 자동 매핑과 직접 매핑이다

resultType은 SELECT 결과를 지정한 Java 타입에 자동으로 담는다. 컬럼명과 필드명이 맞거나, mapUnderscoreToCamelCase 설정이 켜져 있으면 코드가 짧고 읽기 쉽다. MyBatis가 JDBC 결과를 객체로 옮기는 기본 흐름이 낯설다면 MyBatis 기본 구조를 먼저 잡고 보는 편이 이해가 빠르다.

<select id="selectMember" parameterType="string" resultType="Member">
  SELECT member_id, member_name, email
  FROM member
  WHERE member_id = #{memberId}
</select>

반대로 resultMap은 어떤 컬럼을 어떤 필드에 넣을지 직접 적는다.

<resultMap id="memberMap" type="Member">
  <id property="memberId" column="member_id"/>
  <result property="memberName" column="member_name"/>
  <result property="email" column="email"/>
</resultMap>

기본 비교 자체는 이미 발행한 매핑 방식 비교에서 다뤘다. 이번 글에서는 거기서 한 단계 더 들어가, 실무에서 resultMap으로 넘어가야 하는 지점을 중심으로 본다.

resultType으로 충분한 경우

단일 테이블 조회

단일 테이블에서 VO 하나로 바로 담을 수 있는 결과라면 resultType이 가장 단순하다.

<select id="selectBoard" parameterType="long" resultType="Board">
  SELECT board_id, title, writer_id, created_at
  FROM board
  WHERE board_id = #{boardId}
</select>

이 경우 BoardboardId, title, writerId, createdAt 필드가 있고 camelCase 설정이 맞다면 별도 resultMap 없이도 충분하다.

컬럼 alias로 해결할 수 있는 조회

필드명이 조금 다르더라도 쿼리에서 alias를 주면 resultType으로 처리할 수 있다.

<select id="selectBoardSummary" resultType="BoardSummary">
  SELECT
    board_id AS boardId,
    title AS boardTitle,
    created_at AS writtenAt
  FROM board
</select>

단순 목록 화면처럼 컬럼 몇 개만 가져와 DTO에 담는 구조라면 alias를 주는 편이 resultMap보다 가볍다. 하지만 alias가 너무 많아지고 여러 쿼리에서 같은 매핑을 반복한다면 resultMap 재사용을 검토할 시점이다.

Map으로 임시 조회하는 경우

운영 점검용이나 임시 화면처럼 타입을 엄격하게 만들지 않는 조회는 resultType="map"을 쓰기도 한다.

<select id="selectRawStats" resultType="map">
  SELECT status, COUNT(*) AS cnt
  FROM orders
  GROUP BY status
</select>

다만 Map 결과는 컴파일 단계에서 필드명을 검증하기 어렵다. 운영 코드에서는 DTO나 VO로 받는 편이 유지보수에 낫다.

필드가 null로 들어올 때 보는 순서

컬럼명과 필드명이 맞는지 본다

resultType을 썼는데 일부 필드만 null이면 가장 먼저 컬럼명과 Java 필드명을 비교한다.

DB 컬럼 Java 필드 resultType 처리 해결 방법
member_id memberId 설정 없으면 실패 가능 camelCase 설정 또는 alias
member_nm memberName 자동 추론 어려움 alias 또는 resultMap
dept_name departmentName 이름 불일치 alias 또는 resultMap
reg_dt createdAt 의미가 다름 resultMap 권장

member_idmemberId처럼 규칙이 명확하면 mapUnderscoreToCamelCase로 해결할 수 있다. 하지만 member_nmmemberName처럼 약어와 의미가 섞이면 alias나 resultMap이 더 명확하다.

camelCase 설정이 실제로 적용됐는지 본다

설정을 켰다고 생각했는데 적용되지 않는 경우도 있다. Spring MyBatis에서는 mybatis-config.xml이 실제로 SqlSessionFactoryBean에 연결되어 있어야 한다.

<settings>
  <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

설정 파일이 읽히지 않으면 resultType 매핑도 기대대로 동작하지 않는다. 설정 파일 위치와 Mapper XML 위치를 함께 확인해야 하는 경우에는 연동 설정 흐름에서 configLocation과 mapperLocations를 같이 점검한다.

alias가 오히려 충돌하는지 본다

쿼리에서 alias를 주는 방식은 간단하지만, JOIN에서 같은 이름을 반복하면 충돌이 생긴다.

SELECT
  m.member_id AS id,
  d.dept_id AS id
FROM member m
JOIN dept d ON d.dept_id = m.dept_id

두 컬럼 모두 id가 되면 어떤 값이 어떤 필드로 들어갈지 애매해진다. JOIN에서는 부모와 자식 컬럼에 명확한 alias를 주거나 resultMap에서 column을 분리해야 한다.

JOIN 조회에서 resultMap이 필요한 이유

1:1 관계는 association으로 나눈다

회원과 부서처럼 한 회원에 부서 하나가 붙는 구조는 resultType으로 평평하게 담을 수도 있다. 하지만 객체 안에 Dept 객체를 넣고 싶다면 resultMap의 association이 필요하다.

<resultMap id="memberWithDeptMap" type="Member">
  <id property="memberId" column="member_id"/>
  <result property="memberName" column="member_name"/>

  <association property="dept" javaType="Dept">
    <id property="deptId" column="dept_id"/>
    <result property="deptName" column="dept_name"/>
  </association>
</resultMap>

이 매핑은 Member 객체 안의 dept 필드에 Dept 객체를 만들어 넣는다. 단순 DTO 하나로 펼쳐서 받을지, 도메인 구조대로 중첩 객체를 만들지는 화면과 서비스 로직의 사용 방식에 따라 결정한다.

컬럼 alias는 명확하게 분리한다

JOIN에서는 같은 이름의 컬럼이 자주 나온다. id, name, created_at 같은 컬럼은 테이블마다 있을 수 있다.

<select id="selectMemberWithDept" parameterType="string" resultMap="memberWithDeptMap">
  SELECT
    m.member_id,
    m.member_name,
    d.dept_id,
    d.dept_name
  FROM member m
  LEFT JOIN dept d ON d.dept_id = m.dept_id
  WHERE m.member_id = #{memberId}
</select>

여기서는 컬럼명이 겹치지 않게 가져왔지만, 실제 테이블이 둘 다 name을 가진다면 member_name, dept_name처럼 alias를 분리하는 것이 안전하다.

1:N 매핑에서는 collection을 쓴다

부모 하나에 자식 여러 개를 묶는 구조

게시글 하나에 댓글 여러 개, 부서 하나에 회원 여러 명처럼 1:N 구조는 resultMap이 필요한 대표 상황이다. SQL 결과는 행 여러 개지만, Java 객체는 부모 하나 안에 리스트를 넣고 싶기 때문이다.

<resultMap id="boardWithCommentsMap" type="Board">
  <id property="boardId" column="board_id"/>
  <result property="title" column="title"/>

  <collection property="comments" ofType="Comment">
    <id property="commentId" column="comment_id"/>
    <result property="content" column="comment_content"/>
    <result property="writerId" column="comment_writer_id"/>
  </collection>
</resultMap>

이렇게 정의하면 MyBatis가 같은 board_id를 가진 행을 하나의 Board 객체로 묶고, 각 댓글은 comments 리스트에 넣는다.

id 태그를 빼면 중복이 생긴다

collection 매핑에서 id 태그는 단순한 장식이 아니다. MyBatis가 어떤 행을 같은 부모로 볼지 판단하는 기준이다.

<select id="selectBoardWithComments" parameterType="long" resultMap="boardWithCommentsMap">
  SELECT
    b.board_id,
    b.title,
    c.comment_id,
    c.content AS comment_content,
    c.writer_id AS comment_writer_id
  FROM board b
  LEFT JOIN comment c ON c.board_id = b.board_id
  WHERE b.board_id = #{boardId}
  ORDER BY c.comment_id
</select>

부모의 <id property="boardId" column="board_id"/>가 명확해야 같은 게시글 행이 반복되어도 하나의 객체로 묶인다. 자식의 comment_id도 id로 잡아야 댓글 중복을 줄일 수 있다.

LEFT JOIN에서 자식이 없을 때도 고려한다

LEFT JOIN을 쓰면 부모는 있지만 자식 댓글이 없는 행이 나올 수 있다. 이때 자식 id 컬럼이 null이면 빈 객체가 만들어지는지, 빈 리스트로 남는지 프로젝트 설정과 매핑 방식에 따라 확인해야 한다.

운영에서는 “댓글이 없는 게시글인데 comments에 빈 Comment 객체 하나가 들어 있다” 같은 문제가 생기기도 한다. 이럴 때는 자식 id 컬럼이 제대로 지정되어 있는지, SELECT 컬럼 alias가 null 판단을 방해하지 않는지 먼저 본다.

resultType과 resultMap 선택 기준

상황 권장 방식 이유
단일 테이블 단순 조회 resultType 코드가 짧고 자동 매핑으로 충분
컬럼명과 필드명이 규칙적으로 다름 resultType + camelCase 전역 규칙으로 처리 가능
컬럼 alias가 많고 반복됨 resultMap 매핑 규칙 재사용 가능
1:1 중첩 객체 resultMap + association 객체 안 객체를 명확히 구성
1:N 리스트 resultMap + collection 여러 행을 하나의 부모 객체로 묶음
단순 목록 DTO resultType 또는 별도 DTO 화면용 평면 구조면 가볍게 처리

실무에서 자주 나는 오류

resultType과 resultMap을 같이 쓰는 경우

하나의 select에는 resultTyperesultMap 중 하나만 지정한다.

-- 피할 형태
<select id="selectMember" resultType="Member" resultMap="memberMap">

복잡한 매핑이 필요하면 resultMap으로 통일하고, 단순 조회는 resultType으로 둔다.

typeAlias 문제를 매핑 문제로 착각하는 경우

resultType="Member"처럼 짧은 이름을 쓰려면 typeAlias가 등록되어 있어야 한다.

<typeAliases>
  <package name="com.example.domain"/>
</typeAliases>

alias가 등록되지 않았는데 resultType만 보고 있으면 원인을 놓친다. 이 경우에는 전체 클래스명을 써서 먼저 동작 여부를 확인해 볼 수 있다.

Mapper XML이 로딩되지 않은 경우

resultMap을 잘 작성해도 Mapper XML이 로딩되지 않으면 아무 의미가 없다. mapperLocations, XML 파일 위치, namespace, SQL id를 같이 확인해야 한다.

Mapper 인터페이스가 Bean으로 잡히지 않는 문제와 SQL statement를 못 찾는 문제는 다르다. Bean 등록 문제가 의심되면 MapperScan 설정부터 보고, statement 문제면 namespace와 Mapper XML 경로를 본다.

실무에서 바로 쓰는 팁

  • 목록 DTO는 resultType으로 가볍게 시작한다. 화면에 필요한 컬럼만 alias로 맞추면 충분한 경우가 많다.
  • JOIN 결과를 도메인 구조로 담으면 resultMap을 쓴다. association과 collection이 필요한 순간 자동 매핑보다 직접 매핑이 낫다.
  • 1:N에서는 부모 id와 자식 id를 모두 명확히 둔다. 중복 행을 객체로 묶는 기준이 된다.
  • 필드 null은 컬럼명, alias, camelCase 설정 순서로 본다. SQL 결과 자체에 값이 있는지도 같이 확인한다.
  • 기본 비교 글과 중복되지 않게 문제 상황으로 접근한다. 단순 정의보다 “왜 null인지”, “왜 중복되는지”를 기준으로 보면 실무 적용이 쉽다.

자주 묻는 질문

  • Q. MyBatis resultType resultMap 차이는 무엇인가요? A. resultType은 자동 매핑이고, resultMap은 컬럼과 필드의 연결 규칙을 직접 정의하는 방식입니다.
  • Q. 단순 조회도 resultMap을 쓰는 것이 더 좋은가요? A. 꼭 그렇지는 않습니다. 단일 테이블 단순 조회는 resultType이 더 짧고 읽기 쉽습니다.
  • Q. JOIN 조회는 무조건 resultMap인가요? A. 평면 DTO로 받을 수 있으면 resultType도 가능합니다. 다만 중첩 객체나 리스트가 필요하면 resultMap이 맞습니다.
  • Q. 1:N 매핑에서 id 태그가 왜 중요한가요? A. MyBatis가 같은 부모 행과 같은 자식 행을 식별해 중복을 줄이고 객체를 묶는 기준으로 사용하기 때문입니다.
  • Q. resultType으로 필드가 null이면 무엇부터 확인하나요? A. SELECT 결과 컬럼명, Java 필드명, alias, mapUnderscoreToCamelCase 설정 순서로 확인합니다.
  • Q. resultMap을 여러 쿼리에서 재사용해도 되나요? A. 같은 객체 구조와 같은 컬럼 alias 규칙을 공유한다면 재사용해도 됩니다. 다만 쿼리마다 컬럼 의미가 다르면 별도 resultMap이 안전합니다.

정리

resultTyperesultMap의 차이는 단순히 짧게 쓰느냐 길게 쓰느냐가 아니다. resultType은 평평한 결과를 자동으로 담는 방식이고, resultMap은 JOIN 결과를 객체 구조로 조립하는 방식이다.

필드가 null이면 컬럼명과 alias부터 보고, JOIN 결과가 중복되면 부모와 자식의 id 매핑을 본다. 1:1은 association, 1:N은 collection으로 구조를 나누면 Mapper XML이 길어지더라도 문제 원인을 훨씬 명확하게 추적할 수 있다.

단순 조회는 resultType으로 가볍게, JOIN과 1:N 매핑은 resultMap으로 명확하게 잡는 것이 실무에서 가장 덜 흔들리는 기준이다.
반응형