엔티티의 연관관계를 매핑하기 위해서는 “다중성, 방향, 연관관계 주인” 3가지가 반드시 고려되어야 한다.
다중성
DB 테이블 관점에서 다중성을 의미한다.
- 다대일 관계 : DB에서 N:1 관계를 의미하며, @ManyToOne을 이용해 표현한다.
- 일대다 관계 : DB에서 1:N 관계를 의미하며, @OneToMany를 이용해 표현한다.
- 일대일 관계 : DB에서 1:1 관계를 의미하며, @OneToOne을 이용해 표현한다.
- 다대다 관계 : DB에서 N:M 관계를 의미하며, @ManyToMany를 이용해 표현한다. 실무에서 잘 사용하지 않는다.
방향
테이블은 외래키를 가지고 테이블을 조인해 조인한 테이블 데이터를 사용할 수 있지만, 객체는 참조를 통해 연관된 객체를 조회하기 때문에 A객체가 B객체를 조회할 수 있는지, B객체가 A객체를 조회할 수 있는지와 같이 방향이라는 개념이 도입된다.
객체 관계에서 A → B만 참조할 수 있는 관계를 단방향 관계라고 하고, A → B, B → A 처럼 두 객체가 서로를 참조할 수 있는 관계를 양방향 관계라고 한다.
연관관계의 주인
객체의 양방향 관계에서는 참조하는 필드가 결국 두 객체에게 생긴다.
이 경우, 데이터의 수정이 이뤄졌을 때 두 객체가 서로 참조하고 있기 때문에 둘 중 한 객체에서 변경 값을 반영해야한다.
이 때, 어떤 객체가 참조를 관리할지(외래키를 관리할 지) 선택하는 개념이 연관관계의 주인이다.
@ManyToOne
관계형 DB에서는 항상 “다" 쪽에 외래키가 존재한다.
DB의 관계는 대칭성을 가지고 있어 다대일 관계의 반대는 일대다 관계이다.
단방향
다대일 단방향 관계는 @JoinColumn을 이용해 연관관계를 매핑 (테이블의 외래키 매핑)할 수 있다.
외래키를 가진 테이블과 매핑된 엔티티에 래퍼런스 필드를 생성한다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "team_id") // 단방향 연관관계 매핑
private Team team;
}
@Entity
public class Team {
@Id @GenreatedValue
@Column(name = "team_id")
private Long id;
private String name;
}
양방향
양방향 관계에서는 반드시 연관관계의 주인을 지정해주어야한다.
연관관계의 주인을 지정해줄 때는 mappedBy 프로퍼티를 이용한다. (Read-Only)
다대일 관계의 반대 사이드는 대칭성에 의해 일대다 관계(@OneToMany)를 갖는다.
또한, DB 설계 상 항상 “다”쪽의 테이블이 외래키를 관리하기 때문에 연관관계의 주인은 “다" 외래키를 가진 엔티티 (Member 엔티티)가 된다.
@Entity
public class Team {
@Id @GenreatedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "member")
private List<Member> members = new ArrayList<>();
}
연관관계의 주인이 아닌 테이블에 @OneToMany를 이용해 래퍼런스를 추가한다고 해서 테이블 구조의 변경에는 영향을 끼치지 않는다.
양방향 연관관계에서는 항상 서로를 참조해야하기 때문에 연관관계 편의 메소드를 이용해 두 엔티티에 모두 래퍼런스가 수정되어야 한다.
✔️ 연관관계 메소드를 양쪽에 모두 작성한 경우 무한 루프 이슈가 생길 수 있으므로, 둘 중 하나만 호출한다.
// Member 엔티티에 연관관계 메소드가 있는 경우
public void setTeam(Team team) {
this.team = team;
if(!team.getMembers().contains(this) {
team.getMembers().add(this);
}
}
// Team 엔티티에 연관관계 메소드가 있는 경우
public void addMember(Member member) {
this.members.add(member);
if(member.getTeam() != this) {
member.setTeam(this);
}
}
@OneToMany
단방향
일대다에서는 “일"이 연관관계의 주인이 된다. 실무에서 주로 사용하지 않고, 권장되지 않는다.
“일"이 연관관계의 주인이 된다고 했는데 DB에서는 항상 “다"쪽에 외래키가 존재한다.
이런 경우 매핑한 객체가 관리하는 외래키가 다른 테이블에 있기 때문에 엔티티 저장 처리 시 insert 이외에도 다른 테이블의 외래키 처리를 위해 update 쿼리를 추가로 실행하는 단점이있다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
}
@Entity
public class Team {
@Id @GenreatedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "team_id")
private List<Member> members = new ArrayList<>(); // 회원 테이블이 외래키를 가지므로 추가 업데이트 발생
}
따라서, 일대다 단방향 연관관계는 객체지향적인 엔티티 구현을 조금 포기하더라도 다대일 양방향 관계로 변환하여 사용하는 것이 유지보수 측면에서 바람직하다.
양방향
일대다 양방향은 존재하지 않고 다대일 양방향을 사용한다.
항상 테이블의 “다"쪽에 외래키가 존재하기 때문에 @OneToMany는 연관 관계의 주인이 될 수 없고 @ManyToOne이 연관관계 주인이 되어야 한다.
@ManyToOne에는 mappedBy 프로퍼티가 존재하지 않는다.
@OneToOne
일대일 관계는 대칭성에 의해 반대 테이블도 일대일 관계를 갖는다.
일대일 관계에서는 두 테이블 중 아무 테이블에서나 외래키를 관리하는 것이 가능하다.
따라서, 주 테이블(주로 Access 하는 테이블)이나 대상 테이블에 외래키를 넣어 사용한다.
단방향
주 테이블에 외래키를 관리하는 것은 객체 참조와 비슷하게 사용할 수 있다.
다대일 단방향과 유사하며 일대일 연관관계 이기때문에 외래키는 항상 Unique하다.
@Entity
public class Member {
@Id @GeneratedValue
@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 = "lokcer_id")
private Long id;
private String name;
}
양방향
주 테이블이 외래키를 관리하기 때문에 연관관계의 주인이 되며 반대쪽 대상 엔티티는 mappedBy를 이용해 연관관계 주인이 아님을 설정해야한다.
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "lokcer_id")
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member;
}
주 테이블과 대상 테이블 어디든 외래키를 관리할 수 있다.
주 테이블이 외래키를 관리하는 경우 주 테이블만 조회해도 대상 테이블에 데이터가 존재하는지 판단이 가능하지만, 대상 테이블이 값을 갖지 않는 경우 외래키를 null로 허용해야 한다.
대상 테이블이 외래키를 관리하는 경우 추후 일대일 관계에서 일대다 관계로 변경 시 대상 테이블이 “다"가 되기때문에 테이블 구조에 대한 변경을 할 필요가 없다.
하지만 프록시 기능의 한계로 지연 로딩을 사용할 수 없다.
@ManyToMany
DB에서 다대다 관계는 정규화된 테이블 2개로 표현할 수 없기 때문에 일대다, 다대일 관계로 풀어주는 중간 테이블을 이용한다.
하지만 객체에서는 두 객체를 이용해 다대다 관계를 풀어낼 수 있다.
@ManyToMany를 이용하면 중간 테이블을 자동으로 생성해주는 장점이 있지만, 중간 테이블내에 컬럼을 추가하고자 하는 경우에는 사용할 수가 없다.
따라서, @ManyToMany는 사용을 지양하고, @OneToMany를 이용하는 두 테이블, @ManyToOne을 이용하는 중간 테이블을 엔티티로 승격해 관계를 풀어내는 것이 바람직하다.
중간 테이블을 사용하는 경우 두 테이블의 외래키를 기본키로 두는 식별 관계로 사용하지 않고, 새로운 기본키를 두고, 외래키로만 관리하는 비식별 관계를 사용한다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
@OneToMany(mappedBy = "member") // 회원이 주문을 조회하는 경우 있으므로 양방향
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Product {
@Id @GeneratedValue
@Column(name = "product_id")
private Long id;
private String name;
// 상품이 주문을 조회할 일이 없다고 판단하여 단방향
}
// 중간 테이블
@Entity
public class Order {
@Id @GeneratedValue
@Column(name = "order_id") // 새로운 기본키로 사용
private Long id;
@ManyToOne
@JoinColumn(name = "member_id") // 외래키로 사용
private Member member;
@ManyToOne
@JoinColumn(name = "product_id") // 외래키로 사용
private Product product;
... 추가 필요 컬럼들
}
📄 References
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 : https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
'Backend > JPA' 카테고리의 다른 글
상속관계 매핑 (0) | 2022.07.20 |
---|---|
엔티티 설계의 주의사항 간단 정리 (0) | 2022.07.05 |
단방향, 양방향 매핑 (0) | 2022.04.26 |
기본키 매핑, @Id, @GeneratedValue (0) | 2022.04.19 |
필드와 컬럼 매핑, @Column (0) | 2022.04.19 |