자바 ORM 표준 JPA1
자바 ORM 표준 JPA 프로그래밍1
김영한 지음
5장 연관관계 매핑 기초
JoinColumn
JoinColumn : 조인 컬럼은 왜래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정합니다.
회원과 팀을 생각하면, 팀에는 여러 회원이 존재합니다. 회원은 자기가 속한 팀의 FK 를 가지고 있습니다. 따라서, 어떤 FK 를 가지고 team 과 연관 관계를 맺을 것인지를 정할 수 있습니다.
이 어노테이션은 생략이 가능합니다. 생략한다면 기본 전략을 사용합니다.
- 기본 전략 : ‘필드명’ + ‘_’ + ‘참조하는 테이블의 컬럼명’
- team_TEAM_ID 가 위와 같은 경우에는 외래키의 컬럼명이 될 것입니다.
ManyToOne
다대일 관계에서 사용합니다. 기본값이 true 인 Optional 이라는 속성이 존재합니다. 이 속성을 false 로 설정하면 연관된 엔티티가 반드시 존재해야만 합니다.
ManyToOne 으로 연관관계를 맺는 예시를 살펴보겠습니다.
1
2
3
4
5
6
7
8
Team team1=new Team("team1");
em.persist(team);
Member member1=new Member("member1");
member.setTeam(team1);
em.persist(member);
만약 team1 을 영속 상태로 만들지 않고 member 를 저장하면 어떻게 될까요? 오류가 발생합니다. JPA 에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 합니다.
연관된 엔티티 삭제
연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 제거하고 삭제해야 합니다. 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생합니다.
양방향 연관관계
양방향 연관관계를 위해서는 일대다의 일쪽에 컬렉션이 필요합니다.
1
2
3
4
@OneToMany(mappedBy = "team")
private List<Member> members=new ArrayList<Member>();
mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 주면 됩니다. 사실 객체는 양방향 연관관계가 없습니다.
db 를 생각해보면, team 테이블의 어떤 칼럼을 만들어야 팀에 속한 여러 member id 들을 가질 수 있을까요? 아마 불가능할 것입니다. db 상에서는 결국 외래 키 하나로 양쪽이 서로 조인하는 구조입니다.
이런 차이로 JPA 에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하고, 이를 연관관계의 주인이라고 부릅니다.
연관관계의 주인
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래키를 관리할 수 있습니다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
연관관계의 주인 = 외래 키의 관리자입니다. 외래 키가 있는 곳에 연관관계의 주인을 지정해야 하기에, Member.team 이 주인이 됩니다.
주인이 아닌 Team.members 에는 mappedBy=”team” 속성을 사용해서 주인이 아님을 설정할 수 있을 것입니다.
1
2
3
4
5
6
7
8
Team team1=new Team("team1");
em.persist(team);
Member member1=new Member("member1");
member.setTeam(team1);
em.persist(member);
양방향 연관관계인 경우에도, 위와 같이 서로 간의 관계를 저장할 수 있습니다. Team 이 가지고 있는 컬렉션에 Member 객체를 등록해줄 필요는 없습니다.
주인이 아닌 쪽에 값을 등록하고, 주인에는 값을 등록해주지 않으면 서로 간의 연관관계가 정상적으로 맺어지지 못합니다.
순수한 객체를 고려한다면
객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것을 추천합니다. JPA 를 사용하지 않는 순수한 객체 상태에서 문제가 발생할 수 있기 때문입니다.
JPA 를 사용하지 않고 엔티티에 대한 테스트 코드를 작성한다고 가정해보겠습니다. ORM 은 객체와 RDB 모두 중요하게 고려해야 합니다.
1
2
3
4
5
6
7
8
9
10
Team team1=new Team("team1");
em.persist(team);
Member member1=new Member("member1");
member.setTeam(team1);
em.persist(member);
System.out.println(team1.getMembers().size()); // 0 이 출력됨
주인에만 값을 설정해주는 경우엔, 결과가 0 이 나옵니다. 반면에 양쪽에 모두 설정을 해줬다면 값이 정상적으로 나왔겠죠.
연관관계 편의 메서드
둘 모두에게 값을 할당해주는 경우, 어느 한쪽에만 할당해주는 실수를 방지하기 위해 두 코드를 하나처럼 사용하는 것이 좋습니다.
1
2
3
4
5
6
7
8
9
10
11
12
public class Member {
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
이제 member.setTeam(team1) 과 같은 한 줄의 코드로 양방향 연관관계를 안전하게 맺을 수 있습니다.
편의 메서드 사용 시 주의 사항
1
2
3
4
5
6
member1.setTeam(teamA);
member1.setTeam(teamB);
teamA.getMembers(); // member1 이 여전히 조회됨
이를 방지하기 위해서는 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 합니다.
1
2
3
4
5
6
7
8
9
public void setTeam(Team team){
if(this.team!=null){
this.team.getMembers().remove(this);
}
this.team=team;
team.getMembers().add(this);
}
이처럼 양방향 연관관계를 사용하기 위해서는 많은 고민이 필요합니다. 양방향 연관관계를 통해서 양방향 객체 그래프 탐색 기능을 얻는 대신, 로직의 번거로움을 더하는 것이 좋을지는 비즈니스적 관점으로 고민해봅시다.
양방향 매핑 시에는 toString 을 통한 무한 루프도 조심해야 합니다.
6장 다양한 연관관계 매핑
일대일
일대일 관계에서는 어느 곳이나 외래 키를 가질 수 있다. 프록시를 사용할 때, 외래 키를 직접 관리하지 않는 일대일 관계를 지연 로딩으로 설정해도 즉시 로딩된다.
예를 들어, Locker 와 Member 가 일대일 관계이고 Locker 에 외래 키가 잇다고 가정하자. Locker.member 는 지연로딩할 수 있지만, Member.locker 는 즉시 로딩된다.
이것은 프록시의 한계 때문이다. 프록시 대신에 bytecode instrumentation 을 사용하면 해결할 수 있다.
다대다
다대다 관계를 위해서는 중간에 연결 테이블을 추가해야 한다. 연결 테이블을 사용하면 다대다 관계를 일대다, 다대일 관계로 풀어낼 수 있다.
@ManyToMany 를 사용하는 것만으로도 연결 테이블을 자동으로 처리하므로 편리해지지만, 실무에서는 어려운 문제가 있다.
예를 들어, 회원과 상품을 다대다로 매핑하기 위한 MemberProduct 테이블이 있다고 생각해보자. MemberProduct 테이블은 회원과 상품의 기본 키를 받아서 자신의 기본 키로 사용한다.
두 기본 키를 묶어서 복합 키로 사용하는데, 이 복합키에 대응하기 위한 식별자 클래스를 만들어야 한다. 물론, 대리 키를 사용하는 방법도 있다. 이렇게 하는 경우에는 아에 새로운 엔티티를 만드는 것과 다름 없다.
이를 테면, Order 이라는 엔티티를 만들고 해당 엔티티가 멤버와는 다대일, 상품과도 다대일 관계를 맺도록 구성하면 될 것이다.
멤버와 상품의 FK 를 모두 갖고 있으므로, 매핑이 단순해진다. 새로운 엔티티를 사용해 다대다 관계를 풀어내는 것도 좋은 방법이다.