-
5. [JPA] 양방향 연관관계와 연관관계 주인웹개발/Hibernate(JPA) 2020. 4. 1. 15:18
안녕하세요 현우입니다.
이번 포스팅은 [ 양방향 연관관계에 대한 이해 및 중요 TIP ] 입니다.
참고도서
http://acornpub.co.kr/book/jpa-programmig
자바 ORM 표준 JPA 프로그래밍
JPA 기초 이론과 핵심 원리, 그리고 실무에 필요한 성능 최적화 방법까지 JPA에 대한 모든 것
www.acornpub.co.kr
양방향 매핑시에 무한루프 발생 위험
- toString
- JSON 생성라이브러리 :
- lombok
class Member ... ... ... @Override public String toStrig(){ return "Member{" + "id=":+ id + ", username='" + username +'\'' + ", team=" +team + '}'" } } //Team 에서도 toString 생성
양쪽으로 계속호출 stackoverflow 엔티티를 직접 Controller 에서 response를 직접보낼때 엔티티의 연관관계가 양방향으로 걸려 있으면 계속 묶여서 무한루프가일어난다. *컨트롤러에서는 절대 엔티티를 반환하지 말자. 엔티티가 설계중 계속 변경될 수 있으므로 Dto로 변환을해서 반환하는 것을 추천한다.
이전 글에서는 회원에서 팀으로만 접근하는 @ManyToOme 다대일 단방향 매핑을 알아봤습니다. 이번에는 팀에서 회원으로 접근하는 관계를 추가하여 양방향 연관관계 매핑을 해보겠습니다.
테이블의 연관관계는 외래키 하나로 양방향이 설립된다 위 사진은 객체와 테이블의 연관관계 차이점을 보여줍니다.
우선 객체 연관관계의 경우 회원과 팀은 다대일 관계로 설정되고 팀에서 회원은 일대다 관계로 설정 되었습니다. 일대다 관계는 여러 건의 데이터와 연관관계를 맺을 수 있으므로 컬렉션을 사용하여 복잡하게 설계됩니다. 반대로 테이블 연관관계는 외래키 하나로 양방향 조회가 가능 합니다. ( 매우 편리 )
이전 단방향관계에서는 Member이 Team을 FK로 가졌지만, 반대로 Team에서 Member로 갈수 있는 방법이 없었습니다. team에서 Listmember을 넣어줘야 양방향 관계가 성립됩니다.
-
mappedBy : 지금 1:n 매핑에서 연결되어있는 클래스의 변수명과 연결시켜 준다.
@Entity public class Member{ @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; // 변수명이 mappedby와 연결되있다 } @Entity public class Team{ ... ... @OneToMany(mappedBy = "team") // add시 null포인트가 안뜨게 ArrayList를 관례로 설정한다. private List<Member> members = new ArrayList<>() ; }
Member findMember = em.find(Member.class, member.getId()); List<Member> members = findMember.getTeam().getMembers(); for(Member m : members){ System.out.println("m = " + m.getUsername()); }
- Member -> Team -> Member로 다시 조회. 양방향 연관관계이다.
연관관계 주인
@OneToMany 속성에 mappedBy를 볼 수 있습니다. 단순히 @OneToMany만 있으면 되지 mappedBy는 왜 필요한 걸까요? 사실 객체에는 양방향 연관관계 라는 것이 없습니다. 서로 다른 단방향 연관관계 2개를 어플리케이션 로직으로 잘 묶어서 양방향 처럼 보이게 할 뿐 입니다.
-
객체 : 연관관계가 2개이다.
-
회원 → 팀
-
팀 → 회원
-
-
테이블 : FK하나로 양방향 연관관계가 끝난다
-
회원 ↔ 팀
-
엔티티도 단방향으로 매핑시에 참조를 하나만 사용하므로 이 참조로 외래키를 관리하면 됩니다.
그런데 엔티티를 양방향 으로 매핑하면 회원 → 팀, 팀 ← 회원 두 곳에서 서로를 참조를 하게되고 객체의 연관관계를 관리하는 포인트는 두곳으로 늘어나게 됩니다. 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는 하나이기 때문에 둘 사이에서 차이가 발생합니다.
그렇다면 둘 중 어떤 관계를 사용해서 외래키를 관리해야 할까요?
이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리하는데 이것을 연관관계의 주인(Owner) 이라고 합니다. 이때 연관관계의 주인은 데이터베이스의 연관관계와 매핑되 외래키를 관리(등록,수정,삭제)할 수 있습니다. 반면에 주인아 아닌 쪽은 읽기만 가능합니다.
이때 연관관계를 주인으로 정할지는 mappedBy속성을 사용하면 됩니다.
Member.team vs Team.members
Team -> Member로 가는 @OneToMany 의 List Members 참조값과 Member -> Team으로 가는 @ManyToOne의 team 참조값 두개가 있습니다.
둘 중에 어떤 것을 연관관계의 주인으로 정해야 할까요?
연관관계의 주인을 정한다는 것은 사실 외래키 관리자를 선택하는 것입니다.
내가 팀을 바꾸고 싶을때 맴버에서 팀을 바꾸는 것이 맞을까? 아니면 팀에서 맴버를 바꾸는 것이 맞을까? 사실 둘다 같은 말입니다. 하지만 데이터베이스 입장으로는 참조를 어떻게하든 멤버에 있는 FK값만 업데이트 되면 됩니다.
즉 서비스를 어떻게 구성하느냐에 따라서 FK를 갖는 엔티티 클래스가 달라지는 것이지요.
아래 예제코드를 보겠습니다.
//회원 -> 팀(Member.team) 방향 class Member { @ManyToOne @JoinColumn(name ="TEAM_ID") private Team team; ... } //팀 -> 회원(Team.members)방향 class Team{ @OneToMany(mappedBy="team") //Member.class에 선언된 team을 주인으로 설정한다 private List<Member> members = new ArrayList<Member>(); ... }
회원 테이블이 외래키를 갖게 하였으므로 Member.team이 주인이 됩니다. 주인이 아닌 Team.members에는 mappedBy="team"속성을 사용해서 주인이 아님을 설정해 줍니다. mappedBy의 값으로 사용된 team은 연관관계의 주인인 Member 엔티티의 team필드를 말합니다.
양방향 연관관계 저장
양방향 연관관계를 사용해서 팀1, 회웜1, 회원2를 저장 해보겠습니다.
public void testSave() { //1팀 저장 Team team1 = new Team("team1","팀1"); em.persist(team1); //회원1 저장 Member member1 = new Member("Member1","회원"); member1.setTeam(team1); em.persist(member1); //회원2 저장 Member member2 = new Member("member2", "회원2"); member2.setTeam(team1); em.persist(member2); }
이제 회원 테이블을 조회해 보겠습니다.
SELECT * FROM MEMBER;
MEMBER_ID USERNAME TEAM_ID member1 회원1 team1 member2 회원2 team1 TEAM_ID 외래키에 팀의 기본키 값이 저장되어 있습니다. 양방향 연관관계는 연관관계의 주인이 키를 관리하기 때문에 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래키 값이 정상 입력 됩니다.
public void Test(){ team1.getMembers().add(member1); //무시 ( 연관관계 주인 x ) team2.getMembers().add(member2); //무시 ( 연관관계 주인 x ) member1.setTeam(team1); //연관관계 설정(주인) member2.setTEam(team2); //연관관계 설정(주인) }
양방향 연관관계의 주의점
*(중요)연관관계의 주인에 값을 매핑하지 않을 경우입니다.
try{ Member member = new Member(); member.setName("현우의 코딩스토리"); em.persist(member); Team team = new Team(); team.setName("1팀"); team.getMembers().add(member); //연관관계에 삽입 em.persist(team); em.flush(); em.clear(); tx.commit() }cactch (Exception e) { tx.rollback(); }finally{ em.close(); }
SQL문 외래키 삽입 실패 반대로 연관관계의 주인에만 값을 넣어 보겠습니다.
try{ Member member = new Member(); member.setName("현우의 코딩스토리"); Team team = new Team(); team.setName("1팀"); //team.getMember().add(member); //연관관계 삽입 em.persist(team); member.setTeamId(team); //연관관계 주인에 삽입 em.persist(member); em.flush(); em.clear(); tx.commit(); }
team.getMembers().add(member); 을 적어주지 않아도 team에 member 값이 추가는 된다 역방향(주인이 아닌 방향)에만 연관관계를 설정한다면 members는 주인이 아니기 때문에 외래키가 업데이트 되지 않습니다. 주인 인 team 값에만 값을 세팅해주면 외래키 값이 잡힙니다.
무한 루프 발생
// Team에서 member.toString() 호출 @Override public String toString() { return "Team{" + "Team_id=" + Team_id + ", name='" + name + '\'' + ", members=" + members + '}'; } // Member에서 teamId.toString() 호출 @Override public String toString() { return "Member{" + "id=" + id + ", name='" + name + '\'' + ", teamId=" + teamId + '}'; }
에러 코드 toString 뿐만 아니러 Json, lombok 생성 라이브러리에서도 무한루프가 발생합니다. Controller에서 엔티티를 직접 응답 보낼때 엔티티에 연관관계가 양방향(Member ↔ Team) 으로 설정되어 있다면, 값을 json으로 변경시 Memer 와 Team이 계속 묶여서 무한루프가일어나게 됩니다.
컨트롤러에서 엔티티를 반환하면 JSON으로 반환되어 무한루프가 발생한다.
+추가 (엔티티로 API 변경하면 API스펙이 바껴버린다)
엔티티가 설계중 계속 변경될 수 있으므로 DTO로 변환을 해서 반환하는 것을 추천합니다.
순수한 객체까지 고려한 양방향 연관관계
그렇다면 정말 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 될까요?
사실은 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 안전합니다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있습니다.예를 들어 JPA를 사용하지 않고 엔티티에 대한 테스트 코드를 작성한다고 가정해봅시다. ORM은 객체와 관계형 데이터베이스 둘 다 중요합니다. 데이터베이스뿐만 아니라 객체도 함께 고려해야 합니다.
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(team2); List<Member> members = team1.getMembers(); System.out.println("members.size = " + members.size()); } // 결과: member.size = 0 **2가지 오류 Case** 1. em.clear() / em.flush() - 이때는 DB에서 select 문이 나가지 않는다. 1차 캐시에서 값을 가져오는데 team의 컨텍스트는 비어져있으므로 당연히 출력시 값을 가져올 수 없게된다. 2. Jpa 를 사용하지 않고 Test - member.getTeam() 값은 나오지만 반대의 team.getMembers() 는 null 값이 나올 수 있다.
연관관계 편의 메서드
양방향 연관관계는 결국 양쪽 다 신경 써야 합니다. member.setTeam(team)과 team.getMembers().add(member)를 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있습니다. 그래서 Member 클래스의 setTeam() 메서드를 수정해서 코드를 리팩토링 해보겠습니다.
public class Member{ private Team team; public void setTeam(Team team) { //양방향 매핑 주의! this.team = team; team.getMembers().add(this); } // ... }
setTeam() 메서드 하나로 양방향 관계를 모두 설정하도록 변경했습니다.
이렇게 수정한 메서드를 사용하는 코드를 보겠습니다.public void test() { 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", "회원1"); member2.setTeam(team1); em.persist(member2); }
이렇게 한 번에 양방향 관계를 설정하는 메서드를 연관관계 편의 메서드라 합니다.
//수정필요
2. 무한루프
양방향 매핑시에 무한루프 발생 위험
- toString
- JSON 생성라이브러리 :
- lombok
연관관계 편의 메서드 작성 시 주의사항
member.setTeam(team1); member.setTEam(team2); Member findMember = teamA.getMember(); // member1이 여전히 조회된다.
teamB로 변경할 때 teamA → member1 관계를 제거하지 않았기 때문에 teamA.getMember() 메서드를 실행했을 때 member1이 남아있습니다.
연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 합니다.
아래는 Member 클래스 setTeam() 메서드입니다.
public void setTeam(Team team){ if(this.team != null) { // this.team이 null이 아니면 이 member객체는 team이 있음을 의미 this.team.getMembers().remove(this); // 해당 팀의 멤버에서 삭제 } this.team = team; team.getMembers().add(this); }
양방향 매핑 정리
양방향 매핑 규칙 객체의 두 관계중 하나를 연관관계의 주인으로 지정한다 연관관계의 주인만이 외래키를 관리한다(등록, 수정, 삭제) 주인이 아닌쪽은 읽기만 가능하다 주인은 mappedBy속성을 사용 할 수 없다. 주인이 아니면 mappedBy속성으로 주인을 지정해줘야 한다. class Team{ @OneToMany(mappedBy = "team") //나의 주인님은 Mamber 클래스의 "team"변수님이야! private List<Member> members = new ArrayList<>(); }
Team클래스에서는 조회만 가능하다 <내용 정리>
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되어 있다.
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
- 양방향 매핑은 매우 복잡하므로 객체그래프 탐색기능이 필요할 때 양방향을 사용하도록 코드를 추가 하자.
코드 자세히 보기 (Github)
https://github.com/HyeonWuJeon/KimYoungHan-JPA
HyeonWuJeon/KimYoungHan-JPA
자바 ORM표준 JPA 프로그래밍 책 정리 및 예제 작성. Contribute to HyeonWuJeon/KimYoungHan-JPA development by creating an account on GitHub.
github.com
추가 학습 자료
https://www.inflearn.com/course/ORM-JPA-Basic
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 프로그�
www.inflearn.com
'웹개발 > Hibernate(JPA)' 카테고리의 다른 글
10. 즉시 로딩과 지연 로딩 (0) 2020.04.28 9. 프록시와 연관관계 (0) 2020.04.25 7. 고급매핑 (0) 2020.04.20 6. [JPA] 다양한 연관 관계매핑 방법 (0) 2020.04.10 4. [JPA] 연관관계 매핑 (0) 2020.04.01