티스토리 뷰
객체는 상속관계가 존재하지만, 관계형 데이터베이스는 상속 관계가 없다.(대부분)
그나마 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.
상속관계 매핑이라는 것은 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.
슈퍼타입 서브타입 논리 모델 -> 물리모델 구현 방법
객체는 상속을 지원하므로 모델링과 구현이 똑같지만, DB는 상속을 지원하지 않으므로 논리 모델을 물리 모델로 구현할 방법이 필요하다.
DB의 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법은 세가지 있다.
중요한건, DB입장에서 세가지로 구현하지만 JPA에서는 어떤 방식을 선택하던 매핑이 가능하다.
JPA가 이 세가지 방식과 매핑하려면
@Inheritance(strategy=InheritanceType.XXX)의 stategy를 설정해주면 된다.
default 전략은 SINGLE_TABLE(단일 테이블 전략)이다.
InheritanceType 종류
JOINED
SINGLE_TABLE
TABLE_PER_CLASS
@DiscriminatorColumn(name="DTYPE")
부모 클래스에 선언한다. 하위 클래스를 구분하는 용도의 컬럼이다. 관례는 default = DTYPE
@DiscriminatorValue("XXX")
하위 클래스에 선언한다. 엔티티를 저장할 때 슈퍼타입의 구분 컬럼에 저장할 값을 지정한다.
어노테이션을 선언하지 않을 경우 기본값으로 클래스 이름이 들어간다.
객체의 상속관계 구현
Item
strategy = InheritanceType.XXX) // 상속 구현 전략 선택 (
public class Item {
strategy = GenerationType.IDENTITY) (
private Long id;
private String name;
private int price;
}Album
public class Album extends Item {
private String artist;
}Movie
public class Movie extends Item {
private String director;
private String actor;
}
Book
public class Book extends Item {
private String author;
private String isbn;
}
1 - 각각의 테이블로 변환하는 조인 전략(JOINED)
가장 정규화 된 방법으로 구현하는 방식이다.
NAME, PRICE가 ITEM 테이블에만 저장되고, ALBUM, MOVIE, BOOK이 각자의 데이터만 저장한다.
Item 엔티티 - @Inheritance(strategy = InheritanceType.JOINED) 전략
하이버네이트의 조인 전략에서는 @DiscriminatorColumn을 선언하지 않으면 DTYPE 컬럼이 생성되지 않는다.
어차피 조인하면 앨범인지 무비인지 알 수 있다. 그래도, DTYPE을 넣어주는 것이 명확하다. 넣어주자.
strategy = InheritanceType.JOINED) (
// 하위 테이블의 구분 컬럼 생성(default = DTYPE)
public class Item {
strategy = GenerationType.IDENTITY) (
private Long id;
private String name;
private int price;
}실제 실행된 DDL
테이블 4개 생성
하위 테이블에 외래키 제약조건 생성. 하위 테이블 입장에서는 ITEM_ID가 PK이면서 FK로 잡아야 한다.
조인 전략에 맞는 테이블들이 생섬됨.
Hibernate:
create table Album (
artist varchar(255),
id bigint not null,
primary key (id)
)
Hibernate:
create table Book (
author varchar(255),
isbn varchar(255),
id bigint not null,
primary key (id)
)
Hibernate:
create table Item (
DTYPE varchar(31) not null,
id bigint generated by default as identity,
name varchar(255),
price integer not null,
primary key (id)
)
Hibernate:
create table Movie (
actor varchar(255),
director varchar(255),
id bigint not null,
primary key (id)
)
Hibernate:
alter table Album
add constraint FKcve1ph6vw9ihye8rbk26h5jm9
foreign key (id)
references Item
Hibernate:
alter table Book
add constraint FKbwwc3a7ch631uyv1b5o9tvysi
foreign key (id)
references Item
Hibernate:
alter table Movie
add constraint FK5sq6d5agrc34ithpdfs0umo9g
foreign key (id)
references ItemMovie 객체를 저장하면?
Insert 쿼리가 두개 나간다.
Item 테이블, Movie 테이블 저장.
DTYPE에 클래스 이름이 디폴트로 저장됨.
Movie movie = new Movie();
movie.setDirector("감독A");
movie.setActor("배우B");
movie.setName("분노의질주");
movie.setPrice(35000);
em.persist(movie);
tx.commit();Hibernate:
/* insert advancedmapping.Movie
*/ insert
into
Item
(id, name, price, DTYPE)
values
(null, ?, ?, 'Movie')
Hibernate:
/* insert advancedmapping.Movie
*/ insert
into
Movie
(actor, director, id)
values
(?, ?, ?)Movie 객체를 조회하면?
flush(), clear() 해주면, DB에 insert쿼리 날리고, 1차 캐시 지우므로 find에서 SELECT 쿼리가 나간다.
Item과 inner join을 통해서 결과를 조회한다.
Movie movie = new Movie();
movie.setDirector("감독A");
movie.setActor("배우B");
movie.setName("분노의질주");
movie.setPrice(35000);
em.persist(movie);
em.flush();
em.clear(); //DB에 insert쿼리 날리고, 1차 캐시 지우므로 find에서 SELECT 쿼리가 나간다.
em.find(Movie.class, movie.getId());
tx.commit();Hibernate:
select
movie0_.id as id2_2_0_,
movie0_1_.name as name3_2_0_,
movie0_1_.price as price4_2_0_,
movie0_.actor as actor1_3_0_,
movie0_.director as director2_3_0_
from
Movie movie0_
inner join
Item movie0_1_
on movie0_.id=movie0_1_.id
where
movie0_.id=?
2 - 통합 테이블로 변환하는 단일 테이블 전략(SINGLE_TABLE)
서비스 규모가 크지 않고, 굳이 조인 전략을 선택해서 복잡하게 갈 필요가 없다고 판단 될 때에는
한 테이블에 다 저장하고, DTYPE으로 구분하는 단일 테이블 전략을 선택할 수 있다.
INSERT 쿼리도 한 번, SELECT 쿼리도 한 번이다. 조인할 필요가 없고, 성능이 좋다.
단일 테이블 적용
strategy를 SINGLE_TABLE로 변경하면 끝난다.
JPA의 장점이다. 테이블 구조의 변동이 일어났는데, 코드를 거의 손대지 않고 어노테이션만 수정했다.
만약 JPA를 쓰지 않았더라면, 테이블 구조의 변경이 일어나면 거의 모든 코드를 손대야 할 것이다.
단일 테이블 전략에서는 @DiscriminatorColumn을 선언해 주지 않아도, 기본으로 DTYPE 컬럼이 생성된다.
한 테이블에 모든 컬럼을 저장하기 때문에, DTYPE 없이는 테이블을 판단할 수 없다.
strategy = InheritanceType.SINGLE_TABLE) (
public class Item {
strategy = GenerationType.IDENTITY) (
private Long id;
private String name;
private int price;
}실행된 DDL
통합 테이블이 하나 생성된다.
Hibernate:
create table Item (
DTYPE varchar(31) not null,
id bigint generated by default as identity,
name varchar(255),
price integer not null,
artist varchar(255),
author varchar(255),
isbn varchar(255),
actor varchar(255),
director varchar(255),
primary key (id)
)조인 전략에서 실습했던 Movie 저장, 조회 예제를 그대로 돌려보면?
Item 테이블을 그냥 조회한다. 조인하지 않고, DTYPE을 검색 조건으로 추가해서 Movie를 조회
Hibernate:
select
movie0_.id as id2_0_0_,
movie0_.name as name3_0_0_,
movie0_.price as price4_0_0_,
movie0_.actor as actor8_0_0_,
movie0_.director as director9_0_0_
from
Item movie0_
where
movie0_.id=?
and movie0_.DTYPE='Movie'
3 - 서브타입 테이블로 변환하는 구현 클래스마다 테이블을 생성하는 전략(TABLE_PER_CLASS)
조인 전략과 유사하지만, 슈퍼 타입의 컬럼들을 서브 타입으로 내린다. NAME, PRICE 컬럼들이 중복되도록 허용하는 전략이다.
구현 클래스마다 테이블 생성 전략 적용
@Id 생성 전략 GenerationType.AUTO를 사용하는 경우 strategy를 TABLE_PER_CLASS로 변경하고
이때, ITEM 엔티티는 실제 생성되는 테이블이 아니므로 abstract 클래스여야 하고, @DiscriminatorColumn도 필요가 없어진다.
하지만, @Id 생성 전략 GenerationType.IDENTITY를 사용하는 경우 문제가 있다.
@Inheritance의 TABLE_PER_CLASS와 @Id의 GenerationType을 IDETITY를 같이 사용할 경우 에러 발생
Cannot use identity column key generation with <union-subclass> mapping for
@Id의 GenerationType을 TABLE 타입으로 변경 적용해서 해결. 그러나, 시퀀스 테이블이 생성되므로 이것에 대한 매핑까지 추가해줘야 완벽히 해결이 된다.
Item 엔티티 설정
strategy = InheritanceType.TABLE_PER_CLASS) (
public abstract class Item {
strategy = GenerationType.AUTO) (
private Long id;
private String name;
private int price;
}생성된 DDL
하위 테이블 3개만 생성된다.
Hibernate:
create table Album (
id bigint not null,
name varchar(255),
price integer not null,
artist varchar(255),
primary key (id)
)
Hibernate:
create table Book (
id bigint not null,
name varchar(255),
price integer not null,
author varchar(255),
isbn varchar(255),
primary key (id)
)
Hibernate:
create table Movie (
id bigint not null,
name varchar(255),
price integer not null,
actor varchar(255),
director varchar(255),
primary key (id)
)
조인 전략에서의 Movie 저장, 조회를 그대로 실행한 결과는?
Movie 테이블에 insert
Movie 테이블에서 select
Hibernate:
/* insert advancedmapping.Movie
*/ insert
into
Movie
(name, price, actor, director, id)
values
(?, ?, ?, ?, ?)
Hibernate:
select
movie0_.id as id1_2_0_,
movie0_.name as name2_2_0_,
movie0_.price as price3_2_0_,
movie0_.actor as actor1_3_0_,
movie0_.director as director2_3_0_
from
Movie movie0_
where
movie0_.id=?문제점
객체지향 프로그래밍에서는 MOVIE, ALBUM, BOOK 객체를 ITEM 타입으로도 조회할 수 있다.
Movie movie = new Movie();
movie.setDirector("감독A");
movie.setActor("배우B");
movie.setName("분노의질주");
movie.setPrice(35000);
em.persist(movie);
em.flush();
em.clear();
em.find(Item.class, movie.getId());
tx.commit();실행된 SQL
union all로 전체 하위 테이블을 다 찾는다.
INSERT 까진 심플했으나, 조회가 시작되면 굉장히 비효율적으로 동작한다.
Hibernate:
select
item0_.id as id1_2_0_,
item0_.name as name2_2_0_,
item0_.price as price3_2_0_,
item0_.artist as artist1_0_0_,
item0_.author as author1_1_0_,
item0_.isbn as isbn2_1_0_,
item0_.actor as actor1_3_0_,
item0_.director as director2_3_0_,
item0_.clazz_ as clazz_0_
from
( select
id,
name,
price,
artist,
null as author,
null as isbn,
null as actor,
null as director,
1 as clazz_
from
Album
union
all select
id,
name,
price,
null as artist,
author,
isbn,
null as actor,
null as director,
2 as clazz_
from
Book
union
all select
id,
name,
price,
null as artist,
null as author,
null as isbn,
actor,
director,
3 as clazz_
from
Movie
) item0_
where
item0_.id=?
상속관계 매핑 정리
조인 전략
장점
테이블이 정규화가 되어있고,
외래 키 참조 무결성 제약조건 활용 가능
ITEM의 PK가 ALBUM, MOVIE, BOOK의 PK이자 FK이다. 그래서 다른 테이블에서 아이템 테이블만 바라보도록 설계하는 것이 가능 하다
저장공간 효율화
테이블 정규화로 저장공간이 딱 필요한 만큼 소비된다.
단점
조회시 조인을 많이 사용한다. 단일 테이블 전략에 비하면 성능이 안나온다. 조인하니까.
그래서 조회 쿼리가 복잡하다
데이터 저장시에 INSERT 쿼리가 상위, 하위 테이블 두번 발생한다.
정리
성능 저하라고 되어있지만, 실제로는 영향이 크지 않다.
오히려 저장공간이 효율화 되기 때문에 장점이 크다.
기본적으로는 조인 정략이 정석이라고 보면 된다. 객체랑도 잘 맞고, 정규화도 되고, 그래서 설계가 깔끔하게 나온다.
단일 테이블 전략
장점
조인이 필요 없으므로 일반적인 조회 성능이 빠르다.
조회 쿼리가 단순핟.
단점
자식 엔티티가 매핑한 컬럼은 모두 NULL을 허용해야 한다.
단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
상황에 따라서 조인 전략보다 성능이 오히려 느려질 수 있다.
보통 이 상황에 해당하는 임계점을 넘을 일은 많지 않다.
구현 클래스마다 테이블 전략
결론은
이 전략은 쓰지말자.
ORM을 하다보면 데이터 쪽과 객체 쪽에서 trade off를 할 때가 있는데, 이 전략은 둘 다 추천하지 않는다.
장점
서브 타입을 명확하게 구분해서 처리할 때 효과적이다
NOT NULL 제약조건을 사용할 수 있다.
단점
여러 자식 테이블을 함께 조회할 때 성능이 느리다(UNION SQL)
자식 테이블을 통합해서 쿼리하기 어렵다.
변경이라는 관점으로 접근할 때 굉장히 좋지 않다.
예를 들어, ITEM들을 모두 정산하는 코드가 있다고 가정할 때, ITEM 하위 클래스가 추가되면 정산 코드가 변경된다. 추가된 하위 클래스의 정산 결과를 추가하거나 해야 한다.
상속관계 매핑 정리
기본적으로는 조인 전략을 가져가자.
그리고 조인 전략과 단일 테이블 전략의 trade off를 생각해서 전략을 선택하자.
굉장히 심플하고 확장의 가능성도 적으면 단일 테이블 전략을 가져가자. 그러나 비즈니스 적으로 중요하고, 복잡하고, 확장될 확률이 높으면 조인 전략을 가져가자.
Reference
'ICT Eng > JPA' 카테고리의 다른 글
[JPA] 영속성 컨텍스트와 플러시 이해하기 (1) | 2019.08.27 |
---|---|
[JPA] @MappedSuperclass (2) | 2019.08.26 |
[JPA] @ManyToMany, 다대다[N:M] 관계 (6) | 2019.08.23 |
[JPA] @OneToOne, 일대일[1:1] 관계 (0) | 2019.08.23 |
[JPA] @OneToMany, 일대다[1:N] 관계 (1) | 2019.08.23 |
- Total
- Today
- Yesterday
- Recursion
- Algorithm
- Wisoft
- 시간복잡도
- RBT
- ORM
- 알고리즘
- Vue.js
- 라즈베리파이
- 젠킨스
- 순환
- 무선통신소프트웨어연구실
- github
- vuejs
- Spring
- Spring Boot
- JPA
- vuex
- Java
- 인프런
- AWS
- 스프링부트
- 한밭대학교
- 정렬
- 레드블랙트리
- 자바
- 한밭이글스
- IT융합인력양성사업단
- springboot
- Raspberry Pi
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 30 |