Dev/JPA

JPA 상속관계 매핑 - 상속 전략과 MappedSuperClass

린네의 2023. 12. 27. 21:21

1.  JPA 에서 상속관계매핑이란?

객체(Entity) 의 상속구조, 데이터베이스에서 Super/Sub 타입 관계를 매핑하는 것을 의미한다.
 
이처럼 부모 객체(Super)를 자식 객체(Sub)가 상속하게 되면 부모객체의 변수와 메서드에 접근 가능한 것처럼, JPA 에서도 상속관계 매핑을 사용하여 동일하게 구현할 수 있다. 
 
예를 들어 판매자가 판매하려고 하는 상품(Item) 에는 서적(Book), 앨범(Album), 영화(Movie) 가 있다고 하자. 이 때 상품이 가지는 공통적인 특징을 묶어 Item 객체를 생성하고 서적과 앨범, 영화는 각각 Item 을 상속받는 Book, Album, Movie 객체로 생성할 수 있다.  
이렇게 정의한 Item 과 Book, Item 과 Album, Item과 Movie 관계를 총 세가지의 방법으로 분류할 수 있는데 이는 다음과 같다.
 

  • 조인 전략 ( InheritanceType.JOINED ) 
  • 단일 테이블 전략 ( InheritanceType.SINGLE_TABLE )
  • 구현 클래스마다 테이블 생성 전략  ( InheritanceType.TABLE_PER_CLASS )

 

2.  조인 전략

상속관계 매핑에서 사용하는 세가지의 전략중 가장 정석으로 사용하는 전략이다. 부모와 자식관계에 있는 객체가 각각 생성 된다.
@Inheritance(strategy = InheritanceType.JOINED) 를 통해 사용할 수 있다.
 

  • 조인 전략을 사용한 코드
// 부모에 해당하는 객체
@Entity
@Inheritance(strategy = InheritanceType.JOINED) /* 상속관계에서 테이블들을 표현하는 방식 */
@DiscriminatorColumn(name = "dtype") /* 자식 테이블을 구분하기 위한 값 */
@Getter @Setter
public abstract class Item {

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

    private String name;
    private int price;
    
}

// 자식 객체 - Book
@Entity
@DiscriminatorValue("B")
@Getter @Setter
public class Book extends Item{

    private String author;
    private String isbn;
}


// 자식 객체 - Album
@Entity
@DiscriminatorValue("A")
@Getter @Setter
public class Album extends Item{
    private String artist;
    private String etc;
}


// 자식 객체 - Movie 
@Entity
@DiscriminatorValue("M")
@Getter @Setter
public class Movie extends Item{

    private String  director;
    private String actor;
}

 
 

  • 실제 실행 되는 결과
    create table item (
        price integer not null,
        item_id bigint not null,
        dtype varchar(31) not null,
        name varchar(255),
        primary key (item_id)
    )
    
    
     create table book (
        item_id bigint not null,
        author varchar(255),
        isbn varchar(255),
        primary key (item_id)
    )
    
    
    create table album (
        item_id bigint not null,
        artist varchar(255),
        etc varchar(255),
        primary key (item_id)
    )
    
    
    create table movie (
        item_id bigint not null,
        actor varchar(255),
        director varchar(255),
        primary key (item_id)
    )

 
부모 객체에 @Inheritance 애노테이션을 통해 전략(strategy) 를 지정하고,  @DiscriminatorColumn 을 통해  지정된 컬럼에 값에 자식객체의 타입을 저장할 수 있다. 자식객체의   @DiscriminatorValue  에 지정된 값이 @DiscriminatorColumn 에  지정한 컬럼에 들어간다고 생각하면 된다.
 
즉 위 코드에서 Item 의  "dtype" 이라는 항목에 Book, Album, Movie 는 각각 "B", "A", "M" 로 구분되어 들어간다.
 
조인 전략의 장단점은 다음과 같다 
 
장점

  •  정규화가 되어있다
  • 외래키 참조 제약조건을 활용할 수 있다
  • 저장공간이 효율화 되어 있다

단점

  •  조회시 조인을 많이 사용해서 성능이 저하될 수 있다
  • 데이터 저장시 insert 쿼리가 두번 실행 된다 ( 위 예시에서 Item , movie 에 대해  insert 문이 두번 생성 됨 ) 

 

3.  단일 테이블 전략

 
부모와 자식 객체를 하나의 객체로 구현하는 것을 의미한다. @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 을 통해 사용할 수 있다.  단일테이블에서는 자식객체를 구분하기 위한 @DiscriminatorColumn 를 반드시 적어 주어야 한다.
 
