26/07/23 IL(I Learned) - MyBatis 3

Let's take a break·2026년 7월 27일

다대다(M:N) 관계와 중간 테이블을 이용한 객체 매핑

이번 MyBatis3 프로젝트에서는 이전 MyBatis2 프로젝트에서 학습했던 1:N(회원-주문) 관계를 확장하여 학생(Student)과 강의(Course)의 다대다(M:N) 관계를 구현하였다.

기존 프로젝트에서는 하나의 회원이 여러 주문을 가지는 구조였기 때문에 하나의 객체가 다른 객체의 리스트를 가지는 형태만 구현하면 되었다. 하지만 이번 프로젝트에서는 학생은 여러 강의를 수강할 수 있고, 하나의 강의 역시 여러 학생이 수강할 수 있기 때문에 중간 테이블(Enrollment)을 이용한 관계 표현을 새롭게 학습하였다.

이를 통해 관계형 데이터베이스의 M:N 관계를 Java 객체에서는 어떻게 표현하는지 이해할 수 있었다.


학생과 강의 등록 기능

학생과 강의를 등록하는 구조는 기존 프로젝트와 크게 다르지 않다.

Student 등록

@PostMapping("/students")
public String addStudent(@ModelAttribute Student student) {
    enrollService.createStudent(student);
    return "redirect:/";
}

Course 등록

@PostMapping("/courses")
public String addCourse(@ModelAttribute Course course) {
    enrollService.createCourse(course);
    return "redirect:/";
}

학생과 강의를 등록하는 방식은 이전 프로젝트의 Member, Order 등록과 동일하게 Controller → Service → Mapper 순으로 동작한다.

Controller는 사용자의 요청을 받아 Service에 전달하고, Service는 Mapper를 호출하여 데이터베이스에 저장한다. 이를 통해 MVC 구조는 그대로 유지하면서도 새로운 도메인(Student, Course)으로 확장할 수 있다는 점을 확인하였다.


수강 신청(Enrollment) 기능 구현

이번 프로젝트에서 가장 새롭게 배운 기능은 학생과 강의를 연결하는 수강 신청 기능이다.

Controller

@PostMapping("/enrollments")
public String addEnrollment(
        @RequestParam Long studentId,
        @RequestParam Long courseId) {

    Student student = new Student();
    student.setId(studentId);

    Course course = new Course();
    course.setId(courseId);

    enrollService.createEnrollment(student, course);

    return "redirect:/";
}

사용자로부터 전달되는 값은

  • studentId
  • courseId

두 개의 기본키이다.

하지만 Mapper에서는 단순한 숫자가 아니라 Student 객체와 Course 객체를 전달받는다.

따라서 Controller에서는

Student student = new Student();
student.setId(studentId);

Course course = new Course();
course.setId(courseId);

와 같이 객체를 생성한 뒤 id만 설정하여 Service에 전달한다.

이 과정은 이전 프로젝트에서 Order 객체 안에 Member 객체를 저장했던 방식과 유사하지만, 이번에는 두 개의 객체를 동시에 전달하여 중간 테이블에 저장한다는 점이 가장 큰 차이점이다.


EnrollMapper의 확장

기존 프로젝트의 Mapper는 CRUD와 1:N 관계 조회 정도의 기능만 담당하였다.

이번 프로젝트에서는 Mapper가 학생, 강의, 수강신청, 다대다 조회까지 담당하도록 확장되었다.

@Mapper
public interface EnrollMapper {

    void insertStudent(Student student);

    void insertCourse(Course course);

    void insertEnrollment(Student student, Course course);

    List<StudentWithCoursesDTO> findAllStudentsWithCourses();

    List<CourseWithStudentsDTO> findAllCoursesWithStudents();
}

기존에는 하나의 객체를 기준으로 조회하였다면, 이번 프로젝트에서는

  • 학생 기준 조회
  • 강의 기준 조회

두 방향 모두 조회할 수 있도록 Mapper가 구성되었다.

이를 통해 하나의 관계를 다양한 관점에서 조회하는 방법을 학습하였다.


StudentWithCoursesDTO

이번 프로젝트에서는 학생이 수강 중인 강의를 저장하기 위한 DTO가 추가되었다.

