본문 바로가기
[ ORM ]/JPA

[ JPA ] 상속 관계 매핑

by 환이s 2024. 3. 18.


Intro

 

관계형 데이터베이스에는 객체 지향 언어에서 다루는 상속이라는 개념이 없다.

대신에 슈퍼타입 서브타입 관계라는 모델링 기법이 객체의 상속 개념과 가장 유사하다.

ORM에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

 

 

슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현할 때는 3가지 방법을 선택할 수 있다.

 

  • 각각의 테이블로 변환
    • 각 테이블을 생성하고 조회할 때 조인을 사용한다.
  • 통합 테이블로 변환
    • 테이블을 하나만 사용해서 통합한다. (JPA에서는 단일 테이블 전략이라 한다.)
  • 서브타입 테이블로 변환
    • 서브 타입마다 하나의 테이블을 만든다. (JPA에서는 구현 클래스마다 테이블 전략이라 한다.)

 

세 가지 방법 모두 JPA를 통해 구현할 수 있다.

먼저 주요 애노테이션을 살펴보자.

 

@Inheritance(strategy=InheritanceType.XXX)
	• 상속관계 매핑 방법을 선택한다.
    • JOINED: 조인 전략
    • SINGLE_TABLE: 단일 테이블 전략
    • TABLE_PER_CLASS: 구현 클래스마다 테이블 전략
    
@DiscriminatorColumn(name=“DTYPE”)
	• 부모 클래스에서 자식 클래스를 구분하는 컬럼의 이름을 설정한다.
    
@DiscriminatorValue(“XXX”)
	• 자식 클래스를 식별하기 위해 부모의 DTYPE 컬럼에 저장될 값을 설정한다.

조인 전략

 

 

조인 전략은 슈퍼타입, 서브타입 논리 모델을 각각 테이블로 옮긴 방식이다.

테이블이 구분되어 있기 때문에 데이터를 조회할 때 조인이 필요해서 조인 전력이라고 부른다.

 

코드로 살펴보자.

@Entity
@Getter 
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
}

 

위 코드는 슈퍼타입 Item Entity이다.

슈퍼타입이 혹시라도 객체화되지 않기 위해 추상 클래스로 선언했다.

 

슈퍼타입에서 @Inheritance(strategy = InheritanceType.JOINED)을 통해 매핑 전략을 조인 전략으로 지정했다.

 

@DiscriminatorColumn 애노테이션은 구분자 컬럼을 말한다.

조인 전략과 단일 테이블 전략에서 구분자 컬럼은 필수다.

 

구분자 컬럼을 통해 어떤 서브타입의 데이터인지 구분하는데, 생략하면 자동으로 추가된다.

하지만 명시적으로 적어주는 것을 추천한다.

 

구분자 컬럼의 이름은 기본값으로 "DTYPE"이다.

사용자가 원하는 값으로 지정해 줄 수 있지만 특별한 이유가 없다면 기본값을 그대로 쓰는 것을 추천한다.

@Entity
@Getter
@DiscriminatorValue("AlbumJPA")
public class Album extends Item {

    private String artist;
}

@Entity
@Getter
@DiscriminatorValue("MovieJPA")
public class Movie extends Item {

    private String director; // 감독
    private String actor; // 배우
}

@Entity
@Getter
@DiscriminatorValue("BookJPA")
public class Book extends Item {

    private String author; // 작가
    private String isbn; //ISBN
}

 

위 서브타입 엔티티 클래스는 슈퍼타입 Item 클래스를 상속받는다.

@DiscriminatorValue 애노테이션을 통해 구분자 컬럼의 값을 정할 수 있다.

기본값은 엔티티 이름이다.

 

