1. 상속관계 매핑
대부분의 RDB는 객체와 달리 상속 관계를 지원하지 않는다. 대신 슈퍼타입과 서브타입 관계라는 모델링 기법이 객체의 상속과 유사하다. 따라서 상속 관계를 매핑하려면, 객체의 상속 구조와 DB의 슈퍼타입 & 서브타입 관계를 매핑해야 한다.
슈퍼타입 & 서브타입 논리 모델링을 실제 물리 모델로 구현하는 방법은 아래 3가지로 나눌 수 있다. 전략을 바꾸려면 코드에 손댈 필요 없이 애노테이션에서 전략만 수정하면 된다. DB 입장에서 어떤 방법으로 구현하더라도 JPA에서 다 매핑할 수 있다.
- 조인 전략 = 각각을 테이블로 변환한다.
- 단일 테이블 전략 = 하나의 통합된 테이블로 변환한다.
- 구현 클래스마다 테이블 전략 = 서브타입을 테이블로 변환한다.
애노테이션 | 전략 / 기본값 |
@Inheritance(strategy = InheritanceType.XXX) | JOINED: 조인 전략 |
SINGLE_TABLE: 단일 테이블 전략 | |
TABLE_PER_CLASS: 구현 클래스마다 테이블 전략 | |
@DiscriminatorColumn(name = "DTYPE") | 기본값: DTYPE |
@DiscriminatorValue("XXX") | 기본값: 엔티티명 |
조인 전략
조인 전략을 사용하면, 부모 테이블과 자식 테이블로 나누고 데이터를 분리한다. 따라서 필요한 데이터는 조인해서 가져와야 한다.
- 장점
- 테이블을 가장 정규화한 방식이다.
- 외래 키 참조 무결성 제약조건을 활용할 수 있다.
- 저장 공간을 효율적으로 구성할 수 있다.
- 단점
- 조회 시 조인을 많이 사용해야 하기 때문에 성능이 저하된다.
- 조회 쿼리가 복잡해진다.
- 데이터 저장 시 INSERT SQL이 2번 호출(자식 테이블, 부모 테이블)된다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private int isbn;
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artist;
}
참고
비즈니스적으로 복잡하고 중요하다면 대체로 조인 전략을 사용하는 게 낫다.
단일 테이블 전략
단일 테이블 전략을 사용하면, 상속 관계를 갖는 논리 모델을 하나의 테이블로 합쳐버린다. @DiscriminatorColumn를 사용하지 않아도 DTYPE이 자동으로 생성된다.
- 장점
- 조인이 필요 없으므로, 일반적으로 조회 성능이 빠르다.
- 조회 쿼리가 단순하다.
- 단점
- 자식 엔티티가 매핑한 컬럼은 모두 null이 허용된다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 따라서 상황에 따라 조회 성능이 오히려 느려질 수 있다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn // 없어도 자동으로 DTYPE이 생성됨
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
...
참고
DB가 단순하고, 확장 가능성도 적을 때 사용하면 좋다.
구현 클래스마다 테이블 전략
구현 클래스마다 테이블을 두는 전략을 사용하면, 부모 테이블에 공통된 속성을 모아두지 않고 각각의 테이블에 포함되도록 한다.
- 장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적이다.
- not null 제약조건을 사용할 수 있다.
- 단점
- 여러 자식 테이블을 함께 조회할 때 UNION SQL이 필요하기 때문에 성능이 느리다.
- 자식 테이블을 통합해서 쿼리 하기 어렵다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
...
참고
이 전략은 데이터베이스 설계자와 ORM 전문가 모두가 추천하지 않는 전략이다. 사용하지 말자.
2. Mapped Superclass - 매핑 정보 상속
@MappedSuperclass 애노테이션은 id나 name 같은 공통 매핑 정보가 필요할 때 사용한다.
- 객체 입장에서 같은 속성이 존재하는 경우, 공통 매핑 정보를 가진 BaseEntity를 만들고 상속받는 방식으로 구현할 수 있다.
- 상속관계 매핑과는 관련이 없고, 부모 클래스를 상속받는 자식 클래스에 매핑 정보만 제공한다.
- 엔티티도 아니고, 다른 테이블과 매핑하지도 않기 때문에 조회나 검색이 불가능하다. → ex. em.find(BaseEntity) 불가능
- 직접 생성해서 사용할 일이 없으므로 추상 클래스로 만드는 걸 권장한다.
아래 예시에서 BaseEntity를 보면 테이블과는 관계가 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 한다.
- 주로 등록자, 등록일, 수정자, 수정일 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.
@MappedSuperclass
public abstract class BaseEntity {
private String createdBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
}
@Entity
public class Member extends BaseEntity {
...
}
@Entity
public class Team extends BaseEntity {
...
}
참고
@Entity class는 엔티티나 @MappedSuperclass로 지정한 class만 상속할 수 있다.
- @Entity: 상속관계 매핑
- @MappedSuperclass: 공통 속성 매핑