본문 바로가기
Framework/JPA

[JPA] JPA 상속 관계 매핑 전략 : 조인, 단일 테이블, 구현 클래스별 테이블 비교 및 주요 어노테이션 사용법

by 곰민 2023. 4. 15.

JPA 상속 관계 매핑 전략과 주요 어노테이션에 대해 알아보고, 조인 전략, 단일 테이블 전략, 구현 클래스별 테이블 전략을 비교하여 각각의 장단점을 설명확인해 보도록 합니다.
더 자세한 정보는
https://www.inflearn.com/course/ORM-JPA-Basic 김영한님 강의나
http://www.yes24.com/Product/Goods/19040233 책을 확인하시는 것이 좋습니다.


상속관계 매핑 전략에 대해 알아보자

 

 

상속 관계 매핑


관계형 데이터베이스는 상속 관계가 없다.

관계형 디비 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사

상속관계 매핑 : 객체의 상속과 구조와 DB의 슈퍼타입 서브타입 관계를 매핑

왼쪽 논리모델 공통적인 속성 공통적인 데이터는 물품으로 두고 각각 음반 영화 책으로 서브타입으로 두는 것

오른쪽 아이템이라는 추상타입을 만들어 놓고 앨범, 영화, 책으로 가져갈 수 있음

 

슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법


각각 테이블로 변환 → 조인 전략

통합 테이블로 변환 → 단일 테이블 전략

서브타입 테이블로 변환 → 구현 클래스마다 테이블 전략

 

디비입장에서 물리모델로 구성할 세 가지 전략이 존재한다

 

1. 조인전략


 

item이라는 테이블을 만들고 앨범, 영화, 책 테이블로 두고 조인으로 이 테이블을 연결하는 것

데이터를 정규화된 방식으로

앨범테이블에 이름과 가격은 아이템 테이블에 들어가고 앨범테이블에 아티스트는 앨범 테이블로 들어감 insert를 각각 두 번 하고 조회는 pk가 같기 때문에 조인해서 가져온다.

그렇다면 구분을 해야 하는데 어떻게 구분을 하는가 아이템 테이블에서 DTYPE이라는 구분을 할 수 있는 컬럼을 두어서 조인을 어떤 테이블로 할지 정해서 한다.

 

장점

데이터가 정규화가 되어있음

제약조건을 item에 맞춰서 걸 수가 있음

가격을 가지고 정산을 해야 한다? → ITEM 테이블만 보면 된다.

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

주문 테이블에서 외래키 참조로 ITEM을 본다고 한다면 ITEM 테이블만 보면 됩니다.

저장공간이 효율적이다.

저장공간이 효율적이어서 오히려 성능이 좋을 수 있다.

 

단점

조회 시 조인을 많이 사용, 성능 저하

조인을 잘 맞추면 크게 저하되진 않음.

조회 쿼리가 복잡하다

데이터저장 시 INSERT SQL 2번 호출

 

기본적으로 조인 전략이 정석이라고 봐야 함.
객체랑 잘 맞고 정규화도 잘되고 설계입장에서 깔끔하게 떨어져 나옴.

2. 단일 테이블 전략


논리 모델을 한 테이블로 다 합쳐버리는 것

pk 그대로 두고 컬럼을 다 떄려박는 것.

단순하기도 하고 성능 때문에 고려하기도 함.

앨범인지 영화인지 책인지는 DTYPE 컬럼명을 두고 구분을 하는 것. 아무거나 구분용으로 컬럼명 잡아도 됨

 

장점

 

조인이 필요 없으므로 일반적으로 조회 성능이 빠름

pk 던지면 팍 나옴

 

조회 쿼리가 단순하다.

 

단점

 

자식 엔티티가 매핑한 컬럼은 모두 null 허용

치명적 단점 위 그림을 예시로 말하면 Name 이랑 price 빼고는 다 null 허용해줘야 함

album 넣으면 director 나 actor가값이 들어가지 않는다.

데이터 무결성 입장에서 본다면 애매한 게 존재.

 

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

상황에 따라서 조회 성능이 오히려 느려질 수 있음.

보통 임계점을 넘지는 않아 보임.

 

3. 구현 클래스마다 테이블을 만드는 전략


 

앨범테이블 영화 테이블 책테이블 각각 테이블을 만들고 name과 price를 다 각각 가져서 만드는 방법

다 각각 가져와버리는 것

아이템 테이블 같은 것 없애고 좀 중복이긴 해도 각각 구체적인 테이블을 만듦

 

각각 객체로 구성하기 때문에 묶이는 게 하나도 없음..

