티스토리 뷰

 

양방향 연관관계와 연관관계의 주인

Member, TeamClass

@Entity
public class Member {
 
 @Id @GeneratedValue
 private Long id;
 
 @Column(name = "USERNAME")
 private String name;
 private int age;
 
 @ManyToOne
 @JoinColumn(name = "TEAM_ID")
 private Team team;
 ...
}
@Entity
public class Team {
  
 @Id @GeneratedValue
 private Long id;
 
 private String name;
 
 @OneToMany(mappedBy = "team")
 List<Member> members = new ArrayList<Member>();
 ...
}

일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용한다. @OneToMany의 mappedBy 속성은 양방향 매핑일 때 사용하는데, 반대쪽 매핑의 필드 이름을 값으로 주면 된다. 반대쪽 매핑이 Member.team이므로 team을 값으로 주었다.

양방향 매핑(반대 방향으로 객체 그래프 탐색)

//조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); //역방향 조회

연관관계의 주인

객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다. 반면 데이터베이스 테이블은 외래 키 하나로 양쪽 조인이 가능하다. 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.

 

객체 연관관계 = 2개

  • 회원 -> 팀: 연관관계 1개(단방향)
  • 팀 -> 회원: 연관관계 1개(단방향)
    테이블 연관관계 = 1개
  • 회원 <-> 팀: 연관관계 1개(양방향)

엔티티를 양방향 관계로 설정하면 객체의 참조는 2개인데 외래 키는 1개이다. 따라서 둘 사이에 차이가 발생한다.

이런 차이로 인해 JPA에서는 두 객체의 연관관계 중 하나를 정해서 테이블의 외래 키를 관리하는데 이것을 연관관계의 주인이라 한다.

회원 -> 팀(Member.team) 방향

class Member {

 @ManyToOne
 @JoinColumn(name = "TEAM_ID")
 private Team team;
 // ...
}

팀 -> 회원(Team.members) 방향

class Team {

 @OneToMany
 private List<Member> members = new ArrayList<>();
 // ...
}

연관관계의 주인을 정한다는 것은 외래 키 관리자를 선택하는 것이다.

위 그림을 보면, 여기서는 회원 테이블에 있는 TEAM_ID 외래 키를 관리할 관리자를 선택해야 한다. 

만약 회원 엔티티에 있는 Member.team을 주인으로 선택하면 자기 테이블에 있는 외래 키를 관리하면 되지만,

팀 엔티티에 있는 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다.

왜냐하면 이 경우 Team.members가 있는 Team 엔티티는 TEAM 테이블에 매핑되어 있는데, 관리해야 할 외래 키는 MEMBER에 있기 때문이다.

 

양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정(FK가 있는 쪽)
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • 주인이 아닌쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용불가
  • 주인이 아니면 mappedBy 속성으로 주인 지정

외래키가 있는 쪽을 주인으로

* 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다.

양방향 연관관계 저장

양방향 연관관계를 사용하여 팀1, 회원1, 회원2를 저장해보자.

public void save() {

 //팀1 저장
 Team team1 = new Team("team1", "팀1");
 em.persist(team1);
 
 //회원1 저장
 Member member1 = new Member("member1", "회원1");
 member.setTeam(team1); //연관관계 설정 member1 -> team1
 em.persist(member1);
 
 //회원2 저장
 Member member1 = new Member("member2", "회원2");
 member.setTeam(team1); //연관관계 설정 member2 -> team1
 em.persist(member2);
}

팀1을 저장하고 회원1, 회원2에 연관관계의 주인인 Member.team 필드를 통해서 회원과 팀의 연관관계를 설정하고 저장했다.

데이터베이스에서 회원 테이블을 조회하면 아래와 같이 나온다. (SELECT * FROM MEMBER;)

MEMBER_ID USERNAME TEAM_ID
member1 회원1 team1
member2 회원2 team1

TEAM_ID 외래 키에 팀의 기본 키 값이 저장되어 있다.

양방향 연관관계는 연관관계의 주인이 외래 키를 관리한다. 따라서 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력된다.

 

team1.getMembers().add(member1); //무시(연관관계의 주인이 아님)

team1.getMembers().add(Member2); //무시(연관관계의 주인이 아님)

Team.members는 연관관계의 주인이 아니다. 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.

양방향 연관관계의 주의점

Member member1 = new Member("member1", "회원1");
em.persist(member1);

Member member2 = new Member("member2", "회원2");
em.persist(member2);

Team team1 = new Team("team1", "팀");
team1.getMembers().add(member1);
team1.getMembers().add(member2);

em.persist(team1);

 

 

위 코드를 실행하고 데이터베이스에서 회원 테이블을 조회하면 다음과 같이 결과가 나온다.

