[Spring] 양방향 연관관계 주인

hyewon jeong·2023년 6월 5일
0

Spring

목록 보기
44/59

양방향 연관관계 경우

연관관계 주인을 나타내는 @OneToMany에 mappedBy 속성은 왜 있을까요?

엄밀히 이야기하면 객체에는 양방향 연관관계라는 것이 없습니다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐입니다. 반면에 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 조인할 수 있습니다. 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺습니다.

객체 연관관계 = 2개

회원 ➡️ 팀 : 연관관계 1개(단방향)
팀 ➡️ 회원 : 연관관계 1개(단방향)
테이블 연관관계 = 1개
회원 ↔️ 팀 : 연관관계 1개(양방향)

엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나입니다. 따라서 둘 사이에 차이가 발생합니다. 그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야 할까요?
이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 합니다.

양방향 매핑의 규칙: 연관관계의 주인

양방향 연관관계 매핑 시 지켜야할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 합니다.

  1. @ManyToOne , @OneToMany 의 경우 다 쪽이 외래키를 가지며 , 외래키를 가진 쪽이 연관관계의 주인이 돤다.
    • 연관관계의 주인은 mappedBy 속성을 사용하지 않는다.
    • 연관관계 주인이 아닌 것은 mapppedBy 속성을 통해 연관관계 주인을 나타낸다.
      • 예:
@OneToMany(MappedBy = “team”). //주인인 [Member.team](http://Member.team) 을 말하는 것임
List<Member> members = new ArrayList<>();
  1. 연관관계의 주인이 데이터베이스의 연관관계와 매핑이 되고, 외래키의 관리( 등록, 수정 , 삭제 ) 등을 할 수 있다. 반대편은 읽기만 가능하고 외래키 변경 불가하다.

    그렇기 때문에 아래와 같이 ERD를 보면 연관관계 주인이 아니기 때문에 team테이블에는 외래키인 List members 업데이트 되지 않은 것이다.

순수객체까지 고려하여 양방향 연관관계 맺기

JPA에서 연관 관계에 대한 관리는 일반적으로 연관 관계의 주인(Owner) 쪽에서 수행됩니다. 연관 관계의 주인이란 외래 키(Foreign Key)를 가지고 있는 쪽을 말합니다. 연관 관계 주인 쪽에서 값을 등록하거나 수정하면, JPA는 해당 변경을 데이터베이스에 반영합니다.

반면, 연관 관계의 반대편은 읽기 전용입니다. 즉, 연관 관계의 반대편은 연관 관계 주인 쪽에서 값을 등록해주기만 하면 됩니다. 연관 관계의 반대편은 변경이 불가능하며, JPA는 해당 변경을 데이터베이스에 반영하지 않습니다. 연관 관계의 반대편은 양방향 연관 관계에서 주로 조회를 위해 사용됩니다.

하지만 순수 객체에서는 양쪽 객체 모두에게 값을 등록해주어야 하는 경우가 있습니다. 이는 순수 객체에서는 데이터베이스의 제약 조건이나 외래 키 관계 등을 고려하지 않기 때문입니다. 양쪽 객체 모두에게 값을 등록해야 양방향 연관 관계가 제대로 설정되어 객체 간의 일관성이 유지됩니다.

  • 즉 양쪽 다 연관관계를 설정하자!
  • 연관관계 편의 메서드 : 한번에 양쪽연관관계를 설정하는 메서드
    이부분에 대해 양방향 연관관계와 연관관계의 주인 자세히 설명되어 있으니 참고하자.

Member.class

package study.querydsl3.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id","username","age"})
public class Member {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String username;

  private int age;
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "team_id")
  private Team team;


  public Member(String username, int age){
    this(username,age,null);
  }

  public Member(String username){
    this(username,0);
  }

  public Member(String username, int age, Team team) {
    this.username = username;
    this.age = age;
    change(team);
  }

  public Member(Long id, String username, int age, Team team){

  }
  
//---------연관관계 편의 메서드 ---------------------
  private void change(Team team) {
    if(team != null){
        team.getMembers().remove(this); // 기존 팀에서 멤버 제거 후
        this.team = team;
        team.getMembers().add(this); // 변경된 팀에 멤버 추가
    }
  }
}

Team.class

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {

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

  private String name;
  @OneToMany(mappedBy = "team")
  private List<Member> members = new ArrayList<>();

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


}
profile
개발자꿈나무

0개의 댓글