관리 메뉴

나구리의 개발공부기록

고급 매핑, 상속관계 매핑, Mapped Superclass - 매핑 정보 상속, 실전 예제 - 상속관계 매핑 본문

인프런 - 스프링부트와 JPA실무 로드맵/자바 ORM 표준 JPA 프로그래밍 - 기본편

고급 매핑, 상속관계 매핑, Mapped Superclass - 매핑 정보 상속, 실전 예제 - 상속관계 매핑

소소한나구리 2024. 10. 11. 11:31

출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍 - 기본편(유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


** 실전예제 전까지는 DB URL 설정을 test로 설정하여 실습

<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>

 

** 실전예제는 jdbc.url 설정을 jpashop으로 변경해서 진행

<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/jpashop"/>

1. 상속관계 매핑

1) 관계형 DB의 상속관계

  • 관계형 데이터베이스는 상속관계가 없음
  • 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사함
  • 객체의 상속 구조와 슈퍼타입 서브타입 관계를 매핑함

(1) 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법

  • 각각의 정규화가 잘 된 테이블로 변환 -> 조인 전략
  • 통합 테이블로 변환 -> 단일 테이블 전략
  • 서브타입 테이블로 변환 -> 구현 클래스마다 테이블 전략

관계도 - 좌) 조인 전략 / 중) 단일 테이블 전략 / 우) 구현 클래스 마다 테이블 전략

2) 주요 애노테이션

(1) @Inheritance(strategy=InheritanceType.XXX)

  • JOINED: 조인전략
  • SINGLE_TABLE: 단일 테이블 전략 (기본값)
  • TABLE_PER_CLASS: 구현 클래스마다 테이블 전략

(2) @DiscriminatorColumn(name="DTYPE")

  • 상속관계 중 조상 엔터티에 적용
  • 조상 테이블에 입력된 값이 어떤 자손 테이블의 값인지 구분하는 컬럼
  • name 속성으로 컬럼명을 변경할 수 있음
  • 항상 적용하는 것을 권장함

(3) @DiscriminatorValue("XXX")

  • 상속관계 중 자손 엔터티에 적용
  • DTYPE컬럼에 매핑되는 기본값은 자손타입의 엔터티명이 필드 값으로 입력됨
  • 그 값에 입력되는값을 원하는 대로 변경할 수 있음

3) 코드 및 실행 결과

(1) Item 생성

  • Item 클래스는 단독으로 사용할 일이 없으므로 추상클래스로 생성
package jpabook.jpashoptest.domain;

@Entity
public abstract class Item {

    @Id @GeneratedValue
    private Long id;
    private String name;
    private int price;
    // ... getter, setter 생략
}

 

(2) Album 생성

package jpabook.jpashoptest.domain;

@Entity
public class Album extends Item {
    private String artist;
    // ... getter, setter 생략
}

 

(3) Movie 생성

package jpabook.jpashoptest.domain;

@Entity
public class Movie extends Item {
    private String director;
    private String actor;
    // ... getter, setter 생략
}

 

(4) Book 생성

package jpabook.jpashoptest.domain;

@Entity
public class Book extends Item {
    private String author;
    private String isbn;
    // ... getter, setter 생략
}

 

(5) @Inheritance 적용 - SINGLE_TABLE

  • Item 테이블에 @Inheritance를 적용, 기본값이 SINGLE_TABLE 전략임
  • Item 엔터티의 자손 엔터티들이 ITEM 테이블에 모두 통합되어 생성됨
  • 한 테이블에 통합되어 있어 조인이 필요없어서 성능이 좋음
  • 애플리케이션이 매우 단순할 때 적용
@Entity
@Inheritance
public abstract class Item {
   // ... 코드 생략
}

// @Inheritance(strategy = InheritanceType.SINGLE_TABLE)

 

(6) @Inheritance 적용 - JOINED

  • Item 테이블에 @Inheritance를 적용할 때 strategy 속성을 JOINED로 설정하면 엔터티의 상속관계가 각각의 정규화된 테이블로 생성됨
  • JPA와 가장 유사한 모델링이며 조회 시 조인을 사용해야 함
  • 테이블이 정규화 되어있고 외래키 참조 무결성 제약조건을 활용할 수 있음
  • 복잡한 비즈니스 모델에서 적용
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Item {
    // ... 코드 생략
}

각각의 정규화된 테이블로 생성된 모습

(7) @Inheritance 적용 - TABLE_PER_CLASS

  • 상속관계 중 자손 엔터티에 조상클래스의 속성을 각각 적용시켜서 자손클래스의 엔터티만 테이블이 생성됨
  • Item 클래스가 없으므로 DTYPE을 생성하는 애노테이션을 사용할 필요가 없음
  • 값을 입력할 때는 쿼리도 한 번으로 입력할 수 있고 각 테이블의 값을 조회할 때에도 깔끔하게 조회할 수 있음
  • 그러나 부모타입으로 조회할 때가 문제가 발생하는데 부모타입인 Item으로 값을 조회하게되면 상속관계의 모든 테이블을 union으로 전부 조회하는 굉장히 비효율적으로 동작할 수 밖에 없어 성능문제가 발생함
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
    // ... 코드 생략
}

