티스토리 뷰


상속관계 매핑

  • 객체는 상속관계가 존재하지만, 관계형 데이터베이스는 상속 관계가 없다.(대부분)

  • 그나마 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.

  • 상속관계 매핑이라는 것은 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

슈퍼타입 서브타입 논리 모델 -> 물리모델 구현 방법

  • 객체는 상속을 지원하므로 모델링과 구현이 똑같지만, DB는 상속을 지원하지 않으므로 논리 모델을 물리 모델로 구현할 방법이 필요하다.

  • DB의 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법은 세가지 있다.

  • 중요한건, DB입장에서 세가지로 구현하지만 JPA에서는 어떤 방식을 선택하던 매핑이 가능하다.

  • JPA가 이 세가지 방식과 매핑하려면

    • @Inheritance(strategy=InheritanceType.XXX)의 stategy를 설정해주면 된다.

      • default 전략은 SINGLE_TABLE(단일 테이블 전략)이다.

      • InheritanceType 종류

        • JOINED

        • SINGLE_TABLE

        • TABLE_PER_CLASS

    • @DiscriminatorColumn(name="DTYPE")

      • 부모 클래스에 선언한다. 하위 클래스를 구분하는 용도의 컬럼이다. 관례는 default = DTYPE

    • @DiscriminatorValue("XXX")

      • 하위 클래스에 선언한다. 엔티티를 저장할 때 슈퍼타입의 구분 컬럼에 저장할 값을 지정한다.

      • 어노테이션을 선언하지 않을 경우 기본값으로 클래스 이름이 들어간다.

  • 객체의 상속관계 구현

    • Item

      @Entity
      @Inheritance(strategy = InheritanceType.XXX) // 상속 구현 전략 선택
      public class Item {

         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private Long id;

         private String name;
         private int price;
      }
    • Album

      @Entity
      public class Album extends Item {

         private String artist;
      }
    • Movie

      @Entity
      public class Movie extends Item {

         private String director;
         private String actor;
      }
    • Book

      @Entity
      public class Book extends Item {

         private String author;
         private String isbn;
      }

1 - 각각의 테이블로 변환하는 조인 전략(JOINED)

  • 가장 정규화 된 방법으로 구현하는 방식이다.

  • NAME, PRICE가 ITEM 테이블에만 저장되고, ALBUM, MOVIE, BOOK이 각자의 데이터만 저장한다.

  • Item 엔티티 - @Inheritance(strategy = InheritanceType.JOINED) 전략

    • 하이버네이트의 조인 전략에서는 @DiscriminatorColumn을 선언하지 않으면 DTYPE 컬럼이 생성되지 않는다.

    • 어차피 조인하면 앨범인지 무비인지 알 수 있다. 그래도, DTYPE을 넣어주는 것이 명확하다. 넣어주자.

    @Entity
    @Inheritance(strategy = InheritanceType.JOINED)
    @DiscriminatorColumn // 하위 테이블의 구분 컬럼 생성(default = DTYPE)
    public class Item {

       @Id
       @GeneratedValue(strategy = GenerationType.IDENTITY)
       private Long id;

       private String name;
       private int price;
    }
  • 실제 실행된 DDL

    • 테이블 4개 생성

    • 하위 테이블에 외래키 제약조건 생성. 하위 테이블 입장에서는 ITEM_ID가 PK이면서 FK로 잡아야 한다.

    • 조인 전략에 맞는 테이블들이 생섬됨.

    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 generated by default as identity,
          name varchar(255),
          price integer not null,
          primary key (id)
      )
    Hibernate:
       create table Movie (
          actor varchar(255),
          director varchar(255),
          id bigint not null,
          primary key (id)
      )
       
       
    Hibernate:
       alter table Album
          add constraint FKcve1ph6vw9ihye8rbk26h5jm9
          foreign key (id)
          references Item
    Hibernate:
       alter table Book
          add constraint FKbwwc3a7ch631uyv1b5o9tvysi
          foreign key (id)
          references Item
    Hibernate:
       alter table Movie
          add constraint FK5sq6d5agrc34ithpdfs0umo9g
          foreign key (id)
          references Item
  • Movie 객체를 저장하면?

    • Insert 쿼리가 두개 나간다.

    • Item 테이블, Movie 테이블 저장.

    • DTYPE에 클래스 이름이 디폴트로 저장됨.

    Movie movie = new Movie();
    movie.setDirector("감독A");
    movie.setActor("배우B");
    movie.setName("분노의질주");
    movie.setPrice(35000);

    em.persist(movie);

    tx.commit();
    Hibernate: 
       /* insert advancedmapping.Movie
           */ insert
           into
              Item
              (id, name, price, DTYPE)
           values
              (null, ?, ?, 'Movie')
    Hibernate:
       /* insert advancedmapping.Movie
           */ insert
           into
              Movie
              (actor, director, id)
           values
              (?, ?, ?)
  • Movie 객체를 조회하면?

    • flush(), clear() 해주면, DB에 insert쿼리 날리고, 1차 캐시 지우므로 find에서 SELECT 쿼리가 나간다.

    • Item과 inner join을 통해서 결과를 조회한다.

    Movie movie = new Movie();
    movie.setDirector("감독A");
    movie.setActor("배우B");
    movie.setName("분노의질주");
    movie.setPrice(35000);

    em.persist(movie);

    em.flush();
    em.clear();  //DB에 insert쿼리 날리고, 1차 캐시 지우므로 find에서 SELECT 쿼리가 나간다.

    em.find(Movie.class, movie.getId());

    tx.commit();
    Hibernate: 
      select
          movie0_.id as id2_2_0_,
          movie0_1_.name as name3_2_0_,
          movie0_1_.price as price4_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=?

