[JPA] @ManyToOne, @OneToMany 뭘 써야할까?
Spring

[JPA] @ManyToOne, @OneToMany 뭘 써야할까?

728x90

@OneToMany로 속박하는 바루스 궁

전에 Spring Data JPA를 사용할 때의 N+1문제를 다루면서 연관관계 엔티티에 대해서 이야기를 했었습니다!

 

 JPA에서는 A엔티티, B엔티티 연관성(1:1, N:1, N:N)을 명시해주면, 연관된 엔티티를 저장하거나 조회하는 것이 가능합니다. 대표적으로 다음의 어노테이션을 쓸 수 있죠!

  • @OneToOne (1:1)
  • @OneToMany (1:N)
  • @ManyToOne (N:1)
  • @ManyToMany  (N:N)

우선 그럼 오늘의 주인공인 일대다 연관성을 나타내는 @OneToMany, @ManyToOne 어노테이션부터 알아볼까요?


@OneToMany,  @ManyToOne 예제

교실과 학생 예제를 통해서 나타내보겠습니다. 보통 하나의 교실에 여러명의 학생이 속해있겠죠!

Tip) @OneToMany,  @ManyToOne 어노테이션에서 첫번째 단어(One 또는 Many)는 항상 현재 엔티티를 명시합니다. 예를 들어 현재 엔티티에서 @OneToMany로 맵핑된 연관 엔티티가 있다면, 앞에 One은 현재 엔티티(현재 클래스)를 나타내고 Many는 연관 엔티티를 뜻합니다.

@OneToMany 단방향

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

  private String classroomName;

  @OneToMany
  @JoinColumn(name = "classroom_id")
  private List<Student> studentList;
}
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String studentName;
}

@ManyToOne 단방향

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

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

  private String studentName;

  @ManyToOne
  @JoinColumn(name = "classroom_id")
  private Classroom classroom;
}

양방향

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

  private String classroomName;

  @OneToMany(mappedBy = "classroom")
  private List<Student> studentList;
}
@Entity
public class Student {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String studentName;

  @ManyToOne
  @JoinColumn(name = "classroom_id")
  private Classroom classroom;
}

이런식으로 엔티티의 관계를 명시해주면 엔티티는 테이블이 이런 관계라는 것을 예상합니다.

관계라는건 한쪽에만 명시해줘도 되는거아닌가요..? 꼭 두 엔티티 모두에 서로의 관계를 명시를 해줘야하나요..?

이것이 오늘의 핵심 주제입니다. 둘 중 한곳에서만 명시를 해줘도 됩니다. 자 그럼 이제부터 메인 주제에 대해 들어가보겠습니다!


단방향,  양방향 맵핑

DB 에서는 여러 테이블의 데이터를 조회시 외래키와 join 쿼리를 사용할 수 있고, 그럴 경우 테이블 자체가 하나처럼 다뤄지기 때문에 단방향, 양방향을 나눌 필요가 없습니다. 하지만 엔티티(하나의 테이블과 맵핑된 객체)는 자기 내부에 연관 엔티티(다른 테이블 데이터)를 변수로 명시해주지 않으면 사용할 수 없기 때문에 맵핑을 해주어야하고, 어떤 엔티티 내부에 어떤 엔티티가 변수로 존재하냐에 따라 단방향, 양방향 맵핑으로 나누어집니다.

위에서 말했던 것처럼 연관관계는 한쪽에서만 명시를 해줘도 됩니다. 한쪽에서만 명시를 해준 경우 경우 단방향 맵핑, 두 객체 모두에서 명시를 해줄 경우 양방향 맵핑이라고 합니다!

[주의] 엄밀히 말하면, 이 경우에도 양방향이 아니라 서로 다른 방향의 단방향 관계로 맺어진 상태라고 하는게 정확합니다. 왜냐하면 결국 한 엔티티에서는 자기 내부에  연관맵핑을 시켜준 엔티티만 참조할 수 있고, 반대의 경우를 사용하려면 다른 엔티티를 사용해야하기 때문이죠. 하지만 통상적으로는 이 경우 양방향 맵핑이라고 합니다. 

 

그럼 단방향에서 둘 중 하나만 쓰는거면... 뭘써야되죠?? 

