1. 상속관계 매핑 전략
엔티티 계층화와 테이블 생성 전략
상속관계 매핑은 아래 그림과 같이 두 엔티티 사이에 공통적인 속성이 있는 경우 엔티티를 상속관계로 만들어서 공통화하고, 독립적인 속성만 각 엔티티의 속성으로 두는 경우에 필요하다.
원래는 테이블의 스키마를 기준으로 엔티티를 생성하지만, 만약 이러한 계층화된 엔티티를 기준으로 테이블을 생성한다면, 3가지의 생성 전략을 적용할 수 있다.
1) 단일 테이블 전략(Single Table) : 1개 테이블에 모든 필드값 넣기
2) 클래스별 테이블 전략(Table per class) : 위 그림의 왼쪽과 같이 중복되는 항목 무시하기
3) 조인 테이블 전략(Joined) : 위 그림의 오른쪽과 같이 중복되는 항목만 따로 테이블화하기
2. 단일 테이블 전략
JPA의 기본 전략은 단일 테이블 전략이다.
JPA에서의 기본 전략은 1)과 같이 1개의 테이블에 모든 필드값을 넣는 방식으로 작동한다. 만약 아래와 같이 Item 객체를 상속받는 Album, Movie, Book의 엔티티를 만들고 JpaMain 파일을 실행하면 모든 속성값이 Item 테이블에 포함되어 생성되는 것을 확인할 수 있다.
src/main/java/hellojpa/Item.java
1
2
3
4
5
6
7
8
9
10
11
|
@Entity
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
|
cs |
src/main/java/hellojpa/Book.java
1
2
3
4
5
6
7
|
@Entity
public class Book extends Item {
private String author;
private String isbn;
}
|
cs |
... 이하 Movie, Album 객체 생략
부모 Entity에 abstract 적용
부모 엔티티가 되는 Item 엔티티는 독단적으로 사용되는 경우는 없다고 가정하여 abstract를 적용한다. 반드시 자식 엔티티인 Book, Movie, Album 등에서 Item 엔티티를 extends 받는 구조로 만든다. 만약 자식 엔티티에서 Item 엔티티를 상속받지 않으면, @Entity 어노테이션이 적용된 상태이기 때문에 Id값을 따로 넣어주어야만 한다. 그리고 abstract가 적용된 Item 엔티티는 아래 사진과 같이 독립된 엔티티로 생성할 수 없다.
실행 코드
Item 엔티티를 상속받은 Movie 객체만 불러와서 업데이트 해줬다. 아래 실행 결과에서 보면, Item 테이블의 필드값 중 Movie와 관련된 값들만 입력되고 나머지는 null처리 되는 것을 확인할 수 있다.
src/main/java/hellojpa/JpaMain2.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public class JpaMain2 {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Movie movie = new Movie();
movie.setDirector("aaa");
movie.setActor("bbbb");
movie.setName("오징어 게임");
movie.setPrice(10000);
em.persist(movie);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
|
cs |
실행 결과
모든 필드값을 갖는 Item 단일 테이블이 생성된다.
업데이트한 필드 값들은 입력되고, 나머지 값들은 null처리 된 것을 볼 수 있다.
DType(Discrimination Type)
그런데 위 경우처럼 단일 테이블 전략을 적용하여 Item 단일 테이블만 생성된 경우, 맨 앞에 DTYPE 컬럼이 생성된 것을 볼 수 있다! 이것은 다음과 같은 이유 때문에 생성되는 것이다. 만약 Book 객체를 생성하고, Movie, Book, Album에서 공통으로 갖고 있는 name값만 넣었다고 가정해보자. 그럼 ITEM 단일 테이블에 입력된 값이 과연 어떤 객체를 통해 입력된 것인지 알 수가 없다. 그래서 단일 테이블 전략에서는 강제로 DTYPE 값이 입력된다.
DTYPE 값을 적용할 수 있는 어노테이션과 방법은 아래 다른 전략에서 살펴본다.
3. 조인 테이블 전략
@Inheritance
조인 테이블 전략은 흔히 사용되는 전략이다. 이 전략대로 테이블을 구성하면 중복을 제거하고 짜임새 있는 테이블을 만들 수 있다. 적용 방법은 부모가 되는 객체 클래스에 다음과 같이 @Inheritance를 적용해주면 된다.
src/main/java/hellojpa/Item.java
1
2
3
4
5
6
7
8
9
10
11
12
|
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
|
cs |
실행 결과
각 엔티티마다 테이블이 생성되고, join을 위한 id값이 FK로 적용된 것을 확인할 수 있다.
......
DType : @DiscriminatorColumn
@Inheritance를 통해 테이블 생성 전략을 조인 전략으로 변경해주면 단일 테이블 전략과는 다르게 DType이 자동으로 생성되지 않는다. 조인 전략에서는 엔티티마다 테이블을 가지게 되므로 단일 테이블 전략에서 발생했던 문제가 발생하지 않기 때문이다. 만약 Movie 엔티티에 Item 엔티티에서 받아온 name, price만 삽입한다고 하더라도, FK에 의해 연결되어 있으므로 어떤 엔티티에서 Item 엔티티의 내용을 넣어줬는지 확인할 수 있다.
@DiscriminatorColumn
그러나, 굳이 Join을 통해 확인을 해야하는 번거로움이 발생하므로 기본적으로는 조인 전략도 부모 테이블에 DType을 넣어주는 것이 편리하다. 이를 위해 Item 엔티티에 @DiscriminatorColumn 어노테이션을 넣어주면 된다.
@DiscriminatorColumn(name = "DISC_TYPE")
DType의 컬럼명(DTYPE)은 엔티티명을 기본으로하여 삽입된다. 만약 변경하고 싶다면 name 속성을 지정해주면 된다.
@DiscriminatorValue(value = "M")
DType의 Value값은 자식 엔티티의 이름을 기본으로하여 삽입된다. 변경이 필요하면 @DiscriminatorValue로 지정해주면 된다.
4. 클래스별 테이블 전략
클래스별 테이블 전략은 공통 속성을 갖는 부모 엔티티인 Item 엔티티에 해당하는 테이블을 생성하지 않고, name, price 등 공통 속성들을 Album, Movie, Book 등 각각의 테이블에 중복되게 넣는 방식이다.
클래스별 테이블 전략은 사용하지 말자
각 테이블별로 필요한 속성값들을 모두 명명하게 되므로 깔끔한 방식이라고 볼 수도 있다. 그러나 이러면 Item 엔티티를 상속받는 의미가 사라진다. 각 자식 테이블에 Item 테이블의 id값이 unique하게 삽입된다는 것 외에는 구조적 장점이 사라진다.
더 큰 문제는, 삽입된 데이터를 조회할 때 부모 엔티티 타입으로 조회하면 해당 데이터를 찾기 위해서 모든 테이블을 union한다. 아래 예제를 살펴보자.
src/main/java/hellojpa/JpaMain2.java 일부
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
try {
Movie movie = new Movie();
movie.setName("이터널 선샤인");
movie.setPrice(1000000);
em.persist(movie);
em.flush();
em.clear();
Item item = em.find(Item.class, movie.getId());
System.out.println("item = " + item);
tx.commit();
} catch (Exception e) {
|
cs |
비즈니스 로직상 Item 엔티티의 id만 아는 상태로 Item 객체를 조회한다면, 해당 Item이 Album, Book, Movie 중 어떤 테이블에 저장되었는지 모르기 때문에 전부 union한 쿼리를 작성해야하므로 성능에 상당한 부하를 줄 수 있다.
5. 전략별 장단점
각 전략별 장단점을 정리해보자
조인 전략
장점
- 테이블 정규화(저장공간 효율화)
- 외래 키 참조 방식 활용 가능
단점
- 조회 시 조인이 필요하여 성능저하(조회 쿼리가 복잡함)
- 저장 시 INSERT SQL 2번 호출
단일 테이블 전략
장점
- 조인이 필요없어 조회 성능이 좋음
- 조회 쿼리 단순
단점
- 자식 엔티티가 매핑한 컬럼은 모두 nullable
- 테이블의 비정규화(1개 테이블이 너무 커서 조회 성능 저하 위험)
6. @MappedSuperClass
MappedSuperClass는 위에서 다룬 Entity-Table간 상속관계 정의를 하는 내용이 아니라, SuperEntity - SubEntity간 관계를 정의하는 내용이다. 예를 들어 createdBy, updatedBy, createdAt, updatedAt과 같이 모든 엔티티에서 공통으로 사용하는 필드값이 있다면, 이를 MappedSuperClass로 지정하여 모든 엔티티가 공통으로 사용할 수 있도록 해주는 개념이다.
공통 필드값들이 포함된 BaseEntity를 만들고 상속시켜본다.
src/main/java/hellojpa/BaseEntity.java
1
2
3
4
5
6
7
8
9
10
11
|
@Getter
@Setter
@MappedSuperclass
public class BaseEntity {
private Long createdBy;
private Long updatedBy;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
|
cs |
src/main/java/hellojpa/Team.java (중략...)
1
2
3
4
5
|
@Entity
@Getter
@Setter
@DynamicInsert
public class Team extends BaseEntity{
|
cs |
src/main/java/hellojpa/Member.java(중략...)
BaseEntity의 각 필드값 위에 @Column 속성을 적용할 수도 있다.
참조
1. 인프런_자바 ORM 표준 JPA 프로그래밍 - 기본편_김영한 님 강의
https://www.inflearn.com/course/ORM-JPA-Basic/lecture/21735?tab=curriculum
2. mindddi velog - DB모델링 - 관계 (슈퍼-서브타입)
'Programming-[Backend] > JPA' 카테고리의 다른 글
[JPA기본] 9. 프록시 객체 (0) | 2021.10.15 |
---|---|
[JPA기본] 8. 연관, 상속관계 매핑 실습 : 테이블 -> 엔티티 환원 (0) | 2021.10.13 |
[JPA기본] 6. 양방향 연관관계 (0) | 2021.09.28 |
[JPA기본] 5. 기본 키 매핑과 연관관계 매핑 기초 (0) | 2021.09.27 |
[JPA기본] 4. DDL 자동 생성과 엔티티 매핑 어노테이션 (0) | 2021.09.26 |