JPA는 다음과 같은 상속관계에서 세 가지의 상속관계 매핑을 지원한다.
- InheritanceType.JOINED: 조인 전략
- InheritanceType.SINGLE_TABLE: 단일 테이블 전략 <-- Default 전략
- InheritanceType.TABLE_PER_CLASS: 구현 클래스마다 테이블 생성 전략
1. 조인 전략
Item 클래스에 @Inheritance(strategy = InheritanceType.JOINED)으로 설정하면 아래와 같이 테이블이 설계된다. ALBUM, MOVIE, BOOK이 ITEM_ID를 PK, FK로 사용하고 있다.
DTYPE은 뭘까?
DTYPE은 조인되는 테이블의 이름이다. 즉 ALBUM을 저장하는 경우에는 DTYPE이 ALBUM이 되고 MOVIE를 저장하는 경우에는 DTYPE이 MOVIE가 된다.
DTYPE이 꼭 필요할까?
DTYPE이 없으면 ITEM만 조회했을 때 어떤 테이블과 연관이 있는 데이터인지 알 수가 없으므로 DB 데이터 작업 시 어려움이 있을 수도 있다.
DTYPE 컬럼명 변경
DTYPE말고 D_TYPE이라는 컬럼명을 쓰고 싶으면 아래 어노테이션을 적용하면 된다.
@DiscriminatorColumn(name = "D_TYPE")
JOIN되는 테이블의 DTYPE 변경
ALBUM을 저장할 때 ITEM의 DTYPE은 기본적으로 엔티티의 클래스명인 "ALBUM"으로 설정된다. 이를 변경하고 싶을 때 자식 클래스에 아래 어노테이션을 적용하면 된다.
@DiscriminatorValue("원하는 이름 설정")
조인 전략은 엔티티를 저장할 때 insert가 상속 계층 깊이 만큼 수행된다.
예를 들어서 ALBUM을 저장하면 ITEM을 먼저 저장하고 ITEM의 ID 값을 바탕으로 ALBUM이 저장된다. 위에 예시는 계층이 두 개가 끝이어서 더 깊은 상속 계층 구조로 테스트를 진행해보았다.
@Entity
@Getter
@Setter
@Inheritance(strategy = InheritanceType.JOINED)
public class Item1 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
String item1Name;
}
// ================================================================================
@Entity
@Getter
@Setter
public class Item2 extends Item1 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
String item2Name;
}
// ================================================================================
@Entity
@Getter
@Setter
public class Item3 extends Item2 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
String item3Name;
}
// ================================================================================
@Test
void test() {
Item3 item3 = new Item3();
item3.setItem1Name("name1");
item3.setItem2Name("name2");
item3.setItem3Name("name3");
em.persist(item3);
}
// ================================================================================
log
Hibernate:
insert
into
item1
(id, item1name)
values
(default, ?)
Hibernate:
insert
into
item2
(item2name, id)
values
(?, ?)
Hibernate:
insert
into
item3
(item3name, id)
values
(?, ?)
insert가 총 세 번 수행되는 것을 알 수 있다. 그런데 item2와 item3의 상속 매핑 전략은 다르게 설정할 수 있는지도 궁금해졌다. 따라서 item2의 클래스에 새로운 전략을 설정했다. SINGLE_TABLE 전략은 Item2에 Item3의 내용까지 모두 합쳐서 단일 테이블로 맵핑하는 JPA의 Default 맵핑 전략이다.
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
그러나 테스트 결과 SINGLE_TABLE 전략은 무시되었다. 따라서 이러한 방식은 지원하지 않는 것으로 보인다.
조인 전략은 엔티티를 찾을 때 inner join을 수행한다.
각 상속 관계는 무조건 null이 아니기 때문에 inner join을 수행해도 문제가 없다. 따라서 내부적으로 inner join으로 조회하는 것을 볼 수 있다.
@Test
void test() {
Item3 item3 = new Item3();
item3.setItem1Name("name1");
item3.setItem2Name("name2");
item3.setItem3Name("name3");
em.persist(item3);
em.flush();
em.clear();
item3 = em.find(Item3.class, item3.getId());
System.out.println(item3);
}
// ===========================================================================
log
Hibernate:
select
item3x0_.id as id1_3_0_,
item3x0_2_.item1name as item2_3_0_,
item3x0_1_.item2name as item1_4_0_,
item3x0_.item3name as item1_5_0_
from
item3 item3x0_
inner join
item2 item3x0_1_
on item3x0_.id=item3x0_1_.id
inner join
item1 item3x0_2_
on item3x0_.id=item3x0_2_.id
where
item3x0_.id=?
조인 전략의 장단점
장점
- 테이블 정규화
- 외래 키 참조 무결성 제약조건을 활용할 수 있다
- 저장공간의 효율화
단점
- 조회 시 조인을 많이 사용, 성능이 저하됨
- 조회 쿼리가 복잡하다
- 데이터 저장 시 INSERT 쿼리가 두 번 호출
- 하지만 이러한 단점은 큰 단점으로 적용되지 않는다. 따라서 조인 전략이 가장 객체 지향적이고 설계도 깔끔하게 이루어지므로 기본 전략으로 가져가는 것이 좋다.
2. 단일 테이블 전략
Item 클래스에 @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 으로 설정하면 아래와 같이 테이블이 설계된다. 또한 JPA의 Default 설정이기 때문에 아무런 설정을 하지 않아도 단일 테이블 전략으로 설계된다.
단일 테이블의 주의할 점은 ALBUM이든 MOVIE든 BOOK이든 모든 데이터가 하나의 테이블에 INSERT 되므로 이 데이터가 어떤 데이터인지 구분해주기 위하여 DTYPE을 필수로 사용하는 것이 좋다.
단일 테이블 전략의 장단점
장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다
- 조회 쿼리가 단순하다
단점
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용한다
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있고 상황에 따라서 조회 성능이 오히려 느려질 수 있다.
3. 클래스 별 테이블 전략
Item 클래스에 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 으로 설정하면 아래와 같이 테이블이 설계된다.
NAME과 PRICE는 ITEM의 필드였는데 ALBUM, MOVIE, BOOK 각 테이블에 포함된 것을 볼 수 있다. 이렇게 클래스 별 테이블 전략을 사용하는 경우에는 보통 ITEM 클래스를 추상 클래스로 구현한다. ITEM 클래스만 따로 테이블이 필요하다면 추상 클래스로 만들지 않으면 된다.
테이블이 깔끔해보이고 좋은데??
맞다. 깔끔하고 좋아보이고 실제로 데이터를 INSERT할 때도 깔끔하다. 하지만 데이터를 찾을 때 문제가 될 수 있다. 예를 들어서 Item 클래스로 ALBUM 데이터를 찾는 상황을 가정해보자. (Item을 상속하고 있기 때문에 당연히 가능하다)
여기서는 Item1 <-- Item2 , Item1 <-- Item3 상속 관계를 예시로 사용했다.
@Entity
@Getter
@Setter
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item1 {
@Id
@GeneratedValue
private Long id;
String item1Name;
}
// ===========================================================================
@Entity
@Getter
@Setter
public class Item2 extends Item1 {
@Id
@GeneratedValue
private Long id;
String item2Name;
}
// ===========================================================================
@Entity
@Getter
@Setter
public class Item3 extends Item1 {
@Id
@GeneratedValue
private Long id;
String item3Name;
}
// ===========================================================================
@Test
void test() {
Item3 item3 = new Item3();
item3.setItem1Name("name1");
item3.setItem3Name("name3");
em.persist(item3);
em.flush();
em.clear();
Item1 item = em.find(Item1.class, item3.getId());
System.out.println(item3);
}
// ===========================================================================
log
Hibernate:
insert
into
item3
(item1name, item3name, id)
values
(?, ?, ?)
Hibernate:
select
item1x0_.id as id1_3_0_,
item1x0_.item1name as item2_3_0_,
item1x0_.item2name as item1_4_0_,
item1x0_.item3name as item1_5_0_,
item1x0_.clazz_ as clazz_0_
from
( select
id,
item1name,
null as item2name,
null as item3name,
0 as clazz_
from
item1
union
all select
id,
item1name,
item2name,
null as item3name,
1 as clazz_
from
item2
union
all select
id,
item1name,
null as item2name,
item3name,
2 as clazz_
from
item3
) item1x0_
where
item1x0_.id=?
조회 쿼리를 살펴보면 Item1을 상속하는 모든 테이블을 UNION해서 데이터를 찾는 모습을 볼 수 있다. JPA 입장에서는 Item1으로 데이터를 조회하면 해당 데이터가 실질적으로 어떤 테이블에 속해 있는지 알 수가 없기 때문에 일단 상속 관계에 있는 모든 테이블을 UNION하는 것이다. 따라서 클래스 별 테이블 전략은 조회 시 굉장히 비효율적이며 DBA나 ORM 전문가 모두 추천하지 않는 방식이라고 한다.
클래스 별 테이블 전략의 장단점
장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적이다
- not null 제약조건을 사용할 수 있다
단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다
- 자식 테이블을 통합해서 쿼리하기 어렵다
Ref
'JPA' 카테고리의 다른 글
같은 클래스와 다대일 양방향 관계 맺기 (0) | 2022.09.01 |
---|---|
OneToOne 정리 (0) | 2022.08.29 |
JPA fetch join 대상의 별칭 사용과 주의사항 (0) | 2022.08.16 |
fetch join과 paging 시 OOM 문제 (OneToOne은 괜찮지) (0) | 2022.08.10 |
JPA에서 발생할 수 있는 문제들과 해결방법 정리 (N + 1, 무작위 JOIN..) (0) | 2022.08.09 |