조인 전략의 장단점을 살펴보자.

 

  • 장점
    • 테이블이 정규화 된다.
    • 외래키 참조 무결성 제약 조건을 활용할 수 있다.
    • 저장 공간이 효율화된다.
  • 단점
    • 조회 시 조인이 필요하다.
      • 조인이 필요하기 때문에 단일 테이블 전략에 비해 성능이 떨어질 수도 있다.
    • 조인이 필요하기 때문에 단일 테이블 전략에 비해 조회 쿼리가 복잡하다.
    • 테이블이 나뉘어 있기 때문에 저장 시 INSERT SQL이 2번 호출된다.
      • 단일 테이블 전략에 비해 성능이 떨어진다.

 

단점으로 성능이 떨어질 수도 있다고 했지만 데이터가 엄청 많지 않은 이상 성능이 한순간에 떨어지지는 않는다.

 

바로 다음에 살펴볼 단일 테이블 전략은 조인이 필요 없지만 null 데이터가 많아지기 때문에

상황에 따라 오히려 조인 전략보다 성능이 더 떨어질 수 있다.

 

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


단일 테이블 전략

 

단일 테이블 전략은 이름 그대로 테이블을 하나만 사용한다.

그리고 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분한다.

 

단일 테이블 전략은 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.

 

하지만 위에서 언급했듯 이 전략을 사용하면 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다는 주의점이 있다.

 

@Entity
@Getter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
}

@Entity
@Getter
@DiscriminatorValue("AlbumJPA")
public class Album extends Item {

    private String artist;
}

 

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)로 지정하면 단일 테이블 전략을 사용한다.

테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 한다.

 

단일 테이블 전략의 장단점을 살펴보자.

 

  • 장점
    • 조인이 필요 없기 때문에 일반적인 상황에서 조회 성능이 빠르다
    • 조회 쿼리가 단순하다.
  • 단점
    • 자식 엔티티에 매핑한 컬럼은 모두 null을 허용해야 한다.
    • 테이블의 컬럼 길이가 커지기 때문에 상황에 따라 조회 성능이 오히려 느려질 수 있다.

 

단일 테이블 전략은 한 테이블에 서브 타입의 모든 데이터가 들어가기 때문에 테이블이 커진다.

그 대신 한 테이블만 사용하기 때문에 다루기는 편리하다.

 

지금까지 알아보면

조인 전략은 테이블을 정규화한 정석적이고 이상적인 전략이다.

 

단일 테이블 전략은 테이블을 다루기 편리한 실용적인 전략이다.

상황에 따라 둘 중 하나를 선택하면 될 거 같다.

단일 테이블을 전략은 정규화도 포기해야 하고 null값도 많이 들어가야 하는 문제가 있지만

큰 문제가 없다고 예상되면 사용해도 괜찮을 거 같다.

 

조인 전략은 정규화로 테이블이 많아지고 관리가 힘들어질 수 있기 때문에 

상황에 따라 단일 테이블 전략보다 안 좋을 수 있다.


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

 

구현 클래스마다 테이블 전략은 권장하지 않는 전략이다.

그 이유는 슈퍼타입은 테이블로 매핑하지 않고 서브타입만 테이블로 매핑하는 방법인데,

단일 테이블 전략과 반대로 모든 자식 클래스에 부모 클래스의 속성을 때려 넣는 방식이다.

 

@Entity
@Getter
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn
public abstract class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
}

 

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)로 지정하면 구현 클래스마다 테이블 전략을 사용한다.

 

위에서 언급했듯이 이 전략은 자식 엔티티마다 테이블을 만든다. 일반적으로 추천하지 않는 전략이다.

그래도 개념은 알고 넘어가야 하니 장단점에 대해 살펴보자.

 

  • 장점
    • 서브 타입을 구분해서 처리할 때 효과적이다.
    • not null 제약 조건을 사용할 수 있다.
  • 단점
    • 여러 자식 테이블을 함께 조회할 때 성능이 느리다.
      • 이 부분이 정말 골치 아픈데, 전체 조회하므로 SQL에서 UNION을 사용해야 한다.
      • 전체 테이블을 UNION을 사용해서 조회하므로 성능이 많이 느려진다.
    • 자식 테이블을 통합해서 쿼리 하기 어렵다.

 

결론적으로 

