오늘은 JPA의 상속관계 매핑에 대해서 포스팅해보고자 한다.
이전 프로젝트를 진행하면서 상속관계 매핑을 고려하지 않고 엔티티를 구성하여, 많은 부분에서 고생을 한 경험이 있었다ㅠㅠ...
하나 하나 알아가보자.
상속 관계에 대한 데이터베이스와 객체의 패러다임 불 일치
우선 상속이라는 것은 객체에는 존재하지만, 관계형 데이터베이스에는 존재하지 않는다. 이 존재하지 않는다는 것은 물리적으로 존재하지 않는다는 것이다. 하지만, 논리적인 데이터 모델링을 통해서 물리적으로 가능하게끔 만드는 방법은 존재한다.
그 방식이 슈퍼타입, 서브타입 방식이다.
이러한 논리 모델을 물리 모델로 변환하는 세 가지 기법이 있고, 해당 기법들을 JPA가 지원을 해준다.
- 조인 전략 : 부모 기본 키를 받아 테이블을 생성 (InheritanceType.JOINED)
- 단일 테이블 전략 : 모든 요소를 합쳐서 하나의 테이블로 변환 (JPA 기본 값, InheritanceType.SINGLE_TABLE)
- 구현 클래스마다 테이블 전략 : 속성을 중복하여 각각 다른 테이블로 생성 (InheritanceType.TABLE_PER_CLASS)
그리고 각 구현은 엔티티에 다음과 같이 설정해서 적용이 가능하다.
@Entity
@Inheritance(strategy = InheritanceType.XXX)
public class Member {
}
이렇게만 봐서는 느낌이 잘 오지 않을 것이다. 각각에 대해서 한 번 살펴보자.
조인 전략
부모의 기본키를 각 테이블에서 기본키, 외래키로써 사용하여 테이블을 구분한다.
그리고 각각의 테이블은 따라서 각 테이블에 있는 요소를 가지고 오기 위해서는 조인이 필수이다.
슈퍼 타입에 DTYPE이 보이는데, 실무에서 DTYPE이 없다면 해당 인스턴스가 어떤 서브 타입을 구현했는지 알 수 없기 때문에, 운영상의 이점을 살리기 위해 추가하는 것이 좋다고 한다. 조인 전략에서는 기본 값으로 DTYPE이 들어가지 않는다. 따라서 추가하기 위한 옵션이 필요하다.
@Entity
@Inheritance(strategy = InheritanceType.XXX)
@DiscriminatorColumn // 추가해주어야 한다. 기본 이름은 DTYPE
public class Member {
}
위 처럼 DiscriminatorColumn을 추가해주어야 한다. DTYPE에는 각 서브타입들에 대한 엔티티명이 들어가게 되는데,
서브타입에서는 DiscriminatorValue를 사용하여 다른 이름을 지정해 줄 수 있다.
그럼 조인 전략의 장단점을 살펴보자.
장점
1. 테이블의 정규화
- 정규화로 인하여 속성의 중복이 많이 사라지게 된다.
2. 따라서 속성 값이 많이 줄어들어 상대적으로 저장공간을 효율화 할 수 있다.
단점
1. 조회 시 조인을 많이 사용하게 된다. 약간의 성능 저하가 있을 수 있다.
2. 테이블이 많다면 조회 쿼리가 복잡해진다.
3. 데이터 저장 시 INSERT SQL이 2번 호출 된다.
- 위 그림에서 ALBUM 서브 타입을 객체로 저장하면, ALBUM 타입에도 저장이 되고, ITEM 타입에도 저장이 되어야하기 때문이다.
단일 테이블 전략
해당 전략은 JPA에서 상속관계매핑시에 기본 값으로 지정해둔 전략이다. 위의 사진과 같이 서브 타입의 모든 필드가 슈퍼 타입에 필드에 들어가게 되고, 하나의 테이블로만 운영하는 것을 볼 수 있다.
각 서브타입에 있는 값들은 NULL이 허용되며, 자동으로 DiscriminatorColumn이 적용된다.
실무에서는 주로 조인전략과 단일테이블 둘의 Trade-Off를 고려하여 어떻게 상속할 것인지 결정하게 되는데, 그럼 단일 테이블의 장단점이 무엇인지 살펴보자.
장점
1. 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
2. 따라서, 조인 전략에 비해 조회 쿼리가 단순해진다.
단점
1. 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
2. 단일 테이블에 모든 것을 저장하기 때문에, 서브 타입 엔티티가 많다면, 테이블이 엄청 커질 수 있다. 따라서 상황에 따라 조회 성능이 조인 전략보다 더 떨어 질 수 있다.
이제 두 전략에 대한 Trade-Off가 느낌이 올 것이다.
조인 전략은 정규화를 채택함으로써 조회를 하는데 있어서 쿼리가 복잡해지고, 조인이 발생하여 조회 성능이 떨어지지만 공간을 절약 할 수 있고, 테이블이 깔끔해진다는 장점이 있다.
단일 테이블은 정규화를 하지 않고, 하나의 테이블에 모두 넣어서 조회 쿼리가 상당히 간단해지지만, 서브 타입이 많을 때에 조회 성능이 떨어질 수 있다는 단점이 있다.
따라서, 둘을 사용해야 하는 시점은 서브 타입이 많이 생기지 않을 것 같은 경우에는 단일 테이블 전략을 택하고, 서브 타입이 많을 것 같은 경우에는 조인 전략을 사용하는 것이 가장 현명하다.
이런 생각이 들 수 있다. Create 문도 두 번 발생하고 조회 시에 무조건 조인을 사용해야 해서, 성능이 많이 떨어지지 않나요?
실제로, 이러한 연산들은 최적화에 있어서 크게 영향을 끼치지 않는다고 한다. 최적화에 대한 범위가 어디까지 일지는 아직 의문이지만... 저런 부분이 별로 연산에 큰 영향을 미치지 않는다는 것을 보면 더 큰 이유들이 있을 것 같다.
예를 들어, JPA의 지연로딩, 즉시로딩 같은 상황은 최적화에 큰 영향을 미치는 것들이다.
구현클래스마다 테이블 생성 전략
구현 클래스마다 테이블을 생성하는 전략은 위처럼 중복되는 속성을 가지고, 말 처럼 테이블을 객체에 맞게 하나씩 생성하는 방식이다.
이 방식에서는 이제 ITEM 테이블은 생성조차 되지 않는다.
결론부터 얘기하자면 이 방식은 절대로 추천하지 않는다. 그 이유는 조회 성능때문이다.
실제로 ITEM에 속하는 모든 서브타입에 대한 데이터를 가져오고 싶은 경우가 있을 것이다. 이럴 경우에 구현클래스마다 테이블 생성전략은 UNION ALL 을 이용해 각 서브타입에 대한 데이터를 조합으로 가져온다. UNION은 조회를 하는데 있어서 상당히 많은 비용이 소모된다.
실제로도 DBA와 ORM 개발자들이 추천하지 않는 방식이라고 한다.
결론
결국 실무에서는 위에서 잠시 거론되었지만, 상속 매핑이 필요하다면 조인전략 혹은 단일테이블 전략을 Trade-off를 생각해서 사용하면 된다. 참고로, 슈퍼타입 객체를 추상 클래스를 생성해서 사용해도 동일하게 부모 테이블에 저장된다. 추상클래스에서 적절하게 메서드를 생성하고, 적절하게 오버라이딩해서 사용하거나 하는 것이 좋을 것 같다.
해당 포스팅은 인프런 김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편을 바탕으로 작성되었습니다.
'Backend > JPA' 카테고리의 다른 글
Spring Data JPA는 왜 @Repository를 사용하지 않아도 될까? (1) | 2023.05.14 |
---|---|
JPA 연관관계 매핑의 다중성 (0) | 2023.04.28 |
영속성 컨텍스트 (1) | 2023.04.25 |