데이터 베이스에서 상속 관계
관계형 데이터베이스에서는 상속이라는 개념이 존재하지 않는다. 그래서 보통 슈퍼타입과 서브타입으로 분리시키는 방식으로 구현을 한다. 이러한 논리 모델을 구현하기 위해서는 세가지 방법이 존재하는데 각각 방법에 대한 장점과 단점을 알아보자
조인전략: 각각의 테이블로 변환
각각의 논리 모델을 모두 테이블로 만들고 부모테이블의 PK를 받아서 기본키와 외래키를 통해 사용하는 전략이다. 해당 방법을 사용할 때 주의할 점은 객체에서는 타입으로 구분할 수 있지만 데이터베이스에는 그런 개념이 없기 때문에 타입을 구분하는 전략을 사용해 주어야한다.
Table 구조
Entity 구조
부모 클래스(Product)
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public class Product {
@Id
@GeneratedValue
@Column(name = "id")
private Long id;
private String name;
private int price;
}
@Inheritance
어노테이션은 SINGLE_TABLE, JOINED, TABLE_PER_CLASS 타입으로 구분되어 사용되어 집니다. 가장 먼저 JOINED는 조인전략을 사용하여 매핑하기 위해 사용되어지는 옵션입니다. @DiscriminatorColumn
어노테이션을 통해 아까 언급했듯이 테이블에는 타입이 없기 때문에 따로 객체를 구분지어줄 타입을 설정하여야한다.
자식 클래스(Product)
@Entity
@Getter
@DiscriminatorValue("M")
@PrimaryKeyJoinColumn(name = "PRODUCT_ID")
public class Movie extends Product{
private String director;
private String actor;
}
@PrimaryKeyJoinColumn
을 통해 슈퍼타입의 테이블의 기본키를 서브타입의 테이블의 기본키로 사용할 수 있습니다. name을 설정하지 않으면 부모객체의 필드값을 그대로 사용하므로 재정의를 해줍니다. @DiscriminatorColumn
어노테이션을 통해 해당 자식객체가 부모테이블의 어떠한 타입으로 저장할지 정의를 해줍니다.
@SpringBootTest
@ExtendWith(SpringExtension.class)
class MovieRepositoryTest {
@Autowired
private MovieRepository movieRepository;
@Test
void testSave() {
Movie testMovie = Movie.builder()
.name("testMovie")
.price(5000)
.actor("testerActor")
.director("testDirector")
.build();
Movie movie = movieRepository.save(testMovie);
assertEquals(movie.getActor(), testMovie.getActor());
}
}
위와 같이 테스트를 진행해봅니다. 조인 전략은 저장이나 조회를 할 때 슈퍼타입 테이블에 연관되어 사용되어 지기 때문에 아래의 결과처럼 insert 쿼리가 두번 나가게 됩니다.
Hibernate:
call next value for hibernate_sequence
Hibernate:
insert
into
product
(name, price, dtype, id)
values
(?, ?, 'Movie', ?)
Hibernate:
insert
into
movie
(actor, director, product_id)
values
(?, ?, ?)
장점
- 정규화된 테이블을 사용할 수 있다.
- 외래키 참조 무결성 조건을 활용할 수 있습니다.(외래키는 참조할 수 없는 값을 가질 수 없다는 규칙)
- 저장 공간을 효율적으로 활용할 수 있습니다.
단점
- 조회할 때 무조건 조인을 많이 사용해야되서 성능이 저하될 수 있다.
- 테이블이 분리되어 있기 때문에 조회쿼리가 복잡합니다.
- 저장을 할 때 두번의 insert 쿼리가 발생하게 됩니다.
위와 같은 전략은 구분컬럼을 사용하여 구현하도록 권장하지만 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼을 굳이 사용하지 않아도 구현할 수 있습니다.
참조: https://www.baeldung.com/hibernate-inheritance
단일 테이블 전략: 하나의 테이블로 구현
싱글 테이블 전략은 아래와 같이 하나의 테이블로 구현하여 사용합니다. 하나의 테이블에서 사용하기 때문에 저장 및 조회를 할 때 3가지 전략 중 가장 빠른 전략이입니다. 주의해야될 점은 매핑한 컬럼이 모드 null을 허용해야된다. Book객체를 저장한다면 이와 관련없은 영화에 관련된 Directort, Actor 컬럼은 값이 들어가지 않기 때문입니다.
테이블 구조
Entity 구조
부모 클래스
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public class Product {
@Id
@GeneratedValue
@Column(name = "id")
private Long id;
private String name;
private int price;
}
자식 클래스
@Entity
@DiscriminatorValue("B")
public class Book extends SingleProduct{
private String author;
private String isbn;
}
테스트를 실행해보면 하나의 테이블을 통해 상속하여 사용하고 있기 때문에 하나의 insert 쿼리만 발생하게 됩니다.
@SpringBootTest
@ExtendWith(SpringExtension.class)
class BookRepositoryTest {
@Autowired
private BookRepository bookRepository;
@Test
void saveTest() {
Book testBook = Book.builder()
.name("book")
.price(4000)
.author("author")
.isbn("111-111-111")
.build();
Book savedBook = bookRepository.save(testBook);
assertEquals(savedBook.getAuthor(), testBook.getAuthor());
}
}
결과
insert
into
single_product
(name, price, author, isbn, dtype, id)
values
(?, ?, ?, ?, 'B', ?)
장점
- 조인을 사용하지 않아서 조회할 때 상대적으로 빠르다.
- 조회쿼리가 단순해진다.
단점
- 자식 테이블의 데이터를 제외한 다른 테이블의 데이터를 전부 null을 허용한다.
- 모든 데이터를 하나의 테이블에서 관리하므로 하나의 테이블에 부하가 몰리게 되서 상황에 따라 오히려 성능이 저하될 수 있다.
그렇다며 실무에서 어떤 상황에 싱글 테이블을 사용하고 멀티 테이블을 사용해야 될까?
싱글 테이블 전략 VS 멀티 테이블 전략
하위클래스에서 같은 필드를 가지고 있지만 다른 동작을 할 경우 (싱글 테이블)
- 고객이라는 테이블이 있고 고객별로 멤버십 마다 서로 다른 서비스를 제공한다면 싱글 테이블 전략이 효과적입니다.
- 확인 해야할 정보가 같고 서로 다른 서비스를 제공해야되는 상황이면 서로 다른 테이블에 흩어져 있는 데이터를 한곳에 모으기 위해 조인이라는 코스트를 소모해야합니다. 그렇기에 싱글테이블 전략이 더 효과적입니다.
상위 클래스가 아닌 하위 클래스에서 쿼리를 수행해야 되는 경우 (싱글 테이블)
- 예를 들어 통계를 낼 때 Book에서 가장 수익이 잘나오는 책 10권이라는 쿼리가 실행된다면 싱글테이블은 구매내역과 상품이라는 테이블로 조인하면 끝이지만 멀티 테이블은 상품, 책, 구매내역 라는 테이블 들을 조인해야 합니다. 그렇기에 이러한 상황에는 싱글 테이블전략을 사용하는 것이 좋습니다.
하위클래스에서 서로 다른 필드를 가지고 있고 서로 같은 동작을 할 경우 (멀티 테이블)
- 조회라는 행동은 같지만 책, 영화, 앨범이 제공하는 정보는 서로 너무 다릅니다. 이것을 하나의 테이블에 처리를 해야된다면 그만큼 하나의 테이블에 서로 다른 내용의 컬럼을 가지고 있어야하고 null 필드가 많아짐에 따라 관리하기가 힘들어집니다.
- 예를 들어 우리는 영화 상품을 상세 조회를 할때 감독 이름, 배우 이름만 제공하는 기능을 구현할 때 굳이 조인 없이 영화 테이블에 조회해서 사용하면 됩니다. 하지만 싱글 테이블인 경우 잘잘한 처리가 모두 하나의 테이블에 묶이게 됨으로써 부하를 주고 조회성능이 떨어지게 됩니다.
- 조회성능을 올리기 위해 필요없는 데이터 필드를 Dto로 가공하는 경우가 많이 생깁니다.
테이블에 데이터가 방대한 경우(멀티 테이블)
- 하나의 테이블의 데이터가 방대해질수록 조회성능이 떨어지는 것은 당연한 결과입니다. 그리고 스케일 아웃하기에도 굉장히 손이 많이가고 까다롭습니다.
- 그렇기에 데이터를 분산 저장하고 쉽게 관리할 수 있도록 멀티테이블 전략을 활용하는 것이 효과적입니다.
확신이 없다면 멀티 테이블 전략이 선택하자
싱글테이블 명백한 녹아웃 선택(하위 클래스가 정확히 동일한 필드/열을 가지고 있고 우리는 모든 하위 클래스에 걸쳐 자주 쿼리할 것으로 예상하는 것)이 아니라면 싱글 테이블 전략은 개념적으로 혼란스러워지고 테이블이 느리고 비대해질 가능성이 높다.
[When To Use Single Table Inheritance vs Multiple Table Inheritance
With Examples in Active Record
user3141592.medium.com](https://user3141592.medium.com/when-to-use-single-table-inheritance-vs-multiple-table-inheritance-db7e9733ae2e)
**구현 클래스별 테이블 전략(서브타입과 슈퍼타입을 두지않고 개별적으로 관리)
**
좋지 않은 전략이고 되도록이면 사용하지 않는 것을 권장한다. 여러 자식테이블에 UNION을 통해 조회하게되는데 이러한 조인은 조회쿼리 성능에 비대한 영향력을 끼친다. 특징은 구분 컬럼을 사용하지 않는 것이다.
부모 클래스
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn(name = "DTYPE")
public abstract class PerItem {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
자식 클래스
package com.book.jpa.chapter07.JoinedStrategy.table_per;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.PrimaryKeyJoinColumn;
@Entity
@DiscriminatorValue("B")
public class PerBook extends PerItem {
private String author;
private String isbm;
}
장점
- 서브 타입을 구분해서 사용할 때 효과적이다.
- not null 조건을 사용할 수 있다.
단점
- 여러 자식과 함께 조회할 때 성능이 떨어진다.
- 자식 테이블을 통합해서 조회하기 어렵다.
MappedSuperClass
상속은 서로 관련있는 도메인을 응집성이 높게하기 위해 사용하기도 하지만 서로 관련없는 도메인끼리 공통 기능이나 필드를 상속받기 위해 사용하기도 한다. 이와같은 상속이 필요할 때 JPA에서는 추상클래스같은 Entity를 사용할 수 있도록 @MappedSuperClass
를 제공합니다. 대표적인 예로 수정일과 생성일이 있습니다.
BaseTimeEntity
@Getter
@MappedSuperclass
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
}
@MappedSuperClass
이와 같이 공통되는 필드를 제공하기 상속받아서 사용하기 위해 사용합니다. @EntityListeners(AuditingEntityListener.class)
은 JPA에서 생성일과 수정일을 관리하여 Persistence Context에서 flush되는 순간에 트리거처럼 자동으로 주입할 수 있도록 도와주는 어노테이션 입니다.
그리고 상속받은 필드를 재정의하여 사용할 수 있도록 도와주는 기능이 있는데 그것이 바로 @AttributeOvrrides
입니다. 아래와 같이 상속을 받은 후 재정의하여 사용할 수 있다.
@Entity
@NoArgsConstructor
@AllArgsConstructor
@AttributeOverrides({
@AttributeOverride(name = "createdAt", column = @Column(name = "WROTE_AT")),
@AttributeOverride(name = "updatedAt", column = @Column(name = "EDITED_AT"))
})
public class Community extends BaseTimeEntity {
@Id
@GeneratedValue
private Long id;
private String title;
private String content;
}
@MappedSuperClass
는 필드 뿐만 아니라 Entity도 상속받아 사용할 수 있습니다. 그리고 이러한 상속관계를 재정의하도록 해주는 것이 @AssociationOverrides
입니다.
@MappedSuperclass
public class BaseCommentEntity {
...
@ManyToOne
private Comment comment;
@ManyToOne
private Emoticon emoticon
...
}
@Entity
@NoArgsConstructor
@AllArgsConstructor
@AssociationOverrides({
@AssociationOverride(name="comment", joinColumns=@JoinColumn(name="COMMUNITY_COMMENT_ID"))
@AssociationOverride(name="emoticon", joinColumns=@JoinColumn(name="COMMUNITY_EMOTICON_ID"))
})
public class Community extends BaseTimeEntity, BaseCommentEntity {
@Id
@GeneratedValue
private Long id;
private String title;
private String content;
}
예제가 조금 이상하기는 하지만 사용하는 방법을 익히고 할 수 있다는 것을 알고 있으시면 좋습니다. MappedSuperClass는 테이블에 매핑되지 않고 매핑되는 정보만 사용하기 때문에 따로 Entity처럼 find 하거나 Jpql에서 쿼리로 사용할 수 없습니다. 이러한 클래스는 직접 사용하는 경우가 없으니 추상 클래스로 만들어서 상속하여 사용하는 것을 권장합니다.
'JAVA > JPA' 카테고리의 다른 글
<JPA> Criteria의 사용 방법 (0) | 2022.10.22 |
---|---|
복합키와 식별관계 (1) | 2022.09.24 |
@OneToOne 관계에서 지연로딩(LAZY)이 안되는 문제 (0) | 2022.09.18 |
(JPA) fetch join (0) | 2021.08.12 |
(JPA) 조인 (0) | 2021.07.09 |