[JPA] 상속관계 매핑하기
슈퍼타입과 서브타입
객체 지향 프로그래밍에는 클래스 간의 상속관계가 있지만, 데이터베이스에는 상속의 개념이 없다.
하지만 슈퍼타입(super type)과 서브타입(sub type)의 모델링 기법이 객체 상속과 유사한 개념을 가진다.
- 슈퍼타입: 여러 서브타입의 공통 특성을 가지고 있는 엔티티
- 서브타입: 슈퍼타입의 특성을 상속받은 엔티티
여러 서브타입이 하나의 슈퍼타입을 가질 수 있으며, 각 서브타입은 슈퍼타입에서 정의된 속성 외에도 고유한 속성을 가질 수 있다.
이런 식으로 엔티티 클래스 간의 상속 관계를 설정하면 여러 가지 장점이 있다.
- 부모 클래스에서 정의한 속성과 메서드를 자식 클래스에서 재사용할 수 있다.
- 공통된 속성을 하나의 부모 클래스에 정의함으로써 데이터 일관성을 유지할 수 있다.
- 상속을 통해 새로운 엔티티 클래스를 쉽게 추가하고 확장할 수 있다.
데이터베이스에서 슈퍼타입과 서브타입을 구현하는 방법으로는 다음의 세 가지가 있다.
- 단일 테이블 전략
- 조인 전략
- 구현 클래스마다 테이블 전략
JPA에서는 엔티티 클래스 간의 상속 관계를 `@Inheritance` 애너테이션으로 매핑하는데, 이 세 가지 전략 중 하나를 선택하여 상속 관계를 매핑할 수 있다.

실제 물리 모델로 구현하기 위해 Item 엔티티를 상속받은 Tv, Phone, Keyboard 엔티티를 만들어 상속관계를 매핑하였고, 각 클래스는 아래와 같이 작성하였다.
@Entity
public abstract class Item {
@Id @GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int quantity;
}
@Entity
public class Tv extends Item {
private String release;
private int inch;
}
@Entity
public class Phone extends Item {
private String color;
private int capacity;
}
@Entity
public class Keyboard extends Item {
private int layout;
private int battery;
}
기본적인 준비는 다 되었으니 이 상속관계를 각각의 전략을 사용하여 구현해보고자 한다.
단일 테이블 전략(Single Table Strategy)
단일 테이블 전략은 모든 슈퍼타입과 서브타입의 속성을 하나의 테이블에 저장하는 전략이다.
서브타입의 속성이 없는 경우에는 null값이 저장된다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Item {
@Id @GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int quantity;
}
`@Inheritance(strategy = InheritanceType.SINGLE_TABLE)`로 설정하면 단일 테이블 전략으로 설정되는데, `@Inheritance`의 디폴트 값이 SINGLE_TABLE이기 때문에 strategy를 생략해도 결과는 동일하다.
실행해 보면 ITEM 테이블 하나만 생성되었고, Item을 상속받은 세 클래스의 필드를 모두 포함하여 컬럼으로 생성된 것을 확인할 수 있다.

테스트를 위해 Phone과 Keyboard 객체를 만들어 값을 넣은 결과는 다음과 같다.

서브타입이 가지고 있지 않은 속성은 null이 들어가고, DTYPE이 생긴 것을 볼 수 있다.
DTYPE은 Discriminator의 약자로, 슈퍼타입과 서브타입을 구분하기 위해 사용되는 특별한 컬럼이다.
디폴트로 서브타입과 동일한 값으로 생성되는데, 만약 컬럼명이나 저장되는 값을 변경하고 싶다면 다음과 같이 작성하면 된다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "newtype") //newtype으로 컬럼명 지정
public abstract class Item {
//생략
}
@Entity
@DiscriminatorValue("P") //P로 값 지정
public class Phone extends Item {
//생략
}
지정한 이름으로 데이터가 들어간 것을 확인할 수 있다.

