당니의 개발자 스토리

상속관계 매핑 본문

스프링/실전! 스프링 부트와 JPA 활용1

상속관계 매핑

clainy 2024. 8. 17. 15:05

상속관계 매핑

이번 시간에는 고급 맵핑, 그러니까 주로 상속관계 맵핑에 대해서 알아보겠습니다.

우선 목차를 보시면 상속관계 맵핑이라는 것을 jpa가 어떻게 지원하는지, 그러니까 객체의 상속관계를 테이블에 어떻게 맵핑하는지 이것에 대해서 설명을 드릴 거구요. 그리고 이제 @MappedSuperclass 라고 속성만 상속하는 건데, 그것에 대해서 설명 드리고 실전 예제에서 이 상속관계 맵핑을 어떻게 사용하는지 보여드리겠습니다.

상속관계 맵핑입니다.

우선 객체는 상속관계가 있죠. 그런데 이제 관계형 데이터베이스에는 상속관계가 사실 상속관계 비슷하게 지원하는 데이터베이스들도 있는데 사실 객체의 상속관계랑은 또 다르거든요. 그런데 이제 그나마 데이터적으로 보면 객체의 상속관계와 비슷한 모델이 있어요. 그게 뭐냐면 관계형 DB에는 슈퍼타입 서브타입 관계라는 모델링 기법이 있습니다. 그게 객체의 상속관계와 그나마 유사해서 맵핑해서 쓸 수 있고요.

그래서 이제 정리해서 말씀드리면 상속관계 맵핑이라는 건 객체의 상속구조 DB의 슈퍼타입 서브타입 관계맵핑하는 겁니다.

자 여기 왼쪽에 나오는 그림이 이제 DB 쪽을 공부해 보신 분들은 알겠지만 관계형 DB 설계논리 모델물리 모델이 있거든요. 이 그림이 논리 모델의 예인데, 지금 ITEM이라는 게 있고 그거에 대한 구체적인 음반, 영화, 이 나와있습니다. 그래서 음반, 영화, 책 이라는 걸 팔면 뭔가 공통적인 속성이 있을 수 있잖아요. 뭐 상품의 이름이라던가, 가격이라던가 이런 거는 공통적인 속성이 데이터니까 그거는 물품 이라는 데에 두고요. 그리고 나서 음반, 영화, 책 각각에 맞는 데이터들을 밑으로 내리는 거죠. 이런 식으로 슈퍼타입 서브타입 관계라는 논리 모델을 구상할 수 있고요.

그 다음에 이제 오른쪽 그림을 보시면, 객체는 사실 딱 명확하게 상속 관계가 있죠. 예를 들어서 ITEM 이라는 추상타입을 만들어 놓고 구체적으로 Album, Movie, Book 이런 식으로 상속관계로 가져갈 수가 있죠.

그러면 이제 슈퍼타입 서브타입 실제 물리 모델을 구현하는 방법은 DB 입장에서 보면 사실 세 가지 방법이 있어요. 이렇게 적어놨는데 이걸 그림으로 먼저 보여드릴게요.


DB 입장에서 이 그림 있잖아요. '이 데이터를 이런 식으로 구성해야지' 라고 했을 때 DB 입장에서는 세 가지 방법으로 이 그림을 구현할 수가 있어요. 즉 물리 모델로 구체화 할 수가 있는데요.

첫 번째가 조인 전략이라는 방식입니다. 지금 테이블만 설명드리는 거에요. ITEM 이라는 테이블을 만들고 ALBUM 테이블, MOVIE 테이블, BOOK 테이블을 만든 다음에 데이터를 나누고, 필요하면 데이터를 가져올 조인으로 구성하는 거에요. 예를 들어서 ITEM에는 ITEM_ID 라는 것을 두고 ALBUM 데이터를 추가한다고 했을 때, 그 Album의 어떤 이름이나 가격 같은 공통 데이터는 ITEM 테이블에 들어가고 Album에 어떤 artist나 이런 정보는 ALBUM 테이블에 들어가는 거죠.

그래서 Insert는 ITEM 테이블과 ALBUM 테이블 이렇게 각각 하나씩 총 두 번의 Insert를 하고, 조인은 지금 ITEM_ID로 PK가 똑같거든요. 그래서 PK, FK로 이렇게 조인을 해서 가지고 오는 거예요. 이게 조인 전략입니다.


