티스토리 뷰

JPA

[JPA] 상속관계 매핑(1): 상속 전략

Jaime.Lee 2022. 7. 5. 10:30

소스 코드는 여기 있습니다. (commit hash: c93b796)

> git clone https://github.com/lcalmsky/jpa
> git checkout c93b796

Warning: 이번 소스 코드는 최종 커밋 기준으로 작성되어 있어 모든 테스트 결과를 정확히 확인할 순 없으니 참고 부탁드립니다.

Overview

객체의 상속관계를 테이블로 어떻게 표현할 수 있는지 알아봅니다.

슈퍼타입, 서브타입 관계

관계형 데이터베이스에는 상속 관계가 존재하지 않습니다.

반면 객체는 상속관계를 나타낼 수 있는데요, 이 상속과 유사한 방법이 DB 모델링 기법에도 존재합니다.

바로 슈퍼타입, 서브타입 관계인데 확장된 ER 모델이라고 부릅니다.

슈퍼-서브 타입이 도출되는 과정은 다음과 같습니다.

  1. 공통된 데이터만 슈퍼 타입에 남김
  2. 개별 속성은 서브타입으로 구성
    • 서브타입간에 교집합이 존재하지 않을 경우 배타적(exclusive) 서브 카테고리라고 함
    • 서브타입간에 교집합이 존재할 경우 포괄적(inclusive) 서브 카테고리라고 함

객체를 추상화시키는 과정과 매우 유사하죠?

각 관계를 표현하기 위한 방법은 다음과 같습니다.

  • Rollup
    • 하나의 테이블로 통합
    • 슈퍼타입에 서브타입의 모든 컬럼을 통합하여 하나의 테이블로 생성
  • Rolldown
    • 여러 개의 테이블로 분리
    • 슈퍼타입을 각각의 서브타입에 추가하여 서브타입별로 테이블 생성
  • Identity
    • 각각의 테이블로 분리
    • 슈퍼타입과 서브타입 각각을 테이블로 생성

이 관계를 JPA를 이용해 객체에 적용하여 DB와 매핑할 수 있습니다.

상속관계 매핑

객체의 상속 구조와 DB의 슈퍼타입, 서브타입 관계를 매핑합니다.

구현하는 방법은 다음과 같습니다.

  • Rollup: 단일 Entity 매핑
  • Rolldown: 구현 클래스마다 Entity 매핑
  • Identity: Join 사용

애너테이션

상속관계 매핑을 위해 사용하는 애너테이션은 다음과 같습니다.

  • @Inheritance
    • 상속 관계를 나타냄
    • strategy attribute 사용
      • InheritanceType.JOINED
      • InheritanceType.SINGLE_TABLE
      • InheritanceType.TABLE_PER_CLASS
  • @DiscriminatorColumn(name="DTYPE")
    • 타입을 구분하는데 특정 컬럼을 사용
  • @DiscriminatorValue("XXX")
    • 타입을 구분하는데 특정 값을 사용

@Inheritance(stragegy=InheritanceType.JOINED)

JOINED 전략을 사용하면 테이블 정규화에 유리하고, FK 참조시 무결성을 보장할 수 있으며, 저장 공간을 효율적으로 사용할 수 있습니다.

하지만 조회를 위해 쿼리에 join을 많이 사용하기 때문에 성능이 저하되고 쿼리가 복잡해지며 insert 쿼리를 수행할 땐 두 번씩 호출됩니다.

@Inheritance(stragegy=InheritanceType.SINGLE_TABLE)

SINGLE_TABLE 전략은 쿼리가 단순하고 join을 사용하지 않아도 되기 때문에 성능이 좋습니다.

반면에 자식 Entity가 매핑한 컬럼은 모두 null을 허용해야하고 모든 내용을 다 저장하기 때문에 테이블이 커질 수 있어 상황에 따라 조회 속도가 오히려 느려질 수도 있습니다.

@Inheritance(stragegy=InheritanceType.TABLE_PER_CLASS)

TABLE_PER_CLASS 전략을 사용하면 서브 타입을 명확하게 구분하여 처리할 때 효과적이고 컬럼에 not null을 명시하여 데이터를 제한할 수 있습니다.

반면에 여러 테이블을 같이 조회해야 할 때 union을 사용해야 해서 조회 성능이 매우 느려집니다.

이 방법은 DB에서도 JPA에서도 추천하지 않는 방법입니다.

