0+ 스프링/0 + 스프링 ORM(JPA)

[JPA] 상속관계 매핑

힘들면힘을내는쿼카 2023. 2. 20. 21:41
728x90
반응형

[JPA] 상속관계 매핑

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

객체지향 언어에서는 명확하게 상속 관계가 존재합니다.
부모의 속성에서 더 작은 그룹으로 분리해서 객체를 관리해야할 때 우리는 상속을 사용합니다.

 

데이터 베이스에서도 부모의 속성에서 더 작은 그룹으로 분리해서 관리해야 할 수도 있습니다.
하지만, 관계형 데이터베이스는 상속 관계가 존재하지 않습니다.
이러한 문제점을 해결하기 위해 슈퍼타입, 서브타입 관계라는 모델링 기법을 사용했습니다.

 

ORM을 사용하여 우리는 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑 할 수 있습니다.
상속관계를 매핑하기 위해서는 먼저 슈퍼타입 서브타입 논리 모델 실제 물리 모델 구현해야 합니다.

 

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

  • 각각 테이블로 변환
    • 조인 전략
  • 통합 테이블로 변환
    • 단일 테이블 전략
  • 서브타입 테이블로 변환
    • 구현 클래스마다 테이블 전략

 

주요 애노테이션

JPA에서는 다음과 같이 애노테이션을 사용해서 테이블 전략을 선택할 수 있습니다.

@Inheritance(strategy=InheritanceType.XXX)

  • JOINED
    • 조인 전략
  • SINGLE_TABLE
    • 단일 테이블 전략
  • TABLE_PER_CLASS
    • 구현 클래스마다 테이블 전략

 

 

@DiscriminatorColumn(name=“DTYPE”)

DTYPE 생성

@Entity
@Getter @Setter
@DiscriminatorColumn(name = "DTYPE")
public class Item {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private Integer price;
}

 

@DiscriminatorValue(“XXX”)

자식 테이블의 DTYPEXXX로 변경
Book을 저장하면 Item 테이블 DTYPEB가 저장됩니다.(기본값은 엔티티 이름(Book) 저장)

@Entity
@Getter @Setter
@DiscriminatorValue("B")
public class Book extends Item {
    private String author;
    private String isbn;
}

 

조인 전략

예를 들어서 ALBUM데이터를 추가하면

INSERT INTO ITEM … ;
INSERT INTO ALBUM … ;

이렇게 SQL이 2개 발생합니다.

 

조회는 PK, FKITEM_ID이기 때문에 ITEM_ID로 조인하면 됩니다.

SELECT * FROM ITEM i
JOIN ALBUM a ON a.ITEM_ID = i.ITEM_ID;

 

DTYPE으로 ALBUM, MOVIE, BOOK을 구분할 수 있습니다.
아래 예제에서 확인해 봅시다.

 

Item
조인 전략을 사용하여 Entity를 매핑하는 코드는 아래와 같습니다.
(@Inheritance(strategy = InheritanceType.JOINED)을 작성하여 조인전략을 사용합니다.)

@Entity
@Getter @Setter
// 조인 전략
@Inheritance(strategy = InheritanceType.JOINED)
// name은 기본값이 DTYPE
@DiscriminatorColumn(name = "DTYPE")
public class Item {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private Integer price;
}

 

Album

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

 

Book

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

 

Movie

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

 

Hibernate: 

create table Album (
       artist varchar(255),
        id bigint not null,
        primary key (id)
    )
Hibernate: 

    create table Book (
       author varchar(255),
        isbn varchar(255),
        id bigint not null,
        primary key (id)
    )
Hibernate: 

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

    create table Movie (
       actor varchar(255),
        director varchar(255),
        id bigint not null,
        primary key (id)
    )

Item, Album, Book, Movie테이블이 생성된것을 확인할 수 있습니다.

 

Movie를 저장해 볼까요?

Movie movie = new Movie();
movie.setDirector("봉준호");
movie.setActor("송강호");
movie.setName("기생충");
movie.setPrice(15000);

em.persist(movie);

 

Hibernate: 
    /* insert com.study.purejpa.entity.item.Movie
        */ insert 
        into
            Item
            (name, price, DTYPE, id) 
        values
            (?, ?, 'Movie', ?)
Hibernate: 
    /* insert com.study.purejpa.entity.item.Movie
        */ insert 
        into
            Movie
            (actor, director, id) 
        values
            (?, ?, ?)

데이터 저장시 INSERT SQL 2번 호출된것을 확인할 수 있습니다!

DTYPEMovie라고 저장된것을 확인할 수 있습니다.

 

 

조회 쿼리는 어떻게 발생하는지 확인해 볼까요?

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

 

Hibernate: 
    select
        movie0_.id as id1_2_0_,
        movie0_1_.name as name2_2_0_,
        movie0_1_.price as price3_2_0_,
        movie0_.actor as actor1_3_0_,
        movie0_.director as director2_3_0_ 
    from
        Movie movie0_ 
    inner join
        Item movie0_1_ 
            on movie0_.id=movie0_1_.id 
    where
        movie0_.id=?

inner join을 사용해서 조회하는 것을 알수 있습니다.

 

장점

  • 테이블 정규화
  • 외래 키 참조 무결성 제약조건 활용가능
  • 저장공간 효율화

 

 

단점

  • 조회시 조인을 많이 사용
    • 성능 저하
  • 조회 쿼리가 복잡함
  • 데이터 저장시 INSERT SQL 2번 호출
    • 엄청난 단점은 아니라고 봅니다. 🤗

 

 

