관계형 데이터베이스에는 상속 개념이 없다.
대신, 슈퍼타입-서브타입(Suppertype-Subtype) 관계 모델을 통해 유사한 구조를 표현한다.
JPA는 이 DB 구조와 객체 상속을 매핑할 수 있는 기능을 제공한다.
┌──────┐
│ Item │ ← 공통 속성 (id, name, price)
└───┬──┘
│
┌───┼───┬───────┬──────┐
│ Album │ Movie │ Book │
└───────┴───────┴──────┘
| 전략 | 설명 |
|---|---|
| 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" 등)슈퍼타입과 서브타입을 각각 테이블로 두고 조인해서 사용
Item
├─ item_id (PK)
├─ name
├─ price
└─ DTYPE (Movie/Album/Book)
Album
├─ item_id (PK, FK)
└─ artist
select ... from item i join album a ...하지만 실제로는 "조인"이 꼭 느리진 않다.
인덱스와 쿼리 튜닝이 잘 되어 있으면 오히려 빠를 수도 있다.
@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;
}
모든 엔티티를 한 테이블에 몰아넣는 방식이다.
Item
├─ item_id (PK)
├─ name
├─ price
├─ artist
├─ director
├─ actor
├─ author
├─ isbn
└─ DTYPE
null 허용 (데이터 무결성 애매)임계점을 넘는다는 말의 의미
데이터량이 많아져 테이블이 너무 넓고(Row/Column 많음)
인덱스 효율이 급격하게 떨어지는 시점을 말함.
보통 수백만 건 이상에서 발생하지만 대부분 서비스에선 해당되지 않음
각 자식 클래스마다 별도의 테이블을 생성하고, 부모 테이블은 존재하지 않음
Album
├─ item_id
├─ name
├─ price
└─ artist
Movie
├─ item_id
├─ name
├─ price
└─ director, actor
UNION ALLNOT NULL 제약 가능UNION ALL 필요 -> 성능 매우 나쁨실무에서는 비추천 !!
조인 전략보다 비효율적이고, ORM/DB 양쪽 모두 관리하기 어려움
| 전략 | 장점 | 단점 | 사용 권장도 |
|---|---|---|---|
| JOINED | 정규화, 무결성 | 조인 많음, insert 2회 | 추천 |
| SINGLE_TABLE | 빠른 조회, 단순 쿼리 | null 많음, 테이블 커짐 | 상황에 따라 |
| TABLE_PER_CLASS | 서브타입 명확 | union 느림, 유지보수 어려움 | 비추천 |
개발 초기엔 조인 전략으로 가는게 안전하며, 데이터 양이 많아지고 성능 이슈가 생기면 단일 테이블 전략으로 변경하는 식으로 접근한다.
전략 변경 시 코드가 아닌 어노테이션만 바꾸면 되는 게 JPA의 장점이다.
상속 관계 매핑과는 별개의 개념이다.
공통 매핑 정보(id, createdAt 등)를 부모 클래스에 모아두기 위한 기능이다.
BaseEntity 같은 형태로 사용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번 INSERT | 1번 INSERT | 빠름 |
| 데이터 무결성 | FK 제약 가능 | 컬럼 NULL 많음 | 각 테이블 제약 가능 |
| 테이블 크기 | 적당 | 커짐 | 여러 개 |
| 유지보수/확장성 | 구조 명확 | 단순 | 불편, 확장 어려움 |
| 추천도 | 일반적으로 추천 | 대규모 조회 시스템 추천 | 비추천 |
데이터 정합성과 테이블 구조의 명확성이 중요한 시스템에 적합하다.
조회가 압도적으로 많은 서비스에 적합한 현실적인 전략
실제로 이전 프로젝트에서 민원 관련 프로젝트를 했었는데 민원 메인 테이블에 컬럼이 100개도 넘게 들어간 적이 있었다. 물론 엮여있는 테이블도 많았지만 메인 테이블은 조회 중심이었고 여러가지 통계 데이터도 필요해서 그렇게 구성을 했을 수도 있다.
이론적으로만 존재하는 전략이며 실제 실무에서는 거의 사용하지 않는다.
UNION ALL 발생 -> 성능에 문제 많아짐JPA 장점은 전략을 교체해도 엔티티 구조나 비즈니스 로직은 그대로 유지된다는 것이다. 초반엔 단일 테이블로 시작해도 나중엔 조인 전략으로 충분히 옮길 수가 있다.
┌──────────────────────────────────────────┐
│ 선택 기준 도식 │
└──────────────────────────────────────────┘
"성능" ↑
│
│ 단일 테이블(SINGLE_TABLE)
│ ▲
│ │
│ 조인전략(JOINED)
│ │
│ ▼
│ 구현클래스별(TABLE_PER_CLASS)
│
└──────────────────────────→ "정규화/정합성"
위 축에서 오른쪽으로 갈수록 데이터 안정성↑, 위로 갈수록 조회 성능↑
결국 서비스 성격에 따라 “균형점(Trade-off Point)”을 잡는 게 핵심이다.
조회가 훨씬 많은 시스템인가?
→ 단일 테이블 전략 우선 고려
데이터 무결성과 정합성이 더 중요한가?
→ 조인 전략 선택
자식 클래스 간 완전한 독립성이 필요한가?
→ (거의 없음) TABLE_PER_CLASS 고려
공통 컬럼만 상속하고 싶은가?
→ @MappedSuperclass 사용
성능이 걱정된다면?
→ 조인 전략이라도 FetchType.LAZY / JPQL fetch join 등으로 조정 가능
Item: name, price, stockQuantityBook, Album, MovieTransaction: id, date, amounCardTransaction, BankTransactionBaseEntity: createdAt, updatedAt@MappedSuperclass (공통 매핑)실제 한 프로젝트에서 전략을 섞어서 쓸까? 에 대한 의문 정리하기