[자바 ORM 표준 JPA 프로그래밍] 5.3 양방향 연관관계
개발서적/자바 ORM 표준 JPA

[자바 ORM 표준 JPA 프로그래밍] 5.3 양방향 연관관계

Reference. 자바 ORM 표준 JPA 프로그래밍

책 목차 및 이전 글

5.3 양방향 연관관계

JPA 양방향 관계

  • 회원 → 팀 (Member.team) [다대일]
  • 팀 → 회원 (Team.members) [일대다]

<참고>

  • JPA는 List를 포함해서 Collection, Set, Map 같은 다양한 컬렉션을 지원(14.1절 참고)

 

데이터베이스 양방향 관계

  • 데이터베이스 테이블은 외래 키 하나로 양방향 조회 가능
    • MEMBER JOIN TEAM, TEAM JOIN MEMBER 가능

 

5.3.1 양방향 연관관계 매핑

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

		@OneTomany (mappedBy = "team")
		private List<Member> members = new ArrayList<Member>();

		//Getter, Setter ...
}
  • 팀과 회원은 일대다 관계, 팀 엔티티에 컬렉션인 List<Member> member를 추가
  • 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용
  • mappedBy 속성은 양방향 매핑일 때 사용 (반대쪽 매핑의 필드를 할당)

5.3.2 일대다 컬렉션 조회

Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers(); //(팀 -> 회원)

for (Member member : members) {
		System.out.println("member.username = " + member.getUsername());
}

//결과
//member.username = 회원1
//member.username = 회원2

 

5.4 연관관계의 주인

mappedBy가 필요한 이유

  • 객체에 양방향 관계는 존재하지 않고 연관관계 2개를 잘 묶어서 표현
  • 객체 연관관계 표현
    • 회원 → 팀 연관관계 1개 (단방향)
    • 팀 → 회원 연관관계 1개 (단방향)
  • 테이블 연관관계 표현
    • 회원 <-> 팀의 연관관계 1개(양방향)
  • 엔티티를 양방향 연관관계로 설정하면 객체 참조는 둘인데 외래키는 하나인 문제점 발생
    • JPA는 두 객체 연관관계 중 하나를 정해서 테이블 외래키를 관리, 이것이 연관관계 주인

5.4.1 양방향 매핑의 규칙: 연관관계의 주인

  • 양방향 연관관계는 두 연관관계 중 하나를 주인으로써 지정해야 함
    • 연관관계의 주인만이 데이터베이스 연관관계와 매핑하고 외래 키(등록, 수정 삭제)를 관리
    • 주인이 아닌 쪽은 읽기만 가능
  • mappedBy: 연관관계의 주인으로 지정하는 속성으로 사용
    • 주인mappedBy 속성을 사용하지 않음
    • 주인이 아니면 mappedBy 속성을 사용해 속성의 값으로 연관관계 주인을 지정

 

  • 연관관계의 주인을 정한다는 것은 외래 키 관리자를 선택하는 것
  • 회원 엔티티에 있는 Member.team자기 테이블에 외래 키를 관리 (MEMBER.TEAM_ID)
  • 팀 엔티티에 있는 Team.members를 선택하면 물리적으로 다른 테이블의 외래키를 관리

5.4.2 연관관계의 주인은 외래 키가 있는 곳

  • 회원 테이블이 외래 키를 가지고 있으므로 Member.team주인
  • 주인이 아닌 Team.members에는 mappedBy="team"속성을 통해 주인이 아님을 설정
  • 연관관계의 주인
    • mappedBy 속성 사용X
    • 데이터베이스 연관관계와 매핑
    • 외래 키를 관리
  • 주인이 아닌 반대편
    • mappedBy 속성 사용
    • 읽기만 가능
    • 외래 키를 변경하지 못함

<참고>

  • 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래키
  • @ManyToOne은 항상 연관관계의 주인, mappedBy를 설정할 수 없으므로 속성도 없음

 

5.5 양방향 연관관계 저장

JPA를 통한 저장

public void testSave() {

		//팀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);
}

데이터베이스 결과

MEMBER_ID USERNAME TEAM_ID
member1 회원1 team1
member2 회원2 team1
  • 양방향 연관관계는 연관관계의 주인이 외래 키를 관리
    • 주인이 아닌 방향은 설정하지 않아도 데이터베이스 외래 키 값이 정상 입력
    //이런 코드가 필요해 보이지만 주인이 아니므로 영향X
    team1.getMembers().add(member1); 
    team1.getMembers().add(member2); 

 

5.6 양방향 연관관계의 주의점

  • 양방향 연관관계의 흔한 실수는 주인에 값을 입력하지 않고 주인이 아닌 곳에 입력하는 실수
    • 데이터베이스에 외래키 값이 정상적으로 저장되지 않았다면 이것부터 의심
team1.getMembers().add(member1); 
team1.getMembers().add(member2); 