단일 테이블 전략

단순하고 확장 가능성이 없어보일때 사용하는 것을 추천 드립니다.

 


논리 모델을 하나의 테이블로 합치는 방법 입니다.
DTYPE으로 ALBUM, MOVIE, BOOK을 구분할 수 있습니다.

 

Item
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)으로 설정하면 단일 테이블 전략을 사용할 수 있습니다.
(사실 @Inheritance(strategy = InheritanceType.SINGLE_TABLE)를 생략해도 됩니다.!)

@Entity
@Getter @Setter
// 단일 테이블 전략
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public class Item {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private Integer price;
}

 

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

 

테이블을 조회하면 다음과 같은 결과를 보여줍니다.

 

장점

  • 조인이 필요 없으므로 일반적인 조회 성능이 빠름 💨
  • 조회 쿼리가 단순

 

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null 허용(치명적 단점!)
  • 단일 테이블에 모든 것을 저장하기 때문에 테이블 사이즈가 커질 수 있음
  • 상황에 따라서 조회 성능이 느려질 수 있음
    • 하지만 이런 상황은 거의 존재하지 않음

 

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

이 전략은 사용하지 않는것이 좋습니다.😵‍💫


ALBUM, MOVIE, BOOK을 독립적인 테이블로 만드는 방법 입니다.
(각 테이블 마다 기존의 Item 테이블이 담당했던 name, price중복으로 갖게 됩니다.)

데이터베이스 설계자와 ORM 전문가 둘 다 추천 하지 않습니다.

 

Item
Item을 추상(abstract) 클래스로 만들고,
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)으로 설정하면 구현 클래스마다 테이블 전략을 사용할 수 있습니다.


@DiscriminatorColumn(name = "DTYPE”)을 넣어도 사용할 수 없습니다.
Item 테이블을 사용하지 않고 ALBUM, MOVIE, BOOK 테이블을 독립적으로 사용하기 때문입니다.^^

@Entity
@Getter @Setter
// 구현 클래스마다 테이블 전략
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private Integer price;
}

 

Hibernate: 

    create table Album (
       id bigint not null,
        name varchar(255),
        price integer,
        artist varchar(255),
        primary key (id)
    )
Hibernate: 

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

    create table Movie (
       id bigint not null,
        name varchar(255),
        price integer,
        actor varchar(255),
        director varchar(255),
        primary key (id)
    )

 

Item 테이블이 생성되지 않고 ALBUM, MOVIE, BOOK 테이블만 생성되는 것을 확인할 수 있습니다.^^

 

데이터를 저장하면 다음과 같은 결과를 확인할 수 있습니다.

Movie movie = new Movie();
movie.setDirector("봉준호");
movie.setActor("송강호");
movie.setName("기생충");
movie.setPrice(15000);

em.persist(movie);

 

 

오… 엄청 좋아보이는데요…?


그런데 부모타입으로 조회를 하면 어떻게 될까요? 🤔

Item item = em.find(Item.class, 1L);
System.out.println("Item="+item);

 

Movie의 부모는 Item 이기때문에 부모타입인 Item으로 조회할 수 있습니다.

Hibernate: 
    select
        item0_.id as id1_2_0_,
        item0_.name as name2_2_0_,
        item0_.price as price3_2_0_,
        item0_.artist as artist1_0_0_,
        item0_.actor as actor1_3_0_,
        item0_.director as director2_3_0_,
        item0_.author as author1_1_0_,
        item0_.isbn as isbn2_1_0_,
        item0_.clazz_ as clazz_0_ 
    from
        ( select
            id,
            name,
            price,
            artist,
            null as actor,
            null as director,
            null as author,
            null as isbn,
            1 as clazz_ 
        from
            Album 
        union
        all select
            id,
            name,
            price,
            null as artist,
            actor,
            director,
            null as author,
            null as isbn,
            2 as clazz_ 
        from
            Movie 
        union
        all select
            id,
            name,
            price,
            null as artist,
            null as actor,
            null as director,
            author,
            isbn,
            3 as clazz_ 
        from
            Book 
    ) item0_ 
where
    item0_.id=?

부모타입(Item)으로 조회하게 되면 자식 클래스를 모두 조회할수 밖에 없습니다.

 

장점

  • 서브 타입을 명확하게 구분해서 처리할 때 효과적
  • not null 제약조건 사용 가능

 

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느림(UNION ALL 필요)
    • UNION ALL은 여러 쿼리문들이 합쳐서 하나의 쿼리문으로 만들어주는 방법 입니다.
    • 중복된 값을 모두 조회합니다.
    • UNIONUNION ALL과 동일하지만, 중복된 값을 제거하고 조회합니다.
  • 자식 테이블을 통합해서 쿼리하기 어려움

 

 

정리

  • 조인 전략
    • 부모 테이블과 각각 자식 테이블로 변환(슈퍼타입과 서브타입을 조인하여 조회)
    • 일반적으로 사용하는 것을 추천
  • 단일 테이블 전략
    • 통합 테이블로 변환(하나의 테이블에서 관리)
    • 비지니스가 매우 단순하고 확장 가능성이 없을때 사용하는 것을 추천
  • 구현 클래스마다 테이블 전략
    • 서브 타입 테이블로 변환(슈퍼타입을 없애고, 서브타입으로만 사용)
    • 비추천

 

 

 

 

728x90
반응형