본문 바로가기
프로그래밍이야기/JPA

[JPA] 상속 전략 정리

by 사랑꾼이야 2022. 2. 6.
반응형

JPA에서 상속 관계 전략을 세우는 방법 정리

자바에서 상속의 대해서는 많이 접해보았지만 최근 JPA를 통해서 상속에 대해서 어떻게 대응을 해야되는지 공부를 하다가 관련 내용을 정리하려고 합니다.

자바 ORM 표준 JPA 프로그래밍에서는 다음과 같이 설명을 하고 있습니다.

관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없다. 대신에 슈퍼타입과 서브타입 관계라는 모델링 기법이 객체의 상속 개념과 가장 유사하다. ORM에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

그렇다면 객체의 상속 관계 매핑을 위해서 어떻게 해야 할까요?

슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현하기 위해서는 3가지 방법을 선택할 수 있습니다.

  • 조인 전략 : 부모와 모든 자식 객체를 각각 테이블로 만들고 조회할 때 조인을 사용합니다.
  • 단일 테이블 전략 : 하나의 테이블만 사용해서 통합합니다.
  • 구현 클래스마다 테이블 전략 : 자식 객체마다 부모 객체를 포함하여 하나의 테이블을 만들어서 사용합니다.

해당 내용을 정리하기 위해서 사용한 소스는 Github에서 확인하실 수 있습니다. - 소스

조인 전략

엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략입니다. 따라서 조회할 때 조인을 자주 사용합니다. 이 전략을 사용할 때 주의할 점이 있는데, 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없습니다. 따라서 타입을 구분하는 컬럼을 추가해야 합니다.

이해를 돕기 위해서 직업이라는 부모 테이블이 존재하고 각 자식 테이블로 개발자, 연구원, 영업사원으로 구성된 테이블이 존재한다고 가정하겠습니다. 도메인 구성은 다음과 같습니다.

joined_1.png

조인 전략 매핑

직업 테이블을 구성하는 소스를 먼저 살펴보면 다음과 같습니다.

직업 테이블

joined_2

  • @Inheritance(strategy = JOINED): 상속 관계 전략을 설정하는 어노테이션이며, 여기서는 JOINED 로 설정하였습니다.
  • @DiscriminatorColumn: 부모 클래스에 구분 컬럼을 지정합니다. 기본 값이 DTYPE이므로 @DiscriminatorColumn으로만 선언해도 됩니다.

자식 테이블인 개발자, 연구원, 영업사원 3개를 연속해서 살펴보면 다음과 같습니다. 테스트용으로 만들었기 때문에 필드가 많지는 않습니다.

영업사원 테이블

joined_3

개발자 테이블

joined_5.png

연구원 테이블

joined_4.png

  • DiscriminatorValue(""): 직업 테이블에서 선언된 @DiscriminatorColumn의 기본 값인 DTYPE 필드에 저장되는 값이며, 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정합니다.

생성 로그

    create table job (
       dtype varchar(31) not null,
        job_id bigint generated by default as identity,
        name varchar(255),
        salary integer,
        primary key (job_id)
    )

    create table developer (
       skill varchar(255),
        job_id bigint not null,
        primary key (job_id)
    )

    create table researcher (
       title varchar(255),
        job_id bigint not null,
        primary key (job_id)
    )

    create table sales (
       amount integer,
        job_id bigint not null,
        primary key (job_id)
    )

    alter table developer 
       add constraint FK9wvkqrgtvg058m2k1l57gpl0l 
       foreign key (job_id) 
       references job

    alter table researcher 
       add constraint FKs9piex8wwmtsbbgoiky1yqua7 
       foreign key (job_id) 
       references job

    alter table sales 
       add constraint FKdjs9ufnj9wc9oa25vkaa8hvsv 
       foreign key (job_id) 
       references job
  • job 테이블의 dtype 필드가 생성된 것을 확인할 수 있습니다. 기본 사이즈는 31 이며 NOT NULL 입니다.
  • developer, researcher, sales 테이블에 job 테이블의 id인 job_id 가 pk로 잡혀 있습니다. 데이터 조회시 조인 전략을 활용하기 때문에 각 pk로 설정되어 있습니다.

테스트

개발자를 생성해보는 테스트를 진행해보겠습니다.

joined_6.png

  • Developer 객체를 팩토리 메소드를 이용해서 생성한 뒤 save하였습니다.
  • 저장된 id로 findById로 조회하였고 그 결과를 테스트 하였습니다.

테스트 결과는 성공하였습니다.