구현 및 테스트

위 관계들을 JPA에서 어떻게 매핑하는지 예시와 함께 확인해보겠습니다.

상속만 구현

package com.tistory.jaimenote.jpa.inheritance.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Product {
  @Id
  @GeneratedValue
  private Long id;

  private String name;

  private Integer price;
}
package com.tistory.jaimenote.jpa.inheritance.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Song extends Product {

  @Id
  @GeneratedValue
  private Long id;

  private String singer;

  private String writer;

}
package com.tistory.jaimenote.jpa.inheritance.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Movie extends Product {

  @Id
  @GeneratedValue
  private Long id;

  private String director;

  private String actor;

}
package com.tistory.jaimenote.jpa.inheritance.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Book extends Product {

  @Id
  @GeneratedValue
  private Long id;

  private String author;

  private String isbn;

}

객체간 상속관계만 지정한 상태에서 애플리케이션을 실행해보겠습니다.

2022-07-01 23:31:40.537 DEBUG 24303 --- [           main] org.hibernate.SQL                        : 

    create table product (
       dtype varchar(31) not null,
        id bigint not null,
        name varchar(255),
        price integer,
        author varchar(255),
        isbn varchar(255),
        actor varchar(255),
        director varchar(255),
        singer varchar(255),
        writer varchar(255),
        primary key (id)
    )

로그를 보면 SINGLE_TABLE 전략으로 실행된 것을 확인할 수 있습니다.

@Inheritance(strategy=InheritanceType.SINGLE_TABLE)

이렇게 명시해주지 않아도 기본 값으로 동작하는 것을 확인할 수 있습니다.

JOINED 전략

JOINED 전략을 사용하려면 직접 부모 객체에 명시해줘야 하고 부모 객체의 필드를 자식이 공유해야 하기 떄문에 관련 부분을 수정하였습니다.

package com.tistory.jaimenote.jpa.inheritance.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
@Inheritance(strategy = InheritanceType.JOINED)
public class Product {

  @Id
  @GeneratedValue
  private Long id;

  @Setter(AccessLevel.PROTECTED)
  private String name;

  @Setter(AccessLevel.PROTECTED)
  private Integer price;
}
package com.tistory.jaimenote.jpa.inheritance.entity;

import javax.persistence.Entity;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString(callSuper = true)
public class Song extends Product {

  private String singer;

  private String writer;

  private Song(String singer, String writer, String name, Integer price) {
    this.singer = singer;
    this.writer = writer;
    setName(name);
    setPrice(price);
  }

  public static Song create(String singer, String writer, String name, Integer price) {
    return new Song(singer, writer, name, price);
  }
}

Song 클래스만 테스트를 위해 static 생성자를 추가하였습니다. 나머지 클래스도 마찬가지로 부모의 name, price 값을 설정해주어야 하지만 생략하겠습니다.

package com.tistory.jaimenote.jpa.inheritance.entity;

import javax.persistence.Entity;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Movie extends Product {

  private String director;

  private String actor;

}
package com.tistory.jaimenote.jpa.inheritance.entity;

import javax.persistence.Entity;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Book extends Product {

  private String author;

  private String isbn;

}

다시 실행해보면,

2022-07-01 23:38:41.715 DEBUG 24860 --- [           main] org.hibernate.SQL                        : 

    create table product (
       id bigint not null,
        name varchar(255),
        price integer,
        primary key (id)
    )
2022-07-01 23:38:41.716 DEBUG 24860 --- [           main] org.hibernate.SQL                        : 

    create table song (
       singer varchar(255),
        writer varchar(255),
        id bigint not null,
        primary key (id)
    )
2022-07-01 23:38:41.700 DEBUG 24860 --- [           main] org.hibernate.SQL                        : 

    create table book (
       author varchar(255),
        isbn varchar(255),
        id bigint not null,
        primary key (id)
    )
2022-07-01 23:38:41.710 DEBUG 24860 --- [           main] org.hibernate.SQL                        : 

    create table movie (
       actor varchar(255),
        director varchar(255),
        id bigint not null,
        primary key (id)
    )
2022-07-01 23:38:41.750 DEBUG 24860 --- [           main] org.hibernate.SQL                        : 

    alter table song 
       add constraint FK9fdrc2217ma14hcfbjjuu7552 
       foreign key (id) 
       references product
