SpringBoot/JPA 기본
[JPA] 다양한 연관관계 매핑
myeongju
2022. 6. 28. 20:50
반응형
연관관계 매핑 시 고려사항
- 다중성
- 다대일 :
@ManyToOne
- 일대다 :
@OneToMany
- 일대일 :
@OneToOne
- 다대다 :
@ManyToMany
- 다대일 :
- 단방향, 양방향
- 테이블
- 외래 키 하나로 양쪽 조인 가능 즉, 방향의 개념이 없다.
- 객체
- 참조용 필드가 있는 쪽으로만 참조 가능 (단방향)
- 한쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향(객체 입장 : 단방향을 2개)
- 테이블
- 연관관계 주인
- 외래 키를 관리하는 곳을 연관관계 주인으로 관리하자. 주인의 반대편은 단순 조회만 가능하도록 설계하는 것이 바람직하다.
다대일 [N:1
]
다대일(N:1
) 단방향
- ERD
- 가장 많이 사용하는 연관관계이다.
- 다대일의 반대 → 일대다
다대일 양방향
- ERD
- 외래 키가 있는 쪽이 연관관계의 주인이다.
- 양쪽을 서로 참조하도록 개발한다.
- 단방향만으로 설계를 마친 후 별도의 비즈니스 로직이 필요하거나 하인 쪽에서 자주 조회할 일이 생길 때 양방향 연관관계를 주로 사용한다.
- 연관관계가 주인이 아닌 쪽은 단순 조회만 가능하기에 필드만 추가해주면 된다.
@OneToMany
private List<Member> members = new ArrayList<>();
일대다 [1:N
]
- 해당 모델은 권장하지 않지만, 스펙 상 존재하기 때문에 간단하게 정리하자.
- 일(1. 외래키가 없는 쪽)이 연관관계의 주인이다. → 실무에서도 거의 사용되지 않음.
일대다(1:N
) 단방향
- 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있다.
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
⁉️ 권장하지 않는 이유
- 테이블에서는 항상 다(N) 쪽에 외래키가 있기 때문에 패러다임 충돌이 있다.
@JoinColumn
을 꼭 사용해야 한다. 그렇지 않으면 조인 테이블 방식을 사용한다(중간에 테이블을 하나 추가함)
→ 실무에선 테이블이 수십개 이상 운영이 되는데, 관리 및 트레이싱이 어렵다.
→ Ex) 일대다(1:N
)에서 저장 될 때 양 쪽 객체를 저장한 뒤 update 쿼리를 통해 외래 키 설정( 3번이나 수행)
결론: 기본은 다대일(N:1
)로 구현하다 필요에 의해 양방향 다대일(N:1
) 관계를 수립하도록 하자.
일대다(1:N
)양방향
- 이런 매핑은 공식적으로는 존재하지 않는다.
@JoinColumn(insertable=false, updatable=false)
/* 팀(Team) */
public class Team{
...
@OneToMany
@JoinColumn(name="TEAM_ID")
private List<Member> members = new ArrayList<>();
...
}
/* 멤버(Member) */
public class Member{
...
@ManyToOne
@JoinColumn(name="TEAM_ID", insertable=false, updatable=false)
private Team team;
...
}
- 읽기 전용 필드를 사용해서 양방향처럼 사용하는 방법 → Member Entity의 team field가 읽기 전용 field가 됐다.
- 다대일 양방향을 사용하자.
일대일(1:1
)
- 주 테이블이나 대상 테이블 중에 외래 키 선택 가능 (주 테이블 외래 키를 선택 권장)
- 외래 키에 데이터베이스 유니크 제약조건 추가된 것과 동일하다.
일대일 단방향
회원 엔티티
package hellojpa;
import javax.persistence.*;
import java.util.concurrent.locks.Lock;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(insertable = false, updatable = false)
private Team team;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
/* @ManyToOne // 해당 클래스의 관점에서 지정
@JoinColumn(name = "TEAM_ID")
private Team team;*/
}
- 다대일(
@ManyToOne
) 단방향 매핑과 매우 유사하며 애노테이션만@OneToOne
을 사용한다.
❓대상 테이블에 외래 키 단방향 즉, 일대다처럼 구현해야 될 때는 어떻게 될까?
- 대상 테이블에 외래 키가 있는 경우 단방향 관계는 JPA에서 지원하지 않는다.
- 양방향은 주 테이블의 양방향과 매핑 방법이 같으므로 지원한다.
일대일 양방향
- 다대일 연관관계와 동일하게 외래 키가 있는 곳이 연관관계의 주인.
- 연관관계의 주인이 아닌 곳에 mappedBy를 넣어준다.
정리
- 주 테이블에 외래 키
- 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
- 객체지향 개발자 선호
- JPA 매핑 편리
- 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
- 단점: 값이 없으면 외래 키에 null 허용
- 대상 테이블의 외래 키
- 대상 테이블에 외래 키가 존재
- 전통적인 데이터베이스 개발자 선호
- 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
- 단점: 프록시 기능의 한계로, 지연 로딩으로 설정해도 항상 즉시 로딩된다. JPA는 프록시를 사용할 때 데이터에 값이 있는지 없는지 여부를 알아야 한다. 하지만, 대상 테이블에 외래 키가 있으면 대상 테이블을 찾아서 조회해서 동작하게 된다. 즉, 어차피 쿼리를 사용해야 되기 때문에 지연 로딩을 사용하지 않게 된다.
다대다(N:M
)
- 실무에서 거의 사용하지도 않고 추천하지도 않는 연관관계
- 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 가능
@ManyToMany
사용@JoinTable
로 연결 테이블 지정
편리해 보이지만 실무에서 쓰지 않는 이유가 있다. 그 이유는 연결 테이블이 단순히 연결만 하고 끝나지 않는다는 것이다.
예를 들어서 주문시간, 수량과 같은 데이터가 추가되면 다대다 관계 사이에 만들어진 테이블에 넣을 수가 없다. 쿼리 자체도 중간 테이블이 숨겨져 있기 때문에 보기가 어렵다.
다대다 관계는 일대다, 다대일로 풀어내자.
일대다, 다대일로 풀어내기
- 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
- Ex: Order와 Item 사이에 OrderItem 연결 테이블을 엔티티로 추가
@ManyToMany
→@OneToMany
,@ManyToOne
예제
지난 포스팅에 이어 예제를 진행해보자. 다대다 관계는 사용하지 않지만, 예제를 통해 확인해보자.
배송, 카테고리 엔티티 추가
- 주문과 배송은 일대일 관계, 상품과 카테고리는 다대다 관계이다.
ERD
엔티티 상세
카테고리 엔티티
package jpabook.jpashop.domain;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Category {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Category parent;
@OneToMany(mappedBy = "parent") // 양방향 관계를 셀프로 설정
private List<Category> child = new ArrayList<>();
@ManyToMany
@JoinTable(name = "CATRGORY_ITEM",
joinColumns = @JoinColumn(name = "CATEGORY_ID"),
inverseJoinColumns = @JoinColumn(name = "ITEM_ID")
)
private List<Item> items = new ArrayList<>();
}
상품 엔티티
package jpabook.jpashop.domain;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();
}
배송 엔티티
package jpabook.jpashop.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
@Entity
public class Delivery {
@Id @GeneratedValue
private Long id;
private String city;
private String street;
private String zipcode;
private DeliveryStatus status;
@OneToOne(mappedBy = "delivery")
private Order order;
}
이 글은 김영한 님의 "자바 ORM 표준 JPA 프로그래밍 - 기본 편" 강의를 듣고 정리한 내용입니다.
반응형