객체 관계 매핑(ORM)에서 가장 어려운 부분이 바로 객체 연관관계와 테이블 연관관계를 매핑하는 일이다.
객체의 참조와 테이블의 외래 키를 매핑하는 것이 이 장의 목표다.
-> 방향, 다중성, 연관관계의 주인
5.1 단방향 연관관계
* 회원과 팀이 있다.
* 회원은 하나의 팀에만 소속될 수 있다.
* 회원과 팀의 다대일 관계다.
- 객체 연관관계
- 회원 객체는 Member.team 필드(멤버변수)로 팀 객체와 연관관계를 맺는다.
- 회원과 팀 객체는 단방향 관계다. 회원은 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없다.
테이블 연관관계
- 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
- 회원 테이블과 팀 테이블은 양방향 관계다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있다.
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.ID;
객체 연관관계와 테이블 연관관계의 가장 큰 차이
- 참조를 통한 연관간계는 언제나 단방향. 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 결국 연관관계를 하나 더 만들어야 한다.
- 이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라 한다. 정확히 이야기하면 이것은 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
- 반면에 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.
단방향 연관관계다.
class A {
B b;
}
class B { }
양방향 연관관계다.
class A {
B b;
}
class B {
A a;
}
5.1.1 순수한 객체 연관관계
JPA를 사용하지 않은 순수한 회원과 팀 클래스의 코드다.
public class Member {
private String id;
private String username;
@Getter
@Setter
private Team team; //팀의 참조를 보관
}
@Getter
@Setter
public class Team {
private String id;
private String name;
}
회원1과 회원 2를 팀 1에 소속시키자.
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team);
member2.setTeam(team);
Team findTeam = member1.getTeam();
객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라고 한다.
5.1.2 테이블 연관관계
CREATE TABLE MEMBER (
MEMBER_ID VARCHAR(255) NOT NULL,
TEAM_ID VARCHAR(255),
USERNAME VARCHAR(255),
PRIMARY KEY (MEMBER_ID)
)
CREATE TABLE TEAM (
TEAM_ID VARCHAR(255) NOT NULL,
NAME VARCHAR(255),
PRIMARY KEY (TEAM_ID)
)
ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM
FOREIGN KEY (TEAM_ID) REFERENCES TEAM;
5.1.3 객체 관계 매핑
지금까지 객체만 사용한 연관관계와 테이블만 사용한 연관관계를 각각 알아보았다.
@Entity
@Getter
@Setter
pubilc class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
//연관관계 매핑
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
@Getter
@Setter
pubilc class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
}
* 객체 연관관계 : 회원 객체의 Member.team 필드 사용
* 테이블 연관관계 : 회원 테이블의 MEMBER.TEAM_ID 외래 키 칼럼을 사용
Member.team 과 MEMBER.TEAM_ID를 매핑하는 것이 연관관계 매핑이다. 연관관계 매핑 코드를 분석해보자.
5.1.4 @JoinColumn
@JoinColumn은 외래 키를 매핑할 때 사용한다.
속성 | 기능 | 기본값 |
name | 매핑할 외리 키 이름 | 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명 |
referencedColumnName | 외래 키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본 키 컬럼명 |
foreignKey(DDL) | 외래 키 제약조건을 직접 지정. 테이블을 생성할 때만 사용 | - |
unique nullable insertable updatable columnDefinition table |
@Column 속성과 같음 | - |
5.1.5 @ManyToOne
@ManyToOne 어노테이션은 다대일 관계에서 사용한다.
속성 | 기능 | 기본값 |
optional | false로 설정하면 연관된 엔티티가 항상 있어야 함 | true |
fetch | 글로벌 페치 전략을 설정 | @MayToOne=EAGER, @OneToMany=LAZY |
cascade | 영속성 전이 | - |
targetEntity | 연관된 엔티티의 타입 정보를 설정(제너릭) | - |
다음 코드는 targetEntity 속성의 사용 예다.
@OneToMany
private List<Member> members; // 제네릭으로 타입 정보를 알 수 있다.
@OneToMany (targetEntity=Member.class)
private List members; // 제네릭이 없으면 타입 정보를 알 수 없다.
5.2 연관관계 사용
5.2.1 저장
//팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
//회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team);
em.persist(member1);
5.2.2 조회
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); //객체 그래프 탐색
5.2.3 수정
//새로운 팀2
Team team2 = new Team("team2", "팀2");
em.persist(team2);
//회원1에 새로운 팀2 설정
Member member1 = em.find(Member.class, "member1");
member1.setTeam(team2);
변경사항을 데이터베이스에 자동으로 반영한다. 이것은 연관관계를 수정할 때도 같은데, 참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리한다.
- 단순히 불러온 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동.
- 변경 사항을 DB에 자동으로 반영
5.2.4 연관관계 제거
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null); //연관관계 제거
5.2.5 연관된 엔티티 삭제
연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
member1.setTeam(null); //회원1 연관관계 제거
member2.setTeam(null); //회원2 연관관계 제거
em.remove(team); //팀 삭제
5.3 양방향 연관관계
- 양방향 매핑의 장점
- 단방향 매핑으로 이미 연관관계 매핑은 완료
- 양방향은 반대 방향으로 객체 그래프 탐색 기능이 추가된 것
- 단방향 매핑을 잘하고 양방향 매핑은 필요할 때 추가해도 됨
5.3.1 양방향 연관관계 매핑
이제 양방향 관계를 매핑하자.
@Entity
public class Member {
// 동일
}
@Entity
public class Team {
//추가되는 부분
@OneToMany(mappedBy = "team")
private List<Membmer> members = new ArrayList<>();
}
- mappedBy
- 양방향 매핑일 때 사용
- 반대쪽 매핑의 필드 이름을 값으로 사용
- 반대쪽 매핑이 Member.team 이므로 "team"
5.3.2 일대다 컬렉션 조회
팀에서 회원 컬렉션으로 객체 그래프 탐색을 사용해서 조회한 회원들을 출력한다.
Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers(); //팀 -> 회원, 객체 그래프 탐색
for (Member member : members) {
//...
}
5.4 연관관계의 주인
- 객체에는 양방향 연관관계는 없음.
- 서로 다른 단방향 연관관계 2개를 어플리케이션 로직으로 양방향인 것처럼 보이게 할 뿐
- 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 조인 가능
- 테이블은 외래 키 하나만으로 양방향 연관관계 가능
- 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는 하나
- 따라서 둘 사이에 차이가 발생
- 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관관계의 주인(Owner)이라 함
5.4.1 양방향 매핑의 규칙:연관관계의 주인
- 두 연관관계 중 하나를 연관관계의 주인으로 설정
- 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있음
- 반면에 주인이 아닌 쪽은 읽기만 할 수 있음
- 주인은 mappedBy 속성을 사용하지 않음
- 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것
다음 두 코드를 보자.
* 회원 -> 팀 (Member.team) 방향
class Member {
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
}
* 팀->회원(Team.members) 방향
class Team {
@OneToMany
private List<Member> members = new ArrayList<Member>();
}
연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.
5.4.2 연관관계의 주인은 외래 키가 있는 곳
- 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인
- 주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해서 주인이 아님을 설정
- 여기서 mappedBy 값으로 사용된 team은 연관관계의 주인인 Member 엔티티의 team 필드를 말함
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래키를 가짐.
다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다.
5.5 양방향 연관관계 저장
member1.setTeam(team); //연관관계 설정(연관관계의 주인)
5.6 양방향 연관관계의 주의점
양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.
//회원1 저장
Member member1 = new Member("member1", "회원1");
em.persist(member1);
Team team = new Team("team1", "팀1");
//주인이 아닌 곳만 연관관계 설정
team.getMembers().add(member1);
em.persist(team1);
5.6.1 순수한 객체까지 고려한 양방향 연관관계
- 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전
- 양쪽 방향에 모두 값을 입력하지 않으면 순수한 객체 상태에서 심각한 문제가 발생할 수 있음
//팀1
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
member1.setTeam(team); //연관관계 설정 member1 -> team1
List<Member> members = team1.getMembers();
//members.size()는?
- ORM은 객체와 데이터베이스 둘 다 고려
//팀1
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
//양방향 연관관계 설정
member1.setTeam(team); //연관관계 설정 member1 -> team1
team1.getMembers().add(member1); //연관관계 설정 team1 -> member1, 저장시 사용하지는 않음
em.persist(member1);
5.6.2 연관관계 편의 메소드
양방향 연관관계는 결국 양쪽 다 신경 써야 한다. 다음처럼 member.setTeam(team)과 team.getMembers().add(member)를 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다.
public class Member {
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
//연관관계 설정
member1.setTeam(team1);
5.6.3 연관관계 편의 메소드 작성 시 주의사항
member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMember(); //member1이 여전히 조회
- teamB로 변경할 때 teamA -> member1 관계를 제거하지 않음
public class Member {
public void setTeam(Team team) {
//기존 팀과 관계를 제거
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
}
5.7 정리
단방향 매핑과 비교해서 양방향 매핑은 복잡하다. 단방향과 비교해서 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.
내용을 정리하면 다음과 같다.
* 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
* 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
* 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
'개인 공부 > JPA' 카테고리의 다른 글
JPA N+1 문제 (0) | 2024.04.12 |
---|---|
05 연관관계 매핑 시작 실전예제 (0) | 2023.10.04 |
04 엔티티 매핑 (0) | 2023.09.20 |
03 영속성 관리 (0) | 2023.09.19 |
02 JPA 시작 (0) | 2023.09.19 |