mojo's Blog

고급 매핑 본문

JPA

고급 매핑

_mojo_ 2022. 8. 12. 16:48

 

상속관계 매핑

 

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

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

상속관계 매핑이란 객체의 상속과 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

 

 

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

 

  1. 각각 테이블로 변환 : 조인 전략
  2. 통합 테이블로 변환 : 단일 테이블 전략
  3. 서브타입 테이블로 변환 : 구현 클래스마다 테이블 전략

 

※ 주요 애너테이션

 

@Inheritance(strategy = InheritanceType.XXX)

   - JOINED : 조인 전략

   - SINGLE_TABLE : 단일 테이블 전략

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

@DiscriminatorColumn(name = "DTYPE")

@DiscriminatorValue("XXX")

 

 

① 조인 전략

 

 

Item 클래스

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

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

    private String name;
    private int price;
    
    ...
}

 

@Inheritance(strategy = InteritanceType.JOINED) 을 통해 조인이 가능하다.

그리고 Item 에 DTYPE 칼럼을 추가하고 싶다면 @DiscriminatorColumn 을 추가하자.

 

Item 을 상속하는 클래스들

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

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

@Entity
public class Movie extends Item  {
    private String director;
    private String actor;
}

 

여기서 id 필드는 굳이 만들지 않아도 Item 을 상속한다는 이유로 알아서 생성이 된다!

 

@Entity
@DiscriminatorValue("A")
public class Album extends Item {
    private String artist;
}

@Entity
@DiscriminatorValue("B")
public class Book extends Item {
    private String author;
    private String isbn;
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item  {
    private String director;
    private String actor;
}

 

추가로 자식 클래스에서 @DiscriminatorValue 를 추가하게 될 경우 테이블 명이 지정된 이름으로 변경된다.

따라서 DTYPE 에서 "Movie" 가 "M" 으로 대체되게 된다.

 

이번엔 movie 를 Item 에 추가하는 코드를 작성해보자. 

public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();

    tx.begin();

    try {
        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);

        tx.commit();
    } catch(Exception e) {
        tx.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

 

위 코드를 실행하면 Item 을 insert 한 다음에 Movie 를 insert 하는 것을 볼 수 있다. (2번 insert)

그리고 영속성 컨텍스트에 있는걸 초기화 한 후에 select 를 하게 될 경우 자연스럽게 join 을 하는 것을 볼 수 있다.

테이블 또한 잘 생성된 것을 확인할 수 있다.

 

조인 장점

 

- 테이블을 정규화 한다.

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

- 저장공간을 효율화 한다.

 

조인 단점

 

- 조회시 조인을 많이 사용하게 되므로 성능이 저하된다.

- 조회 쿼리가 복잡하다.

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

 

 

② 단일 테이블 전략

 

 

Item 클래스

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

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

    private String name;
    private int price;
    
    ...
}

 

조인 전략에서의 Item 클래스에서 딱 하나만 변경하면 된다.

@Inheritance(strategy = InheritanceType.SINGLE_TABLE) 에서 전략만 변경하면 끝이다!

그리고 @DiscriminatorColumn 이 없어도 단일 테이블이 정상적으로 만들어진다.

 

Item 클래스를 상속하는 클래스들

@Entity
@DiscriminatorValue("A")
public class Album extends Item {
    private String artist;
}

@Entity
@DiscriminatorValue("B")
public class Book extends Item {
    private String author;
    private String isbn;
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item  {
    private String director;
    private String actor;
}

 

조인 전략과 다를게 없다.

 

위와 동일하게 movie 를 Item 에 추가하는 코드를 수행하면 다음과 같다. 

 

Item 테이블에 대한 Insert 쿼리가 딱 하나만 수행이 되었다.

즉, Album, Movie, Book 에 대한 테이블은 조인 전략과 다르게 생성되지 않고

딱 하나의 테이블만 생성된 것을 알 수 있다.

select 쿼리에서도 join 이 없어진 것을 확인할 수 있다.

 

단일 테이블 장점

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

- 조회 쿼리가 단순하다.

 

단일 테이블 단점

- 자식 엔티티가 매핑한 컬럼은 모두 null 이 허용된다는 점이다.

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

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

 

상속 클래스가 적을 경우에는 단일 테이블 전략이 적합한 것 같다.

그러나 상속 클래스가 많아질 경우에는 예를 들어 상속 클래스의 개수가 N 개일 경우,

(N - 1) 개의 자식 엔티티가 매핑한 칼럼 모두가 null 이 들어가게 되면서 공간적으로 비효율적이다.

따라서 상속 클래스가 많아질 경우에는 조인 전략으로 가는 것이 적합한 것 같다.

 

 

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

 

 

Item 클래스

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

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

    private String name;
    private int price;
    
    ...
}

 

이번엔 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 으로 지정하여

구현 클래스마다 테이블이 생성될 수 있도록 변경하였다.

그리고 추상 클래스로 지정함으로써 Item 테이블이 생성이 되지 않도록 한다.

 

 

Item 을 상속하는 클래스들

@Entity
@DiscriminatorValue("A")
public class Album extends Item {
    private String artist;
}

