JPA/JPA 기본

24. 상속관계 매핑

sdafdq 2023. 10. 26. 12:23

객체의 상속관계를 테이블에선 어떻게 표현할 수 있고,

JPA에서는 어떻게 지원을 할까?

 

 

예전에도 설명했듯이,

RDB에는 상속관계가 없다.

가장 유사한 것이 슈퍼타입과 서브타입이라는 모델링 기법을 통해 구현해 낸게 가장 유사하다.

 

JPA에서 상속관계 매핑은 이 

객체의 부모 자식 상속 구조와

테이블의 슈퍼타입, 서브타입의 관계를 매핑하는 것이다.

 

왼쪽이 논리모델, 오른쪽이 물리모델.

 

 

여튼, 저 슈퍼타입, 서브타입 논리 모델을 실제 물리모델로 구현하는 방법은 3가지 방법이 있다.

각각 테이블로 변환 -> JOIN

통합 테이블로 변환 -> 단일 테이블 전략

서브타입 테이블로 변환 -> 구현 클래스 마다 테이블

 

 

 

먼저 JOIN전략

타입을 구분하는 열을 하나 둬서 JOIN으로 가져오는 방식이라고 한다. 가장 정규화 된 방식. pk와 fk가 똑같다.

뭐지? if문 같은 거 쓰는건가?

동적쿼리 같은건가?

 

장점

정규화 되어 있다.

어떤 종류인지 필요하지 않을 경우, Item만 조회해도 된다.

외래키이기 때문에 무결성 활용 가능.

저장공간 효율화

 

단점

조회시 조인을 많이 사용해서 성능저하

조회쿼리가 복잡함

데이터 저장시 insert 2번 호출

 

사실 join이나 insert 2번 정도는 괜찮은데, 쿼리가 복잡한 게.

뭐 근데 그것도 대부분 JPA가.. 

 

객체와도 잘 맞고,

정규화도 되어있고,

설계도 깔끔해서

이게 정석이라고 보면 된다.

 

 

 

 

 

 

 

 

단일 테이블

일단 모든 열을 다 때려 넣는다.

Album, Movie, Book

그 다음 DTYPE으로 구분을 해서,

필요한 열만 관리한다.

그런 전략인 듯?

nullable인 곳이 많을 듯.

한 테이블에만 한번에 접근하는거라 성능상 이점이 있을 수 있음.

 

 

 

 

구현클래스마다 테이블 전략.

객체지향적 관점에서 보면 안 좋아 보인다.

 

그냥 다 각각 갖는거다.

 

id는 sequence 이용해서 관리하면 될 듯 싶다.

 

 

 

일단은, 저 세가지 방법 다 지원을 해 준다.

객체입장에서 보면 다 똑같다.

 

 

일단 그럼 코드

@Entity
public abstract class Item {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    private Integer price;
}

추상클래스 만들어도 알아서 테이블이 생성된다.

 

@Entity
public class Album extends Item{
    private String artist;
}

 

 

@Entity
public class Book extends Item{
    private String author;
    private String isbn;
}

 

@Entity
public class Movie extends Item{
    private String director;
    private String actor;
}

그럼 나오는 게

Hibernate: 
    create table Item (
        price integer,
        id bigint not null,
        DTYPE varchar(31) not null,
        actor varchar(255),
        artist varchar(255),
        author varchar(255),
        director varchar(255),
        isbn varchar(255),
        name varchar(255),
        primary key (id)
    )

이렇게 쿼리 날렸다고 로그가 뜬다.(ddl.auto를 create로 했으므로 테이블 자동생성. 이미 테이블 있으면 지웠다가 생성)

 

보면 JPA의 기본 전략은 테이블에 다 때려 넣는거다.

 

물론 이 전략은 내가 선택 가능.

처음에 말한 3가지 전략 JPA에서 모두 지원

 

가장 정규화된 JOIN 전략을 사용해 보면

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Item {
    @Id @GeneratedValue
    private Long id;

    private String name;
    private Integer price;
}

부모클래스에 Inheritance즉, 계승의 전략을 JOIN으로 했다. 

 

그러면,

Hibernate: 
    create table Album (
        id bigint not null,
        artist varchar(255),
        primary key (id)
    )
Hibernate: 
    create table Book (
        id bigint not null,
        author varchar(255),
        isbn varchar(255),
        primary key (id)
    )
