DEV ℧ Developer Diary

[JPA] 고급 매핑

해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.

고급매핑

상속관계 매핑

  • 관계형 데이터베이스는 상속 관계라는 것이 없다.
  • 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사
  • 상속관계 매핑 : 객체의 상속과 구조와 DB의 슈퍼타입 서브타입 관계를 매핑

아래는 DB와 객체의 관계를 그림으로 도식화 한것입니다.

상속관계

위의 그림의 관계를 JPA에서는 여러가지 전략에 의해 상속관계를 표현 할 수 있습니다.

슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법에는 크게 세가지의 방법이 있습니다.

  1. 각각 테이블로 변환 -> 조인 전략
  2. 통합 테이블로 변환 -> 단일 테이블 전략
  3. 서브타입 테이블로 변환 -> 모든 클래스 테이블 구현 전략

주요 어노테이션

각각 전략에 대한 설명에 앞서 해당 전략들을 사용하기 위해서는 아래의 어노테이션을 알아야 한다.

  • @Inheritance(strategy=InheritanceType.XXX)
    • JOINED : 조인전략
    • SINGLE_TABLE : 단일 테이블 전략
    • TABLE_PER_CLASS : 구현 클래스마다 테이블 전략
  • @DiscriminatorColumn(name=“DTYPE”) (기본값 : DTYPE)
  • @DiscriminatorValue(“XXX”) (기본값 : Entity명)

1. 조인 전략

개요

조인 전략에 대한 기본적인 구조는 아래와 같다.

조인전략

위와 같은 그림을 아래의 예시 코드를 통해 테이블을 생성해 보자. 조인 전략은 상속할 부모 객체의 Entity에 @Inheritance(strategy = InheritanceType.JOINED) 해당 Annotation을 선언하면 사용 할 수 있는 전략이다.

예제 코드

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

    private String name;
    private int price;
}

@Entity
@Getter @Setter
@DiscriminatorValue("A")
public class Album extends Item{
    private String artist;
}

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

@Entity
@Getter @Setter
@DiscriminatorValue("M")
public class Movie extends Item{
    private String director;
    private String actor;
}
create table Album (
    artist varchar(255),
    id bigint not null,
    primary key (id)
)

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

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

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

# 테이블 생성 후 상속관계에 대한 foreign key 생성
alter table Album
   add constraint FKcve1ph6vw9ihye8rbk26h5jm9
   foreign key (id)
   references Item

alter table Book
   add constraint FKbwwc3a7ch631uyv1b5o9tvysi
   foreign key (id)
   references Item

alter table Movie
   add constraint FK5sq6d5agrc34ithpdfs0umo9g
   foreign key (id)
   references Item

이제 테이블을 만들었으면 데이터를 넣어서 어떻게 데이터가 들어가는지 확인해보자. 테스트 데이터는 얼마전에 정말 재밌게 본 탑건2의 데이터를 집어 넣고자 한다.

Movie movie = new Movie();
movie.setDirector("조셉 코신스키");
movie.setName("탑건2 매버릭");
movie.setActor("톰 크루즈");
movie.setPrice(9900);

entityManager.persist(movie);
entityTransaction.commit();
/* insert com.dhaudgkr.jpa04.joinstrategy.domain.Movie */

insert
    into
        Item
        (name, price, DTYPE, id)
    values
        (?, ?, 'M', ?)

/* insert com.dhaudgkr.jpa04.joinstrategy.domain.Movie */
insert
    into
        Movie
        (actor, director, id)
    values
        (?, ?, ?)

위의 Java코드에서 Movie Entity를 정의하고 데이터를 넣어주면, 각각 Item 과 Movie 테이블에 데이터가 insert되는 것을 볼 수 있다. 들어간 데이터는 아래와 같다.

조인전략2

그렇다면 데이터를 조회할 때는 어떻게 할까?

