[자바 ORM 표준 JPA 프로그래밍 정리], 6장 다양한 연관관계 매핑
6. 다양한 연관관계 매핑
들어가기 전에
- 연관관계는 사실상 방향이라는 개념이 존재하지 않는다. 외래키 하나로 양쪽을 조인 가능하다.
- 연관관계의 주인은 항상 '다' 즉 [N] 쪽에 설정해줘야 한다.
- 참조용 필드(mappedBy)는 읽기 전용으로, 오로지 참조만 가능하다.
- 객체에서의 양방향은 A->B, B->A 처럼 참조가 2군데인 것이다.
- 다대일 [N:1]
- 일대다 [1:N]
- 일대일 [1:1]
- 다대다 [N:N]
1. 다대일[N:1]
- JPA에서 가장 많이 사용하고, 꼭 알아야 하는 다중성이다.
- 아래 테이블에서 보면 DB설계상 일대다에서 '다' 쪽에 외래키가 존재해야한다. 그렇지 않으면 잘못된 설계이다.
- 테이블에서는 FK가 팀을 찾기 위해 존해하고, 객체에서 Team 필드도 Team을 참조하기 위해 존재한다.
1-1. 다대일 단방향 매핑
- JPA의 @ManyToOne 어노테이션을 사용해서 다대일 관계를 매핑한다.
- @JoinColumn은 외래키를 매핑할 때 사용한다. name은 매핑할 외래 키의 이름이다.
public class Member{
...
@ManyToOne
@JoinCoulmn(name="TEAM_ID")
private Team team;
}
1-2. 다대일 양방향 매핑
- 다대일 관계에서 단방향 매핑을 진행하고, 양방향 매핑을 진행할때 사용한다.
- 반대쪽에서 일대다 단방향 매핑을 해주면 된다.(객체기준으로, 컬렉션을 추가하자)
- 여기서 중요한건, 반대에서 단방향 매핑을 한다고 해서 DB테이블에 영향을 전혀 주지 않는다.
- 다대일의 관계에서 다 쪽에서 이미 연관관계 주인이 되어서 외래키를 관리하고 있다.
- 반대쪽에서 일대다 단방향 매핑. JPA의 @OneToMany 어노테이션을 사용한다.
- 연관관계의 주인이 아니고, 어디에 매핑 되었는지에 대한 정보를 표시하는 (mappedBy="team") 을 꼭 넣어줘야 한다.
- (mappedBy = "team") 에서 team은 Member에서 외래키로 매핑된 필드명이다.
public class Team {
...
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
...
}
정리
- 외래키가 있는 쪽이 연관관계 주인이다.
- 양쪽을 서로 참조하도록 개발하자.
2. 일대다[1:N]
- 일대다 관계에서는 일이 연관관계의 주인이다.
- 일 쪽에서 외래키를 관리하겠다는 의미가 된다
- 결론 먼저 말하자면, 표준스펙에서 지원은 하지만 실무에서는 사용을 권장하지 않는다.
2-1. 일대다 단방향 매핑
- 팀과 멤버가 일대다 관계이다.
- Team이 Members(컬렉션)을 가지는데, Member의 입장에서는 Team을 참조하지 않아도 된다는 설계이다. '객체'의 입장에서 생각해보면 충분히 나올수 있는 설계이다.
- 그러나 DB 테이블 입장에서 보면 무조건 일대다에서 '다' 쪽에 외래키가 들어간다.
- Team에서 members가 바뀌면, DB의 member 테이블에서 업데이트 쿼리가 나가는 상황이 발생한다.
JPA의 @OneToMany와 @JoinColumn()을 이용해 일대다 단방향 매핑을 설정할 수 있다.
- Member는 코드상 연관관계 매핑이 없고, 팀에서만 일대다 단방향 매핑 설정
@Entity public class Team { ... @OneToMany @JoinColumn(name = "TEAM_ID") private List<Member> members = new ArrayList<>();
Member member = new Member();
member.setUsername("MemberA");
em.persist(member);
System.out.println("-----멤버 저장");
Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
em.persist(team);
System.out.println("-----팀 저장");
tx.commit();
- 이렇게 실행하면 쿼리에서 트랜잭션 커밋 시점에 create one-to-many row로 시작되는 주석과 함께 Member 테이블을 업데이트 하는 쿼리가 나간다.
★일대다 단방향의 정리
- 일대다 단방향은 일대다의 일이 연관관계 주인이다.
- 테이블 일대다 관계는 항상 다(N)쪽에 외래키가 있다.
- 객체와 테이블의 패러다임 차이 때문에 객체의 반대편 테이블의 외래키를 관리하는 특이한 구조다.
- @JoinColumn을 반드시 사용해야 한다. 그렇지 않으면 조인 테이블 방식을 사용한다.
★일대다 단방향 매핑의 문제점과 해결방안
- 일단 업데이트 쿼리가 나간다. 성능상 좋지 않으나 크게 문제되지는 않는다.
- 위 예시로 살펴보면, Team의 엔티티를 수정했는데 Member의 엔티티가 업데이트되는 상황이 발생한다.
- 테이블이 적을때는 크게 문제가 되지 않지만 실무에서는 테이블이 수십개가 엮어서 돌아간다.
- 참고로, 일대다 양방향 매핑은 JPA 공식적으로 존재하지 않는 방법이다.
따라서, 다대일 단방향 매핑을 사용하고, 필요시 양방향 설정을 사용하자.
3. 일대일[1:1]
- 일대일 관계는 그 반대도 일대일이다.
- 일대일 관계는 특이하게 주 테이블이나 대상 테이블 중에 외래 키를 넣을 테이블을 선택 가능하다.
- 주 테이블에 외래키 저장
- 대상 테이블에 외래 키 저장
- 외래키에 데이터베이스 유니크 제약조건이 추가되어야 일대일 관계가 가능하다.
3-1. 일대일 - 주 테이블에 외래키 단방향
- 회원이 딱 하나의 락커를 가지고 있는 상황이다. 반대로 락커도 회원 한명만 할당 받을 수 있다. 이때, 둘의 관계는 일대일 관계이다.
- 이 경우 멤버를 주 테이블로 보고 주 테이블 또는 대상 테이블에 외래키를 지정할 수 있다.
- 다대일[N:1] 단방향 관계와 JPA 어노테이션만 달라지고 거의 유사하다.
3-1-2. 일대일- 주 테이블에 외래키 양방향
- 다대일[N:1] 양방향 매핑처럼 외래키가 있는곳이 연관관계의 주인이다.
- JPA @OneToOne 어노테이션으로 일대일 단방향 관계를 매핑하고, @JoinColumn을 넣어준다.
- 여기까지만 매핑하면 단방향 관계이고,
@Entity public class Member { ... @OneToOne @JoinColumn(name = "locker_id") private Locker locker; ... }
- 반대편에 mappedBy를 적용시켜주면 일대일 양방향 관계 매핑이 된다.
@Entity public class Locker { ... @OneToOne(mappedBy = "locker") private Member member; }
- 마찬가지로, mappedBy는 읽기 전용으로 참조만 가능하다.
3-2-1. 일대일 - 대상 테이블에 외래키 단방향
- 일대일 관계에서 대상 테이블에 외래키를 단방향으로 저장하는 방식은 지원하지 않는다.
3-2-2. 일대일 - 대상 테이블에 외래키 양방향
- 일대일 주 테이블에 외래 키 양방향 매핑을 반대로 뒤집었다고 생각하면 된다. 매핑 방법은 같다.
- 주 테이블은 멤버 테이블이지만, 외래 키를 대상 테이블에서 관리하고 주 테이블의 락커 필드는 읽기 전용이 된다.
일대일 정리
- 주 테이블에 외래키
- (주 테이블은 많이 접근하는 테이블로 설정하자.)
- 주 테이블에 외래키를 두고 대상 테이블을 찾는 방식
- 객체지향 개발자들이 선호하고, JPA 매핑이 편리하다
- 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능하다.
- 그러나, 값이 없으면(예를들어, member가 locker를 사용하지 않음) NULL을 허용해야한다.
- 대상 테이블에 외래키
- 대상 테이블에 외래 키가 존재한다.
- 전통적인 데이터베이스 개발자들이 선호하는 방식이다. NULL을 허용해야하는 문제도 없다.
- 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조를 유지할 수 있
- 코드상에서는 주로 멤버 엔티티에서 락커를 많이 엑세스 하는데, 어쩔 수 없이 양방향 매핑을 해야한다.
- 일대일 - 대상 테이블에 외래 키 단방향 매핑을 JPA에서 지원하지 않으므로, 단방향 매핑만 해서는 멤버 객체를 업데이트 했을 때 락커 테이블에 FK를 업데이트 할 방법이 없다. 따라서 양방향 매핑을 해야 한다.
### 4. 다대다[N:N]
- 결론부터 말하자면, 다대다는 실무에서 사용하지 말아야 한다.
- 다대다 (@ManyToMany)는 각각 일대다, 다대일 관계로 풀어서 사용하자.
- 연결 테이블용 엔티티를 추가한다. 연결 테이블을 엔티티로 승격시킨다.
- JPA가 만들어준 숨겨진 매핑테이블의 존재를 바깥으로 꺼낸다고 생각하자.
위 그림과 같은 다대다 관계를 아래처럼 풀어낼 수 있다.
- Member 엔티티에서 @OneToMany 관계로 변경한다.
```java
@Entity
public class Member {
...
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
...
}
```
- Product도 마찬가지로 @OneToMany 관계로 변경한다.
```java
@Entity
public class Product {
...
@OneToMany(mappedBy = "product")
private List<MemberProduct> members = new ArrayList<>();
...
}
```
- MemberProduct
- 연결 테이블을 엔티티르 승격시킨 테이블이다. 그리고 @ManyToOne 매핑을 두개 한다.
- 여기서 추가 데이터가 들어간다면 아예 의미있는 엔티티 이름으로 변경 될 것이다.
```java
@Entity
@Getter
@Setter
public class MemberProduct {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
}
```
6.4.3 다대다: 매핑의 한계와 극복, 연결 엔티티 사용
@ManyToMany를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편하다. 하지만 이 매핑을 실무에서 사용하기에는 한계가 있다. 예를 들어 회원이 상품을 주문하면 연결 테이블에 단순히 주문한 회원 아이디와 상품아이디만 담고 끝나지 않는다. 보통은 연결 테이블에 주문 수량 컬럼이나 주문한 날짜 같은 컬럼이 더 필요하다.
연결 테이블에 주문 수량과 주문 날짜 컬럼을 추가하면 더는 @ManyToMany를 사용할 수 없다. 왜냐하면 주문 엔티티나 상품 엔티티에는 추가한 컬럼들을 매핑할 수 없기 때문이다.
결국 연결 엔티티를 만들고 이곳에 추가한 컬럼들을 매핑해야 한다. 그리고 엔티티 간의 관계도 테이블 관계처럼 다대다에서 일대다, 다대일 관계로 풀어야 한다.
복합 기본 키
JPA에서 복합 기본 키를 사용하려면 별도의 식별자 클래스를 만들어야 한다. 그리고 엔티티에 @IdClass를 사용해서 식별자 클래스를 지정하면 된다.
- 복합 키는 별도의 식별자 클래스로 만들어야 한다.
- Serializable을 구현해야 한다.
- equals와 hashCode 메소드를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다.
- @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다
식별 관계
회원상품은 회원과 상품의 기본 키를 받아서 자신의 기본 키로 사용한다. 이렇게 부모 테이블의 기본 키를 받아서 자신의 기본 키
- 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계라 한다.
복합 키를 사용하는 방법은 복잡하다. 단순히 컬럼 하나만 기본 키로 사용하는 것과 비교해서 복합 키를 사용하면 ORM 매핑에서 처리할 일이 상당히 많아진다. 복합 키를 위한 식별자 클래스도 만들어야 하고 @IdClass 또는 @EmbeddedId도 사용해야 한다. 그리고 식별자 클래스에 equals, hashCode도 구현해야 한다. 복합 키를 사용하지 않고 간단히 다대다 관계를 구성하는 방법을 알아보자.
6.4.4 다대다 : 새로운 기본 키 사용
추천하는 기본 키 생성 전략은 데이터베이스에서 자동으로 생성해주는 대리 키를 Long 값으로 사용하는 것이다.
이것의 장점은 간편하고 거의 영구히 쓸 수 있으며 비즈니스에 의존하지 않는다. 그리고 ORM 매핑 시에 복합키를 만들지 않아도 되므로 간단히 매핑을 완성할 수 있다.
6.4.5 다대다 연관관계 정리
다대다 관계를 일대다 다대일 관계로 풀어내기 위해 연결 테이블을 만들 떄 식별자를 어떻게 구성할지 선택해야 한다.
- 식별 관계 : 받아온 식별자를 기본 키 + 외래 키로 사용한다.
- 비식별 관계 : 받아온 식별자는 외래 키로만 사용하고 새로운 식별자를 추가한다.
데이터베이스 설계에서는 1번처럼 부모 테이블의 기본 키를 받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 것을 식별 관계라 하고, 2번 처럼 단순히 외래 키로만 사용하는 것을 비식별 관계라 한다. 객체 입장에서 보면 2번처럼 비식별 관계를 사용하는 것이 복합 키를 위한 식별자 클래스를 만들지 않아도 되므로 단순하고 편리하게 ORM 매핑을 할 수 있다. 이런 이유로 식별 관계보다는 비식별 관계를 추천한다.
참고 문헌 :