관계형 데이터베이스의 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>으로 일대다/다대다를 처리하며, 중첩 쿼리와 중첩 결과 두 가지 전략을 제공합니다. 실무에서는 조회 성능과 쿼리 복잡도를 고려해 적절한 방식을 선택하면 됩니다.