Spring

상속 관계 매핑을 넘어..

Chemi___6_oj 2023. 3. 24. 14:35

고급 매핑 전략으로 책에서 소개가 될 만큼 상속관계는 관계형 데이터베이스에서 사용하면 이점이 많은 전략이다.

하지만, 어떤 부분에서는 상속관계도 부족한 점이 없지않아 있다고 생각한다. 이는 뒤에서 설명해보려고 한다!

우선, 상속 관계 매핑 전략에 대해 조금 알아본 이후 내가 채택한 방법을 생각해보자.

 

관계형 DB는 원래 상속이라는 개념이 없다. 하지만 슈퍼타입과 서브타입 관계라는 모델링 개념이 있어 이를 사용한다.

그래서 ORM에서는 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것을 상속 관계 매핑라고 한다. 

상속 관계는 대표적으로 3가지 전략이 있다. 간략이 요점들만 설명하고 넘어가겠다

 

조인전략

엔티티 각각을 모두 테이블로 만들고 자식이 부모 테이블의 기본 키를 받아 사용하는 전략을 의미한다.

자식 테이블에서는 부모 키의 기본 키를 그대로 사용해 자식의 기본키  = 부모의 기본 키가 되는 것을 의미한다.

이 경우 객체는 타입으로 구분할 수 있지만, 테이블은 타입의 개념이 없어 타입을 구분하는 컬럼을 추가해 사용해야 한다.

 

사용법

@Ineritance(strategy = InheritanceType.JOINED)

부모 클래스에 이 어노테이션을 사용하고, 매핑 전략을 지정한다.

@DescriminatorColumn(name = "DTYPE")

부모 클래스에 구분 칼럼을 지정한다. 이 컬럼으로 지정된 자식 테이블을 구분할 수 있다.

구분 컬럼이 없어도 동작한다. (@DiscriminatorColumn)

@DescriminatorValue(name = "M")

엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.(자식)

 

장점

테이블이 정규화된다.

외래키 참조 무결성 제약조건을 활용할 수 있다.

저장공간을 효율적으로 사용한다.

 

단점

조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.

조회 쿼리가 복잡하다.

데이터를 등록할 insert sql을 두번 실행한다.

 

몇몇 단점에도 불구하고 상속을 사용할 경우에는 조인 전략이 장점이 많아 이 전략을 많이 사용한다.

 

단일 테이블 전략

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

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

이 전략에는 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야만 한다.(많은 null이 생성된다.)

 

장점

조회시에 조인을 사용하지 않아 조회 속도가 가장 빠르다.

조회 쿼리가 단순하다.

 

단점

자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.

한 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 그리고 상황에 따라서 조회 성능이 느려질 수 있다.

+ 구분 컬럼을 꼭 사용해야 한다. (@DiscriminatorColumn)

+ @DiscriminatorValue 를 지정하지 않으면 기본으로 엔티티 이름을 사용한다.

 

 

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

구현 클래스마다 테이블을 생성한다.

 

장점

서브 타입을 구분해 처리할 경우 효과적이다.

not null 제약조건을 사용할 수 있다.

 

단점

여러 자식 테이블을 함께 조회할 때 성능이 느리다.(union)

자식테이블을 통합해 쿼리하기 어렵다.

구분 컬럼을 사용하지 않는다. 성능이 좋지 못해 사용하지 않음

 

상속 관계 매핑의 문제점과 해결 방법

Hibernate를 살펴보면, join 전략은 말 그대로 SQL의 JOIN을 사용하여 매핑을 하는 전략이다. 이때 Hibernate는 상속과 관련된 모든 데이터를 가져온다. 

 

예를 들어 식물과 꽃이 join 상속 유형을 가지고 있다고 하자.

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

}
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Flower extends Plant {

}

Flower을 포함하는 JPQL쿼리는 모든 Plant 데이터를 가져온다. 만약에 Plant의 부모인 생물 엔티티를 가지고 있다면(더 상위의 상속),

더 많은 JOIN을 활용하고 더 많은 데이터를 가져와야만 한다.

 

이는 EAGER매핑의 한 종류다. HIbernate는 오직 EAGER만 지원이 가능하다. (LAZY 불가!)

 

그래서 우리는 상속 전략에서 오직 Flower만을 가져오는 것은 불가능하다.

 

그래서 이런 문제를 해결하기 위해 상속 관계를 포기하고 조합을 활용한다. 1대 1 매핑 활용!

조합의 개념은 생각보다 간단하다. 기존 클래스가 새로운 클래스의 구성 요소로 사용된다. 

 

아래의 예시를 보면 private 필드로 기존 클래스의 인스턴스를 참조하고 1대1로 매핑 한 것을 볼 수 있다.

@Entity
public class FLower {

    @OneToOne(fetchType = FetchType.LAZY)
    private Plant plant;

}
@Entity
public class Plant {

}

이 경우 조합의 장점이 드러난다.

1. 메서드를 호출하는 방식으로 동작해 캡슐화를 깨뜨리지 않는다. 

2. 기존 클래스에 영향을 주지 않는다.

즉 확장에는 열려있고 변경에는 닫혀있는 개방 폐쇄의 원칙을 지킨 설계가 된다.

 

매핑 상에서도 성능의 이점이 있다. 조회에 대해서도 Flower만을 가져오고 싶다면, Flower에 대한 데이터만 조회해 가져올 수 있다.

 

그렇다면 항상 조합만?

꼭 그렇지는 않다. 

1. 확장을 고려하고 설계가 확실한 is-a 관계일 때,

2. api에 아무련 결함이 없고, 결함이 있어도 하위 클래스까지 전파되어도 괜찮은 경우

 

이 두가지 조건을 모두 만족하는 경우에는 상속을 사용해도 괜찮다. 예시로 든 사례 또한 꽃이 식물이 될 확률이 없고 결함이 있어도 괜찮다면 상속 전략을 사용해도 무방하다. 자기가 설계하는 프로젝트의 크기와 장단점을 고려하여 상속과 장점을 고려하도록 하자!

 

 

참고

 

https://mangkyu.tistory.com/199

https://sodocumentation.net/hibernate/topic/2326/performance-tuning

https://tecoble.techcourse.co.kr/post/2020-05-18-inheritance-vs-composition/