양방향 연관관계
양방향 연관관계는 이전 게시글에서 다룬 단방향 연관관계와 다르게 Member
에서 Team
으로의 접근 뿐만 아니라 Team
에서도 Member
에 접근할 수 있는 이름 그대로 양방향에서 매핑이 가능한 것을 말한다.
객체 연관관계를 살펴보면 member
는 하나의 team
을 가질 수 있다. 반대로 team
은 여러 member
를 가질 수 있다. 그렇기 때문에 team
은 컬렉션으로 member
정보를 가지고 있어야한다.
- 회원 -> 팀 [ Member.team ]
- 팀 -> 회원 [ Team.mebmers(List) ]
이제 테이블에서의 관계를 생각해보자. 테이블은 객체와 달리 외래 키 하나로 양방향으로 조회가 가능하다.
객체의 경우는 위와 같이 필드 하나로 양방향 접근이 불가능 하여 단방향 접근이 가능한 각각의 필드(Member.team, Team.members)를 사용하였다.
결론적으로 테이블은 외래 키 하나만으로 양방향 조회가 가능하므로 처음부터 양방향 관계이다. 따라서 양방향 관계를 위해 별도의 작업은 필요치 않다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
class Member {
@Id1
private Long memberId;
// 연관관계 매핑
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
private String username;
}
class Team {
private Long teamId;
private String name;
//== 양방향 매핑을 위해 필드 추가==//
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
기존 단뱡향 관계를 양방향 관계로 변경하기 위해서 Team
엔티티에 members
필드를 추가하였다. 팀의 입장에서는 여러 회원을 가질 수 있으므로 컬렉션 타입인 List
일대다 컬렉션 조회
아래의 예제는 객체 그래프 탐색을 사용하여 팀에서 회원 목록을 조회하는 예제이다.
1
2
3
4
public void findMembers() {
Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers();
}
연관관계의 주인
JPA에서 양방향 매핑을 공부하다보면 연관관계의 주인이라는 이야기를 많이 들어볼 것이다. 양방향 매핑에서 @OneToMany
를 사용하여 일대다 관계를 나타내고 Member
를 컬렉션 타입으로 가지는 것까지는 직관적으로 이해가 될것이다.
그렇다면 mappedBy 속성은 왜 필요한 것일까? @OneToMany
를 통해 Team
과 Member
가 일대다 관계에 있다는 정보를 충분히 제공할 수 있을 것 같은데 말이다. 이를 설명하기 위해 위에서 테이블과 객체의 연관관계의 차이점에 대해 다시 언급하려 한다.
테이블은 외래 키 하나로 양방향 관계를 나타낼 수 있었다. 하지만 객체의 경우는 어떠한가? 엄밀히 말해 객체에는 양방향 연관관계라는 것이 존재하지 않는다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다.
외래 키는 누가 관리하는가?
테이블의 경우는 외래 키를 하나만 관리한다. 그리고 우리는 보통 자식 테이블에서 외래 키를 관리한다. 단방향 관계일 경우 객체에서 참조 필드를 가지고 있는 곳에서 외래 키를 관리하면 되었다. 둘다 매핑 정보(참조 필드, 외래 키)를 관리하는 포인트는 한 곳이다.
그럼 양방향 관계의 경우를 살펴보자. 테이블에서는 별다른 작업이 없었으므로 변동사항이 없다. 하지만 객체의 경우 members 필드가 추가되면서 매핑 정보가 두개가 되었다. 다시말해 객체의 참조는 두개인데 외래 키는 하나이다. 여기에서 차이가 발생된다.
객체의 입장에서 보면 둘 중 어떤 관계를 사용하여 외래 키를 관리해야 할까? 이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.
양방향 매핑의 규칙: 연관관계 주인
양방향 관계를 사용하기 위해서는 지켜야할 규칙이 있는데 두개의 매핑정보(Member.team, Team.members) 중 하나를 연관관계의 주인으로 정해야한다.
그리고 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 가능하다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니라면 mappedBy 속성을 사용하여 속성의 값으로 연관관계 주인을 지정해야 한다.
연관관계 주인을 정한다는 것은 한마디로 외래 키 관리자를 선택하는 것이다.
1
2
테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 된다.
따라서 @ManyToOne의 경우는 mappedBy를 설정할 수 없으므로 mappedBy 속성을 지원하지 않는다.
양방향 연관관계 저장
양방향 연관관계를 사용하여 엔티티를 저장하는 예제이다.
1
2
3
4
5
6
7
8
9
10
11
public void save() {
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 연관관계 설정
em.persist(member1);
}
위 코드를 살펴보면 연관관계의 주인인 Member(team 필드)를 통해서 회원과 팀의 연관관계를 설정하고 저장하는 것을 볼 수 있다. 이는 이전 단방향 연관관계에서 살펴본 회원과 팀을 저장하는 예제와 동일하다.
그렇다면 연관관계의 주인이 아닌 곳에서 연관관계를 지정하면 어떻게 될까?
1
2
3
4
5
6
7
8
9
10
public void save() {
// 회원1 저장
Member member1 = new Member("member1", "회원1");
em.persist(member1);
// 팀1 저장
Team team1 = new Team("team1", "팀1");
team1.getMembers().add(member1); // 무시(연관관계 주인이 아님)
em.persist(team1);
}
위의 경우는 연관관계의 주인이 아닌 Team(members 필드)을 통해서 연관관계를 설정하여 저장을 시도한다. 이 경우는 위에서 언급하였듯 연관관계 주인이 아닌 필드는 읽기만 가능하기 때문에 주인이 아닌 곳에서 입력된 값은 외래 키에 영향을 주지 않는다.
그리고 이것이 양방향 연관관계를 사용할 때의 주의점이다. 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것인데 외래 키 값이 정상적으로 저장되지 않는다면 이것부터 의심해보자.
순수한 객체까지 고려한 양방향 연관관계
외래 키를 저장하기 위해서는 연관관계 주인에 값을 세팅해야 한다. 그렇다면 연관관계의 주인이 아닌 곳에 값을 지정하는 것이 의미가 없는 것인가?
객체 관점에서는 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. 이유는 객체 입장에서는 단방향 2개를 이용해 양방향인 것처럼 표현하고 있다. 그렇다면 만약 한쪽에만 값이 지정되어 있고 반대쪽에 값이 없다면 데이터를 조회하는 시점에 불일치 되는 현상이 발생될 수 있다. 이는 우리가 기대하는 양방향 연관관계의 결과가 아닐 것이다.
1
2
3
4
5
6
7
8
9
10
11
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(team1); // 연관관계 설정
List<Member> members = team1.getMembers();
System.out.println(members.size()); // 결과: 0
}
위 코드를 살펴보면 연관관계의 주인인 Member
를 통해 Team
을 설정하였다. 그렇다면 Team
의 입장에서는 두 개의 Member
를 가지고 있는 결과가 된다. 하지만 Member.team
에만 값을 설정하였기 때문에 Team.members
의 size는 0인 결과가 나온다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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(team1); // 연관관계 설정
team1.getMembers().add(member2);
List<Member> members = team1.getMembers();
System.out.println(members.size()); // 결과: 2
}
이렇게 양쪽 모두 관계를 설정하면 결과했던 2가 출력된다. 이렇듯 객체의 관계까지 고려하여 양쪽 모두 연관관계를 설정하는 것이 좋다.
Comments powered by Disqus.