다중성과 단방향, 양방향을 고려한 가능한 모든 연관관계를 하나씩 알아보자.
다대일 관계의 반대 방향은 항상 일대다 관계이고 일대다 관계의 반대 방향은 항상 다대일 관계이다.
DB 테이블의 일대다 관계에서 외래 키는 항상 다쪽에 있다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.
회원은 Member.team으로 팀 엔티티를 참조할 수 있지만 팀에는 회원을 참조할 수 있는 필드가 없다. 따라서 회원과 팀은 다대일 단방향 연관관계이다.
Member.team 필드로 회원 테이블의 team_id 외래 키를 관리한다.
그림에서 실선인 Team.members는 연관관계 주인이 아니다.
@Entity
public class Member {
id; username; ...
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public void setTeam(Team team) {
this.team = team;
// 무한 루프에 빠지지 않도록 체크
if(!team.getMembers().contains(this)) {
team.getMembers().add(this);
}
}
}
@Entity
public class Team {
id; name;
...
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
public void addMember(Member member) {
this.members.add(member);
// 무한 루프에 빠지지 않도록 체크
if(member.getTeam() != this) {
member.setTeam(this);
}
}
}
일대다와 다대일 연관관계는 항상 다에 외래 키가 있다. 여기서 다쪽인 Member 테이블이 외래 키를 갖고 있으므로 Member.team이 주인이 되고, 주인이 아닌 Team.members는 조회를 위한 JPQL이나 객체 그래프를 탐색할 때 사용된다.
그리고, 양방향 연관관계는 항상 서로를 참조해야 한다. 항상 서로를 참조하게 하려면 setTeam(), addMember()
같은 연관관계 편의 메소드를 사용하는 게 좋다. 대신 편의 메소드를 양쪽 다 작성하면 무한루프에 빠지므로 주의해야 한다.
일대다 관계는 엔티티를 하나 이상 참조하므로 List, Collection, Set, Map
등의 컬렉션을 사용해야 한다.
하나의 팀은 여러 회원을 참조하는 관계를 일대다 관계라 한다. 그리고 팀은 회원을 참조하지만 반대는 아니라면 단방향 관계이다. JPA 2.0부터 지원된다.
그림 상에서 Team 엔티티의 members로 회원 테이블의 TEAM_ID 외래 키를 관리한다. 특이한 점은 자신이 매핑한 테이블의 외래키가 아닌, 반대 테이블에 있는 외래키를 관리한다는 점이다.
일대다에서 외래키는 다쪽 테이블에 있어야 하지만, 다쪽 엔티티에 외래키를 매핑할 수 있는 참조 필드가 없기 때문에 이런 현상이 나타난다.
@Entity
public class Team {
...id; name;
@OneToMany
@JoinColumn(name = "TEAM_ID") // Member 테이블의 TEAM_ID (FK)
private List<Member> members = new ArrayList<Member>();
}
@Entity
public class Member {
...id; username;
}
일대다 단방향에선 @JoinColumn
을 명시해야 한다. 그렇지 않으면 JPA는 JoinTable
전략을 취하기 때문.
@ManyToOne
는 mappedBy 속성이 없는, 연관관계 주인이다.
일대일 관계는 양쪽이 서로 하나의 관계만 갖는, 서로 일대일 관계이다.
또한 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래 키를 가질 수 있다. 주든 대상이든 외래키 하나로 양쪽 조회가 가능한데, 그래서 누가 외래키를 가질지 잘 선택해야 한다.
객체지향은 주테이블에, DB 지향은 대상 테이블에 외래키가 있는 것을 선호한다. 대상 테이블에 외래키가 있는 단방향 관계는 JPA에서 지원하지 않는다.
OOP는 RDBMS와 다르게 중간 테이블 없이 엔티티 두 개로 다대다를 편리하게 매핑할 수 있다.
@Entity
public class Member {
...id; username;
@ManyToMany
@JoinTable(
name = "MEMBER_PRODUCT",
joinColumns = @JoinColumn(name = "MEMBER_ID"),
inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID")
)
private List<Product> products = new ArrayList<Product>();
}
@Entity
public class Product {
...id;name
}
@JoinTable
의
양방향 연관관계는 연관관계 편의 메소드를 추가해 관리하는 게 편하다.
member.getProducts().add(product); 대신,
member.addProduct(product);처럼 말이다.
이를 위해선,
public void addProduct (Product product) {
products.add(product);
product.getMembers().add(this);
}
와 같은 메소드 정의가 필요하겠다.
@ManyToMany를 사용하면 연결 테이블을 자동으로 처리해 도메인 모델이 단순해지는 장점이 있음에도 실무에 도입하기엔 한계가 있다.
RDBMS 상 연결 테이블에 부가적인 컬럼이 더 필요할 때가 많기 때문이다.
그래서 연결해주는 엔티티를 만들어야 한다.
@Entity
public class Member {
// id 있다 쳐
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts;
}
@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
@Id @ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member; // MemberProductId.member와 연결
@Id @ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product; // MemberProductId.product와 연결
}
public class MemberProductId implements Serializable {
// member, product 있다 쳐
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
중간 테이블인 MemberProduct에서 @Id, @JoinColumn 그리고 @IdClass를 사용해 복합 기본 키를 매핑했다. (혹은 @EmbeddedId가 필요하다.)
JPA에서 복합 기본 키를 만들려면 MemberProductId와 같은 별도의 식별자 클래스가 필요하다. 그리고 식별자 클래스에선 equals, hashCode 도 구현해야 한다.
MemberProduct에서 Member와 Product의 기본키를 받아 자신의 기본키 + 외래키로 사용하는 것을 데이터 베이스 용어로 식별 관계 Identifying Relationship
라고 한다.
복합키를 사용하지 않고 간단히 다대다 관계를 구성하는 방법은 데이터베이스에서 자동으로 생성해주는 대리 키를 Long 값으로 사용하는 것이다.
@Entity
public class MemberProduct {
@Id @GeneratedValue
@Column(name = "MemberProduct_ID")
private Long id;
@Id @ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member; // MemberProductId.member와 연결
@Id @ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product; // MemberProductId.product와 연결
}
이를 통해 식별자 클래스를 사용하지 않고 간결하게 코드를 작성할 수 있다.
다대다 관계를 일대다 다대일 관계로 풀려고 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야 한다.
DB 설계에서와 달리 객체 입장에선 단순한 비식별 관계를 추천한다.
김영한, 『자바 ORM 표준 JPA 프로그래밍』 에이콘(2015)