ITEM 테이블은 없이 자손엔터티들만 테이블로 생성됨

 

(8) @DiscriminatorColumn, @DiscriminatorValue 적용

  • Item 엔터티에 @DiscriminatorColumn 애노테이션을 적용하여 어떤 자손테이블의 값인지 구분할 수 있는 컬럼을 생성
  • 컬럼명의 기본값은 DTYPE이며 name 설정으로 원하는 컬럼명으로 변경할 수 있음
  • Book, Album, Movie에 값을 하나씩 입력해서 테이블을 확인해보면 Item 테이블에 DTYPE이라는 컬럼 생겨서 각각의 엔터티 명이 값으로 매핑되어 적용됨
  • 자손 엔터티들에 @DiscriminatorValue를 적용하여 DTYPE 컬럼에 기본값으로 입력되는 것을 원하는 값으로 변경할 수 있음

JOINED적용 ITEM 테이블 - 좌) @DiscriminatorColumn 적용(기본값) / 우) DiscriminatorValue 적용(엔터티 명에 K_ 를 붙임)

 

  • SINGLE_TABLE 전략으로 적용 시 ITEM 테이블에 자손 엔터티의 속성을 입력하여 null값이 발생함
  • SINGLE_TABLE 전략은 한 테이블에 전부 자손 객체의 값이 들어오다보니 구분할 방법이 없기 때문에 @DiscriminatorColumn 애노테이션을 적용하지 않아도 DTYPE이 자동으로 생성됨

SINGLE_TABLE 적용시 ITEM 테이블

4) 전략별 장단점

(1) JOINED

장점 테이블이 정규화 되어있음
외래키 참조 무결성 제약 조건을 활용 가능함
저장공간의 효율성이 좋음

조인은 잘 사용하면 성능저하가 거의 없고 저장공간이 효율화 되기때문에 오히려 성능이 증가할 수 있음
단점 조회할 때 조인 사용이 많아져 쿼리가 복잡해짐
데이터 저장 시 insert 쿼리가 2번 호출 됨

SINGLE_TABLE전략에 비해 성능저하가 있을 뿐이지 그렇게 성능저하의 영향이 크지 않음

 

(2) SINGLE_TABLE

장점 조인이 필요 없어서 일반적으로 조회 성능이 빠름
조회 쿼리가 매우 단순함
단점 자식 엔터티가 매핑한 컬럼은 모두 null을 허용해야함
단일 테이블에 모든 것을 저장하므로 테이블이 커지기 때문에 상황에 따라 조회 성능이 오히려 느려질 수 있음

 

(3) TABLE_PER_CLASS - 사용하지 말 것

  • 데이터베이스 설계자, ORM 전문가 둘 다 추천하지 않는 전략
장점 서브 타입을 명확하게 구분해서 처리할 때 효과적
not null 제약조건을 사용할 수 있음
단점 여러 자식 테이블을 함께 조회할 때 UNION SQL을 사용하기 때문에 성능이 느림(테이블 전체를 조회함)
자식 테이블을 통합해서 쿼리하기가 어려움
시스템의 변경이 일어나면 수정해야할 부분이 많아짐

 

** 결론적으로 JOINED, SINGLE_TABLE 전략 중에서 고민해야 하며 정말 단순한 경우에는 SINGLE_TABLE을 적용


2. @MappedSuperclass 

1) 매핑 정보 상속 (상속관계 매핑이 아님 - 헷갈림 주의)

  • 공통 매핑 정보가 필요할 때 사용함
  • 즉, 상속관련 매핑과 관련이 있다기보다는 여러 테이블에서 공통적인 속성을 많이 사용할 때 객체입장에서 속성만 상속받은 것이라고 보면됨
  • 객체 구성만 별도의 공통 속성이 존재하는 객체가 있는것이지 테이블은 상속받은 공통 속성과 본인의 속성으로 구성된 각각의 테이블이 생성됨

2) 예제 코드

  • 애플리케이션의 모든 곳에 CRUD 정보가 항상 있어야한다는 룰이 추가되었다고 가정
  • 기존의 코드는 유지한채로 위의 상황을 적용

(1) 단순한 적용 방법

  • 1차원 적으로 생각해보면 정보를 담는 변수들이 모든 엔터티에 삽입하면 됨
  • 그러나 이러한 방식은 개발자 입장에서는 조금 하드코딩 방식이고, 엔터티가 무수히 많으면 작업도 많아짐
private String createBy;                // 작성자
private LocalDateTime createDate;       // 작성일
private String lastModifiedBy;          // 마지막 수정자
private LocalDateTime lastModifiedDate; // 마지막 수정일

 

(2) MappedSuperclass 적용

  • 새로운 클래스를 작성하여 @MappedSuperclass라는 애노테이션을 적용하고 공통적으로 적용할 필드를 입력 후 게터,세터를 생성
  • 공통 정보를 매핑할 클래스들에 extends로 생성한 클래스를 상속시키면 공통정보를 사용할 수 있음