@Entity
@DiscriminatorValue("B")
public class Book extends Item {
    private String author;
    private String isbn;
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item  {
    private String director;
    private String actor;
}

 

movie 를 Item 에 추가하는 코드를 수행하면 다음과 같다.

 

Item 테이블이 생성되지 않은 것을 볼 수 있다.

그리고 Movie 테이블에 insert 쿼리가 하나 수행된 것을 볼 수 있고,

select 쿼리에서 join 을 하지 않은 것을 볼 수 있다.

 

조회할 때, 아래와 같이 Item 객체로 가져와보도록 변경한 후 수행해보자.

public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();

    tx.begin();

    try {
        Movie movie = new Movie();
        movie.setDirector("aaa");
        movie.setActor("bbb");
        movie.setName("바람과함꼐사라지다");
        movie.setPrice(10000);

        em.persist(movie);

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

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

        tx.commit();
    } catch(Exception e) {
        tx.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

 

Item 객체로 가져올 때는 union 으로 상속된 모든 테이블들을 찾아봐야 한다.

즉, 상속된 테이블이 많아진다면 select 쿼리 하나로 성능 자체가 저하된다는 문제가 있다.

따라서 이러한 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천하지 않는 방식이라고 한다.

 

구현 클래스마다 테이블 장점 

- 서브 타입을 명확하게 구분해서 처리할 때 효과적이다. (ex : Album, Movie)

- not null 제약조건 사용이 가능하다.

 

구현 클래스마다 테이블 단점

- 여러 자식 테이블을 함께 조회할 때 성능이 느려진다. (UNION SQL 필요)- 자식 테이블을 통합해서 쿼리하기가 어렵다.

 

 

@MappedSuperclass

 

 

공통 매핑 정보가 필요할 때 사용한다. (id, name 이 중복해서 있는 경우)

 

예를 들어 Member, Team 에 공통으로 들어가야 하는 칼럼이 다음과 같다.

createdBy : 누가 만들었는지
createdDate : 언제 만들었는지
lastModifiedBy : 마지막으로 누가 수정했는지
lastModifiedDate : 마지막으로 언제 수정했는지

 

위와 같은 공통 칼럼을 Member, Team 에 넣기에는 굉장히 번거로운 일이다.

따라서 공통 칼럼을 담을 수 있는 클래스를 생성하고 Member, Team 은 해당 클래스를 상속하면 

클래스마다 추가하지 않고도 해결이 가능하다.

 

BaseEntity 클래스

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

 

@MappedSuperClass 를 붙여서 상속하는 클래스들이 해당 필드가 생성될 수 있도록 해준다.

 

Member 및 Team 클래스

@Entity
public class Member extends BaseEntity {
	...
}

@Entity
public class Team extends BaseEntity {
	...
}

 

BaseEntity 를 상속하게 된다면 BaseEntity 의 4가지 필드가 알아서 Member, Team 에 생성이 된다.

 

 

※ @MappedSuperclass

 

상속관계 매핑을 하지 않고 엔티티가 생성되지 않으며 테이블과 매핑을 하지 않는다.

즉, 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공하는 역할을 한다.

조회, 검색이 불가하며 직접 생성해서 사용할 일이 없으므로 추상 클래스를 사용하는 것을 권장한다.

테이블과 관계가 없으며, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 한다.

주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.

참고로 @Entity 클래스나 엔티티나 @MappedSuperclass 로 지정한 클래스만 상속이 가능하다.

 

 

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

 

 

※ 요구사항 추가

 

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

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

 

도메인 모델

 

 

테이블 설계

 

 

전략은 싱글 테이블 전략으로 설정하였다.

그 이유는 3 개의 상속 테이블만 존재하므로 테이블이 별로 없다는 점에서 선택하였다.

 

Item 클래스

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

 

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 static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();

    tx.begin();

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

        em.persist(book);

        tx.commit();
    } catch(Exception e) {
        tx.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

 

이번엔 조인 전략으로 Album, Book, Member 테이블을 생성하여 따로 관리한다.

그리고 Item 에 Album, Book, Member 인지를 알 수 있는 DTYPE 칼럼을 추가한다.

 

Item 클래스

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

 

 

그리고 모든 테이블에 공통으로 들어가야 하는 칼럼은 다음과 같다.

createdBy : 누가 만들었는지
createdDate : 언제 만들었는지
lastModifiedBy : 마지막으로 누가 수정했는지
lastModifiedDate : 마지막으로 언제 수정했는지

 

BaseEntity 클래스를 생성하여 위 정보들을 담고 나머지 클래스들은 BaseEntity 를 상속하면 된다.

 

BaseEntity 클래스

@MappedSuperclass
public abstract class BaseEntity {

    private String createdBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
    
    ...
}

 

나머지 클래스

@Entity
public class Category extends BaseEntity { ... }

@Entity
public class Delivery extends BaseEntity { ... }

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

@Entity
public class Member extends BaseEntity{ ... }

@Entity
@Table(name = "ORDERS")
public class Order extends BaseEntity { ... }

@Entity
public class OrderItem extends BaseEntity { ... }

 

'JPA' 카테고리의 다른 글

값 타입  (0) 2022.08.16
프록시와 연관관계 관리  (0) 2022.08.12
다양한 연관관계 매핑  (0) 2022.08.10
연관관계 매핑 기초  (0) 2022.08.10
엔티티 매핑  (0) 2022.08.09
Comments