[JPA] JPA 연관관계 매핑 (다대일, 일대다, 일대일, 다대다)
JPA 연관관계 매핑
연관관계 매핑 시 다음 3가지를 고려하여 매핑해야한다.
1) 다중성
다대일 (N : 1) ManyToOne
일대다 (1 : N) OneToMany
일대일 (1 : 1) OneToOne
다대다 (N : N) ManyToMany
2) 방향
양방향, 단방향
DB 테이블은 외래 키(Fk)하나로 조인을 사용해서 양방향으로 쿼리가 가능하다. 따라서 DB에는 방향의 개념이 없다. 그러나 객체의 경우, 참조용 필드를 가지고 있는 객체만 연관된 객체를 조회할 수 있으므로 방향의 개념이 존재한다. 객체 관계에서 한 쪽만 반대쪽을 참조하는 관계를 단방향, 양 쪽 모두 서로를 참조하는 관계를 양방향이라고 한다.
3) 연관관계의 주인
객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.
데이터베이스는 외래 키(FK) 하나로 두 테이블이 연관관계를 맺는다. 즉 연관관계를 관리하는 포인트는 외래 키 하나다. 반면 객체에서는 양방향 관계로 매핑하면 A -> B, B -> A, 두 곳에서 서로를 참조하므로 연관관계를 관리하는 포인트는 두 곳이 된다. 따라서 JPA는 두 객체 중 하나를 정해서 외래 키를 관리하게 만들어야 하는데, 여기서 외래 키를 관리하는 객체를 연관관계의 주인이라고 한다. 외래 키를 가진 테이블과 매핑되는 엔티티가 외래 키를 관리하는 것이 효율적이므로 보통 이곳을 연관관계의 주인으로 선택한다. 주의할 점은 외래 키를 관리하는 연관관계의 주인만이 외래 키를 변경할 수 있으며 주인이 아닌 곳은 읽기만 가능하다는 점이다.
모든 연관관계 정리
1) 다대일 : 단방향, 양방향 (N : 1) ManyToOne
연관관계의 주인을 굵은 글씨 처리했다. 아래는 다대일 단방향 예시 코드.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID") // @JoinColumn 으로 Member.team 필드를 TEAM_ID 외래 키와 매핑.
private Team team;
...
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
...
}
Member.team으로 팀 엔티티를 참조할 수 있지만, 반대로 팀에는 회원을 참조하는 필드가 없다. 따라서 회원과 팀은 다대일 단방향 연관관계.
1-2) 다대일 양방향
단뱡향이 양방향이 되었다고 해서 테이블에 영향을 주는것 x. 테이블은 그대로고, 객체에서만 반대 방향으로의 연관관계가 하나 늘어난 것이다. 다음 코드에와 같이 Team에만 참조가 하나 늘어난다.
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
...
}
@OneToMany(mappedBy = "team")
이 @OneToMany(mappedBy 어노테이션을 통해 연관관계의 주인, 즉 외래키를 관리하는 필드(Member의 team 필드)를 명시해준다.
2. 일대다 : 단방향, 양방향 (1 : N) OneToMany
일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로, 자바 컬렉션 Collection, List, Set, Map 중 하나를 사용해야 한다.
일대다 단방향 관계는 하나의 팀이 회원들을 참조하지만 반대로 회원은 팀을 참조하지 않는 형태다.
특이하게도 Team 엔티티의 Team.members가 MEMBER 테이블의 TEAM_ID 외래 키를 관리한다. 보통은 자신이 매핑한 테이블의 외래 키를 관리하는데, 일대다(연관관계의 주인이 1) 단방향 연관관계에서는 외래키가 항상 다쪽 테이블에 존재하기 때문에 반대편 테이블의 외래 키를 관리하는 특이한 모습이다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
...
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
...
}
포인트는 mappedBy 속성이 없어졌다는 점이다. Team이 연관관계의 주인이므로 mappedBy 속성을 지워주었다. 또한 @JoinColumn을 명시해 주었다. @JoinColumn을 사용하지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용하여 매핑하기 때문에, 꼭 @JoinColumn을 붙여주어야 한다.
[일대다 단방향보다 다대일 양방향을 사용하자]
이런 일대다 단방향 매핑에는 문제가 있는데..
매핑한 객체가 관리하는 외래키가 다른 테이블에 있다는 점이다. 본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT 쿼리 한 번으로 끝낼 수 있는데, 다른 테이블에 외래키가 있으면 연관관계 처리를 위한 UPDATE 쿼리를 추가로 실행해야 한다.
또 자신의 테이블이 아닌 다른 테이블의 외래 키를 관리하게 되기 때문에, 관리가 어려워진다.
2-1. 일대다 양방향
.. 은 존재하지 않는다.
정확히는 일대다 양방향 매핑에서 @OneToMany는 데이터베이스의 특성상 연관관계의 주인이 될 수 없다. 일대다 관계에서는 항상 다 쪽에 외래 키가 존재하기 때문이다. 그래서 @ManyToOne에는 mappedBy 속성이 없다. 정 정말 일대다 양방향 매핑을 하고 싶다면, 방법이 아예 없는것은 아니지만...
사실 일대다 양방향과 다대일 양방향이 똑같은 말이기 때문에 가능한 다대일 양방향을 사용하는 것이 좋다.
3. 일대일 (1 : 1) OneToOne
양쪽이 서로 하나의 관계만을 가진다.
일대일 관계의 반대도 일대일 관계고, 일대일 관계는 두 테이블 중 어느곳에서든 외래 키를 가질 수 있다. 일대일 관계일 경우 고려해야 할 요소가 하나 더 늘어난다.
3-1. 일대일 : 주 테이블 외래키 (단방향)
주 객체가 대상 객체를 참조하는 것처럼, 주 테이블에 외래 키를 두고 대상 테이블을 참조하는 방법이다.
장점은 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다는 점으로, 객체 참조와 외래 키를 비슷하게 사용할 수 있다. 해당 방법을 사용하기 위해서는 외래 키에 데이터베이스 유니크 제약조건을 추가해주어야 한다. (이것 말고는 일대다, 다대일과 굉장히 비슷함) 위 예시에서 주 테이블은 MEMBER이고, 대상 테이블은 LOCKER 다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
}
다대일, 일대다와 비슷한데 어노테이션 차이 정도인 듯 하다.
양방향은 여기서 Locker에 mappedBy로 주인만 정해준다.
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member memeber;
}
3-2. 일대일 : 대상 테이블 외래키 (단방향, 양방향)
데이터베이스 개발자들이 선호하는 방식이라고 한다.
장점은 주 테이블과 대상 테이블의 관계를 1대1에서 일대다 관계로 변경할 때 테이블 구조가 유지된다는 점이다.
일단 일대일/대상테이블 외래키/단방향 관계는 지원하지 않는다.
그리고 양방향은 주 테이블에 외래키, 양방향과 동일하다. 주 테이블이라는 점만 바뀌었다.
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member memeber;
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private Locker locker;
}
단점은 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩이 된다.
4. 다대다
책의 저자인 김영한 선생님께서 비추하는 방식. 쓰지 말라고 하심.
다대다 관계 대신 연결 엔티티 사용을 권장한다. 두 테이블을 연결하는 연결 테이블을 만들고, 그것을 연결 엔티티로 해주는 것이다.
@ManyToMany 가 @OneToMany, @ManyToOne으로 나눠지는 형태를 말한다.
아래 그림에서는 Member와 Product의 다대다 관계를 Order 엔티티를 추가하며 1대다, 다대1 관계로 풀어냈다.
양방향 매핑의 팁. 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 상태다. 단방향 매핑으로 먼저 설계를 끝낸 후 필요한 경우에만 양방향을 추가해도 된다. 이는 DB 테이블에 영향을 주지 않기 때문에..
참고: