JPA 상속 관계 매핑 전략 3가지 비교 (8편)

이번엔 JPA 상속 관계 매핑이다.

세 전략을 각각 H2 에 올려서 테이블이 몇 개 생기는지, 부모 타입으로 조회할 때 어떤 쿼리가 나가는지 직접 찍어봤다. 아래 SQL 은 전부 그 로그다.

Item 이라는 부모 클래스를 두고 Album, Book, Movie 가 상속받는 구조를 DB 테이블로 어떻게 옮길지 정하는 게 @Inheritance 전략이다.

처음엔 다 비슷해 보이는데 조회 성능과 유지보수에서 꽤 갈린다. 잘못 골랐다가 컬럼이 30개 넘게 쌓인 테이블을 보고 후회한 적이 있다.

JPA 상속 관계 매핑 전략 SINGLE_TABLE, JOINED, TABLE_PER_CLASS 비교 다이어그램

테스트 1 — SINGLE_TABLE, 테이블이 몇 개 생기고 쿼리는 어떻게 나가나

→ 테이블 1개. 조인 없이 단일 SELECT.

테이블 하나에 부모와 자식 컬럼을 다 몰아넣고 DTYPE 컬럼으로 이 row 가 Album 인지 Book 인지 구분한다. @Inheritance 를 안 써도 이게 기본값입니다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private int price;
}

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

조회 쿼리는 이렇게 나간다. 조인이 없어서 제일 단순하다.

select si1_0.id, si1_0.dtype, si1_0.name, si1_0.price,
       si1_0.artist, si1_0.author, si1_0.isbn
from single_item si1_0

부모 타입(Item)으로 조회한 실제 로그다. 조인이 하나도 없다.

  • 장점: 조인이 없어서 조회 성능이 제일 좋음. 쿼리도 단순하고 인덱스 관리도 편함
  • 단점: 자식 타입별 컬럼이 전부 한 테이블에 모이니까 artist, author, isbn 같은 컬럼들이 다른 타입 row에서는 계속 NULL로 남음. 자식 클래스의 필드에는 nullable = false를 못 씀 (다른 타입 row에서는 당연히 비어있어야 하니까). 자식이 늘어날수록 테이블이 걷잡을 수 없이 지저분해짐

테스트 2 — JOINED는 조회할 때 조인을 몇 번 하나

→ 테이블 3개(부모 1 + 자식 2). 부모 타입으로 조회하면 자식 테이블마다 LEFT JOIN.

부모 테이블에는 공통 컬럼만 두고, 자식 테이블은 자기 컬럼만 가진 채 부모 PK 를 FK 로 물고 조인한다.

정규화가 잘 된 구조다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private int price;
}

@Entity
@PrimaryKeyJoinColumn(name = "ITEM_ID")
public class Album extends Item {
    private String artist;
}

조회할 땐 조인이 붙는다.

select ji1_0.id,
       case
           when ji1_1.id is not null then 1
           when ji1_2.id is not null then 2
       end,
       ji1_0.name, ji1_0.price,
       ji1_1.artist,
       ji1_2.author, ji1_2.isbn
from joined_item ji1_0
left join joined_album ji1_1 on ji1_0.id=ji1_1.id
left join joined_book  ji1_2 on ji1_0.id=ji1_2.id

실제 로그다. 자식이 둘이라 LEFT JOIN 이 두 번 붙었고, 어느 자식인지 알아내려고 case 문까지 들어갔다. 자식이 다섯이면 조인도 다섯 번이다.

  • 장점: 테이블이 깔끔하고 정규화가 잘 되어 있음. 컬럼 낭비 없고, 자식 필드에 nullable = false도 정상적으로 걸 수 있음
  • 단점: 조회할 때마다 조인이 발생해서 SINGLE_TABLE보다 느림. 자식이 많아질수록, 그리고 여러 자식 타입을 한 번에 폴리모픽하게 조회할수록 조인 비용도 커짐

테스트 3 — TABLE_PER_CLASS는 부모 테이블이 생기나

→ 안 생긴다. 테이블 2개(자식만). 부모 타입 조회는 UNION ALL.

자식 테이블마다 부모 컬럼까지 전부 중복해서 갖는 방식이다. ALBUM, BOOK, MOVIE 테이블이 각자 독립적으로 존재합니다.

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private int price;
}

Item 으로 폴리모픽 쿼리를 날리면 하이버네이트가 UNION ALL 을 만들어낸다.

select ti1_0.id, ti1_0.clazz_, ti1_0.name, ti1_0.price,
       ti1_0.artist, ti1_0.author, ti1_0.isbn
from (
    select price, id, artist, name,
           null as author, null as isbn, 1 as clazz_
    from tpc_album
    union all
    select price, id, null as artist, name,
           author, isbn, 2 as clazz_
    from tpc_book
) ti1_0

부모 테이블이 아예 안 만들어진다. 부모 타입으로 조회하면 자식 테이블을 전부 UNION ALL 로 붙여서 훑는다. 자식이 늘어날수록 union 이 그대로 길어진다.

  • 장점: 자식 테이블 하나만 콕 집어서 조회할 땐 조인이 없어서 빠름
  • 단점: 부모 타입으로 폴리모픽 조회를 하면 UNION ALL 때문에 오히려 더 느려짐. id 채번 전략도 테이블 간 공유가 안 돼서 IDENTITY 전략을 못 씀. 실무에서는 거의 안 씀

JPA 상속 관계 매핑 전략 한눈에 비교

그래서 뭘 써야 하는데?

실무에서는 JOINEDSINGLE_TABLE 둘 중 하나를 쓴다. TABLE_PER_CLASS 는 이론으로만 배우고 실제로 쓰는 경우는 거의 못 봤다.

  • 자식 타입 구분이 단순하고 조회 성능이 중요하면 → SINGLE_TABLE
  • 자식마다 컬럼이 많고 데이터 무결성/정규화가 중요하면 → JOINED

저는 개인적으로 특별한 이유 없으면 기본값인 SINGLE_TABLE로 시작하고, 컬럼이 너무 많아져서 테이블이 지저분해질 때만 JOINED로 바꾸는 편이에요.

미리 최적화하겠다고 JOINED부터 들어갔다가 불필요한 조인 비용만 늘어나는 경우를 몇 번 봤거든요.

반대로 자식 엔티티마다 not null 제약이 중요한 도메인(결제 수단별로 필수 컬럼이 다르다거나)이라면 처음부터 JOINED로 가는 게 맞습니다.

다음 시리즈에서는 상속 매핑 없이 공통 필드만 재사용하는 @MappedSuperclass를 다뤄볼게요.

상속 매핑이랑 헷갈리기 쉬운데, 얘는 테이블 전략이랑 아예 무관하다는 게 포인트예요.

참고

JPA 시리즈 전체 보기

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