그럼 이제 구분할 수 있어야 되잖아요. ITEM 테이블만 보고서는 이게 ALBUM인지, MOVIE인지 딱 안 나오잖아요. 그래서 여기 안에 구분하는 컬럼을 둡니다. 여기서는 DTYPE 이라고 두었고요. 그래서 DTYPE이 ALBUM이면, ALBUM이랑 조인을 해서 가지고 오고 이런 식으로 해결을 하는 거죠. 이게 이제 조인 전략이라는 방식으로 푸는 방법이고요. 그래서 각각 테이블로 변환하는 게 조인 전략이고,

그 다음에 단일 테이블 전략이라는 건 논리 모델을 한 테이블로 다 합쳐버리는 거에요. 생각해보면 이 방법도 가능하거든요. 그래서 그냥 PK를 그대로 두고 컬럼을 여기 다 때려박는 거죠. 그 다음에 이게 앨범인지, 영화인지, 책인지 이런 거는 이 DTYPE 이라는 걸로 구분하는 거죠. 물론 Dtype 말고 다른 컬럼명을 쓰셔도 돼요.

이제 마지막 방법이 뭐냐면 그냥 테이블 구현을 할 때 이렇게 구현하는 겁니다. Album 테이블, Movie 테이블, Book 테이블 이 3개만 딱 만들고요. 그냥 name, price 공통 데이터 정보를 다 각각 들고 있는 거예요.

자 그래서 논리 모델을 세 가지의 어떤 구체적인 물리 모델로 구현할 수 있는데, 객체 입장에서는 사실 다 똑같단 말이에요.

지금 보시면 객체는 그냥 상속 관계를 지원하기 때문에 오른쪽 그림처럼 딱 모델링을 할 거고, 그러면 이 그림의 db 설계를 왼쪽 그림처럼 했는데 지금 이 왼쪽 그림을

오른쪽 그림처럼 이런 식으로 조인하는 스타일로 해서 ITEM_ID를 동일한 걸 쓰도록 DB 설계를 할 수 있습니다. 그리고 보시면 데이터가 좀 굉장히 정교화된 방식이죠. 이런 방식으로 DB 설계가 될 수도 있고,

그 다음에 그냥 되게 심플하게 한 테이블로 때려박기. 단순하기도 하고 보통 성능 때문에 이런 것들을 고려하기도 하는데, DB는 오른쪽 그림처럼 되는데 객체는 또 왼쪽 그림처럼 객체 지향적으로 상속 관계로 되어 있을 수도 있죠.

그 다음에 얘는 이제 DB는 name, price, book 자체를 그냥 각각 가져와 버리는 거죠. 이런 방식으로 해서 ITEM 테이블 같은 거를 없애고 그냥 ALBUM, MOVIE, BOOK 같은 구체적인 테이블만 딱 만들고 거기 안에 속성을 다 넣는 거죠. 오른쪽 그림 보시면 데이터가 좀 중복이긴 해도 이렇게 구현하는 방법이 있단 말이에요.
DB 입장에서는 이 세 가지 방법 중 어떤 방법으로 구현하더라도 JPA에서는 다 맵핑이 가능합니다.

이제 결론은 이 상속관계 맵핑이라는 것은 왼쪽 그림과 같은 DB의 슈퍼타입, 서브타입 관계라는 이 논리 모델링 기법을 세 가지 방법 중 어떤 방법으로 구현을 하든 다 맵핑을 하도록 지원을 해줍니다.

자 그러면 이제 이 세 가지 방법에 대해서 구체적으로 그림으로 하나씩 말씀드릴게요.

제일 먼저 이게 가장 정교화된 방식이에요. 만약에 ITEM, ALBUM, MOVIE, BOOK이 있는데 이거를 DB에서 이제 굉장히 정교화된, 오른쪽 그림처럼 조인하는 방식으로 문제를 해결하면 이제 데이터가 굉장히 정교화 돼서 들어가죠. 보시면은 name, price가 중복이 안되고 name, price는 딱 이 ITEM 테이블에 딱 들어가고 album, movie, book 이것만 이제 각각 다른 데이터들만 이렇게 들어오는 방식으로 DB를 잘 정규화해서 모델링 하는 거죠. 이게 이제 JPA랑도 가장 유사한 모델이기도 합니다.
어쨌든 이거를 제가 맵핑을 어떻게 하는지 한번 보여드리겠습니다.

객체는 지금 ITEM 클래스를 만들게요.

이렇게 필드를 적어주고, 지금 우리가 만드려고 하는 게 쇼핑몰인데 다양한 상품을 판단 말이에요.

그래서 Album 클래스를 만들어주고 여기에는 artist 정도만 있는 거예요.

그리고 extends 해서 Item 클래스를 상속받는 거죠.

