상속관계 매핑
객체는 상속관계가 있지만 관계형 데이터베이스는 상속 관계가 없다.
그나마 객체의 상속관계와 비슷한 데이터 모델이 있다.
슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.
상속관계 매핑이란 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 거다.
DB의 슈퍼타입, 서브타입 관계라는 논리 모델링 기법을 어떤 세 가지 방법으로 구현하든 JPA에서는 매핑을 하도록 지원해준다.
슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법에는 3가지가 있다.
- 각각 테이블로변환 -> 조인 전략
- 통합 테이블로 변환 -> 단일 테이블 전략
- 서브타입 테이블로 변환 -> 구현 클래스마다 테이블 전략
조인 전략
ITEM이라는 테이블을 만들고 ALBUM, MOVIE, BOOK 테이블을 만든 다음에 데이터를 나누고 조인으로 구성하는 방식이다.
ITEM과 ALBUM에 데이터를 넣어야 할 경우 ITEM 테이블, ALBUM 테이블에 각각 INSERT 쿼리가 나가서 총 두 번이 나간다.
조회는 ITEM의 PK 값을 가지고 ITEM 테이블과 다른 테이블을 조인해서 가져온다.
다만 이게 ALBUM인지, MOVIE인지 구분하기 위해 ITEM 테이블에 DTYPE이라는 컬럼이 존재해야 한다.
장점
- 테이블 정규화
- 외래 키 참조 무결성 제약조건 활용가능
- ITEM의 PK와 다른 테이블의 PK가 같다.
- 주문 테이블에서 ITEM 테이블 데이터 필요시 ITEM 테이블만 보면 된다.
- 다른 테이블을 안 봐도 돼서 설계가 깔끔하다.
- 저장공간 효율화
- 정규화가 되어 있어 저장공간 자체가 효율화된다.
단점
- 조회 시 조인을 많이 사용, 성능저하
- 조회 쿼리가 복잡함
- 데이터 저장 시 INSERT SQL 2번 호출
INSERT SQL 2번 호출되거나 조인을 더 하는 부분은 큰 단점이 아니지만 단일 테이블과 비교 시 복잡한 게 단점이다. 조인 전략이 객체와도 잘 맞고 정규화도 되고 설계 입장에서 깔끔하게 설계를 할 수 있어 조인 전략이 정석이라고 생각하자.
단일 테이블 전략
논리 모델을 한 테이블로 다 합치는 거다.
쉽게 말해 ALBUM, MOVIE, BOOK 테이블의 컬럼들을 ITEM 테이블에 다 추가하는 방식이다.
이 방식에서도 ALBUM인지 MOVIE인지 구분하기 위해 ITEM 테이블에 DTYPE이라는 컬럼이 필요하다.
장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
- 조회 쿼리가 단순함
단점
- 자식 엔티티가 매핑한 컬럼은 모두 null 허용
- id, name, price 빼고는 다른 컬럼들은 null을 허용해줘야 한다.
- 왜냐면 album 저장 시 artist 데이터만 들어가지 director, actor, author 데이터는 안 들어가기 때문이다.
- 따라서 데이터 무결성 입장에서 보면은 애매하다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
- 상황에 따라서 조회 성능이 오히려 느려질 수 있다.
구현 클래스마다 테이블 전략
ITEM 테이블을 만들지 않고 ITEM 테이블의 컬럼을 ALBUM, MOVIE, BOOK 테이블 3개에 다 추가하는 방식이다.
이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천X
장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적
- not null 제약조건 사용 가능
단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느림(UNION SQL 필요)
- 자식 테이블을 통합해서 쿼리하기 어려움
주요 어노테이션
- @Inheritance(strategy=InheritanceType.XXX)
- JOINED: 조인 전략
- SINGLE_TABLE: 단일 테이블 전략
- TABLE_PER_CLASS: 구현 클래스마다 테이블 전략
- @DiscriminatorColumn(name=“DTYPE”)
- @DiscriminatorValue(“XXX”)
코드 레벨에서 좀 더 자세히 알아보자.
jpa 기본전략(단일 테이블 전략)
@Entity
public class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
public class Album extends Item {
private String artis;
}
... Movie
... Book
실행결과
Hibernate:
create table Item (
price integer not null,
id bigint not null,
DTYPE varchar(31) not null,
actor varchar(255),
artis varchar(255),
author varchar(255),
director varchar(255),
isbn varchar(255),
name varchar(255),
primary key (id)
)
위와 같이 코드를 짠 상태로 실행을 해보면 하나의 테이블에 상속받은 테이블들의 컬럼이 다 들어가는 걸 볼 수 있다.
JPA 기본 전략이 단일 테이블 전략과 매핑되기 때문이다.
조인전략
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
... Album
... Movie
@Inheritance(strategy = InheritanceType.JOINED)과 @DiscriminatorColumn을 추가했다.
실행결과
Hibernate:
create table Item (
price integer not null,
id bigint not null,
DTYPE varchar(31) not null,
name varchar(255),
primary key (id)
)
Hibernate:
create table Album (
id bigint not null,
artis varchar(255),
primary key (id)
)
Hibernate:
create table Book (
id bigint not null,
author varchar(255),
isbn varchar(255),
primary key (id)
)
Hibernate:
create table Movie (
id bigint not null,
actor varchar(255),
director varchar(255),
primary key (id)
)
ALBUM, BOOK, MOVIE 테이블도 모두 생성된 걸 볼 수 있고 각 테이블의 PK가 ITEM 테이블의 PK와 일치하다.
테스트 코드
Movie movie = new Movie();
movie.setDirector("aaa");
movie.setActor("bbb");
movie.setName("바람 사라짐");
movie.setPrice(10000);
em.persist(movie);
em.flush();
em.clear();
Movie findMovie = em.find(Movie.class, movie.getId());
System.out.println("findMovie: " + findMovie);
실행결과
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.상속.Movie */insert
into
Item (name, price, DTYPE, id)
values
(?, ?, 'Movie', ?)
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.상속.Movie */insert
into
Movie (actor, director, id)
values
(?, ?, ?)
Hibernate:
select
m1_0.id,
m1_1.name,
m1_1.price,
m1_0.actor,
m1_0.director
from
Movie m1_0
join
Item m1_1
on m1_0.id=m1_1.id
where
m1_0.id=?
movie 저장 시 insert 쿼리가 Item 1번, Movie 1번 총 두 번 날아가는 걸 볼 수 있다.
movie 조회 시 Item 테이블과 Movie 테이블을 조인해서 데이터를 가져온 걸 볼 수 있다.
저장할 때 각 테이블에 Insert 해야 되면 insert 하고 조회할 때 조인이 필요하면 조인해서 데이터를 가져오는 등 JPA가 알아서 동작한다. @DiscriminatorColumn을 사용하면 해당 엔티티에 DTPYE 필드가 자동으로 추가된다.
@DiscriminatorColumn(name = "DIS_TYPE") 이런 식으로 이름을 변경할 수 있자만 관례상 DTYPE을 많이 쓴다. 테이블에서 DTYPE 컬럼에 저장되는 값은 MOVIE, BOOK, ALBUM과 같이 테이블 이름이 기본값으로 저장된다. 이러한 기본 값을 변경하고 싶으면 자식 클래스에 @DiscriminatorValue("M")와 같이 추가하면 된다.
코드예시
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
...
}
테이블 결과
단일 테이블 전략
코드예시
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
...
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artis;
}
... MOVIE
... BOOK
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)와 같이 전략을 SINGLE_TABLE로 바꿔주면 된다. 단일 테이블 전략에서는 @DiscriminatorColumn이 없어도 DTYPE이 자동으로 생성된다.
단일 테이블 전략은 하나의 테이블에 다 들어가 있기 때문에 ALBUM 인지 MOVIE 인지 구분하기 위해 DTYPE이 꼭 생성된다.
실행결과
Hibernate:
create table Item (
price integer not null,
id bigint not null,
DTYPE varchar(31) not null,
actor varchar(255),
artis varchar(255),
author varchar(255),
director varchar(255),
isbn varchar(255),
name varchar(255),
primary key (id)
)
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.상속.Movie */insert
into
Item (name, price, actor, director, DTYPE, id)
values
(?, ?, ?, ?, 'M', ?)
Hibernate:
select
m1_0.id,
m1_0.name,
m1_0.price,
m1_0.actor,
m1_0.director
from
Item m1_0
where
m1_0.DTYPE='M'
and m1_0.id=?
실행결과 쿼리를 봐보자.
Item 테이블에 데이터를 다 저장하는 걸 볼 수 있다.
그리고 저장 시 insert 쿼리는 1번 나가고 조회 시 조인 없이 ITEM 테이블에서만 데이터를 가져오는 걸 볼 수 있다.
테이블 결과
조인 테이블 전략에서 단일 테이블 전략으로 바꿨을 때 코드 레벨에서 strategy=InheritanceType.SINGLE_TABLE 요 부분만 수정했다.
만약에 조인 전략으로 애플리케이션을 개발하다가 성능 테스트 해보니 성능이 너무 안 나와서 단일 테이블 전략으로 바꾼다고 가정해 보자.
만약 JPA를 안쓸 경우 코드나 쿼리들을 많이 수정해야 한다. 하지만 JPA를 사용할 경우 위와 같이 어노테이션 부분만 수정하면 된다.
결론적으로는 JPA 사용 시 유연하고 편리하게 개발이 가능하다는 점이다.
@MappedSuperclass
공통 매핑 정보가 필요할 때 사용(id, name)
부모 클래스에 공통 매핑 정보를 두고 속성만 상속해서 사용하고 싶은 경우에 사용
아래 이미지를 보면 DB 테이블은 완전히 다르고 객체 입장에서 속성만 상속받아서 쓰는 방식이다.
코드예시
@MappedSuperclass
public abstract class BaseEntity {
private String createdBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
}
@Entity
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME", nullable = false)
private String name;
...
}
테이블 결과
BaseEntity는 엔티티가 아니다.
테이블 생성 결과를 보면 테이블도 생성되어 있지 않다.
테이블 결과를 보면 BaseEntity에 있는 필드들은 MEMBER 테이블의 컬럼들로 추가되어 있다.
정리
@MappedSuperclass는 상속관계 매핑이 아니다.
@MappedSuperclass를 사용한 클래스는 엔티티가 아니어서 테이블과 매핑되지 않는다.
부모 클래스를 상속받는 자식 클래스에 매핑 정보만 제공한다.
@MappedSuperclass 타입으로는 조회, 검색 불가(em.find(BaseEntity) 불가)하다.
직접 생성해서 사용할 일이 없으므로 추상 클래스가 권장된다.
테이블과 관계없고,, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할이다.
주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.
참고: @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속 가능
JPA에서 extends를 쓸 때는 상속 대상이 @Entity 또는 @MappedSuperclass를 사용해야 한다.
상속 대상이 @Entity가 있을 때는 상속관계 매핑일 때 사용하고 @MappedSuperclass는 속성만 상속받을 때 사용한다.
참고
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본 편 강의를 보고 정리한 내용입니다.
'JPA > JPA 기본' 카테고리의 다른 글
9. 값 타입 (0) | 2024.08.12 |
---|---|
8. 프록시와 연관관계 관리 (0) | 2024.08.11 |
6. 다양한 연관관계 매핑 (0) | 2024.08.10 |
5. 연관관계 매핑 기초 (0) | 2024.08.10 |
4. 엔티티 매핑 (0) | 2024.08.09 |