관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없습니다. 대신에 슈퍼타입 서브타입 관계
라는 모델링 기법이 객체지향의 상속과 유사합니다.
슈퍼타입 서브 타입 논리 모델을 실제 물리 모델로 구현하는 방법은 3가지가 있습니다.
- 각각의 테이블로 변환: 각각을 모두 테이블로 만들고 조회할 때 조인을 사용합니다. JPA에서는
조인 전략
이라 합니다. - 통합 테이블로 변환: 테이블을 하나만 사용해서 통합합니다. JPA에서는
단일 테이블 전략
이라고 합니다. - 서브타입 테이블로 변환: 서브 타입마다 하나의 테이블을 만듭니다.
조인 전략은 위의 그림처럼 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본키 + 외래 키로 사용하는 전략입니다.
- 테이블이 정규화 됩니다.
- 외래 키 참조 무결성 제약조건을 활용할 수 있습니다.
- 저장공간을 효율적으로 사용합니다.
- 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있습니다.
- 조회 쿼리가 복잡합니다.
- 데이터를 등록할 INSERT SQL을 두 번 실행합니다.
@Entity
public class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
public class Book extends Item {
private String author;
private String isbn;
}
@Entity
public class Album extends Item {
private String artist;
}
@Entity
public class Movie extends Item {
private String director;
private String actor;
}
위의 그림에 나오는 관계처럼 Item을 상속하는 Movie
, Album
, Book
클래스를 만들었습니다. 이 상태로 실행한 후에 JPA가 만들어주는 DDL문을 보겠습니다.
기본 전략 자체가 Item 테이블 안에 모든 컬럼을 만든 것을 볼 수 있습니다. 즉, 한 테이블 안에 다 넣는 것이 기본 전략입니다.
그래서 이번에는 strategy를 JOINED
로 주고 실행을 해보겠습니다.
이번에는 테이블이 모두 각각 생긴 것을 볼 수 있습니다. 위에서 말한 JOIN 전략
을 사용한 것입니다.
그리고 위와 같이 Moview 객체를 저장한 후에 실행해보겠습니다.
그러면 위와 같이 INSERT 쿼리가 2번 실행된 것을 볼 수 있습니다.
데이터도 테이블에 잘 들어간 것을 볼 수 있습니다.
그러면 이번에는 위와 같이 Movie 테이블을 조회하면 JPA가 어떤 쿼리를 만들어줄까요?
Movie, Item 테이블을 INNER JOIN
쿼리를 생성해주는 것을 볼 수 있습니다.
위의 조언 전략에 대한 그림에서 보면 DTYPE
이라는 것이 존재하는 것을 보았을텐데요. 이번에는 위와 같이 @DiscriminatorColumn
어노테이션을 사용해서 실행해보겠습니다.
그러면 위와 같이 DTYPE
으로 생성이 되는 것을 볼 수 있습니다.
@DiscriminatorColumn
어노테이션을 사용하면 DTYPE이 생성되어 Entity 이름이 저장되는 것을 볼 수 있습니다. 사용하여 어떤 엔티티를 통해서 저장된 것이 알기 편하기 때문에 사용하는 것이 좋습니다.
단일 테이블 전략은 이름 그대로 테이블을 하나만 사용합니다. ITEM 테이블에 있는 ID와 MOVIE 테이블에 ID랑 똑같은 키입니다.
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠릅니다.
- 조회 쿼리가 단순합니다.
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 합니다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있습니다. 그러므로 상황에 따라서는 조회 성능이 오히려 느려질 수 있습니다.
- 구분 컬럼을 꼭 사용해야 합니다. 따라서
@DiscriminatorColumn
을 꼭 설정해야 합니다. - 즉, 한 테이블에 모든 정보를 다 저장하고 DTYPE을 통해서 구분하는 것입니다.
위와 같이 strategy 하나만 변경하면 됩니다.
그러면 위와 같이 테이블 하나에 모든 정보를 다 생성하는 것을 볼 수 있습니다. (다른 테이블은 생성되지 않았습니다.)
그리고 실행되는 쿼리만 보아도 INSERT 쿼리도 1번만 실행되고 SELECT 쿼리도 JOIN 필요 없이 간단하게 가져오는 것을 볼 수 있습니다.
참고로 단일 테이블 전략은 @DiscriminatorColumn 어노테이션을 사용하지 않아도 DTYPE이 자동으로 생성됩니다.
구현 클래스마다 테이블 전략은 위와 같이 자식 엔티티마다 테이블을 만듭니다. 즉, Item 테이블을 없애고 NAME, PRICE 같은 속성들은 중복되도록 허용하는 것입니다. (일반적으로 추천하지 않는 전략입니다.)
- 서브 타입을 구분해서 처리할 때 효과적입니다.
- not null 제약 조건을 사용할 수 있습니다.
- 여러 자식 테이블을 함께 조회할 때 성능이 느립니다.(SQL에 Union 사용..)
- 자식 테이블을 통합해서 쿼리하기 어렵습니다.
위와 같이 TABLE_PER_CLASS
속성을 주면 됩니다. 그리고 Item 클래스는 지금까지 추상 클래스로 안만들었지만 추상 클래스로 사용해야 합니다.
위와 같이 Movie Key로 Item을 찾아오면 어떤 쿼리가 실행될까요?
엄청나게 복잡한 UNION을 사용한 쿼리가 발생하는 것을 볼 수 있습니다. 즉, 이 전략은 추천하지 않기에 다른 전략을 사용하는 것이 좋습니다.(사용하면 안되는 전략..)
지금까지 학습한 상속 관계 매핑은 부모 클래스와 자식 클래스를 모두 데이터베이스 테이블과 매핑했습니다. 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperclass
을 사용합니다.
만약에 위와 같이 모든 테이블에 생성 시간
, 변경 시간
을 저장해야 한다는 요구사항이 생겼다면 어떻게 할 수 있을까요? 위와 같이 모든 엔티티에 필드를 다 추가해야 할까요?
이럴 때 사용하면 좋은 것이 @MappedSuperclass
입니다.
위와 같이 BaseEntiy
라는 부모 클래스를 만들고 @MappedSuperclass
어노테이션을 추가한 후에 공통적으로 사용할 컬럼들을 적어주면 됩니다.
그리고 위와 같이 공통 필드를 사용할 엔티티는 BaseEntity 클래스를 상속 받으면 됩니다.
그러면 위와 같이 Member 테이블을 생성할 때 자동으로 BaseEntity에 존재하는 필드들이 생성되는 것을 볼 수 있습니다.
- 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용합니다.
@MappedSuperclass
로 지정한 클래스는 엔티티가 아니므로em.find()
나 JPQL에서 사용할 수 없습니다.- 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장합니다.
즉, @MappedSuperclass
는 테이블과는 관계가 없고 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할을 할 뿐입니다.