그 다음에 똑같이 Movie를 만들고 필드를 채워줄 게요. 영화감독이랑 배우만 넣고,

Book도 만들어서 작가랑 isbn만 넣어줄게요.
이렇게 하고 그냥 돌려볼게요.

아 @Entity를 빼먹었네요. album, movie, book 에 모두 @Entity를 적어주고 다시 돌려볼게요.

보시면, 지금 기본 전략 자체가 한 테이블에 다 들어온게 보이시죠. 이 말은 jpa 기본 전략 자체가

싱글 테이블이라고 하는 한 테이블에 다 때려박는 방식으로 맵핑이 된다는 겁니다.
자 그러면 전략을 어떻게 설정하냐,

여기 보시면, @Inheritance 라고 하는 어노테이션이 있는데요.

@Inheritance 해서 strategy를 뭘로 적냐면,

@Inheritance에 들어가보시면, 아까 같은 싱글 테이블, 한 테이블이랑 맵핑하는 게 기본 전략이어서 그렇게 됐던 거고, 이 SINGLE_TABLE 타입을

이렇게 JOINED 라고 잡으시면,

딱 오른쪽 그림과 같은 모양으로 테이블 설계가 됩니다.
실행을 해보면,

스크립트가 쭉쭉 나와있죠.

이 Item의 id가

이거랑 이제 똑같이 맵핑 된 걸 볼 수가 있습니다. h2 데이터베이스를 들어가셔서 보면,

스크립트를 넣어서 보시면 더 구체적으로 이해가 되겠죠.
이제 JpaMain으로 들어가셔서 테스트를 한번 돌려봐야겠죠.

Movie를 등록하겠습니다.

일단 Movie랑 Item에 각각 Getter, Setter를 다 넣어주고요.

이렇게 값을 넣어준 다음, em.persist() 할 게요. 이렇게 하고 돌려보면,

쿼리가 insert into Item 이랑 insert into Movie가 나오는 걸 보실 수 있습니다. insert 쿼리가 두번 나가죠.
h2 데이터베이스를 보시면,

이렇게 됐구요. movie 테이블에도 데이터가 들어간 걸 확인하실 수가 있고, Item의 id랑 Movie 테이블의 id는 똑같습니다. 왜냐하면은 Itemid PK인데, 자식 테이블 입장에서는 자식 테이블의 id가 PK면서 FK를 동시에 잡아야 됩니다.
자 이번에는 조회를 해볼건데, 재밌는게 조회하는 경우에는 조인해서 가져옵니다.

em.flush 하고 em.clear를 하면 영속성 컨텍스트 안에 있는 걸 DB에 다 날리고, 영속성 컨텍스트 안에가 깔끔하게 제거하니까 1차 캐시에는 아무것도 안 남겠죠.

여기서 em.find로 다시 movie 타입으로 찾아서 id로 가지고 온 다음,

findMovie를 보여드리고 싶어서 출력하고 돌려보면,

Movie를 가져오려면 Item을 조인해서 가져와야 되겠죠? 여기 보시면 쿼리가 Movie로 하고, Inner Join을 Item 으로 해서 Movie를 가지고 오게 됩니다. 이제 이런 상속 관계인 경우에 JPA가 이런 것까지 다 해줍니다. 조인이 필요하면 조인까지 다 하고, 여기 보시면 Insert 할 때는 두 번 Insert 해야 되면 이제 그런 것까지 JPA가 다 챙겨서 해주게 됩니다. 이게 맵핑 한번 잘 해서 이제 이런 효과가 있는 거죠.

자 그럼 여기서 이제 Dtype, 이게 지금 생략됐는데요.

그럼 Item에 들어가서 @DiscriminatorColumn 어노테이션을 넣어주고 다시 돌려볼게요.

그럼 Item table에 가보면 create table 돼서 아마 이렇게 생성이 되어있을 거예요. Dtype이 이렇게 들어와있죠. db에 가보시면,

ITEM 테이블에 Dtype의 Movie 라고 되어있죠.

여기에 기본 default, 즉 discriminator value에 엔티티 명이 들어가게 될 건데 뭔지 설명해드릴게요.


이제 구현체마다 다른데 @DiscriminatorColumn을 넣어주면 Dtype이 생기구요. 왼쪽의 album, movie, book 같은 엔티티명이 디폴트 값으로 들어가게 됩니다. Dtype을 안 넣어도 되긴 하죠. 내가 객체를 보고 뭘 조인해야 될지 아니까. 근데 그냥 이 Dtype이란 거 없이 DB에서 ITEM만 제가 Select 쿼리를 해보면 ITEM이 ALBUM 때문에 들어온 건지, MOVIE 때문에 들어온 건지, BOOK 때문에 들어온 건지 모른단 말이에요. 운영하거나 DB에서 어떤 작업들이 필요할 때를 생각해 보면 이 Dtype을 웬만하면 넣어두는 게 좋습니다.