이 전략은 실무에서 사용할 수 없다.

각 서브타입이 명확하게 각자의 테이블로 구분은 된다.

그러나 개념적으로 예제 코드의 Item 클래스로 풀어보자면 슈퍼타입에 종속되어 있는 각각의 서브타입 테이블들을 통합해서 관리하기가 매우 어려워진다.

 

예를 들어, 모든 물품의 가격의 평균을 구한다거나 통계 관련 쿼리를 작성하기 매우 복잡해진다.

상속관계 매핑은 조인 전략, 단일 테이블 전략 중 하나를 선택해서 사용하자.


@MappedSuperclass

 

@MappedSuperclass는 상속 관계 매핑이 아니다. 

상속 관계 매핑은 위에서 부모 클래스와 자식 클래스를 모두 데이터베이스 테이블과 매핑했다.

 

부모 클래스는

테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶으면

@MappedSuperclass 애노테이션을 사용하면 된다.

 

 

위 그림의 테이블에서 id, name이라는 같은 컬럼이 존재하지만

개념적으로 두 테이블은 서로 관련 없는 독립적인 테이블이다.

 

예제 코드를 살펴보자.

@MappedSuperclass
@Getter
public abstract class BaseEntity {

    private String createdBy;
    private LocalDateTime createdAt;
    private LocalDateTime lastModifiedBy;
    private LocalDateTime lastModifiedAt;
}

 

위 코드를 확인해 보면

BaseEntity 클래스에는 객체들이 주로 사용하는 공통 매핑 정보를 정의했다.

실무에서도 주로 사용되는 등록일, 수정일, 등록자, 수정자를 생성해서 알아보자.

 

BaseEntity 클래스는 실제 객체화 할 일이 없기 때문에 추상 클래스로 만들었고,

상위 클래스는 @MappedSuperclass 애노테이션을 통해 단순히 속성 정보만 모은 클래스임을 명시해줘야 한다.

 

@Entity
@Getter
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract class Item extends BaseEntity {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
}

 

공통매핑 정보를 추가하려면 상위 클래스인 BaseEntity 클래스를 상속받도록 설정해야 한다.

 

위 코드에서는 Item Entity Class를 상속받도록 설정했는데,

추가로 예제 코드에서 Item Class를 상속받는 Album, Movie, Book 클래스도 모두 공통 속성을 물려받게 된다.

 

마지막으로

@MappedSuperclass의 특징을 정리하고 포스팅을 마무리한다.

 

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용한다.
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find() 조회 메서드나 JPQL에서 사용할 수 없다.
  • 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장한다.

마치며

 

오늘은 상속 관계 매핑 개념에 대해 알아봤습니다.

다른 말로 고급 매핑이라고 하는데 지금부터가 JPA의 시작이라고 생각합니다.

다음 포스팅으로는 프록시에 대한 포스팅을 작성하려고 하는데 아직 해결하지 못한 부분이 있어서

완벽하게 이해하고 진행하려고 합니다.

 

다음 포스팅에서 뵙겠습니다.

위 포스팅 글은 김영한님의 자바 ORM 표준 JPA 프로그래밍-기본편을 참고했습니다.

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 강의 - 인프런

회사땜에 매일 바쁜 와중에 학원이라도 다닐까 생각했는데 마침 JPA 강의가 생겨서 꿀 타이밍이네요. 저는 이 전에 JPA 책을 보고 공부 했었는데요 궁금했던 점, 업무에 적용하며 고민하고 해결하

www.inflearn.com

 

728x90

'[ ORM ] > JPA' 카테고리의 다른 글

[ JPA ] 즉시 로딩과 지연 로딩(FetchType.LAZY or EAGER)  (36) 2024.03.26
[ JPA ] 프록시(Proxy)  (62) 2024.03.21
[ JPA ] 다양한 연관관계 매핑  (57) 2024.03.13
[ JPA ] 연관관계 매핑 기초  (74) 2024.03.07
[ JPA ] Entity Mapping  (77) 2024.02.26