개인 프로젝트 진행 중 문득 의문이 들었다.
양방향 연관 관계를 사용하는 것이 현재 내 상황에 좋은 방법은 아닌 것 같은데? 하는 의문이다.
큰 고민 없이 답습한대로 사용한 것은 아닐까?
의문에 대한 점검을 하기 위해 오랜만에 펼친 JPA 책.
당장 프로젝트를 끝내야 한다는 급한 마음에 학습한 내용을 꼼꼼히 정리해보지 않은 지난 날의 과오 때문인지… 책의 내용이 새롭기만 하다.
반성하는 마음으로 미뤄 왔던 JPA 관련 내용들을 포스팅 하고자 한다.
1) 연관 관계의 방향이란?
객체(엔티티) 간 연관 관계에서는 방향이 존재한다. 단방향과 양방향이다.
왜 이런 말을 하냐하면, 우리가 익숙한 테이블 연관관계에서는 항상 양방향 관계만 존재하기 때문이다.
예를 들어 회원과 팀의 관계에서 1명의 회원은 1개의 팀에만 소속될 수 있고,
1개의 팀에는 여러 회원이 존재할 수 있다고 가정하자.
테이블 연관관계에서는 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
이 외래키를 통해 회원과 팀을,
또 팀과 회원을 JOIN(아래 코드 참고 *연관 데이터 조회 시 JOIN를 사용)할 수도 있다.
즉 양방향 관계인 것이다.
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
이와 달리 객체 연관관계에서는 회원 객체에 team 필드로 팀 객체와 연관관계를 맺는다.
그래서 회원은 팀을 알 수 있지만(member.getTeam() *연관 데이터 조회 시 참조를 사용)
팀에서는 회원을 알 수 없다.
즉 단방향 관계인 것이다.
2) JPA 에서 단방향 연관 관계 매핑 방법
아까의 회원과 팀 예제로 코드를 살펴보자.
@Entity
public class Member {
@Id @Column(name = "MEMBER_ID")
private String id;
private String name;
@ManyToOne // 다대일
@JoinColumn(name = "TEAM_ID")
private Team 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;
}
3) 연관관계 조회 방법
연관관계를 조회하는 방법은 두 가지이다.
첫번째. 객체 그래프 탐색
이름이 생소할 수 있으나, 객체를 통해 엔티티를 조회하는 것이다.
member.getTeam()
이렇게 말이다.
public void findTeam() {
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
System.out.println("팀 명: " + team.getName());
}
두번째. 객체 지향 쿼리 사용
JPQL 조인을 이용해 조회를 해보자.
private static void queryLogicJoin(EntityManager em) {
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();
for (Member member : resultList) {
System.out.println("[query] member.name=" +
member.getName());
}
}
앞서 짧게 회원 → 팀 단방향 연관관계에 대해 알아봤는데,
이제 반대 방향으로 접근도 추가하여 양방향 연관관계 매핑을 해보겠다.
회원 : 팀 = N : 1 관계이므로, 팀에서 회원과 연관관계를 맺기 위해서는 컬렉션을 사용해야 한다.
@Entity
public class Member {
@Id @Column(name = "MEMBER_ID")
private String id;
private String name;
@ManyToOne // 다대일
@JoinColumn(name = "TEAM_ID")
private Team 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;
@OneToMany(mappedBy = "team") // 연관관계
private List<Member> members = new ArrayList<Member>();
}
컬렉션인 List<Member> members
와
일대다 관계를 매핑하기 위한 정보 @OneToMany
가 추가되었다.
양방향 매핑이 완료된 것이다.
이제 팀에서 회원 컬렉션을 객체 그래프 탐색 방법으로 조회해 보자.
// 일대다 컬렉션 조회
public void biDirection() {
Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers(); // 객체 그래프 탐색
for (Member member : members) {
System.out.println("member.name=" +
member.getName());
}
}
1) 연관관계의 주인이란?
사실 객체에서 양방향 연관관계라는 것은 존재하지 않는다!
서로 다른 단방향 2개가 존재하는 것이다(양쪽에서 서로 참조하는 형태).
이 두 관계를 애플리케이션 로직으로 잘 묶어 양방향처럼 보이도록 하는 것 뿐이다.
1.회원 → 팀 (단방향)
2.팀 → 회원 (단방향)
앞서 살펴본 예제 코드에서 mappedBy
는 연관관계의 주인을 설정하기 위한 것이다.
연관관계의 주인이 왜 필요한 것일까?
그 이유는 테이블과 엔티티간 연관관계를 맺는 방식에 따른 간극 때문이다.
엔티티를 단방향으로 매핑하면 1개의 참조가 생긴다.
회원 → 팀
이 하나의 참조가 테이블의 외래키를 관리하는 식으로 동작한다.
그런데 엔티티를 양방향 연관관계로 설정하면
앞서 말했듯, 단방향 관계 2개가 생기는 것이다(= 참조가 2개이다).
또 앞서 말했듯, 테이블간 연관 관계는 외래키 하나로 관리 된다.
참조로 하나의 외래키를 관리해야 하는데,
2개의 참조 중 어떤 것으로 외래키를 관리할지 정해야 한다.
이것이 연관관계의 주인을 설정하는 것이다.
2) 연관관계의 주인을 뽑는 기준
연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
비지니스 중요도가 아니다.
연관관계의 주인은 결국 외래키 관리자이기 때문인데,
예제를 통해 살펴보면 회원 엔티티의 Member.team이 주인이 되면
자신의 테이블의 외래키를 관리하면 되지만,
팀 엔티티의 Team.members가 주인이 되면
물리적으로 다른 테이블(MEMBER 테이블)에 위치한 외래키를 관리해야 하기 때문이다.
테이블의 다대일 or 일대다 관계에서는 항상 다 쪽이 외래키를 가진다.
때문에 @ManyToOne은 항상 연관관계의 주인이므로 mappedBy 속성이 없다.
3) 연관관계의 주인이 하는 일
연관관계의 주인만 테이블의 외래키를 관리하기 때문에
연관관계의 주인만이 외래키를 변경할 수 있다.
주인이 아닌 반대편은 읽기만 가능하다.
아래 예제에서 살펴보자.
// 연관관계의 주인이 아니면 외래 키를 변경할 수 없다
public void testSaveNonOwner() {
// 회원1 저장
Member member1 = new Member("member1", "회원1");
em.persist(member1);
// 회원2 저장
Member member2 = new Member("member2", "회원2");
em.persist(member2);
Team team = new Team("team1", "팀1");
// 주인이 아닌 곳에 연관관계 설정
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist();
}
이 후 회원 테이블을 조회하면,
연관관계의 주인이 아닌 쪽에 값을 저장했기 때문에
TEAM_ID는 null 이다.
MEMBER_ID | NAME | TEAM_ID |
---|---|---|
member1 | 회원1 | null |
member2 | 회원2 | null |
아래 코드처럼 수정되어야 한다.
// 연관관계의 주인만 외래 키를 변경할 수 있다
public void testSaveOwner() {
// 회원1 저장
Member member1 = new Member("member1", "회원1");
em.persist(member1);
// 회원2 저장
Member member2 = new Member("member2", "회원2");
em.persist(member2);
Team team = new Team("team1", "팀1");
// 연관관계 설정
member1.setTeam(team1);
member2.setTeam(team1);
em.persist();
}
1) 하지만 양쪽 모두에 값을 저장해주자.
위에서는 연관관계의 주인쪽에 값을 저장해야 외래 키의 값을 변경할 수 있다고 했지만,
사실 연관관계의 주인 쪽, 주인이 아닌 쪽 모두에 값을 저장하는 것이 안전하다.
ORM은 객체와 관계형 데이터베이스 둘 다 중요하기 때문인데,
객체까지 고려해 양쪽 다 관계를 맺는 것이다.
public void testORM_양방향() {
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);
}
이렇게 하면 순수한 객체 상태에서도 동작하고, 테이블의 외래 키도 정상 입력된다.
2) 연관관계 편의 메소드
member1.setTeam(team1);
team1.getMembers.add(member1);
양쪽 모두에 저장하는 코드를 각각 호출하다 보면 실수로 누락될 수 있기 때문에
하나의 메소드로 사용하는 것이 안전하다
@Entity
public class Member {
...
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
public void testORM_양방향_리팩토링() {
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");
member1.setTeam(team1);
em.persist(member2);
}
3) 기존관계까지 제거해야 더 안전하다.
만약 위 코드에서 member1이 team1에서 team2로 변경되면 어떻게 될까?
기존 코드에는 기존 팀을 제거하는 부분이 없기 때문에
team1.getMembers을 조회하면 여전히 member1이 조회될 것이다.
때문에 setTeam 메소드는 아래와 같이 수정되어야 더욱 안전하다.
@Entity
public class Member {
...
private Team team;
public void setTeam(Team team) {
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
}
단방향 매핑과 비교해서 양방향 매핑의 장점은
반대방향으로의 객체 그래프 탐색 기능이 추가된 것 뿐이다.
member.getTeam();
team.getMembers();
단방향 매핑으로도 이미 테이블과 객체의 연관관계 매핑은 된다.
또한 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향 모두를 관리해야 한다.
뿐만 아니라 연관관계의 주인도 정해야 하고, 두 개의 단방향 관계를 양방향으로 만들기 위해 로직도 잘 짜야하고, 관리해야 한다.
즉 양방향 매핑은 복잡하다.
우선 단방향 매핑을 사용하고,
반대 방향으로 객체 그래프 탐색 기능이 필요할 때에 코드를 추가하도록
책의 저자는 권고한다.
참고 서적 : 자바 ORM 표준 JPA 프로그래밍 By 김영한