목표
• 객체와 테이블 연관관계의 차이를 이해
• 객체의 참조와 테이블의 외래 키를 매핑
• 용어 이해
• 방향(Direction): 단방향, 양방향
• 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
• 연관관계의 주인(Owner): 객체 양방향 연관관계는 관리 주인이 필요
목차
• 연관관계가 필요한 이유
• 단방향 연관관계
• 양방향 연관관계와 연관관계의 주인
연관관계 매핑?
객체 연관관계와 테이블 연관관계를 매핑하는 것, 객체의 참조와 테이블의 외래 키를 매핑한다.
핵심 키워드 살펴보기
- 방향(Direction) : 회원과 팀이 관계가 있을 때
- 단방향 : 회원 → 팀, 팀 → 회원 둘 중 한 쪽만 참조하는 관계
- 양방향 : 회원 → 팀, 팀 → 회원 양쪽 모두 서로 참조하는 관계
- 다중성(Multiplicity) : 다대일(n:1), 일대다(1:n), 일대일(1:1), 다대다(n:m)
- 연관관계의 주인(Owner) : 객체를 양방향 연관관계로 만든다면, 연관관계의 주인을 정해야함.
연관관계를 알아보기 위한 예제 시나리오
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속될 수 있다.
- 회원과 팀은 다대일 관계다.
객체를 테이블에 맞추어 모델링 (연관관계가 없는 객체)
객체를 테이블에 맞추어 모델링 (참조 대신에 외래 키를 그대로 사용)
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;
@Column(name = "TEAM_ID")
private Long teamId;
…
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
…
}
|
cs |
객체를 테이블에 맞추어 모델링 (외래 키 식별자를 직접 다룸)
1
2
3
4
5
6
7
8
9
|
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);
|
cs |
→ 객체지향스럽지 못하다.
객체를 테이블에 맞추어 모델링 (식별자로 다시 조회, 객체 지향적인 방법은 아니다.)
1
2
3
4
|
//조회
Member findMember = em.find(Member.class, member.getId());
//연관관계가 없음
Team findTeam = em.find(Team.class, team.getId());
|
cs |
단방향 연관관계
객체 지향 모델링 (객체 연관관계 사용)
객체 연관관계
- 회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺는다.
- 회원 객체와 팀 객체는 단방향 관계다. 회원은 Member.team 필드를 통해서 팀을 참조할 수 있으나 팀은 회원을 참조할 필드가 없다. 예를 들면 member 객체는 member.getTeam()을 통해 참조하는 team을 가져올 수 있는 반면 team은 member를 참조할 수가 없다.
테이블 연관관계
-
회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
-
회원 테이블과 팀 테이블은 양방향 관계다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 또한 팀과 회원을 조인할 수도 있다.
-
객체 연관관계와 테이블 연관관계의 차이
참조를 통한 연관관계는 언제나 단방향이다. 객체간에 연관관계를 양방향으로 만들고자 한다면 반대쪽에도 참조하는 필드를 만들어 보관하게 해야한다. 결국 객체 연관관계를 양방향으로 하고자 한다면 연관관계를 두 개를 만들어줘야한다.(1 → 2, 2 →1) 이것은 양방향 관계이나 정확히는 서로 다른 방향 관계 2개가 있는 것이다. 반면에 테이블은 외래 키 하나로 양방향 조인이 가능하다.
객체 지향 모델링 (ORM 매핑)
객체의 참조와 테이블의 외래 키 매핑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
…
|
cs |
- 객체 연관관계: 회원 객체의 Member.team 필드 사용
- 테이블 연관관계: 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼 사용
위 예제를 살펴보면 연관관계 매핑 어노테이션을 통해 객체 연관관계와 테이블 연관관계를 매핑한 것을 알 수 있다.
- @ManyToOne: 다대일(n:1) 관계라는 매핑 정보이다.
- @JoinColumn: 조인 컬럼은 외래 키를 매핑할 때 사용한다.
- name: 매핑할 외래 키 이름을 지정하는 속성
연관관계 저장
1
2
3
4
5
6
7
8
9
|
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장
em.persist(member);
|
cs |
→ em.persist(team)을 통해 생성한 team을 저장하고, 영속된 team을 생성한 member의 참조에 담아 em.persist(member)를 수행한다. 이렇게 되면 member를 저장할 때, 참조하고 있는 영속된 team 엔티티의 id를 외래 키로 하여 데이터베이스에 저장해준다.
참조로 연관관계 조회 - 객체 그래프 탐색
1
2
3
4
|
//조회
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
|
cs |
→ 이 처럼 객체를 통해 연관된 엔티티를 조회하는 것을 객체 그래프 탐색이라 한다.
연관관계 수정
1
2
3
4
5
6
7
8
|
// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);
// 회원1에 새로운 팀B 설정
Member member = em.find(Member.class, "member1");
member.setTeam(teamB);
|
cs |
→ member1을 조회해 member에 담아 영속한 뒤, 새로운 팀B로 바꿔준 다음 commit하게 되면 플러시가 일어나면서 변경을 감지하고 UPDATE 쿼리를 날린다.
연관관계 제거
1
2
|
Member member = em.find(Member.class, "member1");
member.setTeam(null);
|
cs |
지금까지 member → team 으로의 접근만 가능한 다대일 단방향 매핑을 알아보았다. 이제는 반대 방향인 team → member의 단방향 매핑을 추가한 양방향 연관관계를 알아보자.
양방향 연관관계와 연관관계의 주인
양방향 매핑
객체의 연관관계
- 회원 → 팀: 회원의 member.team 을 통해 팀을 참조
- 팀 → 회원: 팀의 team.members를 통해 회원을 참조
양방향 매핑(Member 엔티티는 단방향과 동일)
1
2
3
4
5
6
7
8
9
|
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<Member>();
…
}
|
cs |
→ 팀과 회원은 일대다 관계다. 따라서 팀 엔티티에는 컬렉션인 List members를 추가했다.
연관관계 매핑 어노테이션
- @OneToMany : 일대다 관계를 매핑하기 위해 사용
- mappedBy : 양방향 매핑일 때 사용되는 속성, 반대쪽 매핑의 필드 이름을 값으로 준다
반대 방향으로 객체 그래프 탐색
1
2
3
|
//조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); // 팀 -> 회원, 역방향 조회
|
cs |
→ 팀에서 회원 컬렉션으로 객체 그래프 탐색을 사용해서 회원을 조회할 수 있다.
실행결과
Hibernate:
/* insert me.kyeongho.Team
*/ insert
into
Team
(team_id, name)
values
(null, ?)
Hibernate:
/* insert me.kyeongho.Member
*/ insert
into
Member
(member_id, createdDate, description, lastModifiedDate, roleType, team_id, name)
values
(null, ?, ?, ?, ?, ?, ?)
Hibernate:
select
member0_.member_id as member_i1_0_0_,
member0_.createdDate as createdd2_0_0_,
member0_.description as descript3_0_0_,
member0_.lastModifiedDate as lastmodi4_0_0_,
member0_.roleType as roletype5_0_0_,
member0_.team_id as team_id7_0_0_,
member0_.name as name6_0_0_,
team1_.team_id as team_id1_1_1_,
team1_.name as name2_1_1_
from
Member member0_
left outer join
Team team1_
on member0_.team_id=team1_.team_id
where
member0_.member_id=?
Hibernate:
select
members0_.team_id as team_id7_0_0_,
members0_.member_id as member_i1_0_0_,
members0_.member_id as member_i1_0_1_,
members0_.createdDate as createdd2_0_1_,
members0_.description as descript3_0_1_,
members0_.lastModifiedDate as lastmodi4_0_1_,
members0_.roleType as roletype5_0_1_,
members0_.team_id as team_id7_0_1_,
members0_.name as name6_0_1_
from
Member members0_
where
members0_.team_id=?
m = Kyeongho
연관관계의 주인과 mappedBy
연관관계의 주인
정확하게는 객체에는 양방향 연관관계가 없다. 서로 다른 단방향 연관관계 2개를 어플리케이션 로직으로 양방향인 것처럼 보이게 할 뿐이다. 반면에 데이터베이스는 외래 키 하나로 양쪽이 서로 조인이 가능하다.
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘이지만 외래 키는 하나이다. 여기서 둘 사이에 차이가 발생한다. 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인(Owner)이라 한다.
mappedBy
어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하여 설정한다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
아래의 그림처럼 둘 중 하나로 외래 키를 관리해야 한다.
누구를 주인으로?
- 외래 키가 있는 있는 곳을 주인으로 정하는 걸 권장
→ 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 갖는다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. - 여기서는 Member.team이 연관관계의 주인
Member 엔티티 (연관관계의 주인)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Entity
@Table(name = "Member")
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
...
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
...
}
|
cs |
Team 엔티티
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Entity
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team") // mappedBy에는 반대편 엔티티의 필드명을 적어준다.
private List<Member> members = new ArrayList<>();
...
}
|
cs |
양방향 매핑시 가장 많이 하는 실수 (연관관계의 주인에 값을 입력하지 않음)
1
2
3
4
5
6
7
8
|
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);
|
cs |
-> member -> team의 연관관계 참조가 설정되지 않아, 외래 키가 저장되지 않는다.
양방향 매핑시 연관관계의 주인에 값을 입력해야 한다. (순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.)
1
2
3
4
5
6
7
8
9
|
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);
|
cs |
양방향 연관관계 주의
- 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
- 연관관계 편의 메소드를 생성하자
- 양방향 매핑시에 무한 루프를 조심하자
예: toString(), lombok, JSON 생성 라이브러리
연관관계 편의 메소드
1
2
3
4
5
6
7
|
public void setTeam(Team team) {
if(this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
|
cs |
→ 연관관계의 주인 필드의 setter 메소드를 위와 같이 양방향 관계를 한꺼번에 설정할 수 있게끔 하는것이 연관관계 편의 메소드이다. 기존에 있던 연관관계 참조가 있다면 그 참조가 나를 참조하는 연관관계를 지워준다. 하지만 이 예시도 하나의 예시일 뿐 양방향 연관관계를 설정해 주는 로직은 반대편 엔티티에 있어도 된다.
'Study > spring' 카테고리의 다른 글
자바 ORM 표준 JPA 프로그래밍(9) - 고급 매핑 (0) | 2021.01.04 |
---|---|
자바 ORM 표준 JPA 프로그래밍(8) - 다양한 연관관계 매핑 (0) | 2021.01.03 |
자바 ORM 표준 JPA 프로그래밍(6) - 엔티티 매핑 실습 (0) | 2020.12.27 |
자바 ORM 표준 JPA 프로그래밍(5) - 엔티티 매핑 (0) | 2020.12.25 |
자바 ORM 표준 JPA 프로그래밍(4) - 플러시와 준영속 (0) | 2020.12.23 |