Files
spring-boot-examples/docs/jpa/03_관계 매핑.md
2025-04-08 19:56:24 +09:00

7.1 KiB

아래는 "관계 매핑"에 대해 예시와 함께 설명하는 글입니다. 롬복을 사용해 코드를 간결하게 작성했습니다.


관계 매핑

JPA에서 관계 매핑(Relationship Mapping)은 엔티티 간의 연관성을 정의하는 방법으로, 객체지향 프로그래밍과 관계형 데이터베이스의 다리를 잇는 핵심 기능입니다. 데이터베이스에서는 외래 키(Foreign Key)를 통해 테이블 간 관계를 표현하지만, JPA에서는 어노테이션(@OneToMany, @ManyToOne 등)을 사용해 객체 간 관계를 자연스럽게 매핑합니다. 이를 통해 개발자는 SQL 조인을 직접 작성하지 않고도 연관된 데이터를 쉽게 조회하고 관리할 수 있습니다.

관계 매핑의 주요 유형

JPA는 네 가지 주요 관계 매핑을 지원합니다:

  1. 1:1 (One-to-One): 한 엔티티가 다른 엔티티와 단일 관계를 가짐.
  2. 1:N (One-to-Many): 한 엔티티가 여러 엔티티와 관계를 가짐.
  3. N:1 (Many-to-One): 여러 엔티티가 한 엔티티와 관계를 가짐.
  4. N:M (Many-to-Many): 여러 엔티티가 서로 여러 엔티티와 관계를 가짐.

주요 어노테이션

  • @OneToMany: 1:N 관계를 정의.
  • @ManyToOne: N:1 관계를 정의.
  • @OneToOne: 1:1 관계를 정의.
  • @ManyToMany: N:M 관계를 정의.
  • @JoinColumn: 외래 키 컬럼을 지정.

예시 1: 1:N과 N:1 관계 (팀과 회원)

아래는 TeamMember 엔티티 간의 1:N 및 N:1 관계를 보여주는 예시입니다.

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Column;
import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;

    @Column(name = "team_name")
    private String name;

    @OneToMany(mappedBy = "team") // 1:N 관계, Member의 team 필드에 의해 매핑
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Column;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    @Column(name = "username")
    private String name;

    @ManyToOne // N:1 관계
    @JoinColumn(name = "team_id") // 외래 키 컬럼 지정
    private Team team;

    public Member(String name, Team team) {
        this.name = name;
        this.team = team;
    }
}

코드 설명

  1. Team (1:N)

    • @OneToMany(mappedBy = "team"): Team은 여러 Member를 가질 수 있으며, 관계의 주인은 Memberteam 필드입니다. mappedBy는 양방향 매핑에서 사용되며, 외래 키를 직접 관리하지 않음을 나타냅니다.
  2. Member (N:1)

    • @ManyToOne: Member는 하나의 Team에 속합니다.
    • @JoinColumn(name = "team_id"): Member 테이블에 team_id라는 외래 키 컬럼이 생성됩니다.

매핑된 테이블 구조

  • Team 테이블:
    CREATE TABLE team (
        team_id BIGINT PRIMARY KEY AUTO_INCREMENT,
        team_name VARCHAR(255)
    );
    
  • Member 테이블:
    CREATE TABLE member (
        member_id BIGINT PRIMARY KEY AUTO_INCREMENT,
        username VARCHAR(255),
        team_id BIGINT,
        FOREIGN KEY (team_id) REFERENCES team(team_id)
    );
    

동작 방식

  • Team team = new Team("개발팀");
  • Member member = new Member("홍길동", team);
  • team.getMembers().add(member);
  • 저장 시 Member 테이블에 team_idTeamteam_id를 참조하는 레코드가 삽입됩니다.

예시 2: N:M 관계 (학생과 강의)

N:M 관계는 중간 테이블을 통해 구현됩니다.

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Column;
import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "student_id")
    private Long id;

    @Column(name = "student_name")
    private String name;

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

    public Student(String name) {
        this.name = name;
    }
}
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Column;
import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "course_id")
    private Long id;

    @Column(name = "course_name")
    private String name;

    @ManyToMany(mappedBy = "courses")
    private List<Student> students = new ArrayList<>();

    public Course(String name) {
        this.name = name;
    }
}

코드 설명

  • @ManyToMany: StudentCourse는 다대다 관계입니다.
  • JPA는 자동으로 중간 테이블(예: student_course)을 생성해 외래 키를 관리합니다.

매핑된 테이블 구조

  • student_course (중간 테이블):
    CREATE TABLE student_course (
        student_id BIGINT,
        course_id BIGINT,
        FOREIGN KEY (student_id) REFERENCES student(student_id),
        FOREIGN KEY (course_id) REFERENCES course(course_id)
    );
    

주의사항

  • 양방향 매핑: mappedBy를 사용해 관계의 주인을 명확히 지정해야 중복 매핑을 방지합니다.
  • 지연 로딩(Lazy Loading): @OneToMany@ManyToMany는 기본적으로 지연 로딩이 적용되며, 필요 시 fetch = FetchType.EAGER로 즉시 로딩을 설정할 수 있습니다.
  • N:M의 한계: 실무에서는 N:M 대신 중간 엔티티를 만들어 1:N, N:1로 분리하는 경우가 많습니다.

관계 매핑은 JPA의 강력한 기능으로, 객체 간 관계를 데이터베이스에 자연스럽게 반영합니다. 다음 장에서는 성능 최적화를 위한 페치 전략과 N+1 문제를 다뤄보겠습니다.


책의 흐름에 맞는지, 추가 예시나 설명이 필요하면 말씀해 주세요!