JPA(Java Persistence API)

[JPA] Part 8. 상속관계 매핑

cwchoiit 2023. 10. 22. 13:19
728x90
반응형
SMALL
728x90
반응형
SMALL

이제 상속관계를 매핑하는 법도 알아보자. 왜냐하면 객체는 상속을 받을 수 있으니까. 

우선은 관계형 데이터베이스는 상속 관계가 없다. 대신 슈퍼타입 서브타입 관계라는 모델링 기법이 있고 그 기법이 객체 상속과 유사하다.

 

그래서, 상속관계 매핑을 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑해 보는 것이 주제다.

위 그림에서 ITEM이라는 테이블이 있고 그 테이블이 가지는 컬럼을 모두 동일하게 가지는 3개의 테이블 Album, Movie, Book이 있다고 가정해 보자. 이 경우 데이터베이스에서는 3가지 전략으로 테이블을 구성할 수 있다.

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

 

 

조인 전략

조인 전략은 상위 테이블인 ITEM의 기본키를 각 테이블이 기본키이자 외래키로 가지는 방법이다. 다음 그림이 이를 설명한다.

ITEM 테이블에서 공통으로 사용되는 NAME, PRICE를 가지고 있으며 각 테이블마다 개인적으로 필요한 데이터는 각 테이블이 관리하는 방법이다. 그리고 이 때, ITEM 테이블에서는 어떤 테이블과 연결된 데이터인지를 구분 짓기 위해 DTYPE이라는 필드가 추가된다. 데이터를 가장 정교화된 방식으로 모델링을 깔끔하게 한 모델이다.

 

객체로 이를 구현해보면 다음과 같다.

Item Class

package org.example.entity.inheritance;

import javax.persistence.*;

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

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;

    public Item() {}

    public Item(String name, int price) {
        this.name = name;
        this.price = price;
    }
}

가장 최상위인 아이템 클래스는 살펴볼 점이 두 가지가 있다. 

첫 번째, @Inheritance는 이 클래스를 상속받을 클래스들과 어떤식으로 테이블이 구성될지를 알려주는 어노테이션이다.

이 전략의 기본값은 SINGLE인데 이는 위에서 말한 단일 테이블 전략을 말한다. 그래서 JOINED로 변경했다.

 

두 번째, @DiscriminatorColumn은 위에서 표기한 DTYPE을 칼럼으로 추가한다는 어노테이션이다. 기본 칼럼명은 'DTYPE'이다.

 

Album Class

package org.example.entity.inheritance;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("Album")
public class Album extends Item {

    private String artist;

    public Album(String name, int price, String artist) {
        super(name, price);
        this.artist = artist;
    }

    public Album() {
        super();
    }
}

Item 클래스를 상속받을 Album이다. 이 클래스는 @ID가 필요없다. 왜냐하면 Item의 Id를 PK이자 FK로 받을 거니까.

@DiscriminatorValue("Album")은 Item의 DTYPE값으로 들어갈 Value를 지정하는 것. 기본값은 엔티티명이 된다. 즉, 위 예시에서 굳이 값을 입력할 필요는 없었지만 저렇게 변경할 수 있다는 것을 기억하기 위해 "Album"을 입력했다.

 

Book Class

package org.example.entity.inheritance;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

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

    public Book() {
        super();
    }

    public Book(String name, int price, String author, String isbn) {
        super(name, price);
        this.author = author;
        this.isbn = isbn;
    }
}

Movie Class

package org.example.entity.inheritance;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

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

    public Movie() {
        super();
    }

    public Movie(String name, int price, String director, String actor) {
        super(name, price);
        this.director = director;
        this.actor = actor;
    }
}

 

 

실행

package org.example;

import org.example.entity.inheritance.Movie;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        try {
            Movie movie = new Movie("반지의 제왕", 50000, "감독", "배우");
            em.persist(movie);

            tx.commit();
        } catch (Exception e) {
            System.out.println(e.getMessage());
            tx.rollback();
        } finally {
            em.close();
            emf.close();
        }
    }
}

 

결과

 

조회

Movie 데이터를 DB로부터 가져올 때 JPA는 당연하게도 조인을 사용한다. 이 방식 자체가 조인 전략이니까. 

package org.example;

import org.example.entity.inheritance.Movie;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        try {
            Movie movie = new Movie("반지의 제왕", 50000, "감독", "배우");
            em.persist(movie);

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

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

            tx.commit();
        } catch (Exception e) {
            System.out.println(e.getMessage());
            tx.rollback();
        } finally {
            em.close();
            emf.close();
        }
    }
}

