[F-Lab 모각코 챌린지 57일차] 다대다 연관관계 테이블 구성하기

부추·2023년 7월 27일
0

F-Lab 모각코 챌린지

목록 보기
57/66

# 초간단 JPA 연관관계

JPA에는 일대다 연관관계를 구성하기 위한 @ManyToOne, @OneToMany 어노테이션이 있다. 필드 값에 해당 어노테이션을 붙이고 타입을 collection으로 하면, ORM 측에서 collection(여기서 List라고 하겠다.)의 원소에 DB의 entity값을 매핑해준다.

예시라 하기에도 민망한 기본 코드지만.. 일대다 관계인 Parent : Child는 아래와 같이 구성할 수 있다.

@Entity
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "parent", 
    			cascade = CascadeType.ALL, 
                fetch = FetchType.LAZY)
    private List<Child> children = new ArrayList<>();

    // Constructors, getters, setters, etc.
}

@Entity
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Parent parent;

    // Constructors, getters, setters, etc.
}

@JoinColumn은 many측인 Child에서 fk를 보관할 필드와 column이름을 지정할 수 있도록 해주는 어노테이션이다. 이대로 hibernate가 동작하게 하면 child 테이블의 "parent_id" 컬럼에 parent의 pk가 저장된다.

mappedBy는 연관관계의 주인이 아닌 엔티티, 그러니까 fk를 가지고 있지 않은 Parent측에서 연관관계의 주인 엔티티의 어느 필드에 연관을 맺고있는지 명시하는 기능을 한다. 만약 ParentChild가 위 코드의 일대다 관계 이외에 어떤 관계를 하나 더 갖고있다면 그를 구분해주는 과정이 필요한데, 그때 mappedBy 속성이 쓰인다.

cascade는 영속성 전이 설정인데, 하나의 @Transactional 단위에서 관리되는 연관 영속성 객체들의 행위를 어떻게 전파할지 결정하는 역할을 한다. CascadeType 내의 ALL, PERSIST, REMOVE 등이 있다. 특정 객체를 persist 상태로 설정하는 동작이 수행되었을 경우(save 등), CascadeType.PERSIS가 적용되었다면, 트랜잭션 안에서 관리되는 연관 엔티티 역시 persist 상태로 설정되는 동작이 수행된다. 위의 예시에선 Parent 엔티티의 save동작이 일어날 때 children 리스트의 원소 Child 객체들 역시 save동작이 한꺼번에 일어난다는 뜻이다. forEach같은거 이용해서 원소 하나하나를 돌아다닐 필요가 없다는 뜻!

fetch는 해당 엔티티가 조회되었을 때, 연관 관계에 있는 엔티티 역시 한꺼번에 조회할 것인지 옵션을 설정할 수 있게 해준다. Parent 엔티티에 딸린 Child가 한 1억개가 될 수도 있다. Parent가 조회될 때마다 필요하지도 않은 1억개의 children 필드가 매번 조회된다면 엄청난 손해가 될 것이다. 이 때 FetchType.LAZY를 설정하면 children 필드에 접근하지 않는 한 연관관계의 엔티티 정보는 불러와지지 않는다.


# @ManyToMany ?

가장 직관적이고 간단한 관계가 @OneToMany + @ManyToOne이라 기본 설명은 해당 예시로 진행했다. 이외에도 JPA에는 다대다 연관관계를 구성하기 위한 @ManyToMany가 있다. 아주 간단한 예시로, 학생 엔티티 Student와 학생이 듣는 수업 엔티티 Course를 생각해보자. 한 명의 학생은 여러 개의 수업을 들을 수 있고, 한 개의 수업 역시 여러 명의 학생이 들을 수 있다. 여러 명의 학생이 여러 개의 수업을 들으므로, 두 엔티티의 연관 관계는 다대다라고 할 수 있다.

일반적으로 RDB는 다대다 연관관계를 "relation table"로 표현한다. 일대다 연관 관계에서 "다" 측의 테이블에 fk를 두는 방법이 아니라, 두 엔티티의 연관 관계 정보만을 담은 제 3의 테이블을 따로 두는 것이다.

