MyBatis에서 엔티티 간 관계 매핑 완벽 가이드

관계형 데이터베이스의 3가지 연결 방식

관계형 데이터베이스에서 테이블은 서로 연결되어 있으며, 크게 다음 세 가지 방식으로 구분됩니다.

  • 일대일(1:1) : 한 레코드가 다른 테이블의 단 한 레코드와만 연결됩니다. 예: 사용자 ↔ 주민등록번호
  • 일대다(1:N) : 한 레코드가 다른 테이블의 여러 레코드와 연결되지만, 반대는 불가능합니다. 예: 부서 ↔ 직원
  • 다대다(M:N) : 양쪽 테이블 모두 서로의 여러 레코드와 연결됩니다. 예: 수강생 ↔ 강좌

Java 객체로 관계 표현하기

관계를 객체지향 코드로 옮기는 핵심은 참조 필드를 두는 것입니다.

  • 일대일 : 두 클래스 모두 상대방 타입의 단일 필드를 가짐
  • 일대다 : "일" 쪽은 List/Set 같은 컬렉션, "다" 쪽은 단일 참조 필드
  • 다대다 : 양쪽 모두 컬렉션 필드 사용

실습 1 – 일대일 조회(<association>)

1) 스키마 작성

CREATE DATABASE IF NOT EXISTS relation_demo;
USE relation_demo;

CREATE TABLE passport (
    id   INT AUTO_INCREMENT PRIMARY KEY,
    code VARCHAR(20) UNIQUE
);

CREATE TABLE traveler (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    full_name  VARCHAR(50),
    age        TINYINT,
    passport_id INT UNIQUE,
    FOREIGN KEY (passport_id) REFERENCES passport(id)
);

INSERT INTO passport(code) VALUES('KOR123456'), ('USA987654');
INSERT INTO traveler(full_name, age, passport_id)
VALUES ('김태희', 29, 1), ('John Doe', 35, 2);

2) 엔티티 정의

public class Passport {
    private Integer id;
    private String code;
    // getter/setter 생략
}

public class Traveler {
    private Integer id;
    private String fullName;
    private Integer age;
    private Passport passport;
    // getter/setter 생략
}

3) 매퍼 XML

<!-- PassportMapper.xml -->
<mapper namespace="demo.mapper.PassportMapper">
  <select id="selectPassportById" resultType="demo.entity.Passport">
    SELECT * FROM passport WHERE id = #{id}
  </select>
</mapper>

<!-- TravelerMapper.xml -->
<mapper namespace="demo.mapper.TravelerMapper">
  <resultMap id="travelerPassportMap" type="demo.entity.Traveler">
    <id     column="id"         property="id"/>
    <result column="full_name"  property="fullName"/>
    <result column="age"        property="age"/>
    <association property="passport"
                 column="passport_id"
                 javaType="demo.entity.Passport"
                 select="demo.mapper.PassportMapper.selectPassportById"/>
  </resultMap>

  <select id="selectTravelerWithPassport" resultMap="travelerPassportMap">
    SELECT * FROM traveler WHERE id = #{id}
  </select>
</mapper>

실습 2 – 일대다 조회(<collection>)

1) 스키마

CREATE TABLE department (
    id   INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50)
);

CREATE TABLE employee (
    id      INT AUTO_INCREMENT PRIMARY KEY,
    emp_no  VARCHAR(10),
    dept_id INT,
    FOREIGN KEY (dept_id) REFERENCES department(id)
);

INSERT INTO department(name) VALUES ('개발팀'), ('영업팀');
INSERT INTO employee(emp_no, dept_id)
VALUES ('E001', 1), ('E002', 1), ('E003', 2);

2) 엔티티

public class Employee {
    private Integer id;
    private String empNo;
}

public class Department {
    private Integer id;
    private String name;
    private List<Employee> employees;
}

3) 매퍼

<mapper namespace="demo.mapper.DepartmentMapper">
  <resultMap id="deptEmpMap" type="demo.entity.Department">
    <id     column="id"   property="id"/>
    <result column="name" property="name"/>
    <collection property="employees" ofType="demo.entity.Employee">
      <id     column="emp_id"  property="id"/>
      <result column="emp_no"  property="empNo"/>
    </collection>
  </resultMap>

  <select id="selectDepartmentWithEmployees" resultMap="deptEmpMap">
    SELECT d.id, d.name,
           e.id   AS emp_id,
           e.emp_no
    FROM department d
    LEFT JOIN employee e ON d.id = e.dept_id
    WHERE d.id = #{id}
  </select>
</mapper>

실습 3 – 다대다 조회(중간 테이블 활용)

1) 스키마

CREATE TABLE course (
    id   INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(100)
);

CREATE TABLE student (
    id   INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50)
);

CREATE TABLE enrollment (
    student_id INT,
    course_id  INT,
    PRIMARY KEY (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES student(id),
    FOREIGN KEY (course_id)  REFERENCES course(id)
);

INSERT INTO course(title) VALUES ('Java'), ('Spring'), ('DB');
INSERT INTO student(name) VALUES ('Alice'), ('Bob');
INSERT INTO enrollment VALUES (1,1), (1,2), (2,2), (2,3);

2) 엔티티

public class Course {
    private Integer id;
    private String title;
    private List<Student> students;
}

public class Student {
    private Integer id;
    private String name;
    private List<Course> courses;
}

3) 매퍼 – 중첩 쿼리 방식

<!-- CourseMapper.xml -->
<mapper namespace="demo.mapper.CourseMapper">
  <resultMap id="courseStudentMap" type="demo.entity.Course">
    <id     column="id"    property="id"/>
    <result column="title" property="title"/>
    <collection property="students"
                column="id"
                ofType="demo.entity.Student"
                select="demo.mapper.StudentMapper.selectStudentsByCourse"/>
  </resultMap>

  <select id="selectCourseWithStudents" resultMap="courseStudentMap">
    SELECT * FROM course WHERE id = #{id}
  </select>
</mapper>

<!-- StudentMapper.xml -->
<select id="selectStudentsByCourse" resultType="demo.entity.Student">
  SELECT s.*
  FROM student s
  JOIN enrollment e ON s.id = e.student_id
  WHERE e.course_id = #{id}
</select>

4) 매퍼 – 중첩 결과 방식

<select id="selectCourseWithStudents2" resultMap="courseStudentMap2">
  SELECT c.id, c.title,
         s.id   AS stu_id,
         s.name AS stu_name
  FROM course c
  JOIN enrollment e ON c.id = e.course_id
  JOIN student s    ON s.id = e.student_id
  WHERE c.id = #{id}
</select>

<resultMap id="courseStudentMap2" type="demo.entity.Course">
  <id     column="id"    property="id"/>
  <result column="title" property="title"/>
  <collection property="students" ofType="demo.entity.Student">
    <id     column="stu_id"   property="id"/>
    <result column="stu_name" property="name"/>
  </collection>
</resultMap>

정리

MyBatis는 <association>으로 일대일, <collection>으로 일대다/다대다를 처리하며, 중첩 쿼리와 중첩 결과 두 가지 전략을 제공합니다. 실무에서는 조회 성능과 쿼리 복잡도를 고려해 적절한 방식을 선택하면 됩니다.

태그: MyBatis association Collection one-to-one one-to-many

8월 22일 06:03에 게시됨