본문 바로가기
BackEnd/JPA

[JPA] 고급 매핑

by 개발 Blog 2024. 7. 15.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

상속 관계 매핑

 

- 관계형 데이터베이스는 상속 관계가 없다.

- 하지만 슈퍼타입, 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.

- 상속관계 매핑 : 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑한다.

 

슈퍼타입 서브타입 논리모델을 실제 물리 모델로 구현하는 방법은 크게 3가지가 있다.

1. 조인전략(각각 테이블로 변환)

Item

@Entity
public class Item {

    @Id @GeneratedValue
    private Long id;


    private String name;
    private int price;

}

 

Album, Movie, Book

@Entity
public class Album extends Item{
    private String artist;
}
================================

@Entity
public class Movie extends Item {

    private String director;
    private String actor;
}
================================

@Entity
public class Book extends Item{
    private String author;
    private String isbn;

}

- JPA의 기본 전략은 한 테이블에 다 만들어지도록 되어있다.

 

부모 테이블에서 @Inheritance로 전략을 선택할 수 있다.

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

    @Id @GeneratedValue
    private Long id;


    private String name;
    private int price;

}

 

- 조인 방식으로 테이블이 만들어진 것을 확인할 수 있다.

 

이제 데이터를 넣어보자.

public class JpaMain {
	//생략
    
    Movie movie = new Movie();
    movie.setDirector("AAA");
    movie.setActor("BBB");
    movie.setName("바람과 함께 사라지다.");
    movie.setPrice(10000);

    em.persist(movie);
}

- ID는 같은 값이다. ITEM에서 PK가 MOVIE 테이블에서 PK이면서 FK이다.

 

쿼리 실행 과정을 확인하기 위해 flush()와 clear()를 사용하여 1차 캐시를 비운 후, 다시 findMovie를 호출해 보자.

Movie movie = new Movie();
movie.setDirector("AAA");
movie.setActor("BBB");
movie.setName("바람과 함께 사라지다.");
movie.setPrice(10000);

em.persist(movie);

em.flush();
em.clear();

Movie findMovie = em.find(Movie.class, movie.getId());
System.out.println("findMovie = " + findMovie);

JPA가 상속관계인 경우나 조인이 필요할 때는 조인을 해서 값을 가져오는 것을 확인할 수 있다.

 

ITEM 테이블에 Dtype을 빼고 생성했는데 이번에는 넣어서 생성해 보자.

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

@DiscriminatorColumn를 적어주면 Dtype이 생기고 엔티티명이 입력된다.

@DiscriminatorColumn(name = "")으로 원하는 이름으로 변경도 가능하다.

 

자식 엔티티의 이름을 변경하고 싶으면 각각의 엔티티에 @DiscriminatorValue("")으로 이름을 변경할 수 있다.

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

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {}

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

Album은 A, Movie는 M, Book은 B라고 지정하면 위와 같이 지정한 이름으로 값이 입력된다.

 

이는 DB에서 조회했을 때 어떤 자식의 데이터인지 구분하기 용이하다.

 

장점  

- 테이블 정규화가 되어있다.

- 외래 키 참조 무결성 제약조건 활용가능하다.

- 저장공간이 효율적이다.

 

단점 

- 조회 시 조인을 많이 사용해서 성능 저하가 있다.

- 조회 쿼리가 복잡하다.

- 데이터 저장할 때 INSERT SQL을 2번 호출한다.

 

2. 단일 테이블 전략(통합 테이블로 변환)

DB를 설계했는데 테이블을 싱글로 하는 게 더 유리한 경우에 사용한다.

한 테이블에 모든 컬럼을 입력하고 DTYPE으로 구분한다.

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

전략을 SINGLE_TABLE로 변경하면 된다. 

- INSERT도 한 번에 하고 조인할 필요도 없기 때문에 성능상 이점이 있다.

- @DiscriminatorValue을 생략해도 자동으로 생성된다.

 

조인테이블과 싱글테이블로 DB 설계를 바꿨는데 코드는 바뀐 게 없고 애노테이션의 전략부분만 수정되었다.

이것이 JPA의 장점 중 하나이다. (쿼리를 수정할 필요가 없다.)

 

장점

- 조인이 필요가 없으므로 일반적으로 조회 성능이 빠르다.

- 조회 쿼리가 단순하다.

단점

- 자식 엔티티가 매핑한 컬럼은 모두 NULL을 허용한다.

- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 

- 상황에 따라서 조회 성능이 오히려 느려질 수 있다.

 

 

3. 구현 클래스마다 테이블 전략(서브타입 테이블로 변환) - 쓰면 안 되는 전략

부모 테이블을 생성하지 않고 자식테이블에서 모두 관리하는 전략이다.

전략은 TABLE_PER_CLASS로 수정한다.

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

- 부모 테이블(ITEM)에 있던 PRICE와 NAME을 자식 테이블인 MOVIE에서 관리한다.

 

이 전략은 데이터를 삽입할 때는 괜찮은데 조회할 때는 좋지 않다.

만약 ITEM을 조회한다고 하면 3개의 자식 테이블을 모두 UNION ALL로 조회한다. 

 

장점

- 서브 타입을 명확하게 구분해서 처리할 때 효과적이다.

- NOT NULL 제약조건을 사용할 수 있다.

단점