단일 테이블 전략은 하나의 테이블에 모든 엔티티의 필드가 포함되기 때문에 스키마가 단순해지고 join이 필요하지 않아 조회 성능이 비교적 좋지만, 테이블이 커질 수 있고 상황에 따라서는 조회 성능이 오히려 느려질 수도 있다. 또한 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다는 단점도 있다.
조인 전략(Joined Strategy)
조인 전략은 슈퍼타입과 서브타입에 대해 별도의 테이블을 생성한다. 각 테이블은 서브타입의 속성을 포함하고, 슈퍼타입의 속성은 슈퍼타입 테이블에 저장되는 것이다.
서브타입의 테이블은 슈퍼타입의 기본 키를 외래 키로 사용하여 슈퍼타입과 관계를 맺는 방식으로 데이터를 저장하기 때문에 join을 사용하여 필요한 데이터를 검색한다.
`@Inheritance(strategy = InheritanceType.JOINED)`로 조인 전략을 지정할 수 있다.
@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;
private int quantity;
}
조인 전략에서 DTYPE 필드를 사용하려면 단일 테이블 전략과 달리 `@DiscriminatorColumn`을 적용해야 하며 필드명 등의 변경은 앞서 언급한 방법과 동일하다.
실행해 보면 테이블은 ITEM, KEYBOARD, PHONE, TV가 생성되고 아까와 동일한 값을 넣었을 때 아래와 같은 결과를 얻는다.

PHONE과 KEYBOARD 테이블이 가진 ITEM_ID가 기본 키이자 외래 키로 사용되는 것을 확인할 수 있다.
조인 전략은 테이블 정규화, 저장 공간 효율화 등의 장점이 있으나 조회 시 join을 많이 사용하여 성능이 저하되는 편이며, 단일 테이블 전략과 비교하여 조회 쿼리가 복잡하다. 또한 데이터를 저장할 때 INSERT SQL을 2번 호출한다는 점도 고려해야 한다.
구현 클래스마다 테이블 전략(Table per Class Strategy)
구현 클래스마다 테이블 전략은 말 그대로 각각의 구현 클래스마다 별도의 테이블을 생성하며, 부모 클래스의 테이블은 생성되지 않는다. 각 테이블이 해당 서브타입의 속성만을 가지는 것이다.
`@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)`로 구현 클래스마다 테이블 전략을 지정할 수 있다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
// @DiscriminatorColumn
public abstract class Item {
@Id @GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int quantity;
}
실행해보면 ITEM을 제외한 나머지 세 개의 테이블만 생성된 것을 확인할 수 있다.

ITEM 클래스의 필드를 포함한 각 클래스의 필드를 가진 테이블이 만들어졌고, 이 경우에는 별도의 테이블이 생성되는 것이기 때문에 `@DiscriminatorColumn`를 사용할 필요가 없고 사용해도 적용되지 않는다.
이 전략은 서브타입을 명확하게 구분하여 처리할 때 효과적이며, not null 제약조건을 사용할 수 있지만 2개 이상의 자식 테이블을 함께 조회할 때 성능이 좋지 않으며 자식 테이블을 통합하여 쿼리하는 것이 어렵다.
마무리
각 전략은 장점과 단점이 모두 존재한다. 그렇다면 어떤 전략을 선택하는 것이 좋을까?
데이터베이스의 스키마 구조와 쿼리 및 성능 요구사항 등을 고려하여 가장 적합한 전략을 선택해야겠지만, 기본적으로 조인 전략을 사용하되 데이터가 간단하고 확장 가능성이 낮으면 단일 테이블 전략을 사용하는 것이 좋다고 볼 수 있다.
구현 클래스마다 테이블 전략은 쿼리 최적화 및 데이터의 중복 문제 때문에 사용하지 않는 편이 좋다.
추가적으로 DTYPE은 성능 및 유지보수 측면 등의 이점이 있어 항상 만드는 것이 좋다.
참고
- 자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한)
- Hibernate - @Inheritance Annotation
'Programming > Spring' 카테고리의 다른 글
| Spring Annotation 정리 (0) | 2024.01.23 |
|---|---|
| [Spring Boot] MessageSource - 오류 메시지 처리 방식 (0) | 2024.01.20 |
| [Spring Boot] Embeded Mode로 테스트 하기 (feat. H2 Database) (0) | 2024.01.16 |
| [Spring Boot] Bean Validation - 검증 기능 사용 방법 (0) | 2024.01.05 |
| Session에 대해서 - 컨트롤러에서 HttpSession 사용하기 (0) | 2023.12.21 |
Liked this Posting!