twocowsong

양방향 연관관계의 주의점 본문

IT/JPA

양방향 연관관계의 주의점

WsCode 2022. 6. 6. 13:01

양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지않고, 주인이 아닌곳에만 값을 입력하는것입니다.

DB에 외래 키 값이 정상적으로 저장되지 않으면 이것부터 의심해봐야합니다.

 

public void testSaveNonOwner(EntityManager em) {
   // 회원1 저장
   Member member1 = new Member("member1", "회원1");
   em.persist(member1);
   
   // 회원2 저장
   Member member2 = new Member("member2", "회원2");
   em.persist(member2);

   // 팀1 저장
   Team team1 = new Team("team1", "팀1");
   em.persist(team1);
   // 주인이 아닌 곳만 연관관계 설정
   team1.getMembers().add(member1); // List타입이라 add함수 호출이 가능 
   team1.getMembers().add(member2);
   
   em.persist(team1);
}

실행 결과는 아래와 같습니다.

MEMBER 테이블
TEAM 테이블

 

외래 키 TEAM_ID에 team1이 아닌 null값이 입력되어 있는데,

연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문입니다.

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

 

예제 코드는 연관관계의 주인인 Member.team에 아무 값도 입력하지 않았습니다.

따라서 TEAM_ID외래 키의 값도 null이 저장됩니다.

member1.setTeam(team1) 있었다면 회원에서 직접 팀을 할당했기에 정상적으로 TEAM_ID가 입력이 되었을것입니다.


 

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

사실은 객체 관점에서 양쪽 방향에 모두 값을 입력해주는것이 가장 안전합니다.

양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생 할 수 있습니다.

public static void test3(EntityManager em) {
   // 회원1 저장
   Member member1 = new Member("member1", "회원1");
   em.persist(member1);

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

   // 팀1 저장
   Team team1 = new Team("team1", "팀1");
   em.persist(team1);
   
   member1.setTeam(team1);
   member2.setTeam(team1);

   List<Member> members = team1.getMembers();
   System.out.println(members.size()); // 출력 결과 -> 0
}

코드를 보면 Member.team에만 연관관계를 설정하고 반대 방향은 연관관계를 설정하지 않았습니다.

마지막 System.out.println의 결과는 0이 나오게되면 이것은 우리가 기대한 양방향 결과가 아닙니다.

member1.setTeam(team1);
member2.setTeam(team1);

양방향은 양쪽다 관계를 설정해야 합니다. 

이처럼 회원 -> 팀을 설정하면 반대 방향도 설정해야 합니다.

team1.getMembers().add(member1);
team1.getMembers().add(member2);

 

 

그러면 양쪽 모두 관계를 설정한 전체코드를 같이 확인해보겠습니다.

public static void test4(EntityManager em) {
   Member member1 = new Member("member1", "회원1");
   Member member2 = new Member("member2", "회원2");
   Team team1 = new Team("team1", "팀1");
   em.persist(team1);

   // 회원1 INSERT, 팀 -> 회원 연관관계 추가
   member1.setTeam(team1);
   em.persist(member1);
   team1.getMembers().add(member1);

   // 회원2 INSERT, 팀 -> 회원 연관관계 추가
   member2.setTeam(team1);
   em.persist(member2);
   team1.getMembers().add(member2);
}

 

양쪽도 연관관계를 설정하였습니다.

순수한 객체 상태에서도 동작하며, 테이블의 외래 키도 정상 입력됩니다.

물론 외래 키의 값은 연관관계의 주인인 Member.team값을 사용 합니다.

member1.setTeam(team1); // 연관관계의 주인
team1.getMembers().add(member1); // 주인이 아닙니다. 저장 시 사용되지 않습니다.

 

- Member.team : 연관관계의 주인이며 이 값으로 외래 키를 관리합니다.

- Team.members : 연관관계의 주인이 아닙니다. 저장시에 사용되지 않습니다.

 

객체까지 고려해서 주인이 아닌곳에도 값을 입력하는것을 책에서는 추천하고있습니다.


연관관계 편의 메소드

양방향 연관관계는 결국 양쪽 다 신경을 써야합니다.

다음처럼 member.setTeam(Team) 과 team.getMembers().add(members)를 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있습니다.

member1.setTeam(team1);
team1.getMembers().add(member1);

양방향 관계에서 두 코드는 하나인것처럼 사용하는것이 안전합니다.

Member 클래스의 setTeam() 메소드를 수정해서 코드를 리팩토링 해보겠습니다.

public class Member {
   ...
   @ManyToOne
   @JoinColumn(name = "TEAM_ID")
   private Team team;

   // 연관관계 설정
   public void setTeam(Team team) {
      this.team = team;
      team.getMembers().add(this);
   }
   ...
}

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

public static void test4(EntityManager em) {
   Member member1 = new Member("member1", "회원1");
   Member member2 = new Member("member2", "회원2");
   Team team1 = new Team("team1", "팀1");
   em.persist(team1);

   // 회원1 INSERT, 팀 -> 회원 연관관계 추가
   member1.setTeam(team1);
   em.persist(member1);

   // 회원2 INSERT, 팀 -> 회원 연관관계 추가
   member2.setTeam(team1);
   em.persist(member2);
}

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


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

사실..setTeam() 메소드에는 버그가 있습니다.

member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMembers(); // 여전히 member1이 조회됩니다.

member1에 setTeam의 메소드 호출 시 add로 teamA를 추가하였습니다.

그 후 setTeam을 또 호출하여 teamB를 추가하였지만 teamA 관계를 제거하지 않았습니다.

연관관계를 변경 할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 합니다.

public void setTeam(Team team) {
   if (this.team != null) {
      team.getMembers().remove(this);
   }
   this.team = team;
   team.getMembers().add(this);
}

정리

단방향 매핑과 비교해서 양방향 매핑은 복잡합니다.

연관관계의 주인도 정하고, 두 개의 단방향 연관관계를 양방향으로 만들기 위해 로직도 관리해야합니다.

중요한 사실은 연관관계가 하나인 단방향 매핑은 언제나 연관관계의 주인이라는 점입니다.

양방향은 여기에 주인이 아닌 연관관계를 하나 추가했을 뿐입니다.

양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것 뿐입니다.

member1.getTeam(); // 회원 -> 팀
team1.getMembers(); // 팀 -> 회원 (양방향 매핑으로 추가된 기능)

주인의 반대편은 mappedBy로 주인을 지정해야합니다.

그리고 주인의 반대편은 단순히 보여주는일만 할 수 있습니다.

 

내용을 정리하면 다음과 같습니다.

- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었습니다.

- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가됩니다.

- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향으로 모두 관리해야 합니다.

 

 

'IT > JPA' 카테고리의 다른 글

정리  (0) 2022.06.09
실전 예제  (0) 2022.06.07
양방향 연관관계 저장  (0) 2022.06.05
연관관계의 주인  (0) 2022.06.04
양방향 연관관계 매핑  (0) 2022.06.03