본문 바로가기

Spring/JPA, Hibernate

Entity Inheritance in Hibernate - (3) Table per Class, Joined 상속 전략

반응형

Table Per Class 전략

Table-Per-Class 상속 전략을 사용할 때 Database에 생성되는 Table들은 이전 포스팅에서 소개했던 MappedSuperClass의 경우와 유사하다. 하지만 MappedSuperClass 전략을 사용할 때와는 달리 부모 Class가 다른 Entity Class와 Relation을 가지는 것을 지원하며, 이런 Relation이 Child Entity Class에 상속된다는 점이 다르다. 개념적으로 도식화 하면 <pic 1>과 같다. 부모 Class인 Product class와 Beverage Class가 Table로 생성되지는 않지만, 그 Class에서 정의된 attribute들은 모두 Child Entity Class에 상속되게 된다.

 

<pic 1> Table-Per-Class 상속 전략 개념도

구현

<code 1>과 같이 앞선 포스팅에서 부모 Class에 사용된 @Inheritance annotation의 값을 SINGLE에서 TABLE_PER_CLASS로 수정하고 SINGLE Table 전략에서만 사용하는 @DiscriminatorColumn annotation을 삭제하면 구현은 끝난다. (Basket entity class와의 Relation도 포함된 상태이다.)

@Getter @Setter
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
//@DiscriminatorColumn(name="Product_Type")
public abstract class Product {
...
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "basket_id")
	private Basket basket;

}

...

@Getter @Setter
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public abstract class Beverage extends Product {
...
}
<code 1> Table per Class 전략에서 최상위 부모 Class 수정

 

그리고 <code 2>와 같이 Child Class에서 사용한 @DiscriminatorValue annotation을 제거한다.

...
@Entity(name="Alcohol")
//@DiscriminatorValue("AlcoholBeverage")
public class Alcohol extends Beverage {
...

@Entity(name="BulkProduct")
//@DiscriminatorValue("BulkProduct")
public class Bulk extends Product {
...

@Entity(name="NonAlcohol")
//@DiscriminatorValue("NonAlcoholBeverage")
public class NonAlcohol extends Beverage {
...

@Entity(name="SingleProduct")
//@DiscriminatorValue("SingleProduct")
public class Single extends Product {
...
<code 2> Table per Class 전략에서 Child Class 수정

 

위의 수정을 끝내면 Table-Per-Class 상속 전략을 위한 구현은 끝난다. 

생성된 Database Table Review

위에서 수정한 Code를 실행시키면 Hibernate Library는 아래와 같이 Table Create Query를 생성해서 요청한다.

Hibernate: 

	create table alcohol (
       id int8 not null,
        manufacturer varchar(255),
        name varchar(255),
        price int4 not null,
        basket_id int8,
        type varchar(255),
        volume int4 not null,
        abv float4 not null,
        primary key (id)
    )
Hibernate: 
    
    create table basket (
       id int8 not null,
        user_name varchar(255),
        primary key (id)
    )
Hibernate: 
    
    create table bulk_product (
       id int8 not null,
        manufacturer varchar(255),
        name varchar(255),
        price int4 not null,
        basket_id int8,
        product_count int4 not null,
        primary key (id)
    )
Hibernate: 
    
    create table non_alcohol (
       id int8 not null,
        manufacturer varchar(255),
        name varchar(255),
        price int4 not null,
        basket_id int8,
        type varchar(255),
        volume int4 not null,
        sugar_content float4 not null,
        primary key (id)
    )
Hibernate: 
    
    create table single_product (
       id int8 not null,
        manufacturer varchar(255),
        name varchar(255),
        price int4 not null,
        basket_id int8,
        weight int4 not null,
        primary key (id)
    )
Hibernate: 
    
    alter table if exists alcohol 
       add constraint FK_m3kxg32vc7i86ucwqme40lk9y 
       foreign key (basket_id) 
       references basket
Hibernate: 
    
    alter table if exists bulk_product 
       add constraint FK_6qrngqmnvycwq5rtao5xyqjh1 
       foreign key (basket_id) 
       references basket
Hibernate: 
    
    alter table if exists non_alcohol 
       add constraint FK_43rhmx91aocm1f7jglvb88jdw 
       foreign key (basket_id) 
       references basket
Hibernate: 
    
