북 스터디/스프링 부트 핵심 가이드

[스프링 부트] 09장. 연관관계 매핑

dbssk 2023. 6. 18. 22:53

이 글은 '스프링 부트 핵심 가이드 - 스프링 부트를 활용한 애플리케이션 개발 실무' 책을 통해 학습한 내용을 정리한 글입니다.

09장. 연관관계 매핑

연관관계 매핑 종류와 방향

  • One To One : 일대일(1:1)
  • One To Many : 일대다(1:N)
  • Many To One : 다대일(N:1)
  • Many To Many : 다대다(N:M)

  • 단방향 : 두 엔티티의 관계에서 한쪽의 엔티티만 참조하는 형식
  • 양방향 : 두 엔티티의 관계에서 각 엔티티가 서로의 엔티티를 참조하는 형식

JPA와 데이터베이스의 차이

  • 데이터베이스 : 두 테이블의 연관관계를 설정하면 외래키를 통해 서로 조인해서 참조하는 구조
  • JPA : 엔티티 간 참조 방향을 설정
    • 연관관계가 설정되면 한 테이블에서 다른 테이블의 기본값을 외래키로 갖게 된다. 이런 관계에서는 주인(Owner)라는 개념이 사용된다. 일반적으로 외래키를 가진 테이블이 그 관계의 주인이 되며, 주인은 외래키를 사용할 수 있으나상대 엔티티는 읽는 작업만 수행할 수 있다.

일대일 매핑

1. 단방향 매핑

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String username;
    
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "profile_id")
    private UserProfile userProfile;
}

@Entity
@Table(name = "user_profiles")
public class UserProfile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String firstName;
    private String lastName;
}
  • 'User' 엔티티는 'userProfile' 필드를 가지며, 'UserProfile' 엔티티는 'User' 엔티티와 연관되지 않은 단순한 프로필 정보를 가진다.
  • @OneToOne 어노테이션은 일대일 관계를 나타내는데 사용
    • cascade 속성을 'CascadeType.ALL'로 설정하여 'User' 엔티티가 저장되거나 삭제될 때 'UserProfile' 엔티티에도 해당 변경 사항이 전파되도록 설정
  •  @JoinColumn 어노테이션은 외래 키 컬럼을 지정하는데 사용
    • name : 매핑할 외래키의 이름 설정
    • referencedColumnName : 외래키가 참조할 상대 테이블의 칼럼명 지정
    • foreignKey : 외래키를 생성하면서 지정할 제약조건을 설정(unique, nullable, insertable, updatable 등)

2. 양방향 매핑

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String username;
    
    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
    private UserProfile userProfile;
}

@Entity
@Table(name = "user_profiles")
public class UserProfile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String firstName;
    private String lastName;
    
    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;
}
  • 'User' 엔티티는 'userProfile' 필드로 'UserProfile' 엔티티와 연결되어 있으며, 'UserProfile' 엔티티는 'user' 필드로 'User' 엔티티와 연결되어 있다.
  • 'User' 엔티티에서는 'mappedBy' 속성을 사용하여 양방향 관계의 주인을 'UserProfile' 엔티티의 'user' 필드로 지정
  • 순환참조로 인해 toString() 메서드를 호출할 때 스택오버플로우가 발생할 수 있다.
    • @ToString.Exclude 어노테이션을 'User' 엔티티의 'userProfile' 필드에 붙임으로써 해결할 수 있다.

다대일 매핑

1. 단방향 매핑

@Entity
@Table(name = "departments")
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
}

@Entity
@Table(name = "employees")
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;
}
  • @ManyToOne : 다대일 관계를 나타내는데 사용
  • @JoinColumn : 외래 키 컬럼 지정
    • 위 코드에서는 'department_id'라는 외래 키 컬럼을 'Employee' 엔티티의 'department' 필드와 매핑
  • 순환참조를 막으려면 @ToString.Exclude 추가
  • Employee 레포지토리만으로 Deparetment 객체 조회 가능

2. 양방향 매핑

@Entity
@Table(name = "departments")
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "department")
    private List<Employee> employees;
}

@Entity
@Table(name = "employees")
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;
}
  • 'Department' 엔티티는 'employees' 필드로 'Employee' 엔티티들과의 연관 관계를 가지고 있으며, 'Employee' 엔티티는 'department' 필드로 소속된 'Department' 엔티티를 참조
  • @OneToMany : 일대다 관계
  • mappedBy : 양방향 관계의 주인을 'Employee' 엔티티의 'department' 필드로 지정
  • 일대다 연관관계의 경우 여러 엔티티가 포함될 수 있으므로 컬렉션(Collections, List, Map) 형식으로 필드 생성
  • 'Department' 엔티티는 주인이 아니라서 외래 키를 관리할 수 없다.
    • 즉, Department를 등록한 후 각 Employee 객체에 설정하는 작업을 통해 DB에 저장해야 한다.
    • Department 엔티티에서 정의한 employees 필드에 Employee 객체를 추가하는 방식으로 DB에 저장하게 되면 해당 데이터는 DB에 반영되지 않는다.

