[자바 ORM 표준 JPA 프로그래밍 - 기본편] 07. 고급 매핑
상속관계 매핑
원래 관계형 데이터베이스는 상속 관계가 없다.
하지만 슈퍼타입-서브타입 관계라는 모델링 기법이 객체 상속과 유사하며
상속관계 매핑이란 객체의 상속 구조와 DB의 슈퍼타입-서브타입 관계를 매핑하는 것을 뜻한다.
- 각각 테이블로 변환 → 조인 전략
- 통합 테이블로 변환 → 단일 테이블 전략
- 서브타입 테이블로 변환 → 구현 클래스마다 테이블 전략
3가지 중 어떤 걸로 구현하든 JPA에선 자동적으로 다 매핑을 지원한다.
주요 어노테이션
- @Inheritance(strategy=InheritanceType.XXX)
- JOINED: 조인 전략
- SINGLE_TABLE: 단일 테이블 전략
- TABLE_PER_CLASS: 구현 클래스마다 테이블 전략
- @DiscriminatorColumn(name=“DTYPE”)
- @DiscriminatorValue(“XXX”)
부모 클래스인 Item과 자식 클래스인 Book, Album, Movie를 각각 생성하여 extends하면
@Entity
public class Book extends Item{
private String author;
private String isbn;
}
JPA 기본전략은 single table임을 알 수 있다. 즉 Item테이블에 Album, Book, Movie의 모든 정보를 저장한다.
조인 전략
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Item {
...
}
Item에 @Inheritance를 이용하여 조인 전략을 바꿀 수 있다.
InheritanceType.JOINED인 상태로 Movie값을 저장하면
이런식으로 insert쿼리가 Movie와 Item에 모두 나가게 된다.
em.find를 사용하여 값을 찾을 때도 마찬가지다.
JPA가 상속관계라면 조회할 때 join이 필요하면 알아서 다 해준다.
@DiscriminatorColumn
Item에 위 어노테이션을 넣어주면
DTYPE 컬럼이 자동으로 생성돼서 어떤 자식 테이블인지도 명시한다. 당연히 컬럼명은 바꿀 수 있다.
반대로 자식 테이블에
@DiscriminatorValue("A")
이 어노테이션을 넣으면
DTYPE 값은 디폴트로 엔티티명으로 저장되지만 해당 값을 바꿀 수 있다.
단일 테이블 전략
마찬가지로 단일 테이블 전략을 택할 때는
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public class Item {
...
}
InheritanceType. ~ 부분만 바꿔주면 된다.
심플한 쿼리 하나만 보내기 때문에 성능은 제일 좋다.
insert도 한 번에 되고 join도 필요 없다.
@DiscriminatorColumn 가 없어도 자동으로 DTYPE 필수로 저장된다. 해당 컬럼 값이 없으면 각 정보를 구별할 방법이 없기에 어떻게 보면 당연하다.
(다른 전략들도 운영상 DTYPE을 가지는 걸 추천하긴 한다.)
구현 클래스마다 테이블 전략
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
...
}
테이블 생긴 게 깔끔해서 좋아보일 수도 있다. 하지만 이 전략은 치명적인 단점을 가진다.
예를 들어 부모 타입으로 각 테이블을 조회할 때 ITEM_ID만 안다면 세 테이블을 모두 뒤져야 값을 알 수 있다.
장단점
1. 조인 전략
- 장점
- 테이블이 정규화되어 있어 데이터 중복을 줄일 수 있다.
- 각 하위 클래스의 특정 속성이 별도의 테이블에 저장되므로, 데이터의 일관성과 무결성을 유지하기 쉽다.
- 공통 속성은 상위 테이블에서 관리되며, 특정 속성이 필요할 때는 해당 하위 테이블만 조회하면 된다.
- 단점
- 조회 시 여러 테이블을 조인해야 하므로 SQL 쿼리가 복잡해질 수 있다.
- 큰 데이터 세트에서 많은 조인을 수행할 경우 성능 저하가 발생할 수 있다.
2. 단일 테이블 전략
- 장점
- 모든 속성이 하나의 테이블에 있으므로 조회 쿼리가 간단하다.
- 조인이 없기 때문에 성능이 빠를 수 있다.
- 단점
- 모든 상속받는 클래스의 속성이 한 테이블에 있어서, 데이터 중복이 발생할 수 있다.
- 자식 엔티티의 매핑 컬럼이 모두 NULL을 허용해야 하므로, 데이터 무결성 측면에서 문제가 될 수 있다.
3. 구현 클래스마다 테이블 전략
- 장점
- 각 클래스가 자체 테이블을 가지므로 데이터가 명확하게 분리된다.
- 단점
- 특정 속성을 찾기 위해서는 모든 관련 테이블을 조회해야 하므로 쿼리가 매우 복잡해질 수 있다.
- 새로운 클래스가 추가될 때마다 기존 구조를 변경해야 하므로 유지보수가 어려워진다.
- 일반적으로 ORM에서 이 전략은 권장되지 않는다.
기본적으로 조인 전략을 택하되,
이 관계가 너무 단순하고 딱히 확장 가능성도 없다면 괜히 단순한걸 복잡하게 설계하지 말고 단일 테이블 전략 선택도 좋은 선택이 된다.
@MappedSuperclass
상속관계 매핑이랑 별로 관계가 없다.
귀찮음을 줄이는 용도 ▷ 반복되는 속성과 메소드를 BaseEntity에서 관리함으로서 코드의 중복을 줄인다.
BaseEntity는 테이블이 따로 만들어지는 게 아니다. 따라서 부모타입으로 조회 불가하다.
어차피 직접 생성할 일 없으므로 추상 클래스로 만드는 걸 권장한다.
@MappedSuperclass
public abstract class BaseEntity {
private String createdBy;
private String modifiedBy;
private LocalDateTime createdDate;
private LocalDateTime lastModifiedDate;
}
public class Member extends BaseEntity {
...
}
이렇게만 해주면
상속 클래스와 똑같이 사용이 가능하다. BaseEntity에 있는 속성을 그대로 사용하는것이다.
DB에도 잘 저장되는 걸 확인할 수 있다.
실무에서 상속관계를 쓰는 게 맞는 것일까?
어떤 경우는 상속관계를 쓰기도 하지만 너무 복잡해지거나 데이터가 점점 커지면 테이블을 단순하게 유지하는 게 더 중요할지도 모른다.
우선은 객체지향적으로 설계하다가 이러한 장단점의 trade-off가 넘어가는 순간 시스템을 다시 갈아엎는 경우도 생길 수 있다.