개요
객체에는 상속이라는 개념이 있지만 관계형 데이터베이스에는 상속관계가 없습니다.
객체지향 언어인 Java를 사용하는 프레임워크에서 JPA를 적용하기 위해서는 객체 상속과 유사한 구조를 구현해야 하는데 마침 RDBMS는 객체 상속과 유사한 모델링 기법인 슈퍼타입/서브타입 구조를 제공합니다.
따라서, 이번 게시글에서는 객체의 상속, 구조와 DB의 슈퍼타입/서브타입 관계를 매핑하는 상속관계 매핑에 대해 알아보겠습니다.
1. 상속관계 매핑
상속관계 매핑은 슈퍼타입/서브타입 논리 모델을 실제 물리 모델로 구현하는 방법이며 아래와 같이 크게 3가지가 있습니다.
- 조인 전략 (JOINED)
- 싱글 테이블 전략 (SINGLE_TABLE)
- 구현 클래스마다 테이블 전략 (TABLE_PER_CLASS)
상속관계 매핑 관련된 주요 어노테이션으로는 아래와 같이 크게 3가지가 있습니다.
- @Inheritance(strategy = InheritanceType.OOO)
- 앞서 언급한 전략들 중 하나를 선택하는 어노테이션
- JOINED, SINGLE_TABLE, TABLE_PER_CLASS 중 하나 선택
- 명시 안할 경우 SINGLE_TABLE이 디폴트로 선택이 됨
- @DiscriminatorColumn(name = OOO)
- JOINED 전략의 경우 SINGLE_TABLE과 달리 각 객체마다 테이블이 생성됨
- 따라서 서브 클래스를 삽입할 때 슈퍼 클래스 테이블에도 삽입이 되는데 이때 어떤 서브 클래스의 Entity인지 표현하는 것이 운영적인 측면에서 좋음
- 따라서, @DiscriminatorColumn 어노테이션은 슈퍼클래스에서 선언
- 서브 클래스 엔티티를 명시하는 컬럼은 디폴트로 DTYPE이며 name 속성에 별도 컬럼명을 명시해줄 수 있음
- SINGLE_TABLE 전략 같은 경우 단일 테이블이므로 DTYPE 칼럼이 무조건 생김
- @DiscriminatorValue("OOO")
- @DiscriminatorColumn과 달리 서브클래스에 명시하는 어노테이션
- 슈퍼클래스의 @DiscriminatorColumn 칼럼 내 어떻게 저장할지 지정하는 어노테이션
- 디폴트는 객체명 그대로 삽입되며 별도로 명시할 경우 지정한 명칭대로 DTYPE 칼럼에 삽입됨
2. 조인 전략 (JOINED)
- 비즈니스적으로 중요하고 복잡할 경우 사용되는 테이블 전략이며 이상적으로 데이터를 관리할 수 있는 전략
- 장점
- 객체마다 테이블이 생성되므로 테이블 정규화가 잘 되어 있음
- 객체마다 테이블이 생성되므로 외래 키 참조 무결성 제약조건 활용 가능
- 저장공간이 분리되어있으므로 저장 공간을 효율적으로 활용 가능
- 단점
- 조회 시 Join을 너무 많이 할 경우 성능 저하 우려 (반정규화를 해야 할 수도 있음)
- 기본적으로 Join문을 쓰기 때문에 단일 테이블 전략에 비해 조회 쿼리가 복잡
- 데이터를 삽입할 때도 삽입 SQL을 2번 호출 (네트워크를 여러번 탐)
Superclass - Vehicle, Subclass - Car, Truck
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
@Inheritance(strategy = InheritanceType.JOINED) | |
@Getter | |
@Setter | |
@DiscriminatorColumn // DTYPE 칼럼이 생기고 @DiscriminatorValue명이 들어감 | |
public class Vehicle { | |
@Id | |
@GeneratedValue | |
private Long id; | |
private String brand; | |
private int price; | |
} | |
@Entity | |
@Getter | |
@Setter | |
@DiscriminatorValue("CAR") | |
public class Car extends Vehicle { | |
private String hasHipassYn; | |
private String isElectronicYn; | |
} | |
@Entity | |
@Getter | |
@Setter | |
@DiscriminatorValue("TRUCK") | |
public class Truck extends Vehicle { | |
private int cargoWeight; | |
private String source; | |
private String destination; | |
} |
Join을 통해 조회
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("hello"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Car car = new Car(); | |
car.setBrand("Hyundai"); | |
car.setPrice("30000000"); | |
car.hasHipassYn("Y"); | |
car.isElectronic("N"); | |
entityManager.persist(car); | |
// 영속성 컨텍스트 flush | |
entityManager.flush(); | |
entityManager.clear(); | |
// Join을 통해 조회 | |
/** | |
* select | |
* car0_.id as id1_2_0_, | |
* car0_1_.brand as brand2_2_0_, | |
* car0_1_.price as price3_2_0_, | |
* car0_.hasHipassYn as has_hipass_yn1_6_0_, | |
* car0_.isElectronicYn as is_electronic_yn2_6_0_ | |
* from | |
* car car0_ | |
* inner join | |
* Vehicle car0_1_ | |
* on car0_.id=car0_1_.id | |
* where | |
* movie0_.id=? | |
*/ | |
Car foundCar = entityManager.find(Car.class, car.getId()); | |
System.out.println("foundCar = " + foundCar); | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} |
3. 싱글테이블 전략 (SINGLE_TABLE)
- 테이블들을 분리하지 않고 하나의 테이블에 모든 칼럼을 저장하는 전략
- 장점
- 조인이 필요 없기 때문에 일반적으로 조회 성능이 더 빠름
- 조회 쿼리가 비교적 단순함
- JOINED 전략과 달리 INSERT문도 한 번만 실행
- 단점
- 자식 Entity가 매핑한 칼럼에 대해서는 모두 NULL 허용해줘야 함
- 단일 테이블에 모든 것을 저장하기 때문에 테이블 크기가 커질 경우 조회 성능이 오히려 느려질 수 있음
- 위와 같은 경우, 조회 조건에 맞는 테이블 파티셔닝을 진행할 경우 해결할 수 있는 문제
- 조회를 자주 하는 페이지일 경우 단일 테이블에 대해 테이블 파티셔닝을 진행한 후 조회하는 것이 JOINED 전략보다 유리할 수 있음
Superclass - Vehicle, Subclass - Car, Truck
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 단일 테이블 전략 | |
* create table Item ( | |
* DTYPE varchar(31) not null, | |
* id bigint not null, | |
* brand varchar(255), | |
* price integer not null, | |
* hasHipassYn varchar(255), | |
* isElectronicYn varchar(255), | |
* cargoWeight integer, | |
* source varchar(255), | |
* destination varchar(255), | |
* primary key (id) | |
* ) | |
*/ | |
@Entity | |
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) | |
@DiscriminatorColumn // 단일 테이블 전략의 경우 해당 어노테이션 없어도 생성됨 | |
@Getter | |
@Setter | |
public class Vehicle { | |
@Id | |
@GeneratedValue | |
private Long id; | |
private String brand; | |
private int price; | |
} | |
@Entity | |
@Getter | |
@Setter | |
public class Car extends Vehicle { | |
private String hasHipassYn; | |
private String isElectronicYn; | |
} | |
@Entity | |
@Getter | |
@Setter | |
public class Truck extends Vehicle { | |
private int cargoWeight; | |
private String source; | |
private String destination; | |
} |
* JPA의 장점: JOINED 전략에서 SINGLE_TABLE 전략으로 전환하더라도 코드 변화가 거의 없음 (테이블 Entity 전략만 변경해주면 됨)
* 따라서, 삽입, 조회하는 코드는 JOINED 전략과 완전 동일
4. 구현 클래스마다 테이블 전략 (TABLE_PER_CLASS)
- JPA에서 지원을 하지만 결론부터 말하자면 사용하면 안 되는 전략
- 해당 전략은 슈퍼 클래스의 테이블은 생성되지 않으며 서브 클래스의 테이블들만 생성이 됨
- 서브 클래스 테이블들끼리 연결을 지을 수 있는 칼럼이 없기 때문에 조회 쿼리를 작성하기 어려우며 여러 자식 테이블을 함께 조회할 때 성능이 느린 UNION SQL을 통해 조회를 해야 함
- 따라서, DBA와 개발자 간 trade off를 고려한 뒤, JOINED 전략을 선택할 것인지, SINGLE_TABLE 전략을 선택할 것인지 정하는 것을 추천
Superclass - Vehicle, Subclass - Car, Truck
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) | |
@Getter | |
@Setter | |
public abstract class Vehicle { | |
@Id | |
@GeneratedValue | |
private Long id; | |
private String brand; | |
private int price; | |
} | |
@Entity | |
@Getter | |
@Setter | |
public class Car extends Vehicle { | |
private String hasHipassYn; | |
private String isElectronicYn; | |
} | |
@Entity | |
@Getter | |
@Setter | |
public class Truck extends Vehicle { | |
private int cargoWeight; | |
private String source; | |
private String destination; | |
} |
UNION SQL을 통한 조회
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("hello"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Car car = new Car(); | |
car.setBrand("Hyundai"); | |
car.setPrice("30000000"); | |
car.hasHipassYn("Y"); | |
car.isElectronic("N"); | |
entityManager.persist(car); | |
// 영속성 컨텍스트 flush | |
entityManager.flush(); | |
entityManager.clear(); | |
// UNION을 통해 조회 | |
/** | |
* Vehicle.class로 찾을 경우 UNION ALL로 다 찾아서 성능이 느림 | |
* select | |
* vehicle0_.id as id1_2_0_, | |
* vehicle0_.brand as brand2_2_0_, | |
* vehicle0_.price as price3_2_0_, | |
* vehicle0_.hasHipassYn as has_hipass_yn1_6_0_, | |
* vehicle0_.isElectronicYn as is_electronic_yn2_6_0_, | |
* vehicle0_.cargoWeight as cargo_weight1_0_0_, | |
* vehicle0_.source as source2_0_0_, | |
* vehicle0_.destination as destination3_0_0_, | |
* vehicle0_.clazz_ as clazz_0_ | |
* from | |
* ( select | |
* id, | |
* brand, | |
* price, | |
* hasHipassYn, | |
* isElectronicYn, | |
* null as cargo_weight, | |
* null as source, | |
* null as destination, | |
* 1 as clazz_ | |
* from | |
* Car | |
* union | |
* all select | |
* id, | |
* brand, | |
* price, | |
* null as has_hipass_yn, | |
* null as is_electronic_yn, | |
* cargoWeight, | |
* source, | |
* destination, | |
* 2 as clazz_ | |
* from | |
* Truck | |
* ) vehicle0_ | |
* where | |
* vehicle0_.id=? | |
*/ | |
Vehicle vehicle = entityManager.find(Vehicle.class, car.getId()); | |
System.out.println("foundCar = " + foundCar); | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} |
정리
- 정리를 하자면, 세 가지의 테이블 전략 중 사용할 수 있는 전략은 JOINED와 SINGLE_TABLE 뿐
- 관리하기 편한 방식은 SINGLE_TABLE 전략이며 조회를 자주 할 경우 효율적
- 비즈니스적으로 중요하고 복잡할 경우 JOINED 전략을 추천
출처
자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한 강사님)
반응형
'DB > JPA' 카테고리의 다른 글
[JPA] 프록시와 연관관계 관리 정리 (0) | 2021.09.14 |
---|---|
[JPA] @MappedSuperclass (0) | 2021.09.07 |
[JPA] 다양한 연관관계 매핑 (0) | 2021.08.31 |
[JPA] 연관관계 매핑 간단 정리 (0) | 2021.08.28 |
[JPA] Entity 매핑 정리 (0) | 2021.08.23 |