Hibernate/JPA(EntityManager)

[JPA] 상속 관계 매핑 @Inheritance , @Discriminator____

유혁스쿨 2023. 6. 30. 09:47
728x90
반응형

관계형 데이터베이스는 상속관계가 없다

관계형 데이터베이스에는 슈퍼타입 서브타입 관계라는 모델링 기법이 있으며 이것이 객체 상속과  유사하다.

상속관계 매핑이란? 객체의 상속과 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑한다.

 

슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법은 다음과 같다.

  1. 조인 전략 : 각각 테이블로 변환
  2. 단일 테이블 전략 : 통합 테이블로 변환
  3. 구현 클래스마다 테이블 전략 : 서브타입 테이블로 변환

 

1. 조인 전략 - 각각의 테이블로 변환

예를들어 앨범, 책, 영화 라는 상품이 있다고 가정하자.

상품을 아이템이라는 슈퍼 테이블을 설계하고 앨범, 영화, 책으로 분류하여 서브 테이블을 설계한 뒤 데이터를 같이 조회할 때 JOIN으로 검색한다.

공통 요소인 상품 이름, 상품 가격을 지정하고 앨범, 영화, 책이라는 각각의 상품을 타입으로 구분할수 있는 상품구분타입 컬럼을 추가한다.

 

2. 단일 테이블 전략 : 통합 테이블로 변환

논리 모델을 아이템이라는 하나의 테이블로 통합하고 상품구분타입 컬럼을 추가하여 설계한다.

 

3. 구현 클래스마다 테이블 전략 - 서브타입 테이블로 변환

조인전략에서 사용했던 슈퍼 테이블인 아이템 테이블의 정보(상품이름, 상품가격)를 앨범, 영화, 책 테이블에 각각 구성하여 설계한다.

 

어노테이션 설명
@Inheritence(strategy = InheritanceType.XXX) 테이블 전략을 지정한다.
- JOINED : 조인전략
- SINGLE_TABLE : 단일 테이블 전략
- TABLE_PER_CLASS : 구현 클래스마다 테이블 전략
@DiscriminatorColumn(name = "DTYPE")  
@DiscriminatorValue("XXX")  

 

@Entity
public class Item {
    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    private int price;
}

아래와 상속받는다

@Entity
public class Album extends Item{
    private String artist;
}
@Entity
public class Book extends Item{
    private String author;
    private String isbn;
}
@Entity
public class Movie extends Item {
    private String director;
    private String actor;
}

[CREATE 결과]

    create table item (
       dtype varchar(31) not null,
        item_id bigint not null,
        price integer not null,
        artist varchar(255),
        author varchar(255),
        isbn varchar(255),
        actor varchar(255),
        director varchar(255),
        primary key (item_id)
    )

한 테이블에 다 들어온것으로 보아 JPA의 기본 전략이 싱글 테이블 전략이다.

 

1. 조인 전략

@Inheritance(strategy = InheritanceType.JOINED)

부모 테이블에 해당하는 Item 엔티티 클래스에 위 어노테이션을 추가한다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Item {
    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    private int price;
}

[CREATE 결과]

create table item (
      item_id bigint not null,
      price integer not null,
      primary key (item_id)
)

create table album (
     artist varchar(255),
      item_id bigint not null,
      primary key (item_id)
)

create table book (
     author varchar(255),
      isbn varchar(255),
      item_id bigint not null,
      primary key (item_id)
)

create table movie (
     actor varchar(255),
      director varchar(255),
      item_id bigint not null,
      primary key (item_id)
)

ITEM테이블이 생성되고 ALBUM, BOOK, MOIVE테이블이 각각 생성된다.

ITEM 테이블에는 DTYPE 컬럼이 추가된다.

 

 

@SpringBootTest
@Transactional
public class JpaTest {
    @PersistenceContext
    EntityManager em;
    
    @Test
	void MappedSuperTest() {
		Movie movie = new Movie();
		movie.setDirector("A");
		movie.setActor("마동석");
		movie.setName("범죄도시");
		movie.setPrice(10000);
		em.persist(movie);

		em.flush();
		em.clear();

		Movie findMovie = em.find(Movie.class, movie.getId());
		System.out.println("findMovie = " + findMovie);
	}
    
}

실행시 INSERT 쿼리가 2번 호출되고 INNER JOIN으로 SELECT쿼리가 호출된다.

Item과 Movie는 상속관계이기 때문에 setName()과 setPrice()를 통해 삽입하는 name, price 데이터는 부모클래스의 필드를 오버라이딩한 필드이기 때문에 JPA에서는 Item 엔터티에 데이터를 삽입하게 된다.

조회도 마찬가지로 name과 price를 조회하기 위해서는 Item테이블에서 데이터를 가져와야 하므로 InnerJoin이 발생하게 된다.

 

