[JPA] 고급매핑 - 상속 관계 , 매핑 정보 상속 | [JPA] Advanced Mapping - Inheritance Relationships, Mapped Superclass
인프런에서 에서 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편을 듣고 쓴 정리 글입니다.
https://www.inflearn.com/course/ORM-JPA-Basic
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 서버 데이터베이스 프레임워크 및 라이브러리 프로그래밍 언어 서비스 개발 Java JPA 스프링 데이터 JPA 온라인 강의
www.inflearn.com
평소에 Spring Data JPA 를 썼는데, 김영한님은 JPA 자체를 강의하시더라구요.
김영한님 강의 바탕으로 Spring Data JPA로 강의 소스를 테스트해보고 개념을 기록하기 위해 포스팅을 하게되었습니다.
고급 매핑
1. 상속관계 매핑
- 관계형 데이터베이스는 상속 관계X
- 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사
- 상속관계 매핑: 객체의 상속과 구조와 DB의 슈퍼타입 서브타입 관계를 매핑
- 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법
- 각각 테이블로 변환 -> 조인 전략
- 통합 테이블로 변환 -> 단일 테이블 전략
- 서브타입 테이블로 변환 -> 구현 클래스마다 테이블 전략
테이블은 여러개의 모델링이 나오지만, 객체는 상속관계라는 1개의 개념이다.
객체관계는 같지만 DB설계를 다르게 할 수 있음
- 관계형 데이터베이스는 상속 관계 X
- 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사
- 상속관계 매핑 : 객체의 상속, 구조와 DB의 슈퍼타입 서브타입 관계를 매핑
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorColumn
@Getter
public abstract class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue("Book")
public class Book extends Item {
private String author;
private String isbn;
@Builder
public Book(String name, int price, String author, String isbn) {
super(name, price);
this.author = author;
this.isbn = isbn;
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue("Album")
public class Album extends Item{
private String artist;
@Builder
public Album(String name, int price, String artist) {
super(name, price);
this.artist = artist;
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue("Movie")
public class Movie extends Item {
private String actor;
private String director;
@Builder
public Movie(String name, int price, String actor, String director) {
super(name, price);
this.actor = actor;
this.director = director;
}
}
[Repository 코드]
public interface ItemRepository<T extends Item> extends JpaRepository<T, Long> {}
public interface BookRepository extends JpaRepository<Book, Long> {}
public interface AlbumRepository extends JpaRepository<Album, Long> {}
public interface MovieRepository extends JpaRepository<Movie, Long> {}
이때 ItemRepository extends 를 꼭 기억하자!! [abstract class jpaRepository 상속법]
ItemRepository만 사용해도 Book, Album, Movie를 모두 가져올 수 있다. (type casting 사용해서)
[테스트 코드]
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class ItemTest {
@Autowired
ItemRepository itemRepository;
@Autowired
EntityManager entityManager;
@Before
public void setUp() throws Exception {
Movie movie = Movie.builder()
.actor("맷데이먼")
.director("리들리스콧")
.name("마션")
.price(10000)
.build();
Book book = Book.builder()
.author("조영호")
.isbn("isbn")
.name("객체지향의 사실과 오해")
.price(10000)
.build();
Album album = Album.builder()
.artist("엔플라잉")
.name("야호")
.price(30000)
.build();
itemRepository.save(movie);
itemRepository.save(book);
itemRepository.save(album);
entityManager.clear();
}
@Test
public void Item의_서브클래스_객체들_casting으로_가져오기() {
Movie movie = (Movie) itemRepository.findAll().get(0);
Book book = (Book) itemRepository.findAll().get(1);
Album album = (Album) itemRepository.findAll().get(2);
assertThat(movie.getName()).isEqualTo("마션");
assertThat(book.getName()).isEqualTo("객체지향의 사실과 오해");
assertThat(album.getArtist()).isEqualTo("엔플라잉");
}
}
1-0. 주요 어노테이션
- @Inheritance(strategy = InheritanceType.XXX) = default: SINGLE_TABLE
- JOINED : 조인 전략
- SINGLE_TABLE : 단일 테이블 전략
- TABLE_PER_CLASS : 구현 클래스마다 테이블 전략
- @DiscriminatorColumn(name = "DTYPE") = default: DTYPE DTYPE이라는 Column이 super class의 table에 생기고,
DTYPE의 값은 sub class의 이름으로 지정된다. SingleTable 전략에서 없어도 DTYPE 이생성되기도 하는데, 그래도 운영상 써주자 - @DiscriminatorValue("XXX") = default: classname
[예시]
@Inheritance(strategy = InheritanceType.JOIN)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item{}
@DiscriminatorValue("ALBUM_TYPE")
public class Album extends Item{}
@DiscriminatorValue("BOOK_TYPE")
public class Book extends Item{}
@DiscriminatorValue("MOVIE_TYPE")
public class Movie extends Item{}
DB 설계를 바꿨는데도 코드를 많이 수정하지 않아도 된다!! : JPA의 큰 장점!!
Join이 성능이 안나오네 -> singletable로 고치자!!
: query를 사용하면 코드를 많이 바꿔야함 근데 JPA사용하면 바꾸는게 엄청 쉽다.
1-1. 조인전략
데이터를 가져올 때 JOIN을 이용해서 가져온다.
insert는 두번 ITEAM ALBUM
select는 PK, FK를 이용해서 JOIN해서 가져온다.
abstract class에는 type을 컬럼을 두어서 구분한다.
@Inheritance(strategy = InheritanceType.JOIN)
@DiscriminatorColumn
public abstract class Item{}
1-1-1. 장점
- 테이블 정규화
- 외래 키 참조 무결성 제약조건 활용 가능
- 저장공간 효율화
1-1-2. 단점
- 조회시 조인을 많이 사용, 성능 저하
- 조회 쿼리가 복잡함
- 데이터 저장시 INSERT SQL 2번 호출
조인 성능이 생각보다 치명적이진 않고, 오히려 저장공간이 더 효율적일 수도 있음
그래도 단일 테이블 전략과 비교했을 때 단점이다!
조인이 정규화도 되고 객체랑도 잘 맞고 설계 입장에서 잘 맞아 떨어진다.
1-2. 단일 테이블 전략 - 기본 전략
subclass 의 모든 멤버변수를 테이블의 컬럼으로 가져온다.
insert도 한번에 되고, select도 한번에 되니까 아무래도 성능이 나오지!
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract class Item{}
1-2-1. 장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
- 조회 쿼리가 단순함
1-2-2. 단점
- 자식 엔티티가 매핑한 컬럼은 모두 null 허용
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있고 상황에 따라서 조회 성능이 오히려 느려질 수 있다. NULL 조건이 데이터 무결성 입장에서 애매하다. ALBUM 저장하면 > Book, Movie 관련 column이 모두 null이 되어야한다. 조회 성능을 문제시 하려면 임계점을 넘어야하는데 보통은 없음
1-3. 구현 클래스마다 테이블 전략
subclass 자체를 테이블로 만든다 + superclass의 멤버변수도 포함해서!
superclass를 아예 없애버리고, table을 subclass 기준으로 만든 후,
superclass의 멤버변수도 같이 포함하게 한다.
Item table 자체가 존재하지 않고, Movie, Book, Album table만 존재한다.
@DiscriminatorColumn의 의미가 없다! (없어도 된다.)
단순하게 값을 넣고 뺄 때는 좋은데, 이외의 경우에는 세 개 테이블을 모두 찾아봐서 쿼리가 복잡하게 나간다.
ex ) Item id가 5번이라고 할 때!
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item{}
이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 싫어하는 전략임!
1-3-1. 장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적
- Not Null 제약조건 사용가능
1-3-2. 단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느림 (UNION SQL)
- 자식 테이블을 통합해서 쿼리하기 어려움
2. @MappedSuperclass - 매핑 정보 상속
- 공통 매핑 정보가 필요할 때 사용한다. (ex : baseTimeEntity 같은 것)
위에서 말한 상속 관계 매핑에서 테이블까지 고민하기 싫음.
DB는 따로 쓰되, 객체입장에서 속성만 상속 받아서 쓰고 싶을때!
@MappedSuperclass
public abstract class BaseEntity {
private String createdBy;
private LocalDateTime createdDate;
private String lastModifedBy;
private LocalDateTime lastModifiedDate;
}
@Entity
public class Member extends BaseEntity{ ... }
@Entity
public class Team extends BaseEntity{ ... }
매핑 정보만 받는 슈퍼 클래스로 하고싶다면
- extends 로 클래스 설정하기
- @MappedSuperclass 어노테이션 추가하기.
그냥 속성을 같이 쓰고 싶을 때 사용한다!!
@Column(name = "CREATED_BY") // 이런식으로 column 설정도 충분히 가능하다.
private String createdBy;
JPA의 이벤트 기능으로 아예 어노테이션으로 시간, auth 정보를 편리하게 만들어 버릴 수 있다.
- 상속관계 매핑 X
- 엔티티X, 테이블과 매핑X (@Entity 안붙였다.)
- 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공
- 조회, 검색 불가(em.find(BaseEntity)불가) em.find(BaseEntity.class, 1L); 불가능
- 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장
- 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할
- 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용
- 참고 : @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속가능하다.
@MappedSuperclass //매핑 정보 상속
public abstract class BaseEntity{...}
@Entity //상속 관계 매핑
public abstract class Item extends BaseEntity{...}
@Entity
public class Album extends Item{...}
This is a summary post written after taking Kim Young-han's Java ORM Standard JPA Programming - Basics course on Inflearn.
https://www.inflearn.com/course/ORM-JPA-Basic
Java ORM Standard JPA Programming - Basics - Inflearn
For those who are new to JPA or use JPA in practice but lack foundational theory — build a solid understanding of JPA basics so that even beginners can confidently use JPA in real-world projects. Beginner Web Development Server Database Frameworks & Libraries Programming Languages Service Development Java JPA Spring Data JPA Online Course
www.inflearn.com
I've been using Spring Data JPA, but Kim Young-han actually teaches JPA itself.
Based on his lectures, I'm writing this post to document concepts while testing the course source code with Spring Data JPA.
Advanced Mapping
1. Inheritance Mapping
- Relational databases do NOT have inheritance
- The supertype-subtype modeling technique is similar to object inheritance
- Inheritance mapping: Mapping between object inheritance structure and DB supertype-subtype relationships
- Ways to implement a supertype-subtype logical model into a physical model:
- Convert to separate tables -> Joined strategy
- Convert to a single table -> Single table strategy
- Convert to subtype tables -> Table-per-class strategy
Tables can result in multiple modeling approaches, but on the object side, there's only one concept: inheritance.
The object relationships stay the same, but the DB design can vary.
- Relational databases do NOT have inheritance
- The supertype-subtype modeling technique is similar to object inheritance
- Inheritance mapping: Mapping between object inheritance/structure and DB supertype-subtype relationships
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorColumn
@Getter
public abstract class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue("Book")
public class Book extends Item {
private String author;
private String isbn;
@Builder
public Book(String name, int price, String author, String isbn) {
super(name, price);
this.author = author;
this.isbn = isbn;
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue("Album")
public class Album extends Item{
private String artist;
@Builder
public Album(String name, int price, String artist) {
super(name, price);
this.artist = artist;
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue("Movie")
public class Movie extends Item {
private String actor;
private String director;
@Builder
public Movie(String name, int price, String actor, String director) {
super(name, price);
this.actor = actor;
this.director = director;
}
}
[Repository Code]
public interface ItemRepository<T extends Item> extends JpaRepository<T, Long> {}
public interface BookRepository extends JpaRepository<Book, Long> {}
public interface AlbumRepository extends JpaRepository<Album, Long> {}
public interface MovieRepository extends JpaRepository<Movie, Long> {}
Make sure to remember the ItemRepository extends part!! [How to extend JpaRepository for an abstract class]
You can retrieve Book, Album, and Movie all through ItemRepository alone. (using type casting)
[Test Code]
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class ItemTest {
@Autowired
ItemRepository itemRepository;
@Autowired
EntityManager entityManager;
@Before
public void setUp() throws Exception {
Movie movie = Movie.builder()
.actor("맷데이먼")
.director("리들리스콧")
.name("마션")
.price(10000)
.build();
Book book = Book.builder()
.author("조영호")
.isbn("isbn")
.name("객체지향의 사실과 오해")
.price(10000)
.build();
Album album = Album.builder()
.artist("엔플라잉")
.name("야호")
.price(30000)
.build();
itemRepository.save(movie);
itemRepository.save(book);
itemRepository.save(album);
entityManager.clear();
}
@Test
public void Item의_서브클래스_객체들_casting으로_가져오기() {
Movie movie = (Movie) itemRepository.findAll().get(0);
Book book = (Book) itemRepository.findAll().get(1);
Album album = (Album) itemRepository.findAll().get(2);
assertThat(movie.getName()).isEqualTo("마션");
assertThat(book.getName()).isEqualTo("객체지향의 사실과 오해");
assertThat(album.getArtist()).isEqualTo("엔플라잉");
}
}
1-0. Key Annotations
- @Inheritance(strategy = InheritanceType.XXX) = default: SINGLE_TABLE
- JOINED: Joined strategy
- SINGLE_TABLE: Single table strategy
- TABLE_PER_CLASS: Table-per-class strategy
- @DiscriminatorColumn(name = "DTYPE") = default: DTYPE A column called DTYPE is created in the superclass table,
and the DTYPE value is set to the subclass name. In the SingleTable strategy, DTYPE may be generated even without this annotation, but you should still include it for production use. - @DiscriminatorValue("XXX") = default: classname
[Example]
@Inheritance(strategy = InheritanceType.JOIN)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item{}
@DiscriminatorValue("ALBUM_TYPE")
public class Album extends Item{}
@DiscriminatorValue("BOOK_TYPE")
public class Book extends Item{}
@DiscriminatorValue("MOVIE_TYPE")
public class Movie extends Item{}
Even when the DB design changes, you barely need to modify the code!! This is a huge advantage of JPA!!
Join performance isn't cutting it -> Let's switch to single table!!
With raw queries, you'd have to change a lot of code, but with JPA it's super easy to switch.
1-1. Joined Strategy
When fetching data, it uses JOINs to retrieve it.
Insert happens twice — once for ITEM, once for ALBUM.
Select uses PK and FK to JOIN and fetch the data.
The abstract class has a type column to distinguish between subtypes.
@Inheritance(strategy = InheritanceType.JOIN)
@DiscriminatorColumn
public abstract class Item{}
1-1-1. Pros
- Table normalization
- Can leverage foreign key referential integrity constraints
- Efficient storage space
1-1-2. Cons
- Heavy use of joins during queries, potential performance degradation
- Complex query statements
- INSERT SQL is called twice when saving data
Join performance isn't as fatal as you might think, and it can actually be more storage-efficient.
Still, these are disadvantages compared to the single table strategy!
Joins provide normalization, align well with objects, and fit nicely from a design perspective.
1-2. Single Table Strategy - Default Strategy
All member variables of subclasses become columns in a single table.
Insert happens in one shot, select happens in one shot — so naturally the performance is better!
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract class Item{}
1-2-1. Pros
- No joins needed, so query performance is generally fast
- Simple query statements
1-2-2. Cons
- All columns mapped by child entities must allow null
- Since everything is stored in a single table, the table can get large and in some cases query performance may actually get slower. NULL conditions are awkward from a data integrity standpoint. When you save an ALBUM > all Book and Movie related columns must be null. For query performance to become a real problem, you'd need to cross a threshold, which usually doesn't happen.
1-3. Table-per-Class Strategy
Each subclass becomes its own table — including the superclass's member variables!
The superclass table is completely eliminated, tables are created based on subclasses,
and they include the superclass's member variables as well.
The Item table itself doesn't exist — only Movie, Book, and Album tables exist.
@DiscriminatorColumn has no meaning here! (You don't need it.)
It's fine for simple inserts and retrieves, but for anything else, it has to search all three tables resulting in complex queries.
e.g.) When the Item id is 5!
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item{}
This is a strategy that both database designers and ORM experts dislike!
1-3-1. Pros
- Effective when you need to clearly distinguish and handle subtypes
- Can use NOT NULL constraints
1-3-2. Cons
- Slow performance when querying multiple child tables together (UNION SQL)
- Difficult to write unified queries across child tables
2. @MappedSuperclass - Inheriting Mapping Information
- Used when common mapping information is needed. (e.g., something like BaseTimeEntity)
You don't want to think about table design like in inheritance mapping above.
You want the DB tables to be separate, but from the object perspective, you just want to inherit the attributes!
@MappedSuperclass
public abstract class BaseEntity {
private String createdBy;
private LocalDateTime createdDate;
private String lastModifedBy;
private LocalDateTime lastModifiedDate;
}
@Entity
public class Member extends BaseEntity{ ... }
@Entity
public class Team extends BaseEntity{ ... }
If you want a superclass that only provides mapping information:
- Set up the class with extends
- Add the @MappedSuperclass annotation.
Use it when you simply want to share attributes!!
@Column(name = "CREATED_BY") // 이런식으로 column 설정도 충분히 가능하다.
private String createdBy;
With JPA's event features, you can conveniently create time and auth information using annotations alone.
- NOT inheritance mapping
- NOT an entity, NOT mapped to a table (@Entity is not applied.)
- Only provides mapping information to child classes that inherit from the parent class
- Cannot be queried or searched (em.find(BaseEntity) is not possible) em.find(BaseEntity.class, 1L); is not possible
- Since you'll never instantiate it directly, abstract class is recommended
- Has no relation to tables — it simply gathers mapping information commonly used by entities
- Mainly used to collect information like created date, modified date, created by, modified by that applies commonly across all entities
- Note: @Entity classes can only extend entities or classes annotated with @MappedSuperclass.
@MappedSuperclass //매핑 정보 상속
public abstract class BaseEntity{...}
@Entity //상속 관계 매핑
public abstract class Item extends BaseEntity{...}
@Entity
public class Album extends Item{...}
댓글
Comments