7. 상속관계 매핑

2025. 6. 16. 18:05Spring/JPA

1. 상속관계 매핑
1) 개념
2) 구현 전략 3가지 
3) 주요 어노테이션

2. 상속 전략별 특징
1) 조인 전략 (JOINED)
2) 단일 테이블 전략 (SINGLE_TABLE)
3) 구현 클래스마다 테이블 전략 (TABLE_PER_CLASS)

3. @MappedSuperclass
1) 개념 및 역할
2) 사용 목적
3) 참고 사항


1. 상속관계 매핑

1) 개념

  • 관계형 데이터베이스는 객체의 상속 개념이 없음.
  • 대신 슈퍼타입-서브타입 관계라는 모델링 기법을 통해 객체 상속과 유사하게 표현.
  • JPA에서는 객체의 상속 구조를 DB의 슈퍼타입-서브타입 관계로 매핑하는 것을 상속관계 매핑이라 함
  • 설정해놓은 상속관계 매핑 구현 전략에 따라 JPA가 알아서 테이블 생성 및 데이터 추가, 수정, 삭제 쿼리를 작성해준다. 

2) 구현 전략 3가지 

  • 1. 각각 테이블로 변환 → 조인 전략 (JOINED)
  • 2. 단일 테이블 전략 (SINGLE_TABLE)
  • 3. 구현 클래스마다 테이블 생성  전략 (TABLE_PER_CLASS)

3) 주요 어노테이션

  • @Inheritance(strategy = InheritanceType.XXX)
    • JOINED: 조인 전략
    • SINGLE_TABLE: 단일 테이블 전략
    • TABLE_PER_CLASS: 구현 클래스마다 테이블 전략
  • @DiscriminatorColumn(name = "DTYPE"): 부모 테이블에 자식 타입을 구분하는 컬럼 지정 (기본 컬럼명 DTYPE) 
  • @DiscriminatorValue("XXX"): 자식 클래스가 어떤 구분 값으로 들어갈지 지정 (클래스명이 디폴트) 

2. 상속 전략별 특징

@Entity
@Inheritance(strategy = InheritanceType.XXX) // 전략 지정
@DiscriminatorColumn(name = "DTYPE") // 구분 컬럼
public abstract class Item {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private int price;
}

@Entity
@DiscriminatorValue("ALBUM")
public class Album extends Item {
    private String artist;
}

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

@Entity
@DiscriminatorValue("BOOK")
public class Book extends Item {
    private String author;
    private String isbn;
}

1) 조인 전략 (JOINED)

@Entity
public class AppRunner implements CommandLineRunner {

    @PersistenceContext
    EntityManager em;

    @Override
    @Transactional
    public void run(String... args) {
        Album album = new Album();
        album.setName("Greatest Hits");
        album.setPrice(15000);
        album.setArtist("Queen");

        em.persist(album); // ITEM과 ALBUM 테이블에 INSERT 발생
        //insert into ITEM (NAME, PRICE, DTYPE) values ('Greatest Hits', 15000, 'ALBUM');
        //insert into ALBUM (ITEM_ID, ARTIST) values (1, 'Queen');


        Movie movie = new Movie();
        movie.setName("Inception");
        movie.setPrice(12000);
        movie.setDirector("Christopher Nolan");
        movie.setActor("Leonardo DiCaprio");

        em.persist(movie); // ITEM과 MOVIE 테이블에 INSERT 발생
    }
}
  • @Inheritance(strategy = InheritanceType.JOINED)
    • 부모 테이블과 자식 테이블을 조인해서 하나의 엔티티로 조회함.
    • 조회할 때는 SELECT + JOIN, 저장할 때는 INSERT가 부모, 자식 각각 발생
  • 장점: 테이블 정규화, 외래 키 참조 무결성 제약조건 활용 가능, 저장 공간 효율적 사용 가능
  • 단점: 조회 시 조인 많이 발생 → 성능 저하, 데이터 저장 시 INSERT SQL 2번 호출 필요 (부모, 자식 각각)
  • 정리하면, 자식이 많고 조회가 빈번하다면 성능 고려 필요. 하지만 정규화 구조라 DB 설계가 깔끔하고 확장성에 좋음.

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