조인전략에서 Item 을 부모객체로, Book, Album, Movie 를 자식 객체로 분리하여 각각 생성했다면 단일 테이블에서는 하나의 Item 객체에 부모와 자식 객체를 모두 매핑해서 생성한다.
 

  • 조인 전략을 사용한 코드
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) /* 상속관계에서 테이블들을 표현하는 방식 */
@DiscriminatorColumn(name = "dtype") /* 자식 테이블을 구분하기 위한 값 */
@Getter @Setter
public abstract class Item {

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

    private String name;
    private int price;
 
 }

 

  • 실제 실행되는 결과
    create table item (
        price integer not null,
        item_id bigint not null,
        dtype varchar(31) not null,
        actor varchar(255),
        artist varchar(255),
        author varchar(255),
        director varchar(255),
        etc varchar(255),
        isbn varchar(255),
        name varchar(255),
        primary key (item_id)
    )

 
 
장점

  • 조인이 필요 없어서 조회 성능이 빠르다 
  • 조회 쿼리가 단순하다 

단점

  •  자식 객체가 매핑한 컬럼은 모두 null 을 허용한다  ( 위 예시에서 actor, artist, autor, director ... ) 
  • 테이블이 커질 수 있어서 조회 성능이 오히려 떨어지는 경우도 있다.

 
 
 

4.  구현 클래스마다 테이블 생성 전략 

 
이 전략은 자식과 부모가 따로 나뉘어서 각각 생성되는게 아니라, 자식 객체에  부모객체값이 포함되어 자식객체별로 각각 생성되는 전략이다.  @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 을 통해 사용할 수 있다.
 
실무에서 사용을 지양하는 전략응로 자세한 설명은 생략하도록 하겠다.
 

  • 조인 전략을 사용한 코드 
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) /* 상속관계에서 테이블들을 표현하는 방식 */
@Getter @Setter
public abstract class Item {

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

    private String name;
    private int price;
 
 }

 
 

  • 실제 실행되는 결과
    create table book (
        price integer not null,
        item_id bigint not null,
        author varchar(255),
        isbn varchar(255),
        name varchar(255),
        primary key (item_id)
    )
  
  	create table album (
        price integer not null,
        item_id bigint not null,
        artist varchar(255),
        etc varchar(255),
        name varchar(255),
        primary key (item_id)
    )
    
    
    create table movie (
        price integer not null,
        item_id bigint not null,
        actor varchar(255),
        director varchar(255),
        name varchar(255),
        primary key (item_id)
    )

 
 
전략이 TABLE_PER_CLASS 일 경우, @DiscriminatorColumn 는 사용하지 않는다. 데이터 검색시 굉장히 비효율적이기 떄문이다. 이미 객체별로 구현되어 있는데 추가적으로 구분할 컬럼을 넣을 이유가 없다.
 
장점

  • 서브타입을 분리해서 사용할 떄 효과적이다
  • not null 제약 조건을 사용할 수 있다

단점

  •  여러 자식 테이블을 함께 조회할 떄 성능이 느리다 ( UNION SQL 을 써야 한다 ) 
  • 자식 테이블을 통합해서 쿼리하기 어렵다

 
 
 

5. MappedSuperClass 란 ?

 중복되는 컬럼에 대해 하나로 쓸 수 있는 기능이 있는데, 이것이 Mapped Super Class 다.  @MappedSuperClass 을 통해 사용한다.
주의할 점은,  위에서 기술한 상속관계 매핑과는 전혀 관계 없는 다른 내용이라는 것이다.  상속관계는 객체와 객체의 관계로,  각각 테이블과 매핑 되지만 MappedSuperClass 는 객체가 아니기 때문에 데이터베이스에 이 클래스에 매핑되는 테이블도 존재하지 않는다.
 
Item, Movie, Book, Album 이 각각 객체로서 정의되고 어떤 전략을 지정했는지에 따라 데이터베이스에 매핑되는 테이블이 달라지는 것과는 완전히 다른 개념임을 알아야 한다.
 
단순히 여러 객체 내에서 중복되는 컬럼에 대해서 관리하기 위한 기능이라고 생각하면 편하다.
 
같은 맥락에서, 해당 애노테이션을 지정한 클래스로 부터 조회, 검색은 불가능하다. 
 
예시) em.find(BaseEntiy) 
 
 
또 직접 생성하여 사용하지 않으므로 일반적으로  추상클래스를 권장한다.
 
아래는 간단한 예제 코드다 

 @Entity
 public class Member extends BaseEntity() {
     ...
   }


 @MappedSuperClass
 @Getter @Setter
 public abstract class BaseEntity() {
 
    private String createBy;
    private LocalDateTime createDate;
    private  String lastModfiedBy;
    private  LocalDateTime lastModifiedDate;
 
 }

 
 
위 코드를 실행하면 BaseEntity 클래스 필드에 해당하는 값을 가진 Member 객체는 생성되지만 BaseEntity 는 객체로서 역할을 하지도, 데이터베이스의 테이블과 매핑되지도 않는 것을 알 수 있다.