[실행 결과]

/* ===  ITEM INESRT  === */
insert
into
        item
        (name, price, actor, item_id)
values
        ('범죄도시', 10000, 1)

/* ===  ㅡMOVIE INESRT  === */
insert
into
        movie
        (actor, director, item_id)
values
        ('마동석', 'A', 1)

/* ===  ITEM SELECT === */
select
        movie0_.item_id as item_id2_2_0_,
        movie0_.name as name3_2_0_,
        movie0_.price as price4_2_0_,
        movie0_.actor as actor8_2_0_,
        movie0_.director as director9_2_0_
from
        item movie0_
inner join
          item movie0_1_
                 on movie0_.item_id=movie0_1_.item_id
where
          movie0_.item_id=1

findMovie = Movie(director=A, actor=마동석)

 

1 - 1. DTYPE 구분 컬럼 추가(@DiscriminatorColumn / @DiscriminatorValue)

@DiscriminatorColumn을 통해 DTYPE을 지정할 수 있고, default 옵션이 DTYPE이므로 컬럼명이 DTYPE으로 추가된다.

@Entity
@DiscriminatorColumn
public abstract class Item {}

[CREATE 결과]

create table item (
     dtype varchar(31) not null,
      item_id bigint not null,
      price integer not null,
      primary key (item_id)
)

 

DTYPE 컬럼명 변경

아래와 같이 name속성에 원하는 컬럼명을 설정하면 DTYPE 컬럼명을 변경할 수 있다.

@Entity
@DiscriminatorColumn(name = "item_type")
public abstract class Item {}

 

[CREATE 결과]

create table item (
     item_type varchar(31) not null,
      item_id bigint not null,
      price integer not null,
      primary key (item_id)
)

DTYPE 컬럼명이 ITEM_TYPE으로 변경된다.

 

 

@DiscriminatorValue를 통해 DTYPE에 들어가는 값을 지정할 수 있는데, default로 엔티티 클래스명이 DTYPE 컬럼에 삽입된다.

아래와 같이 DTYPE에 삽입되는 데이터를 변경할 수 있다.

@DiscriminatorValue("A")
public class Album extends Item{}

DTYPE에 삽입되는 데이터가 A로 삽입된다.

 

 

 

2. 단일 테이블 전략

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

@Inheritance 의 strategy 옵션을 SINGLE_TABLE로 설정한다.

ITEM 단일 테이블이 생성되어, 자식 테이블의 모든 구성 컬럼들이 ITEM 테이블에 컬럼으로 추가되고 DTYPE 컬럼이 추가된다.

 

[실행 결과]

/* ===  ITEM CREATE  === */
create table item(
     dtype varchar(31) not null,
      item_id bigint not null,
      name varchar(255),
      price integer not null,
      artist varchar(255),
      author varchar(255),
      isbn varchar(255),
      actor varchar(255),
      director varchar(255),
      primary key (item_id)
)

/* ===  ITEM INESRT  === */
insert
into
        item
        (name, price, actor, director, dtype, item_id)
values
        ('범죄도시', 10000, '마동석', 'A', 'Movie', 1)

/* ===  ITEM SELECT === */
select
        movie0_.item_id as item_id2_2_0_,
        movie0_.name as name3_2_0_,
        movie0_.price as price4_2_0_,
        movie0_.actor as actor8_2_0_,
        movie0_.director as director9_2_0_
from
        item movie0_
where
        movie0_.item_id=1
        and movie0_.dtype='Movie'

 

 

3. 구현 클래스마다 테이블 전략

JOIN전략과 비슷하지만, 부모 테이블인 ITEM을 구성하지 않고, 공통 컬럼으로 관리하던 name, price 속성을 각각 ALBUM, MOVIE, BOOK 테이블에 중복으로 구성한다.

이때, DTYPE 컬럼은 구분값으로 추가된다.

 

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {}

@Inheritance 의 strategy 옵션을 TABLE_PER_CLASS로 설정하고 추상 클래스로 지정해 줘야 한다.

추상클래스는 실체 클래스의 공통적인 부분을 추출해 어느정도 규격을 잡아놓은 추상적인 클래스이다.

그래서 실체클래스 실제 객체를 생성할 정도의 구체성을 가지는 반면,

추상클래스는 아직 메서드와 내용이 추상적이기 때문에 객체를 생성할 수 없다.

JPA에서는 이런 추상클래스 원리를 통해 엔티티를 생성하지 않고, 상속받는 엔티티 클래스들에 컬럼들을 위임한다.

 

Item 테이블은 생성되지 않고 Album, Book, Movie 테이블이 생성되며 Item 엔터티에서 관리하던 name, price 속성이 

