Mybatis 연관관계 매핑: 다대일(N:1)과 일대다(1:N) 처리 기법

Mybatis에서 객체 간의 연관관계를 매핑할 때, 특히 다대일(Many-to-One)과 일대다(One-to-Many) 관계는 자주 마주치는 패턴이다. 여기서는 학생(Student)과 교사(Teacher)라는 간단한 도메인 모델을 통해 두 가지 관계를 각각의 접근 방식으로 구현해본다.

1. 다대일 관계 (여러 학생 → 한 명의 교사)

다수의 학생이 하나의 교사에 속하는 구조다. Student 엔티티에는 Teacher 객체가 필드로 포함된다.

엔티티 클래스

//Student.java
package com.example.domain;
import lombok.Data;

@Data
public class Student {
    private int id;
    private String name;
    private Teacher teacher; // 다대일 관계 표현
}

//Teacher.java
package com.example.domain;
import lombok.Data;

@Data
public class Teacher {
    private int id;
    private String name;
}

방법 A: 서브쿼리 방식 (N+1 문제 가능)

메인 쿼리로 학생 목록을 가져온 후, 각 학생의 tid를 이용해 별도의 교사 조회 쿼리를 실행한다.

<!-- StudentMapper.xml -->
<select id="findAllWithTeacher" resultMap="studentTeacherResult">
    SELECT * FROM student
</select>

<resultMap id="studentTeacherResult" type="com.example.domain.Student">
    <result property="id" column="id"/>
    <result property="name" column="name"/>
    <association property="teacher"
                 column="tid"
                 javaType="com.example.domain.Teacher"
                 select="findTeacherById"/>
</resultMap>

<select id="findTeacherById" resultType="com.example.domain.Teacher">
    SELECT * FROM teacher WHERE id = #{tid}
</select>

방법 B: 조인 방식 (단일 쿼리)

SQL 조인을 통해 한 번에 모든 데이터를 가져온 뒤, ResultMap으로 객체를 조립한다.

<!-- StudentMapper.xml -->
<select id="findAllWithTeacherJoin" resultMap="studentTeacherJoinResult">
    SELECT s.id AS sid, s.name AS sname,
           t.id AS tid, t.name AS tname
    FROM student s, teacher t
    WHERE s.tid = t.id
</select>

<resultMap id="studentTeacherJoinResult" type="com.example.domain.Student">
    <result property="id" column="sid"/>
    <result property="name" column="sname"/>
    <association property="teacher" javaType="com.example.domain.Teacher">
        <result property="id" column="tid"/>
        <result property="name" column="tname"/>
    </association>
</resultMap>

2. 일대다 관계 (한 명의 교사 → 여러 학생)

반대로 한 교사가 여러 학생을 가르치는 구조다. Teacher 엔티티에는 List<Student> 필드가 필요하다.

엔티티 클래스

//Student2.java
package com.example.domain;
import lombok.Data;

@Data
public class Student2 {
    private int id;
    private String name;
    private int tid;
}

//Teacher2.java
package com.example.domain;
import lombok.Data;
import java.util.List;

@Data
public class Teacher2 {
    private int id;
    private String name;
    private List<Student2> students; // 일대다 관계 표현
}

방법 A: 서브쿼리 방식

<!-- TeacherMapper.xml -->
<select id="findTeacherWithStudents" resultMap="teacherStudentResult">
    SELECT * FROM teacher WHERE id = #{id}
</select>

<resultMap id="teacherStudentResult" type="com.example.domain.Teacher2">
    <result property="id" column="id"/>
    <result property="name" column="name"/>
    <collection property="students"
                javaType="java.util.ArrayList"
                ofType="com.example.domain.Student2"
                column="id"
                select="findStudentsByTeacherId"/>
</resultMap>

<select id="findStudentsByTeacherId" resultType="com.example.domain.Student2">
    SELECT * FROM student WHERE tid = #{id}
</select>

방법 B: 조인 방식

<!-- TeacherMapper.xml -->
<select id="findTeacherWithStudentsJoin" resultMap="teacherStudentJoinResult">
    SELECT s.id AS sid, s.name AS sname,
           t.id AS tid, t.name AS tname
    FROM student s, teacher t
    WHERE s.tid = t.id AND t.id = #{id}
</select>

<resultMap id="teacherStudentJoinResult" type="com.example.domain.Teacher2">
    <result property="id" column="tid"/>
    <result property="name" column="tname"/>
    <collection property="students" ofType="com.example.domain.Student2">
        <result property="id" column="sid"/>
        <result property="name" column="sname"/>
        <result property="tid" column="tid"/>
    </collection>
</resultMap>

3. 핵심 정리

  • <association> : 다대일 관계에서 '하나'의 대상 객체를 매핑할 때 사용한다.
  • <collection> : 일대다 관계에서 '여러' 객체를 컬렉션으로 매핑할 때 사용한다.
  • javaType : 자바 객체의 실제 타입을 명시한다. collection에서는 주로 ArrayList를 지정한다.
  • ofType : 컬렉션 내부에 담길 제네릭 타입을 지정한다. 예: Student2.class
  • 서브쿼리 방식 : 매핑 로직이 직관적이나, N+1 쿼리 문제가 발생할 수 있어 주의가 필요하다.
  • 조인 방식 : 한 번의 SQL로 모든 데이터를 가져오므로 성능상 유리하지만, ResultMap이 다소 복잡해질 수 있다.

태그: MyBatis association Collection ResultMap N+1문제

7월 6일 01:11에 게시됨