보시면, 이름은 바꿀 수 있어요. name 이렇게 해서, 내가 컬럼명을 Dtype 말고 DIS_TYPE 이라고 해서 그냥 바꾸셔도 되구요. 그렇게 되면 Dtype이 아니라, 다른 컬러명으로 바뀌겠죠.

자 근데 뭐 이런거는 관례로 쓰는게 좋으니까 그냥 저는 뒀습니다.
그 다음에 예를 들어서 이런 상황일 수 있어요.


dba 분이 Dtype은 알겠는데 나는 우리 회사 rule상 다 이름을 받고 A, M, B 이렇게 Album은 A로 하고 Movie는 M으로 하고 Book은 B로 넣어! 라고 가이드를 줄 수 있단 말이에요.

그러면은 이렇게 해서 @DiscriminatorValue 라고 있어요. 이 애노테이션은 이제 자식 클래스에다가 넣어야 되겠죠.

어노테이션에 들어가 보시면 이제 Default가 아마 Entity 명일 거에요. Default is EntityName 이라고 써있죠. 그러니까 아까

이 EntityName 기본이 클래스명이랑 똑같거든요.

자 근데 만약에 이제 바꿔야 되면 여기에다가 "A" 이렇게 할 수도 있고, 회사 규약에 맞춰서 이름을 바꿀 수도 있습니다. Movie와 Book에다가도 @DiscriminatorValue를 적어줄 게요.
저는 개인적으로는 이렇게 축약하는 걸 좋아하지 않습니다. 어쨌든 이 Annotation을 바꿀 수 있다는 걸 보여드리려는 거구요.
자 이렇게 하고 돌리면,

DB에 보면 Dtype이 M이라고 되겠죠. 이렇게 해서 구분할 수 있는 거에요.

그러니까 예를 들어서 제가 이 ITEM 테이블만 Select 했을 때, '오 얘가 도대체 뭐 때문에 select 됐을 까? 자식 타입에 어떤 애가 들어가지?' 라고 했을 때 DB만 보면 Dtype이라는 게 있어야 알 수가 있겠죠. 한 번에 좀 편하게 운영하기 편하겠죠.

이게 조인 전략에서는 사실 이 @DiscriminatorColumn이 사실 없어도 어떻게든 할 수 있어요. 자식 타입을 다 쿼리 해보면 되니까. 그런데 그 다음에 말씀드릴 싱글 테이블 전략에서는 @DiscriminatorColumn이 꼭 들어가야 돼요.

자 아무튼 이제 이거를 슈퍼타입, 서브타입 논리 모델에서 JOIN 형태로, 가장 정교화된 모델링으로 DB 설계가 되어 있는 것을

이렇게 맵핑을 할 수 있다. 그리고 이거를 

JPA에서는 조인 전략을 맵핑한다고 해서 JOINED 라고 해서 이 @Inheritance 어노테이션에다가 적어 주시면 됩니다. 그 다음 두번째 싱글 테이블.

만약에 오른쪽 그림처럼 DB를 설계를 해봤는데 '아 이게 너무 복잡해.. 프로젝트 단순화하고 우리 그냥 한 테이블로 가자' 라고 할 수도 있거든요.

그러면 이렇게 단일 테이블 전략으로 맵핑 하시면 돼요. 그냥 쉽게 얘기해서 한 테이블, 테이블 하나에 다 때려받고 Dtype 이라는 걸로  Album인지 Book인지 Movie인지 구분하시는 거예요. 이 방식이 단일 테이블 전략이라고 보시면 되고 우선 한번 단일 테이블 전략을 일단 적용해볼게요.
어떻게 하면 되냐?

그냥 이렇게 바꾸면 돼요. 실행해 볼게요.

자 싱글 테이블로 바꿨구요. 테이블 생성된 거 볼게요. 한 테이블에 생성이 되고 MOVIE나 이런 테이블은 생성이 안됩니다.

자 보면 이제 이거는 옛날 데이터예요. 아마 drop이 안돼서 남아있는 건데 Album이랑 Movie에 대해서는 인식을 못하고 있으니까.

