백엔드/Spring

[자바 ORM 표준 JPA 프로그래밍 - 기본편] 07. 고급 매핑

JYUN_ 2025. 3. 8. 15:28

상속관계 매핑


원래 관계형 데이터베이스는 상속 관계가 없다.

하지만 슈퍼타입-서브타입 관계라는 모델링 기법이 객체 상속과 유사하며

상속관계 매핑이란 객체의 상속 구조와 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가 넘어가는 순간 시스템을 다시 갈아엎는 경우도 생길 수 있다.