ORM 표준 JPA 프로그래밍

이승효

Updated:

Intro

김영한님의 ORM 표준 JPA 프로그래밍 기본편을 듣고 정리한 내용을 공유합니다.
이전편에서 리뷰한 내용과 다른내용으로 넣어봤습니다.

상속관계 매핑

  • 객체는 상속관계를 가지고 있지만 관계형 DB는 상속 관계가X
  • 슈퍼타입 서브타입 관계 모델링 기법이 객체 상속과 유사
  • 상속관계 매핑이라는 것은 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

슈퍼타입/서브타입 으로 구성된 논리모델을 객체로 표현 orm-jpa_1

  • 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법
    • 각각 테이블로 변환 -> 조인 전략
    • 통합 테이블로 변환 -> 단일 테이블 전략
    • 서브타입 테이블로 변환 -> 구현 클레스마다 테이블 생성 전략

테이블 변환 전략

  • @Inheritance(strategy=InheritanceType.XXX)
    • JOINED: 조인 전략
    • SINGLE_TABLE: 단일 테이블 전략
    • TABLE_PER_CLASS: 구현 클래스마다 테이블 생성 전략
  • @DiscriminatorColumn(name=“DTYPE”)
    • 부모 클래스에 선언한다. 하위 클래스를 구분하는 용도의 컬럼이다.
  • @DiscriminatorValue(“XXX”)
    • 하위 클래스에 선언한다. 데이터를 저장할 때 슈퍼타입의 구분 컬럼에 저장할 값을 지정한다.
    • 어노테이션을 선언하지 않을 경우 기본값으로 엔티티 이름이 들어간다.

1. 조인 전략

join

  • 가장 정규화 된 방법으로 구현
  • NAME, PRICE가 ITEM 테이블에만 저장되고, ALBUM, MOVIE, BOOK이 각자의 데이터만 저장한다.
  • 조인 전략에서는 @DiscriminatorColumn을 선언하지 않으면 DTYPE컬럼이 생성되지 않는다.
    • 어차피 조인하면 알수 있는 값들이지만 DTPYE을 넣어 주는게 명확하다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name=DTYPE)
public class Item {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int price;
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item {
    private String artist;
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
    private String author;
    private String isbn;
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
    private String director;
    private String actor;
}

테이블 생성

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)
)
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)
)
  • 상속받은 엔티티 테이블 모두 생성됨
  • 하위 테이블에 외래키 제약조건 생성됨
  • 하위 테이블 입장에서는 ITEM_ID가 PK이면서 FK로 생성됨

데이터 저장

    Movie movie = new Movie();
    //MOVIE 필드
    movie.setActor("movieActor");
    movie.setDirector("movieDirector");
    
    //ITEM 상속받은 필드
    movie.setPrice(2000);
    movie.setName("itemName");
    em.persist(movie);

    tx.commit();
Hibernate: 
    insert into Item (name, price, DTYPE, id) values (?, ?, 'M', ?)
Hibernate: 
    insert into Movie (actor, director, id) values (?, ?, ?)
  • 데이터 저장 시 하이버네이트가 상속관계의 테이블에 insert 쿼리를 만들어 실행해줍니다.

데이터 조회

    Movie findMovie = em.find(Movie.class, movie.getId());
    System.out.println("findMovie = " + findMovie.getActor());
Hibernate: 
    select
        movie0_.id as id1_4_0_,
        movie0_1_.name as name2_4_0_,
        movie0_1_.price as price3_4_0_,
        movie0_.actor as actor1_7_0_,
        movie0_.director as director2_7_0_ 
    from
        Movie movie0_ 
    inner join
        Item movie0_1_ 
            on movie0_.id=movie0_1_.id 
    where
        movie0_.id=?
findMovie = movieActor
  • 데이터 조회 시 상속관계가 자동으로 조인되서 데이터를 가져온다.

장점

  • 테이블 정규화
  • 외래 키 참조 무결성 제약조건 활용가능
    • ITEM의 PK가 ALBUM, MOVIE, BOOK의 PK이자 FK이다. 그래서 다른 테이블에서 아이템 테이블만 바라보도록 설계하는 것이 가능 하다.
  • 저장공간 효율화
    • 테이블 정규화로 저장공간이 딱 필요한 만큼 소비된다.