각각의 자식 테이블에 중복되어 추가된다.

Item 테이블이 생성되지 않았기 때문에 Insert시 Movie테이블에 값이 삽입되며, Movie에 해당하지 않는 컬럼들은 모두 null값으로 삽입된다. (조회 또한 Movie테이블에서 조회한다.)

 

[Query 결과]

create table album (
     id bigint not null,
      name varchar(255),
      price integer not null,
      artist varchar(255),
      item_id bigint not null,
      primary key (id)
)

create table book (
     id bigint not null,
      name varchar(255),
      price integer not null,
      author varchar(255),
      isbn varchar(255),
      item_id bigint not null,
      primary key (id)
)

create table movie (
     id bigint not null,
      name varchar(255),
      price integer not null,
      actor varchar(255),
      director varchar(255),
      item_id bigint not null,
      primary key (id)
)

/* ===  MOVIE INESRT  === */
insert
       into
movie
       (name, price, actor, director, item_id)
values
       ('범죄도시', 10000, '마동석', 'A', 1)

/* ===  MOVIE SELECT === */
select
       movie0_.item_id as item_id1_4_0_,
       movie0_.name as name2_4_0_,
       movie0_.price as price3_4_0_,
       movie0_.actor as actor1_6_0_,
       movie0_.director as director2_6_0_
from
       movie movie0_
where
       movie0_.item_id=1

findMovie = Movie(director=A, actor=마동석) 

 

3 - 1. 부모클래스 타입으로 조회

@SpringBootTest
@Transactional
public class JpaTest {
    @PersistenceContext
    EntityManager em;
    
    @Test
	void MappedSuperTest() {
		Movie movie = new Movie();
		movie.setDirector("A");
		movie.setActor("마동석");
		movie.setName("범죄도시");
		movie.setPrice(10000);
		em.persist(movie);

		em.flush();
		em.clear();

		Item findMovie = em.find(Item.class, movie.getId()); // 부모타입으로 조회
		System.out.println("findMovie = " + findMovie);
	}
    
}

 

아래와 같이 UNION ALL이 발생하게 된다.

예를들어 ITEM_ID가 2번인 경우 ALBUM, MOVIE, BOOK에 각각 ITEM_ID가 존재하기 때문에 3개의 테이블을 모두 조회해봐야 한다.

select
          item0_.item_id as item_id1_4_0_,
          item0_.name as name2_4_0_,
          item0_.price as price3_4_0_,
          item0_.artist as artist1_1_0_,
          item0_.author as author1_2_0_,
          item0_.isbn as isbn2_2_0_,
          item0_.actor as actor1_6_0_,
          item0_.director as director2_6_0_,
          item0_.clazz_ as clazz_0_
from
       ( select
                 item_id,
                 name,
                 price,
                 artist,
                 null as author,
                 null as isbn,
                 null as actor,
                 null as director,
                 1 as clazz_
       from
                  album
       union
       all select
                  item_id,
                  name,
                  price,
                  null as artist,
                  author,
                  isbn,
                  null as actor,
                  null as director,
                  2 as clazz_
       from
              book
       union
       all select
                  item_id,
                  name,
                  price,
                  null as artist,
                  null as author,
                  null as isbn,
                  actor,
                  director,
                  3 as clazz_
       from
                  movie
       ) item0_
where
           item0_.item_id=1

findMovie = Movie(director=A, actor=마동석) 

 


전략 별 각각의 장단점

전략 장점 단점
조인 테이블 정규화, 외래키 참조 무결성 제약조건 활용 가능, 저장공간의 효율화

정규화가 되어있으므로 제약조건을 부모테이블인 ITEM에 걸어 맞출수 있게 된다.

예를들어 가격을 통해 정산을 해야한다면 ITEM 테이블로만 조회하면 된다.
- 조회시 조인을 많이 사용하여 성능이 저하된다.
- 조회 쿼리가 복잡하다.
- 데이터 저장시에 INESRT Query가 2번 호출된다.
단일 테이블 조인이 필요 없으므로 일반적으로 조회 성능이 빠르고, 조회 쿼리가 단순하다. - 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야한다.
- 단일 테이블에 모든 것을저장하므로 테이블이 커질 수 있다.
- 상황에 따라서 조회 성능이 오히려 느려질 수 있다.
(일반적으로 조인테이블 전략보다 빠름)
구현 클래스마다
테이블 생성
서브타입을 명확하게 구분해서 처리할 때 효과적이고, Not Null 제약조건 사용이 가능하다. - 여러 자식 테이블을 함게 조회할 때 성능이 느리다 (UNION Query 발생)
- 자식 테이블을 통합해서 쿼리하기 어려운 부분이 있다.

 

728x90
반응형