Movie movie = new Movie();
movie.setDirector("조셉 코신스키");
movie.setName("탑건2 매버릭");
movie.setActor("톰 크루즈");
movie.setPrice(9900);

entityManager.persist(movie);

entityManager.flush();
entityManager.clear();

Movie findMovie = entityManager.find(Movie.class, movie.getId());
System.out.println("findMovie = " + findMovie);

entityTransaction.commit();

입력한 Movie 데이터의 ID값을 얻기 위해 flush 해주고 해당 데이터를 조회해 봤다.

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=?

위와 같이 JPA에서 Movie 데이터가 필요할 경우 알아서 JOIN을 해서 데이터를 가져오는 것을 볼 수가 있다.

DTYPE

여기서 DTYPE이라는 컬럼이 있는데 해당 컬럼은 Item의 데이터가 어떤 Entity에 해당하는 데이터인지 구분값을 넣어준다.

위에서 구술한 @DiscriminatorColumn, @DiscriminatorValue 각각의 어노테이션을 선언해서 사용한다.

  • @DiscriminatorColumn의 경우 부모 Entity에 선언 하여 컬럼으로 만들어준다.
  • @DiscriminatorValue는 구분값 컬럼에 넣어줄 데이터를 지정한다.

장점

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

단점

  • 조회시 조인을 많이 사용, 성능 저하
  • 조회 쿼리가 복잡함
  • 데이터 저장시 INSERT SQL 2번 호출

2. 단일 테이블 전략

개요

싱글 테이블에 대한 기본적인 구조는 아래와 같다.

단일테이블 전략

위와 같은 구조의 테이블을 만들기 위해서는 @Inheritance 어노테이션에 SINGLE_TABLE를 선언해준다.

예제 코드

중복되는 나머지 코드는 생략하고 Item Entity만 보여주고자 한다.

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

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

위와 같이 모든 Entity가 합쳐진 하나의 테이블만 생성된다.

해당 데이터에서 동일하게 Movie Entity에 데이터를 넣고 조회해 보자.

Movie movie = new Movie();
movie.setDirector("조셉 코신스키");
movie.setName("탑건2 매버릭");
movie.setActor("톰 크루즈");
movie.setPrice(9900);

entityManager.persist(movie);

entityManager.flush();
entityManager.clear();

Movie findMovie = entityManager.find(Movie.class, movie.getId());
System.out.println("findMovie = " + findMovie);

entityTransaction.commit();
/* insert com.dhaudgkr.jpa04.singlestrategy.domain.Movie */
insert
    into
        Item
        (name, price, actor, director, DTYPE, id)
    values
        (?, ?, ?, ?, 'Movie', ?)

select
    movie0_.id as id2_0_0_,
    movie0_.name as name3_0_0_,
    movie0_.price as price4_0_0_,
    movie0_.actor as actor5_0_0_,
    movie0_.director as director6_0_0_
from
    Item movie0_
where
    movie0_.id=?
    and movie0_.DTYPE='Movie'

하나의 테이블에서 데이터가 입력되고 아래와 같이 조회되는 것을 확인 할 수 있다.

단일 테이블 전략3

단일 테이블같은 경우 주의할 점은 테이블 하나로 관리되기 때문에 DTYPE 같은 구분 컬럼이 필수로 들어가야 한다.

장점

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

단점

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

3. 모든 클래스 테이블 구현 전략

개요

해당 전략에 대한 기본적인 구조는 아래와 같다.

모든 클래스 테이블 구현 전략

간단하게 말하자면 Item에 대한 부모 테이블을 생성하지 않고 자식테이블에 해당 컬럼을 합쳐서 각각 테이블을 생성하는 전략이다.

위와 같은 구조의 테이블을 만들기 위해서는 @Inheritance 어노테이션에 TABLE_PER_CLASS를 선언해준다. 또한 해당 전략에서는 부모 클래스는 직접 만들어서 사용할 일이 없도록 추상 클래스(abstract)로 만들어서 사용합니다.

