목차
상속관계 매핑
@MappedSuperclass
실전 예제
- 객체는 상속관계가 존재하지만, 관계형 데이터베이스는 상속 관계가 없다.(대부분)
- 그나마 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.
- 상속관계 매핑이라는 것은 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.
-
객체는 상속을 지원하므로 모델링과 구현이 똑같지만, DB는 상속을 지원하지 않으므로 논리 모델을 물리 모델로 구현할 방법이 필요하다.
-
DB의 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법은 세가지 있다.
-
중요한건, DB입장에서 세가지로 구현하지만 JPA에서는 어떤 방식을 선택하던 매핑이 가능하다.
-
JPA가 이 세가지 방식과 매핑하려면
- **@Inheritance(strategy=InheritanceType.XXX)**의 stategy를 설정해주면 된다.
- default 전략은 SINGLE_TABLE(단일 테이블 전략)이다.
- InheritanceType 종류
- JOINED
- SINGLE_TABLE
- TABLE_PER_CLASS
- @DiscriminatorColumn(name="DTYPE")
- 부모 클래스에 선언한다. 하위 클래스를 구분하는 용도의 컬럼이다. 관례는 default = DTYPE
- @DiscriminatorValue("XXX")
- 하위 클래스에 선언한다. 엔티티를 저장할 때 슈퍼타입의 구분 컬럼에 저장할 값을 지정한다.
- 어노테이션을 선언하지 않을 경우 기본값으로 클래스 이름이 들어간다.
- **@Inheritance(strategy=InheritanceType.XXX)**의 stategy를 설정해주면 된다.
-
객체의 상속관계 구현
-
Item
@Entity @Inheritance(strategy = InheritanceType.XXX) // 상속 구현 전략 선택 public class Item { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int price; }
-
Album
@Entity public class Album extends Item { private String artist; }
-
Movie
@Entity public class Movie extends Item { private String director; private String actor; }
-
Book
@Entity public class Book extends Item { private String author; private String isbn; }
-
-
가장 정규화 된 방법으로 구현하는 방식이다.
-
NAME, PRICE가 ITEM 테이블에만 저장되고, ALBUM, MOVIE, BOOK이 각자의 데이터만 저장한다.
-
Item 엔티티 - @Inheritance(strategy = InheritanceType.JOINED) 전략
- 하이버네이트의 조인 전략에서는 @DiscriminatorColumn을 선언하지 않으면 DTYPE 컬럼이 생성되지 않는다.
- 어차피 조인하면 앨범인지 무비인지 알 수 있다. 그래도, DTYPE을 넣어주는 것이 명확하다. 넣어주자.
@Entity @Inheritance(strategy = InheritanceType.JOINED) @DiscriminatorColumn // 하위 테이블의 구분 컬럼 생성(default = DTYPE) public class Item { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int price; }
-
실제 실행된 DDL
- 테이블 4개 생성
- 하위 테이블에 외래키 제약조건 생성. 하위 테이블 입장에서는 ITEM_ID가 PK이면서 FK로 잡아야 한다.
- 조인 전략에 맞는 테이블들이 생섬됨.
Hibernate: create table Album ( artist varchar(255), id bigint not null, primary key (id) ) Hibernate: create table Book ( author varchar(255), isbn varchar(255), id bigint not null, primary key (id) ) Hibernate: create table Item ( DTYPE varchar(31) not null, id bigint generated by default as identity, name varchar(255), price integer not null, primary key (id) ) Hibernate: create table Movie ( actor varchar(255), director varchar(255), id bigint not null, primary key (id) ) Hibernate: alter table Album add constraint FKcve1ph6vw9ihye8rbk26h5jm9 foreign key (id) references Item Hibernate: alter table Book add constraint FKbwwc3a7ch631uyv1b5o9tvysi foreign key (id) references Item Hibernate: alter table Movie add constraint FK5sq6d5agrc34ithpdfs0umo9g foreign key (id) references Item
-
Movie 객체를 저장하면?
- Insert 쿼리가 두개 나간다.
- Item 테이블, Movie 테이블 저장.
- DTYPE에 클래스 이름이 디폴트로 저장됨.
Movie movie = new Movie(); movie.setDirector("감독A"); movie.setActor("배우B"); movie.setName("분노의질주"); movie.setPrice(35000); em.persist(movie); tx.commit();
Hibernate: /* insert advancedmapping.Movie */ insert into Item (id, name, price, DTYPE) values (null, ?, ?, 'Movie') Hibernate: /* insert advancedmapping.Movie */ insert into Movie (actor, director, id) values (?, ?, ?)
-
Movie 객체를 조회하면?
- flush(), clear() 해주면, DB에 insert쿼리 날리고, 1차 캐시 지우므로 find에서 SELECT 쿼리가 나간다.
- Item과 inner join을 통해서 결과를 조회한다.
Movie movie = new Movie(); movie.setDirector("감독A"); movie.setActor("배우B"); movie.setName("분노의질주"); movie.setPrice(35000); em.persist(movie); em.flush(); em.clear(); //DB에 insert쿼리 날리고, 1차 캐시 지우므로 find에서 SELECT 쿼리가 나간다. em.find(Movie.class, movie.getId()); tx.commit();
Hibernate: select movie0_.id as id2_2_0_, movie0_1_.name as name3_2_0_, movie0_1_.price as price4_2_0_, movie0_.actor as actor1_3_0_, movie0_.director as director2_3_0_ from Movie movie0_ inner join Item movie0_1_ on movie0_.id=movie0_1_.id where movie0_.id=?
-
서비스 규모가 크지 않고, 굳이 조인 전략을 선택해서 복잡하게 갈 필요가 없다고 판단 될 때에는
-
한 테이블에 다 저장하고, DTYPE으로 구분하는 단일 테이블 전략을 선택할 수 있다.
-
INSERT 쿼리도 한 번, SELECT 쿼리도 한 번이다. 조인할 필요가 없고, 성능이 좋다.
-
단일 테이블 적용
-
strategy를 SINGLE_TABLE로 변경하면 끝난다.
- JPA의 장점이다. 테이블 구조의 변동이 일어났는데, 코드를 거의 손대지 않고 어노테이션만 수정했다.
- 만약 JPA를 쓰지 않았더라면, 테이블 구조의 변경이 일어나면 거의 모든 코드를 손대야 할 것이다.
-
단일 테이블 전략에서는 @DiscriminatorColumn을 선언해 주지 않아도, 기본으로 DTYPE 컬럼이 생성된다.
-
한 테이블에 모든 컬럼을 저장하기 때문에, DTYPE 없이는 테이블을 판단할 수 없다.
@Entity @DiscriminatorColumn @Inheritance(strategy = InheritanceType.SINGLE_TABLE) public class Item { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int price; }
-
실행된 DDL
- 통합 테이블이 하나 생성된다.
Hibernate: create table Item ( DTYPE varchar(31) not null, id bigint generated by default as identity, name varchar(255), price integer not null, artist varchar(255), author varchar(255), isbn varchar(255), actor varchar(255), director varchar(255), primary key (id) )
-
조인 전략에서 실습했던 Movie 저장, 조회 예제를 그대로 돌려보면?
-
Item 테이블을 그냥 조회한다. 조인하지 않고, DTYPE을 검색 조건으로 추가해서 Movie를 조회
Hibernate: select movie0_.id as id2_0_0_, movie0_.name as name3_0_0_, movie0_.price as price4_0_0_, movie0_.actor as actor8_0_0_, movie0_.director as director9_0_0_ from Item movie0_ where movie0_.id=? and movie0_.DTYPE='Movie'
-
-
-
조인 전략과 유사하지만, 슈퍼 타입의 컬럼들을 서브 타입으로 내린다. NAME, PRICE 컬럼들이 중복되도록 허용하는 전략이다.
-
구현 클래스마다 테이블 생성 전략 적용
-
@Id 생성 전략 GenerationType.AUTO를 사용하는 경우 strategy를 TABLE_PER_CLASS로 변경하고
-
이때, ITEM 엔티티는 실제 생성되는 테이블이 아니므로 abstract 클래스여야 하고, @DiscriminatorColumn도 필요가 없어진다.
- 하지만, @Id 생성 전략 GenerationType.IDENTITY를 사용하는 경우 문제가 있다.
- @Inheritance의 TABLE_PER_CLASS와 @Id의 GenerationType을 IDETITY를 같이 사용할 경우 에러 발생
Cannot use identity column key generation with <union-subclass> mapping for
- @Id의 GenerationType을 TABLE 타입으로 변경 적용해서 해결. 그러나, 시퀀스 테이블이 생성되므로 이것에 대한 매핑까지 추가해줘야 완벽히 해결이 된다.
- https://stackoverflow.com/questions/916169/cannot-use-identity-column-key-generation-with-union-subclass-table-per-clas
-
Item 엔티티 설정
@Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public abstract class Item { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; private int price; }
-
생성된 DDL
-
하위 테이블 3개만 생성된다.
Hibernate: create table Album ( id bigint not null, name varchar(255), price integer not null, artist varchar(255), primary key (id) ) Hibernate: create table Book ( id bigint not null, name varchar(255), price integer not null, author varchar(255), isbn varchar(255), primary key (id) ) Hibernate: create table Movie ( id bigint not null, name varchar(255), price integer not null, actor varchar(255), director varchar(255), primary key (id) )
-
-
조인 전략에서의 Movie 저장, 조회를 그대로 실행한 결과는?
- Movie 테이블에 insert
- Movie 테이블에서 select
Hibernate: /* insert advancedmapping.Movie */ insert into Movie (name, price, actor, director, id) values (?, ?, ?, ?, ?) Hibernate: select movie0_.id as id1_2_0_, movie0_.name as name2_2_0_, movie0_.price as price3_2_0_, movie0_.actor as actor1_3_0_, movie0_.director as director2_3_0_ from Movie movie0_ where movie0_.id=?
-
문제점
-
객체지향 프로그래밍에서는 MOVIE, ALBUM, BOOK 객체를 ITEM 타입으로도 조회할 수 있다.
Movie movie = new Movie(); movie.setDirector("감독A"); movie.setActor("배우B"); movie.setName("분노의질주"); movie.setPrice(35000); em.persist(movie); em.flush(); em.clear(); em.find(Item.class, movie.getId()); tx.commit();
-
실행된 SQL
- union all로 전체 하위 테이블을 다 찾는다.
- INSERT 까진 심플했으나, 조회가 시작되면 굉장히 비효율적으로 동작한다.
Hibernate: select item0_.id as id1_2_0_, item0_.name as name2_2_0_, item0_.price as price3_2_0_, item0_.artist as artist1_0_0_, item0_.author as author1_1_0_, item0_.isbn as isbn2_1_0_, item0_.actor as actor1_3_0_, item0_.director as director2_3_0_, item0_.clazz_ as clazz_0_ from ( select id, name, price, artist, null as author, null as isbn, null as actor, null as director, 1 as clazz_ from Album union all select id, name, price, null as artist, author, isbn, null as actor, null as director, 2 as clazz_ from Book union all select id, name, price, null as artist, null as author, null as isbn, actor, director, 3 as clazz_ from Movie ) item0_ where item0_.id=?
-
-
-
조인 전략
- 장점
- 테이블이 정규화가 되어있고,
- 외래 키 참조 무결성 제약조건 활용 가능
- ITEM의 PK가 ALBUM, MOVIE, BOOK의 PK이자 FK이다. 그래서 다른 테이블에서 아이템 테이블만 바라보도록 설계하는 것이 가능 하다
- 저장공간 효율화
- 테이블 정규화로 저장공간이 딱 필요한 만큼 소비된다.
- 단점
- 조회시 조인을 많이 사용한다. 단일 테이블 전략에 비하면 성능이 안나온다. 조인하니까.
- 그래서 조회 쿼리가 복잡하다
- 데이터 저장시에 INSERT 쿼리가 상위, 하위 테이블 두번 발생한다.
- 정리
- 성능 저하라고 되어있지만, 실제로는 영향이 크지 않다.
- 오히려 저장공간이 효율화 되기 때문에 장점이 크다.
- 기본적으로는 조인 정략이 정석이라고 보면 된다. 객체랑도 잘 맞고, 정규화도 되고, 그래서 설계가 깔끔하게 나온다.
- 장점
-
단일 테이블 전략
- 장점
- 조인이 필요 없으므로 일반적인 조회 성능이 빠르다.
- 조회 쿼리가 단순핟.
- 단점
- 자식 엔티티가 매핑한 컬럼은 모두 NULL을 허용해야 한다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
- 상황에 따라서 조인 전략보다 성능이 오히려 느려질 수 있다.
- 보통 이 상황에 해당하는 임계점을 넘을 일은 많지 않다.
- 장점
-
구현 클래스마다 테이블 전략
- 결론은
- 이 전략은 쓰지말자.
- ORM을 하다보면 데이터 쪽과 객체 쪽에서 trade off를 할 때가 있는데, 이 전략은 둘 다 추천하지 않는다.
- 장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적이다
- NOT NULL 제약조건을 사용할 수 있다.
- 단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다(UNION SQL)
- 자식 테이블을 통합해서 쿼리하기 어렵다.
- 변경이라는 관점으로 접근할 때 굉장히 좋지 않다.
- 예를 들어, ITEM들을 모두 정산하는 코드가 있다고 가정할 때, ITEM 하위 클래스가 추가되면 정산 코드가 변경된다. 추가된 하위 클래스의 정산 결과를 추가하거나 해야 한다.
- 결론은
-
상속관계 매핑 정리
- 기본적으로는 조인 전략을 가져가자.
- 그리고 조인 전략과 단일 테이블 전략의 trade off를 생각해서 전략을 선택하자.
- 굉장히 심플하고 확장의 가능성도 적으면 단일 테이블 전략을 가져가자. 그러나 비즈니스 적으로 중요하고, 복잡하고, 확장될 확률이 높으면 조인 전략을 가져가자.
- 객체의 입장에서 공통 매핑 정보가 필요할 때 사용한다.
- id, name은 객체의 입장에서 볼 때 계속 나온다.
- 이렇게 공통 매핑 정보가 필요할 때, 부모 클래스에 선언하고 속성만 상속 받아서 사용하고 싶을 때 @MappedSuperclass를 사용한다.
- DB 테이블과는 상관없다. 아래에 보면 DB는 매핑 정보 다 따로 쓰고 있다. 객체의 입장이다.
-
생성자, 생성시간, 수정자, 수정시간을 모든 엔티티에 공통으로 가져가야 하는 상황에서
-
아래와 같이 BaseEntity를 정의해서 활용할 수 있다.
-
BaseEntity.java
- 매핑정보만 상속받는 Superclass라는 의미의 @MappedSuperclass 어노테이션 선언
@Getter @Setter @MappedSuperclass public abstract class BaseEntity { private String createdBy; private LocalDateTime createdDate; private String lastModifiedBy; private LocalDateTime lastModifiedDate; }
-
Member.java, Team.java
-
BaseEntity 상속
@Entity public class Member extends BaseEntity { ... }
@Entity public class Team extends BaseEntity { ... }
-
-
실행된 DDL
-
BaseEntity에 선언된 컬럼들이 생성 된다.
Hibernate: create table Member ( id bigint generated by default as identity, createdBy varchar(255), createdDate timestamp, lastModifiedBy varchar(255), lastModifiedDate timestamp, age integer, description clob, roleType varchar(255), name varchar(255), locker_id bigint, team_id bigint, primary key (id) ) Hibernate: create table Team ( id bigint generated by default as identity, createdBy varchar(255), createdDate timestamp, lastModifiedBy varchar(255), lastModifiedDate timestamp, name varchar(255), primary key (id) ) ...
-
- 상속광계 매핑이 아니다.
- @MappedSuperclass가 선언되어 있는 클래스는 엔티티가 아니다. 당연히 테이블과 매핑도 안된다.
- 단순히 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공한다.
- 조회, 검색이 불가하다. 부모 타입으로 조회하는 것이 불가능하다는 이야기.(em.find(BaseEntity) 불가능)
- 직접 생성해서 사용할 일이 없으므로 추상 클래스로 만드는 것을 권장한다.
- 테이블과 관계가 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 한다.
- 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.
- 참고
- JPA에서 @Entity 클래스는 @Entity나 @MappedSuperclass로 지정한 클래스만 상속할 수 있다.