Album album = new Album();
album.setName("Greatest Hits");
album.setPrice(15000);
album.setArtist("Queen");

em.persist(album); //실행쿼리는 한 줄
//insert into ITEM (NAME, PRICE, ARTIST, DTYPE) 
//values ('Greatest Hits', 15000, 'Queen', 'ALBUM');
  • @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
  • 테이블 1개에 모든 데이터 저장하는 전략으로 실무에서 가장 많이 쓰인다. 
  • 장점: 조인 없어서 조회 성능이 빠르고, 쿼리가 단순
  • 단점: 자식 엔티티의 컬럼은 모두 null을 허용해서 null 컬럼 많아짐, 테이블 커져서 오히려 조회 느려지기도 함
  • 가장 큰 단점은 데이터 정규화가 깨지는 것이다
    • 데이터 정규화는 DB 설계 시, 중복을 최소화하고 데이터의 일관성과 무결성을 유지하기 위해 각 데이터는 자신에게 필요한 컬럼만 가지게끔 테이블을 나누는 원칙. (데이터를 쪼개서 구조화하는 원칙)
    • 그런데 단일 테이블 전략은 모든 자식 클래스의 필드를 하나의 테이블에 몰아넣으면서 null이 많고 불필요한 컬럼이 생김
    • 따라서 조인은 없어서 성능은 좋을 수 있지만 데이터 구조가 깔끔하지 않고 유지보수가 어려워진다. 

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

  • 추천하지 않음 (DB 설계자와 ORM 전문가 모두 추천X)
  • 장점: 서브 타입을 명확히 구분해서 처리할 때 효과적, not null 제약조건 각각 사용 가능
  • 단점: 여러 자식 테이블 조회 시 성능 저하 (UNION SQL 필요), 자식 테이블 통합 조회 어려움

3. @MappedSuperclass

1) 개념 및 역할

  • 공통 필드만 정의해서 자식 엔티티들에게 매핑 정보를 물려주는 클래스
    • 상속관계 매핑이 아님!
    • @Entity처럼 엔티티로 인식되지 않음. JPA가 관리하는 대상 X
    • 테이블도 생성되지 않음
    • 단순히 필드와 매핑 정보만 자식에게 상속하는 역할
  • 조회, 검색 불가능 (em.find(BaseEntity) 사용 불가)
  • 직접 사용하지 않으므로 추상 클래스를 권장한다 

2) 사용 목적

  • 등록일, 수정일, 등록자, 수정자 등 전체 엔티티에 공통으로 필요한 필드 정의 시 사용
  • 테이블과는 관계없이 공통 속성만 상속시키는 용도
@MappedSuperclass //엔티티에 필요한 공통 필드 정의
public abstract class BaseEntity {

    private LocalDateTime createdDate;
    private LocalDateTime lastModifiedDate;

    private String createdBy;
    private String lastModifiedBy;
}

@Entity
public class Member extends BaseEntity {
//Member 클래스는 BaseEntity에 있던 필드들도 매핑되어서 
//MEMBER 테이블에 해당 컬럼들이 생성됨!
    
    @Id
    @GeneratedValue
    private Long id;

    private String name;
}

3) 참고 사항

  • @Entity 클래스는 @Entity 또는 @MappedSuperclass로 선언된 클래스만 상속 가능
  • @Entity랑 같이 쓰면 안 됨! @MappedSuperclass는 테이블이 없어야 하니까 @Entity는 빼야 한다. 
  • 상속은 되지만 연관관계 매핑은 안 됨
    • @OneToMany, @ManyToOne 같은 연관관계 어노테이션은 상속해도 적용 안 되고 
    • 단순히 @Column, @Id 같은 필드 매핑 정보만 상속됨
  • 조회 대상이 아님
    • em.find(BaseEntity.class, id) 이런 건 절대 불가
    • 왜냐하면 테이블이 없기 때문에 쿼리를 날릴 수 없음!