album movie book price로 정산을 해야 한다면? 단일테이블이랑 조인전략은 price로 하면 그만임.

 

새로운 게 추가될 때마다 정산 코드 다 추가로 짜야 됨

각테이블 별로 정산을 따로 해줘야 한다.

 

상당히 비추

객체 지향과 디비 둘 다 싫어하는 전략

 

장점

 

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

not null 제약조건 사용 가능

 

단점

 

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

자식 테이블을 통합해서 쿼리 하기 어려움.

변경이라는 관점에서 정말 좋지 않음

 

 

조인전략을 기본으로 깔고 정말 단순해 확장 가능할 일이 없어 보인다? 단일테이블로 갑니다.
비즈니스적으로 정말 중요하고 복잡하다 조인전략으로 갑니다.
두 개 사이에서 고민을 좀 해보자.

 

객체입장에서는 다 같음


사실 객체 입장에서는 다 똑같음 객체는 상속관계를 지원하기 때문에 모델링을 할 것임.

디비 설계를 위에 3가지로 어떻게 하더라도 jpa로 매핑이 가능함.

디비 논리 모델을 어떻게 디비로 구현을 하든 매핑을 다 지원함.

개발을 하다가 조인 전략에서 뭔가 성능이 안 나와서 단일테이블 전략으로 바꾸고 싶다 이러면 테이블 추가 및 @Inheritance에 전략을 변경하면 되기 때문에 시간을 확 줄여주는 장점이 있음.

 

상속관계 매핑


슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법

각각 테이블로 변환 → 조인 전략

통합 테이블로 변환 → 단일 테이블 전략

서브타입 테이블로 변환 → 구현 클래스마다 테이블 전략

 

주요 어노테이션


@Inheritance(strategy=InheritanceType.XXX)

  JOINED : 조인 전략

  SINGLE_TABLE : 단일 테이블 전략

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

 

@DIscriminatorColumn(name=”DTYPE”)

 

@DiscriminatorValue(”XXX”)

 

단일 테이블 전략 매핑 예시


@Entity
public abstract class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;
}
//=================================
@Entity
public class Book extends Item{

    private String author;
    private String isbn;
}
//==================================
@Entity
public class Album extends Item{

    private String artist;
}

//===================================
@Entity
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,
        artist varchar(255),
        author varchar(255),
        isbn varchar(255),
        actor varchar(255),
        director varchar(255),
        primary key (id)
    )

 

Item을 abstract로 만드는 이유?
abstract로 안 만들지 않게 된다면

 

public class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;
}

 

상속과 상관없이 해당 Item 값만으로 독단적으로 사용할 여지가 있기 때문이다.

추상클래스로 만들어야 한다.

 

jpa 기본전략 자체가 싱글테이블이라고 하는 한 테이블에 다 때려 박는 방식과 매핑이 됨.

 

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) //전략을 싱글테이블로 변경
public abstract 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,
        artist varchar(255),
        author varchar(255),
        isbn varchar(255),
        actor varchar(255),
        director varchar(255),
        primary key (id)
    )

 

한 테이블에 다 생성이 됨.

 

DTYPE ID NAME PRICE ARTIST AUTHOR ISBN ACTOR DIRECTOR
Movie_Info 1 바람과함꼐사라지다 10000 null null null bbb aaaa

 

insert 문이 한번 나가며, 아래와 같이 select 문도 join 이 들어가지 않기 때문에 깔끔하게 가져올 수 있다.

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_Info'

 

@DiscriminatorColumn 이 없어도 DTYPE 이 들어갑니다.

조인전략의 경우 테이블이 분리가 되어있어서 다른 테이블을 뒤져서 어떤 정보인지 알 수 있는데 단일테이블 전략은 이 데이터가 앨범인지 영화인지 책인지 알 방법이 없습니다.

그래서 무조건 DTYPE 이 들어갑니다.

운영상 조인전략에서도 DTYPE이 있는 게 활용하기 좋습니다.

 

조인 전략 매핑 예시


