본문 바로가기
Backend/JPA & QueryDSL

[JPA] Entity 계층 구조에 따른 상속 관계 매핑 - @Inheritance

by 제이동 개발자 2023. 7. 15.
728x90

Entity 계층 구조에 따른 상속 관계 매핑 - @Inheritance

 관계형 데이터베이스에서 부모의 속성들 중 더 작은 그룹으로 관리할 필요가 있을 경우 슈퍼타입 또는 서브타입 단위로 슈퍼-서브타입 관계로 테이블을 설계하는 방법이 있습니다. 슈퍼타입은 일반적인 개념을 나타내는 부모 테이블이며, 서브타입은 슈퍼타입의 특수한 유형을 표현하는 자식 테이블입니다. JPA에서는 이와 같은 슈퍼-서브타입 관계를 지원하기 위해 객체의 상속을 이용한 @Inheritance 어노테이션을 지원합니다. 

 

 

1. 주요 어노테이션

1-1. @Inheritance

 부모 엔티티(슈퍼타입)에서 사용되는 어노테이션으로 상속 관계 전략을 선택할 수 있습니다.

public @interface Inheritance {
    /** The strategy to be used for the entity inheritance hierarchy. */
    InheritanceType strategy() default SINGLE_TABLE;
}

 

예시

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

    @Id @GeneratedValue
    @Column(name = "item_id")
    private Long id;
    private String name;
    private int price;
    private int stockQuantity;
}

 

 엔티티 상속 계층 전략은 다음과 같이 세 가지를 지원하며 아래 2. 엔티티 상속 계층 전략에서 자세하게 설명하겠습니다.

  • SINGLE_TABLE (default)
    - 부모 엔티티와 모든 자식 엔티티의 속성을 포함하는 하나의 테이블을 사용
  • TABLE_PER_CLASS
    - 부모 엔티티와 각 자식 엔티티를 개별적인 테이블로 나누어 사용
  • JINED
    - 부모 엔티티와 각 자식 엔티티를 개별적인 테이블로 나누어 사용하며 서브타입은 슈퍼타입과 외래 키로 연결

 

1-2. @DisciminatorColumn

  부모 엔티티에 자식 엔티티를 구분할 수 있는 컬럼을 추가해 주는 어노테이션입니다. default 컬럼 명은 DTYPE이며 name 속성을 이용하여 구분 컬럼 명을 변경할 수 있습니다.

@Entity
@Getter
@NoArgsConstructor
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "ITEM_TYPE") // 구분 컬럼 ITEM_TYPE 추가
public abstract class Item {

    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    private String name;
    private int price;
    private int stockQuantity;

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

 

 

1-3. @DisciminatorValue

 구분 컬럼에 저장되는 자식 엔티티의 명칭을 변경할 수 있는 어노테이션입니다. 해당 어노테이션을 사용하지 않으면 default로 자식 엔티티 명이 저장이 됩니다.

@Entity
@DiscriminatorValue(value = "M") // 부모 엔티티의 구분 컬럼명에 저장할 명칭
@NoArgsConstructor
public class Movie extends Item{

    private String director;
    private String actor;

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

 

 

2. 엔티티 상속 계층 전략

2-1. JOINED(조인 전략)

 조인 전략은 부모 엔티티와 각 자식 엔티티마다 별도의 테이블로 관리하는 방법입니다.  즉, 슈퍼타입의 테이블은 부모 엔티티의 속성만을 포함하고, 각 서브타입의 테이블은 서브타입의 속성과 슈퍼타입의 식별자를 포함합니다. 조인 전략을 사용하면 저장 공간을 효율적으로 사용할 수 있고, 관계에 의해 발생하는 복잡한 참조 무결성 규칙을 적용할 수 있기 때문에 유연성과 확장성이 높습니다. 하지만 조회 시 조인을 사용해야 하기 때문에 성능이 저하된다는 점과 저장 시 insert 쿼리를 2번 수행해야 한다는 단점과 데이터베이스 스키마가 복잡해진다는 단점이 있습니다.

장점 단점
1. 저장 공간을 효율적으로 사용할 수 있다.
2. 복잡한 참조 무결성 규칙을 적용할 수 있다.
3. 유연성과 확장성이 높다.
1. select 시 JOIN 비용이 발생하여 성능이 저하된다.
2. insert 시 쿼리를 2번 수행하야 한다.(부모, 자식)
3. 데이터베이스 스키마가 복잡해진다.

 

예시

// 부모 엔티티
@Entity
@Getter
@NoArgsConstructor
@Inheritance(strategy = InheritanceType.JOINED) // 조인 전략 선택
@DiscriminatorColumn
public abstract class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    private String name;
    private int price;
    private int stockQuantity;

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

// 자식 엔티티
@Entity
@NoArgsConstructor
public class Movie extends Item{

