반응형
연관관계 매핑시 고려할 점
- 다중성
- 두 엔티티가 일대일 관계인지 일대다 관계인지의 다중성을 고려해야 한다.
- 단방향, 양방향
- 두 엔티티 중 한쪽만 참조하는 단방향 관계인지 서로 참조하는 양방향 관계인지 고려해야 한다.
- 연관관계의 주인
- 양방향 관계면 연관관계의 주인을 정해야 한다.
- 다중성
- 다대일(@ManyToOne)
- 일대다(@OneToMany)
- 일대일(@OneToOne)
- 다대다(@ManyToMany) : 실무에서 거의 사용하지 않는다.
- 단방향, 양방향
- 객체 관계에서 한 쪽만 참조하는 것을 단방향, 양쪽이 서로 참조하는 것을 양방향이라 한다.
- A 엔티티와 B 엔티티가 연관관계를 맺는다 할 때, A의 필드에만 B가 있고 B에는 A가 없다면 단방향이다. 서로의 필드에 서로가 있다면 양방향이다.
- 연관관계의 주인
- 데이터베이스는 외래 키 하나로 두 테이블이 연관관계를 맺지만 양방향 매핑된 엔티티는 서로 참조한다.
- JPA는 두 객체 연관관계 중 하나를 정해서 외래 키를 관리하는데 이것을 연관관계의 주인이라 한다.
- 외래 키를 가진 테이블과 매핑한 엔티티가 외래 키를 관리하는 것이 효율적이므로 보통 이곳을 연관관계의 주인으로 선택한다.
- 연관관계의 주인은 mappedBy 속성을 사용하지 않는다. 연관관계의 주인이 아니면 mappedBy 속성을 사용하고 연관관계의 주인 필드 이름을 값으로 입력해야 한다.
다대일
- 데이터베이스 테이블의 1, N 관계에서 외래 키는 항상 N에 있다. 즉, 객체 양방향 관계에서 연관관계의 주인은 항상 N이다.
- 다대일 단방향 [N : 1]
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
private String name;
}
////////////////////////////////////////
@Entity
public class FreeBoard {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "FREE_BOARD_ID")
private long id;
private String title;
private String content;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
}
- 자유 게시글은 FreeBoard.member로 회원 엔티티를 참조할 수 있지만 회원에는 자유 게시글을 참조하는 필드가 없다. 즉, 다대일 양방향 연관관계다.
- @JoinColumn 애너테이션을 사용해서 FreeBoard.member필드를 MEMBER_ID 외래 키와 매핑했다. 따라서 FreeBard.member 필드로 자유 게시글 테이블의 MEMBER_ID 외래 키를 관리한다.
- 다대일 양방향 [N : 1, 1 : N]
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
private String name;
@OneToMany(mappedBy = "member")
private List<FreeBoard> freeBoards = new ArrayList<>();
}
////////////////////////////////////////
@Entity
public class FreeBoard {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "FREE_BOARD_ID")
private long id;
private String title;
private String content;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
}
- 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.
- 외래 키는 항상 N에 있다. 즉, 위의 경우에서 연관관계의 주인은 자유 게시글이다. 따라서, Member에는 mappedBy가 적용되어야 한다.
- JPA는 외래 키를 관리할 때 연관관계의 주인만 사용한다. 주인이 아닌 Member.freeBoards는 조회를 위한 JPQL이나 객체 그래프를 탐색할 때 이용한다.
- 양방향 연관관계는 항상 서로를 참조해야 한다.
- 어느 한 쪽만 참조하면 양방향 연관관계가 성립하지 않는다.
- 항상 서로를 참조하게 하려면 연관관계 편의 메서드를 작성하는 것이 좋다.
- 그러나 편의 메서드를 양쪽에 다 작성하면 무한루프에 빠질 수 있으므로 주의해야 한다. 위의 경우에 작성할 수 있는 편의 메서드는 다음과 같을 것이다.
public void writeFreeBoard(FreeBoard freeBoard){
this.freeBoards.add(freeBoard);
//무한 루프에 빠지지 않도록 검증하는 로직
if(freeBoard.getMember() != this){
freeBoard.setMember(this);
}
}
일대다
- 일대다 연관관계는 다대일 연관관계의 반대 방향이다. 여러 개의 엔티티를 참조할 수 있으므로 컬렉션을 사용한다.
- 일대다 단방향 [1 : N]
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
private String name;
@OneToMany
@JoinColumn(name = "MEMBER_ID")
private List<FreeBoard> freeBoards = new ArrayList<>();
}
////////////////////////////////////////
@Entity
public class FreeBoard {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "FREE_BOARD_ID")
private long id;
private String title;
private String content;
}
- 일대다 단방향 관계는 조금 특이하다. 멤버 엔티티의 Member.freeBoards로 자유 게시판 테이블의 FREE_BOARD_ID 외래 키를 관리한다.
- 일대다 관계에서 외래 키는 항상 N에 있다. 그러나 위의 경우에는 N에 해당하는 자유 게시글에 외래 키를 매핑할 수 있는 참조 필드가 없다. 대신 반대쪽에만 참조 필드가 있기 때문에 반대편 테이블에서 외래 키를 관리한다.
- 일대다 단방향 매핑은 @JoinColumn을 명시해야한다. 그렇지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다.
- 일대다 단방향 매핑의 단점
- 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다.
- 만일 본인 테이블에 외래 키가 있다면 엔티티의 저장과 연관관계 처리를 INSERT SQL 하나로 처리할 수 있다.
- 하지만 다른 테이블에 외래 키가 있기 때문에 UPDATE SQL을 추가로 실행해야 한다.
- 위의 예시에서 FreeBoard엔티티를 저장할 때는 FreeBoard테이블의 MEMBER_ID 에 아무런 값도 저장되지 않는다.
- Member엔티티를 저장할 때 Member.freeBoards의 참조 값을 확인해서 자유 게시글 테이블에 있는 MEMBER_ID 외래 키를 업데이트 한다.
- 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자.
- 일대다 매핑의 경우, 엔티티를 매핑한 테이블의 반대쪽 테이블에서 외래 키를 관리한다. 이는 추가 쿼리의 성능 문제도 야기하지만 관리도 부담스럽게 만든다.
- 다대일 양방향 매핑은 관리해야 하는 외래 키가 본인 테이블에 있다. 따라서 일대다 단방향 매핑의 문제가 발생하지 않는다.
- 어짜피 일대다 매핑과 다대일 매핑의 모양새는 같다. 누가 연관관계의 주인이 될 것인지에만 차이가 있다.
- 따라서 일반적으로 일대다 단방향을 사용해서 문제의 소지를 안고가는 것보다 다대일 양방향을 사용하는 편이 낫다.
- 일대다 양방향 [1:N, N:1]
- 일대다 양방향 매핑은 존재하지 않는다. 정확히 말하자면 @OneToMany는 연관관계의 주인이 될 수 없다.
- 반대쪽 다대일 단방향 매핑을 읽기 전용으로 하나 추가하면 억지로 일대다 양방향 매핑이 가능하다.
- 그러나 여전히 단방향 매핑이 가지는 단점을 그대로 가진다. 굳이 억지로 만들어서까지 사용할 필요가 있을까?
일대일
- 일대일 관계는 그 반대도 일대일 관계다.
- 테이블 관계에서 일대다, 다대일은 항상 N쪽이 외래 키를 가진다. 반면 일대일 관계는 주 테이블이나 대상 테이블 중 어느 곳이나 외래 키를 가질 수 있다.
- 따라서 일대일 관계는 누가 외래 키를 가질지 선택해야 한다.
- 주 테이블이 외래 키를 갖는 방법
- 주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 참조한다.
- 외래 키를 객체 참조와 비슷하게 사용할 수 있어 객체지향 개발자들이 선호한다.
- 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.
- 객체지향 개발자들이 선호할뿐만 아니라 JPA도 주 테이블에 외래 키가 있으면 좀 더 편리하게 매핑할 수 있다.
- 대상 테이블이 외래 키를 갖는 방법
- 전통적인 데이터베이스 개발자들은 보통 대상 테이블에 외래 키를 두는 것을 선호한다.
- 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.
- 주 테이블에 외래키 단방향
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
private String name;
@OneToOne
@JoinColumn(name = "WALLET_ID")
private Wallet wallet;
}
////////////////////////////////////////
@Entity
public class Wallet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "WALLET_ID")
private long id;
private long balance;
}
- Member가 Wallet을 가지는 형태다. 즉, 주 테이블은 MEMBER 테이블이 되며, 외래 키인 WALLET_ID는 MEMBER테이블에 둔다.
- 일대일 관계이므로 @OneToOne을 사용한다.
- 주 테이블에 외래키 양방향
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
private String name;
@OneToOne
@JoinColumn(name = "WALLET_ID")
private Wallet wallet;
}
////////////////////////////////////////
@Entity
public class Wallet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "WALLET_ID")
private long id;
private long balance;
@OneToOne(mappedBy = "wallet")
private Member member;
}
-
- 양방향으로 만들었으니 연관관계의 주인을 설정해야 한다. MEMBER테이블이 외래 키를 가지고 있으므로 Member엔티티에 있는 Member.wallet이 연관관계의 주인이다.
- 대상 테이블에 외래키 단방향
- 일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다. 이런 모양으로 매핑할 수 있는 방법도 없다.
- 따라서 단방향 관계를 대상 테이블에서 주 테이블 방향으로 수정하거나, 양방향 관계로 만들고 대상 테이블을 연관관계의 주인으로 설정해야 한다.
- Member에 Wallet이 있는데, WALLET 테이블이 MEMBER_ID를 갖는 형태는 안된다는 말이다.
- Wallet이 Member를 갖도록 바꾸던지, 양방향 관계로 설정하고 WALLET 테이블에 외래 키를 둬야 한다.
- 대상 테이블에 외래키 양방향
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
private String name;
@OneToOne(mappedBy = "member")
private Wallet wallet;
}
////////////////////////////////////////
@Entity
public class Wallet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "WALLET_ID")
private long id;
private long balance;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
}
다대다
- 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
- 대신 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
- 그러나 테이블과 다르게 객체는 컬렉션을 사용해서 객체 2개만으로 다대다 관계를 만들 수 있다.
- @ManyToMany를 사용하면 다대다 관계를 편리하게 매핑할 수 있다.
- 다대다 단방향
- 고객은 여러 바버샵에 방문할 수 있고, 바버샵에는 여러 고객이 올 수 있다. 즉, 둘은 다대다의 관계다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
private String name;
@OneToOne(mappedBy = "member")
private Wallet wallet;
@OneToMany(mappedBy = "member")
private List<FreeBoard> freeBoards = new ArrayList<>();
@ManyToMany
@JoinTable(
name = "MEMBER_VISITED_BARBER_SHOP",
joinColumns = @JoinColumn(name = "MEMBER_ID"),
inverseJoinColumns = @JoinColumn(name = "BARBER_SHOP_ID")
)
private List<BarberShop> visitedBarberShops = new ArrayList<>();
public void writeFreeBoard(FreeBoard freeBoard){
this.freeBoards.add(freeBoard);
if(freeBoard.getMember() != this){
freeBoard.setMember(this);
}
}
}
////////////////////////////////////////
@Entity
public class BarberShop {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "BARBER_SHOP_ID")
private long id;
private String name;
}
- 회원 엔티티와 바버샵 엔티티를 매핑하면서 @JoinTable을 사용해 연결 테이블을 바로 매핑했다. 이렇게 사용하면 중간 연결 엔티티를 사용하지 않고 다대다 매핑을 바로 진행할 수 있다.
- @JoinTable.name : 연결 테이블을 지정한다.
- @JoinTable.joinColumns : 현재 방향인 회원과 매핑할 조인 칼럼 정보를 지정한다.
- @JoinTable.inverseJoinColumns : 반대 방향인 바버샵과 매핑할 조인 칼럼 정보를 지정한다.
- Member.visitedBarberShop에 특정 바버샵을 추가하면 MEMBER_VISITED_BARBER_SHOP테이블에 INSERT SQL이 실행된다.
- member.getVisitedBarberShop();을 통해 리스트를 가져오면 연결 테이블인 MEMBER_VISITED_BARBER_SHOP과 바버샵 테이블을 조인해서 연관된 상품을 조회한다.
- 다대다 양방향
- 반대 방향에도 @ManyToMany를 추가하고 연관관계의 주인을 정해주면 된다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
private String name;
@OneToOne(mappedBy = "member")
private Wallet wallet;
@OneToMany(mappedBy = "member")
private List<FreeBoard> freeBoards = new ArrayList<>();
@ManyToMany
@JoinTable(
name = "MEMBER_VISITED_BARBER_SHOP",
joinColumns = @JoinColumn(name = "MEMBER_ID"),
inverseJoinColumns = @JoinColumn(name = "BARBER_SHOP_ID")
)
private List<BarberShop> visitedBarberShops = new ArrayList<>();
public void writeFreeBoard(FreeBoard freeBoard){
this.freeBoards.add(freeBoard);
if(freeBoard.getMember() != this){
freeBoard.setMember(this);
}
}
}
////////////////////////////////////////
@Entity
public class BarberShop {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "BARBER_SHOP_ID")
private long id;
private String name;
@ManyToMany(mappedBy = "visitedBarberShops")
private List<Member> visitors = new ArrayList<>();
}
- 양방향 연관계이니 사용자가 특정 바버샵에 방문하면 방문한 샵의 정보와 샵에 방문한 사용자의 정보를 둘 모두에게 추가해줘야한다.
- 따라서 편의 메서드를 만들어 사용하는 것이 낫다.
public void visitBarberShop(BarberShop barberShop){
this.visitedBarberShops.add(barberShop);
barberShop.getVisitors().add(this);
}
- 다대다 매핑의 한계와 극복, 연결 엔티티 사용
- @ManyToMany와 @JoinTable을 이용해 만들어진 테이블에는 각 테이블의 외래 키 값만 담겨있다.
- 즉, 이렇게 만들어진 테이블에는 다른 정보를 저장할 수 없다는 한계가 있다. 그러나 실무에서는 추가적인 정보가 더 많이 필요하다. 위의 예를 들면, 고객이 바버샵에 평점을 남긴다면 평점 정보가 칼럼이 되어 테이블에 담겨야 할 것이다.
- 이렇게 추가 칼럼을 이용하기 위해, 데이터베이스에서 연결 테이블을 사용하는 것처럼 엔티티 간 다대다 관계를 일대다, 다대일로 나눠줄 연결 엔티티를 만들어 연결 테이블로 사용한다.
- 이번에는 VisitedBarberShop이라는 엔티티를 새로 만들고 사용해볼 것이다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
private String name;
@OneToOne(mappedBy = "member")
private Wallet wallet;
@OneToMany(mappedBy = "member")
private List<FreeBoard> freeBoards = new ArrayList<>();
@OneToMany(mappedBy = "member")
private List<VisitedBarberShop> visitedBarberShops = new ArrayList<>();
public void writeFreeBoard(FreeBoard freeBoard){
this.freeBoards.add(freeBoard);
if(freeBoard.getMember() != this){
freeBoard.setMember(this);
}
}
}
////////////////////////////////////////
@Entity
public class BarberShop {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "BARBER_SHOP_ID")
private long id;
private String name;
}
- 회원이 방문한 바버샵의 정보를 조회할 경우는 많기 때문에 회원 -> 바버샵의 관계는 만들었지만, 바버샵에서 자신을 방문한 고객을 확인하는 경우는 많이 없을 것이기에 바버샵 -> 회원의 관계는 만들지 않았다.
- 이제, VisitedBarberShop엔티티를 만들어야한다.
@Entity
@IdClass(VisitedBarberShopId.class)
public class VisitedBarberShop {
@Id
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@Id
@ManyToOne
@JoinColumn(name = "BARBER_SHOP_ID")
private BarberShop barberShop;
private int rate;
}
////////////////////////////////////////
public class VisitedBarberShopId implements Serializable {
private String member;
private String barberShop;
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
}
- 방문한 샵은 기본 키를 매핑하는 @Id와 외래 키를 매핑하는 @JoinColumn을 동시에 사용해서 기본 키 + 외래 키를 한 번에 매핑했다.
- @IdClass를 사용해 복합 기본 키를 매핑했다.
- 방문한 바버샵 테이블은 멤버의 id와 샵의 id를 합쳐서 기본 키로 사용한다.
- 이렇게 부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계라 한다.
- JPA에서 복합 기본 키를 사용하려면 별도의 식별자 클래스를 만들고 엔티티에 @IdClass를 사용해 식별자 클래스로 지정할 수 있다.
- 식별자 클래스는 다음과 같은 특징을 가진다.
- 복합 키는 별도의 식별자 클래스로 만들어야 한다.
- 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
- 예를 들어 식별자 클래스에서 id1, id2라는 변수명을 사용했다면, 해당 식별자 클래스를 바탕으로 사용하는 클래스에서도 id1, id2라는 변수명을 사용해야 한다.
- Serializable을 구현해야 한다.
- equals와 hashCode 메서드를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다.
- @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.
- 그러나 이렇게 복합 키를 기본 키로 사용하는 것은 키 값이 많아질수록 처리해야 할 일들도 많아지며 일일이 equals와 hashCode를 구현해야 하는 단점이 있다.
- 다대다 새로운 기본 키 사용
- 대신 데이터베이스에서 자동으로 생성해주는 대리 키를 long값으로 사용할 수 있다.
- 이 방법은 간편하고 거의 영구히 쓸 수 있으며 비즈니스에 의존하지 않고 ORM 매핑 시 복합 키를 만들지 않아도 되므로 간단히 매핑할 수 있다.
- 대신 VisitedBarberShop같은 이름이 아니라 의미 있는 이름을 붙일 수 있으면 좋겠다. 이번 예시에서는 샵을 방문한 고객이 해당 샵에 별점을 매긴다. 여기에 더해 별점의 이유와 소감을 적으면 Review라는 도메인으로 사용할 수 있을 것이다.
@Entity
public class Review {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "REVIEW_ID")
private long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name = "BARBER_SHOP_ID")
private BarberShop barberShop;
private int rate;
private String content;
}
- 이렇게 대리 키를 사용하면 복합 키를 사용하는 것보다 매핑이 단순하고 이해하기 쉽다.
- 다대다 연관관계 정리
- 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야 한다.
- 식별관계 : 받아온 식별자를 기본 키 + 외래 키로 사용한다.
- 비식별관계 : 받아온 식별자는 외래 키로만 사용하고 새로운 식별자를 추가한다.
- 객체입장에서 보면 비식별 관계를 사용하는 것이 복합 키를 위한 식별자 클래스를 만들지 않아도 되므로 단순하고 편리하게 ORM매핑을 할 수 있다. 이런 이유로 비식별 관계를 추천한다.
- 그러나 비식별 관계를 사용한다는 것은 해당 테이블이 새로운 개념을 지닌 하나의 테이블이라는 것을 보여주므로 명확한 개념 설정을 통해 이름을 잘 지어주자.
반응형
'Programming > JPA 프로그래밍' 카테고리의 다른 글
08. 프록시와 연관관계 관리 (1) | 2023.12.05 |
---|---|
07. 고급 매핑 (0) | 2023.11.13 |
05. 연관관계 매핑 기초 (0) | 2023.10.12 |
03. 영속성 관리 (0) | 2023.08.18 |
01. JPA 소개 (0) | 2023.08.15 |
댓글