본 글은 김영한님의 [인프런 - 자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의와 자바 ORM 표준 JPA 프로그래밍 책을 기반으로 정리해 작성하였습니다.
오늘은 JPA를 배우면서 정말 중요한 연관관계 매핑, 그리고 더 중요한 연관관계의 주인 설정에 관해 알아볼 것이다.
먼저 연관관계 매핑에 대해 공부하는 이유를 한 문장으로 설명하자면 다음과 같다. (필자의 생각)
객체에다가 테이블처럼 외래 키를 저장해두면 협력 관계를 가질 수 없기 때문에 객체의 참조와 테이블의 외래 키를 매핑하는 것이 필요하다.
단순히 정리하면, 객체에는 외래 키를 저장하지 않을 것이고, 객체의 참조와 테이블의 외래키를 매핑할 것이다.
이 부분은 JPA에서 매우 어려운 부분에 해당하고 아래에서는 강조한 위 문장이 무슨 뜻인지, 그리고 어떻게 해결하는 지에 대한 내용들을 정리해두었다.
1. 단방향 연관관계
연관관계 중에서는 다대일(N:1) 단방향 관계를 가장 먼저 이해해야 한다. 다음은 단방향 연관관계의 예시이다.
- 회원과 팀이 있다.
- 하나의 팀엔 여러 회원이 속해있다.
- 하나의 회원은 하나의 팀에만 속할 수 있다.
그림을 통해 살펴보면,
먼저 객체 연관관계에서 Member 객체는 객체 내의 Team 필드를 통해서 회원이 어떤 팀에 속해 있는지 알 수 있지만, Team 객체에서 회원을 조회할 수 없다. (단순히 그림을 보면, Team에서 Member로 가는 화살표가 없다는 것을 알 수 있다.)
하지만 테이블 연관관계는 FK, PK 관계를 이용해서 서로의 테이블을 조인할 수 있다. (서로 FK, PK를 알면 JOIN ON을 이용해 연결할 수 있다는 것이다.)
따라서 참조를 사용하는 객체의 연관관계는 단방향, 외래 키를 사용하는 테이블의 연관관계는 양방향이다. (위 그림 참조)
만약 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야하는데, 위 예시대로면 Team 객체에도 Member 필드를 만들어야한다.
일단 객체와 테이블에 대해서는 이정도 차이만 알아두고 이제 JPA를 사용해 둘을 매핑하면 다음과 같다.
4일차에서 매핑했던 것과 똑같고 코드는 다음과 같다.
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private Long id;
private String username;
//연관관계 매핑
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
//연관관계 설정
public void setTeam(Team team) {
this.team = team;
}
}
연관관계 매핑이라고 주석친 부분과 그림을 잘 보자.
//연관관계 매핑
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
이 부분은 객체의 연관관계(참조) 부분이다.
그 위에 쓰여있는 어노테이션들은 JPA에서 지원하는 연관관계 매핑, 즉 객체와 테이블 매핑 부분이 되겠다.
- @ManyToOne : 이름 그대로 다대일(N:1) 관계라는 매핑 정보다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
- @JoinColumn (name = "TEAM_ID") : 조인 컬럼은 외래 키를 매핑할 때 사용한다. 이 어노테이션을 생략 시 "필드명 + _ + 참조하는 테이블의 컬럼명" 으로 외래 키가 설정된다.
@ManyToOne 에는 fetch, cascade 등 아주 중요한 속성들이 있지만, 이번 포스팅에서 설명하기엔 너무 복잡하고 어려운 내용들이라 다음에 설명하겠다. (참고 글)
다음은 이렇게 만들어진 연관관계를 사용하는 코드이다.
1) 회원 M에 팀 T 저장하기
public void saveTeamT() {
//팀 T 저장
Team teamT = new Team("teamT", "팀T");
em.persist(teamT);
//회원 M 저장
Member memberM = new Member("memberM", "회원M");
//회원 M에 있는 팀에 연관관계 설정
memberM.setTeam(teamT);
em.persist(memberM);
}
2) 회원 M의 팀 조회하기
Member m = em.find(Member.class, "memberM");
Team team = m.getTeam();
3) 회원 M의 팀 연관관계 제거
Member m = em.find(Member.class, "memberM");
m.setTeam(null); //연관관계를 null로 설정 -> 제거
2. 양방향 연관관계
이번엔 팀에서도 회원을 조회할 수 있는 연관관계를 만들어보겠다.
테이블 연관관계는 이전과 같고, 객체 연관관계에서 Team 객체 필드에 List 컬렉션이 추가되었다.
Team은 반대로 Member와 일대다 관계이기때문에 반드시 컬렉션을 사용해야 한다.
Team 엔티티에 매핑 설정을 한 코드를 보자.
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
}
설명대로 객체 연결 부분엔 List 컬렉션을 사용했고, 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했다.
mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드명을 값으로 주면 된다. 자세한 내용은 아래에서 나오는 연관관계 주인에서 바로 설명하도록하겠다.
3. 연관관계의 주인
@OneToMany는 @ManyToOne의 반댓말로, 처음보면 그 의미 자체는 이해하기 쉬울 것이다.
하지만 그 옆에 붙어있는 mappedBy 속성은 왜 필요할까? 그 답은 바로 연관관계의 주인을 정하기 위해서이다.
반복해서 이야기할 정도로 이번 장에서 중요한 내용이지만 다시 한 번 언급하자면, 객체와 테이블은 근본적으로 연관관계에 있어서 차이가 존재한다.
테이블은 외래 키 하나만으로 양쪽다 참조가 가능하지만, 객체는 위와 같이 참조를 총 2개 사용해야 양방향으로 매핑이 가능하다는 것이다.
따라서 둘 사이에 차이가 발생한다. 이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.
Tip. 공부하면서 계속 헷갈리는 부분이라 처음부터 짚고 넘어가자면, 양방향 매핑으로 인해 연관관계의 주인이란 개념이 나온 것이다. 단방향 관계를 설정할때는 전혀 상관없는 내용이다. 계속해서 새로운 단어가 등장할텐데, 등장하는 이유를 짚고 넘어갈 필요가 있다.
규칙
연관관계의 주인에는 규칙이 있는데, 우선 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다.
즉, 등록 수정 삭제는 연관관계의 주인만이 가질 수 있는 기능이다. 반면 주인이 아닌 반대 쪽은 읽기만 가능하다.
어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 된다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다. ex) mappedBy="주인"
이러한 규칙으로 인해 만약 우리가 남의 코드를 보고 연관관계의 주인을 찾고자 할 때는 mappedBy가 어느쪽에 걸려있는 지만 보면 쉽게 찾을 수 있을 것이다.
그렇다면 우리는 어떤 것을 연관관계의 주인으로 설정해야 할까?
일반적으로 외래 키가 존재하는 테이블과 물리적으로 밀접한 엔티티에 있는 쪽을 연관관계의 주인으로 정한다.
왜냐하면 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이기 때문이다.
따라서 이 때는 서로 관계가 있는 참조 (Member.team, Team.members) 둘 중 Member.team이 주인이 된다.
주의점
연관관계의 주인을 설정하고 데이터베이스를 다룰 때 꼭 알아야 할 주의사항이 두 가지 있다.
1) 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력한 경우
이 경우 만약 주인이 아닌 곳에 값을 추가했다면, 나중에 db에 조회 쿼리를 날렸을 때 해당 값이 저장되지 않는다. (기존에 값이 없던 경우 null로 표시될 것이다)
원인은 당연히 외래 키의 값을 변경할 수 있는 것은 주인이기 때문에 테이블에는 반영되지 않는 것이다.
2) 주인에만 값을 저장하는 경우
이 경우 문제가 없어 보인다. 실제로 필자도 처음 볼 때 무슨 문제인지 이해하기 어려웠지만 실제 코드를 짜다보니 이해가 되었다.
만약 주인에만 값을 저장하는 경우, JPA 특성상 트랜잭션이 커밋되어야 db에 제대로 반영이 될텐데 그 사이에 주인이 아닌 객체를 이용한다면 이전에 저장된 값을 다루는 작업이 불가능해진다.
좀 더 간단히 말하자면, 우리가 지금까지 연관관계의 주인을 하나로 설정하는 이유는 객체 관계를 외래 키가 하나인 테이블쪽에 맞춘 것이지 테이블을 거치기 전 상태의 객체 관점에서 다루려면 객체처럼 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다는 것이다.
이때 양쪽 다 입력하다보면 실수로 둘 중 하나만 호출해서 양방향이 깨지는 경우가 있는데, 이럴 땐 다음과 같이 한 쪽에 추가할 때 다른 쪽도 추가해주는 방법을 사용하면 좋다.
public class Member {
private Team team;
public void setTeam(Team team) {
this.team = team; //주인 -> 하인
team.getMembers().add(this); //하인 -> 주인
}
}
하지만 이때 만약 팀이 있던 멤버 A가 갑자기 다른 팀으로 이적하면 다음과 같은 문제가 발생한다.
- 멤버 A는 팀 X 소속이었다. 둘은 서로 양방향으로 연결된 상태
- 멤버 A가 팀 Y로 이적을 했고, A-Y는 양방향으로 연결을 시켜줬다.
- 팀 X의 멤버를 조회해보니 멤버 A가 존재한다.
이 경우를 사전에 방지하고자, setTeam 메소드에서 만약 팀이 존재한다면 꼭 이전 팀에 대해 remove 연산처리를 해줘야한다.
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); //하인 -> 주인
}
}
위 상황들에 대해 그림, 추가 코드를 통해 훨씬 자세히 설명해놓은 블로그가 있다.
* 잼민이의 질문 모음
(정리하면서 생긴 궁금증과 책에 있는 Q&A 내용들을 정리해두었습니다.)
Q1. 외래 키와 밀접하지 않은 객체쪽에서 연관관계의 주인을 가질 수는 없나요?
A1. 그건 아니란다. 다만 밀접한 객체쪽 (본인 테이블에 외래 키가 있다면) 엔티티의 저장과 연관관계 처리가 insert 쿼리 한 번에 끝나지만 다른 테이블에 외래 키가 있다면 연관관계 처리를 위해 update 쿼리를 추가로 실행해야 한단다. 그래서 성능에 차이가 있어
Q2. 자꾸 주인 주인(master)하는데 인종 차별적 발언 아닌가요?
A2. 그래서 요즘 github에서는 브런치 이름을 master -> main 으로 바꿨다고 하더라.. 너는 중심으로 이해하렴..
'JPA' 카테고리의 다른 글
[JPA] 배워서 엔터프라이즈 개발하자 7일차 - 프록시와 연관관계 관리 (2) | 2022.03.12 |
---|---|
[JPA] 배워서 엔터프라이즈 개발하자 6일차 - 일대다, 다대다 관계의 단점 (0) | 2022.03.12 |
[JPA] 배워서 엔터프라이즈 개발하자 4일차 - 엔티티 매핑 (0) | 2022.03.12 |
[JPA] 배워서 엔터프라이즈 개발하자 3일차 - 영속성 관리 (0) | 2022.03.12 |
[JPA] 배워서 엔터프라이즈 개발하자 2일차 - JPA 시작 (0) | 2022.03.12 |