2 - 통합 테이블로 변환하는 단일 테이블 전략(SINGLE_TABLE)

  • 서비스 규모가 크지 않고, 굳이 조인 전략을 선택해서 복잡하게 갈 필요가 없다고 판단 될 때에는

  • 한 테이블에 다 저장하고, DTYPE으로 구분하는 단일 테이블 전략을 선택할 수 있다.

  • INSERT 쿼리도 한 번, SELECT 쿼리도 한 번이다. 조인할 필요가 없고, 성능이 좋다.

  • 단일 테이블 적용

    • strategy를 SINGLE_TABLE로 변경하면 끝난다.

      • JPA의 장점이다. 테이블 구조의 변동이 일어났는데, 코드를 거의 손대지 않고 어노테이션만 수정했다.

      • 만약 JPA를 쓰지 않았더라면, 테이블 구조의 변경이 일어나면 거의 모든 코드를 손대야 할 것이다.

    • 단일 테이블 전략에서는 @DiscriminatorColumn을 선언해 주지 않아도, 기본으로 DTYPE 컬럼이 생성된다.

    • 한 테이블에 모든 컬럼을 저장하기 때문에, DTYPE 없이는 테이블을 판단할 수 없다.

      @Entity
      @DiscriminatorColumn
      @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
      public class Item {

         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private Long id;

         private String name;
         private int price;
      }
    • 실행된 DDL

      • 통합 테이블이 하나 생성된다.

      Hibernate: 
         create table Item (
            DTYPE varchar(31) not null,
            id bigint generated by default as identity,
            name varchar(255),
            price integer not null,
            artist varchar(255),
            author varchar(255),
            isbn varchar(255),
            actor varchar(255),
            director varchar(255),
            primary key (id)
        )
    • 조인 전략에서 실습했던 Movie 저장, 조회 예제를 그대로 돌려보면?

      • Item 테이블을 그냥 조회한다. 조인하지 않고, DTYPE을 검색 조건으로 추가해서 Movie를 조회

        Hibernate: 
           select
              movie0_.id as id2_0_0_,
              movie0_.name as name3_0_0_,
              movie0_.price as price4_0_0_,
              movie0_.actor as actor8_0_0_,
              movie0_.director as director9_0_0_
           from
              Item movie0_
           where
              movie0_.id=?
               and movie0_.DTYPE='Movie'

