Springboot Jpa - 연관관계 매핑 기초
연관관계의 매핑하는 방법을 알아보자.
관계형 DB와 객체의 패러다임 차이에서 오는 어려움이 있지만, 기초부터 찬찬히 학습해보자.
단방향 연관관계
객체에서 참조가 한 방향으로만 되어있는 관계를 단방향 연관관계라고 한다.
연관관계가 필요한 이유
관계형 DB와 객체의 차이
- 테이블: 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
- 객체: 참조를 사용해서 연관된 객체를 찾는다.
이러한 차이를 극복하기 위해서는 연관관계가 필요하다.
단방향 연관관계
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@Column(name = "TEAM_ID")
private Long teamId;
// 생략
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
// 생략
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 생략
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
// 생략
}
객체를 테이블에 맞춰 모델링시 team의 id를 저장하며, Member
에서 Team
의 정보는 team_id
로 한번 더 Team
의 정보를 조회해야한다. 이러한 방식은 객체 지향적이지 않은 방식이다.
객체 지향 모델링을 한다면 team을 저장하며, Member
가 Team
의 정보를 가지고 있어, team_id
로 한번 더 조회하지 않아도 된다.
양방향 연관관계
관계형 DB에서는 연관관계의 방향이 따로 없다. MEMBER
테이블을 중심으로, TEAM
의 정보를 조회하는 것과 TEAM
테이블을 중심으로, MEMBER
정보를 조회하는 것은 MEMBER
테이블의 외래키 TEAM_ID
로 다 가능하기 떄문이다.
그런데 객체 지향에서는 다르다. 아까 예제에서, Team
에서 getMembers()
를 한다면 팀원 정보가 나올 수 없다. 팀원 정보를 보여주기 위해서는 양방향 매핑을 해주어야한다.
이것이 객체 참조와 테이블의 외래키의 가장 큰 차이점이다.
양방향 매핑
위의 예제를 양방향 매핑으로 바꿔보자.
1
2
3
4
5
6
7
8
9
10
11
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<Member>();
//생략
}
Member는 유지하고, Team에만 추가한다.
- 6~7
Team
에Member
에 대한 연관관계를 추가했다.
Team
-> Member
방향의 객체 그래프 탐색과 Member
-> Team
방향의 객체 그래프 탐색이 모두 가능하다.
객체와 테이블의 관계를 맺는 차이
이제 테이블의 연관관계와 같을까? 이렇게 변경한다고 해도 차이가 존재한다.
- 객체 연관관계
- 2개
- 회원 -> 팀 연관관계 1개 = 참조값이
Member
에서Team
참조 ,단방향- 팀 -> 회원 연관관계 1개 = 참조값이
Team
에서Member
참조 ,단방향- 객체의 양방향 관계는 사실 양방향 관계가 아닌, 다른 단방향 2개를 의미한다.
- 회원 -> 팀 연관관계 1개 = 참조값이
- 테이블 연관관계
- 1개
- 회원 <-> 팀의 연관관계 1개 = 참조값이
MEMBER
테이블의TEAM_ID
의 참조로 양방향 조회, 양방향 - 회원 <-> 팀의 연관관계 1개 = 참조값이
연관관계의 주인
딜레마 발생
방금 양방향 매핑을 한 객체의 연관관계에서, 멤버가 팀을 변경한 상황이라고 생각해보자.
MEMBER 테이블의 TEAM_ID 업데이트는 어느 경우에 되어야할까?
Member
에서team
을 변경할 때Team
의members
가 변경되었을 때
사실 둘 다 수정을 해야하는 경우가 맞고, 사실 DB에서는 TEAM_ID만 변경되면 된다.
이 경우를 대비해 연관관계의 주인을 정하는 규칙이 생겼다. Member
와 Team
중 하나만 외래키를 관리하는 주인으로 지정하는 것이다.
연관관계의 주인
- 양방향 매핑 규칙
- 객체의 두 관계중 하나를 연관관계의 주인으로 지정한다.
- 등록, 수정이 있을 때, 연관관계의 주인만이 외래 키를 관리한다.
- 주인이 아니면
mappedBy
속성으로 주인 지정하고, 주인은@JoinColumn
을 사용해 외래키를 매핑한다.- 주인이 아닌쪽의 참조데이터(=
Team
의memebers
)는 읽기만 가능하다. - 등록, 수정이 있을 때, 연관관계의 주인만이 외래 키를 관리한다.
- 연관관계의 주인을 정하는 가이드
- 외래키가 있는 곳을 주인으로 지정한다.
외래키를 포함하는 곳을 주인으로 지정한 이유
Team
을 변경시MEMBER
테이블에 대한 query가 생성되는 것보다,Member
를 변경시MEMBER
테이블에 대한 query가 생성되는 것이 더 직관적이다.- 반대의 경우를 지정하면 성능 이슈(이후 내용에서 설명)
가장 많이하는 실수
연관관계의 주인에 값을 미입력
1
2
3
4
5
6
7
8
9
10
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
team.getMembers().add(member);
em.persist(member);
- 1~3 팀 생성 및 저장
- 5~6 멤버 생성
- 8 실수하는 부분! 역방향(주인이 아닌 방향,
team
)만 연관관계 설정 - 역방향인
team
에 값을 수정해도 읽기 전용이라 아무런 일도 발생하지 않는다.
- 10 멤버 저장, 결과 = 멤버는 저장이되지만, 해당멤버의 팀 정보는 누락된다.
- 8 실수하는 부분! 역방향(주인이 아닌 방향,
연관관계 편의 메서드 생성
양방향 매핑에 있는 양쪽(주인과 주인이 아닌 방향) 모두에 값을 넣어주자.
- 값을 연관관계의 주인에게 입력하는 것을 누락하는 실수를 방지하기 위해
- 순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 해서
- 또 다른 실수를 방지하기 위해
1 2 3 4 5 6 7 8 9 10
Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); team.getMembers().add(member); member.setTeam(team);//연관관계의 주인에 값 설정 em.persist(member);
- : 한쪽에만 값을 넣어준다면,
team.getMembers()
호출시 제대로 값이 나오지 않아 문제가 발생할 가능성이 생긴다.commit()
이나flush()
,clear()
,team
새로 조회 를 하기 전까지는team
의 정보는 1차 캐시에서 조회되기 때문에 값이 조회되지 않는다. - 테스트 케이스 작성시에는 JPA없이 자바 코드 상태로 작성하는데, 이때에도 문제가 된다.
- : 한쪽에만 값을 넣어준다면,
가이드
항상 양쪽에 값을 넣어주면 되는데, 모든 코드에 대해서 하기가 번거롭기도 하고 그러려고 했지만 누락되는 경우도 있다. 이런 경우를 방지해 편의 설정을 위한 메서드를 생성하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
public class Member {
// 생략
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
// 생략
}
- 11
team
이 변경시 사용되는Member.changeTeam()
을 정의하고, 내용으로team
변경과team.members
에도 같이 추가될 수 있도록 작성한다. - 조금 더 세세히 작성하자면, 기존에 있던 팀의 members에서 자신을 제거 후 새로운 곳에 추가해야한다.
- 반대로
Team
에서member
를 저장하도록 하는 방법도 있는데 이건 개인적으로 선택해 사용하면 된다. - 반대로
- 11
무한 루프
가능성
- 연관관계 편의 메서드를 양방향에서 다 작성
member
의 편의 메서드가team
의 편의메서도 호출 ->team
의 편의 메서드가member
의 편의 메서드 호출 … 무한루프
toString()
정의시 IDE를 이용한 방식이나 lombok을 이용한 방식으로 작성하는 것member.toString()
에서team.toString()
호출 ->team.members.toString()
에서 각 멤버에 대한member.toString()
호출 … 무한루프
- JSON 라이브러리 ->
Controller
에서 직접Entity
리턴시, 응답을 하기 위해Entity
를 json으로 변환하려 한다. - JSON 변환시에도 위와 비슷한 방식으로, 전부 각 필드의 값을 모두 변환하기 위해 계속 호출해 무한루프가 생성된다.
- JSON 라이브러리 ->
가이드
- 연관관계 편의 메서드를 양방향에서 다 작성 -> 한 쪽을 정해서 한쪽만 작성
toString()
-> lombok의@toString
사용 지양- JSON 라이브러리 ->
Controller
에서Entity
반환 금지, DTO 사용
Controller
에서Entity
직접 반환 금지를 권고하는 또 다른 이유 JSON의 문제 외에도Entity
를Controller
에서 반환하게 된다면,Entity
변경시 마다 API 스펙이 변경되기 때문에 DTO 사용을 권장한다.
정리
지금까지 단방향과 양방향의 연관관계가 무엇인지, 어떻게 설정하는지, 또 양방향 연관관계 설정시 많이하는 실수에 대해 알아보았다.
추가로 실제 JPA 설계시에는 단방향 매핑만으로도 이미 연관관계 매핑이 되도록 설계하고, 이후 반대 매핑이 필요한 부분만 양방향으로 설계하는것이 좋다. 양방향을 나중에 고려하는 이유는 양방향에 대한 연관관계는 jpa 코드만 추가 작성되면 되기 때문에 언제든 수정할 수 있기 때문이다. 만약 단방향을 추가하는 경우라면, DB에 영향이 가게된다.