상속관계 매핑과 @MappedSuperClass

dereck·2025년 2월 20일

JPA

목록 보기
9/13
post-thumbnail

김영한님의 JPA 강의를 보고 정리한 내용입니다.

상속관계 매핑

객체지향에는 클래스끼리 상속이 가능하다. 하지만 관계형 데이터베이스는 상속관계를 지원하지 않는다.

그 대신 상속 관계와 비슷한 논리 모델링 기법인 슈퍼타입, 서브타입 관계를 통해 객체의 상속 관계를 매핑할 수 있다.

슈퍼타입, 서브타입을 실제 테이블로 구현하는 방법엔 3가지 방법이 있다.
1. 조인 전략
2. 단일 테이블 전략
3. 구현 클래스마다 테이블 전략

위의 3가지 방법 모두 JPA의 어노테이션인 @Inheritance(strategy = InheritanceType.XXX)를 통해 구현할 수 있다. 이때 기본 값은 단일 테이블 전략이다.

  • JOINED
  • SINGLE_TABLE
  • TABLE_PER_CLASS

구분자 컬럼(DTYPE)이 필요한 경우 부모 클래스에 @DiscriminatorColumn으로 DTYPE을 설정할 수 있고 name 속성으로 DTYPE의 컬럼명을 바꿀 수 있다. 기본 값은 DTYPE이다.

자식 클래스엔 @DiscriminatorValue 어노테이션으로 DTYPE이 저장되는 값을 바꿀 수 있다. 기본 값은 클래스명이다.

어떤 전략을 사용하든지 크게 바뀌는 코드는 없다. 단지 어노테이션만 바꿔주면 JPA가 알아서 해당하는 전략에 맞게 테이블을 매핑해준다.

조인 전략

조인 전략은 슈퍼타입, 서브타입 논리 모델을 각각 테이블로 옮긴 방식이다. 테이블이 구분되어 있기 때문에 데이터를 조회할 때 조인이 필요해서 조인 전략이라고 부른다.

조인 테이블 전략은 구분자 컬럼이 반드시 필요하진 않지만 구분자 컬럼을 설정하는 것이 부모 테이블 조회만 해도 어떤 데이터 타입을 가지고 있는지 확인할 수 있어서 설정하는 것이 좋다.

장/단점

장점

  • 테이블이 정규화 된다.
  • 외래 키 참조 무결성 제약 조건을 활용할 수 있다.
  • 저장 공간 효율화
    • 정규화가 되어있기 때문

단점

  • 조회 시 조인을 많이 사용하기에 성능이 저하됨
  • 조회 쿼리가 복잡함
  • 데이터 저장 시 insert 쿼리 2번 호출

단점으로 성능이 떨어질 수 있다고 했지만 데이터가 엄청 많은게 아닌 이상 성능에 대한 이슈가 있지는 않다.

조인 전략은 상속 관계를 매핑하는 전략 중 데이터베이스의 관점에서 가장 정규화된 깔끔하고 정석적인 선택이다.

단일 테이블 전략

단일 테이블 전략은 이름 그대로 하나의 테이블에 모든 데이터를 몰아넣는 전략이다.

조인 전략에서는 구분자 컬럼이 필수는 아니었지만 단일 테이블 전략은 하나의 테이블에 모든 데이터가 들어가기 때문에 구분자 컬럼이 필수다. (개발자가 지정해주지 않아도 JPA가 자동으로 만들어 준다)

장/단점

장점

  • 조인이 필요 없다
    • 일반적인 상황에서 조회 성능이 빠름
    • 조회 쿼리가 단순함

단점

  • 자식 엔티티에 매핑한 컬럼은 모두 null 값을 허용해야 한다.
    • 데이터 무결성 입장에서 애매함
  • 테이블 컬럼의 길이가 커지기 때문에 조인 전략과 비교해서 성능이 더 느려질 수도 있다.

조인 전략과 비교해서 성능이 더 느려지려면 임계점을 넘어야 하는데 보통 넘을 일은 없다.

단일 테이블 전략은 조인 전략과는 다르게 테이블 하나만 사용하기 때문에 가볍고, 덜 복잡한 프로젝트와 잘 어울리는 전략이다. 프로젝트가 덜 복잡하고, 바뀌지 않을 것 같다면 추천한다.

구현 클래스마다 테이블 전략

조인 전략과 좀 비슷하지만 부모 객체를 없애버리고, 부모 객체에 있는 속성들을 자식 객체에 내리는 전략이다.

이때 부모 객체는 테이블이 만들어지면 해당 객체만 독단적으로 쓰는 일도 있다는 것이기 때문에 아예 만들어 지지 않게 추상 클래스로 정의한다.

장단점

장점

  • 서브타입을 명확하기 구분해서 처리할 때 효과적
  • not null 제약 조건 사용 가능

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느림
    • 여러 테이블을 조회할 때 UNION 쿼리를 사용함
  • 자식 테이블을 종합해서 쿼리하기 어려움
    • 뭘 묶어낼 수 있는게 있어야 통합이 가능한데 묶어낼 수 있는 것이 없음
      • ex) 시스템에 새로운 타입이 추가돼서 정산 로직을 다시 짜야 함
      • ex) 정산을 할 때 한 테이블만 정산해서 되는게 아니라 각 테이블에서 모두 정산을 해야 함

구현 클래스마다 테이블 전략은 변경이라는 관점에서 볼 때 매우 안좋다. 시스템에 새로운 것이 추가 될 때마다 굉장히 많은 걸 뜯어내야 한다.

구현 클래스마다 테이블 전략은 DBA와 개발자 모두 선호하지 않는 전략이다. 선택을 하게 되면 먼 미래에 언젠가 큰 후회를 하게 된다. 따라서 사용하지 않도록 하자.

@MappedSuperClass

공통 매핑 정보가 필요할 때 사용한다. 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.

아래 예시를 보면 공통으로 들어가는 정보인 createdBy, createdAt, lastModifiedBy, lastModifiedAtBaseEntity 클래스에 모아서 사용하는 것을 볼 수 있다.

@Getter
@MappedSuperClass
public abstract class BaseEntity {
	private String createdBy;
	private LocalDateTime createdAt;
	private String lastModifiedBy;
	private LocalDateTime lastModifiedAt;
}

@Entity
public class Post extends BaseEntity {
	...
}

@MappedSuperClass는 상속관계 매핑이 아니니 헷갈리지 말자.

@MappedSuperClass는 엔티티가 아니라 그저 속성만 내려주는 클래스이다. 그래서 해당 어노테이션이 적용되어 있는 클래스는 테이블과 매핑이 되지 않고, 부모 클래스를 상속받는 자식 클래스에 매핑 정보만 제공한다.

같은 맥락으로 해당 어노테이션이 적용된 클래스의 타입으로는 조회와 검색이 불가능하다. 이것도 상속관계 매핑과 다른 점이라고 볼 수 있다. (상속관계 매핑은 가능)

해당 클래스는 직접 사용할 일이 없으므로 추상 클래스로 만드는 것을 권장한다.

@Entity 클래스는 같은 @Entity 클래스나 @MappedSuperClass로 지정한 클래스만 상속 가능하다.

References

0개의 댓글