3 - 서브타입 테이블로 변환하는 구현 클래스마다 테이블을 생성하는 전략(TABLE_PER_CLASS)

  • 조인 전략과 유사하지만, 슈퍼 타입의 컬럼들을 서브 타입으로 내린다. NAME, PRICE 컬럼들이 중복되도록 허용하는 전략이다.

  • 구현 클래스마다 테이블 생성 전략 적용

    • @Id 생성 전략 GenerationType.AUTO를 사용하는 경우 strategy를 TABLE_PER_CLASS로 변경하고

    • 이때, ITEM 엔티티는 실제 생성되는 테이블이 아니므로 abstract 클래스여야 하고, @DiscriminatorColumn도 필요가 없어진다.

    • Item 엔티티 설정

      @Entity
      @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
      public abstract class Item {

         @Id
         @GeneratedValue(strategy = GenerationType.AUTO)
         private Long id;

         private String name;
         private int price;
      }
    • 생성된 DDL

      • 하위 테이블 3개만 생성된다.

        Hibernate:
           create table Album (
              id bigint not null,
               name varchar(255),
               price integer not null,
               artist varchar(255),
               primary key (id)
          )
        Hibernate:
           create table Book (
              id bigint not null,
               name varchar(255),
               price integer not null,
               author varchar(255),
               isbn varchar(255),
               primary key (id)
          )
        Hibernate:
           create table Movie (
              id bigint not null,
               name varchar(255),
               price integer not null,
               actor varchar(255),
               director varchar(255),
               primary key (id)
          )
    • 조인 전략에서의 Movie 저장, 조회를 그대로 실행한 결과는?

      • Movie 테이블에 insert

      • Movie 테이블에서 select

      Hibernate: 
         /* insert advancedmapping.Movie
             */ insert
             into
                 Movie
                (name, price, actor, director, id)
             values
                (?, ?, ?, ?, ?)
      Hibernate:
         select
             movie0_.id as id1_2_0_,
             movie0_.name as name2_2_0_,
             movie0_.price as price3_2_0_,
             movie0_.actor as actor1_3_0_,
             movie0_.director as director2_3_0_
         from
             Movie movie0_
         where
             movie0_.id=?
    • 문제점

      • 객체지향 프로그래밍에서는 MOVIE, ALBUM, BOOK 객체를 ITEM 타입으로도 조회할 수 있다.

        Movie movie = new Movie();
        movie.setDirector("감독A");
        movie.setActor("배우B");
        movie.setName("분노의질주");
        movie.setPrice(35000);

        em.persist(movie);

        em.flush();
        em.clear();

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

        tx.commit();
      • 실행된 SQL

        • union all로 전체 하위 테이블을 다 찾는다.

        • INSERT 까진 심플했으나, 조회가 시작되면 굉장히 비효율적으로 동작한다.

        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_.author as author1_1_0_,
              item0_.isbn as isbn2_1_0_,
              item0_.actor as actor1_3_0_,
              item0_.director as director2_3_0_,
              item0_.clazz_ as clazz_0_
           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
          ) item0_
        where
          item0_.id=?

상속관계 매핑 정리

  • 조인 전략

    • 장점

      • 테이블이 정규화가 되어있고,

      • 외래 키 참조 무결성 제약조건 활용 가능

        • ITEM의 PK가 ALBUM, MOVIE, BOOK의 PK이자 FK이다. 그래서 다른 테이블에서 아이템 테이블만 바라보도록 설계하는 것이 가능 하다

      • 저장공간 효율화

        • 테이블 정규화로 저장공간이 딱 필요한 만큼 소비된다.

    • 단점

      • 조회시 조인을 많이 사용한다. 단일 테이블 전략에 비하면 성능이 안나온다. 조인하니까.

      • 그래서 조회 쿼리가 복잡하다

      • 데이터 저장시에 INSERT 쿼리가 상위, 하위 테이블 두번 발생한다.

    • 정리

      • 성능 저하라고 되어있지만, 실제로는 영향이 크지 않다.

      • 오히려 저장공간이 효율화 되기 때문에 장점이 크다.

      • 기본적으로는 조인 정략이 정석이라고 보면 된다. 객체랑도 잘 맞고, 정규화도 되고, 그래서 설계가 깔끔하게 나온다.

  • 단일 테이블 전략

    • 장점

      • 조인이 필요 없으므로 일반적인 조회 성능이 빠르다.

      • 조회 쿼리가 단순핟.

    • 단점

      • 자식 엔티티가 매핑한 컬럼은 모두 NULL을 허용해야 한다.

      • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.

      • 상황에 따라서 조인 전략보다 성능이 오히려 느려질 수 있다.

        • 보통 이 상황에 해당하는 임계점을 넘을 일은 많지 않다.

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

    • 결론은

      • 이 전략은 쓰지말자.

      • ORM을 하다보면 데이터 쪽과 객체 쪽에서 trade off를 할 때가 있는데, 이 전략은 둘 다 추천하지 않는다.

    • 장점

      • 서브 타입을 명확하게 구분해서 처리할 때 효과적이다

      • NOT NULL 제약조건을 사용할 수 있다.

    • 단점

      • 여러 자식 테이블을 함께 조회할 때 성능이 느리다(UNION SQL)

      • 자식 테이블을 통합해서 쿼리하기 어렵다.

      • 변경이라는 관점으로 접근할 때 굉장히 좋지 않다.

        • 예를 들어, ITEM들을 모두 정산하는 코드가 있다고 가정할 때, ITEM 하위 클래스가 추가되면 정산 코드가 변경된다. 추가된 하위 클래스의 정산 결과를 추가하거나 해야 한다.

  • 상속관계 매핑 정리

    • 기본적으로는 조인 전략을 가져가자.

    • 그리고 조인 전략과 단일 테이블 전략의 trade off를 생각해서 전략을 선택하자.

    • 굉장히 심플하고 확장의 가능성도 적으면 단일 테이블 전략을 가져가자. 그러나 비즈니스 적으로 중요하고, 복잡하고, 확장될 확률이 높으면 조인 전략을 가져가자.

Reference







댓글