새소식

Programming/Spring

[JPA] 상속관계 매핑하기

 

 

슈퍼타입과 서브타입

 

객체 지향 프로그래밍에는 클래스 간의 상속관계가 있지만, 데이터베이스에는 상속의 개념이 없다.

하지만 슈퍼타입(super type)서브타입(sub type)의 모델링 기법이 객체 상속과 유사한 개념을 가진다.

 

  • 슈퍼타입: 여러 서브타입의 공통 특성을 가지고 있는 엔티티
  • 서브타입: 슈퍼타입의 특성을 상속받은 엔티티

 

여러 서브타입이 하나의 슈퍼타입을 가질 수 있으며, 각 서브타입은 슈퍼타입에서 정의된 속성 외에도 고유한 속성을 가질 수 있다.

 

 

이런 식으로 엔티티 클래스 간의 상속 관계를 설정하면 여러 가지 장점이 있다.

 

  • 부모 클래스에서 정의한 속성과 메서드를 자식 클래스에서 재사용할 수 있다.
  • 공통된 속성을 하나의 부모 클래스에 정의함으로써 데이터 일관성을 유지할 수 있다.
  • 상속을 통해 새로운 엔티티 클래스를 쉽게 추가하고 확장할 수 있다.

 

 

데이터베이스에서 슈퍼타입과 서브타입을 구현하는 방법으로는 다음의 세 가지가 있다.

 

  • 단일 테이블 전략
  • 조인 전략
  • 구현 클래스마다 테이블 전략

 

JPA에서는 엔티티 클래스 간의 상속 관계를 `@Inheritance` 애너테이션으로 매핑하는데, 이 세 가지 전략 중 하나를 선택하여 상속 관계를 매핑할 수 있다.

 

 

 

 

 

 

실제 물리 모델로 구현하기 위해 Item 엔티티를 상속받은 Tv, Phone, Keyboard 엔티티를 만들어 상속관계를 매핑하였고, 각 클래스는 아래와 같이 작성하였다.

 

@Entity
public abstract class Item {

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

    private String name;
    private int price;
    private int quantity;

}


@Entity
public class Tv extends Item {

    private String release;
    private int inch;

}


@Entity
public class Phone extends Item {

    private String color;
    private int capacity;
    
}


@Entity
public class Keyboard extends Item {

    private int layout;
    private int battery;
    
}

 

 

기본적인 준비는 다 되었으니 이 상속관계를 각각의 전략을 사용하여 구현해보고자 한다.

 

 

 

단일 테이블 전략(Single Table Strategy)

 

단일 테이블 전략은 모든 슈퍼타입과 서브타입의 속성을 하나의 테이블에 저장하는 전략이다.

서브타입의 속성이 없는 경우에는 null값이 저장된다.

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

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

    private String name;
    private int price;
    private int quantity;

}

 

 

`@Inheritance(strategy = InheritanceType.SINGLE_TABLE)`로 설정하면 단일 테이블 전략으로 설정되는데, `@Inheritance`의 디폴트 값이 SINGLE_TABLE이기 때문에 strategy를 생략해도 결과는 동일하다.

 

 

실행해 보면 ITEM 테이블 하나만 생성되었고, Item을 상속받은 세 클래스의 필드를 모두 포함하여 컬럼으로 생성된 것을 확인할 수 있다.

 

 

 

 

테스트를 위해 Phone과 Keyboard 객체를 만들어 값을 넣은 결과는 다음과 같다.

 

 

서브타입이 가지고 있지 않은 속성은 null이 들어가고, DTYPE이 생긴 것을 볼 수 있다.

DTYPE은 Discriminator의 약자로, 슈퍼타입과 서브타입을 구분하기 위해 사용되는 특별한 컬럼이다.

디폴트로 서브타입과 동일한 값으로 생성되는데, 만약 컬럼명이나 저장되는 값을 변경하고 싶다면 다음과 같이 작성하면 된다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "newtype") //newtype으로 컬럼명 지정
public abstract class Item {
    //생략
}

@Entity
@DiscriminatorValue("P") //P로 값 지정
public class Phone extends Item {
    //생략
}

 

 

지정한 이름으로 데이터가 들어간 것을 확인할 수 있다.

 

 

 