그래서 실제로는 여기 ITEM 테이블만 있다 보시면 됩니다. 다 지우고 다시 생성하면 ITEM만 생성돼요. 이 경우에는 한 테이블에 그냥 딱 한 줄로 쫙 들어가죠. 그리고 뭐 Movie와 상관이 없는 artist나, ISB나, 책과 관련된 것들 이런 나머지들은 다 null로 들어가게 됩니다. 이 방식의 장점은 성능이 아무래도 제일 잘 나오겠죠.

자 그리고 Insert 문 보시면 쿼리가 한 번에 들어가죠. 쿼리를 두 번 칠 필요가 없죠.

그 다음에 Select 쿼리를 보시면 굉장히 심플하게 Select 쿼리가 나갑니다. 조인할 필요가 없는 거죠. Insert도 한번에 되고 조인할 필요도 없기 때문에 아무래도 성능상 훨씬 더 이점이 있겠죠.
제가 조인 전략의 장단점은 마지막에 다시 한번 정리를 할게요.

그래서 다시 돌아와서, 이전에 조인 전략에서는 @DiscriminatorColumn이 없으면 Dtype이 안 생겼는데요.

단일 테이블 전략은 @DiscriminatorColumn이 없어도 됩니다. 없애시고 돌려보시면

Dtype이 들어가있죠. 왜냐하면 단일 테이블 전략은 생각해보시면, 조인 전략은 테이블에 분리가 되어있으니까 어떻게든 다른 테이블, Album 테이블을 뒤져서 보면 '이게 Album의 데이터구나' 라고 알 수가 있거든요. 근데 단일 테이블 전략은 내가 Album인지 artist인지 알 수 있는 방법이 없어요. 한 테이블이 들어가있기 때문에 이 Dtype이 꼭 필수로 생성이 됩니다.
JPA 스펙에서는 사실 조인 전략에도 무조건 이 Dtype이 들어가게 되어 있어요. 근데 아마 Hibernate @DiscriminatorColumn이 없으면 Dtype을 그냥 생략하는 것 같아요.
근데 이제 결론은 이 Dtype은 운영상 항상 있는게 좋습니다.

자 다시 한번 정리해 드리면 단일 테이블은 테이블이 하나 생성된다.

그리고 여기 보시면 이렇게 Movie에서 데이터를 생성해서 Persist 하면,

Query가 한 번만 나가면 된다.

조회도 Join할 필요 없이 굉장히 심플한 Query가 하나 나간다. 라고 이해하시면 됩니다.

JPA가 내부적으로 이런 Dtype으로 한번 걸르고 이런 것들은 다 해주고요. 여기 이제 재미난 게 있죠. 뭐냐면 제가 DB 설계를 바뀌었단 말이에요. 그런데 코드를 손댄게 거의 없어요.

이 어노테이션의 이거 전략만 딱 바꿨어요. 이게 JPA의 되게 큰 장점 중에 하나인데, 예를 들어서 애플리케이션을 조인 전략으로 해서 막 개발했단 말이에요. 근데 막 성능 테스트 하고 해보니까 개발 시점에 성능이 너무 안 나오는 거에요. 그러면 '안되겠다, 단일 테이블 전략으로 가자' 라고 이제 DBA 분이랑 얘기해서 만약에 정리가 됐어요. 그러면 만약에 우리가 JPA를 안쓰고 개발을 안한다 치면 소스 코드를 정말 많이 뜯어 고쳐야 되거든요. 쿼리 같은 걸 다 고쳐야 되겠죠.
그런데 JPA를 사용하면 단일 테이블 전략으로 바꾸려면,

이것만 바꾸면 돼요. 테이블만 새로 만드시고, 이것만 싱글 테이블로 바꿔서 맵핑 하시면 됩니다. 그런 게 되게 큰 장점이죠. 코딩하는 시간 자체에 확 줄어들죠.

자 그 다음에 이제 마지막으로는 구현 클래스마다 테이블 전략이라는 게 있는데요. 이거는 그림으로 설명을 드리면 조인 전략이랑 비슷한데, 위에 ITEM 클래스를 없애버리고, 그러니까 ITEM 테이블을 없애버리고 그냥 거기 있는 속성들을 그냥 다 밑으로 내리는 거에요. 그래서 name, price 같은 이런 속성들을 중복되게 허용하는 거에요.
이걸 한번 해보겠습니다.

TABLE_PER_CLASS 라는 걸로 하시면 되구요.

자 실행을 하면 얘는 ITEM 이라는 테이블이 안 만들어져야 하는데 있어요. 이건 제가 잘못 만들어서 그런건데요.

abstract class로 만드는 게 맞죠. 왜냐면 제가 단순하게 public class Item 으로 만들면,

