객체의 참조와 테이블의 외래 키를 매핑하는 것이 이 장의 목표다.
연관관계 매핑을 이해하기 위한 핵심 키워드
객체 연관관계
테이블 연관관계
객체 연관관계와 테이블 연관관계
그럼 객체 연관관계와 테이블 연관관계를 어떻게 매핑시킬까?
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
// 연관관계 매핑
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 연관관계 설정
public void setTeam(Team team) {
this.team = team;
}
// ...
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
// ...
}
@ManyToOne
@Column
의 속성과 같음@JoinColumn
외래 키를 매핑할 때 사용
생략 가능
속성
@ManyToOne
=FetchType.EAGER@OneToMany
= FetchType.LAZY영속성 전이 기능 사용
targetEntity
연관된 엔티티의 타입 정보 설정
@OneToMany
private List<Member> members; // 제네릭으로 타입 정보를 알 수 있다.
@OneToMany(targetEntity=Member.class)
private List members; // 제네릭이 없으면 타입 정보를 알 수 없다.
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // member1 → team1
em.persist(member1);
// 회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // member2 → team1
em.persist(member2);
엔티티 조회 방법은 크게 2가지이다.
객체 그래프 탐색
// Member와 연관된 Team 엔티티 조회
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
객체지향 쿼리 사용(JPQL)
// 팀1에 소속된 회원만 조회
String jpql = "select m from Member m join m.team t where " + "t.name=:teamName";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setParameter("teamName", "팀1")
.getResultList();
// 새로운 팀2
Team team2 = new Team("team2", "팀2");
em.persist(team2);
// 회원1에 새로운 팀2 설정
Member member = em.find(Member.class, "member1");
member.setTeam(team2);
// 회원1을 팀에 소속하지 않도록 변경
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null); // 연관관계 제거
만약 팀을 삭제하려고 한다면 기존 연관관계를 제거하고 삭제해야 한다(외래 키 제약조건 때문).
// 팀1 삭제
member1.setTeam(null);
member2.setTeam(null);
em.remove(team1);
테이블 연관관계는 똑같다. 왜냐하면 데이터베이스 테이블은 외래 키로 양방향으로 조회가 가능하기 때문이다.
Member 엔티티는 똑같다.
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 연관관계 설정
public void setTeam(Team team) {
this.team = team;
}
// ...
}
Team 엔티티를 보자.
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
// 추가
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
// ...
}
팀과 회원은 일대다 관계다. 따라서 List<Member> members
를 추가했다. 그리고 일대다 관계이기 때문에 @OneToMany
매핑 정보를 사용했다. mappedBy
는 반대쪽 매핑이 Member.team
이므로 team
을 주었다.
Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers(); // 팀 → 회원
테이블 연관관계
하지만 정확히 말하면 객체에는 양방향 연관관계가 없다. 서로 다른 방향 2개가 존재하는 것이다.
객체 연관관계
서로 객체를 참조하고 있지만 외래 키는 하나이다. 그러면 어떤 관계를 사용해서 외래 키를 관리해야 할까?
두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관관계 주인(Owner)이라 한다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되는 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
mappedBy
속성을 사용mappedBy
속성을 사용하지 않음mappedBy
속성을 사용해서 연관관계의 주인을 지정연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.
회원 테이블이 외래 키(TEAM_ID)를 관리하고 있다. 따라서 Member와 Team 엔티티를 볼 때 Member 엔티티가 Team을 참조하고 있기 때문에 Member.team
을 주인으로 선택하여 관리하면 된다.
Team 엔티티에 있는 Team.members
를 주인으로 선택할 수도 있지만, 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다. 왜냐하면 Team.members
는 Team 엔티티에 있고, Team 엔티티는 TEAM 테이블에 매핑되어 있는데 외래 키는 MEMBER 테이블이 가지고 있기 때문이다.
연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
따라서 외래 키를 가지고 있는 MEMBER 테이블과 매핑되어 있는 Member 엔티티의 Member.team이 주인이 되고, Team.members는 주인이 아님을 설정하기 위해 mappedBy = "team"
속성을 추가해야 한다. 여기서 mappedBy
의 team
은 Member 엔티티의 team 필드를 말한다.
참고
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다.
// 팀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(team2);
em.persist(member2);
뭔가 이상하다. setTeam()
메서드로 member1, member2는 team1과 연관관계를 맺었지만 Team.members는 어떤 코드도 없다. team1.getMembers().add(member1)
메서드를 통해 Team.members에도 저장을 해야 할 것 같다. 하지만 설명했듯이 Team.members는 연관관계 주인이 아니다. 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다. 따라서 무시해도 된다.
만약 연관관계 주인이 아닌 곳에만 값을 입력하면 어떻게 될까?
// 회원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");
// 주인이 아닌 곳만 연관관계 설정
team1.getMembers().add(member1);
team1.getMembers().add(member2);
// 팀1 저장
em.persist(team1);
데이터베이스 MEMBER 테이블은 조회하면 이렇게 나올 것이다.
MEMBER_ID | USERNAME | TEAM_ID |
---|---|---|
member1 | 회원1 | null |
member2 | 회원2 | null |
연관관계 주인이 아닌 Team.members에만 값을 저장했기 때문에 적용이 안된 것이다.
연관관계 주인만이 외래 키의 값을 변경할 수 있다.
그러면 연관관계 주인에만 값을 저장하면 되는 것일까?
객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
왜 그럴까?
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 = " + members.size());
다음 코드가 실행되면 출력 결과는 어떤 결과가 나올까? 결과는 members.size = 0
이다. JPA가 없다고 생각하고 객체로 생각하면 당연한 결과이다. 하지만 이것은 기대했던 양방향 연관관계 결과가 아니다.
양방향은 양쪽 다 관계를 설정해야 한다.
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(team1); // 연관관계 주인이 아닌 곳도 저장
List<Member> members = team1.getMembers();
System.out.println("members.size = " + members.size());
출력 결과는 members,size = 2
가 나온다. 이제 JPA를 사용해서 코드를 완성해보자.
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(team1);
em.persist(member2);
이제 순수한 객체 상태에서도 동작하고, 테이블의 외래 키도 정상적으로 입력된다.
결론: 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자.
결국, 양방향 연관관계는 양쪽 다 설정을 해야 한다. 하지만 이전처럼 member.setTeam()
과 team.getMembers().add()
를 각각 호출하면 하나만 호출하는 실수를 할 때가 있다. 따라서 양방향 관계에서는 두 코드를 하나처럼 사용하는 것이 안전하다.
public class Member {
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
// ...
}
그러면 위에서 사용한 코드는 이렇게 바뀐다.
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", "회원2");
member2.setTeam(team1); // 양방향 설정
em.persist(member2);
이렇게 한 번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라고 한다.
문제가 있다. 만약 member1
이 team2
로 바뀐다면 어떻게 될까?
member1.setTeam(team1); // 처음 연관관계 설정
member1.setTeam(team2); // 변경
member1.setTeam(team1)
연관관계 설정하면 그림과 같을 것이다.
하지만 member1.setTeam(team2)
변경하면 어떻게 될까?
team1 → member1 관계가 제거되지 않았다. 왜일까? setTeam()
메서드를 보면 this.team = team
과 team.getMembers().add(this)
가 있다. member1.setTeam(team2)
를 실행하면 this.team = team
으로 member1 → team1 관계가 끊기지만 team1 → member1 관계를 제거하는 코드는 없다. 따라서 연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 끊어야 한다.
// 기존 팀과 관계를 제거
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
team1 → member1 관계를 제거하는 코드가 없어도 데이터베이스에는 문제가 없다. 왜냐하면, 연관관계 주인이 아니기 때문이다. 하지만 왜 이렇게 하는 것일까? 그것은 영속성 컨텍스트 때문이다. 관계를 변경하고 아직 영속성 컨텍스트가 살아있는 상태에서 team1.getMembers()
메서드를 호출하면 member1이 살아있을 것이다. 따라서 관계를 제거하는 것이 안전하다.
양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.