[JPA] 상속 관계 매핑
📌 목차
- 상속 관계 매핑
- @MappedSuperclass
- 실전 예제 - 4. 상속 관계 매핑
✔ 상속 관계 매핑
객체는 상속을 지원하지만 DB는 상속을 지원하지 않는다.
이번 장에서는 JPA가 이러한 패러다임의 차이를 어떻게 극복했는지 알아보자.
✅ DB(DataBase)
- 위 사진은 DataBase(슈퍼타입/서브타입)의 논리 모델링을 구상한 사진입니다.
✅ 객체(Entity)
- 관계형 데이터베이스는 상속 관계 x
- RDBMS의 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사
- 상속 관계 매핑 : 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑
✅ 슈퍼타입과 서브타입
슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법은 아래와 같다.
- 각각 테이블로 변환
- 조인 전략
- 통합 테이블로 변환
- 단일 테이블 전략
- 서브타입 테이블로 변환
- 구현 클래스마다 테이블 전략
💡 조인 전략
- 기존 SQL에서 수행하는 JOIN
- 추상화를 통해 상위 테이블 하위 테이블을 구분한다
- 상위 테이블에는 기본키(PK), 하위 테이블에는 외래키(FK)를 통해 조인
- 데이터 삽입 시에는 상위, 하위 테이블에 모두 INSERT 수행
💡 단일 테이블 전략
- 기존 DB의 논리 모델을 하나의 테이블로 만드는 것
- 데이터를 구분하기 위해 추가적인 컬럼이 필요하다 (ex: DTYPE)
💡 구현 클래스마다 테이블 전략
- 추상화 단계를 거치지 않고 각각의 테이블에 모든 컬럼을 생성하는 것
❓ 패러다임의 차이
JPA에서는 조인 전략, 단일 테이블 전략, 구현 클래스마다 테이블 전략이 존재한다.
여기서 아래와 같은 의문점이 들어야 하는 것 같다.
DB와 JPA를 어떻게 매핑 할까? 🤔
JPA는 DB 테이블 전략과는 상관없이 JPA에서는 상속 관계 매핑이 가능하다.
⚡ JPA의 상속 관계 매핑 주요 어노테이션
✅ Inheritance(strategy = InheritanceType.XXX)
@Inheritance(strategy = InheritanceType.JOINED)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
- JOINED
- 조인 전략
- SINGLE_TABLE
- 단일 테이블 전략
- TABLE_PER_CLASS
- 구현 클래스마다 테이블 전략
- @DiscriminatorColumn(name = “DTYPE명”)
- 부모 엔티티에서 설정
- DTYPE명을 지정하고 싶을 때 사용한다
- @DiscriminatorValue(“엔티티명”)
- 자식 엔티티에서 설정
⚡ 기본 상속 관계 매핑 실습
Item > Album, Movie, Book 엔티티 생성
✅ Item 엔티티 생성
@Entity
public class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
✅ Album 엔티티 생성
@Entity
public class Album extends Item {
private String artist;
}
✅ Book 엔티티 생성
@Entity
public class Book extends Item {
private String author;
private String isbn;
}
✅ Moive 엔티티 생성
@Entity
public class Movie extends Item {
private String director;
private String actor;
}
🖨️ 결과 화면 출력
Hibernate:
create table Item (
DTYPE varchar(31) not null,
id bigint not null,
name varchar(255),
price integer not null,
actor varchar(255),
director varchar(255),
author varchar(255),
isbn varchar(255),
artist varchar(255),
primary key (id)
)
각각의 엔티티를 상속 관계로 연결한 후 실행을 한 결과를 위와 같다.
위와 같은 결과를 통해 JPA에서는 기본적으로 단일 테이블 전략을
사용한다는 것을 알 수 있다.
⚡ 조인 전략으로 변경
기존 부모(상위)클래스에 @Inheritance 어노테이션을 추가한다.
✅ Item 엔티티 생성
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 상속 관계 매핑 추가
public class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
//..중략 Getter Setter
}
- 기본 전략을 JOIN 전략으로 변경 하였다.
🖨️ 엔티티 생성 결과 출력
Hibernate:
create table Item (
id bigint not null,
name varchar(255),
price integer not null,
primary key (id)
)
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 Movie (
actor varchar(255),
director varchar(255),
id bigint not null,
primary key (id)
)
✅ 영화 데이터 등록
public class JpaMainSample {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
/* movie 등록 */
Movie movie = new Movie();
movie.setDirector("영화감독");
movie.setActor("이준기");
movie.setName("바람과 함께 사라지다");
movie.setPrice(10000);
em.persist(movie);
/* 영속성 컨텍스트에 존재하는 데이터 DB 전송 */
em.flush();
/* 1차 캐시 비우기 */
em.clear();
/* 조회 */
Movie findMovie = em.find(Movie.class, movie.getId()); // 1번 id
System.out.println("findMovie = " + findMovie);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
🖨️ 조인 전략 결과 출력
Hibernate:
call next value for hibernate_sequence
Hibernate:
/* insert com.hello.jpasample.Movie
*/ insert
into
Item
(name, price, id)
values
(?, ?, ?)
Hibernate:
/* insert com.hello.jpasample.Movie
*/ insert
into
Movie
(actor, director, id)
values
(?, ?, ?)
Hibernate:
select
movie0_.id as id1_6_0_,
movie0_1_.name as name2_6_0_,
movie0_1_.price as price3_6_0_,
movie0_.actor as actor1_11_0_,
movie0_.director as director2_11_0_
from
Movie movie0_
inner join
Item movie0_1_
on movie0_.id=movie0_1_.id
where
movie0_.id=?
findMovie = com.hello.jpasample.Movie@4d74c3ba
- Item 엔티티와 Movie 엔티티를 조인한 결과를 출력한다.
⚡ 조인 전략 DTYPE
🖨️ 조인 전략 결과 출력의 결과에서는 현재 DTYPE을 생성하지 않았다.
이번에는 테이블에 DTYPE이 생성되도록 Item 엔티티에 @DiscriminatorColumn
어노테이션을 추가 해보자.
✅ Item 엔티티에 해당 어노테이션 추가
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn // 해당 어노테이션 추가
// @DiscriminatorColumn(name = "DIS_TYPE") 이름을 지정할 수 있다
public class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
//..중략 Getter Setter
}
- @DiscriminatorColumn 어노테이션을 추가한다.
- 이름을 설정 할 수 있지만 가급적이면 관례를 따르는것이 좋다.
🖨️ 조인 전략 결과 출력
Hibernate:
/* insert com.hello.jpasample.Movie
*/ insert
into
Item
(name, price, DTYPE, id)
values
(?, ?, 'Movie', ?)
- @DiscriminatorColumn 어노테이션을 추가하였기에, 위와 같은 결과가 출력된다.
- 부모 - 자식 관계에서 어떤 자식의 타입인지 구분하기 위해 사용.
⚡ DBA 요구사항이 있다고 가정
😀 하위 자식들 A(Album), B(Book), M(Movie) 값을
상위 엔티티 Item 필드에 DTYPE으로 넣어주면 좋을 것 같아요.
✅ 하위 엔티티 @DiscriminatorValue 추가
@Entity
@DiscriminatorValue("A") // 추가
public class Album extends Item {
//..중략
}
@Entity
@DiscriminatorValue("B") // 추가
public class Book extends Item {
//..중략
}
@Entity
@DiscriminatorValue("M") // 추가
public class Movie extends Item {
//..중략
}
✅ @DiscriminatorValue 내부 구성
@Target({TYPE})
@Retention(RUNTIME)
public @interface DiscriminatorValue {
/**
* (Optional) The value that indicates that the
* row is an entity of the annotated entity type.
*
* <p> If the <code>DiscriminatorValue</code> annotation is not
* specified and a discriminator column is used, a
* provider-specific function will be used to generate a value
* representing the entity type. If the <code>DiscriminatorType</code> is
* <code>STRING</code>, the discriminator value default is the
* entity name.
*/
String value();
}
- DiscriminatorValue default value는 엔티티명이다.
✅ DB 출력 화면
ITEMSAMPLE 테이블을 ITEM 테이블로 봐주시면 감사하겠습니다. 🤣
- 위와 같이 적용 시 ITEM 테이블에 DTYPE이 M으로 들어간 것을 확인할 수 있다.
✔ 조인 전략 장 단점
👍 장점
- 테이블 정규화
- 외래 키 참조 무결성 제약조건 활용 가능
- 기존 SQL의 기본키(PK), 외래키(FK)를 통해 조인 하는 방식
- 저장 공간 효율화
👎 단점
- 조회시 조인을 많이 사용, 성능 저하
- 조회 쿼리 복잡
- 데이터 저장 시 INSERT SQL 2번 호출
⚡ 단일 테이블 전략
✅ 단일 테이블 전략 적용
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 단일 테이블 전략
@DiscriminatorColumn // 없어도 된다
public class Item {
//..중략
}
- 단일 테이블 전략은 테이블 구조가 복잡하지 않은 경우에 사용을 하는것을 권장한다.
- 해당 엔티티의 strategy 속성을 위와 같이 적용 해주면 된다.
🖨️ 단일 테이블 전략 결과 출력
Hibernate:
create table Item (
DTYPE varchar(31) not null,
id bigint not null,
name varchar(255),
price integer not null,
actor varchar(255),
director varchar(255),
author varchar(255),
isbn varchar(255),
artist varchar(255),
primary key (id)
)
- 하위 엔티티의 필드값이 Item 테이블에 모두 들어온 것을 확인할 수 있다.
- 하위 엔티티의 테이블이 생성 되지 않는다.
- @DiscriminatorColumn 어노테이션은 생략 되어도 상관 없다.
✅ DB 출력 화면
ITEMSAMPLE 테이블을 ITEM 테이블로 봐주시면 감사하겠습니다. 🤣
- ITEM 테이블 하나에 모든 필드값이 초기화 된 것을 볼 수 있다.
✔ 단일 테이블 전략 장 단점
👍 장점
- 조인이 필요 없으며, 조회 쿼리가 단순함
👎 단점
- 자식 엔티티가 매핑한 컬럼은 모두 null 허용
- 단일 테이블에 모든 데이터를 저장하므로 테이블의 사이즈가 커지는 경우 상황에 따라서 조회 성능이 오히려 더 느려질 수 있다.
⚡ 구현 클래스마다 테이블 전략
결론부터 말하면 구현 클래스마다 테이블 전략은 쓰면 안되는 전략이다.
- Item 테이블을 제거하고 모든 테이블에 필드를 넣어주는 전략이다.
✅ 구현 클래스마다 테이블 전략
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) // 구현 클래스마다 테이블 전략
public abstract class Item {
//..중략
}
- 단독으로 해당 엔티티가 사용 될 수 있기에, abstract를 통해 추상클래스 선언.
🖨️ 구현 클래스마다 테이블 전략
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)
)
- 위 전략 사용 시 Item 엔티티는 생성되지 않는다.
⚡ 해당 전략의 문제점
✅ 다형성을 통해 엔티티 조회
public class JpaMainSample {
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("영화감독");
movie.setActor("이준기");
movie.setName("바람과 함께 사라지다");
movie.setPrice(10000);
em.persist(movie);
em.flush();
em.clear();
Item item = em.find(Item.class, movie.getId()); // 여기가 문제
System.out.println("item = " + item);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 다형성을 통해 Item 엔티티를 조회 한다.
🖨️ 출력 결과
Hibernate:
select
item0_.id as id1_6_0_,
item0_.name as name2_6_0_,
item0_.price as price3_6_0_,
item0_.actor as actor1_11_0_,
item0_.director as director2_11_0_,
item0_.author as author1_1_0_,
item0_.isbn as isbn2_1_0_,
item0_.artist as artist1_0_0_,
item0_.clazz_ as clazz_0_
from
( select
id,
name,
price,
actor,
director,
null as author,
null as isbn,
null as artist,
1 as clazz_
from
Movie
union
all select
id,
name,
price,
null as actor,
null as director,
author,
isbn,
null as artist,
2 as clazz_
from
Book
union
all select
id,
name,
price,
null as actor,
null as director,
null as author,
null as isbn,
artist,
3 as clazz_
from
Album
) item0_
where
item0_.id=?
item = com.hello.jpasample.Movie@f8f56b9
- 여기서 문제점은 JPA가 union all을 통해 모든 객체를 찾는다는 점이다.
- 즉, 위에서도 언급 했지만 구현 클래스마다 테이블 전략 사용은 지양하자.
🔥 마지막으로 상속 관계 매핑의 장점
😀 DB 테이블 설계 변경 되니까 설계랑 로직 수정 해주세요.
- JPA를 사용하지 않는다면 기존에 작성된 쿼리, 비즈니스 로직의 수정 필요.
- 상속 관계 매핑 전략을 통해 위 같은 상황을 쉽게 방지 할 수 있다.
댓글남기기