joined_7.png

저장 로그를 보면 다음과 같습니다.

    insert 
    into
        job
        (name, salary, dtype, job_id) 
    values
        (?, ?, 'D', ?)
2022-02-06 23:59:00.476 TRACE 11653 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Lee]
2022-02-06 23:59:00.477 TRACE 11653 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [5000]
2022-02-06 23:59:00.479 TRACE 11653 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]


    insert 
    into
        developer
        (skill, job_id) 
    values
        (?, ?)
2022-02-06 23:59:00.481 TRACE 11653 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Java]
2022-02-06 23:59:00.481 TRACE 11653 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
  • save를 한번만 호출하였지만 job 테이블과 developer 테이블의 각각 같은 pk(JOB_ID)로 저장된 것을 확인할 수 있습니다.

조회 로그도 확인해보겠습니다.

    select
        jobs0_.job_id as job_id2_2_0_,
        jobs0_.name as name3_2_0_,
        jobs0_.salary as salary4_2_0_,
        jobs0_1_.skill as skill1_0_0_,
        jobs0_2_.title as title1_3_0_,
        jobs0_3_.amount as amount1_4_0_,
        jobs0_.dtype as dtype1_2_0_ 
    from
        job jobs0_ 
    left outer join
        developer jobs0_1_ 
            on jobs0_.job_id=jobs0_1_.job_id 
    left outer join
        researcher jobs0_2_ 
            on jobs0_.job_id=jobs0_2_.job_id 
    left outer join
        sales jobs0_3_ 
            on jobs0_.job_id=jobs0_3_.job_id 
    where
        jobs0_.job_id=?
2022-02-06 23:59:00.505 TRACE 11653 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-02-06 23:59:00.512 TRACE 11653 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([dtype1_2_0_] : [VARCHAR]) - [D]
2022-02-06 23:59:00.515 TRACE 11653 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([name3_2_0_] : [VARCHAR]) - [Lee]
2022-02-06 23:59:00.516 TRACE 11653 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([salary4_2_0_] : [INTEGER]) - [5000]
2022-02-06 23:59:00.516 TRACE 11653 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([skill1_0_0_] : [VARCHAR]) - [Java]
  • findById를 호출한 쿼리를 보면 각 자식 테이블을 조인하여서 결과를 조회한 것을 확인할 수 있습니다.

정리

장점

  • 필요한 데이터가 각각 테이블의 저장되어서 중복된 데이터가 저장되지 않습니다. 즉 테이블이 정규화됩니다.
  • 부모의 기본키가 자식 테이블의 외래키로 지정되기 때문에 참조 하는 값을 가지고 있습니다. 즉 외래 키 참조 무결성 제약 조건을 활용할 수 있습니다.

단점

  • 조회 쿼리를 보시면 알듯이, 조인이 많이 사용되므로 성능의 영향을 줄 수 있습니다.
  • 단순 조회이지만 쿼리 조건이 상당히 복잡해집니다. 자식 테이블이 많아진다면 더 복잡해질 수 있습니다.
  • 데이터 등록시 INSERT SQL이 2번 실행되어야 합니다.

단일 테이블 전략

테이블을 하나만 사용한다. 그리고 구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다. 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다. 이 전략을 사용할 때 주의점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다는 점이다.

이해를 돕기 위해서 품목이라는 부모 테이블이 존재하고 각 자식 테이블로 앨범, 영화, 도서로 구성된 테이블이 존재한다고 가정하겠습니다. 도메인 구성은 다음과 같습니다.

single_1.png

단일 테이블 전략 매핑

품목 테이블을 구성하는 소스를 먼저 살펴보면 다음과 같습니다.

품목 테이블

single_2.png

  • Inheritance(strategy = SINGLE_TABLE) : 싱글 테이블 전략을 사용합니다. 테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 합니다.

자식 테이블인 앨범, 영화, 도서 3개를 연속해서 살펴보면 다음과 같습니다. 테스트용으로 만들었기 때문에 필드가 많지는 않습니다.

앨범 테이블

single_3.png

영화 테이블

single_5.png

도서 테이블

single_4.png

  • @DiscriminatorValue("") : 품목 테이블에서 선언된 @DiscriminatorColumn의 기본 값인 DTYPE 필드에 저장되는 값이며, 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정합니다.