    alter table if exists single_product 
       add constraint FK_194uu2qqoy205lhxkum27nn92 
       foreign key (basket_id) 
       references basket

위의 Query 결과 Database(PostgreSQL)에 생성된 Table들은 아래 '생성된 Table들'을 펼쳐서 확인할 수 있다. MappedSuperClass 전략과 동일하게 <code 1>에서 abstract class를 제외한 Entity Class만 Table로 생성된 것을 확인할 수 있다.

 

생성된 Table들
                    릴레이션(relation) 목록
 스키마 |      이름      |  종류  |   소유주   |  크기   | 설명
--------+----------------+--------+------------+---------+------
 public | alcohol        | 테이블 | browndwarf | 16 kB   |
 public | basket         | 테이블 | browndwarf | 0 bytes |
 public | bulk_product   | 테이블 | browndwarf | 16 kB   |
 public | non_alcohol    | 테이블 | browndwarf | 16 kB   |
 public | single_product | 테이블 | browndwarf | 16 kB   |
(5개 행)

 

하지만 MappedSuperClass과 상이한 점은 최상위 부모 class에서 선언된 다른 entity class와의 Relation이 모든 Child Entity Class에 적용되서 실제 Table에 Foregin Key로 설정된다는 점이 다르다. 좀 더 자세하게 비교하기 위해 alcohol table 생성 Query를 자세히 비교하면 <Table 1>과 같다.

Mapped Super Class 상속 전략 Table per class 상속 전략
create table alcohol (
    id bigint not null,
    manufacturer varchar(255),
    name varchar(255),
    price integer not null,
    type varchar(255),
    volume integer not null,
    abv float not null,
    primary key (id) )
createtable alcohol (
    id int8 not null,
    manufacturer varchar(255),
    name varchar(255),
    price int4 not null,
    basket_id int8,
    type varchar(255),
    volume int4 not null,
    abv float4 not null,
    primary key (id)
)

alter table if exists alcohol
    add constraint FK_m3kxg32vc7i86ucwqme40lk9y foreign key (basket_id)
references basket
<table 1> 상속 전략에 따른 DDL 차이

 

<Table 1>에서 보는 바와 같이 MappedSuperClass 상속 전략을 사용할 시에 포함되지 않았던, basket_id column이 Table-per-Class 상속 추가되어 있고 basket table에 있는 Column을 Foregien Key로 설정한 것을 확인할 수 있다.

Data Insert Review

각 Table에 Data를 Insert할 때 Hibernate Library가 생성하는 Insert query는 아래와 같다. 실제 생성되는 Child Entity Class의 Schema에 따라 Insert될 때 Data들이 조금씩 변경되지만, 모두 공통적으로 부모 Class인 Product Class가 가진 attribute들을 포함하고 있다.

Hibernate: insert into single_product(basket_id, manufacturer, name, price, weight, id) values (?, ?, ?, ?, ?, ?)
Hibernate: insert into bulk_product (basket_id, manufacturer, name, price, product_count, id) values (?, ?, ?, ?, ?, ?)
Hibernate: insert into alcohol(basket_id, manufacturer, name, price, type, volume, abv, id) values (?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into non_alcohol(basket_id, manufacturer, name, price, type, volume, sugar_content, id) values (?, ?, ?, ?, ?, ?, ?, ?)

그리고 실제 생성된 Data는 아래 <table 2> 와 같다. 모든 Child Table은 MappedSuperClass 전략에서 만들어진 Table들과 유사하지만, 자기가 담긴 Basket을 가르키기 위해 basket_id column을 Foriegn Key로 가진다는 점이 상이하다는 것을 알 수 있다. (아직 Relation을 가지는 Basket이 없기 때문에 Foreign Key인 basket_id는 모두 Null 값이다.)

 

id manufacturer name price basket_id weight
1 Moon SingleEx 1000   10
'single_product' Table
id manufacturer name price basket_id product_count
2 Mars Bulk 2000   5
'bulk_product' Table
id manufacturer name price basket_id type volume abv
3 Tiger Tiger 2500   Beer 360 3.6
'alcohol' Table
id manufacturer name price basket_id type volume sugar_content
4 Bingrae Morning Juice 1000   Juice 180 2
'non_alcohol' Table
<Table 2> Single Table 전략 사용시 초기화된 Table

 

Relation을 가지는 Data 추가

앞선 포스팅들과 동일하게 John이 낱개 상품(Single)과 Tiger 맥주를 장바구니에 담고, Jane이 묶음 상품(Bulk)와 Juice를 장바구니에 담는 시나리오를 수행했을 때 Hibernate Library에서 아해와 같은 Query를 DB에 요청한다.

 

John의 쇼핑에 의해 발생하는 Query

Hibernate: select product0_.id as id1_5_0_, product0_.basket_id as basket_i5_5_0_, product0_.manufacturer as manufact2_5_0_, product0_.name as name3_5_0_, product0_.price as price4_5_0_, product0_.type as type1_2_0_, product0_.volume as volume2_2_0_, product0_.abv as abv1_0_0_, product0_.product_count as product_1_3_0_, product0_.sugar_content as sugar_co1_4_0_, product0_.weight as weight1_6_0_, product0_.clazz_ as clazz_0_ 
		from ( select id, manufacturer, name, price, basket_id, type, volume, abv, null::int4 as product_count, null::float4 as sugar_content, null::int4 as weight, 
				2 as clazz_ 
			from alcohol 
			union all 
			select id, manufacturer, name, price, basket_id, type, volume, null::float4 as abv, null::int4 as product_count, sugar_content, null::int4 as weight, 
				4 as clazz_ 
			from non_alcohol 
			union all 
			select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, product_count, null::float4 as sugar_content, null::int4 as weight, 
				3 as clazz_ 
			from bulk_product
			union all 
			select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, null::int4 as product_count, null::float4 as sugar_content, weight, 
				5 as clazz_ 
			from single_product 
		) product0_ 
		where product0_.id=?
Hibernate: select product0_.id as id1_5_0_, product0_.basket_id as basket_i5_5_0_, product0_.manufacturer as manufact2_5_0_, product0_.name as name3_5_0_, product0_.price as price4_5_0_, product0_.type as type1_2_0_, product0_.volume as volume2_2_0_, product0_.abv as abv1_0_0_, product0_.product_count as product_1_3_0_, product0_.sugar_content as sugar_co1_4_0_, product0_.weight as weight1_6_0_, product0_.clazz_ as clazz_0_ 
		from ( select id, manufacturer, name, price, basket_id, type, volume, abv, null::int4 as product_count, null::float4 as sugar_content, null::int4 as weight, 
				2 as clazz_ 
			from alcohol 
			union all 
			select id, manufacturer, name, price, basket_id, type, volume, null::float4 as abv, null::int4 as product_count, sugar_content, null::int4 as weight, 
				4 as clazz_ 
			from non_alcohol 
			union all 
			select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, product_count, null::float4 as sugar_content, null::int4 as weight, 
				3 as clazz_ 
			from bulk_product 
			union all 
			select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, null::int4 as product_count, null::float4 as sugar_content, weight, 
				5 as clazz_ 
			from single_product 
		) product0_ 
		where product0_.id=?
Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into basket (user_name, id) values (?, ?)
Hibernate: update single_product set basket_id=?, manufacturer=?, name=?, price=?, weight=? where id=?
Hibernate: update alcohol set basket_id=?, manufacturer=?, name=?, price=?, type=?, volume=?, abv=? where id=?

 

Jane의 쇼핑에 의해 발생하는 Query

Hibernate: select product0_.id as id1_5_0_, product0_.basket_id as basket_i5_5_0_, product0_.manufacturer as manufact2_5_0_, product0_.name as name3_5_0_, product0_.price as price4_5_0_, product0_.type as type1_2_0_, product0_.volume as volume2_2_0_, product0_.abv as abv1_0_0_, product0_.product_count as product_1_3_0_, product0_.sugar_content as sugar_co1_4_0_, product0_.weight as weight1_6_0_, product0_.clazz_ as clazz_0_ 
		from ( select id, manufacturer, name, price, basket_id, type, volume, abv, null::int4 as product_count, null::float4 as sugar_content, null::int4 as weight, 
				2 as clazz_ 
			from alcohol 
			union all 
			select id, manufacturer, name, price, basket_id, type, volume, null::float4 as abv, null::int4 as product_count, sugar_content, null::int4 as weight, 
				4 as clazz_ 
			from non_alcohol 
			union all 
			select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, product_count, null::float4 as sugar_content, null::int4 as weight, 
				3 as clazz_ 
			from bulk_product 
			union all 
			select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, null::int4 as product_count, null::float4 as sugar_content, weight, 
				5 as clazz_ 
			from single_product 
		) product0_ 
		where product0_.id=?
Hibernate: select product0_.id as id1_5_0_, product0_.basket_id as basket_i5_5_0_, product0_.manufacturer as manufact2_5_0_, product0_.name as name3_5_0_, product0_.price as price4_5_0_, product0_.type as type1_2_0_, product0_.volume as volume2_2_0_, product0_.abv as abv1_0_0_, product0_.product_count as product_1_3_0_, product0_.sugar_content as sugar_co1_4_0_, product0_.weight as weight1_6_0_, product0_.clazz_ as clazz_0_ 
		from ( select id, manufacturer, name, price, basket_id, type, volume, abv, null::int4 as product_count, null::float4 as sugar_content, null::int4 as weight, 
				2 as clazz_ 
			from alcohol 
			union all 
			select id, manufacturer, name, price, basket_id, type, volume, null::float4 as abv, null::int4 as product_count, sugar_content, null::int4 as weight, 
				4 as clazz_ 
			from non_alcohol 
			union all 
			select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, product_count, null::float4 as sugar_content, null::int4 as weight, 
				3 as clazz_ 
			from bulk_product 
			union all 
			select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, null::int4 as product_count, null::float4 as sugar_content, weight, 
				5 as clazz_ 
			from single_product 
		) product0_ 
		where product0_.id=?
Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into basket (user_name, id) values (?, ?)
Hibernate: update bulk_product set basket_id=?, manufacturer=?, name=?, price=?, product_count=? where id=?
Hibernate: update non_alcohol set basket_id=?, manufacturer=?, name=?, price=?, type=?, volume=?, sugar_content=? where id=?

 

생성된 Query들 중 처음 두 Line을 보면 전체 Child Entity Class를 'union all'로 전체 Product에 대한 임시 View를 만든 후에 'where product0_.id=' 구문으로 Filtering을 한다.  그리고 Hibernate Library내부에서는 이 결과로 조회된 상품이 각각 어떤 Entity Class와 Mapping되는지를 확인하고, 추가된 basket의 id를 앞에서 구해진 Child Entity Class와 Mapping되는 Table의 basket_id column의 Data로 Update 한다.

 

Result

 

최종에 Database Table들에는 <Table 2> 와 같이 Data들이 Update된다. 

id manufacturer name price basket_id weight
1 Moon SingleEx 1000 5 10
'single_product' Table
id manufacturer name price basket_id product_count
2 Mars Bulk 2000 6 5
'bulk_product' Table
id manufacturer name price basket_id type volume abv
3 Tiger Tiger 2500 5 Beer 360 3.6
'alcohol' Table
id manufacturer name price basket_id type volume sugar_content
4 Bingrae Morning Juice 1000 6 Juice 180 2
'non_alcohol' Table
id user_name
5 John
6 Jane
'basket' Table
<Table 3> Basket Table과 Relation이 생긴 상태

 

Polymorphic Queries

앞선 포스팅의 예와 같이 전체 장바구니(Basket)에 어떤 물건(Product)들이 담겨있는 지 확인하는 Process Flow를 적용하면, 이 과정에서 Hibernate Library는 부모 Class인 Product List를 구하기 위해 Polymorphic Query를 생성한다. 처음에는 Basket List를 조회하는 Query를 보내고, 이 Basket에 포함되는 Product List를 조회하기 위해 전체 Child Entity Class가 'union all'로 합치고 basket_id로 Filtering 하는 Query를 생성한다.

Hibernate: select basket0_.id as id1_1_, basket0_.user_name as user_nam2_1_ from basket basket0_
Hibernate: select productlis0_.basket_id as basket_i5_5_0_, productlis0_.id as id1_5_0_, productlis0_.id as id1_5_1_, productlis0_.basket_id as basket_i5_5_1_, productlis0_.manufacturer as manufact2_5_1_, productlis0_.name as name3_5_1_, productlis0_.price as price4_5_1_, productlis0_.type as type1_2_1_, productlis0_.volume as volume2_2_1_, productlis0_.abv as abv1_0_1_, productlis0_.product_count as product_1_3_1_, productlis0_.sugar_content as sugar_co1_4_1_, productlis0_.weight as weight1_6_1_, productlis0_.clazz_ as clazz_1_ 
	from ( 
		select id, manufacturer, name, price, basket_id, type, volume, abv, null::int4 as product_count, null::float4 as sugar_content, null::int4 as weight, 
			2 as clazz_ 
		from alcohol 
		union all 
		select id, manufacturer, name, price, basket_id, type, volume, null::float4 as abv, null::int4 as product_count, sugar_content, null::int4 as weight, 
			4 as clazz_ 
		from non_alcohol 
		union all 
		select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, product_count, null::float4 as sugar_content, null::int4 as weight, 
			3 as clazz_ 
		from bulk_product 
		union all 
		select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, null::int4 as product_count, null::float4 as sugar_content, weight, 
			5 as clazz_ 
		from single_product 
	) productlis0_ 
	where productlis0_.basket_id=?
Hibernate: select productlis0_.basket_id as basket_i5_5_0_, productlis0_.id as id1_5_0_, productlis0_.id as id1_5_1_, productlis0_.basket_id as basket_i5_5_1_, productlis0_.manufacturer as manufact2_5_1_, productlis0_.name as name3_5_1_, productlis0_.price as price4_5_1_, productlis0_.type as type1_2_1_, productlis0_.volume as volume2_2_1_, productlis0_.abv as abv1_0_1_, productlis0_.product_count as product_1_3_1_, productlis0_.sugar_content as sugar_co1_4_1_, productlis0_.weight as weight1_6_1_, productlis0_.clazz_ as clazz_1_ 
	from ( select id, manufacturer, name, price, basket_id, type, volume, abv, null::int4 as product_count, null::float4 as sugar_content, null::int4 as weight, 
			2 as clazz_ 
		from alcohol 
		union all 
		select id, manufacturer, name, price, basket_id, type, volume, null::float4 as abv, null::int4 as product_count, sugar_content, null::int4 as weight, 
			4 as clazz_ 
		from non_alcohol 
		union all 
		select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, product_count, null::float4 as sugar_content, null::int4 as weight, 
			3 as clazz_ 
		from bulk_product 
		union all 
		select id, manufacturer, name, price, basket_id, null::varchar as type, null::int4 as volume, null::float4 as abv, null::int4 as product_count, null::float4 as sugar_content, weight, 
			5 as clazz_ 
		from single_product 
	) productlis0_ 
	where productlis0_.basket_id=?

Remark

Table-Per-Class 상속 전략을 사용할 때, 위의 예와 같이 부모 Class Data를 조회하는 경우에 모든 Child Class와 Mapping되는 Table들을 'union all'로 합친 후에 조건에 맞는 Data를 Filtering 하게 되는데 이 과정에서 union all에 의한 성능 저하는 불가피 하다. 

JOINED 상속 전략

마지막으로 살펴볼 상속전략은 JOINED 상속전략이다. 이 전략을 사용할 때 생성되는 Table 구조는 대략 <pic 2> 와 같이 부모 Class와 Child Entity Class 모두 Mapping되는 Table이 만들어진다. 즉, 최상위 부모 Class인 Product Class와 중간에 위치한 부모 Class인 Beverage Class 모두 Mapping되는 Table이 만들어진다.

 

<pic 2> Joined 상속 전략을 사용할 때 생성된 Table들의 관계도

Java Code로 정의한 모든 Class들이 Table로 생성되기 때문에 DB쪽 지식이 많지 않은 경우에는 Databse의 Table과 Java의 Class 관계를 이해하기 가장 쉬울 수 있다.

 

구현

Table-Per-Class 상속 전략과 유사하게 각 부모 Class에 @Inhritance annoation만 <code 2>와 같이 InheritanceType.JOINED로 정의하면 구현은 끝난다다. (Child class에 @PrimaryKeyJoinColumn annotation을 추가할 수도 있지만 이 포스팅에서는 사용하지 않았다.)

@Getter @Setter
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
public abstract class Product {

...
}

@Getter @Setter
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
public abstract class Beverage extends Product {

...
}
<code 2> JOINED 상속 전략 구현

생성된 Database Table Review

위에서 수정한 Code를 실행시키면 Hibernate Library는 아래와 같이 Table Create Query를 생성해서 요청한다.

Hibernate: 
    
    create table alcohol (
       abv float4 not null,
        id int8 not null,
        primary key (id)
    )
Hibernate: 
    
    create table basket (
       id int8 not null,
        user_name varchar(255),
        primary key (id)
    )
Hibernate: 
    
    create table beverage (
       type varchar(255),
        volume int4 not null,
        id int8 not null,
        primary key (id)
    )
Hibernate: 
    
    create table bulk_product (
       product_count int4 not null,
        id int8 not null,
        primary key (id)
    )
Hibernate: 
    
    create table non_alcohol (
       sugar_content float4 not null,
        id int8 not null,
        primary key (id)
    )
Hibernate: 
    
    create table product (
       id int8 not null,
        manufacturer varchar(255),
        name varchar(255),
        price int4 not null,
        basket_id int8,
        primary key (id)
    )
Hibernate: 
    
    create table single_product (
       weight int4 not null,
        id int8 not null,
        primary key (id)
    )
Hibernate: 
    
    alter table if exists alcohol 
       add constraint FKov2vnwilqq6ocfp8qlivki1g0 
       foreign key (id) 
       references beverage
Hibernate: 
    
    alter table if exists beverage 
       add constraint FKon1jxpyfflm1dxexjwvd4kidp 
       foreign key (id) 
       references product
Hibernate: 
    
    alter table if exists bulk_product 
       add constraint FKpx18m28gp1uit7iomyly1l9om 
       foreign key (id) 
       references product
Hibernate: 
    
    alter table if exists non_alcohol 
       add constraint FKmbcsflos5u45s494o4gnk3kmu 
       foreign key (id) 
       references beverage
Hibernate: 
    
    alter table if exists product 
       add constraint FKmx1cy9qt0buugec33aoy4ro4a 
       foreign key (basket_id) 
       references basket
Hibernate: 
    
    alter table if exists single_product 
       add constraint FKqauhgq8wawqoqef5mvwfh3s9f 
       foreign key (id) 
       references product

위의 Query에 의해 실제 생성된 Table List는 아래 '생성된 Table들'을 펼쳐서 확인할 수 있다. 앞서 설명한대로 부모 Entity Class와 Child Entity Class 모두 실제 Table로 생성되고, 최상위 부모 Class인 Product class의 PK로 사용되는 id column이 모든 Child Entity Class와 Mapping되는 Table에 추가된다.

 

생성된 Table들
db1=# \dt+
                      릴레이션(relation) 목록
 스키마 |      이름      |  종류  |   소유주   |    크기    | 설명
--------+----------------+--------+------------+------------+------
 public | alcohol        | 테이블 | browndwarf | 8192 bytes |
 public | basket         | 테이블 | browndwarf | 0 bytes    |
 public | beverage       | 테이블 | browndwarf | 8192 bytes |
 public | bulk_product   | 테이블 | browndwarf | 8192 bytes |
 public | non_alcohol    | 테이블 | browndwarf | 8192 bytes |
 public | product        | 테이블 | browndwarf | 16 kB      |
 public | single_product | 테이블 | browndwarf | 8192 bytes |
(7개 행)

Data Insert Review

기존과 동일한 Code로 각 Child Entity의 Data를 추가하면 Hibernate Library는 아래와 같은 Insert query를 생성해서 Request한다. 각 상품별로 자신의 부모 Class에 해당하는 Data를 Insert 하고 Child Class의 Specific attribute data를 Insert하는 것을 확인할 수 있다.

Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into product (basket_id, manufacturer, name, price, id) values (?, ?, ?, ?, ?)
Hibernate: insert into single_product (weight, id) values (?, ?)
Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into product (basket_id, manufacturer, name, price, id) values (?, ?, ?, ?, ?)
Hibernate: insert into bulk_product (product_count, id) values (?, ?)
Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into product (basket_id, manufacturer, name, price, id) values (?, ?, ?, ?, ?)
Hibernate: insert into beverage (type, volume, id) values (?, ?, ?)
Hibernate: insert into alcohol (abv, id) values (?, ?)
Hibernate: select nextval ('hibernate_sequence')
// Juicy data를 추가할 때 상위에 있는 product와 beverage table에 data를 먼저 insert한 후에 non_alcohol table에 data를 Insert한다.
Hibernate: insert into product (basket_id, manufacturer, name, price, id) values (?, ?, ?, ?, ?)
Hibernate: insert into beverage (type, volume, id) values (?, ?, ?)
Hibernate: insert into non_alcohol (sugar_content, id) values (?, ?)

 

위 결과가 <Table 4>와 같다.

최상위 부모 Class 중간 부모 Class Child Class

'product' table

id manufacturer name price basket_id
1 Moon SingleEx 1000  
2 Mars Bulk 2000  
3 Tiger Tiger 2500  
4 Bingrae Morning Juice 1000  
-

'single_product' table

id weight
1 10

'bulk_product' table

id product_count
2 5

'beverage' table

id type volume
3 Beer 360
4 Juice 180

'alcohol' table

id abv
3 3.6

'non_alcohol' table

id sugar_content
4 2
<Table 4> Joined방식 으로 생성된 Table들의 초기 상태

 

Relation을 가지는 Data 추가

이전과 동일하게 John과 Jane이 장바구니에 상품을 담는 시나리오를 수행했을 때, Hibernate Library에서 발생시킨 Query는 아래와 같다.

 

John의 쇼핑에 의해 발생하는 Query

Hibernate: select product0_.id as id1_5_0_, product0_.basket_id as basket_i5_5_0_, product0_.manufacturer as manufact2_5_0_, product0_.name as name3_5_0_, product0_.price as price4_5_0_, product0_1_.type as type1_2_0_, product0_1_.volume as volume2_2_0_, product0_2_.abv as abv1_0_0_, product0_3_.product_count as product_1_3_0_, product0_4_.sugar_content as sugar_co1_4_0_, product0_5_.weight as weight1_6_0_, 
		// Class Type 정보를 위해 아래 Query가 포함
		case when product0_2_.id is not null then 2 
			when product0_4_.id is not null then 4 
			when product0_1_.id is not null then 1 
			when product0_3_.id is not null then 3 
			when product0_5_.id is not null then 5 
			when product0_.id is not null then 0 end 
		as clazz_0_ 
	from product product0_ 
	left outer join beverage product0_1_ on product0_.id=product0_1_.id 
	left outer join alcohol product0_2_ on product0_.id=product0_2_.id 
	left outer join bulk_product product0_3_ on product0_.id=product0_3_.id 
	left outer join non_alcohol product0_4_ on product0_.id=product0_4_.id 
	left outer join single_product product0_5_ on product0_.id=product0_5_.id 
	where product0_.id=?
Hibernate: select product0_.id as id1_5_0_, product0_.basket_id as basket_i5_5_0_, product0_.manufacturer as manufact2_5_0_, product0_.name as name3_5_0_, product0_.price as price4_5_0_, product0_1_.type as type1_2_0_, product0_1_.volume as volume2_2_0_, product0_2_.abv as abv1_0_0_, product0_3_.product_count as product_1_3_0_, product0_4_.sugar_content as sugar_co1_4_0_, product0_5_.weight as weight1_6_0_, 
		case when product0_2_.id is not null then 2 
			when product0_4_.id is not null then 4 
			when product0_1_.id is not null then 1 
			when product0_3_.id is not null then 3 
			when product0_5_.id is not null then 5 
			when product0_.id is not null then 0 end 
		as clazz_0_ 
	from product product0_ 
	left outer join beverage product0_1_ on product0_.id=product0_1_.id 
	left outer join alcohol product0_2_ on product0_.id=product0_2_.id 
	left outer join bulk_product product0_3_ on product0_.id=product0_3_.id 
	left outer join non_alcohol product0_4_ on product0_.id=product0_4_.id 
	left outer join single_product product0_5_ on product0_.id=product0_5_.id 
	where product0_.id=?
Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into basket (user_name, id) values (?, ?)
Hibernate: update product set basket_id=?, manufacturer=?, name=?, price=? where id=?
Hibernate: update product set basket_id=?, manufacturer=?, name=?, price=? where id=?

 

Jane의 쇼핑에 의해 발생하는 Query

Hibernate: select product0_.id as id1_5_0_, product0_.basket_id as basket_i5_5_0_, product0_.manufacturer as manufact2_5_0_, product0_.name as name3_5_0_, product0_.price as price4_5_0_, product0_1_.type as type1_2_0_, product0_1_.volume as volume2_2_0_, product0_2_.abv as abv1_0_0_, product0_3_.product_count as product_1_3_0_, product0_4_.sugar_content as sugar_co1_4_0_, product0_5_.weight as weight1_6_0_, 
		case when product0_2_.id is not null then 2 
			when product0_4_.id is not null then 4 
			when product0_1_.id is not null then 1 
			when product0_3_.id is not null then 3 
			when product0_5_.id is not null then 5 
			when product0_.id is not null then 0 end 
		as clazz_0_ 
	from product product0_ 
	left outer join beverage product0_1_ on product0_.id=product0_1_.id 
	left outer join alcohol product0_2_ on product0_.id=product0_2_.id 
	left outer join bulk_product product0_3_ on product0_.id=product0_3_.id 
	left outer join non_alcohol product0_4_ on product0_.id=product0_4_.id 
	left outer join single_product product0_5_ on product0_.id=product0_5_.id 
	where product0_.id=?
Hibernate: select product0_.id as id1_5_0_, product0_.basket_id as basket_i5_5_0_, product0_.manufacturer as manufact2_5_0_, product0_.name as name3_5_0_, product0_.price as price4_5_0_, product0_1_.type as type1_2_0_, product0_1_.volume as volume2_2_0_, product0_2_.abv as abv1_0_0_, product0_3_.product_count as product_1_3_0_, product0_4_.sugar_content as sugar_co1_4_0_, product0_5_.weight as weight1_6_0_, 
		case when product0_2_.id is not null then 2 
			when product0_4_.id is not null then 4 
			when product0_1_.id is not null then 1 
			when product0_3_.id is not null then 3 
			when product0_5_.id is not null then 5 
			when product0_.id is not null then 0 end 
		as clazz_0_ 
	from product product0_ 
	left outer join beverage product0_1_ on product0_.id=product0_1_.id 
	left outer join alcohol product0_2_ on product0_.id=product0_2_.id 
	left outer join bulk_product product0_3_ on product0_.id=product0_3_.id 
	left outer join non_alcohol product0_4_ on product0_.id=product0_4_.id 
	left outer join single_product product0_5_ on product0_.id=product0_5_.id 
	where product0_.id=?
Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into basket (user_name, id) values (?, ?)
Hibernate: update product set basket_id=?, manufacturer=?, name=?, price=? where id=?
Hibernate: update product set basket_id=?, manufacturer=?, name=?, price=? where id=?

John과 Jane이 각각 2개의 물건을 구입했는데, 이 물건이 어느 Class와 Mapping되는 지 확인하기 위해 모든 Child Class을 left outer join으로 묶은 후에 각 Child Class Type에 따라 clazz_0_이라는 임시 Field에 숫자를 부여하는 것을 확인할 수 있다. Hibernate Library에서는 이 값을 이용해 특정 Child Entity Class와 Mapping한다. 그리고 Basket Table과 Relation을 가지는 것은 Product Table 이기 때문에 Product Table의 Data를 Update 하게 된다. 이 과정이 끝나면 아래의 <Table 5>과 같이 Data가 Update 된다. 

 

id manufacturer name price basket_id
1 Moon SingleEx 1000 5
3 Tiger Tiger 2500 5
2 Mars Bulk 2000 6
4 Bingrae Morning Juice 1000 6
'product' Table
id user_name
5 John
6 Jane
'basket' Table
<Table 5> Basket Table과 Relation이 생긴 상태

 

Polymorphic Queries

Data를 조회할 때도 마찬가지 이다. Product Table에 저장된 Data가 어떤 Child Entity Class와 Mapping 되는 지를 알아내기 위해 모든 Child Table을 Left Outer Join한 후에 Child Class Type에 따라 clazz_1_이라는 임시 Field에 숫자를 부여하고 있는 것을 확인할 수 있다.

Hibernate: select basket0_.id as id1_1_, basket0_.user_name as user_nam2_1_ from basket basket0_
Hibernate: select productlis0_.basket_id as basket_i5_5_0_, productlis0_.id as id1_5_0_, productlis0_.id as id1_5_1_, productlis0_.basket_id as basket_i5_5_1_, productlis0_.manufacturer as manufact2_5_1_, productlis0_.name as name3_5_1_, productlis0_.price as price4_5_1_, productlis0_1_.type as type1_2_1_, productlis0_1_.volume as volume2_2_1_, productlis0_2_.abv as abv1_0_1_, productlis0_3_.product_count as product_1_3_1_, productlis0_4_.sugar_content as sugar_co1_4_1_, productlis0_5_.weight as weight1_6_1_, 
		case 
			when productlis0_2_.id is not null then 2 
			when productlis0_4_.id is not null then 4 
			when productlis0_1_.id is not null then 1 
			when productlis0_3_.id is not null then 3 
			when productlis0_5_.id is not null then 5 
			when productlis0_.id is not null then 0 
		end as clazz_1_ 
	from product productlis0_ 
	left outer join beverage productlis0_1_ on productlis0_.id=productlis0_1_.id 
	left outer join alcohol productlis0_2_ on productlis0_.id=productlis0_2_.id 
	left outer join bulk_product productlis0_3_ on productlis0_.id=productlis0_3_.id 
	left outer join non_alcohol productlis0_4_ on productlis0_.id=productlis0_4_.id 
	left outer join single_product productlis0_5_ on productlis0_.id=productlis0_5_.id 
	where productlis0_.basket_id=?
Hibernate: select productlis0_.basket_id as basket_i5_5_0_, productlis0_.id as id1_5_0_, productlis0_.id as id1_5_1_, productlis0_.basket_id as basket_i5_5_1_, productlis0_.manufacturer as manufact2_5_1_, productlis0_.name as name3_5_1_, productlis0_.price as price4_5_1_, productlis0_1_.type as type1_2_1_, productlis0_1_.volume as volume2_2_1_, productlis0_2_.abv as abv1_0_1_, productlis0_3_.product_count as product_1_3_1_, productlis0_4_.sugar_content as sugar_co1_4_1_, productlis0_5_.weight as weight1_6_1_, 
		case 
			when productlis0_2_.id is not null then 2 
			when productlis0_4_.id is not null then 4 
			when productlis0_1_.id is not null then 1 
			when productlis0_3_.id is not null then 3 
			when productlis0_5_.id is not null then 5 
			when productlis0_.id is not null then 0 
		end as clazz_1_ 
	from product productlis0_ 
	left outer join beverage productlis0_1_ on productlis0_.id=productlis0_1_.id 
	left outer join alcohol productlis0_2_ on productlis0_.id=productlis0_2_.id 
	left outer join bulk_product productlis0_3_ on productlis0_.id=productlis0_3_.id 
	left outer join non_alcohol productlis0_4_ on productlis0_.id=productlis0_4_.id 
	left outer join single_product productlis0_5_ on productlis0_.id=productlis0_5_.id 
	where productlis0_.basket_id=?

 

Remark

Joined 상속 전략은 상속 관계에 있는 Entity Class들이 Database Table로 만들어진 결과를 Java Code 관점에서 이해하기 제일 쉽다. 그리고 Single Table 전략과 달리 각 Child Entity Class만의 Specific Attribute들이 Null 값을 가질 수 있다는 점도 Data 관리 측면에서 좋다. 하지만 위에서와 같이 Child Entity Class가 많아지게 되면 Database에 요청되는 Query에 엄청나게 많은 join 문이 포함되게 되는데 이는 Performance 측면에서 매우 취약하다.

 

조언

  • MappedSuperClass 전략은 매우 제한적인 시나리오에서만 사용가능하다. 이 전략이 맞다하더라도 향후 확장성을 고려해야할 경우면 Table-Per-Class 전략을 사용하는 것이 낫다.
  • 다른 Entity Class와 Releation을 이용한 작업들이 많을 것이라 예상되는데 처리 속도가 중요한 경우에는 Single Table 전략이 적합하다. 다만, Child Entity Class에서 추가되는 attribute들의 Data 크기를 최소화 하는데 힘써야 하고, 이 attribute들이 null 값을 가지지 못하는 제약을 명심해야 한다.
  • 빠른 처리보다 Child Entity Class의 data 일관성 처리가 중요하며, 특정 Child Entity Class 기준으로 Data 처리가 빈번한 경우, 또 Child Entity Class가 새롭게 추가되는 경우가 잦으면 Joined 전략이 적합하다
  • Table-Per-Class 전략은 Single Table전략과 Joined 전략의 중간이라고 생각하면 된다. 시나리오 특성이 파악안될 때는 이 전략으로 시도하는 것이 위험부담이 적다.

경험담

JPA/Hibernate에서 Class Inheritance가 어떻게 처리되는 지 몰랐던 꼬꼬마 시절에 예제를 따라 만들다 보니 아무 생각 없이 JOINED 상속 전략을 사용했었다. 여기서 만들어진 Child Table 몇 개에서 조회와 Update가 매우 빈번하게 일어 났는데, 많은 Join 문이 포함되면서 성능이 매우 떨어지는 문제를 야기했었다. 이를 수정하기 위해 Business Logic을 몇 개월 동안 수정하는 일을 했는데, 지나고 나서 생각해보니 그 때 Single Table 전략을 사용했어야 했다. Child Entity Class들의 Specific Field들을 모두 더해도 100 byte 미만이어서 저장 공간에 심각한 영향을 미치지 않을 것 같고, 무엇보다 빠른 처리가 필요했기 때문에 더 적합했을 것 같다. 

 

물론 Joined 전략을 사용해서 좋은 점도 있다. 가끔가다 Child Entity가 추가될 때에는, Database Schema 관리 측면에서 신규 Table을 하나 추가하는 것이 기존 Table에 Column을 하나 추가하는 것보다 용이하기 때문이다. 특히 기존 Table의 크기가 클 때 Column 추가하는 것은 피하고 싶다.