    private String director;
    private String actor;

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

 

 아래 예시코드를 보면 알 수 있듯이 자식 엔티티인 Movie 엔티티를 저장하면 자동으로 Item 엔티티를 저장합니다.

@Test
public void item_save_테스트() {
    movieRepository.save(
            new Movie("영화이름", 10000, 10, "감독이름", "배우이름")
    );
}

Hibernate: insert into item (name, price, stock_quantity, dtype, item_id) values (?, ?, ?, 'Movie', ?)
Hibernate: insert into movie (actor, director, item_id) values (?, ?, ?)

 

2-2. SINGLE_TABLE(싱글 테이블 전략)

 싱글 테이블 전략은 단순히 부모 엔티티와 모든 자식 엔티티들의 속성들을 하나의 단일 테이블로 관리하는 전략입니다. 간단하고 직관적인 구조로 되어 있으며 단일 테이블에 모든 데이터가 저장되어 있기 때문에 조인이 필요하지 않으므로 성능이 조회 성능이 좋다는 장점이 있습니다. 하지만 자식 엔티티들이 많게 되면 테이블의 크기가 커지게 되고, NULL 값을 가지는 속성들이 많아지는 단점이 있습니다.

장점 단점
1. 간단하고 직관적인 구조
2. JOIN 비용이 발생하지 않기 떄문에 조회 성능이 좋다.
1. 단일 테이블의 크기가 커지게 된다.
2. NULL 값을 갖는 속성들이 많아질 수 있다.

 

예시

// 부모 엔티티
@Entity
@Getter
@NoArgsConstructor
@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;
    private int stockQuantity;

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

// 자식 엔티티
@Entity
@NoArgsConstructor
public class Movie extends Item{

    private String director;
    private String actor;

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

 

 자식 엔티티인 Movie를 저장하면 다음과 같이 단일 테이블에 저장된 것을 확인할 수 있습니다.

@Test
public void item_테스트() {
    movieRepository.save(
            new Movie("영화이름", 10000, 10, "감독이름", "배우이름")
    );
}

Hibernate: insert into item (name, price, stock_quantity, actor, director, dtype, item_id) values (?, ?, ?, ?, ?, 'Movie', ?)

 

2-3. TABLE_PER_CLASS(테이블 당 클래스 전략)

 테이블 당 클래스 전략은 각 자식 엔티티들이 부모 엔티티들의 모든 속성을 포함하여 테이블로 생성이 되며 부모 엔티티의 테이블은 생성되지 않는 전략입니다. 이 전략은 ORM 입장에서 상속을 이용하여 사용하고 있지만 DB 입장에서는 어떠한 관계가 생성되지 않고 부모 엔티티의 모든 속성들을 포함한 자식 엔티티들이 생성되기 때문에 추천하지 않는 방법입니다. 따라서 테이블 당 클래스 전략은 사용하지 않는 것을 추천합니다.

 

예시

// 부모 엔티티
@Entity
@Getter
@NoArgsConstructor
@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;
    private int stockQuantity;

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

// 자식 엔티티
@Entity
@NoArgsConstructor
public class Movie extends Item{

    private String director;
    private String actor;

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

 

 자식 엔티티들을 저장해 보면 다음과 같이 구분할 수 있는 DTYPE 컬럼이 없는 것을 확인할 수 있습니다. 때문에 특정 Item을 조회하기 위해서는 자식 테이블들을 UNION을 이용하여 조회하기 때문에 성능이 좋지 않습니다.

@Test
public void item_테스트() {
    movieRepository.save(
            new Movie("영화이름", 10000, 10, "감독이름", "배우이름")
    );
    albumRepository.save(
            new Album("앨범 이름", 20000, 20, "가수이름", "1집")
    );
}

728x90