2022-07-01 23:38:41.717 DEBUG 24860 --- [           main] org.hibernate.SQL                        : 

    alter table book 
       add constraint FK8cjf4cjanicu58p2l5t8d9xvu 
       foreign key (id) 
       references product
2022-07-01 23:38:41.741 DEBUG 24860 --- [           main] org.hibernate.SQL                        : 

    alter table movie 
       add constraint FKann45li0a78wurkcpk3x3mpb7 
       foreign key (id) 
       references product

테이블이 각각 생성되고 FK로 ID를 매핑하는 것을 확인할 수 있습니다.

그리고 어떻게 동작하는지 확인하기 위해 테스트 클래스를 작성해 insert, select 쿼리를 확인해보겠습니다.

package com.tistory.jaimenote.jpa.inheritance.infra.repository;

import com.tistory.jaimenote.jpa.inheritance.entity.Song;
import java.util.List;
import javax.persistence.PersistenceContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest
class SongRepositoryTest {

  @Autowired
  SongRepository songRepository;

  @BeforeEach
  void setup() {
    Song song = Song.create("IU", "IU", "너의 의미", 500);
    songRepository.save(song);
  }

  @Test
  void findSongTest() {
    List<Song> songs = songRepository.findAll();
    for (Song song : songs) {
      System.out.println(song);
    }
  }
}

테스트를 실행해보면,

Hibernate: 
    insert 
    into
        product
        (name, price, id) 
    values
        (?, ?, ?)
2022-07-02 02:07:24.315 TRACE 29540 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [너의 의미]
2022-07-02 02:07:24.317 TRACE 29540 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [500]
2022-07-02 02:07:24.318 TRACE 29540 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
Hibernate: 
    insert 
    into
        song
        (singer, writer, id) 
    values
        (?, ?, ?)
2022-07-02 02:07:24.321 TRACE 29540 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [IU]
2022-07-02 02:07:24.321 TRACE 29540 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [IU]
2022-07-02 02:07:24.322 TRACE 29540 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
Hibernate: 
    select
        song0_.id as id1_9_,
        song0_1_.name as name2_9_,
        song0_1_.price as price3_9_,
        song0_.singer as singer1_10_,
        song0_.writer as writer2_10_ 
    from
        song song0_ 
    inner join
        product song0_1_ 
            on song0_.id=song0_1_.id
Song(super=Product(id=1, name=너의 의미, price=500), singer=IU, writer=IU)

insert가 2회 발생하고 select 시에는 inner join으로 productsong 테이블의 데이터를 조회해오는 것을 확인할 수 있습니다.

JOINED 전략을 사용하면서 @DiscriminatorColumn을 사용했을 때는 어떻게 바뀌는지 확인해보겠습니다.

부모 클래스에 @DiscriminatorColumn 애너테이션을 추가합니다.

// 생략
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public class Product {
  // 생략
}

name attribute를 이용해 컬럼명을 수정할 수 있습니다. 아무것도 지정하지 않으면 DTYPE을 기본 값으로 사용합니다.

자식 클래스에는 @DiscriminatorValue 애너테이션을 추가합니다.

// 생략
@DiscriminatorValue("song")
public class Song extends Product {
  // 생략
}
// 생략
@DiscriminatorValue("movie")
public class Movie extends Product {
  // 생략
}
// 생략
@DiscriminatorValue("Book")
public class Book extends Product {
  // 생략
}

다시 실행해보면,

Hibernate: 

    create table product (
       dtype varchar(31) not null,
        id bigint not null,
        name varchar(255),
        price integer,
        primary key (id)
    )

dtype이라는 컬럼이 자동으로 생성된 것을 확인할 수 있고, insert, select 쿼리를 확인해보면,

Hibernate: 
    insert 
    into
        product
        (name, price, dtype, id) 
    values
        (?, ?, 'song', ?)
2022-07-02 02:16:34.323 TRACE 30335 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [너의 의미]
2022-07-02 02:16:34.325 TRACE 30335 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [500]
2022-07-02 02:16:34.326 TRACE 30335 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
Hibernate: 
    insert 
    into
        song
        (singer, writer, id) 
    values
        (?, ?, ?)
2022-07-02 02:16:34.330 TRACE 30335 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [IU]
2022-07-02 02:16:34.330 TRACE 30335 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [IU]
2022-07-02 02:16:34.330 TRACE 30335 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
Hibernate: 
    select
        song0_.id as id2_9_,
        song0_1_.name as name3_9_,
        song0_1_.price as price4_9_,
        song0_.singer as singer1_10_,
        song0_.writer as writer2_10_ 
    from
        song song0_ 
    inner join
        product song0_1_ 
            on song0_.id=song0_1_.id