단일 테이블 전략은 하나의 테이블에 모든 엔티티의 필드가 포함되기 때문에 스키마가 단순해지고 join이 필요하지 않아 조회 성능이 비교적 좋지만, 테이블이 커질 수 있고 상황에 따라서는 조회 성능이 오히려 느려질 수도 있다. 또한 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다는 단점도 있다.

 

 

 

조인 전략(Joined Strategy)

 

조인 전략은 슈퍼타입과 서브타입에 대해 별도의 테이블을 생성한다. 각 테이블은 서브타입의 속성을 포함하고, 슈퍼타입의 속성은 슈퍼타입 테이블에 저장되는 것이다.

 

서브타입의 테이블은 슈퍼타입의 기본 키를 외래 키로 사용하여 슈퍼타입과 관계를 맺는 방식으로 데이터를 저장하기 때문에 join을 사용하여 필요한 데이터를 검색한다.

 

`@Inheritance(strategy = InheritanceType.JOINED)`로 조인 전략을 지정할 수 있다.

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

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

    private String name;
    private int price;
    private int quantity;

}

 

조인 전략에서 DTYPE 필드를 사용하려면 단일 테이블 전략과 달리 `@DiscriminatorColumn`을 적용해야 하며 필드명 등의 변경은 앞서 언급한 방법과 동일하다.

 

 

실행해 보면 테이블은 ITEM, KEYBOARD, PHONE, TV가 생성되고 아까와 동일한 값을 넣었을 때 아래와 같은 결과를 얻는다.

 

 

PHONE과 KEYBOARD 테이블이 가진 ITEM_ID가 기본 키이자 외래 키로 사용되는 것을 확인할 수 있다.

 

 

조인 전략은 테이블 정규화, 저장 공간 효율화 등의 장점이 있으나 조회 시 join을 많이 사용하여 성능이 저하되는 편이며, 단일 테이블 전략과 비교하여 조회 쿼리가 복잡하다. 또한 데이터를 저장할 때 INSERT SQL을 2번 호출한다는 점도 고려해야 한다.

 

 

 

구현 클래스마다 테이블 전략(Table per Class Strategy)

 

구현 클래스마다 테이블 전략은 말 그대로 각각의 구현 클래스마다 별도의 테이블을 생성하며, 부모 클래스의 테이블은 생성되지 않는다. 각 테이블이 해당 서브타입의 속성만을 가지는 것이다.

 

`@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)`로 구현 클래스마다 테이블 전략을 지정할 수 있다.

@Entity
@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;
    private int quantity;

}

 

 

실행해보면 ITEM을 제외한 나머지 세 개의 테이블만 생성된 것을 확인할 수 있다.

 

 

 

ITEM 클래스의 필드를 포함한 각 클래스의 필드를 가진 테이블이 만들어졌고, 이 경우에는 별도의 테이블이 생성되는 것이기 때문에 `@DiscriminatorColumn`를 사용할 필요가 없고 사용해도 적용되지 않는다.

 

 

이 전략은 서브타입을 명확하게 구분하여 처리할 때 효과적이며, not null 제약조건을 사용할 수 있지만 2개 이상의 자식 테이블을 함께 조회할 때 성능이 좋지 않으며 자식 테이블을 통합하여 쿼리하는 것이 어렵다.

 

 

 

 

마무리

 

각 전략은 장점과 단점이 모두 존재한다. 그렇다면 어떤 전략을 선택하는 것이 좋을까?

 

데이터베이스의 스키마 구조와 쿼리 및 성능 요구사항 등을 고려하여 가장 적합한 전략을 선택해야겠지만, 기본적으로 조인 전략을 사용하되 데이터가 간단하고 확장 가능성이 낮으면 단일 테이블 전략을 사용하는 것이 좋다고 볼 수 있다.

구현 클래스마다 테이블 전략은 쿼리 최적화 및 데이터의 중복 문제 때문에 사용하지 않는 편이 좋다.

 

추가적으로 DTYPE은 성능 및 유지보수 측면 등의 이점이 있어 항상 만드는 것이 좋다.

 

 

 

 

참고

 

 

Contents

Copied URL!

Liked this Posting!