Database 와 Object 간에는 기본적인 패러다임 차이가 존재한다.
Database 관점
슈퍼타입 - 서브타입의 논리 모델이 존재한다.
이를 물리 모델로 구현할 수 있는 여러 전략이 존재한다.
- Joined Table 전략
- Single table 전략
- Table per class 전략
1. Joined Table 전략
슈퍼타입 테이블을 만들고
각각의 모든 서브타입 테이블을 별도로 생성하는 구조.
Item 슈퍼타입 테이블에 별도의 `data_type` 컬럼을 두어 해당 테이블에서 어느 서브타입 테이블을 가리키는지 구분한다.
가장 정규화된 방식으로 저장 공간이 효율화됨.
정석적인 방법으로 JOIN 으로 인해 성능 저하 위험 존재한다고는 하나, 미미함. (JOIN을 잘쓴다면)
부모 - 서브타입 테이블에 대해 INSERT 쿼리를 2번씩 날리게 된다는 것과 테이블 갯수 증가로 복잡도가 증가한다는 것이 단점
Item.java
@Inheritance annotation 에 InheritanceType.JOINED 를 명시한다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Inheritance(strategy = InheritanceType.JOINED)
// Item 만 봐서 어떤 서브타입으로 매핑해야할지 알기 위해서는 필요하다.
// 관례상 기본값인 'dtype'을 사용한다.
@DiscriminatorColumn(name = "data_type")
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "item_id")
private Long id;
@Column(name = "item_name")
private String name;
}
Album.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Album extends Item {
// 상속 받았기 때문에 Item 의 필수 속성들을 명시하지 않아도 테이블에 알아서 매핑된다.
// @Id
// @GeneratedValue
// @JoinColumn(name = "item_id")
// @OneToOne
// private Item item;
@Column(name = "artist_name", length = 63)
private String artistName;
public void changeAlbumInfo(String artistName) {
this.artistName = artistName;
}
}
2. Single table 전략
@DiscriminationColumn 어노테이션을 반드시 사용해야한다.
Item.java
// annotation 만 수정
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "data_type")
public class Item {
// ..
}
쿼리가 단순하고 성능이 올라가는 대신, 공간 낭비 발생이 불가피하다.
공강 낭비가 심하면 쿼리 성능이 되려 저하된다.
모든 서브타입 컬럼이 nullable 해야함.
3. Table per class 전략
쿼리는 단순하고 컬럼에 not null 제약조건을 걸 수 있지만 , 테이블 갯수가 증가할 수록 중복이 발생한다.
Item.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
//DiscriminatorColumn 어노테이션이 필요하지 않다.
// 마찬가지로 Item 또한 추상클래스화 가능하다. (Item 테이블이 생성되지 않는다.)
public abstract class Item {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "item_id")
private Long id;
// ..
}
Book 테이블에 하나의 레코드가 존재한다고 하자.
BookTest.java
@Test
void bookCreateTest() {
initBookData();
var tx = getEntityTransaction();
tx.begin();
try {
// 추상 클래스 Item 으로 캐스팅
Item itemBook = em.find(Item.class, 1L);
System.out.println(itemBook.getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
}
private EntityTransaction getEntityTransaction() {
this.em = emf.createEntityManager();
return em.getTransaction();
}
Book 이 아닌 추상클래스 `Item` 으로 캐스팅하여 조회하면
다음과 같이 괴랄한 UNION ALL 쿼리를 날리게된다.
그도 그럴 것이, JPA와 DB 입장에서는 `ITEM` 타입만 봐서는 어느 테이블에 데이터가 존재하는지 알 수 없기 때문에 모든 서브타입 테이블을 찔러봐야만 한다.
select
i1_0.item_id,
i1_0.clazz_,
i1_0.item_name,
i1_0.price,
i1_0.stock_quantity,
i1_0.artist_name,
i1_0.author_name,
i1_0.isbn,
i1_0.director_name,
i1_0.distributor_name
from
( select
item_id,
item_name,
price,
stock_quantity,
artist_name,
null as director_name,
null as distributor_name,
null as author_name,
null as isbn,
1 as clazz_
from
album
union
all select
item_id,
item_name,
price,
stock_quantity,
null as artist_name,
director_name,
distributor_name,
null as author_name,
null as isbn,
2 as clazz_
from
movie
union
all select
item_id,
item_name,
price,
stock_quantity,
null as artist_name,
null as director_name,
null as distributor_name,
author_name,
isbn,
3 as clazz_
from
book
) i1_0
where
i1_0.item_id=?
Hibernate:
select
i1_0.item_id,
i1_0.clazz_,
i1_0.item_name,
i1_0.price,
i1_0.stock_quantity,
i1_0.artist_name,
i1_0.author_name,
i1_0.isbn,
i1_0.director_name,
i1_0.distributor_name
from
( select
item_id,
item_name,
price,
stock_quantity,
artist_name,
null as director_name,
null as distributor_name,
null as author_name,
null as isbn,
1 as clazz_
from
album
union
all select
item_id,
item_name,
price,
stock_quantity,
null as artist_name,
director_name,
distributor_name,
null as author_name,
null as isbn,
2 as clazz_
from
movie
union
all select
item_id,
item_name,
price,
stock_quantity,
null as artist_name,
null as director_name,
null as distributor_name,
author_name,
isbn,
3 as clazz_
from
book
) i1_0
where
i1_0.item_id=?
TABLE PER CLASS는 그냥 안쓰는게 좋다.
DB와 OOP 개발자 모두에게 좋지 않다.
성능이 매우 좋은 것도 아니고, 정규화가 된 것도 아니고, 슈퍼타입 쿼리에서도 비정상적인 쿼리가 날아간다.
OOP 관점
부모 클래스를 `extends` 키워드로 상속받기만 하면된다.
상속관계 매핑이란 OOP의 상속과 DB의 슈퍼-서브타입 관계를 매핑하는 것이다.
Book.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book extends Item {
@Column(name = "author_name", length = 63)
private String authorName;
@Column(name = "isbn", length = 127)
private String isbn;
public void changeBookInfo(String authorName, String isbn) {
this.authorName = authorName;
this.isbn = isbn;
}
}
결론
JOINED 전략과 Single table 전략중
기본적으로 JOINED 전략을 취하되, 매우 단순한 비즈니스 로직이 있고 테이블 변경이 별로 없어보이는 경우에는 Single table 전략을 취하는 것을 권장한다.
🔗 Reference
https://www.baeldung.com/hibernate-inheritance
'JVM > JPA' 카테고리의 다른 글
[JPA] 1:1 연관관계 설정 (0) | 2023.01.12 |
---|---|
[JPA] auto_increment 전략 IDENTITY vs SEQUENCE (0) | 2023.01.09 |
[JPA] JPA 는 왜 등장했을까? (0) | 2023.01.07 |
[JPA] API 생성시 Entity 를 반환하지 말자. (0) | 2022.12.29 |