Song(super=Product(id=1, name=너의 의미, price=500), singer=IU, writer=IU)

아쉽게도 song 컬럼이 추가된 것 외에는 이전 쿼리와 동일합니다.

그렇다면 SINGLE_TABLE 전략을 사용하면 어떻게 될까요?

SINGLE_TABLE 전략

// 생략
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public class Product {
  // 생략
}

부모클래스에서 상속 전략을 수정하였습니다.

그리고 테스트를 다시 실행해보면,

Hibernate: 

    create table product (
       dtype varchar(31) not null,
        id bigint not null,
        name varchar(255),
        price integer,
        author varchar(255),
        isbn varchar(255),
        actor varchar(255),
        director varchar(255),
        singer varchar(255),
        writer varchar(255),
        primary key (id)
    )

product 테이블 한 개만 생성되었고,

Hibernate: 
    insert 
    into
        product
        (name, price, singer, writer, dtype, id) 
    values
        (?, ?, ?, ?, 'song', ?)
2022-07-02 02:19:03.145 TRACE 30535 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [너의 의미]
2022-07-02 02:19:03.147 TRACE 30535 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [500]
2022-07-02 02:19:03.147 TRACE 30535 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [IU]
2022-07-02 02:19:03.147 TRACE 30535 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [VARCHAR] - [IU]
2022-07-02 02:19:03.148 TRACE 30535 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [5] as [BIGINT] - [1]
Hibernate: 
    select
        song0_.id as id2_7_,
        song0_.name as name3_7_,
        song0_.price as price4_7_,
        song0_.singer as singer9_7_,
        song0_.writer as writer10_7_ 
    from
        product song0_ 
    where
        song0_.dtype='song'
Song(super=Product(id=1, name=너의 의미, price=500), singer=IU, writer=IU)

insert, select 쿼리가 1회만 발생하였고 join 대신 where 절에 dtype을 사용한 것을 확인할 수 있습니다.

사실 SINGLE_TABLE 전략을 사용하면 @DiscriminatorColumn을 사용하지 않아도 자동으로 DTYPE 컬럼을 생성합니다.

하나의 테이블 안에서 타입을 구분해가면서 저장하려면 추가 컬럼을 사용하지 않고는 불가능하기 때문입니다.

자식 클래스에서도 마찬가지로 @DiscriminatorValue를 모두 제거하여도 테스트에 성공하고 이 때 값은 Entity 이름과 동일하게 기본 값이 설정됩니다.

TABLE_PER_CLASS 전략

마지막으로 TABLE_PER_CLASS로 설정하여 테이블이 어떻게 생성되는지 확인해보겠습니다.


// 생략
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Product {
  // 생략
}
Hibernate: 

    create table product (
       id bigint not null,
        name varchar(255),
        price integer,
        primary key (id)
    )
Hibernate: 

    create table song (
       id bigint not null,
        name varchar(255),
        price integer,
        singer varchar(255),
        writer varchar(255),
        primary key (id)
    )
Hibernate: 

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

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

테이블이 생성될 때 product의 속성인 name, price가 모든 서브 타입에 추가된 것을 확인할 수 있습니다.

테스트를 실행해보면,

Hibernate: 
    insert 
    into
        song
        (name, price, singer, writer, id) 
    values
        (?, ?, ?, ?, ?)
2022-07-02 02:26:15.882 TRACE 31121 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [너의 의미]
2022-07-02 02:26:15.885 TRACE 31121 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [500]
2022-07-02 02:26:15.886 TRACE 31121 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [IU]
2022-07-02 02:26:15.886 TRACE 31121 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [VARCHAR] - [IU]
2022-07-02 02:26:15.887 TRACE 31121 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [5] as [BIGINT] - [1]
Hibernate: 
    select
        song0_.id as id1_9_,
        song0_.name as name2_9_,
        song0_.price as price3_9_,
        song0_.singer as singer1_10_,
        song0_.writer as writer2_10_ 
    from
        song song0_
Song(super=Product(id=1, name=너의 의미, price=500), singer=IU, writer=IU)

이렇게 song 테이블에만 쿼리가 발생한 것을 확인할 수 있습니다.

