[Java] JPA - 상속관계 매핑

daheenamic·2025년 11월 9일
0

JAVA

목록 보기
37/41

JPA 상속관계 매핑

관계형 데이터베이스에는 상속 개념이 없다.
대신, 슈퍼타입-서브타입(Suppertype-Subtype) 관계 모델을 통해 유사한 구조를 표현한다.

JPA는 이 DB 구조와 객체 상속을 매핑할 수 있는 기능을 제공한다.

  • 객체: 클래스 상속 (Item -> Album, Movie, Book)
  • DB: 슈퍼타입 테이블(Itme) + 서브타입 테이블들(Album, Movie, Book)

슈퍼타입-서브타입 논리 모델 예시

┌──────┐
│ Item │  ← 공통 속성 (id, name, price)
└───┬──┘
    │
┌───┼───┬───────┬──────┐
│ Album │ Movie │ Book │
└───────┴───────┴──────┘
  • 논리 모델: 설계 개념 단계(슈퍼/서브 관계만 정의)
  • 물리 모델: 실제 테이블 구조. 구현 방식은 3가지 전략 중 선택

상속 관계 매핑 전략 3가지

전략설명
JOINED슈퍼·서브 테이블을 각각 만들어 조인
SINGLE_TABLE모든 필드를 하나의 테이블에 통합
TABLE_PER_CLASS자식 클래스마다 독립 테이블 생성

주요 어노테이션

