1. 데이터베이스에 상속관계 표현하기
- 관계형 데이터베이스는 상속 관계 개념이 없다.
- Java 객체는 상속 관계 개념이 있다.
- 부모 클래스는 독립적으로 인스턴스화 할 필요가 없다면, 추상클래스로 만드는게 일반적이다.
관계형 데이터베이스에 상속 관계를 표현하기 위해 JPA는 세 가지 주요 전략을 제공한다.
1-1) 조인전략
- ITEM_ID를 외래키로 하위 테이블에 INSERT를 할 때, ITEM과 하위 테이블에 두 번 INSERT를 한다.
ITEM에 DTYPE으로 하위 테이블을 구분한다.
1-2) 단일 테이블 전략
- 한 테이블에 컬럼을 다 때려박고 DTYPE으로 자식 엔티티를 구분하는 전략이다.
- 조인할 필요가 없으므로 조회 성능이 빠르고 조회쿼리가 단순하다.
1-3) 구현 클래스마다 테이블 전략
- 상위테이블(슈퍼타입)없이 상위 테이블이 가질 NAME, PRICE와 같은 속성을 하위 테이블(서브타입)이 갖는다.
2. 조인 전략
@Inheritance(stratagy = InheritanceType.XXX) 어노테이션
DB에 상속관계를 표현하기 위해 사용하는 어노테이션이다.
- JOINED : 조인 전략
- SINGLE_TABLE : 단일 테이블 전략
- TABLE_PER_CLASS : 구현 클래스마다 테이블 전략
✅@Inheritance 어노테이션이 없을 때
@Entity
@Getter
@Setter
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
@Getter
@Setter
public class Album extends Item {
private String artist;
}
@Entity
@Getter
@Setter
public class Book extends Item{
private String author;
private String isbn;
}
@Entity
@Getter
@Setter
public class Movie extends Item{
private String director;
private String actor;
}
애플리케이션을 실행하면, Item테이블에 하위 테이블의 모든 컬럼을 때려박는 쿼리가 실행된다.
create table Item (
price integer not null,
id bigint not null,
DTYPE varchar(31) not null,
actor varchar(255),
artist varchar(255),
author varchar(255),
director varchar(255),
isbn varchar(255),
name varchar(255),
primary key (id)
)
✅@Inheritance(strategy = InheritanceType.JOINED)
Item 클래스에 어노테이션 한 줄을 추가한다.
✔️Item 클래스 (슈퍼 타입)
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
✔️애플리케이션 실행부
public class JpaMain {
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("directorA");
movie.setActor("ActorA");
movie.setName("movieA");
movie.setPrice(14000);
em.persist(movie);
tx.commit();
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
}
}
movie에 값을 입력하고 애플리케이션을 실행하면, 조인 전략대로 매핑이 되어 create 쿼리가 실행된다.
그리고 insert 쿼리가 item테이블에 한 번, movie 테이블에 한 번. 총 두 번 실행된다.
create table Item (
price integer not null,
id bigint not null,
name varchar(255),
primary key (id)
)
create table Album (
id bigint not null,
artist varchar(255),
primary key (id)
)
create table Movie (
id bigint not null,
actor varchar(255),
director varchar(255),
primary key (id)
)
create table Book (
id bigint not null,
author varchar(255),
isbn varchar(255),
primary key (id)
)
/* insert for
hellojpa.Movie */
insert into
Item (name, price, id)
values
(?, ?, ?)
/* insert for
hellojpa.Movie */
insert into
Movie (actor, director, id)
values
(?, ?, ?)
H2 데이터베이스를 확인해보면, 조인 전략대로 테이블이 생성되었고, 데이터가 잘 들어갔음을 확인할 수 있다.
@DiscriminatorColumn(name="DTYPE") 어노테이션
부모 테이블에 자식 엔티티의 타입 정보를 저장하는 열을 생성하는 어노테이션이다.
✔️Item 클래스에 어노테이션 추가
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
@Getter
@Setter
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
@DiscriminatorColumn(name="DTYPE") 을 추가하고 애플리케이션을 실행하고 DB를 확인해보면
ITEM 테이블에 DTYPE 컬럼이 추가됐고 Movie 엔티티 타입 정보를 저장한 것을 확인할 수 있다.
@DiscrimiatorValue("XXX")
특정 자식 엔티티에 부모 테이블 DTYPE에 어떤 값을 저장할 지 지정하는 어노테이션이다.
✔️Moive 를 "M"으로 축약해서 DTYPE에 저장하기
@Entity
@Getter
@Setter
@DiscriminatorValue("M")
public class Movie extends Item{
private String director;
private String actor;
}
애플리케이션을 실행하고 DB를 확인해보면, DTYPE에 M으로 축약한 값이 들어간 것을 확인할 수 있다.
3. 단일 테이블 전략
부모 엔티티를 상속받은 모든 자식 엔티티를 하나의 테이블에 저장하는 전략이다.
✅@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
부모 엔티티에 이 어노테이션 한 줄만 추가해주면 된다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
@Getter
@Setter
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
애플리케이션을 실행하면, 조인 전략과 달리 자식 테이블(Album, Book, Movie)이 생성되지 않고 Item 테이블만 생성된 걸 볼 수 있다.
또한, insert쿼리는 한 번만 실행된다.
조회를 할 때도 join할 필요없어 조회 쿼리가 단순하다.
create table Item (
price integer not null,
id bigint not null,
DTYPE varchar(31) not null,
actor varchar(255),
artist varchar(255),
author varchar(255),
director varchar(255),
isbn varchar(255),
name varchar(255),
primary key (id)
)
/* insert for
hellojpa.Movie */
insert into
Item (name, price, actor, director, DTYPE, id)
values
(?, ?, ?, ?, 'M', ?)
select
m1_0.id,
m1_0.name,
m1_0.price,
m1_0.actor,
m1_0.director
from
Item m1_0
where
m1_0.DTYPE='M'
and m1_0.id=?
DB를 확인해보면, ITEM 테이블에서 자식 엔티티에 대한 정보를 모두 담고있는 걸 볼 수 있다.
(PRICE, DTYPE, ACTOR, DIRECTOR, NAME에 값이 있는 것은 애플리케이션 실행부에서 Movie에 대한 정보를 넣어줬기 때문이다.)
단일 테이블 전략에선 DTYPE이 필수다.
왜냐하면, 단일 테이블 전략에 DTYPE이 없다면 어느 자식 엔티티에서 온 정보인지 알 방법이 없기 때문이다.
그래서 단일 테이블 전략을 사용하면, @DiscriminatorColumn 어노테이션이 없어도 DTYPE 컬럼이 자동 생성된다.
🤔Q. 부모 엔티티를 상속받는 자식 엔티티가 있을 때, @Inheritance 어노테이션이 없으면 기본적으로 단일 테이블 전략을 사용하는데, 왜 @Inheritance(starategy = InheritanceType.SINGLE_TABLE)을 쓰는 걸까?
💡A. 단일 테이블 전략을 명시적으로 설정하는 것이 코드의 가독성과 명확성을 높이기 때문이다.
또한, 명시적으로 선언해야 구분자 컬럼(DTYPE)과 관련된 설정을 커스터마이징 할 수 있다.
4. 구현 클래스마다 테이블 전략
부모 테이블을 만들지 않고 부모 엔티티의 속성을 모두 자식 엔티티로 받아서 자식 테이블만 만드는 전략이다.
속성들이 중복되는 것을 허용한다.
이 때, 부모 엔티티가 추상 클래스여야 부모테이블이 생성되지 않는다.
(추상 클래스가 아니면 JPA는 부모 클래스가 독립적으로 사용될 필요가 있다고 보기 때문에 부모테이블이 만들어진다.)
✅@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
자식 엔티티를 구분할 필요가 없기때문에, @DiscriminatorColumn 애노테이션이 필요없다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@Getter
@Setter
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
애플리케이션을 실행하면, ITEM 테이블은 만들어지지 않고 자식 엔티티에 대한 테이블만 생성된다.
Movie에 값을 입력하면 Movie 테이블에 한 번 insert 쿼리가 실행되고 select 쿼리도 조인 없이 실행된다.
create table Album (
price integer not null,
id bigint not null,
artist varchar(255),
name varchar(255),
primary key (id)
)
create table Book (
price integer not null,
id bigint not null,
author varchar(255),
isbn varchar(255),
name varchar(255),
primary key (id)
)
create table Movie (
price integer not null,
id bigint not null,
actor varchar(255),
director varchar(255),
name varchar(255),
primary key (id)
)
/* insert for
hellojpa.Movie */
insert into
Movie (name, price, actor, director, id)
values
(?, ?, ?, ?, ?)
select
m1_0.id,
m1_0.name,
m1_0.price,
m1_0.actor,
m1_0.director
from
Movie m1_0
where
m1_0.id=?
DB 조회를 해보면, Movie 테이블에 값이 들어간 것을 확인할 수 있다.
그러나, 이 전략에는 큰 단점이 있다.
✔️Item (부모 엔티티)에서 자식 엔티티를 찾으려 할 때
Item item = em.find(Item.class, movie.getId());
System.out.println("item.Id = " + item);
부모 엔티티에서 자식 엔티티의 id로 탐색을 하고자 하면, 복잡한 쿼리가 실행된다.
union all로 자식 테이블을 모두 뒤져서 그 안에 id를 찾아내야하기 때문이다.
select
i1_0.id,
i1_0.clazz_,
i1_0.name,
i1_0.price,
i1_0.artist,
i1_0.author,
i1_0.isbn,
i1_0.actor,
i1_0.director
from
(select
price,
id,
artist,
name,
null as author,
null as isbn,
null as actor,
null as director,
1 as clazz_
from
Album
union
all select
price,
id,
null as artist,
name,
author,
isbn,
null as actor,
null as director,
2 as clazz_
from
Book
union
all select
price,
id,
null as artist,
name,
null as author,
null as isbn,
actor,
director,
3 as clazz_
from
Movie
) i1_0
where
i1_0.id=?
5. 상속매핑 전략별 특징 및 장단점
전략 | 특징 | 장점 | 단점 |
조인 전략 | - 부모와 자식 클래스에 대해 별도의 테이블 생성 - 조인 사용 |
- 테이블 정규화 - 중복 없음 - 스키마 변경 용이 - 저장공간 효율화 - 외래키 참조 무결성 제약조건 활용가능 |
- 쿼리 복잡 - 조인 연산 많음 - 조회 성능 저하 - 데이터 저장시 INSERT SQL 2번 호출 |
단일 테이블 전략 | - 모든 클래스를 하나의 테이블에 저장 - 구분자 컬럼(DTYPE) 사용 |
- 쿼리 단순 - 조회 성능 좋음 - 조인 필요 없음 |
- 단일 테이블에 모든 것을 저장하여 테이블 커짐 - 자식 엔티티가 매핑한 컬럼은 모두 null 허용 - 상황에 따라 조회 성능 저하 |
구현 클래스마다 테이블 전략 | - 각 클래스마다 별도의 테이블 생성 - 자식 테이블에 부모 클래스의 모든 필드 포함 |
- 구조 단순 - 각 클래스 데이터만 저장하여 서브타입을 명확하게 구분해서 처리할 때 효과적 - not null 제약조건 사용 가능 |
- 중복 데이터 많음 - 여러 자식 테이블을 함께 조회 시 성능 저하 - 자식 테이블을 통합해서 쿼리하기 어려움 |
정리하자면,
1. 기본적으로 조인 전략이 정석적이다.
INSERT 쿼리가 두 번 실행되고, 조인 연산이 많다는 단점은 크게 치명적인 단점이 아니다.
관리하는 테이블이 많아져서 복잡할 수 있지만 장기적인 유지보수를 생각하면 좋은 전략이다.
🤔왜 '조인 전략'이 유지보수에 좋을 까?
- 데이터가 정규화와 무결성이 보장 되기 때문에 데이터를 일관적으로 유지하기 좋다.
- 부모 클래스에 공통된 필드를 추가하거나 변경할 때, 하나의 테이블만 수정하면 된다.
- 새로운 자식 클래스를 추가하거나 변경할 때, 기존 테이블 구조를 크게 변경하지 않고 추가할 수 있다.(확장성👍)
2. 단일 테이블 전략의 치명적 단점은 자식 엔티티가 매핑한 컬럼은 모두 null이 허용된다는 점이다.
🤔이게 왜 치명적인 단점이라는 걸까?
- 데이터 무결성 문제 : 특정 자식 엔티티가 반드시 가져야 할 필드가 null로 저장될 수 있는 등 데이터 무결성을 보장하기 어렵다.
- 공간 낭비: 자식 엔티티마다 존재하는 모든 필드를 포함해야 하니 테이블 컬럼 수가 많아지고 저장 공간이 낭비된다.
- 쿼리 복잡성 증가 : 특정 자식 엔티티의 데이터를 조회할 때 null 체크가 많이 필요하다. 인덱스를 효율적으로 사용하기 어렵다.
3. 구현 클래스마다 테이블 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천하지 않는 방식이다.
🤔왜 추천하지 않는 걸까?
- 부모 클래스와 자식 클래스의 데이터를 통합 조회하기가 어렵다. 쿼리가 매우 복잡해진다.
- 변경에 유연하지 않다. 부모 클래스의 공통 데이터를 변경하려면 여러 자식 테이블의 데이터를 모두 업데이트해야한다. 그리고 부모 클래스에 새로운 필드를 추가하려면 모든 자식 테이블에 해당 필드를 추가해야해서 유지보수 비용이 증가한다.
여담으로, 김영한 강사님 강의를 듣다보면 강사님 PC시간을 볼 수 있는데 오전 1시 넘어서까지 강의를 찍으신 것을 보고
반성의 시간을 가졌다..
이런 대단한 분도 이렇게 열심히 사는데 나도 열심히 살아야 겠다고 다짐했다. 화이팅..!
'인프런 김영한 강의 정리 > 자바 ORM 표준 JPA 프로그래밍 기본편' 카테고리의 다른 글
JPA 기본 | 프록시 (즉시 로딩, 지연 로딩을 이해하기 위한) (0) | 2024.07.13 |
---|---|
JPA 기본 | @MappedSuperclass (0) | 2024.07.10 |
JPA 기본 | 다양한 연관관계 매핑 (0) | 2024.07.05 |
JPA 기본 | 양방향 연관관계와 연관관계의 주인 (0) | 2024.07.02 |
JPA 기본 | 연관관계 매핑 기초 (0) | 2024.06.13 |