연관 관계 정의 규칙
1. 방향 (단방향, 양방향)
DB 테이블은 외래 키 하나로 양 쪽 테이블 조인이 가능하기 때문에 단방향, 양방향 나눌 필요가 없다.
그러나 객체는 참조용 필드가 있는 객체만 다른 객체를 참조할 수 있다. 그래서 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 있으면 단방향, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향 관계라고 한다.
무조건적인 양방향 매핑은 복잡성을 증가시키고 유지 보수도 어렵게 만들기 때문에 기본적으로 단방향 매핑을 사용하되 역방향 객체 탐색이 반드시 필요한 경우에 추가하는 것이 바람직하다.
고객(Member) 객체와 주문(Order) 객체가 있을 때, 고객이 주문한 내역을 모두 확인하기 위해서는 고객 객체에 주문을 참조할 수 있는 필드가 필요하다. 만약 주문 정보에 고객 정보도 포함하고 싶다면 똑같이 주문 객체 내에 고객을 참조할 수 있는 필드를 넣으면 된다. 이것이 양방향이다.
2. 연관 관계의 주인 (양방향일 때 연관 관계에서 관리 주체)
두 엔티티간에 관계가 맺어지면, 특히 다대일, 일대다의 관계에서는 참조의 주인과 대상으로 나뉜다. 보통 "다" 쪽에 해당하는 엔티티가 FK를 가지고 있고 관계의 주인이 되며 다른 하나는 참조의 대상이 된다.
참조의 대상이 되는 클래스는 @OneTo**(mappedBy="어쩌구")로 주인의 어느 필드에 매핑되는지 명시해줘야 한다. 또한 주인과 달리 FK를 변경할 수 없고 읽기만 가능하다.
3. 다중성 (일대일, 일대다, 다대일, 다대다)
다중성은 1:1, 1:N, N:1, N:N 으로 나뉜다. DB 기준으로 다중성을 결정한다.
다중성은 방향과 관계가 없다. 일대일 단방향, 일대일 양방향, 다대일 단방향, 일대다 양방향… 모두 가능하다. 다중성으로 방향이 정해지거나 하지 않는다. 그 반대도 마찬가지.
1:1 (@OneToOne)
외래키를 주 테이블, 대상 테이블 어느 곳에 넣어도 상관 없다. (근데 JPA는 대상 테이블에 fk 넣는 걸 지원 안함)
게시글에 최대 하나의 첨부파일을 넣을 수 있다고 할 때, 게시글(1) : 첨부파일(1) 이 된다.
단방향 (주 테이블: Post, PK 소유: Post)
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@OneToOne
@JoinColumn(name = "ATTACH_ID")
private Attach attach;
}
@Entity
public class Attach {
@Id @GeneratedValue
@Column(name = "ATTACH_ID")
private Long id;
private String name;
}
양방향
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@OneToOne
// 내 쪽에서 Attach의 fk 가짐
@JoinColumn(name = "ATTACH_ID")
private Attach attach;
}
@Entity
public class Attach {
@Id @GeneratedValue
@Column(name = "ATTACH_ID")
private Long id;
private String name;
// 내가 대상 테이블. 내 주인은 attach로 날 참조 중
@OneToOne(mappedBy = "attach")
private Post post;
}
N:1 (@ManyToOne)
작성자와 게시글의 관계를 예로 들어보자면, 이는 게시글(N) : 작성자(1) 관계이다.
한 명의 작성자는 여러가지 게시글을 작성할 수 있고, 하나의 게시글은 (일반적으로) 한 명의 작성자를 가진다. DB 적으로 보면, 작성자(==User) 객체는 혼자 알아서 잘 있는데 게시글(Post) 객체가 이 작성자 객체의 pk를 외래키로 잡고 자신의 컬럼에 포함시킨 것이다.
fk는 반드시 N쪽 테이블에 저장된다.
단방향
// N
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
private String title;
private String content;
@ManyToOne
@JoinColumn(name = "USER_ID")
private User author;
}
// 1
@Entity
public class User {
@Id @GeneratedValue
@Column(name = "USER_ID")
private Long id;
private String name;
private String password;
}
이 때, 기본적으로 N이 되는 게시글 객체가 외래키를 참조하고 있으므로 관계의 주인이 된다.
양방향
// N
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
private String title;
private String content;
// 내가 다(N)이고 저쪽이 일(1)이다 -> ManyToOne
@ManyToOne
@JoinColumn(name = "USER_ID")
private User author;
}
// 1
@Entity
public class User {
@Id @GeneratedValue
@Column(name = "USER_ID")
private Long id;
private String name;
private String password;
// 내가 일(1)이고 저쪽이 다(N)이다 -> OneToMany
// N의 변수명 참조
@OneToMany(mappedBy = "author")
List<Post> posts = new ArrayList<>();
}
양방향 참조가 되었으므로, 이 때 작성자 객체에 @OneToMany를 추가하고 mappedBy 속성을 달아 내가 연관 관계의 주인이 아님을 알려야 한다.
1:N (@OneToMany)
앞의 다대일은 연관관계의 주인이 다(N)쪽에 있지만, 일대다는 일(1)쪽에 있다. 일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 Collection을 사용해야 한다.
- 단점
- 객체 매핑시 사용된 FK가 주 테이블이 아닌 다른 테이블에 있다는 것. (N쪽에 저장)
- TEAM : MEMBER = 1 : N
실무에서는 권장하지 않는 방식이다. 아까 전에는 양방향 매핑이었기 때문에 1쪽에 붙여주었지만, 양방향도 아닌데 @OneToMany 단독으로 사용하는 것은 지양해야한다.
// N
@Entity
class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
private String title;
private String content;
}
// 1
@Entity
class User {
@Id @GeneratedValue
@Column(name = "USER_ID")
private Long id;
private String name;
private String password;
// 내가 일(1)이고 저쪽이 다(N)이다 -> OneToMany
@OneToMany
@JoinColumn(name = "POST_ID")
List<Post> posts = new ArrayList<>();
}
일대다 양방향은 @JoinColumn(updatable = false, insertable = false) 이지만, 일대다 양방향을 사용해야할 때는 다대일 양방향 사용하도록 하는게 더 좋다.
N:N (@ManyToMany)
역시 실무에서 사용을 권장하지 않는다.
RDB는 정규화된 테이블 2개로 다대다를 표현할 수 없다. 그래서 보통 연결 테이블을 하나 추가해서 다대다를 일대다, 다대일로 풀어내는 방식을 사용한다.
예로, 회원과 상품의 관계를 보았을 때, 회원은 여러개의 상품을 구매할 수 있고, 하나의 상품도 여러 회원에게 구입될 수 있다. 따라서 이 둘은 다대다 관계인데 이를 RDB로 풀기 위해서는 연결 테이블을 추가해야 한다.
반면 객체는 바로 다대다 관계를 만들 수 있다. 그냥 회원 객체에 상품 리스트를 만들고, 상품 객체에 회원 리스트를 만들면 바로 다대다 관계가 만들어진다.
하지만 실무에서 이렇게 사용하지 않는데, 그 이유는 Member_Product 테이블에 단순히 다대다 관계 매핑에 필요한 컬럼만 존재하지 않기 때문이다. (??)
주문 수량, 주문 날짜 등 주문 엔티티나 상품 엔티티에 추가한 컬럼을 매핑할 수 없기 때문에 @ManyToMany는 더이상 사용할 수 없게 된다. (??) 그래서 결국 객체에서 바로 다대다 관계를 만들 수 있다 하더라도 연결 테이블용 엔티티를 추가해서 @ManyToMany 를 @OneToMany, @ManyToOne로 분할 시키는 것이다.
참고