개발자로 후회없는 삶 살기

JPA PART.고급 매핑 본문

[백엔드]/[JPA | 학습기록]

JPA PART.고급 매핑

몽이장쥰 2023. 8. 20. 23:12

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/149

 

김영한의 스프링 부트와 JPA 실무 완전 정복 로드맵 - 인프런 | 로드맵

Java, JPA 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com

 

본론

- 상속관계 매핑

객체의 상속을 JPA에서 어떻게 처리하는지 알아봅니다. 객체는 상속이 있는데 RDB는 없습니다. 근데 RDB에는 비슷하게 슈퍼타입, 서브타입 관계라는 기법이 있고 상속과 그나마 유사해서 매핑해서 사용할 수 있습니다.

 

상속관계 매핑이란? ✅

객체의 상속과 DB의 슈퍼, 서브를 매핑하는 것입니다.

 

RDB에서 음반, 영화, 책이 있고 그것들의 공통적인 속성을 물품 테이블에 둬서 수퍼, 서브를 구성할 수 있습니다. 객체는 이를 상속으로 표현할 수 있습니다.

 

=> 슈퍼 타입, 서브타입 논리 모델을 물리 모델로 구현하는 방법 3가지

1. 조인 전략

상품, 앨범, 무비, 북을 테이블을 다 나누고 fk를 둬서 조인으로 가져오는 것입니다.

 

ex) 앨범을 저장할 때 앨범의 이름, 가격은 아이템 테이블에 들어가고 id와 아티스트는 앨범 테이블에 들어갑니다. insert를 2번하고 조회는 item의 pk와 앨범, 무비, 책의 fk가 Item_id로 같은 데 조인해서 가져옵니다.

 

그리고 상품에 type이라는 구분 컬럼을 둬서 (앨범/ 무비/ 책)을 구분합니다.

 

2. 단일 테이블 전략

논리 모델을 한 테이블에 다 합치고 pk를 그대로 두고 컬럼을 다 넣습니다. 그리고 type을 넣습니다.

 

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

그냥 별도의 테이블 하나씩으로 각각 존재하도록 만드는 방법입니다. 이렇게 3개로 만들 수 있는데 객체 입장에서는 다 똑같습니다. 

 

객체 설계는 그냥 상속을 생각해서 1가지 방법으로 하면 되고 DB를 3가지 중 하나로 할 수 있습니다. JPA는 이 3가지 DB를 다 매핑하도록 지원합니다

 

- 조인 전략으로 DB를 만들고 JPA와 매핑하는 법

-> 객체

@Entity
public abstract class Item {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    private int price;
}

상품 클래스를 만들고 이름, 가격을 잡습니다. 이때 반드시 추상으로 만들어야 하며 독단적으로 만들 수 없는 것임을 밝혀야 합니다.

 

@Entity
public class Album extends Item{

    private String artist;
}

그리고 앨범, 영화, 책 클래스를 만듭니다. extends해서 상품을 상속받습니다. 자식 테이블에 id를 만들지 않습니다.

 

실행해보면 기본 전략 자체가 한 테이블에 들어왔습니다. 이로써 상속은 한 테이블에 다 들어오도록 JPA가 default로 하는 것을 확인했습니다.

 

-> 조인 전략 선택

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

어노테이션을 Inheritance를 합니다. 기본 전략이 single_table(단일 테이블 전략)이라서 그렇게 나온 것입니다.

 

joined로 잡으면 이렇게 설계한 것과 매핑이 됩니다.

 

실행해보면 다 따로 만들어졌습니다.

 

-> 값 넣어보기

Movie movie = new Movie();
movie.setDirector("aaaa");
movie.setActor("bbbb");
movie.setName("바람과 함께");
movie.setPrice(10000);

em.persist(movie);

tx.commit();

movie를 하나 등록해보겠습니다.(persist) 영화는 direct와 actor를 넣으면 끝나는게 아니고 item을 상속한 것이니 item에 있는 가격, 이름도 넣어야 합니다.

 