@Inheritance(strategy = InheritanceType.XXX) // 상속 전략 선택
@DiscriminatorColumn(name = "DTYPE")         // 구분 컬럼
@DiscriminatorValue("XXX")                   // 자식 구분값
  • @DiscriminatorColumn 기본값은 `DTYPE, 데이터는 엔티티명 (ex: "Movie", "Album")
  • 이름 변경 가능: @DiscriminatorColumn(name = "DIS_TYPE")
  • @DiscriminatorValue는 각 자식클래스에 명시적으로 지정 가능. (ex: "M", "A" 등)

조인 전략 (JOINED)

슈퍼타입과 서브타입을 각각 테이블로 두고 조인해서 사용

구조

Item
 ├─ item_id (PK)
 ├─ name
 ├─ price
 └─ DTYPE (Movie/Album/Book)

Album
 ├─ item_id (PK, FK)
 └─ artist

특징

  • insert 시: 부모(item), 자식(album) 각각 insert -> 쿼리 두번
  • select 시: join 수행 -> select ... from item i join album a ...

장점

  • 데이터 정규화, 중복 최소화
  • 외래키 제약조건 활용 가능 (참조 무결성 보장)
  • 공통 속성(Item)에 제약조건 걸이 용이

단점

  • 조회 시 항상 join 필요 -> 쿼리 복잡
  • insert 시 SQL 2회 발생

하지만 실제로는 "조인"이 꼭 느리진 않다.
인덱스와 쿼리 튜닝이 잘 되어 있으면 오히려 빠를 수도 있다.

예시 코드

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private int price;
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
    private String director;
    private String actor;
}

단일 테이블 전략 (SINGLE_TABLE)

모든 엔티티를 한 테이블에 몰아넣는 방식이다.

구조

Item
 ├─ item_id (PK)
 ├─ name
 ├─ price
 ├─ artist
 ├─ director
 ├─ actor
 ├─ author
 ├─ isbn
 └─ DTYPE

특징

  • insert, select 모두 SQL 한 번씩만
  • DTYPE 컬럼 자동 생성 (객체 종류 구분용)
  • @DiscriminatorColumn 없어도 기본으로 추가됨

장점

  • 조인이 필요 없어 조회 성능 빠름
  • 쿼리가 단순

단점

  • 자식 전용 컬럼은 항상 null 허용 (데이터 무결성 애매)
  • 한 테이블이 너무 커질 수 있음 -> 임계점 초과 시 성능 저하 기능

임계점을 넘는다는 말의 의미
데이터량이 많아져 테이블이 너무 넓고(Row/Column 많음)
인덱스 효율이 급격하게 떨어지는 시점을 말함.
보통 수백만 건 이상에서 발생하지만 대부분 서비스에선 해당되지 않음


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

각 자식 클래스마다 별도의 테이블을 생성하고, 부모 테이블은 존재하지 않음

구조

Album
 ├─ item_id
 ├─ name
 ├─ price
 └─ artist

Movie
 ├─ item_id
 ├─ name
 ├─ price
 └─ director, actor

특징

  • insert 시 자식 테이블에만 쿼리 발생
  • select 시 Item 타입으로 조회하면 모든 자식 테이블을 UNION ALL

장점

  • 서브 타입을 명확히 분리 가능
  • 각 테이블에 NOT NULL 제약 가능

단점

  • Item 전체 조회 시, UNION ALL 필요 -> 성능 매우 나쁨
  • 공통 필드 중복, 통합 쿼리 어려움
  • 테이블 추가 시 코드와 쿼리 수정량이 큼

실무에서는 비추천 !!
조인 전략보다 비효율적이고, ORM/DB 양쪽 모두 관리하기 어려움


정리

전략장점단점사용 권장도
JOINED정규화, 무결성조인 많음, insert 2회추천
SINGLE_TABLE빠른 조회, 단순 쿼리null 많음, 테이블 커짐상황에 따라
TABLE_PER_CLASS서브타입 명확union 느림, 유지보수 어려움비추천

개발 초기엔 조인 전략으로 가는게 안전하며, 데이터 양이 많아지고 성능 이슈가 생기면 단일 테이블 전략으로 변경하는 식으로 접근한다.
전략 변경 시 코드가 아닌 어노테이션만 바꾸면 되는 게 JPA의 장점이다.


@MappedSuperclass

상속 관계 매핑과는 별개의 개념이다.
공통 매핑 정보(id, createdAt 등)를 부모 클래스에 모아두기 위한 기능이다.

  • 테이블과 매핑되지 않음 (엔티티가 아님)
  • 매핑 정보만 상속 -> BaseEntity 같은 형태로 사용
  • 공통 속성: id, createdAt, updatedAt, createdBy 등
  • em.find(BaseEntity) 불가능
  • 직접 생성하지 않으므로 추상 클래스 권장

예시 코드

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseEntity {

    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
}

@EntityListeners(AuditingEntityListener.class)
스프링 데이터 JPA의 Auditing 기능.
@CreatedDate, @LastModifiedDate 등을 자동으로 채워준다.
추후 @EnableJpaAuditin을 설정하면 로그인 유저 기반 createdBy, modifiedBy도 자동 입력 가능

구조 다이어그램

BaseEntity (테이블X)
 ├─ createdAt
 ├─ updatedAt
 └─ (공통 필드 상속만 제공)
     ↓
     ├─ MemberEntity
     ├─ PostEntity
     └─ CommentEntity

요약

개념설명
@Inheritance상속 전략 지정 (JOINED, SINGLE_TABLE, TABLE_PER_CLASS)
@DiscriminatorColumn / @DiscriminatorValue구분 컬럼 및 값 지정
@MappedSuperclass공통 매핑 정보만 상속 (엔티티X)
전략 선택 가이드JOINED → 데이터 무결성 / SINGLE_TABLE → 단순·빠름 / TABLE_PER_CLASS → 비추천
JPA 장점전략 변경 시 코드 수정 없이 매핑 전략만 변경 가능

상속 매핑 전략 선택 기준과 실무 적용 사례

강의에서 다루지는 않았지만 실제 실무 조건으로 자주 언급하던 내용과 실무 개발자들이 어떻게 선택하는지의 관점까지 같이 알아보았다.

전략 선택 기준 한눈에 보기

구분조인 전략 (JOINED)단일 테이블 (SINGLE_TABLE)구현 클래스별 (TABLE_PER_CLASS)
DB 정규화높음낮음 (NULL 많음)각자 독립
조회 성능JOIN 필요빠름느림 (UNION ALL)
INSERT 성능2번 INSERT1번 INSERT빠름
데이터 무결성FK 제약 가능컬럼 NULL 많음각 테이블 제약 가능
테이블 크기적당커짐여러 개
유지보수/확장성구조 명확단순불편, 확장 어려움
추천도일반적으로 추천대규모 조회 시스템 추천비추천

1. 조인 전략 (JOINED)

데이터 정합성과 테이블 구조의 명확성이 중요한 시스템에 적합하다.

적합한 경우

  • 결제, 주문, 재고, 정산 등 정확한 데이터 정합성이 중요한 서비스
  • 공통 속성에 FK 제약을 걸고싶은 경우
  • 정규화가 중요한 ERP 또는 회계 시스템

주의할 점

  • join이 많아지는 쿼리는 N+1 이슈로 이어질 수 있음 (Fetch 전략 관리 필요)
  • 읽기보다 쓰기 (INSERT/UPDATE)가 자주 발생한다면 오히려 부담이 될 수 있다.

2. 단일 테이블 전략 (SINGLE_TABLE)

조회가 압도적으로 많은 서비스에 적합한 현실적인 전략

적합한 경우

  • 온라인 쇼핑몰, 게시판 등 조회 비율이 매우 높은 시스템
  • 단순 CRUD 중심 서비스
  • 성능에 민감하지만 정합성 제약은 덜 중요한 경우

주의할 점

  • NULL 컬럼이 많아지고 테이블이 커질 수 있음
  • DB 설계 관점에서는 비정규화 구조
  • 너무 많은 컬럼을 한 테이블에 몰면 인덱스 효율이 떨어짐.

실제로 이전 프로젝트에서 민원 관련 프로젝트를 했었는데 민원 메인 테이블에 컬럼이 100개도 넘게 들어간 적이 있었다. 물론 엮여있는 테이블도 많았지만 메인 테이블은 조회 중심이었고 여러가지 통계 데이터도 필요해서 그렇게 구성을 했을 수도 있다.

3. 구현 클래스별 테이블 전략 (TABLE_PER_CLASS)

이론적으로만 존재하는 전략이며 실제 실무에서는 거의 사용하지 않는다.

적합한 경우

  • 자식 클래스가 완전히 독립적으로 운용되고, 통합 조회나 공통 처리가 전혀 업는 경우
    예) 모듈별 독립 DB를 사용하는 분산 환경 일부

문제점

  • 상위 타입(Item)으로 조회 시, UNION ALL 발생 -> 성능에 문제 많아짐
  • 공통 속성 관리 불가 -> 유지보수 비효율

JPA 장점은 전략을 교체해도 엔티티 구조나 비즈니스 로직은 그대로 유지된다는 것이다. 초반엔 단일 테이블로 시작해도 나중엔 조인 전략으로 충분히 옮길 수가 있다.


성능 & 유지보수 트레이드오프 다이어그램

 ┌──────────────────────────────────────────┐
 │               선택 기준 도식                │
 └──────────────────────────────────────────┘

  "성능" ↑
    │
    │       단일 테이블(SINGLE_TABLE)
    │            ▲
    │            │
    │       조인전략(JOINED)
    │            │
    │            ▼
    │       구현클래스별(TABLE_PER_CLASS)
    │
    └──────────────────────────→ "정규화/정합성"

위 축에서 오른쪽으로 갈수록 데이터 안정성↑, 위로 갈수록 조회 성능↑
결국 서비스 성격에 따라 “균형점(Trade-off Point)”을 잡는 게 핵심이다.


개인 공부 및 설계시 체크리스트

  1. 조회가 훨씬 많은 시스템인가?
    → 단일 테이블 전략 우선 고려

  2. 데이터 무결성과 정합성이 더 중요한가?
    → 조인 전략 선택

  3. 자식 클래스 간 완전한 독립성이 필요한가?
    → (거의 없음) TABLE_PER_CLASS 고려

  4. 공통 컬럼만 상속하고 싶은가?
    @MappedSuperclass 사용

  5. 성능이 걱정된다면?
    → 조인 전략이라도 FetchType.LAZY / JPQL fetch join 등으로 조정 가능


실무 설계 예시

예시1) 온라인 쇼핑몰 상품 시스템

  • Item: name, price, stockQuantity
  • Book, Album, Movie
    → 단일 테이블 전략 (조회 빈도 높음, JOIN 최소화)

예시2) 정산/결제 시스템

  • Transaction: id, date, amoun
  • CardTransaction, BankTransaction
    → 조인 전략 (정규화, FK 제약 중요)

예시3) 서비스 공통 Entity

  • BaseEntity: createdAt, updatedAt
    @MappedSuperclass (공통 매핑)

궁금한점 (TODO)

실제 한 프로젝트에서 전략을 섞어서 쓸까? 에 대한 의문 정리하기

0개의 댓글