로그 및 SQL문:

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        Item
        (name, price, DTYPE, ITEM_ID) 
    values
        (?, ?, 'Movie', ?)
Hibernate: 
    insert 
    into
        Movie
        (actor, director, ITEM_ID) 
    values
        (?, ?, ?)
Hibernate: 
    select
        movie0_.ITEM_ID as item_id2_3_0_,
        movie0_1_.name as name3_3_0_,
        movie0_1_.price as price4_3_0_,
        movie0_.actor as actor1_6_0_,
        movie0_.director as director2_6_0_ 
    from
        Movie movie0_ 
    inner join
        Item movie0_1_ 
            on movie0_.ITEM_ID=movie0_1_.ITEM_ID 
    where
        movie0_.ITEM_ID=?
findMovie = 감독

보면 알겠지만, INSERT문이 두 번 들어가고 조회할 땐 조인을 해서 가져오기 때문에 아무래도 단일 테이블 전략에 비해 성능이 저하될 수 있다. 근데 사실 뭐 INSERT문 두번 한다고 해서 성능에 큰 차이는 없지만 그래도 뭐..

 

 

단일 테이블 전략

단일 테이블 전략은 하나의 테이블에서 모든 데이터를 다 집어넣으면 된다. 다른 게 없다. 성능적인 이점을 가져갈 순 있다. 조인도 필요 없고 INSERT문도 한 번만 하면 되니까. 그 대신 NULL값이 지저분할 순 있겠지.

 

우선 그림으로 살펴보면 다음과 같다.

정말 한 테이블에 다 넣은 것. 얘는 조인 테이블과 달리 반드시 DTYPE 칼럼이 들어가야 한다. 조인 테이블은 DTYPE 컬럼이 없어도 가능하지만 이 테이블은 그렇게 되면 안 된다. 물론 JPA가 @DiscriminatorColumn 어노테이션을 붙이지 않더라도 알아서 SINGLE_TABLE인 경우에 DTYPE을 추가해 준다. 반드시 필요한 데이터니까.

 

이를 코드상으로 구현해 보면 딱 하나만 변경해 주면 된다.

package org.example.entity.inheritance;

import javax.persistence.*;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 이 부분
@DiscriminatorColumn
public abstract class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;

    public Item() {}

    public Item(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }
}

@Inheritance의 전략을 SINGLE_TABLE로 변경하면 된다.

 

결과는 다음과 같다.

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        Item
        (name, price, actor, director, DTYPE, ITEM_ID) 
    values
        (?, ?, ?, ?, 'Movie', ?)
Hibernate: 
    select
        movie0_.ITEM_ID as item_id2_1_0_,
        movie0_.name as name3_1_0_,
        movie0_.price as price4_1_0_,
        movie0_.actor as actor8_1_0_,
        movie0_.director as director9_1_0_ 
    from
        Item movie0_ 
    where
        movie0_.ITEM_ID=? 
        and movie0_.DTYPE='Movie'
findMovie = 감독

INSERT문 한 번에 조회 시에도 조인이 필요 없어진다.

 

 

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

아이템 테이블을 아예 없애고, 아이템 테이블이 가지고 있는 필드들을 각 테이블 (Movie, Book, Album)이 모두 가지는 경우를 말한다.

그림으로 보면 다음과 같다.

각 테이블이 모두 NAME, PRICE라는 칼럼을 동일하게 가지고 있고, PK를 ITEM_ID로 가지고 있는 그림이다.

이 경우도 마찬가지로 딱 한 부분만 변경하면 된다.

package org.example.entity.inheritance;

import javax.persistence.*;

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

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;

    public Item() {}

    public Item(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }
}

@Inheritance 어노테이션의 전략을 TABLE_PER_CLASS로 전략을 변경하고 실행해 보자. ITEM 테이블은 만들어지지 않는다. 

Hibernate: 
    
    create table Album (
       ITEM_ID bigint not null,
        name varchar(255),
        price integer not null,
        artist varchar(255),
        primary key (ITEM_ID)
    )
Hibernate: 
    
    create table Book (
       ITEM_ID bigint not null,
        name varchar(255),
        price integer not null,
        author varchar(255),
        isbn varchar(255),
        primary key (ITEM_ID)
    )