@Entity
@Inheritance(strategy = InheritanceType.JOINED) //전략 선택가능 joined
public abstract class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;
}
//위의 예시에서 부모클래스수정
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 (
       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)
)
//테이블 생성
try{ //item 클래스와 movie 클래스에 getter setter 있다는 전제하에
  Movie movie = new Movie();
  movie.setDirector("aaaa");
  movie.setActor("bbb");
  movie.setName("바람과함께사라지다");
  movie.setPrice(10000);

	em.persist(movie);

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

  Movie findMovie = em.find(Movie.class, movie.getId());
	tx.commit();
}catch{
	tx.rollback(); 
}finally{
	em.close();
}
emf.close();
Hibernate: 
    /* insert hellojpa.Movie
        */ insert 
        into
            Item
            (name, price, id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert hellojpa.Movie
        */ insert 
        into
            Movie
            (actor, director, id) 
        values
            (?, ?, ?)
Hibernate: 
    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_4_0_,
        movie0_.director as director2_4_0_ 
    from
        Movie movie0_ 
    inner join
        Item movie0_1_ 
            on movie0_.id=movie0_1_.id 
    where
        movie0_.id=?

 

각각의 테이블에 insert로 값이 들어가고

flush, clear 하고 나서 1차 캐시를 다 비운뒤에

find로 값을 가져오려면 두 테이블을 조인해서 값을 가져와야 하는데 쿼리문을 보면 Inner join 이 나가는 것을 확인할 수 있다.
조인하는 fk는 moive id 값

 

DTYPE 컬럼이 없다?? 

 

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn //어노테이션 추가
public abstract class Item {
	...
}
create table Item (
       DTYPE varchar(31) not null, //테이블 생성시 추가가 된다
        id bigint not null,
        name varchar(255),
        price integer not null,
        primary key (id)
)
DTYPE ID NAME PRICE
Movie 1 바람과함께사라지다 10000

DTYPE 이 Movie 로 되어있음.

@DiscriminatorColum을 넣어주면 DTYPE이라고 생기고 default가 Entity명이 들어갑니다.

@DiscriminatorColum(name = “DIS_TYPE”) 이런 식으로 이름을 줄 수 도 있습니다.

 

DTYPE 이 있는 게 좋습니다.
안 넣어도 되긴 하는 게 객체로 뭘 조인해야 되는지 알 수 있기 때문이긴 합니다만
디비로 쿼리를 날렸을 때는 DTYPE이 없으면 ITEM만 Select 해보면 어떤 것 때문에 해당 쿼리가 날아왔는지 알 수가 없습니다.
이름은 크게 이상하다 싶지 않으면 관례로 default로 사용하는 것을 추천합니다.

 

만약에 엔티티명이 아니라 회사 내에서 정해놓은 규칙으로 DTYPE 값을 넣어야 한다면?

@DiscriminatorValue()를 사용한다.

default 값은 entity 값

 

@Entity
@DiscriminatorValue("Book_Info")
public abstract class Book extends Item{

    private String author;
    private String isbn;
}
//==================================
@Entity
@DiscriminatorValue("Album_Info"
public class Album extends Item{

    private String artist;
}

//===================================
@Entity
@DiscriminatorValue("Movie_Info")
public class Movie extends Item {

    private String director;
    private String actor;
}
DTYPE ID NAME PRICE
Movie_Info 1 바람과함께사라지다 10000

DTYPE 이 Movie_Info로 들어간다.

조인 전략에서는 DiscriminatorColumn 이 없어도 어떻게 해볼 수는 있음.

자식 테이블을 쿼리 해보면 돼서 하지만 단일 테이블 전략에서는 필수.

 

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


@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)// 테이블 퍼 클래스 전략 선택
@DiscriminatorColumn //이전략에서는 이게 의미가 없음 테이블마다 다 분리되어있기 때문에 넣어도 적용안됨
public abstract class Item {
    ...
}

 

create table에서 Item 테이블이 존재하지 않음.

album, movie, book 각각의 테이블이 생김

 

//movie 값을 넣는 로직은 계속 동일하다고 전재한다
Hibernate: 
    /* insert hellojpa.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_4_0_,
        movie0_.director as director2_4_0_ 
    from
        Movie movie0_ 
    where
        movie0_.id=?

 

insert와 select를 movie 테이블로 각각 해줍니다.

 

id NAME PRICE ACTOR DIRECTOR
1 바람과함꼐사라지다 10000 bbb aaaa

 

그럼 정말 좋은 것일까????

값을 넣고 뺄 때는 정말 좋아 보이는데...

 

만약 조회를 하는데

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

부모 타입으로 조회를 한다면?

 

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_4_0_,
        item0_.director as director2_4_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=?

JPA는 union all로 다 뒤지게 됩니다.

정확하게 딱 정해서 가져올 때는 상관이 없는데 그게 아니면 다 뒤져 봐야 합니다 전부다

위 이미지를 예시로 만약에 item id만 아는 경우에 item_id 가 5번이다.

데이터를 찾아야 한다면 테이블 3개를 다 select 해봐야 하게 됩니다.

단일테이블 전략은 아이디가 명확하니까 그냥 테이블 하나 조회하면 되고

조인 전략도 DTYPE 다 알고 id 다 아니까 심플하게 조회가 됩니다.

 

 

@Mappedsuperclass


상속관계 매핑 그런 게 아닌
정말 단순하게 객체 입장에서 id 랑 name 이 계속 나오는 상황
매번 클래스 만들 때마다 id랑 name이 계속 나오니까 상위 클래스에 id name을 만들어두고 속성만 상속받아서 쓰고 싶은 경우.

공통 매핑 정보가 필요할 때 사용(id, name)

 

누가등록했고 몇 시에 등록했어 누가 수정했고 몇 시에 수정했어라는 정보가 모든 테이블에 있어야 한다면.

 

@MappedSuperclass
public class BaseEntity {

    private String createBy;
    private LocalDateTime createDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;

		...
}
@Entity
public class Member extends BaseEntity{

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMEBER_ID")
    private Long Id;

    @Column(name ="USERNAME")
    private String username;
		//...
}

//===================================
@Entity
public class Team extends BaseEntity {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
		//...
}

 

@MappedSuperclass 매핑정보만 받는 상위 클래스라고 생각하면 됨.

 

Member member = new Member();
  member.setUsername("user1");
  member.setCreateBy("kim");
  member.setCreateDate(LocalDateTime.now());

  em.persist(member);            
  tx.commit(); //트랜잭션 커밋 할떄 변경사항 확인하고 업데이트 합니다.
create table Member (
   MEMEBER_ID bigint not null,
    createBy varchar(255),
    createDate timestamp,
    lastModifiedBy varchar(255),
    lastModifiedDate timestamp,
    USERNAME varchar(255),
    TEAM_ID bigint,
    primary key (MEMEBER_ID)
)
create table Team (
       TEAM_ID bigint not null,
        createBy varchar(255),
        createDate timestamp,
        lastModifiedBy varchar(255),
        lastModifiedDate timestamp,
        name varchar(255),
        primary key (TEAM_ID)
    )

 

상속받은 두 엔티티 클래스 테이블이 공통적으로 사용하는 속성을 컬럼으로 만드는것을 볼수 있다.

@MappedSuperclass 상위 클래스에 속성을 같이 쓰고싶어요 ㅜㅜ 하면 사용하는 겁니다.

 

public class BaseEntity {

    @Column(name = "INSERT_MEMBER")
    private String createBy;
    private LocalDateTime createDate;
    @Column(name = "UPDATE_MEMBER")
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
		
		//...
}

 

컬럼명을 지정하는것 또한 가능하다

 

create table Member (
       MEMEBER_ID bigint not null,
        INSERT_MEMBER varchar(255),
        createDate timestamp,
        UPDATE_MEMBER varchar(255),
        lastModifiedDate timestamp,
        USERNAME varchar(255),
        TEAM_ID bigint,
        primary key (MEMEBER_ID)
    )
create table Team (
       TEAM_ID bigint not null,
        INSERT_MEMBER varchar(255),
        createDate timestamp,
        UPDATE_MEMBER varchar(255),
        lastModifiedDate timestamp,
        name varchar(255),
        primary key (TEAM_ID)
    )

 

생성하는 컬럼명이 변경된것을 확인할 수 있다.

공통적으로 사용할 속성에 한에서는 @MappedSuperclass 를 사용할 수 있다

 

 

createBy

createData

lastModifiedBy

lastModifiedData

위 와 같이 서비스 운영에 있어서 필수적인 정보들은 공통으로 깔고 가는 게 좋다.

 

특징


상속관계 매핑이 아니다

엔티티 x , 테이블과 매핑 x

부모 클래스를 상속받는 자식 클래스에 매핑 정보만 제공

조회, 검색 불가(em.find(BaseEntity) 불가)

상속관계 매핑이 아니므로 부모클래스로 조회가 안됨

 

직접 생성해서 사용할 일이 없으므로 추상 클래스 권장

 

baseEntity 만 가지고 할 수 있는 게 없기 때문에 추상클래스가 딱 좋다

 

테이블과 관계없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할

주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용

참고 : @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속 가능

 

JPA에서 extends라고 쓸 때는
@MapppedSuperClass가 있거나 → 속성만 사용할 때
@Entity가 있거나 → 상속관계 매핑
baseEntity로 추상클래스로 만들어서 사용하면 정말 편하게 사용할 수 있다.

 

참조


https://www.inflearn.com/course/ORM-JPA-Basic

http://www.yes24.com/Product/Goods/19040233

 

반응형

댓글