JPA : 상속관계 맵핑, 슈퍼 타입 - 서브 타입 모델링
- Spring/JPA
- 2021. 11. 28.
상속관계 맵핑
- 관계형 데이터베이스에는 상속관계가 없음.
- 슈퍼타입 - 서브타입 관계 모델링 기법이 상속관계가 유사함
- 상속관계 맵핑은 객체의 상곡구조를 DB의 슈퍼타입 - 서브타입 관계에 맵핑하는 작업이 필요하다.
객체에는 상속관계가 있으나 RDBMS에는 상속관계가 없다. 그렇다면 객체의 상속관계를 RDBMS에는 어떻게 맵핑을 해야하는걸까? RDBMS에는 슈퍼타입 - 서브타입 관계 모델링이 있는데, 이 관계가 객체의 상속 관계 구조와 매우 유사하다. 따라서 객체의 상속 관계를 표현하기 위해서는 RDBMS의 슈퍼타입 - 서브타입 관계 모델링에 하나 하나 적용하는 방법이 필요하다.
슈퍼타입 - 서브타입 논리 모델의 구현
- 조인 전략 : 각 테이블을 하나씩 구현해서 맵핑
- 단일 테이블 전략 : 통합 테이블을 하나 만들어서 맵핑
- 구현 클래스마다 테이블 전략 : 서브타입 테이블로 변환해서 맵핑
위의 각 전략을 어떻게 구현할지는 슈퍼 타입(부모 엔티티)에 내가 어떤 전략을 사용할지를 어노테이션으로 명시해줘야한다. @Inheritance 어노테이션으로 이 객체가 슈퍼 타입임을 명시해주고, 옵션의 strategy에 어떤 테이블 전략을 사용할지 명시해주면 된다. 서브 타입 엔티티는 extends로 슈퍼타입 엔티티를 상속 받으면 된다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // JOIN TABLE 전략
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 단일 테이블 전략
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) // 구현 클래스마다 테이블 전략
public class Item {
}
슈퍼타입 어노테이션
- @Inheritance : 이 엔티티가 슈퍼타입 엔티티임을 명시해준다. strategy 옵션으로 어떤 전략을 사용할지 명시해준다.
- @DiscriminatorColumn(name = "DTYPE") : 이 슈퍼 타입이 어떤 서브 타입과 연결되었는지를 알려주는 컬럼명을 설정한다. 이 식별자의 기본값은 'DTYPE'이다.
DiscrimantorColumn 어노테이션을 사용하지 않으면, 슈퍼 타입 테이블에 어떤 식별자 컬럼도 들어가지 않는다. 이 식별자 컬럼은 어떤 테이블 전략을 쓰느냐에 따라 자동 생성이 된다. 예를 들어 Join Table 전략에서는 식별자 컬럼을 쓰지 않으면 정말로 사용되지 않는다.
반면에 Single Table 전략에서는 식별자 컬럼을 쓰지 않아도 자동으로 식별자 컬럼이 생성되고, 조회 시 이용한다.
슈퍼타입 맵핑 : Join Table 전략
Join Table 전략은 각 Table을 하나씩 다 만든다. 그리고 슈퍼타입에 있는 변수는 슈퍼타입에 저장, 나머지는 서브타입에 저장된다. 읽어올 때는 Join해서 읽어온다.
JoinTable 전략 구현해보기
- Item 클래스 만들고 @Inheritance 어노테이션 달아주고, Option은 JoinTable로 한다. @Entity 추가한다.
- Item 클래스에 식별자를 추가하기 위해 @DiscriminatorColumn 어노테이션을 달아준다.
- Album, Book, Movie 클래스 만들고 extends Item해서 상속받아준다. @Entity 어노테이션 달아준다.
- 이 때, Item에 있는 필드 변수는 자식 객체에서 만들어주지 않는다.
위를 바탕으로 작성한 코드는 아래와 같다.
// Item 클래스
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
// Album 클래스
@Entity
public class Album extends Item {
private String artist;
}
// Book, Movie 클래스는 위와 유사하게 작성.
이를 바탕으로 조회 및 테스트 하는 코드를 작성했다.
Album album = new Album();
album.setArtist("artist");
album.setName("aaaaa");
album.setPrice(10000);
em.persist(album);
em.flush();
em.clear();
System.out.println("======================");
Album album1 = em.find(Album.class, album.getId());
System.out.println("album1 = " + album1);
먼저 서브타입이 정상적으로 저장되는지를 확인해본다.
실행 결과는 위와 같다. 저장과정에서 em.persist()를 한번했지만, Insert가 두 번 나가는 것을 알 수 있다. 왜냐하면 Item Table과 Album Table은 따로 나누어져서 관리되고 있기 때문이다. 각각의 Table에 Album 엔티티의 정보를 저장하기 때문에 Insert 쿼리는 2번 나가게 된다. 저장된 DB를 확인해보면, ITEM에 저장된 ID 값을 FK로 ALUBM에도 값이 같이 저장된 것을 볼 수 있다.
서브 타입으로 조회해보기
조회는 Select 쿼리가 한번 나가는 것을 볼 수 있다. 이 때 Select 쿼리는 필요한 Table을 Join해서 가져오는 것을 볼 수 있다.
슈퍼 타입으로 조회해보기
슈퍼타입인 Item 엔티티에 대해서 PK값으로 조회를 해봤다. 조회를 하니 대참사가 발생했다. 슈퍼타입의 모든 서브타입을 Join해서 Table을 만들고, 그 Table에서 PK값으로 필요한 값을 찾아온다. 슈퍼 타입으로 조회하면 쓸 데 없는 Join Table이 많이 이루어지기 때문에 아주 비효율적일 것으로 예상된다.
슈퍼타입 맵핑 : Single Table 전략
Signel Table 전략은 하나의 Table에 모든 값들을 넣고, DTYPE으로 식별하는 전략이다. 이 때 DiscriminatorColumn은 필수적으로 들어가고, 이걸로 값을 식별한다.
Single Table 전략 구현해보기
- Item 클래스 만들고 @Inheritance 어노테이션 달아주고, Option은 SingleTable로 한다. @Entity 추가한다.
- Item 클래스에 식별자를 추가하기 위해 @DiscriminatorColumn 어노테이션을 달아준다.
- Album, Book, Movie 클래스 만들고 extends Item해서 상속받아준다. @Entity 어노테이션 달아준다.
이 때, Item에 있는 필드 변수는 자식 객체에서 만들어주지 않는다.
Single Table 전략, create Table 쿼리 확인.
Single Table 전략은 앞서 말한 것처럼 모든 것을 한 군데에 다 넣은 테이블을 하나 만들어서 통합으로 관리를 한다. 따라서 Table은 오로지 슈퍼타입의 Item으로만 만들어진다.
Single Table 전략, 데이터 저장해보기
Album album = new Album();
album.setArtist("artist");
album.setName("aaaaa");
album.setPrice(10000);
em.persist(album);
위 코드를 이용해 Single Table 전략 시, 데이터가 어떻게 저장되는지, 그리고 실행결과는 어떻게 나오는지를 확인해봤다.
Insert 쿼리는 한번만 나가는 것을 볼 수 있다. 하나의 테이블로 데이터를 관리하기 때문에 당연하기 한번의 INSERT 쿼리가 나가는 것을 볼 수 있다.
DB에서 실제 TABLE을 조회해보면 정상적으로 저장이 된 것을 볼 수 있다. 그런데, 다른 자식 엔티티의 값들은 모두 null값으로 들어가있는 것을 볼 수 있다. 싱글 테이블의 단점은 이런 null값이 허용되는 것이 치명적일 것 같다.
SingleTable 전략, 슈퍼 타입으로 조회해보기
Item item = em.find(Item.class, album.getId());
System.out.println("album1 = " + item.getName());
위 코드를 활용해서 SigleTable에 저장된 데이터를 가져오는 코드를 수행했다.
수행 결과는 위와 같다. select 쿼리는 한번만 나가고, 이 때 Join 쿼리는 없다. 왜냐하면 이미 Table을 하나로 관리하기 때문에 Join을 할 필요가 없다.
SingleTable 전략, 서브 타입으로 조회해보기
Album album1 = em.find(Album.class, album.getId());
System.out.println("album1 = " + album1.getName());
서브타입으로 조회했을 때, 나가는 쿼리가 어떤지를 확인해봤다.
서브타입으로 조회를 하면 위 결과가 나온다. 기본적으로 PK를 기준으로 검색을 하는데, 여기에 DTYPE이라는 식별자 컬럼에서 같은 타입을 가지는 것을 찾는다. 불러오는 변수명도 Album에 대한 것만 불러오기 때문에 다른 서브 타입 필드 변수에 있는 null을 걱정하지 않아도 될 것 같다.
슈퍼타입 맵핑 전략 : 구현 클래스마다 테이블 전략
이 전략은 슈퍼 타입인 ITEM 테이블을 포함해서 모든 테이블을 만든다. 그리고 서브 테이블에 ITEM 테이블의 필드 변수를 각각 생성해주는 형태이다.
슈퍼타입 클래스 테이블은 어차피 사용하지 않기 때문에 abstract class로 만들어주는 것이 추천된다. abstract 클래스로 만들면, 테이블은 만들어지지 않는다.
구현 클래스마다 테이블 전략 구현
- Item 클래스 만들고 @Inheritance 어노테이션 달아주고, Option은 TABLE_PER_CLASS로 한다. @Entity 추가한다.
- Item 클래스에 식별자를 추가하기 위해 @DiscriminatorColumn 어노테이션을 달아준다.
- Album, Book, Movie 클래스 만들고 extends Item해서 상속받아준다. @Entity 어노테이션 달아준다.
이 때, Item에 있는 필드 변수는 자식 객체에서 만들어주지 않는다.
구현 클래스마다 테이블 전략 create table 쿼리 확인
DB에 만들어진 Table을 확인해본다. ITEM, ALBUM, BOOK, MOVIE 테이블이 모두 만들어져 있는 것을 볼 수 있다. 앞서 이야기 했던 것처럼 구현 클래스마다 테이블 생성 전략은 관련된 모든 테이블이 생성되기는 한다.
구현 클래스마다 테이블 전략, 저장해보기
Album album = new Album();
album.setArtist("artist");
album.setName("aaaaa");
album.setPrice(10000);
em.persist(album);
위 코드로 값을 저장할 때의 저장 쿼리와 저장 결과물을 확인하고자 한다.
INSERT 쿼리는 서브타입 테이블로만 나가는 것을 볼 수 있고, 실제 DB에서도 마찬가지로 서브타입에만 저장되는 것을 볼 수 있다. 슈퍼타입 테이블인 ITEM에는 어떠한 값도 저장되지 않는 것을 볼 수 있다.
구현 클래스마다 테이블 전략, 슈퍼 타입으로 조회해보기.
Item item = em.find(Item.class, album.getId());
System.out.println("item = " + item);
위 코드를 활용해서 슈퍼 타입으로 조회를 했을 때, 어떤 쿼리가 나가는지를 살펴봤다.
쿼리를 실행해보면, 슈퍼타입 아래에 있는 모든 서브 타입 테이블을 union으로 하나로 묶은 후에 값을 가져오는 것을 볼 수 있었다. 그래도 값은 정상적으로 나오는 것을 볼 수 있다.
구현 클래스마다 테이블 전략, 서브 타입으로 조회해보기.
Album album1 = em.find(Album.class, album.getId());
System.out.println("album1 = " + album1.getName());
위 코드를 활용해 서브 타입으로 조회해봤다. 실행 결과는 아래에서 볼 수 있다.
실행 시, Select 쿼리가 한번만 나가는 것을 볼 수 있고 이 때 Join 쿼리가 없는 것을 확인할 수 있었다.
상속관계 맵핑 전략 정리
상속관계 맵핑 전략은 Join Table 전략을 기본으로 생각하고 사용하자.
Join Table 전략 정리 → 기본 전략으로 쓰자
- 장점
1. 데이터가 정규화 되어있음. 즉, 저장 공간을 효율화해서 사용할 수 있음.
2. 외래 키 참조 무결성 제약조건을 활용할 수 있다. 예를 들어 Order Table에서 외래키 참조로 ITEM을 봐야하면, ITEM_ID로 ITEM TABLE만 보면 된다. - 단점
1. 조회 시 조인을 많이 사용한다. 따라서 성능이 저하되고 조회 쿼리가 복잡하다.
2. 데이터 저장 시, 두 군데 테이블에 저장해야해서 INSERT QUERY가 2회 나간다.
3. 단일 테이블과 비교하면, 테이블이 전반적으로 복잡하다.
Single Table 전략 정리
- 장점
1. Join이 필요없어서 조회 성능이 빠르다.
2. 조회 쿼리가 단순하다. - 단점
1. 자식 엔티티가 맵핑한 컬럼은 모두 Null 허용된다. 데이터 무결성 관점에서 치명적이다
2. 단일 테이블에 모든 것을 저장하기 때문에 상황에 따라 조회 성능이 오히려 느릴 수 있음(변수가 많을 경우)
Table PER Class 전략 정리→ 쓰면 안된다.
- 장점
1. 서브 타입을 명확하게 구분해서 처리할 때 효과적임
2. Not Null 제약조건을 사용 가능하다. - 단점
1. 여러 자식 테이블을 함께 조회할 때 성능이 느림(슈퍼타입으로 조회하면 모든 테이블 Union 해서 찾는다)
2. 자식 테이블을 통합해서 쿼리하기가 어렵다.
3. 변경 관점에서 매우 안 좋다. 새로운 서브 타입이 추가되면 굉장히 많은 것들이 변경되어야 한다.
'Spring > JPA' 카테고리의 다른 글
JPA : CASCADE를 통한 영속성 전이, 고아 객체 (0) | 2021.11.28 |
---|---|
JPA : 상속관계 맵핑 MappedSuperClass (0) | 2021.11.28 |
JPA, 다양한 연관관계 맵핑 (0) | 2021.11.27 |
다대일 연관 관계, 외래키는 어디에 있어야할까? (0) | 2021.11.27 |
JPA의 연관관계 맵핑 (0) | 2021.11.25 |