단점

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

2. 단일 테이블 전략

onetable

  • 서비스 규모가 크지 않고, 조인 전략을 선택해서 복잡하게 갈 필요가 없다고 생각될 때 사용
  • 한 테이블에 다 저장하고 DTYPE으로 구분
  • INSERT 쿼리도 한 번, SELECT 쿼리도 한 번이다. 조인할 필요가 없고, 성능이 좋다.
  • 단일 테이블 적용
    • 단일 테이블에선 DTYPE으로 구분하기 떄문에 @DiscriminatorColumn(name=“DTYPE”) 기본 값으로 들어감
    • 한 테이블에 모든 컬럼을 저장하기 때문에, DTYPE 없이는 구분이 불가능하다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) //이 부분만 변경
public class Item {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int price;
}

테이블 생성

Hibernate: 
    create table Item ( //단일테이블로 생성
       DTYPE varchar(31) not null,
        id bigint not null,
        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)
    )
  • 상속관계 테이블이 단일 테이블로 생성된다.
  • DTYPE이 기본으로 생성된다.

데이터 저장

Hibernate: 
    /* insert hellojpa.Movie */ 
    insert into Item (name, price, actor, director, DTYPE, id) values (?, ?, ?, ?, 'M', ?)
  • 데이터를 단일 테이블에 저장하고 DTYPE으로 구분한다.

데이터 조회

Hibernate: 
    select
        movie0_.id as id2_2_0_,
        movie0_.name as name3_2_0_,
        movie0_.price as price4_2_0_,
        movie0_.actor as actor8_2_0_,
        movie0_.director as director9_2_0_ 
    from
        Item movie0_ 
    where
        movie0_.id=? 
        and movie0_.DTYPE='M'
findMovie = movieActor
  • 데이터조회 시 단일테이블에서 쿼리해온다.

장점

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

단점

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

3. 구현 클레스마다 테이블 생성 전략

eachtable

  • 조인 전략과 유사하지만, 슈퍼 타입 테이블을 생성하지않고 슈퍼타입 컬럼들을 서브 테이블로 내린다.
  • NAME, PRICE 컬럼들이 중복되도록 허용하는 전략이다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) //이 부분 변경
public abstract class Item { // 실제 생성되는 테이블이 아니므로 abstract클래스로 만든다.
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int price;
}

테이블 생성

Hibernate: 
    create table Movie (
       id bigint not null,
        name varchar(255),
        price integer not null,
        actor varchar(255),
        director 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 Album (
       id bigint not null,
        name varchar(255),
        price integer not null,
        artist varchar(255),
        primary key (id)
    )
  • 하위테이블 3개만 생성되고 부모테이블 컬럼들을 내려 받는다.

데이터 저장

insert into Movie (name, price, actor, director, id) values (?, ?, ?, ?, ?)
insert into Album (name, price, artist, id) values (?, ?, ?, ?)
  • 각각의 테이블에 데이터가 저장된다.

데이터 조회

  • 문제점
    • 객체지향 프로그래밍에서는 MOVIE, ALBUM, BOOK 객체를 ITEM 타입으로 조회 가능
Movie movie = new Movie();
movie.setActor("movieActor");
movie.setDirector("movieDirector");
movie.setPrice(2000);
movie.setName("itemName");

em.persist(movie);

Item findMovie = em.find(Item.class, movie.getId());
System.out.println("findMovie = " + findMovie.getName());
  • ITEM 타입으로 조회 시 union all로 전체 하위테이블을 다 찾아서 비효율 적으로 동작
Hibernate: 
    select
        item0_.id as id1_4_0_,
        item0_.name as name2_4_0_,
        item0_.price as price3_4_0_,
        item0_.artist as artist1_0_0_,
        item0_.author as author1_1_0_,
        item0_.isbn as isbn2_1_0_,
        item0_.actor as actor1_7_0_,
        item0_.director as director2_7_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=?
  • 이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천X

장점

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

단점

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

정리

  • 기본적으로는 조인 전략을 가져가고
  • 심플하고 확장가능성 적은 테이블일 경우 단일 테이블 전략을 사용하는 것도 좋을 것 같다.

Tags:

Categories:

Updated:

Leave a comment