Hibernate: 
    
    create table Movie (
       ITEM_ID bigint not null,
        name varchar(255),
        price integer not null,
        actor varchar(255),
        director varchar(255),
        primary key (ITEM_ID)
    )
Hibernate: 
    insert 
    into
        Movie
        (name, price, actor, director, ITEM_ID) 
    values
        (?, ?, ?, ?, ?)
Hibernate: 
    select
        movie0_.ITEM_ID as item_id1_3_0_,
        movie0_.name as name2_3_0_,
        movie0_.price as price3_3_0_,
        movie0_.actor as actor1_6_0_,
        movie0_.director as director2_6_0_ 
    from
        Movie movie0_ 
    where
        movie0_.ITEM_ID=?
findMovie = 감독

 

근데, 결론은 이 구현 클래스마다 테이블 전략은 사용하지 말자. 왜냐하면 비효율적이다. 우선 같은 칼럼이 다 테이블마다 들어가는 것도 그렇지만 가장 큰 문제는 ITEM으로 조회했을 때 일어나는 현상 때문이다.

 

다음 코드를 보자. Item이 부모인데 당연히 Item으로 조회가 가능할 거고, 그때 일어나는 현상을 Hibernate가 찍어주는 로그로 확인해 보자.

package org.example;

import org.example.entity.inheritance.Item;
import org.example.entity.inheritance.Movie;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        try {
            Movie movie = new Movie("반지의 제왕", 50000, "감독", "배우");
            em.persist(movie);

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

            Item item = em.find(Item.class, movie.getId());
            System.out.println("Movie item = " + item.getName());

            tx.commit();
        } catch (Exception e) {
            System.out.println(e.getMessage());
            tx.rollback();
        } finally {
            em.close();
            emf.close();
        }
    }
}

위 코드를 실행했을 때 실행 결과:

Hibernate: 
    insert 
    into
        Movie
        (name, price, actor, director, ITEM_ID) 
    values
        (?, ?, ?, ?, ?)
Hibernate: 
    select
        item0_.ITEM_ID as item_id1_3_0_,
        item0_.name as name2_3_0_,
        item0_.price as price3_3_0_,
        item0_.author as author1_1_0_,
        item0_.isbn as isbn2_1_0_,
        item0_.artist as artist1_0_0_,
        item0_.actor as actor1_6_0_,
        item0_.director as director2_6_0_,
        item0_.clazz_ as clazz_0_ 
    from
        ( select
            ITEM_ID,
            name,
            price,
            author,
            isbn,
            null as artist,
            null as actor,
            null as director,
            1 as clazz_ 
        from
            Book 
        union
        all select
            ITEM_ID,
            name,
            price,
            null as author,
            null as isbn,
            artist,
            null as actor,
            null as director,
            2 as clazz_ 
        from
            Album 
        union
        all select
            ITEM_ID,
            name,
            price,
            null as author,
            null as isbn,
            null as artist,
            actor,
            director,
            3 as clazz_ 
        from
            Movie 
    ) item0_ 
where
    item0_.ITEM_ID=?
Movie item = 반지의 제왕

조회 시 Album, Book, Movie 이 세 가지를 다 UNION으로 묶어서 조회하는 끔찍한 일이 일어난다. 그러니까 결론은 쓰지 말자. 

 

마무리

그럼 조인 전략단일 테이블 전략 둘 중 사용하면 되는데, 각 장단점이 있다. 그때그때 필요한 더 적합한 방식을 사용하면 되는데 우선 조인 전략이 정규화된, 정석적인 방법이란 걸 알아 두자. 그러니까 기본은 조인 전략을 사용한다는 것을 전제하에 두고 시작하면 된다.

 

조인 전략

장점:

- 테이블 정규화

- 정석적인 방식

- 객체와 상호보완적

- 무결성 보존의 장점 (단일 테이블과 비교해서 NULL값이 들어가지 않는다)

 

단점:

- 조회 시 조인을 사용하니까 단일 테이블 전략에 비해 성능 저하 가능성

- 생성 시 INSERT문이 두 번 사용되니까 단일 테이블 전략에 비해 성능 저하 가능성

 

 

단일 테이블 전략

장점:

- 간단하다

- 조회 및 생성 시 조인을 사용하지 않고 INSERT문이 한 번으로 끝난다.

 

단점:

- 불 필요한 칼럼들에 대한 NULL값이 생긴다. (Movie 관련 데이터를 추가하면 Album, Book 관련 데이터는 모두 NULL)

728x90
반응형
LIST