Hibernate: 
    create table Item (
        price integer,
        id bigint not null,
        name varchar(255),
        primary key (id)
    )
Hibernate: 
    create table Movie (
        id bigint not null,
        actor varchar(255),
        director varchar(255),
        primary key (id)
    )
    
    
Hibernate: 
    alter table if exists Album 
       add constraint FKcve1ph6vw9ihye8rbk26h5jm9 
       foreign key (id) 
       references Item
Hibernate: 
    alter table if exists Book 
       add constraint FKbwwc3a7ch631uyv1b5o9tvysi 
       foreign key (id) 
       references Item
Hibernate: 
    alter table if exists Movie 
       add constraint FK5sq6d5agrc34ithpdfs0umo9g 
       foreign key (id) 
       references Item

이렇게 테이블들이 자동 생성 된다.

 

또, 뭔가 이렇게 alter 해서 id가 외래키로도 수정되는 것을 볼 수 있다.

즉, id는 pk면서 fk이다.

 

 

이 이후 이제 가져올 때 나가는 쿼리를 보면

em.find(Movie.class, 1L);
Hibernate: 
    select
        m1_0.id,
        m1_1.name,
        m1_1.price,
        m1_0.actor,
        m1_0.director 
    from
        Movie m1_0 
    join
        Item m1_1 
            on m1_0.id=m1_1.id 
    where
        m1_0.id=?

이렇게 자기에서 부모를 join 하면서 가져온다.

 

근데 아직 생성되는 쿼리나 그런 걸 보면,

Item 테이블에 DTYPE이라는 열은 생성되지 않았다.

 

이거는 Item에 @DiscriminatorColumn이라는 직역하면 판별자 열인데, 넣어주면

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract class Item {
    @Id @GeneratedValue
    private Long id;

    private String name;
    private Integer price;
}

자동으로 생성되는 쿼리에

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

이렇게 DTYPE이라는 열이 추가로 생성된다.

DTYPE이라는 이름이 기본값인 듯 하다. Data Type인 듯 하다.

 

저 이름 바꿀 수도 있다.

@DiscriminatorColumn(name="ITEM_TYPE")


여튼, 저 DiscriminatorColumn 넣어서 DB 확인해 보면

이런 식으로 찍힌다.

Movie, 저기 들어가는 값은 Item 테이블이 아니라 하위 테이블에 의해서 들어가는 듯 한 모양이다.

 

저걸 바꿀 수도 있다.

자식에다가

@Entity
@DiscriminatorValue("Video")
public class Movie extends Item{
    private String director;
    private String actor;
}

@DiscriminatorValue, 즉 판별자 값을 VIdeo로 넣어주면,

이렇게 된다.

기본은 @Entity 명 인거 같다.

 

 

여기까지가 JOIN전략이었고,

 

 

다음은 싱글테이블 전략.

하나에 다 때려넣는거.

 

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="ITEM_TYPE")
public abstract class Item {
    @Id @GeneratedValue
    private Long id;

    private String name;
    private Integer price;
}

이렇게 단일 테이블 전략으로 바꿨다.

 

Hibernate: 
    create table Item (
        price integer,
        id bigint not null,
        ITEM_TYPE varchar(31) not null,
        actor varchar(255),
        artist varchar(255),
        author varchar(255),
        director varchar(255),
        isbn varchar(255),
        name varchar(255),
        primary key (id)
    )

다른 테이블은 생성되지 않는다.

 

Movie movie = new Movie();
movie.setName("Name AAAA");
movie.setActor("actorAAAA");
movie.setDirector("Director AAAA");
movie.setPrice(10000);

em.persist(movie);

tx.commit();

이렇게 영화 하나 넣어보면

이렇게 관련 없는 거 빼고 들어간다.

 

장점은 성능이 잘 나옴. 한 테이블에서 한번에 가져오는 거니.

 

em.find(Movie.class, 1L);

도 나가는 쿼리 보면

Hibernate: 
    select
        m1_0.id,
        m1_0.name,
        m1_0.price,
        m1_0.actor,
        m1_0.director 
    from
        Item m1_0 
    where
        m1_0.ITEM_TYPE='Video' 
        and m1_0.id=?

 

@DiscriminatorValue("Video")

에 잘 맞춰 쿼리가 나간다.

타입과 id가 같은 걸 찾아온다.

 

 

 

참고로, JOIN전략과 다르게 단일테이블 전략은,

@DiscriminatorColumn