이렇게되면 product 테이블은 사용하지 않지만 생성되게 되는데요, Product 클래스를 abstract로 수정하게되면 테이블은 생성하지 않고 추상 객체로만 사용 가능합니다.

@DiscriminatorColumn은 테이블 자체가 다르기 때문에 적용할 필요가 없고, 실제로 애너테이션을 추가하더라도 적용되지 않습니다.

여기까지만 보면 아무런 문제가 없어보이는데요, 객체지향적으로 추상클래스만 필요한 상황이 되었다고 가정하고 테스트를 수정해보겠습니다.

package com.tistory.jaimenote.jpa.inheritance.infra.repository;

import com.tistory.jaimenote.jpa.inheritance.entity.Product;
import com.tistory.jaimenote.jpa.inheritance.entity.Song;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.annotation.Rollback;

@DataJpaTest
class ProductRepositoryTest {

  @Autowired
  ProductRepository productRepository;

  @PersistenceContext
  EntityManager entityManager;

  private Song song;

  @BeforeEach
  void setup() {
    song = Song.create("IU", "IU", "너의 의미", 500);
    productRepository.save(song);
    entityManager.flush();
    entityManager.clear();
  }

  @Test
  @Rollback(false)
  void findProductTest() {
    Product product = productRepository.findById(song.getId())
            .orElseThrow(() -> new IllegalArgumentException("잘못된 ID 입니다."));
    System.out.println(product);
  }
}

SongProduct의 자식 클래스이기 때문에 ProductRepository를 이용해 Song을 저장하였습니다.

앞으로 MovieBook도 같은 인터페이스를 이용해 저장할 생각에 벌써 싱글벙글 해집니다.

조회할 때도 마찬가지로 ID만 넣어주면 알아서 착착 찾아주겠죠?

테스트를 실행해보겠습니다.

Hibernate: 
    insert 
    into
        song
        (name, price, singer, writer, id) 
    values
        (?, ?, ?, ?, ?)
2022-07-02 02:39:34.869 TRACE 32209 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [너의 의미]
2022-07-02 02:39:34.871 TRACE 32209 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [500]
2022-07-02 02:39:34.872 TRACE 32209 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [IU]
2022-07-02 02:39:34.872 TRACE 32209 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [VARCHAR] - [IU]
2022-07-02 02:39:34.873 TRACE 32209 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [5] as [BIGINT] - [1]
Hibernate: 
    select
        product0_.id as id1_9_0_,
        product0_.name as name2_9_0_,
        product0_.price as price3_9_0_,
        product0_.author as author1_0_0_,
        product0_.isbn as isbn2_0_0_,
        product0_.actor as actor1_6_0_,
        product0_.director as director2_6_0_,
        product0_.singer as singer1_10_0_,
        product0_.writer as writer2_10_0_,
        product0_.clazz_ as clazz_0_ 
    from
        ( select
            id,
            name,
            price,
            null as author,
            null as isbn,
            null as actor,
            null as director,
            null as singer,
            null as writer,
            0 as clazz_ 
        from
            product 
        union
        all select
            id,
            name,
            price,
            author,
            isbn,
            null as actor,
            null as director,
            null as singer,
            null as writer,
            1 as clazz_ 
        from
            book 
        union
        all select
            id,
            name,
            price,
            null as author,
            null as isbn,
            actor,
            director,
            null as singer,
            null as writer,
            2 as clazz_ 
        from
            movie 
        union
        all select
            id,
            name,
            price,
            null as author,
            null as isbn,
            null as actor,
            null as director,
            singer,
            writer,
            3 as clazz_ 
        from
            song 
    ) product0_ 
where
    product0_.id=?

insert 문은 예상대로 동작하였지만 select 문이 대환장파티가 되었습니다.

어떻게 보면 가장 객체지향 적인 구조이지만 DB 테이블과는 맞지 않다고 할 수 있습니다.

결론

객체 상속 관계를 테이블과 매핑시키기 위해서는 @Inheritance 애너테이션을 사용하고 attribute로 상속 전략을 설정할 수 있습니다.

각각의 전략은 장단점이 존재하므로 상황에 맞게 필요한 전략을 선택해서 사용하면 되지만, TABLE_PER_CLASS 전략은 절대 추천하지 않습니다.

데이터의 수가 많지 않을 때는 SINGLE_TABLE 전략이, 데이터가 많고 무결해야 할 경우 JOINED 전략이 유리할 수 있습니다.

댓글