Item 혼자만 독단적으로 쓰는 일도 있다는 뜻이거든요. 상속과 상관없이.
그래서 추상 클래스로 만들어서 실행해 보겠습니다. 처음부터 지금 제가 추상으로 만들었어야 했는데 추상 클래스로 만들어 보면,

Create Table 에서 Album, Book 뭐 이런 애들이 있는데 ITEM이 없죠.

자 그렇게 해서, 오른쪽처럼 3개의 테이블만 생긴 거예요. Album,Movie,Book 이렇게.
데이터가 어떻게 들어갔냐면,

DB에 들어가서 한번 볼게요. movie에 데이터가 딱 fit 하게 들어갔죠. 그럼 나머지 테이블에는 뭐 안 들어갔겠죠. ITEM 테이블 자체가 존재하지 않죠.

자 여기 보시면 쿼리도 되게 심플합니다. 당연하겠죠 그냥 movie에 넣으면 되죠.

그 다음에 select도 movie 테이블에서 그냥 하면 되죠. jpa가 이런건 그냥 자기가 테이블을 분리해가지고 해줍니다.

그리고 사실 여기서는 @DiscriminatorColumn이 의미가 없겠죠. 이 전략에서는 @DiscriminatorColumn을 넣어도 사용이 안됩니다. 왜냐하면 구분할 필요가 없죠. 테이블 자체가 다르기 때문에.

자 그러면 뭔가 '오 이거 되게 좋네요' 라고 할 수 있는데, 이 전략이 이렇게 보면 단순하게 값을 넣고 뺄 때는 딱 좋은데, 언제 망하냐면 한번 보여드릴게요.

제가 조회를 하는데 이제 타입을 제가 객체 지향이니까

ITEM 타입으로도 조회할 수 있단 말이에요. 당연히 부모 클래스 타입으로 조회할 수 있어야죠.

이렇게 했을 때 그러면 구현 클래스마다 테이블 전략은 조회할 때 어떤 일이 발생하냐면, 데이터를 넣을 때는 괜찮은데 찾을 때가 문제에요.

보시면 찾을 때 union이 보이시나요. JPA는 union all로 해서 이걸 다 뒤집니다. 명확하게 딱 찔러서 가져올 땐 괜찮은데 그 외의 경우에는 테이블 다 뒤져봐야 돼요. 이 데이터가 여기 있는지 없는지, 이 아이디가 있는지 데이터를 다 찔러봐야 되거든요. 그래서 이런 되게 복잡한 쿼리가 나가게 됩니다.


오른쪽 그림을 보시면, 예를 들어서 우리가 ITEM_ID만 알아요. 만약 'ITEM_ID5번이야, 데이터를 여기서 찾아야 돼' 라고 하면 이 테이블 3개를 다 Select 해봐야 되는 거거든요. 굉장히 비효율적으로 동작하게 됩니다.

이 전략은 이제 아이디가 명확하니까 부모 타입으로 조회를 해도 이 ITEM 테이블만 조회하면 되고,

얘도 뭐 Dtype 이런 거 다 알고 ITEM_ID 이런 걸 다 아니까 되게 심플하게 조회가 되거든요.

 

자 그럼 이제 세 가지를 대략적으로 보여드렸고 각각의 장단점을 설명해 드리겠습니다.


우선 이 조인 전략은 장점이 정규화 되어있죠. 데이터가 정규화도 되어있고, 제약 조건을 ITEM 테이블에 걸어서 맞출 수가 있죠. 예를 들어서 가격을 가지고 정산을 해야 된다고 하면 이 ITEM 테이블만 보면 되고, 되게 장점이 많아요.

조인 전략의 장점은 뭐냐면 테이블이 정규화되어 있다. 그리고 외래키 참조 무결성 제약 조건을 활용 가능하다.


무결성이란?



외래키 참조 무결성 제약 조건이 뭐냐면, 여기 ITEMITEM_ID랑 이 ALBUMITEM_ID, 이거를 쓸 수 있고, 그 다음에 또 장점이 예를 들어서 뭔가를 주문해서 ITEM을 봐야 돼요. 주문 테이블에서 외래키 참조로 ITEM을 봐야 되면 ITEM 테이블만 보면 되겠죠. 막 아래 테이블 3개를 다 봐야 되면 아무래도 모델링이 이상해지겠죠. 다른 테이블 입장에서 주문이나 테이블 입장에서 ITEM을 봐야 될 때 ITEM 테이블을 딱 보면  돼요. 그렇기 때문에 되게 설계가 깔끔하게 들어갈 수 있습니다.