이것은 비즈니스 로직에 따라서 달라지게 됩니다. 위에 있는 Classroom, Student를 예제로 보겠습니다.

  • @ManyToOne: 만약 비즈니스 로직에서 학생이 메인이 되어서, 학생 객체를 주로 사용하고 부가적으로 그 학생이 어떤 교실에 속해있는지 조회해야하는 로직이 쓰인다면 Student에 Classroom을 @ManyToOne으로 맵핑하여 사용하는게 편하겠죠! 
  • @OneToMany (단방향 추천하지 않음 - 이유를 꼭 봐주세요!! ): 반대로 교실이 메인이 되어서,  교실 객체를 주로 사용하고 부가적으로 어떤 학생들이 해당 교실에 속해 있는지 조회해야한다면 Classroom에 Student 리스트를 @OneToMany로 맵핑하여 사용하면 훨씬 편할겁니다.

뭐야 그럼 무조건 양방향 관계로 해놓으면 마음 편하겠네요?

비즈니스 로직을 고려할때는 편하겠지만, 단방향 맵핑만 해도 충분히 연관관계가 형성될 수 있어서 항상 양방향으로 하게 되면 쓸데없이 코드의 복잡도가 올라가게 됩니다. 따라서 필요에 따라 양방향이 필요할때만 양방향 맵핑을 해주는게 좋습니다. 하지만.. 제 개인적인 경험으로는 여기같은 위험이 있기때문에 코드복잡도가 올라가더라도 양방향을 사용하는 것이 더 좋은것 같습니다.


연관 관계의 주인 (Owner)

양방향 관계를 생각했을때 저장 또는 수정 시 조금 의문이 드실 수도 있습니다. 왜냐하면 두 엔티티 모두 내부에 연관 엔티티가 맵핑되어있기 때문인데요. 어느 엔티티를 기준으로 저장을 해야되는지 고민이 될 수가 있을것같습니다. 한번 봐볼까요?

 

만약 교실A에 학생1, 학생2, 학생3을 맵핑하여 저장하려면 어떻게 해야할까요?

  • (옵션1) Classroom 객체에서 getStudentList()로 비어있는 학생리스트를 불러온 후, 학생1, 학생2, 학생3 개체를 추가하여 Classroom 객체를 저장한다.
  • (옵션2) Classroom 객체 먼저 저장. 저장된 Classroom 객체를 학생1, 학생2, 학생3 객체 내부에 각각 등록하여 학생1,2,3을 저장한다

외에도 다른 옵션이 있겠지만, 더 깊게 들어간다면 자기 내부에 들어가는 객체 내부에 들어가는 객체 내부에 들어가는 ..... 에는 어떤 객체를 맵핑해야하죠? 라는 의문이 생기겠죠.

인셉션 - 자기가 아직 꿈 내부에 있는지 판단하기 위한 팽이

이같은 문제로 양방향에서는 연관관계의 주인을 지정해주어야 합니다. 연관관계의 주인(Owner)이란 실질적으로 외래키를 관리하는 객체라고 생각하시면 됩니다.

 

연관관계의 주인(Owner)은 두 객체 사이에서 조회, 저장, 수정, 삭제를 할 수 있지만, 연관관계의 주인이 아니면 조회만 가능합니다.

주의) 이는 Cascade(영속성 전이) 옵션을 사용하지 않았을 때의 기준입니다. Cascade 옵션은 다른 글에서 다루도록 하겠습니다!

 

누가 주인(Owner)인지 어떻게 알아요?

연관 관계의 주인이 아닌 객체에서는 mappedBy 옵션을 사용해서 연관관계 맵핑을 합니다. 따라서 객체 내부에 mappedBy가 보인다면 그 객체는 연관관계에서 주인이 아닙니다. 위에서 양방향 예제를 보시면 Classroom 객체에서 @OneToMany 어노테이션 내부에 mappedBy 옵션을 보실 수 있습니다. 따라서 위 예제에서는 Student 객체가 연관관계의 주인입니다!

 

그럼 주인에만 값을 넣어주고 제어하면 되나요?

물론 연관관계 주인만 DB테이블에 영향을 주지만, 객체 관점에서는 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전합니다. 왜냐하면 두 객체는 모두 이용하기 위해서는 동기화가 되어있어야 하기 때문입니다.


어떻게 보면 JPA를 쓸 때 가장 자주쓰는 어노테이션 중 하나인데, 구글링으로만 해결하다보니 너무 모르는채로 사용했다는 생각이 드네요 ㅠㅡㅠ

 

읽어주셔서 감사합니다!

 

출처:
- [JPA] 양방향 관계 매핑 @OneToMany, @ManyToOne 그리고 mappedby
- JPA 연관 관계 한방에 정리 (단방향/양방향, 연관 관계의 주인, 일대일, 다대일, 일대다, 다대다)
- [JPA] 연관관계 매핑 기초(단방향/양방향)
728x90