저장하면 insert 쿼리가 item과 movie가 들어갑니다.

 

item의 id와 movie의 id는 똑같습니다.

 

em.find(Movie.class, movie.getId());

find로 조회하면 join해서 가져옵니다. 

 

movie를 조회하는데 inner로 item을 가져와서 무비를 가져옵니다. 이런 것을 JPA가 다 해줍니다.

 

+ type

@DiscriminatorColumn
public abstract class Item {

type이 생략이 됐는데 @DiscriminatorColumn을 ITem에 붙이면 

 

타입이 생성됩니다.

 

테이블을 보면 movie라고 들어갑니다. @DiscriminatorColumn를 넣으면 dtype이 생기고 앨범, 무비, 북이라는 엔터티 명이 들어갑니다. 어떤 type인지 알기 위해 꼭 있는 게 좋고 이를 JPA에서 해줍니다.

 

@Entity
@DiscriminatorValue("A")
public class Book extends Item{

db에 들어가는 자식 엔터티 명을 바꾸고 싶으면 @DiscriminatorValue을 자식(앨범 등)에 붙이고 원하는 값을 넣으면 됩니다.

 

- 단일 테이블 전략으로 DB를 만들고 JPA와 매핑하는 법

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

한 테이블에 다 때려박고 dtype으로 구분하는 방식입니다. single_table로 전략을 바꾸고 실행하면 기본이 단일 테이블이라서 한 테이블에 생성이 되고

 

db에는 무비의 경우 없는 actor, author 등의 다른 자식의 데이터는 null이 들어갑니다.

 

이것의 장점은 insert나 select나 한 테이블만 보기 때문에 하나로 끝나서 성능이 좋습니다.

 

근데 재밌는 게 있습니다. 테이블 전략을 바꿨는데 코드를 수정한게 전략 말고는 없습니다. 이게 JPA의 장점입니다. 조인 전략으로 개발하고 테스트를 했는데 성능이 안 좋아서 단일 테이블로 바꾸려고 했습니다. JPA를 안 쓰면 모든 쿼리를 다 바꿔야 하는데 JPA는 전략만 바꾸면 되어서 코딩하는 시간이 확 줄어듭니다.

 

- 각각 테이블 전략으로 DB를 만들고 JPA와 매핑하는 법

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

이건 그냥 각각의 테이블로 중복되게 가지는 것입니다. table_per_class로 전략을 바꾸고 실행하면 각 테이블이 다 생기는 item 테이블이 없어집니다.

 

쿼리도 심플하게 테이블 하나에 쿼리를 날리는 게 됩니다. 여기서는 discriminator를 할 의미가 없습니다.

 

DB를 보면 영화에 fit하게 들어가고 item 테이블은 자체가 존재하지 않습니다.

 

-> 설명

이 전략은 단순하게 값을 넣고 뺄 때는 좋은데 부모 클래스 타입으로 조회하면 union으로 다 뒤집니다. jpa는 데이터가 어디에 있다는 것이 명확하지 않으면 모든 테이블을 다 찔러봐야해서 복잡한 쿼리가 나갑니다. 

 

itemid가 5번인데 타입을 모르면 앨범, 무비, 책 3개의 테이블을 다 뒤져봐야 하는 거라서 굉장히 비효율적으로 동작하는 것입니다. 조인과 단일 테이블은 dtype도 알고 pk도 item과 자식들이 연결되어서 괜찮은데 per table 전략만 이런 문제가 있습니다.

 

- 장, 단점
=> 조인 전략

-> 장점

1. 정규화 : 데이터가 정규화 되어있다.
2. 정산을 할 때는 부모에 있는 price만 보면 된다.
3. 외캐키 활용 : Order가 있을 때 Item을 봐야하면 부모인 Item만 보면 된다.
4. 저장공간 효율화

 

-> 단점

1. 조인시 조인 쿼리가 복잡함
2. insert 쿼리 두 번

하지만 단점은 그렇게 크게 성능이 떨어지지 않습니다. 따라서 조인 전략을 무조건 사용해야 합니다.

 

=> 단일 테이블 전략

1. null을 모두 허용해야 하는 게 치명적인 단점이다.
2. 쿼리와 구조가 단순함
3. 단일 테이블에 모든 것을 적용해서 성능이 느려질 수도 있다. (이런 경우는 거의 없음)

 

=> per table 전략

1. 사용하면 안되는 구조
2. 뭘 묶는 것도 없고 다 따로 노는 느낌(정산 한번 하려면 앨범, 무비, 책을 다 따로 봐야함

 

- Mapped Superclass

name이라는 필드가 계속 나옵니다. 그래서 이걸 부모 클래스에 두고 공통 속성만 상속해서 쓰고 싶습니다. ex) BaseEntity의 생성일, 수정일이 되겠습니다.

 

근데 DB는 완전히 다르게 있습니다. 회원, 셀러에 id, name이 다 따로 있습니다. 부모, 자식 관계가 아닙니다. 이렇게 공통 매핑 정보가 필요할 때(name 같은) 사용합니다.

 

-> 예시 

우리 어플에는 모든 곳에 누가 등록했고 몇시에 등록했는지 모든 테이블에 항상 있어야 한다고 DBA가 정했습니다.

 

private String createBy;
private LocalDateTime createDate;

그러면 모든 클래스에 createBy(누가 생성했어), createDate(누가 수정했어) 필드를 전부 만들어야 합니다.

 

-> mapped superclass 적용

@MappedSuperclass
public abstract class BaseEntity {
    private String createBy;
    private LocalDateTime createDate;
}

@Entity
public class Team extends BaseEntity{

BaseEntity를 추상으로 만들고 공통 속성을 넣습니다. 그리고 모든 객체는 Base를 extends 합니다. 이제 객체에서는 상속받은 걸 가져다 쓰면 됩니다. 

 

-> 실행

상속 받은 테이블에 보면 createBy, createDate 속성이 들어옵니다. 정리해보자면 mapped super class는 상속 관계가 전혀 아니고 그냥 공통 속성 쓰고 싶을 때 하는 것입니다. BaseEntity로 생기는 테이블은 없습니다. JPA에서 extends를 하려면 mapped super class나 entity가 무조건 있어야 합니다.

 

- 상속 관계 매핑 실전 예제

=> 요구사항 추가

상품의 종류는 음반, 도서, 영화가 있고 이후 더 확장될 수 있다.
모든 데이터는 등록일과 수정일이 필수다.

 

-> 설계

테이블은 싱글 테이블로 했습니다. 

 

-> 상품

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

@Entity
public class Book extends Item{

상품은 extends로 상속 관계를 만들고 single table 전략으로 DTYPE을 넣어서 만듭니다. 이렇게 하면 끝납니다.

 

try {
    Book book = new Book();
    book.setName("JPA");

    em.persist(book);
    tx.commit();

사용할 때는 Item이 아닌 자식 속성으로 저장해야 합니다.

 

DB에 보면 데이터가 들어가고 insert 문에서 dtype은 자동으로 들어갑니다.

 

+ 전략을 조인으로

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

조인으로 바꾸고 싶으면 조인으로만 바꾸면 알아서 DB가 생성이 됩니다.

 

-> BaseEntity

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

BaseEntity를 만들고 모든 테이블에 다 extends하면 되는데 Item에는 Item에만 넣고, Item을 상속받은 책이나 무비에는 안 넣어도 됩니다. 싱글 테이블 전략이라면 item에 공통 속성이 들어가고 조인이어도 item에 들어가는데 사용할 때는 자식을 find해서 조인해서 가져올 것입니다.

 

-> 실무에서 상속을 쓰나요? ✅

장, 단을 고민해 봐야합니다. 노가다를 하더라도 어플이 커지고 데이터가 억단위 넘어가면 테이블을 단순하게 유지해야 해서 상속을 사용하지 않지만 단순 프로젝트는 사용합니다. 처음부터 모든 어플이 다 큰 것이 아니므로 상속을 쓰다가 나중에 분리하면 됩니다.

Comments