예제코드

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

    private String name;
    private int price;
}
create table Album (
    id bigint not null,
    name varchar(255),
    price integer not null,
    artist varchar(255),
    primary key (id)
)

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

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

추상 클래스로 선언한 Item 클래스는 만들어 지지않고 각각 Alnum, Movie, Book Entity에 대한 테이블이 생성 되는것을 볼 수 있다.

동일한 데이터를 통해 데이터를 넣고 조회해보자.

Movie movie = new Movie();
movie.setDirector("조셉 코신스키");
movie.setName("탑건2 매버릭");
movie.setActor("톰 크루즈");
movie.setPrice(9900);

entityManager.persist(movie);

entityManager.flush();
entityManager.clear();

Movie findMovie = entityManager.find(Movie.class, movie.getId());
System.out.println("findMovie = " + findMovie);

entityTransaction.commit();
/* insert com.dhaudgkr.jpa04.alltablestrategy.domain.Movie */
insert
    into
        Movie
        (name, price, actor, director, id)
    values
        (?, ?, ?, ?, ?)

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=?

위와 같은 쿼리를 사용하며 데이터는 아래와 같이 들어간다.

모든 클래스 테이블 구현 전략2

하지만 해당 전략에는 문제가 있다. Movie, Book, Album Entity의 부모테이블을 조회하면 상속받은 모든 테이블을 UNION해서 가져와야 하기 때문에 비효율적으로 볼 수 있다. 해당 예제 코드는 아래와 같다.

Movie movie = new Movie();
movie.setDirector("조셉 코신스키");
movie.setName("탑건2 매버릭");
movie.setActor("톰 크루즈");
movie.setPrice(9900);

entityManager.persist(movie);

entityManager.flush();
entityManager.clear();

/* 객체의 다형성을 이용해ㅑ Movie의 부모 객체인 Item을 조회 */
Item item = entityManager.find(Item.class, movie.getId());
System.out.println("findMovie = " + item);

entityTransaction.commit();
select
        item0_.id as id1_2_0_,
        item0_.name as name2_2_0_,
        item0_.price as price3_2_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_.artist as artist1_0_0_,
        item0_.clazz_ as clazz_0_
    from
        ( select
            id,
            name,
            price,
            actor,
            director,
            null as author,
            null as isbn,
            null as artist,
            1 as clazz_
        from
            Movie
        union
        all select
            id,
            name,
            price,
            null as actor,
            null as director,
            author,
            isbn,
            null as artist,
            2 as clazz_
        from
            Book
        union
        all select
            id,
            name,
            price,
            null as actor,
            null as director,
            null as author,
            null as isbn,
            artist,
            3 as clazz_
        from
            Album
    ) item0_
where
    item0_.id=?

위와 같이 UNION을 통해 데이터를 가져오는 무식한 방법이 사용되는 것을 볼 수 있다..

앞서 해당 전략은 데이터베이스 설계좌와 ORM 전문가 둘다에게 추천하지 않는 방식이다.

장점

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

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느림 (UNION SQL 사용)
  • 자식 테이블을 통합해서 쿼리하기 어려움

정리

크게 상황에 따라 사용을 하지만 주로 조인 전략이 객체입장에서 본다면 정석적인 전략이라고 볼 수 가 있다. INSERT SQL 가 2번 날라가는 경우도 크게 신경쓸정도로 성능이 저하되지 않는다. 단일 테이블 전략을 사용하는 경우는 정말 단순한 테이블을 설계할 때 해당 전략을 사용 할 수 있다. 마지막으로 구현클래스마다 테이블 전략은 새로운 타입이 추가될때마다 모든 클래스에 추가를 해줘야 하고, 만약 정산이라도 들어간다면 테이블마다 따로 돌려줘야하는 일이 생긴다. 따라서 실무에서 사용을 지양하는것이 좋은 전략 방법이다.