- 여러 자식 테이블을 함께 조회할 때 성능이 느리다(UNION SQL 필요)

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

 

기본적으로는 조인 전략을 쓰고 테이블을 설계했는데 단순할 때, 단일 테이블 전략을 쓰자.

 

Mapped Superclass - 매핑 정보 상속

등록일자, 수정일자, 등록자, 수정자 등 모든 테이블에 필요한 컬럼이 중복으로 있을 때 사용한다.

 

공통된 속성을 BaseEntity로 만들고 @MappedSuperclass로 자식들에게 매핑 정보만 제공할 수 있다.

@MappedSuperclass
public class BaseEntity {
    private String createdBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
}
@Entity
public class Team extends BaseEntity{}

=========================================

@Entity
public class Member extends BaseEntity{}
public class JpaMain {
    public static void main(String[] args) {
			//생략
       
        try {
            Member member = new Member();
            member.setUsername("user1");
            member.setCreatedBy("kim");
            member.setCreatedDate(LocalDateTime.now());

            em.persist(member);

            em.flush();
            em.clear();

            tx.commit();
    }
}

- 속성 정보만 상속받아서 테이블 생성을 한다.

- BaseEntity는 엔티티가 아니다. 오로지 속성만 제공한다.

 

정리

- @MappedSuperclass는 상속관계 매핑을 지원하지 않는다.
- 이 어노테이션이 붙은 클래스는 엔티티도 아니고 테이블과 직접 매핑되지도 않는다.
- 단지, 부모 클래스를 상속받는 자식 클래스에 매핑 정보를 제공할 뿐이다.
- 따라서 @MappedSuperclass로 지정된 클래스는 직접 조회하거나 검색할 수 없다 (em.find(BaseEntity) 불가).
- 직접 생성해서 사용할 일이 없으므로 추상 클래스로 만드는 것이 좋다.
- 이 클래스는 테이블과 관계없이 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 한다.
- 주로 등록일, 수정일, 등록자, 수정자 같은 공통 정보를 모을 때 사용된다.
- JPA에서 extends로 상속받을 때는 @Entity나 @MappedSuperclass로 지정한 클래스만 상속할 수 있다.

 

실전 예제 4 - 상속관계 매핑

 

요구사항 추가

- 상품의 종류는 음반, 도서, 영화가 있고 이후 더 확장될 수 있다.

- 모든 데이터는 등록일과 수정일이 필수다.

 

도메인 모델

 

- 기존 도메인에 Album, Book, Movie를 추가하여 상속관계를 만든다.

- 테이블은 싱글 테이블로 설계한다.

 

Item

package jpabook.jpashop.domain;

import jakarta.persistence.*;

import java.util.ArrayList;
import java.util.List;

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

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

    private String name;
    private int price;
    private int stockQuantity;

    @ManyToMany(mappedBy = "items")
    private List<Category> categoryList = new ArrayList<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public int getStockQuantity() {
        return stockQuantity;
    }

    public void setStockQuantity(int stockQuantity) {
        this.stockQuantity = stockQuantity;
    }
}

 

Album, Book, Movie

@Entity
public class Album extends Item{

    private String artist;
    private String etc;
}
=====================================
@Entity
public class Book extends Item{
 	private String author;
    private String isbn;
}
=====================================
@Entity
public class Movie extends Item{

    private String director;
    private String actor;
}

- 싱글 테이블로 모든 속성이 포함되어 생성된다.

 

Book으로 데이터를 저장해 보자.

public class JpaMain {
    public static void main(String[] args) {
       //생략
        try {

            Book book = new Book();
            book.setName("JPA");
            book.setAuthor("김영한");

            em.persist(book);

            tx.commit();
            
    }
}

 

다음으로는 등록일과 수정일을 공통으로 매핑시켜 줄 BaseEntity를 만든다.

@MappedSuperclass
public abstract class BaseEntity {
    private String createdBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;

    public String getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }

    public LocalDateTime getCreatedDate() {
        return createdDate;
    }

    public void setCreatedDate(LocalDateTime createdDate) {
        this.createdDate = createdDate;
    }

    public String getLastModifiedBy() {
        return lastModifiedBy;
    }

    public void setLastModifiedBy(String lastModifiedBy) {
        this.lastModifiedBy = lastModifiedBy;
    }

    public LocalDateTime getLastModifiedDate() {
        return lastModifiedDate;
    }

    public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }
}

기존에 만들었던 엔티티에 extends BaseEntity로 상속시킨다.

(Album은 Item을 상속받고 Item은 BaseEntity를 상속받으니 안 해도 된다.)

모든 테이블에 등록일, 등록자, 수정일, 수정자가 생성된다.

싱글 테이블이기 때문에 ITEM에 모든 컬럼이 포함되어 있다. 따로 만들고 싶으면 ITEM의 @Inheritance 전략을 JOINED로 바꾸면 된다.

 

바꿔보자

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

 

자식 테이블이 다 따로 생성되는 것을 확인할 수 있다.

'BackEnd > JPA' 카테고리의 다른 글

[JPA] 영속성 전이와 고아 객체  (0) 2024.07.16
[JPA] 프록시와 연관관계 관리  (0) 2024.07.16
[JPA] 다양한 연관관계 매핑  (0) 2024.07.14
[JPA] 연관관계 매핑 기초  (2) 2024.07.14
[JPA] 엔티티 매핑  (0) 2024.07.12