1. 연관관계 매핑시 고려사항 3가지
다중성
- 다대일: @ManyToOne
- 일대다: @OneToMany
- 일대일: @OneToOne
- 다대다: @ManyToMany (실무 사용x)
단방향, 양방향
테이블
- 외래 키 하나로 양쪽 조인 가능
- 사실 방향이라는 개념이 없음
객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향 (참고로 양방향은 단방향 두개라고 생각하면 된다.)
연관관계의 주인
테이블은 외래 키 하나로 두 테이블이 연관관계를 맺고 객체 양방향 관계는 A→B, B→A 처럼 참조가 2군데이다.
객체 양방향 관계는 참조가 2군데 있으므로, 둘중 테이블의 외래 키 를 관리할 곳, 즉 연관관계의 주인을 지정해야한다.
- 연관관계의 주인: 외래 키를 관리하는 참조
- 주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능
2. 다대일 [N:1]
다대일 단방향
Member와 Team, N : 1
DB설계상 N : 1 이므로, Member에 외래키가 있어야한다. (항상 DB는 N쪽에 외래키가 있는 것이 맞다.)
객체 설계시 외래키가 있는 곳을 기준으로 매핑을 걸면 이것이 다대일 단방향 매핑이다.
다대일 양방향
다대일 단방향에서 반대쪽에 연관관계 객체를 추가해주면 된다. 하지만 반대쪽에서 추가한다고해서 DB 테이블에 영향을 주지 않는다. Member.Team(외래키 있는 곳)이 연관관계의 주인이기 때문에 반대쪽 Team.mebers는 읽기만 가능하다.
3. 일대다 [1:N]
🙅♂️ 이 모델은 실무에서 권장하지 않는다. 다대일 매핑을 사용하자.
일대다 단방향 (권장 x)
다대일 단방향의 반대이다.
테이블 일대다 단방향은 항상 다(N)쪽에 외래 키가 있는데, 일대다 단방향 매핑은 1 : N 에서 외래키가 없는 쪽, 일(1)이 연관관계의 주인(Team.memebers)이 된다.
객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조이다. 일대다 단반향 매핑을 하려면 @JoinColumn을 꼭 사용해야 한다. 그렇지 않으면 조인 테이블 방식을 사용하여 자동으로 중간에 테이블을 하나 추가한다.
예제) 아래의 코드를 실행하면 쿼리가 어떻게 실행될까?
// 일대다 매핑 테스트
Member member = new Member();
member.setName("member1");
em.persist(member);
Team team = new Team();
team.setName("teamA");
team.getMembers().add(member);
em.persist(team);
답은 INSERT 2번과 UPDATE 1번 실행된다. 왜냐하면 Team객체에서 members를 등록했지만 DB입장에서는 외래키는 MEMBER테이블에 있기 때문에 MEMBER테이블 UPDATE를 한 번 해줘야 한다.
☞ 그래서 일대다 매핑은 관리하는 외래키가 다른 테이블에 있어서 연관관계 관리 추가로 UPDATE 쿼리를 추가로 실행하므로 다대일 매핑보다 연산 성능이 낮을 수 밖에 없다. (외래키 - 연관관계 주인 위치 차이에서 발생)
☞ 일대다 매핑보다는 다대일 매핑을 사용하자!
일대다 양방향 (권장 x)
이런 매핑은 공식적으로 존재하지 않는다.
@ManyToOne
@JoinColumn(insertable = false, updatable = false)
private Team team;
읽기 전용 필드를 사용해서 양방향처럼 사용하는 방법이다. 연관관계 관리는 Team.members로 하고 Member.Team은 읽기만 한다.
결론
다대일 양방향을 사용하자
4. 일대일 [1:1]
일대일 매핑은 주 테이블이나 대상 테이블 중에 외래키 선택이 가능하다. (대칭)
1) 주 테이블에 외래 키 넣기
일대일 단방향
어노테이션(@OneToOne)만 다르지 다대일(@ManyToOne) 단방향 매핑과 유사하다.
일대일 양방향
다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인이고, 반대편은 mappedBy 적용한다
2) 대상 테이블에 외래 키 넣기
일대일 단방향
🙅♂️ 대상 테이블에 외래키를 넣는 일대일 단방향 매핑은 JPA지원도 안되고 방법도 없다.
일대일 양방향
일대일 양방향 관계는 지원한다. 사실 일대일 주 테이블 외래키 양방향 매핑과 외래키 위치만 다르지 방법은 같다.
- LOCKER.MEMEBER_ID(FK) --- Locker.member(연관관계 주인) <-> Member.locker(읽기 전용)
일대일 정리
주 테이블 외래 키 | 대상 테이블 외래 키 | |
특징 | - 주 객체가 대상 객체의 참조를 가지는 것 처럼
주 테이블에 외래 키를 두고 대상 테이블을 찾음 - 객체지향 개발자 선호 - JPA 매핑 편리 |
- 대상 테이블에 외래 키가 존재 - 전통적인 데이터베이스 개발자 선호 |
장점 | 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능 | 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지 |
단점 | 값이 없으면 외래 키에 null 허용 | 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨(프록시는 나중에 설명) |
그래서 백엔드 개발자 입장으로 주 테이블에 외래 키를 넣는 방법을 선호한다. (하지만 DB설계분들은 대상 테이블 외래키를 선호할 수 있어서 협의 중요)
만약 멤버가 여러 개의 락커를 가질 수 있다고 요구사항이 변경된 경우
- 대상 테이블 외래키를 사용한 경우 Locker 테이블에 있는 Memeber_id의 유니크 제약조건만 빼주면 된다.
- 주 테이블 외래키를 사용한 경우 Member 테이블의 Locker_id를 지우고 Locker 테이블에 새로운 컬럼을 추가해야 되어서 변경사항이 많다.
- 그런데 비즈니스 적으로 또 한 개의 락커가 여러 멤버를 가질 수 있다고 하면 현 상황이 맞는 방법인 것이다.
- 개발자 입장에서는 Member에 fk가 있는 것이 좋다. "해당 멤버가 락커를 갖고있냐 안갖고있냐"를 물어봤을 때 멤버만 조회하면 되고 굳이 Locker를 조인할 필요가 없기 때문이다.
5. 다대다 [N:M]
관계형 DB는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.
객체는 컬렉션을 사용해서 객체 2개로 다대다 관계(@ManyToMany)가 가능하다. @JoinTable로 연결 테이블을 지정한다. 다대다 매핑은 단방향과 양방향이 가능하다.
위의 DB테이블을 보고 Member에 product를 @ManyToMany로 관계를 맺고 JoinTable로 연결테이블 이름을 설정해주면 다대다 매핑이 된다.
// Member Entity
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();
// Product Entity
@ManyToMany(mappedBy = "products")
private List<Member> members = new ArrayList<>();
🙅♂️ 그런데 다대다 매핑은 편리해보이지만 실무에서 사용하지 않는다.
연결 테이블이 단순히 연결만하고 끝나지 않고 주문시간, 수량같은 추가적인 데이터가 들어올 수 있다. 쿼리도 연결테이블 Member_Product이 숨겨져있기 때문에 이상하게 발생할 수도 있다.
그래서 이와같은 한계를 극복하기 위해서는 연결테이블을 엔티티로 승격해줘야 한다.
@ManyToMany → @OneToMany, @ManyToOne
@Entity
public class Member {
//...
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
@Entity
public class MemberProduct {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
}
@Entity
public class Product {
//...
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
이와같이 설계하면 중간 테이블에 원하는 변수를 마음대로 넣을 수 있다.
@Entity
public class MemberProduct {
//...
private int count;
private int price;
private LocalDateTime orderDateTime;
}
+ 배민 개발팀장으로 계시는 김영한 개발자님은 실무에서는 복합키 매핑보다는 중간 테이블의 PK(MemberProduct.id)는 의미없는 값을 사용하면 유연성이 더 생긴다고 한다.
복합키 매핑은 FK 두개를 묶어서 PK로 설정하면 Composite Key을 해야하는데 조큼 귀찮다. 그런걸 떠나서 PK가 다른 테이블의 복합키로 종속이 되어있으면 시스템적으로 유연하지 않는 느낌을 받았다고 한다.
물론 DB설계에서도 장단점이 있고 복합키 매핑이 좀 더 정석적인 방법인 것 같아서 충분히 공부해보고 선택하자
+ 연관관계 매핑 어노테이션 속성
@JoinColumn
- 외래 키를 매핑할 때 사용
속성 | 설명 | 기본값 |
name | 매핑할 외래 키 이름 | 필드명 + _ + 참조하는 테 이블의 기본 키 컬럼명 |
referencedColumnName | 외래 키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본 키 컬럼명 |
foreignKey(DDL) | 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다. | |
unique
nullable insertable updatable columnDefinition table |
@Column의 속성과 같다. |
@ManyToOne
속성 | 설명 | 기본값 |
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | TRUE |
fetch | 글로벌 페치 전략을 설정한다. | - @ManyToOne=FetchType.EAGER - @OneToMany=FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다. | |
targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거 의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타 입 정보를 알 수 있다. |
@OneToMany
속성 | 설명 | 기본값 |
mappedBy | 연관관계의 주인 필드를 선택한다. | |
fetch | 글로벌 페치 전략을 설정한다. | - @ManyToOne=FetchType.EAGER - @OneToMany=FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다. | |
targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거 의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타 입 정보를 알 수 있다. |
❊ 출처
인프런 - 자바 ORM 표준 JPA 프로그래밍
'Dot Programming > JPA' 카테고리의 다른 글
[JPA] 프록시와 연관관계 관리 (0) | 2021.05.21 |
---|---|
[JPA] 상속관계 매핑(조인, 단일 테이블)과 @MappedSuperclass (0) | 2021.05.20 |
[JPA] 연관관계 매핑 기초 (0) | 2021.05.11 |
[JPA] 엔티티 매핑 (0) | 2021.05.07 |
[JPA] 영속성 컨텍스트란 무엇인가 (0) | 2021.05.04 |