생성 로그

    create table item (
       dtype varchar(31) not null,
        item_id bigint not null,
        name varchar(255),
        price integer,
        stock_quantity integer,
        artist varchar(255),
        etc varchar(255),
        author varchar(255),
        isbn varchar(255),
        actor varchar(255),
        director varchar(255),
        primary key (item_id)
    )
  • item 테이블의 dtype필드가 생성된 것을 확인할 수 있습니다. 기본 사이즈는31이며NOT NULL 입니다.
  • Album, Movie, Book 테이블에 변수들이 item테이블의 모두 생성된 것을 확인할 수 있습니다.

테스트

책을 생성해보는 테스트를 진행해보겠습니다.

single_6.png

  • book 객체를 팩토리 메소드를 이용해서 생성한 뒤 save하였습니다.
  • 저장된 id로 findById로 조회하였고 그 결과를 테스트 하였습니다.

테스트 결과는 성공하였습니다.

single_7.png

저장 로그를 보면 다음과 같습니다.

    insert 
    into
        item
        (name, price, stock_quantity, author, isbn, dtype, item_id) 
    values
        (?, ?, ?, ?, ?, 'B', ?)
2022-02-07 00:01:49.899 TRACE 11944 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [자바의정석]
2022-02-07 00:01:49.901 TRACE 11944 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [15000]
2022-02-07 00:01:49.901 TRACE 11944 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [INTEGER] - [1]
2022-02-07 00:01:49.901 TRACE 11944 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [VARCHAR] - [LEE]
2022-02-07 00:01:49.902 TRACE 11944 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [5] as [VARCHAR] - [12312313]
2022-02-07 00:01:49.903 TRACE 11944 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [6] as [BIGINT] - [1]
  • 싱글 테이블 전략이기 때문에 save를 한번만 호출합니다.

조회 로그도 확인해보겠습니다.

    select
        item0_.item_id as item_id2_1_0_,
        item0_.name as name3_1_0_,
        item0_.price as price4_1_0_,
        item0_.stock_quantity as stock_qu5_1_0_,
        item0_.artist as artist6_1_0_,
        item0_.etc as etc7_1_0_,
        item0_.author as author8_1_0_,
        item0_.isbn as isbn9_1_0_,
        item0_.actor as actor10_1_0_,
        item0_.director as directo11_1_0_,
        item0_.dtype as dtype1_1_0_ 
    from
        item item0_ 
    where
        item0_.item_id=?
2022-02-06 22:17:01.955 TRACE 4417 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-02-06 22:17:01.960 TRACE 4417 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([dtype1_1_0_] : [VARCHAR]) - [B]
2022-02-06 22:17:01.963 TRACE 4417 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([name3_1_0_] : [VARCHAR]) - [자바의정석]
2022-02-06 22:17:01.964 TRACE 4417 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([price4_1_0_] : [INTEGER]) - [15000]
2022-02-06 22:17:01.964 TRACE 4417 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([stock_qu5_1_0_] : [INTEGER]) - [1]
2022-02-06 22:17:01.964 TRACE 4417 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([author8_1_0_] : [VARCHAR]) - [LEE]
2022-02-06 22:17:01.965 TRACE 4417 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([isbn9_1_0_] : [VARCHAR]) - [12312313]
  • findById를 호출한 쿼리를 보면, 싱글 테이블 전략이기 때문에 조인 없이 조회합니다.

정리

장점

  • 조인이 필요 없으므로 일반적으로 조회 성능이 빠릅니다.
  • 조회 쿼리를 보시면 알겠지만 단순합니다.

단점

  • 자식 엔티티가 매핑한 컬럼 모두 null을 허용해야 합니다.
  • 단일 테이블에 모든 것을 저장하고 있기 때문에 테이블이 커질 수 있습니다. 그러므로 상황에 따라서 조회 성능이 오히려 느려질 수 있습니다.

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

자식 엔티티마다 테이블을 만든다. 그리고 자식 테이블 각각에 필요한 컬럼이 모두 있다. 이 전략은 일반적으로 추천하지 않는 전략이다.

정리

장점

  • 자식 테이블의 각각 필요한 컬럼이 모두 존재하기 때문에 서브 타입을 구분해서 처리할 때 효과적입니다.
  • not null 제약조건을 사용할 수 있습니다.

단점

  • 단일 테이블로 여러 자식 테이블이 구성되어 있기 때문에 함께 조회할 때 UNION을 사용해야 합니다. 그래서 성능이 느립니다.
  • 그래서 자식 테이블을 통합해서 쿼리하기 어렵습니다.

자주 사용하지 않는 방식이기 때문에 여기서는 다루지 않겠습니다.

반응형

댓글