JPA에는 기본적으로 이를 위한 어노테이션을 제공한다. @ManyToMany이다. 앞선 학생-수업 예시를 엔티티 코드로 나타내보자.

@Entity
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 = new ArrayList<>();

    // Constructors, getters, setters, etc.
}

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

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

    // Constructors, getters, setters, etc.
}

@ManyToMany는 어노테이션의 configuration에 따라 relation 테이블을 만들어주는 역할을 한다. @JoinTable을 봐야한다.

  • name으로 연관 테이블의 이름을 설정했다.
  • joinColumns로 현재 엔티티의 pk값을 참조할 relation 테이블의 fk 컬럼명을,
  • inverseJoinColumns로 연관 엔티티의 pk값을 참조할 relation 테이블의 fk 컬럼명을 설정했다.
  • 마지막으로 연관 관계에 있는 Course 엔티티의 mappedBy로 엔티티의 필드값을 매핑했다.

이렇게 되면, "student_course"라는 이름을 가진 제3의 테이블이 새로 생성된다. StudentParent 쪽에서 가진 List 객체에 원소를 더하거나 뺀 뒤 트랜잭션을 완료하면 JPA가 알아서 relation table에 관계 정보를 추가해준다.


# @OneToMany 두개 쓰세요

그. 러. 나.
relation table을 개발자가 직접 생성하고 설정해주지 못한 채 라이브러리에 숨겨져있다는건 꽤.. 아니 많이 별로다. relation table에 원하는 쿼리를 날릴 수도 없고, 해당 테이블에 추가 정보나 필드를 더하고 싶어도 할 수 없다. 그렇기 때문에 보통은 제 3의 relation table을 직접 만들고 각각의 엔티티에 @OneToMany를 붙이는 방법을 사용할 수 있다.

원래 Student, Course@ManyToMany@OneToMany로 바꿔보자.

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "student")
    private List<StudentCourse> studentCourses = new ArrayList<>();

    // Constructors, getters, setters, etc.
}

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "course")
    private List<StudentCourse> studentCourses = new ArrayList<>();

    // Constructors, getters, setters, etc.
}

StudentCourse라는 타입의 엔티티를 리스트 필드로 갖고있다. 해당 엔티티 클래스는 이렇게 생겼다.

@Entity
public class StudentCourse {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "student_id")
    private Student student;

    @ManyToOne
    @JoinColumn(name = "course_id")
    private Course course;

    // Constructors, getters, setters, etc.
}

두 개의 fk를 갖고있다는 점을 제외하고는 처음 봤던 @OneToMany, @ManyToOne과 같은 구조이다. 이제 연관 관계 테이블을 개발자가 자유롭게 수정하고 설정할 수 있다.


# 양방향? 단방향?

정답은 없는듯. 연관 관계를 맺고 있는 어느 쪽의 엔티티에서도 어노테이션을 이용하기만 하면 "Many"측에서 fk 컬럼이 생성되긴 한다. 단방향이 맞다고 주장하는 사람도, 양방향이 맞다고 주장하는 사람도 있는데 역시 가장 많은건 두 쪽 모두 맞다, 상황에 따라 맞는 것을 선택하는게 좋다인듯.

many 측에서 one측을 불러올 일이 있으면 many측에 @ManyToOne필드를 두면 좋다. 반대의 경우에 @OneToMany를 두면 좋다. 뭐 사실 @ManyToOne은 거의 반필수인 것 같긴 하지만ㅋㅋ

연관 관계 설정할 때의 순환 문제 이런건 코딩하면서 잘 피하면 되는거고. 일단 프로젝트에서는 User을 제외한 모든 관계에 대해 양방향을 적용했다. 이것과 관련해서 객체지향적으로 문제가 되는 부작용이 있을 것 같은데, 그것은 멘토님과 나중에 한 번 더 상의를 해보도록 하자!


REFERENCE

https://www.nowwatersblog.com/jpa/ch6/6-4
https://velog.io/@yuseogi0218/JPA-%EB%8B%A4%EB%8C%80%EB%8B%A4-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84#manytomany-%EC%9D%98-%ED%95%9C%EA%B3%84

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글