김영한님이 지음, [자바 ORM 표준 JPA 프로그래밍] 책을 읽고 정리한 필기입니다.📢
양방향 연관관계의 주의점
데이터베이스에 외래 키 값이 정상적으로 저장되지 않다면 주인이 아닌 곳에만 값을 입력했는지 확인해라.(제발!)
주인이 아닌 곳에만 값을 설정할 경우
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void testSaveNonOwner(EntityManager em){
//회원1 저장
Member member1 = new Member("member1", "회원1");
em.persist(member1);
//회원2 저장
Member member2 = new Member("member2", "회원2");
em.persist(member2);
Team team1 = new Team("team1", "팀1");
//주인이 아닌 곳만 연관관계 설정
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(team1);
}
MEMBER 테이블 조회 결과
MEMBER_ID | USERNAME | TEAM_ID |
---|---|---|
member1 | 회원1 | null |
member2 | 회원2 | null |
위 코드는 연관관계의 주인인 Member.team
에 아무 값도 입력하지 않았기 때문에 외래 키 TEAM_ID
에 team1
이 아닌 null
값이 입력 됨
객체까지 고려한 양방향 연관관계
연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 저장을 안해도 되는가?
→ No. 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 바람직함
양쪽 방향 모두 값을 입력하지 않으면 객체를 사용할 때 문제 발생
1
2
3
4
5
6
7
8
9
10
11
12
13
public void test순수한객체_양방향(){
//팀1
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Mbmber member2 = new Member("member2", "회원2");
member1.setTeam(team1); //연관관계 설정 member1 -> team1
member2.setTeam(team1); //연관관계 설정 member2 -> team1
List<Member> members = team1.getMembers();
System.out.println("members.size = " + members.size());
}
//결과 : members.size = 0
때문에, 객체까지 고려하면 양쪽 다 관계를 맺어야 함
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
public static void testSave(EntityManager em){
//팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
//회원1 저장
Member member1 = new Member("member1", "회원1");
//양방향 연관관계 설정
member1.setTeam(team1); //연관관계 설정 member1 -> team1
team1.getMembers().add(member1); //연관관계 설정 team1 -> member1
em.persist(member1);
//회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); //연관관계 설정 member2 -> team1
team1.getMembers().add(member2); //연관관계 설정 team1 -> member2
em.persist(member2);
em.flush();
Team findTeam = em.find(Team.class, "team1");
System.out.println("members.size = " + findTeam.getMembers().size());
}
//결과 : member.size = 2
team.getMembers().add(member)
를 하지 않을 경우 결과는 member.size = 0
이 출력 됨
연관관계 편의 메소드
양방향 연관관계는 결국 양쪽 다 신경 써야 하는데 각 방향마다 신경 쓰다보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있음
양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전
1
2
3
member.setTeam(team);
team.getMembers().add(member);
//실수 여지가 있음
1
2
3
4
5
6
7
8
public class Member {
...
public void setTeam(Team team){
this.team = team;
this.team.getMembers().add(this);
}
...
} //실수를 없앨 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void testSave(EntityManager em){
//팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
//회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); //양방향 설정
em.persist(member1);
//회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); //양방향 설정
em.persist(member2);
em.flush();
Team findTeam = em.find(Team.class, "team1");
System.out.println("members.size = " + findTeam.getMembers().size());
}
//결과 : member.size = 2
연관관계 평의 메소드 : 한 번에 양방향 관계를 설정하는 메소드
연관관계 편의 메소드 작성 시 주의사항
아래와 같은 문제가 생길 수 있음
1
2
3
member1.setTeam(teamA); //1
member1.setTeam(teamB); //2
Member findMember = teamA.getMember(); //팀이 B로 변경 되었음에도 A팀에 member1이 여전히 조회 됨
삭제되지 않은 관계
연관관계를 변경할 때는 기존 객체와의 관계를 제거해줘야 한다.
1
2
3
4
5
6
7
8
public void setTeam(Team team){
//기존 팀과 관계를 제거
if(this.team != null){
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
양방향 관계는 서로 다른 단방향 연관관계 2개를 양방향인 것처럼 보이게 하려고 비용이 많이 듬, 양방향을 사용하려거든 로직을 견고하게 작성해야 함
💡 Tip.
삭제되지 않은 관계에서
teamA
→member
관계가 제거되지 않아도 데이터베이스 외래 키를 변경하는 데는 문제가 없다. `Team.members`는 연관관계의 주인이 아니기 때문이다. 문제는 영속성 컨텍스트가 아직 유지되는 상태에서teamA
의getMembers()
를 호출하면member1
이 반환된다는 점이다. 따라서 변경된 연관관계는 기존 관계를 제거하는 것이 안전하다.
정리
단방향 매핑과 비교해 양방향 매핑은 복잡함, 주인 설정해야 하고, 로직 관리 해야 함
연관관계가 하나이자 언제나 연관관계의 주인인 단방향 관계에 주인이 아닌 연관관계를 추가했을 뿐
→ 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다 !
3줄 요약
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료 됨
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가 됨
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 함
💡 Tip. 연관관계의 주인을 정하는 기준
단방향은 항상 외래 키가 있는 곳을 기준으로 매핑하면 된다. 하지만 양방향은 연관관계의 주인(Owner)이라는 이름으로 인해 오해가 있을 수 있다. 비즈니스 로직상 더 중요하다고 연관관계의 주인으로 선택하면 안된다. 비즈니스 중요도를 배제하고 단순히 외래 키 관리자 정도의 의미만 부여해야 한다.
예를 들어 자동차의 차체와 바퀴를 생각해보면 바퀴가 외래 키가 있는 다 쪽이다. 따라서 바퀴가 연관관계의 주인이 된다. 차체가 더 중요한 것 같아 보이지만 연관관계의 주인은 단순히 외래 키를 매핑한 바퀴를 선택하면 된다. 따라서 연관관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안된다.
⚠️ 양방향 매핑 시에는 무한 루프에 빠지지 않게 조심해야 함. Member.toString()
에서 getTeam()
호출하고 Team.String()
에서 getMember()
호출 시 발생 함. Lombok 사용시 특히 주의해야 하고 JSON 변환할 때 자주 발생하니 무한루프에 빠지지 않도록 방어해주는 어노테이션을 사용하자.
댓글남기기