데이터베이스 결과

MEMBER_ID USERNAME TEAM_ID
member1 회원1 null
member2 회원2 null

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

  • 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전
    • 모두 입력하지 않은 경우 JPA를 사용하지 않는 순수한 객체에선 심각한 문제 발생
  • ORM은 객체와 관계형 데이터베이스 둘 다 중요
순수한 객체를 통한 테스트 코드
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");

member1.setTeam(team1); //연관관계 설정 member1 -> team1
member2.setTeam(team1); //연관관계 설정 member2 -> team1

List<Member> members = team1.getMembers();
System.out.println("member.size = " + members.size());

//결과: members.size = 0
양방향 모두 설정한 테스트 코드
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");

member1.setTeam(team1); //연관관계 설정 member1 -> team1
team1.getMembers().add(member1) //연관관계 설정 team1 -> member1

member2.setTeam(team1); //연관관계 설정 member2 -> team1
team1.getMembers().add(member2) //연관관계 설정 team1 -> member2

List<Member> members = team1.getMembers();
System.out.println("member.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); //연관관계 설정 member1 -> team1
team1.getMembers().add(member1) //연관관계 설정 team1 -> member1
em.persist(member1);

Member member2 = new Member("member2", "회원2");

//양방향 연관관계 설정
member2.setTeam(team1); //연관관계 설정 member2 -> team1
team1.getMembers().add(member2) //연관관계 설정 team1 -> member2
em.persist(member2);
  • 위에 과정을 거쳐 연관관계를 설정하면 순수한 객체 상태 및 테이블 외래 키 정상입력도 가능
※결론: 객체의 양방향 연관관계는 양쪽 모두 관계를 구성

5.6.2 연관관계 편의 메서드

  • 양방향 관계를 설정하다보면 실수로 둘 중 하나만 호출해서 양방향이 깨지기 쉬움
  • 이런 실수를 줄이기 위해 한번에 설정하는 메소드연관관계 편의 메소드
연관관계 편의 메서드의 예시
//Member.java
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);

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

  • setTeam() 메소드에는 버그가 존재 (리팩토링 전에도 존재)
member1.setTeam(teamA); //1
member1.setTeam(teamB); //2
Member findMember = teamA.getMember(); //member1이 여전히 조회
  • teamB로 변경할 때 teamAmember1 관계를 제거 X
    • 연관관계를 변경할 때는 기존 팀과 회원의 연관관계를 삭제 후 추가가 필요
    //Member.java
    public void setTeam(Team team){
    		//기존 팀과 관계를 제거
    		if (this.team != null) {
    				this.team.getMembers().remove(this);
    		}
    		this.team = team;
    		team.getMembers().add(this);
    }
  • 단방향 연관관계 2개를 양방향인 것처럼 보이게 하려면 많은 수고가 필요
  • 반면 관계형 데이터베이스는 외래 키 하나로 문제를 단순하게 해결
  • 객체에서 양방향 연관관계를 사용하려면 견고한 로직이 필요

<참고>

  • teamAmember1 관계가 제거 되지 않아도 외래 키 변경에는 문제가 없음
  • 연관관계의 주인인 Member.team의 참조를 teamB로 변경했으므로 정상 반영
  • 문제는 관계 변경 후 영속성 컨텍스트가 있는 경우 teamAgetMembers()member1을 반환
    • 이런 문제를 해결하기 위해 관계를 제거하는 것이 안전한 방법

 

정리

  • 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것 뿐 (단방향 2개)
    member.getTeam(); //회원 -> 팀
    team.getMembers(); //팀 -> 회원 (양방향 매핑으로 추가된 기능)
  • 주인의 반대편은 mappedBy로 주인을 지정

내용 정리

  • 단방향 매핑만으로 테이블과 객체 연관관계 매핑은 이미 완료
  • 단방향을 양방향으로 만들면 반대바향으로 객체 그래프 탐색 기능이 추가
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리

※연관관계의 주인을 정하는 기준

  • 단방향항상 외래 키가 있는 곳을 기준으로 매핑
  • 양방향은 비즈니스 로직상 더 중요하다고 주인으로 선택하면 안됌
  • 양방향비즈니스 중요도를 배제하고 단순히 외래 키 관리자 정도의 의미만 부여
  • 연관관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스로 접근하면 안됌

 

<주의>

  • 양방향 매핑시 무한루프에 빠지는 것에 주의
    • 예) Member.toString()에서 getTeam()을 호출, Team.toString()에서 getMember() 호출시 무한 루프 발생
    • 이 문제는 JSON 변환할때 자주 발생, 라이브러리들은 어노테이션 기능을 제공해서 해결

<참고>

  • 일대다를 연관관계의 주인으로 선택하는 것이 가능 (6.2.1절 참고)
    • Team.members를 연관관계의 주인으로 선택이 가능
    • 하지만 성능과 관리 측면에서 권장하지 않음