그 다음에 저장 공간이 효율화가 돼요.


오른쪽 그림을 보시면 딱 정규화가 되어있거든요. 저장 공간 자체가 되게 효율화됩니다.


단점은 뭐냐면 아무래도 조회할 때 조인이 많이 사용이 되는 편이죠.


뭘 해도 여기 ITEM이랑 ALBUM을 막 조인해서 가져와야 되잖아요.


그래서 결국 조회 쿼리가 좀 복잡한 편이구요. 그리고 이제 데이터를 저장할 때 Insert 커리가 두 번 나갑니다. 뭐 이런 단점이 있는데 사실 Insert 쿼리가 두 번 나가거나 조회할 때 조인하거나 하는 거는 단점은 아니긴 한데, 이제 단일 테이블이랑 비교하면 좀 복잡한게 단점이죠. 아무래도 테이블이 많이 있으니까 관리되는 게 좀 복잡하죠.

자 그래서 뭐 조회 시 조인을 많이 사용해서 성능 저하가 된다. 이것도 사실 조인이라는게 잘 맞추면 성능이 잘 나오기 때문에 막 그렇게 저하되지 않습니다. 그리고 이제 저장 공간이 효율화되기 때문에 오히려 또 어떤 측면에서는 성능이 더 잘 나올 수도 있고요.


기본적으로는 조인 전략이 정석이라고 딱 보고 들어가셔야 돼요. 조인 전략이 객체랑도 되게 잘 맞고 뭔가 정규화도 되고 뭔가 설계 입장에서 되게 깔끔하게 설계가 떨어져 나와요.
그런데 이제 단일 테이블 전략과 비교를 해서 단점은 아무래도 좀 복잡하고 성능이 아무래도 좀 안 나올 확률이 높죠.

그 다음 단일 테이블 전략.

일반적으로 조회 성능빠릅니다.


PK 값을 딱 넣으면 바로 팍 나오죠


그래서 조회 쿼리가 되게 단순하죠. 한 테이블만 보고 쿼리를 짜면 되니까. 대신 치명적인 단점이 있는데, 자식 엔티티가 맵핑한 컬럼은 모두 null을 허용해야 됩니다.


그니까 뭐 예를 들어서 여기서 name이랑 price 두 개 빼고는 다 널 허용해줘야 돼요. 여기서 이 artist, director 왜냐면 예를 들어서 Album에 넣으면 작곡가 데이터만 들어가지, 뭐 director나 actor 이런 거는 안 들어갈 거잖아요. 그래서 얘네가 다 null 허용이 돼야 됩니다.


그러니까 이제 데이터 무결성 입장에서 볼 때는 조금 애매한 게 있죠. 그 다음에 단점이 단일 테이블의 모든 것을 다 때려받기 때문에 오히려 테이블이 커질 수 있고, 상황에 따라서는 조인 테이블 전략과 비교해서 성능이 오히려 좀 느려질 수도 있습니다. 그런데 단순하기 때문에 일반적으로는 성능이 더 빠르죠. 이게 뭔가 이 정도 되려면 임계점을 넘어야 되는데 보통 이렇게 임계점을 넘을 리는 거의 없는 것 같아요.

그 다음에 구현 클래스마다 테이블 전략이라고 하는데, 이거는 결론부터 말씀드리면 이거는 쓰면 안되는 전략입니다. 왜 안되냐면 일단은 ORM 세계는 객체지향 스타일을 좋아하시는 분들과 DB를 좋아하시는 분들의 양쪽에서 가끔 트레이드 오프를 해야 될 때가 있는데 이거는 둘 다 싫어합니다.

이 전략은 데이터베이스 설계자와 ORM 전문가들이 보통 둘 다 추천하지 않는 전략입니다.


뭔가 묶이는 게 없잖아요. 잘 보시면 뭘 묶어내야 시스템이라는 게 통합이 가능한데, 예를 들어서 뭐 Album, Movie, Book 데이터를 가지고 정산을 해야 돼요.

그러면 단일 테이블 전략의 오른쪽 ITEM 테이블 같은 경우에도 price 있으니까 ITEM 테이블을 다 돌려 가지고 그냥 정산만 하면 돼요. 얘가 Album이다,  Movie다, Book이다 상관없이 어떻게든 정산을 해내면 돼요.

이것도 마찬가지예요. 조인 전략도 보시면 price 라는 게 ITEM 테이블에 다 모여 있기 때문에 ITEM 테이블의 price를 가지고 어떻게든 정산을 해내면 돼요.