** 참고

  • 실무에서 이런 운영정보는 진짜 변경이 안되는 테이블을 제외하고는 항상 기본적으로 깔려있어야 함
  • JPA는 이러한 기본정보들은 이벤트라는 기능을 통해서 이미 자동화 할 수 있고, 스프링 데이터 JPA함께 사용하면 애노테이션을 사용하여 심플하게 적용할 수 있음
@MappedSuperclass
public abstract class BaseEntity {

    private String createBy;                // 작성자
    private LocalDateTime createDate;       // 작성일
    private String lastModifiedBy;          // 마지막 수정자
    private LocalDateTime lastModifiedDate; // 마지막 수정일
    // getter, setter   
}

// Member 클래스에 공통으로 사용해야할 필드가 정의된 BaseEntity를 상속시킴
@Entity
public class Member extends BaseEntity {
    // 코드 생략
}

3) 정리

  • 상속관계 매핑이 아니고, 엔터티도 아니여서 테이블과 매핑이 되지도 않음(테이블이 생성되지 않음)
  • 부모 클래스를 상속 받은 자식 클래스에 매핑 정보만 제공
  • @MappedSuperclass는 조회, 검색이 불가능함 즉, em.find(BaseEntity)가 불가능
  • 직접 생성해서 사용할 일이 없으므로 추상 클래스로 생성하는 것을 권장함
  • 테이블과 관계 없고 단순히 엔터티가 공통으로 사용하는 매핑 정보를 모으는 역할을 하며 주로 등록일, 수정일, 등록인, 수정인과 같이 전체 엔터티에서 공통적으로 정보를 적용해야할 때 사용하며 실무에서도 자주 사용함

** 참고

  • 엔터티(@Entity가 적용된 클래스)는 @Entity나 @MappedSuperclass가 적용된 클래스끼리만 상속이 가능함

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

1) 요구사항 추가

  • 상품의 종류는 음반, 도서, 영화가 있고 이후 더 확장될 수 있음 - 상속관계매핑 적용
  • 모든 데이터는 등록일과 수정일이 필수임 - @MappedSuperclass 적용

(1) 도메인 모델

  • Item과 Album, Book, Movie의 엔터티들을 상속관계로 적용

좌) 기본 도메인 모델 / 우) 상세 도메인 모델

 

(2) 테이블 설계

  • 상속관계 매핑의 테이블 전략은 애플리케이션이 복잡하지 않으므로 싱글테이블 전략으로 설정

테이블 설계

1) 상속관계 매핑 적용

(1) Book, Movie, Album 생성

  • Item 엔터티를 상속받은 Book, Movie, Album 클래스를 @Entity로 설정
@Entity
public class Book extends Item {
    private String author;
    private String isbn;
    // ...getter, setter 생략
}

@Entity
public class Album extends Item{
    private String artist;
    private String etc;
    // ...getter, setter 생략
}

@Entity
public class Movie extends Item {
    private String director;
    private String actor;
    // ...getter, setter 생략
}

 

(2) Item 수정

  • Item 클래스는 단독으로 사용하지 않으므로 abstract를 붙혀서 추상클래스로 변환
  • @Inheritance 애노테이션을 적용하여 상속관계 매핑
  • 요구사항이 싱글테이블 전략인데, 기본값이 싱글테이블 전략이므로 따로 전략설정을 하지않아도 적용되며 싱글테이블전략은 DTYPE도 자동으로 생성되기 때문에 @DiscriminatorColumn 애노테이션을 적용하지 않아도 됨
@Entity
@Inheritance // 기본값이 싱글테이블 전략, 싱글테이블 전략은 DTYPE 속성이 자동으로 생성됨
public abstract class Item {
    // 기존 코드 생략
}

 

** @MappedSuperclass 실습 생략

  • @MappedSuperclass는 위에서 적용한것과 다른것이 없어 실습을 생략
  • 각 엔터티에 적용할 공통 필드가 선언된 클래스를 하나 선언하여 @MappedSuperclass 애노테이션을 적용시키고, 적용하고싶은 모든 엔터티에 생성한 클래스를 상속 시키면, 해당클래스에 작성된 필드들이 모든 테이블에 들어가있음

 

** 참고

  • 실무에서 상속관계를 적용하는 것은 장단점이 있으므로 트레이드오프가 필요함
  • 애플리케이션이 작은경우는 상속관계가 대부분 잘 동작하지만 애플리케이션이 매우 커지고 사용자가 많아지면 정상적으로 동작하지 않는 경우가 발생하거나 너무 구조가 복잡해짐
  • 애플리케이션이 커지는 경우에는 테이블을 최대한 단순하게 유지해야 기에 JSON으로 데이터를 묶어버리는 경우도 있음
  • 초기 설계에서는 사용량이 많지 않으므로 객체지향적으로 설계한 후 절충을 해야하는 시점에 시스템을 보완하는 식으로 하는 것을 권장함