https://github.com/MoochiPark/jpa/tree/master/chapter05/README.md
[ ▶ 연관관계 매핑 기초 ]
대부분의 엔티티는 다른 엔티티와 연관관계가 있다. 그런데 객체는 참조 (주소)를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다. 이 둘을 완전히 다르므로 객체 관계 매핑 (ORM)에서 가장 어려운 부분이 객체 연관관계와 테이블 연관관계를 매핑하는 것이다.
- 방향 (Direction): 단방향, 양방향이 있다. 회원과 팀 관계가 있을 때, 회원 → 팀 또는 팀 → 회원 둘 중 한 쪽만 참고하는 것은 단방향. 양쪽 모두 서로 참조하는 것을 양방향 관계라 한다. 방향은 객체관계에만 존재하고 테이블 관계는 항상 양방향이다.
- 다중정 (Multiplicity): 다대일 (N:1), 일대다 (1:N), 일대일 (1:1:), 다대다 (N:M) 다중성이 있다. 회원과 팀 관계에서 여러 회원은 한 팀에 속하므로 회원과 팀은 다대일 관계이고, 반대로 한 팀에 여러 회원이 소속될 수 있으므로 팀과 회원을 일대다 관계다.
- 연관관계의 주인 (Owner): 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.
[ ▷ 단반향 연관관계 ]
연관관계 중에선 다대일 (N:1) 단방향 관계를 가장 먼저 이해해야 한다. 아래의 예제는 회원과 팀의 관계를 통해 다대일 단방향 연관관계를 보여준다.
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속될 수 있다.
- 회원과 팀은 다대일 관계다.
▼ 객체 연관관계
▼ 테이블 연관관계
▲ 객체 연관 관계
- 회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺는다.
- 회원 객체와 팀 객체는 단방향 관계다. 회원은 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없다. member → team 조회는 member.getTeam()으로 가능하지만 반대 방향으로 접근하는 필드는 없다.
▲ 테이블 연관관계
- 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
- 회원 테이블과 팀 테이블은 양방향 관계다. 회원 테이블의 TEAM_ID 외 키를 통해서 회원과 팀을 조인할 수 있고, 반대로 팀과 회원도 조인할 수 있다.
▼ 회원 테이블과 팀 테이블을 SELECT하는 SQL
SELECT *
FROM member, team
WHERE member.team_id = team.team_id;
▲ 객체 연관관계와 테이블 연관관계의 가장 큰 차이
참조를 통한 연관관계는 단방향일 수 있다. 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가하여 참조를 보관해야 한다. 결국 연관관계를 하나 더 만들어야 하는 것이다. 이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라고 한다.
하지만 정확히 말하면 이것은 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다. 반면에 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.
▲ 객체 연관관계 vs 테이블 연관관계 정리
- 객체는 참조로 연관관계를 맺는다.
- 테이블은 외래 키로 연관관계를 맺는다.
이 둘은 비슷해 보이지만 매우 다른 특성을 가진다. 연관 데이터를 조회할 때 객체는 참조, 테이블은 조인을 사용한다.
- 참조를 사용하는 객체의 연관관계는 단방향이다. A → B (a,b)
- 외래 키를사용하는 테이블의 연관관계는 양방향이다. A JOIN B가 가능하면 B JOIN A도 가능하다.
- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다. A → B (a,b) / B → A (b,a)
[ ▷ 순수한 객체 연관관계 ]
아래의 코드는 JPA를 사용하지 않은 순수하게 객체만 사용한 회원과 팀 클래스의 코드이다.
public class Member {
private String id;
private String username;
private Team team; // 팀의 참조를 보관
public void setTeam(final Team team) {
this.team = team;
}
// Getter, Setter ...
}
public class Team {
private String id;
private String name;
// Getter, Setter ...
}
▼ 동작 코드
public static void main(String... args) {
// 생성자(id, 이름)
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
Team team1 = new Team("team1", "팀1");
member1.setTeam(team1);
member2.setTeam(team1);
Team findTeam = member1.getTeam();
}
Team findTeam = member1.getTeam(); 처럼 회원 1이 속한 팀 1을 조회할 수 있다. 이처럼 객체는 참조를 사용해 연관관계를 탐색할 수 있는 이것을 객체 그래프 탐색이라고 한다.
[ ▷ 테이블 연관관계 ]
▼ 테이블 DDL
CREATE TABLE member
(
member_id varchar(255) primary key,
team_id varchar(255),
username varchar(255),
CONSTRAINT fk_member_team FOREIGN KEY (team_id)
REFERENCES team (team_id)
);
CREATE TABLE team
(
team_id varchar(255) primary key,
name varchar(255)
);
▼ INSET SQL
INSERT INTO team VALUES ('team1', '팀1');
INSERT INTO member VALUES ('member1', 'team1', '회원1'),
('member2', 'team1', '회원2');
▼ 회원1이 소속된 팀을 조회하는 SQL
select t.*
FROM member m, team t
WHERE m.team_id = t.team_id and m.member_id = 'member1';
이처럼 데이터베이스는 외래 키를 사용해서 연관관계를 탐색할 수 있다.
[ ▷ 객체 관계 매핑 ]
▼ 매핑한 회원 엔티티
import javax.persistence.*;
@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(final Team team) {
this.team = team;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Team getTeam() {
return team;
}
}
Comment
▼ 매핑한 팀 엔티티
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Team {
@Id
@Column(name = "team_id")
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Comment
- 객체 연관관계: 회원 객체의 Member.team 필드 사용
- 테이블 연관관계: 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼을 사
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
- @ManyToOne: 이름 그래도 다애일 관계라는 매핑 정보다. 회원과 팀은 다대일 관계다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
- @JoinColumn(name="team_id"): 조인 컬럼은 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. 이 어노테이션은 생략이 가능하다.
[ ▷ @JoinColumn ]
@JoinColumn 외래 키를 매핑할 때 사용한다.
속성 | 기능 | 기본값 |
name | 매핑할 외래 키 이름 | 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명 |
referencedColumnName | 외래 키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본 키 컬럼명 |
foreignKey(DDL) | 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다. | |
unique nullable insertable updatable columnDefinition table |
@Column의 속성과 같다. |
[ ▷ @ManyToOne ]
속성 | 기능 | 기본값 |
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | ture |
fetch | 글로벌 페치 전략을 설정한다. | @ManyToOne=FetchType.EAGER @OneToMany=FetchType.LAZY |
cascade | 영속성 전이 기능을사용한다. | |
targetEntity | 연관된 엔티티의 타입정보를 설정한다. 이 기능은 거의 사용되지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다. |
@OneToMany
private List<Member> members; // 제네릭으로 타입 정보를 알 수 있다.
@OneToMany(targetEntity=Member.class)
private List members; // 제네릭이 없으면 타입 정보를 알 수 없다.
[ ▷ 연관관계 사용 ]
[ ▷ 저장 ]
▼ 회원과 팀을 저장하는 코드
public static void testSave() {
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
// 팀1 저장
Team team1 = new Team("team1", "팀1");
// 회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);
// 회원1 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member2);
tx.commit();
em.close();
}
member1.setTeam(team1); // 회원 -> 팀 참조
em.persist(member1); // 저장
회원 엔티티는 팀 엔티티를 참조하고 저장했다. 이때 실행된 SQL을 보면 회원 테이블의 외래 키 값으로 참조한 팀의 식별자 값이 입력된 것을 알 수 있다.
Hibernate:
/* insert io.wisoft.daewon.entity.Team
*/ insert
into
Team
(name, team_id)
values
(?, ?)
Hibernate:
/* insert io.wisoft.daewon.entity.Member
*/ insert
into
Member
(team_id, username, member_id)
values
(?, ?, ?)
Hibernate:
/* insert io.wisoft.daewon.entity.Member
*/ insert
into
Member
(team_id, username, member_id)
values
(?, ?, ?)
member_id | username | team_id | team_name |
member1 | 회원1 | team1 | 팀1 |
member2 | 회원2 | team1 | 팀1 |
[ ▷ 조회 ]
연관관계가 있는 엔티티를조회하는 방법은 크게 2가지이다.
△ 객체 그래프 탐색 (객체 연관관계를 사용한 조회)
member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있다.
Member member = em.find(Member.class, "member");
Team teeam = member.getTeam(); // 객체 그래프 탐색
System.out.println("팀 이름 = " + team.getName()); // 출력 결과 : 팀 이름 = 팀1
객체를 통해 연과뇐 엔티티를 조회하는 것을 객체 그래프 탐색이라고 한다.
△ 객체지향 쿼리 사용 (JPQL)
JPQL도 조인을 지원하므로 연관된 테이블을 조인해서 검색조건을 사용하면 된다.
private static void queryLogicJoin(final EntityManager em) {
String jpql = "select m from Member m join m.team t where t.name=:teamName";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setParameter("teamName", "팀1")
.getResultList();
resultList.forEach(m -> System.out.println("[query] member.username=" + m.getUsername()));
}
JPQL의 from Member m from m.team t 부분을 보면 회원이 팀과 관계를 가지고 있는 필드 (m.team)를 통해서 Member와 Team을 조인햇다. 그리고 where 절에서 t.name을 검색 조건으로 사용해서 팀1에 속한 회원만 검색했다.
참고로 teamName과 같이 :로 시작하는 것은 파라미터 바인딩받는 문법이다.
SELECT m.*
FROM member m
inner join team t on m.team_id = t.team_id
where t.name='팀1';
SQL과 JPQL을 비교하면 JPQL은 객체 (엔티티)를 대상으로 하고 SQL보다 간결하다.
[ ▷ 수정 ]
팀1 소속이던 회원을 새로운 팀 2에 속하도록 수정한다.
private static void updateRelation(final EntityManager em) {
// 새로운 팀2
Team team2 = new Team("team2", "팀2");
em.persist(team2);
// 회원에 새로운 팀2 설정
em.find(Member.class, "member1").setTeam(team2);
}
Hibernate:
/* update
io.wisoft.daewon.entity.Member */ update
Member
set
team_id=?,
username=?
where
member_id=?
실행되는 수정 SQL은 위와 같다. 단순히 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다. 그리고 변경사항을 데이터베이스에 자동으로 반영한다. 이것은 연관관계를 수정할 때도 같으므로 참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리한다.
[ ▷ 연관관계 제거 ]
회원1을 팀에 소속하지 않도록 변경한다.
private static void deleteRelation(final EntityManager em) {
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null);
}
update
Member
set
team_id=null,
...
where
member_id='member1'
[ ▷ 연관된 엔티티 제거 ]
연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생한다. 이때 팀1을 삭제해려면 연관관계를 먼저 끊어야 한다.
member1.setTeam(null);
member2.setTeam(null);
em.remove(team);
[ ▷ 양방향 연관관계 ]
팀에서 회원으로 접근하는 관계를 추가한다.
▼ 양방향 객체 연관관계
먼제 객체 연관관계를 보면 회원과 팀은 다대일 관계다. 반대로 팀에서 회원은 일대다 관계다. 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다. Team.members를 List 컬렉션으로 추가했다.
- 회원 → 팀: Member.team
- 팀 → 회원: Team.members
JPA는 List를 포함한 Collection, Set, Map 같은 다양한 컬렉션을지원한다.
테이블의 관계는 미리 말한 것처럼 데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다. 따라서 데이터베이스에 추가할 내용은 없다.
[ ▷ 양방향 연관관계 매핑 ]
회원 엔티티에는 변경할 부분이 없다. 팀의 엔티티를 본다.
▼ 매핑한 팀 엔티티
@Entity
public class Team {
@Id
@Column(name = "team_id")
private String id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
...
팀과 회원은 일대다 관계다. 따라서 members를 추가한다. 그리고 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용한다. mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 준다.
[ ▷ 일대다 컬렉션 조회 ]
▼ 일대다 방향으로 객체 그래프 검색
private static void biDirection(final EntityManager em) {
Team team = em.find(Team.class, "team1");
team.getMembers().forEach(t -> System.out.println("member.username=" + t.getUsername()));
}
▼ 실행 결과
member.username=회원1
member.username=회원2
[ ▷ 연관관계의 주인 ]
객체에서의 양방향 연관관계는 사실 없다. 서른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다. 반면에 데이터베이스 테이블은 앞서 설명했듯이 외래 키 하나로 양쪽이 서로 조인할 수 있다.
- 회원 → 팀 연관관계 1개 (단방향)
- 팀 → 회원 연관관계 1개 (단방향)
테이블은
- 회원 → 팀 연관관계 1개 (양방향)
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 된다. 그런데 엔티티를 양방향으로 매핑하면 두 곳에서 서로 참조하므로 객체의 연관관계를 관리하는 포인트는 2개로 늘어난다.
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다.
이러한 차이로 인해 JPA는 두 객체 연관관계 중 하나를 정해 테이블의 외래 키를 관리하는데 이걸 연관관계의 주인 (owner)라고 한다.
[ ▷ 양방향 매핑의 규칙: 연관관계의 주인 ]
양방향 연관관계 매핑 시 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다. 연관관계의 주인만이 데이터베이스 연관관계와 매피외고 외래키를 관리 (등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 가능하다.
어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 된다.
- 주인은 mappedBy 속성을사용하지 않는다.
- 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
연관관계의 주인을 정한다는 것은 외래 키 관리자는 선택하는 것이다.
만약 회원 엔티티에 있는 Member.team을 주인으로 선택하면 자기 테이블에 있는 외래 키를 관리하면 된다. 하지만 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다.
[ ▷ 연관관계의 주인은 외래 키가 있는 곳 ]
따라서 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다. 여기서는 회원 테이블에 외래 키가 있으므로 Member.team이 주인이 된다. 주인이 아닌 Team.members에는 mappedBy="team" 속성으로 주인이 아님을 설정한다.
mappedBy의 값은 연관관계의 주인을 주면된다. 여기서 team은 Member 엔티티의 team 필드를 말한다.
정리하면 연관관계의 주인만 데이터베이스 연관관계에 매핑되고 외래 키를 관리할 수 있다, 주인이 아닌 반대편 (inverse, non-owning side)는 읽기만 가능하고 외래 키를 변경하지는 못한다.
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 ManyToOne에는 mappedBy 속성이 없다.
[ ▷ 양방향 연관관계 저장 ]
양방향 연관관계를 사용해서 팀1, 회원1, 회원2를 저장한다.
private static void testSave(final EntityManager em) {
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);
// 회원1 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member2);
}
참고로 이 코드는 단방향 연관관계에서 살펴본 회원과팀을저장하는 코드와 완전히 같다. 데이터베이스에서 회원 테이블을 조회한다.
member_id | team_id | username |
member1 | team1 | 회원1 |
member2 | team1 | 회원2 |
양방향 연관관계에서는 연관관계의 주인이 외래 키를 관리한다. 따라서 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력된다.
// 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.
team1.getMembers().add(member1); // 무시(연관관계의 주인이 아님)
team1.getMembers().add(member2); // 무시(연관관계의 주인이 아님)
// Member.team은 연관관계의 주인이므로 엔티티 매니저는 이곳에 입력된 값을 사용해서 외래 키를 관리한다.
member1.setTeam(team1); // 연관관계 설정(연관관계의 주인)
member2.setTeam(team1); // 연관관계 설정(연관관계의 주인)
[ ▷ 양방향 연관관계의 주의점 ]
데이터베이스에 외래 키 값이 정상적으로 저장되지 않으면 주인이 아닌 곳에만 값을 입력하지 않는지 의심해야 한다.
주인이 아닌 곳에만 값을 설정한다고 가정한다.
private static void testSaveNonOwner(final EntityManager em) {
// 회원1 저장
Member member1 = new Member("member1", "회원1");
em.persist(member1);
// 회원1 저장
Member member2 = new Member("member2", "회원2");
em.persist(member2);
Team team1 = new Team("team1", "팀1");
// 주인이 아닌 곳에만 연관관계 설정
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(team1);
}
회원 1, 회원 2를 저장하고 팀의 컬렉션에 담은 후에 팀을 저장한다. 데이터베이스에서 회원 테이블 조회한다.
member_id | team_id | username |
member1 | team1 | 회원1 |
member2 | team1 | 회원2 |
연관관계의 주인이 아닌 Team.members에만 값을 저장했으므로 team_id에 null 값이 입력되어 있는 것을 볼 수 있다.
[ ▷ 순수한 객체까지 고려한 양방향 연관관계 ]
정말 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에 값은 저장하지 않는 것은 안전하지 않은 방법이다. 객체 관점에서 양쪽방향에 모두 값을 입력해주는 것이 가장 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.
예를 들어 JPA를 사용하지 않고 엔티티에 대한 테스트코드를 작성한다고 해보자. ORM은 객체와 관계형 데이테베이스 둘다 중요한다, 데이터베이스와 객체 모두 고려해야 한다.
▼ 순수한 객체 연관관계
private static void testPlainObjectBiDirection() {
// 팀1
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
member2.setTeam(team1); // 연관관계 설정 member2 -> team1
List<Member> members = team1.getMembers();
System.out.println("members.size = " + members.size());
}
▼ 실행 결과
members.size = 0
Member.team에만 연관관계를 설정하고 반대 방향은 연관관계를 설정하지 않았다. 실행 결과를 보면 기대하던 양방향 연관관계가 아닌 것을 볼 수 있다.
양방향은 양쪽다 관계를 설정해야 한다. 위처럼 회원 → 팀을 설정하면 반대방향인 팀 → 회원도 설정해야 한다.
team1.getMembers().add(member1);
▼ 양방향 모두 관계를 설정
private static void testPlainObjectBiDirection() {
// 팀1
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
team1.getMembers().add(member1); // 연관관계 설정 team1 -> member1
member2.setTeam(team1); // 연관관계 설정 member2 -> team1
team1.getMembers().add(member2); // 연관관계 설정 team1 -> member2
List<Member> members = team1.getMembers();
System.out.println("members.size = " + members.size());
}
}
▼ 실행 결과
members.size = 2
양쪽 모두 관계를 설정하니 기대했던 결과 2가 나온다. 객체까지 고려하면 이렇게 양쪽 다 관계를 맺어야한다.
▼ JPA로 코드 완성
private static void testPlainObjectBiDirection(final EntityManager em) {
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
team1.getMembers().add(member1); // 연관관계 설정 team1 -> member1
em.persist(member1);
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team1
team1.getMembers().add(member2); // 연관관계 설정 team1 -> member2
em.persist(member2);
List<Member> members = team1.getMembers();
System.out.println("members.size = " + members.size());
}
양쪽에 연관관계를 설정한다. 따라서 순수한 객체 상태에서도 동작하고, 테이블의 외래 키도 정상 입력된다. 물론 외래 키의 값은 연관관계의 주인인 Member.team 값을 사용한다.
- Member.team: 연관관계의 주인, 이 값으로 외래 키를 관리한다.
- Team.members: 연관관계의 주인이 아니다. 따라서 저장 시에 사용되지 않는다.
따라서 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어야 한다.
[ ▷ 연관관계 편의 메서드 ]
양방향 연관관계는 결국 양쪽 다 신경 써야 한다. 위처럼 양방향 연관관계 코드를 작성하다보면 둘 중 하나만 실수로 놓치게되면 양방향이 깨질 수 있다.
따라서 양방향 관계에선 두 코드를 하나인 것처럼 사용하는 것이 안전하다.
@Entity
public class Member {
private Team team;
public void setTeam(final Team team) {
this.team = team;
team.getMembers().add(this);
}
...
이렇게 리팩토링하면 실수도 줄어들고 좀 더 그럴듯하게 양방향 연관관계를 설정할 수 있다.
이렇게 한 번에 양방향 관계를 설정하는 메서드를 연관관계 편의 메서드라 한다.
[ ▷ 연관관계 편의 메서드 작성 시 주의사항
이전의 setTeam() 메서드에는 버그가 있다.
member.setTeam(teamA); // 1
member.setTeam(teamB); // 2
Member findMember = teamA.getMember(); // member1이 여전히 조회된다.
▼ 1번
▼ 2번 (삭제되지 않은 관계)
teamB로 변경할 때 teamA → member1 관계를 제거하지 않았다. 따라서 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.
public void setTeam(final Team team) {
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
이 코드는 서로 다른 단방향 연관관계 2개를 양방향인 것처럼 보이게 하기위해 얼마나 많은 고민과 수고가 필요한지 보여준다.
반면에 관계형 데이터베이스는 외래 키 하나로 문제를 단순하게 해결한다. 객체에서 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 한다.
'JPA (Java Persistence API)' 카테고리의 다른 글
7장 고급 매핑 (1) | 2024.09.23 |
---|---|
6장 다양한 연관관계 매핑 (0) | 2024.09.13 |
4장 엔티티 매핑 (0) | 2024.09.11 |
3장 영속성 관리 (0) | 2024.09.11 |
2장 JPA 시작 (0) | 2024.09.11 |