근데 이거는 난리가 나요. 새로운 게 추가가 될 때마다 테이블에 추가 한 걸로 정산 코드를 다시 다 짜야 되죠. Album 테이블로도 정산 돌리고, Movie 테이블로도 정산 돌리고, Book 테이블로도 정산돌려야 되니까 난리가 나는거죠.

장점은 서브타입을 명확하게 구분해서 처리할 때 효과적이에요. 아까 보여드린 것처럼 Insert하고 Select 할 때는 되게 효과적일 수 있어요. 그리고 이제 not null 제약 조건을 사용할 수 있어요.


왜냐면 이게 다 따로 돼 있거든요. artist는 not null. 다 쓸 수 있는 거죠. 그러니까 Movie는 무조건 director가 있어야 돼. 라고 해서 이 not null을 다 쓸 수 있어요.


역시 치명적인 단점은 여러 테이블을 함께 조회해야 할 때 성능이 union 쿼리를 날려야 되죠? 쿼리를 여러번 다 해봐야 돼요. 굉장히 좋지 않은 상황이 나오고, 자식 테이블을 통합해서 쿼리하기 어렵죠?


이 세 테이블을 통합해서 쿼리하려면 난리가 나고 문제가 변경이라는 관점에서 볼 때 되게 안 좋아요. 뭔가 시스템이 새로운 타입이 추가가 될 때 이 케이스는 굉장히 많은 걸 뜯어야 됩니다. 그렇게 한번 설명을 쭉 드렸구요.

 

이제 상속 관계 맵핑을 정리해 드릴게요.

관계형 db에는 상속 관계 맵핑이 없다. 그런데 슈퍼타입, 서브타입 관계라는 논리 모델링 기법이 있어요. 보통 DB 공부해 보시면 왼쪽과 같은 그림으로 나와요. 왼쪽 그림이 이제 객체 상속 관계 맵핑과 유사하고, 이 논리 모델을 물리적으로는 세 가지 방법으로 DB 입장에서 구체화 할 수가 있다.
그 방법이 이제 조인 전략이라고 불리는

이렇게 굉장히 정규화된 방식으로 구현하는 방법이 있고,

그 다음에 테이블 하나로 그냥 딱 뭉치는 방법이 있고

또 이렇게 중복을 허용하고 구체적인 테이블 위주로 이렇게 Album, Movie, Book 세 개의 테이블을 뽑아내는 방식이 있습니다.

jpa 에서는 이거를 이렇게 annotation 으로 다 맵핑을 할 수가 있습니다. JOINED, SINGLE_TABLE, TABLE_PER_CLASS 이렇게 할 수가 있다. 라는 걸 설명 드렸고요.


그 다음에 기본적으로는 조인 전략을 머리에 갖고 가셔야 돼요. 그리고 여기서 이제 뭐랑 트레이드 오프를 해야 되냐면,

조인 전략의 어떤 장단점이랑

단일 테이블 전략의 장단점. 이 두 가지만 가지고 둘 중에 뭘 하지 라고 DBA랑 충분히 상의하시거나 본인이 DB 설계를 잘 하시면 고민하셔서 선택하시면 돼요.

근데 얘는 고민을 하지 마라. 먼 미래에 언젠가는 정말 큰 후회를 하게 됩니다.
저같은 경우에는 주로 어떤 선택을 하냐면,

조인 전략을 일단 기본으로 깔고요.
그런데 진짜 단순할 때가 있거든요. 단순한 거에는 너무 복잡하게 에너지를 들이면 안 돼요. 데이터도 얼마 안 되고 너무 단순하고 확장할 일도 별로 없을 것 같다고 하면,


그럴 경우에 저는 단일 테이블 전략으로 많이 갑니다. 실제 애플리케이션 개발할 때도 단일 테이블 전략을 많이 선택을 했고요.
근데 얘가 비즈니스적으로 되게 중요하고 좀 복잡해. 라고 하면,


저는 조인 전략을 가져갑니다.


이거는 이제 다 트레이드 오프가 있기 때문에 두 개 사이에서 고민을 해보셔야 돼요.

이렇게 상속관계에서 설명을 한번 드렸구요. 자 다음 시간에는 @MappedSuperclass 라는 거에서 설명을 해드리겠습니다.

'스프링 > 실전! 스프링 부트와 JPA 활용1' 카테고리의 다른 글

필드와 컬럼 매핑  (0) 2024.06.07
상품 주문  (0) 2024.05.19
변경 감지와 병합(merge)  (0) 2024.05.16
상품 수정  (0) 2024.05.15
상품 목록  (0) 2024.05.14