일대다 매핑

1. 일대다 단방향 매핑

@Entity
@Table(name = "posts")
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String title;
}

@Entity
@Table(name = "comments")
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String content;
    
    @ManyToOne
    @JoinColumn(name = "post_id")
    private Post post;
}
  • 'Comment' 엔티티는 'Post' 엔티티에 대한 참조인 'post' 필드를 가지고 있다.
  • @ManyToOne : 다대일 관계
  • @JoinColumn : 'post_id' 라는 외래 키 컬럼을 'Comment' 엔티티의 'post' 필드와 매핑
  • 단점
    • 매핑의 주체가 아닌 반대 테이블에 외래키가 추가된다.
    • 다대일 구조와 다르게 외래키를 설정하기 위해서 다른 테이블에 대한 update 쿼리를 발생시킨다.
  • 단점을 해결하기 위해서는 다대일 연관관계를 사용하는 것이 좋다.

2. 일대다 양방향 매핑

  • 일대다 양방향 매핑의 경우 어느 엔티티 클래스도 연관관계의 주인이 될 수 없다.

다대다 매핑

  • 실무에서 거의 사용되지 않는 구성
  • 각 엔티티에서 서로를 리스트로 가지는 구조가 만들어 진다. 이런 경우에는 교차 엔티티라고 부르느 중간 테이블을 생성해서 다대다 관계를 일대다 또는 다대일 관계로 해소한다.

1. 다대다 단방향 매핑

@Entity
@Table(name = "students")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToMany
    @JoinTable(name = "student_course",
               joinColumns = @JoinColumn(name = "student_id"),
               inverseJoinColumns = @JoinColumn(name = "course_id"))
    private List<Course> courses;
}

@Entity
@Table(name = "courses")
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
}
  • @ManyToMany : 다대다 관계
  • 리스트로 필드를 가지는 객체에서는 외래키를 가지지 않기 때문에 별도의 @JoinColumn 을 설정할 필요가 없다.
  • @JoinTable(name = " ") : 중간 테이블 이름 지정
  • 중간 테이블에서는 두 테이블에서 id 값을 가져와 두 개의 외래키가 설정된다.

2. 다대다 양방향 매핑

@Entity
@Table(name = "students")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToMany
    @JoinTable(name = "student_course",
               joinColumns = @JoinColumn(name = "student_id"),
               inverseJoinColumns = @JoinColumn(name = "course_id"))
    private List<Course> courses;
}

@Entity
@Table(name = "courses")
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToMany(mappedBy = "courses")
    private List<Student> students;
}
  • @ManyToMany : 다대다 관계
  • mappedBy : 양방향 관계의 주인 설정
  • 중간 테이블이 연관관계를 설정하고 있기 때문에 DB의 테이블 구조는 변경되지 않는다.
  • 중간 테이블을 통해 연관된 엔티티의 값을 가져온다.
  • 그러나 중간 테이블 생성으로 인해 예기치 못한 쿼리가 생길 수 있으므로 관리가 힘들다.
  • 따라서, 중간 테이블 생성 대신 일대다/다대일로 연관관계를 맺을 수 있는 테이블을 생성하여 JPA 에서 관리할 수 있도록 한다.
  • @ToString.Exlucde : 순환참조자 발생하기 때문에 둘 중 한 엔티티의 필드에 어노테이션을 붙인다.

영속성 전이 (Cascade)

특정 엔티티의 영속성 상태를 변경할 때 그 엔티티와 연관된 엔티티의 영속성에도 영향을 미쳐 영속성 상태를 변경하는 것

  • ALL : 모든 영속 상태 변경에 대해 영속성 전이를 적용
  • PERSIST : 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화
  • MERGE : 엔티티를 영속성 컨텍스트에 병합할 때 연관된 엔티티도 병합
  • REMOVE : 엔티티를 제거할 때 연관된 엔티티도 제거
  • REFRESH : 엔티티를 새로고침할 때 연관된 엔티티도 새로고침
  • DETACH : 엔티티를 영속성 컨텍스트에서 제외하면 연관된 엔티티도 제외

고아 객체

  • JPA 에서 고아(orphan)란 부모 엔티티와 연관관계가 끊어진 엔티티를 의미
  • JPA 에는 고아 객체를 자동으로 제거하는 기능 존재
  • but, 자식 엔티티가 다른 엔티티와 연관관계를 가지고 있다면 고아 객체를 자동으로 제거하는 기능은 사용하지 않는 것이 좋다.
  • ex) @OneToMany(..., orphanRemoval true)