본문 바로가기
Framework and Tool/JPA

JPA - 고급맵핑 - 슈퍼타입과 서브타입

by ocwokocw 2021. 7. 10.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- 상속 관계 맵핑

관계형 DB(RDB) 는 상속이라는 개념이 없다. 대신 슈퍼타입과 서브타입이라는 유사한 기법이 존재한다. 그래서 ORM 의 상속관계맵핑이라고 하면 객체의 상속과 슈퍼타입 서브타입의 관계를 맵핑하는것을 말한다. 맵핑방법은 아래와 같이 3 가지 방법이 있다.

  • 각각의 테이블 변환: 공통된 속성을 부모 테이블로 두고 달라지는속성을 모두 하위 테이블로 만들어서 조회시 조인을 사용한다. JPA 에서는 조인전략(JOINED)이라고 한다.
  • 통합 테이블로 변환: 테이블을 하나만 사용하여 통합한다. JPA 에서는 단일 테이블 전략(SINGLE_TABLE)이라고 한다.
  • 서브타입 테이블로 변환: 모든 속성을 서브타입마다 하나의 테이블에 둔다. JPA 에서는 구현 클래스마다 테이블 전략(TABLE_PER_CLASS)이라고 한다.

- 조인 전략

자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용한다. 따라서 조회시 조인을 자주 사용한다. 객체는 타입이있지만 테이블은 타입이 없으므로 타입을 구분하는 컬럼(ex - DTYPE)을 사용한다.

위 ER 다이어그램에서 ITEM 은 슈퍼타입이며 ALBUM, MOVIE, BOOK 서브타입이다. ITEM 의 ITEM_ID PK 를 이어 받아 PK 이자 FK 로 사용하였다. 이를 ER 다이어그램을 JPA 로 변환해보자.

 

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

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

 

위는 슈퍼타입이 될 Item 엔티티이다. 상속맵핑은 부모클래스에 @Inheritance 를 사용해야한다. strategy 에는 앞에서 언급한 3 가지 전략중 하나를 명시하는데 위 예제에서는 조인전략을 명시하였다.

 

@DiscriminatorColumn 는 부모 클래스의 구분 컬럼을 설정하는 어노테이션이다. 이 컬럼을 이용하여 자식 테이블을 구분한다. Default 값은 DTYPE 이다.

 

이제 서브타입인 Album, Movie, Book 코드를 작성해본다.

 

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

	private String artist;

.......

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
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 로 슈퍼타입 테이블의 구분 컬럼에 저장된 값을 명시해준다. 위 예제에서는 서브 타입과 맵핑될 엔티티들의 앞글자를 따서 지정해주었다.

 

기본값으로 PK 는 슈퍼타입 테이블의 ID 를 그대로 사용하는데 변경하고 싶다면 @PrimaryKeyJoinColumn 로 지정해준다.

 

테스트 코드를 작성해보자.

 

public static void save(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();
	
	Book book = new Book();
	book.setAuthor("AUTHOR#1");
	book.setIsbn("ISBN#1");
	book.setName("NAME#1");
	book.setPrice(11111);
	em.persist(book);
	
	Movie movie = new Movie();
	movie.setActor("ACTOR#1");
	movie.setDirector("DIRECTOR#1");
	movie.setName("NAME#2");
	movie.setPrice(22222);
	em.persist(movie);
	
	Album album = new Album();
	album.setArtist("ARTIST#1");
	album.setName("NAME#3");
	album.setPrice(33333);
	em.persist(album);
	
	tx.commit();
	em.close();
}

 

실행 후 H2 에서 조회한 결과는 아래와 같다

조인전략은 슈퍼타입과 서브타입으로 클래스들을 쪼개고 슈퍼타입의 PK 를 서브타입에서 사용함으로써 정규화 수준이 높다. 또한 외래키 참조 무결성 제약조건을 활용할 수 있으며, 저장공간을 효율적으로 사용한다.

 

하지만 조회시 무조건 조인을 사용해야 하므로 성능이슈가 생길 수 있으며 등록시 INSERT 를 2 번 사용해야 한다.


- 단일 테이블 전략

단일 테이블 전략은 모든 서브타입의 속성들을 하나의 테이블로 통합하고 DTYPE 으로 어떤 데이터인지 구분한다. 조회시 조인을 사용하지 않으므로 일반적인 상황에서는 가장 빠르다.

 

한 가지 조심해야 하는 점이 있는데, 속성들이 모두 nullable 이어야 한다. 만약 BOOK 을 저장하면 BOOK 에서 사용하는 AUTHOR, ISBN 외에 나머지 속성에는 null 이 들어가기 때문이다.

 

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

......

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

 

@Inheritance 의 타입을 SINGLE_TABLE 로 변경해준다. 그리고 Book 에서 지정했던 @PrimaryKeyJoinColumn 을 삭제해준다.

 

아까와 같은 테스트코드를 작성해서 수행 후 H2 Console 의 ITEM 테이블을 조회하면 아래와 같이 결과가 나온다.

하나의 테이블에 모든 속성이 포함되었다. 그리고 앞에서 언급했듯이 모든 속성들이 nullable 이어야 하는 이유가 나오는데, Book, Album, Movie DTYPE 마다 사용한 속성들이 다르다.

 

조인이 필요없어서 일반적으로 성능이 가장 좋다. 하지만 모든 속성들이 nullable 이어야 한다.

 

위의 특징에서 무조건 성능이 좋은것은 아님에 유의해야 한다. DB 는 일정 수 이상으로 컬럼수가 많아지면 성능이 오히려 하락되는 경우가 있기 때문에 무조건 빠르다는 생각은 위험하다.


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

구현 클래스마다 테이블 전략은 말 그대로 서브타입마다 하나의 테이블을 가지며 테이블 각각에 필요한 컬럼이 있다.

부모 클래스를 코드를 살펴보자.

 

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

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

 

@Inheritance 의 strategy 타입을 TABLE_PER_CLASS 로 변경하였다. 또 @DiscriminatorColumn 을 삭제하였는데, 어차피 서브타입마다 테이블을 별도로 하나씩 두므로 구분자 컬럼이 필요없기 때문이다.

 

테스트코드를 수행하면 H2 Console 의 결과는 아래와 같다.

이 전략은 서브 타입을 구분해서 처리할 때 효과적이며, not null 제약조건을 사용할 수 있다. 하지만 여러 자식들을 한꺼번에 조회할 때 union 을 사용해야 하기 때문에 성능이 느리다.

 

TABLE_PER_CLASS 전략은 DB 설계자와 ORM 전문가 모두 추천하지 않는 전략이라고 한다. 내가 DB 설계 전문가이거나 ORM 전문가는 아니지만 실제로 개발을 하다가 슈퍼타입 서브타입을 사용해야 하는 상황이 생겼을 때, 이런 방식으로 테이블을 설계해서 사용하지는 않았다. 가급적이면 앞의 2 개의 전략을 사용하도록 하자.

댓글