연관관계
- 객체와 테이블 연관관계의 차이를 이해한다.
- 객체의 참조와 테이블의 외래 키를 매핑한다.
연관관계가 있다는 것은 테이블이 조회, 생성, 삭제 시 서로 연관되어 있다는 의미입니다. 예를 들어, 축구선수A가 B팀에 입단하면, 축구선수A 정보에 B팀을 추가해야하며, B팀 리스트에도 축구선수가 추가되어야 합니다. 만약 C팀으로 옮기면, 축구선수 A는 소속팀을 C팀으로 바꿔야 하고, B팀 리스트에서 선수A를 삭제합니다.
연관관계의 기본 개념은 다음과 같습니다.
<기본개념>
- 1:N 혹은 N:1 관계에서 외래키는 항상 N쪽에 있다.(외래키의 주인은 N이다.)
- 1:1 관계에서 외래키의 위치는 설계에 따라 달라진다.
- (N:M 관계는 다른 게시물에서 설명)
JPA의 연관관계 매핑을 학습하기 이전에, 기본개념을 꼭 숙지하여야 외래키 관리, 외래키 주인을 헷갈리지 않습니다.
기존의 "테이블"에 맞추는 모델링
테이블은 외래 키인 TEAM_ID(FK)로 조인을 사용해서 연관된 테이블을 찾습니다. 전통적으로 테이블 관계에서 다른 테이블을 찾을 때 사용하는 방법입니다. 대부분 쿼리문을 작성할 때는 다른 테이블의 FK를 참조합니다.
하지만, 객체는 "참조"를 사용해서 연관관계를 찾습니다.
"객체 지향" 모델링
테이블 연관관계에서 FK를 가질 경우, private Long teamId;를 칼럼으로 두지 않고 객체지향적인 관점에 따라 Team team 정의합니다. @JoinColumn로 FK를 명시합니다.
1. 단방향 매핑
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
//private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@JoinColumn로 객체 연관관계의 Team team을 테이블 연관관계의 TEAM_ID(FK)로 외래키를 설정합니다.
2. 양방향 매핑
양방향 매핑이 필요한 이유는 무엇일까요?
=> 테이블 연관관계의 경우 FK를 가지고 있으면 FK를 이용해서 테이블을 자유롭게 조회 할 수 있지만, 객체 연관관계에서는 본인 엔티티에 반대 테이블 정보를 가지지 않으면 조회가 불가능합니다. 따라서, FK가 없는 곳에서는 반대 엔티티를 "조회"할 수 있도록 List를 만들어줍니다.
양방향 매핑을 만들 경우, 연관관계를 "관리" 할 주인을 설정해야 합니다. 주인은 @JoinColum을, 주인이 아닌 경우 mappedBy를 사용합니다. mappedBy를 붙인다는 것은 다음과 같습니다. "내가 관리의 주인이 아니고, mappedBy의 name이 나의 연관관계 주인이야, mappedBy가 설정된 칼럼을 통해서 반대 엔티티를 조회할 수 있어!"
( mappedBy는 테이블과는 전혀 관계가 없다. mappedBy는 "읽기"만 가능하다. 쉽게말해 "가짜 매핑"이다. 즉, mappedBy를 이용한 칼럼은 있어도 그만, 없어도 그만이다. )
아래 코드에서 mappedBy를 활용해 Team에서도 Member를 조회 할 수 있도록 했습니다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
@Column
private String name;
//양방향 매핑 조회를 목적으로 새롭게 추가된 List!
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<>();
}
아래 코드처럼, 테이블은 FK 칼럼을 하나 만들어주면 연관된 엔티티가 서로 조회가 가능합니다.
//Member에서 Team 조회
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
//Team에서 Member 조회
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
아래 코드처럼, JPA를 사용하면 엔티티가 서로 양방향 매핑을 하여야만 서로 조회를 할 수 있습니다.
//Member.java
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
====================================
member.getTeam();//Member에서 Team조회
//Team.java
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<>();
====================================
team.getMembers();//Team에서 Member조회
정리하자면 다음과 같습니다.
1. 객체의 양방향 관계는 서로 다른 단뱡향 관계 2개다.
2. 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다(@JoinColum, mappedBy)
* 외래키 "관리"는 누가?
양방향 매핑에서 외래키를 "관리"하는 주인을 정하는 것을 좀 더 자세히 확인해봅시다. 외래키 "소유"와 "관리"는 다른 개념입니다. ( 1: N 관계에서 N이 외래키를 "소유" 한다는 것이 기본개념이었다. 외래키를 소유하는 엔티티는 이미 구조적으로 확정되어 있습니다). 외래키를 "관리"한다는 것은 @JoinColumn을 가지고 있다는 의미입니다. @JoinColumn은 단순히 FK를 설정하는 것 뿐 아니라, 실제 DB에 데이터 저장, 수정, 삭제의 권한이 있습니다. 따라서 DB를 변경시킬 이 권한을 가질 엔티티를 선택하는 것은 중요합니다.
지금까지 Member가 @JoinColumn으로 외래키를 "관리"하는 것을 계속 배웠지만, 이론적으로 Team에서 List members가 @JoinColumn을 가질 수도 있습니다. 따라서 2개의 엔티티 중에 어느 엔티티가 @JoinColumn을 가질지 결정해야 합니다.
결론은 외래키를 가지고 있는 쪽이 외래키를 "관리"하는 주인이 됩니다. (Member, Team 중에서 Member)
//Member.java
@ManyToOne
@JoinColumn(name = "TEAM_ID") //1:N의 N에 설정
private Team team;
//Team.java
@JoinColumn(name = "TEAM_ID") //1:N의 1에 설정도 가능하다!
List<Member> members = new ArrayList<>();
아래코드를 보시면, @JoinColumn을 Member.java의 Team team이 가지고 있는 것을 알 수 있습니다. 외래키 관리는 Team team이 합니다.
//Member.java
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
//Team.java
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<>();
@JoinColumn을 가지고 있는 Team team이 team.add(member);를 하면, member가 team에 추가됩니다.
@JoinColumn이 없는 List<Member> members가 members.add(team);을 해도 member가 team에 추가 되지 않습니다.
칼럼이 @JoinColumn을 가지고 있어야만, 실제 DB에 데이터를 저장, 수정, 삭제가 반영되기 때문입니다.
mappedBy를 가지고 있는 칼럼은 단순히 "조회"만 가능합니다
그러면 양방향 매핑에서 @JoinColumn을 가진 엔티티 쪽만 변수를 만들면 될까요?
실무에서는, 항상 양쪽에 다 값을 입력해주는 것이 안전합니다.
이유는 다음과 같습니다.
1. 영속성 컨텍스트의 em.flush() 여부에 따라서 의도와 다른 결과가 나올 수 있습니다.
Team team = new Team();
team.setName("TeamA");
em.persist(team); //영속성 컨텍스트에 추가
Member member = new Member();
member.setName("member1");
member.setTeam(team); //연관관계의 주인에 값 설정
em.persist(member); //영속성 컨텍스트에 추가
//em.flush();
//em.clear();
Team findTeam = em.find(Team.class, team.getId()); //1차 캐시
List<Member> members = findTeam.getMembers(); //Team에 member 반영 안된다!
만약 em.flush()가 누락되었다면, findTeam은 캐시로 조회된 처음 순수한 team의 상태입니다. 왜냐하면 team과 member가 영속성 컨텍스트에 있을 뿐, DB에 반영이되지 않은 상태이기 때문입니다. member에는 team이 있지만, team에는 member가 없습니다.
2. 테스트코드 작성 시, 순수 자바로 작성하는 경우가 있기 때문에 직접 추가해야한다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team); //연관관계의 주인에 값 설정
team.getMembers().add(member); // !!!양방향 매핑이면 서로 해준다!!!
em.persist(member);
순수한 자바 코드 작성 시, 마찬가지로 테스트를 위해서 양방향으로 추가를 해주어야 각 엔티티에 값이 정상적으로 들어간 상태로 테스트가 가능합니다.
결론
1. 엔티티의 연관관계는 객체 참조를 하기 때문에 외래키 자체가 아닌 객체를 칼럼으로 가지고 있다.
2. 엔티티의 단방향 매핑만으로도 DB 설계를 모두 온전하게 표현할 수 있다.
3. 엔티티의 양방향 매핑은 단방향 매핑 2개이기 때문에 외래키를 관리하는 주인이 필요하다.
4. 외래키 주인은 무조건 외래키를 가지고 있는 엔티티이다.
(1:N에서는 외래키가 N에 있는 것이 DB 설계의 전제이다.)
5. 외래키를 관리하는 곳에서만이 저장, 수정, 삭제의 권한이 있다.
6. 외래키가 없는 곳에서는 단순히 조회만 가능할 뿐이다.
7. 양방향 매핑에서 외래키 관리는 @JoinColumn을, 그 반대는 mappedBy로 설정한다.
8. 그럼에도 양방향 매핑에서 저장, 수정, 삭제는 엔티티 서로가 모두 진행해야 안전하다.
* 참고
'Spring > Spring JPA' 카테고리의 다른 글
JPA 프록시(+즉시로딩, 지연로딩 비교) (0) | 2021.09.14 |
---|---|
N:M 테이블 관계 설계하기 (0) | 2021.09.13 |
N:1 테이블 관계 설계하기 (0) | 2021.09.07 |
1:N 테이블 관계 설계하기 (0) | 2021.09.07 |
1:1 테이블 관계 설계하기 (0) | 2021.09.07 |