MEMBER_ID USERNAME TEAM_ID
member1 회원1 null
member2 회원2 null

외래 키 TEAM_ID에 team1이 아닌 null 값이 입력되어 있는데, 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문이다.

연관관계의 주인만이 외래 키의 값을 변경할 수 있다.

연관관계의 주인인 Member.team에 아무 값도 입력하지 않았기 때문에, TEAM_ID 외래 키의 값도 null이 저장된다.

순수한 객체까지 고려한 양방향 연관관계

그렇다면 정말 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 될까?

사실은 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.

public void test(){
 
 Team team1 = new Team("team1", "팀1");
 Member member1 = new Member("member1", "회원1");
 Member member2 = new Member("member2", "회원2");
 
 member1.setTeam(team1);
 member2.setTeam(team2);
 
 List<Member> members = team1.getMembers();
 System.out.println("members.size = " + members.size());
}
// 결과: member.size = 0

코드를 보면 Member.team에만 연관관계를 설정하고 반대 방향은 연관관계를 설정하지 않았다. 마지막 줄에 팀에 소속된 회원이 몇 명인지 출력해보면 결과는 0이 나온다. 이것은 우리가 기대하는 양방향 연관관계의 결과가 아니다.

따라서 양방향은 양쪽다 관계를 설정해야 한다. 회원 -> 팀을 설정하면 다음 코드처럼 반대방향인 팀 -> 회원도 설정해야 한다.

양쪽 모두 관계를 설정한 코드를 보자.

public void test(){
  
  Team team1 = new Team("team1", "팀1");
  Member member1 = new Member("member1", "회원1");
  Member member2 = new Member("member2", "회원2");
  
  member1.setTeam(team1);
  team1.getMembers().add(member1);
  
  member2.setTeam(team2);
  team1.getMembers().add(member2);
  
  List<Member> members = team1.getMembers();
  System.out.println("members.size = " + members.size());
}
// 결과: member.size = 2

양쪽 모두 관계를 설정하니 기대했던 결과가 나온다.

이제 JPA를 사용해서 완성한 코드를 보자.

public void test(){
  
  Team team1 = new Team("team1", "팀1");
  em.persist(team1);
  
  Member member1 = new Member("member1", "회원1");
  member1.setTeam(team1);
  team1.getMembers().add(member1);
  em.persist(member1);
  
  Member member2 = new Member("member2", "회원2");
  member2.setTeam(team1);
  team1.getMembers().add(member2);
  em.persist(member2);
}

이렇게하면 순수 객체 상태에서도 동작하며, 테이블의 외래 키도 정상 입력된다.

연관관계 편의 메서드

양방향 연관관계는 결국 양쪽 다 신경을 써야 한다. member.setTeam(team)과 team.getMember().add(member)를 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다.

 

그래서 Member 클래스의 setTeam() 메서드를 수정해서 코드를 리팩토링 해보자.

public class Member{
  
  private Team team;
  
  public void setTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
  }
  // ...
}

setTeam() 메서드 하나로 양방향 관계를 모두 설정하도록 변경했다.

수정한 메서드를 사용하는 코드를 보자.

public void test() {
  
  Team team1 = new Team("team1", "팀1");
  em.persist(team1);
  
  Member member1 = new Member("member1", "회원1");
  member1.setTeam(team1);
  em.persist(member1);
  
  Member member2 = new Member("member2", "회원1");
  member2.setTeam(team1);
  em.persist(member2);
}

이렇게 한 번에 양방향 관계를 설정하는 메서드를 연관관계 편의 메서드라 한다.

연관관계 편의 메서드 작성 시 주의사항

member1.setTeam(team1);
member1.setTEam(team2);
Member findMember = team1.getMember(); // member1이 여전히 조회된다.

team2로 변경할 때, team1 -> member1 관계를 제거하지 않았기 때문에 teamA.getMember() 메서드를 실행했을 때 member1이 남아있다. 따라서 연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.

setTeam() 메서드를 수정해보자.

public void setTeam(Team team){
 
 if(this.team != null) {	// this.team이 null이 아니면 이 member객체는 team이 있음을 의미
   this.team.getMembers().remove(this);		// 해당 팀의 멤버에서 삭제
 }
 this.team = team;
 team.getMembers().add(this);
}

 

양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료
  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것뿐
  • JPQL에서 역방향으로 탐색할 일이 많음
  • 단방향 매핑을 잘하고 양방향은 필요할 때 추가해도 됨(테이블에 영향을 주지 않음)
  • 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 함(다대일에서 다 쪽)

 

 

 

참고

자바 ORM 표준 JPA 프로그래밍(김영한)

링크
최근에 올라온 글
글 보관함
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31