이게 없어도 자동으로 DTYPE이 생성된다.

왜냐하면 단일 테이블이기 때문에 구분할 수가 없다.

그래서 없더라도 자동으로 들어간다.

 

 

근데, 어떤 전략이든 운영상 저거는 그냥 있는 게 좋다.

 

단일 테이블 전략은 성능 상 이점을 챙기고 싶을 때.

JOIN전략은 확장? 그런 것이 용이할 거 같다.

 

단일테이블 전략의

장점

조인이 필요 없으므로 조회 성능이 비교적 빠름.

쿼리가 단순함.

 

단점

많은 컬럼을 null 허용을 해 줘야 함. (데이터 무결성 입장에서는 좀..)

테이블이 커지고 상황에 따라 조회성능이 오히려 느려질 수 있는데, 이게 어느정도 임계점을 넘어야 느려지는데, 임계점을 넘는 경우는 거의 없긴 하다.

 

 

 

 

 

 

 

 

 

다음, 구현클래스마다 각각의 테이블 전략

여기서는 아예 Item 테이블이 생성이 되지 않는다.

하긴 필요한 정보들을 각각이 모두 가지고 있으니. 필요없다.

Hibernate: 
    create table Album (
        price integer,
        id bigint not null,
        artist varchar(255),
        name varchar(255),
        primary key (id)
    )
Hibernate: 
    create table Book (
        price integer,
        id bigint not null,
        author varchar(255),
        isbn varchar(255),
        name varchar(255),
        primary key (id)
    )
Hibernate: 
    create table Movie (
        price integer,
        id bigint not null,
        actor varchar(255),
        director varchar(255),
        name varchar(255),
        primary key (id)
    )

이렇게 된다.

insert도 그냥

Hibernate: 
    /* insert for
        hellojpa.Movie */insert 
    into
        Movie (name,price,actor,director,id) 
    values
        (?,?,?,?,?)

이렇게 나가고

em.find(Movie.class, 1L);

이것도 뭐 당연히,

Hibernate: 
    select
        m1_0.id,
        m1_0.name,
        m1_0.price,
        m1_0.actor,
        m1_0.director 
    from
        Movie m1_0 
    where
        m1_0.id=?

그냥 보이는 그대로 Movie 엔티티와 연결된 Movie 테이블에서 가져온다.

뭐 그냥 JPA의 지원에 의한 추상적인 상속관계지, 

그냥 관련없는 각각의 테이블을 사용하는 것과 같다.

 

근데 이거 안 좋은게,

아까 말했듯이 Item 테이블은 안 만들어지는데,

만약

em.find(Item.class, movie.getId())

하면..

select
    i1_0.id,
    i1_0.clazz_,
    i1_0.name,
    i1_0.price,
    i1_0.artist,
    i1_0.author,
    i1_0.isbn,
    i1_0.actor,
    i1_0.director 
from
    (select
        id,
        name,
        price,
        artist,
        null as author,
        null as isbn,
        null as actor,
        null as director,
        1 as clazz_ 
    from
        Album 
    union
    all select
        id,
        name,
        price,
        null as artist,
        author,
        isbn,
        null as actor,
        null as director,
        2 as clazz_ 
    from
        Book 
    union
    all select
        id,
        name,
        price,
        null as artist,
        null as author,
        null as isbn,
        actor,
        director,
        3 as clazz_ 
    from
        Movie
) i1_0 
where
i1_0.id=?

이렇게 조회 된다.

 

자식이었던 모든 테이블을 조회한다.

 

쓰지마세요

DB전문가도, 개발자도 둘다 반대하는 전략

 

 

 

JOIN전략을 주로 두고,

단일테이블 전략은 상황에 따라.(추가할 테이블의 여지가 없고 조금이라도 성능을 높이고 싶을 때? 근데 데이터 무결성 때문에 좀 고민되기는 함.. 정말 단순하고 확장할 일이 없을 때. 사용)

 

구현클래스마다 테이블은 염두에 두지 말 것.

 

 

'JPA > JPA 기본' 카테고리의 다른 글

27. 프록시  (0) 2023.10.27
25. Mapped Superclass 매핑 정보 상속  (0) 2023.10.27
23. 실전3 다양한 연관관계 매핑  (0) 2023.10.25
22. N : M 다대다 연관관계  (0) 2023.10.25
21. 1 : 1 연관관계  (0) 2023.10.25