public class StudentWithCoursesDTO {

    private Long id;

    private String name;

    private List<Course> courses = new ArrayList<>();
}

이 DTO는 학생 한 명이 여러 강의를 수강하는 관계를 표현한다.

예를 들어

김학생

├── Java

├── Database

└── Spring

처럼 하나의 Student 객체 안에 여러 Course 객체를 저장할 수 있다.

이전 프로젝트에서는 Member가 Order 목록을 가지는 구조였다면, 이번에는 Student가 Course 목록을 가진다는 점이 달라졌다.


CourseWithStudentsDTO

반대로 강의를 기준으로도 학생 목록을 조회할 수 있도록 DTO를 작성하였다.

public class CourseWithStudentsDTO {

    private Long id;

    private String name;

    private List<Student> students = new ArrayList<>();
}

이 DTO는 하나의 강의를 여러 학생이 수강하는 관계를 표현한다.

예를 들어

Spring

├── 김학생

├── 이학생

└── 박학생

처럼 하나의 Course 객체가 여러 Student 객체를 포함하게 된다.

기존 프로젝트에서는 한 방향의 관계만 표현했지만, 이번 프로젝트에서는 학생 기준과 강의 기준 양방향 조회를 구현하면서 관계 매핑에 대한 이해를 넓힐 수 있었다.


Service 계층의 변화

Service 역시 기존보다 기능이 확장되었다.

public List<StudentWithCoursesDTO> findAllStudentsWithCourses() {
    return enrollMapper.findAllStudentsWithCourses();
}

public List<CourseWithStudentsDTO> findAllCoursesWithStudents() {
    return enrollMapper.findAllCoursesWithStudents();
}

기존에는 Member와 Order의 관계만 조회하였다면, 이번에는

  • 학생 → 강의 목록
  • 강의 → 학생 목록

두 가지 조회 기능을 모두 Service에서 제공한다.

이를 통해 하나의 관계를 다양한 관점에서 활용할 수 있는 구조를 구현하였다.


MainController에서 확인한 프로젝트 흐름

MainController에서는 학생, 강의, 학생별 수강 목록, 강의별 수강 학생 목록을 모두 Model에 담아 View로 전달한다.

model.addAttribute("students", enrollService.findAllStudents());
model.addAttribute("courses", enrollService.findAllCourses());
model.addAttribute("studentWithCourses", enrollService.findAllStudentsWithCourses());
model.addAttribute("courseWithStudents", enrollService.findAllCoursesWithStudents());

이전 프로젝트에서는 하나의 관계 조회만 수행하였다면, 이번 프로젝트에서는 같은 데이터를 학생 중심과 강의 중심으로 각각 조회하여 화면에 출력하였다. 이를 통해 MyBatis의 resultMap을 활용한 다양한 객체 매핑 방식을 경험할 수 있었다.


새롭게 배운 점

이전 MyBatis2 프로젝트MyBatis3 프로젝트
1:N(Member-Order) 관계M:N(Student-Course) 관계
하나의 관계 조회양방향 관계 조회
MemberWithOrdersDTOStudentWithCoursesDTO, CourseWithStudentsDTO
FK를 객체로 표현중간 테이블을 이용한 객체 연결
JOIN 결과 한 방향 매핑JOIN 결과를 두 방향으로 매핑

느낀 점

이번 프로젝트에서는 단순히 데이터를 조회하는 수준을 넘어 다대다(M:N) 관계를 객체지향적으로 표현하는 방법을 학습하였다. 특히 학생과 강의 사이에 직접 관계를 맺는 것이 아니라 중간 테이블을 통해 연결한다는 점과, 이를 Java에서는 객체와 DTO를 이용해 자연스럽게 표현할 수 있다는 점이 인상적이었다.

또한 StudentWithCoursesDTOCourseWithStudentsDTO를 각각 구현하면서 하나의 관계를 학생 중심과 강의 중심 두 가지 관점으로 조회할 수 있는 구조를 경험하였다. 이전 프로젝트에서 학습한 1:N 관계를 기반으로 M:N 관계까지 확장해 보면서 MyBatis의 resultMap과 객체 매핑 원리를 한 단계 더 깊게 이해할 수 있었다.

0개의 댓글