관계형 데이터베이스에는 상속 관계가 없다. 대신 서브타입과 슈퍼타입의 모델링 기법이 존재한다.
공통의 속성을 뽑아 슈퍼타입으로 만들고 나머지 속성은 서브타입으로 설정을 하면 상속 관계와 비슷하게 설계가 된다.
아래 그림을 참조해서 보자.
이러한 테이블 구조를 객체에서 매핑하는 전략을 알아보자.
전략 3가지
- 조인 전략
- 단일 테이블 전략
- 구현 클래스마다 테이블 전략
1. 조인 전략
슈퍼타입과 서브타입의 테이블을 각각 생성
- Item
@Entity
@Getter
@NoArgsConstructor
@DiscriminatorColumn -> 핵심
@Inheritance(strategy = InheritanceType.JOINED) -> 핵심
public class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
private int stockQuantity;
public Item(String name, int price, int stockQuantity) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
}
- Album
@Getter
@NoArgsConstructor
@Entity
public class Album extends Item {
private String artist;
private String ect;
@Builder
public Album(String name, int price, int stockQuantity, String artist, String ect) {
super(name, price, stockQuantity);
this.artist = artist;
this.ect = ect;
}
}
- Movie
@Getter
@NoArgsConstructor
@Entity
public class Movie extends Item {
private String director;
private String actor;
@Builder
public Movie(String name, int price, int stockQuantity, String director, String actor) {
super(name, price, stockQuantity);
this.director = director;
this.actor = actor;
}
}
- Book
@Getter
@NoArgsConstructor
@Entity
public class Book extends Item {
private String author;
private String isbn;
@Builder
public Book(String name, int price, int stockQuantity, String author, String isbn) {
super(name, price, stockQuantity);
this.author = author;
this.isbn = isbn;
}
}
슈퍼타입과 서브타입의 테이블에 각 객체들이 매핑을 하기위해 객체들을 설계했다.
Item 객체를 살펴보면 어노테이션이 붙어 있을 것이다. 붙은 어노테이션 중 @DiscriminatorColumn, @Inheritance 2개가 핵심이다.
@Inheritance
- @Inheritance 는 매핑 전략을 설정하는 어노테이션이다.
- 매핑 전략은 SINGLE_TABLE, TABLE_PER_CLASS, JOINED 3가지로 나타낸다.
- 먼저 조인 전략이니 JOINED로 설정해준다.
@DiscriminatorColumn
- @DiscriminatorColumn 는 DTYPE의 컬럼을 생성해주고 각 엔티티의 데이터가 저장 시 저장된 엔티티의 이름이 저장된다.
- 부가적으로 @DiscriminatorValue 을 서브타입의 객체에 붙여주고 속성으로 저장 시 엔티티의 이름을 설정한다.
스키마 생성 결과
Hibernate:
create table album (
artist varchar(255),
ect 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 not null,
name varchar(255),
price integer not null,
stock_quantity 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 FKrl4nl1yn7tatob2buih6y9qws
foreign key (id)
references item
Hibernate:
alter table book
add constraint FKqk00l5u7w76kq5n45m9h5t5rj
foreign key (id)
references item
Hibernate:
alter table movie
add constraint FK77hfitoaq24bt17vl2307651e
foreign key (id)
references item
각각의 테이블을 만들어서 JPA 가 객체와 매핑을 할 수 있게 해준다.
여기서 조인전략은 테이블 조회시에 조인 구문이 추가되고 데이터 저장시에 insert문 이 2번 동작하게 된다.
- insert 시
Hibernate:
insert
into
item
(name, price, stock_quantity, dtype, id)
values
(?, ?, ?, 'Book', ?)
Hibernate:
insert
into
book
(author, isbn, id)
values
(?, ?, ?)
insert 끝@@@@@@@@
save 시에 insert문 2번 동작
- select시
Hibernate:
select
item0_.id as id2_2_0_,
item0_.name as name3_2_0_,
item0_.price as price4_2_0_,
item0_.stock_quantity as stock_qu5_2_0_,
item0_1_.artist as artist1_0_0_,
item0_1_.ect as ect2_0_0_,
item0_2_.author as author1_1_0_,
item0_2_.isbn as isbn2_1_0_,
item0_3_.actor as actor1_3_0_,
item0_3_.director as director2_3_0_,
item0_.dtype as dtype1_2_0_
from
item item0_
left outer join
album item0_1_
on item0_.id=item0_1_.id
left outer join
book item0_2_
on item0_.id=item0_2_.id
left outer join
movie item0_3_
on item0_.id=item0_3_.id
where
item0_.id=?
ItemRepository로 만들어 테스트를 실행하여 각 테이블에 조인을 하여 아이디가 같은 것을 찾는 query문이 나갔다. 테이블이 3개여서 조인을 3번하였다.
장점
- 정규화가 잘되어 있음
- 외래키의 참조 제약조건 설정 가능
- 저장 공간 활용 가능
단점
- 조회 쿼리가 복잡하고 조인을 많이 사용함
- 데이터 저장시에 insert문이 두번 동작함
2. 단일 테이블 전략
조인 전략과 다르게 한테이블에 모든 속성을 지정하는 방식이다. 이 전략은 어떤 테이블에서 데이터가 저장되었는지 구분하는 DTYPE이 무조건 있어야 한다. JPA에서 기본설정으로 되어 있어 어노테이션 @DiscriminatorColumn 을 붙여도 되고 안붙여도 된다. 하지만 명시적으로 붙여주는 것이 괜찮은것 같다.
- Item
@Entity
@Getter
@NoArgsConstructor
@DiscriminatorColumn -> 핵심
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) -> 핵심
public class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
private int stockQuantity;
public Item(String name, int price, int stockQuantity) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
}
다음으로 어노테이션 @Inheritance 의 전략을 SINGLE_TABLE 으로 설정해준다.
그러면 단일 테이블 전략으로 바뀌어 스키마 생성이되고 각 객체와 테이블을 JPA가 매핑해준다.
스키마 생성 결과
Hibernate:
create table item (
dtype varchar(31) not null,
id bigint not null,
name varchar(255),
price integer not null,
stock_quantity integer not null,
artist varchar(255),
ect varchar(255),
author varchar(255),
isbn varchar(255),
actor varchar(255),
director varchar(255),
primary key (id)
)
모든 서브타입의 속성이 슈퍼타입의 테이블에 집중되어 생성된 것을 확인할 수 있다.
장점
- 조회시에 조인을 하지 않아도 select 문 1번으로 조회가 가능하다.
- 데이터 저장시에도 insert문 1번으로 저장 가능하다.
단점
- 각 서브타입의 속성들을 null 허용을 해야 한다.
- 각 서브타입의 속성들을 가지고 있으므로 테이블이 커질수 있는 문제점이 있다.
3. 구현 클래스마다 전략
이 전략은 구현 클래스마다 테이블을 생성을 하는 전략이다. 사용하지 않는 것을 권장한다.
단순히 저장할 땐 좋으나 조회시에 각 테이블들을 모두 조회를 하기 때문에 성능에 문제가 될 수 있다.
코드를 변경해보자.
- Item
@Entity
@Getter
@NoArgsConstructor
@DiscriminatorColumn
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) -> 핵심
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
private int stockQuantity;
public Item(String name, int price, int stockQuantity) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
}
@Inheritance 전략 속성을 TABLE_PER_CLASS 으로 변경 해준다.
다음으로 구현체인 Item 클래스는 테이블을 생성하지 않으므로 abstrat 로 추상 클래스를 만든다.
스키마 생성 결과
Hibernate:
create table album (
id bigint not null,
name varchar(255),
price integer not null,
stock_quantity integer not null,
artist varchar(255),
ect varchar(255),
primary key (id)
)
Hibernate:
create table book (
id bigint not null,
name varchar(255),
price integer not null,
stock_quantity 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,
stock_quantity integer not null,
actor varchar(255),
director varchar(255),
primary key (id)
)
구현한 객체의 테이블만 만들어진 것을 볼 수 있다.
저장과 조회의 쿼리문을 살펴보자.
저장 쿼리
Hibernate:
insert
into
book
(name, price, stock_quantity, author, isbn, id)
values
(?, ?, ?, ?, ?, ?)
insert문이 한번만 동작하게 된다.
조회 쿼리
Hibernate:
select
item0_.id as id1_2_0_,
item0_.name as name2_2_0_,
item0_.price as price3_2_0_,
item0_.stock_quantity as stock_qu4_2_0_,
item0_.artist as artist1_0_0_,
item0_.ect as ect2_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,
stock_quantity,
artist,
ect,
null as author,
null as isbn,
null as actor,
null as director,
1 as clazz_
from
album
union
all select
id,
name,
price,
stock_quantity,
null as artist,
null as ect,
author,
isbn,
null as actor,
null as director,
2 as clazz_
from
book
union
all select
id,
name,
price,
stock_quantity,
null as artist,
null as ect,
null as author,
null as isbn,
actor,
director,
3 as clazz_
from
movie
) item0_
where
item0_.id=?
장점
- 데이터 저장시 insert문 한번 동작
단점
- 데이터 조회시 모든 테이블 조회
- 성능 문제
공통 속성을 모아 상속한 클래스 매핑
각 테이블마다 공통 속성이 들어있을 수 있다. 개발자 입장에서 공통 속성을 매 테이블에 속성을 생성하는 것은 아주 불편한 일이고 속성이 중복되어 문제가 발생될 수 있다.
각 테이블의 공통 속성을 JPA에서 제공해주는 어노테이션 @MappedSuperClass 로 해결해보자.
먼저 공통 속성으로 생성시간과 수정시간이라고 룰을 정해보자.
- BaseEntity
@Getter
@NoArgsConstructor
@MappedSuperclass -> 핵심
@EntityListeners(value = AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
private LocalDateTime createDate;
@LastModifiedDate
private LocalDateTime lastModified;
}
어노테이션 @MappedSuperclass 을 사용하면 JPA가 공통속성을 각각 매핑해준다는 설정을 해준다.
다음으로 BaseEntity를 사용할 각 객체에 상속을 하면 적용이 된다.
- Item
@Entity
@Getter
@NoArgsConstructor
@DiscriminatorColumn
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item extends BaseEntity { -> BaseEntity 상속 핵심
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
private int stockQuantity;
public Item(String name, int price, int stockQuantity) {
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
}
스키마 생성 결과
Hibernate:
create table album (
id bigint not null,
create_date timestamp,
last_modified timestamp,
name varchar(255),
price integer not null,
stock_quantity integer not null,
artist varchar(255),
ect varchar(255),
primary key (id)
)
Hibernate:
create table book (
id bigint not null,
create_date timestamp,
last_modified timestamp,
name varchar(255),
price integer not null,
stock_quantity integer not null,
author varchar(255),
isbn varchar(255),
primary key (id)
)
Hibernate:
create table movie (
id bigint not null,
create_date timestamp,
last_modified timestamp,
name varchar(255),
price integer not null,
stock_quantity integer not null,
actor varchar(255),
director varchar(255),
primary key (id)
)
각 테이블에 생성시간과 수정시간이 들어가있는 것을 확인할 수 있다.
매핑 전략은 성능에 크게 문제가 되지 않는 조인 전략과 단일 테이블 전략을 사용하여 개발자 입장과 디비 입장에서의 생각을 고려해서 전략을 짜는게 좋을것 같다.
'JPA' 카테고리의 다른 글
[Jpa] Deprecated 된 getById() 대안 getReferenceById() (0) | 2022.08.02 |
---|---|
[Jpa] Dirty Checking (0) | 2022.07.19 |
[Jpa] Projections (0) | 2022.07.18 |
[Spring JPA] 트랜잭션은 언체크, 체크 예외에 대해 어떻게 커밋과 롤백을 처리할까? (1) | 2022.05.18 |
[Spring JPA] CASCADE 는 무엇일까? (0) | 2022.05.15 |
[Spring JPA] N + 1의 문제점이 무엇이고 어떻게 해결 해야할까? (0) | 2022.05.15 |
[Spring JPA] 프록시 객체는 어떻게 동작할까? (0) | 2022.05.12 |
[Spring JPA] 객체의 연관관계는 어떻게 관계를 맺는것이 좋을까? (0) | 2022.05.10 |