JPA 상속 관계 매핑이란?

maketheworldwise·2023년 1월 25일


이 글의 목적?

이번 강의 챕터에서는 상속 관계 매핑과 관련된 내용을 학습했다. 처음에는 RDB에 상속 개념이 없는데 이게 가능한가? 생각했지만 강의를 보면서 "아~" 라는 감탄사와 함께 이해가 되었다. 정리해보자. ⭐️

상속 관계 매핑이란?

상속 관계 매핑이란 객체의 상속 구조와 DB의 슈퍼타입 및 서브타입 관계를 매핑하는 것을 의미한다. RDB에서 슈퍼타입 및 서브타입 모델을 구현하는 방법은 총 3가지가 있다. (생각해보면 그저 정확한 명칭을 몰랐을 뿐, 내가 예전에 한 번씩 사용했봤던 내용이다. ㅎㅎ 😂)

상속 관계 매핑에서 사용되는 주요 어노테이션은 다음과 같다. (나는 직접 어노테이션 코드를 열어서 확인해봤다! 😤)

  • @Inheritance(strategy = InheritanceType.xxx) : 상속 관계 전략 지정 (기본 단일 테이블)
  • @DiscriminatorColumn(name = "DTYPE") : 상위 테이블의 DTYPE 컬럼명 지정 (기본 DTYPE)
  • @DiscriminatorValue(value = "xxx") : 상위 테이블에 저장될 DTYPE 값 지정 (기본 엔티티)

조인 전략

조인 전략은 말 그대로 조인을 이용한 방법이다. 가장 정석적인 방법이며, 객체의 상속 관계를 RDB에서도 표현한 방법으로 봐도 될 것 같다.

@Entity
@Table(name = "ITEM")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "d_type")
public class Item {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  private String name;

  private int price;

  // Getter, Setter 생략
@Entity
@Table(name = "ALBUM")
@DiscriminatorValue(value = "A")
public class Album extends Item {

  private String artist;
  
  // Getter, Setter 생략
@Entity
@Table(name = "MOVIE")
@DiscriminatorValue(value = "M")
public class Movie extends Item {

  private String director;

  private String actor;
  
  // Getter, Setter 생략
@Entity
@Table(name = "BOOK")
@DiscriminatorValue(value = "B")
public class Book extends Item {

  private String author;

  private String isbn;
  
  // Getter, Setter 생략

테이블이 생성되는 쿼리를 확인해보면 아이템과 아이템의 PK를 가진 앨범, 영화, 책 테이블이 새로 생성되는 것을 확인할 수 있다. 그럼 데이터를 저장하고 조회할 때는 어떻게 동작할까?

try {
  // ITEM, MOVIE 테이블에 데이터 저장
  Movie movie = new Movie();
  movie.setDirector("A");
  movie.setActor("B");
  movie.setName("바람과함께사라지다");
  movie.setPrice(10000);

  entityManager.persist(movie);

  entityManager.flush();
  entityManager.clear();

  // 조인을 이용한 조회
  Movie findMovie = entityManager.find(Movie.class, movie.getId());
  System.out.println("findMovie = " + findMovie);

  entityTransaction.commit();
} catch (Exception e) {
  entityTransaction.rollback();
} finally {
  entityManager.close();
}

entityManagerFactory.close();

코드처럼 영화에 대한 데이터를 저장할 때는 아이템과 영화 테이블에 INSERT문 2개가 전달되고, 조회할 때는 아이템과 영화 테이블을 조인해서 정보를 가져오는 것을 확인할 수 있다.

동작 과정을 통해서 알 수 있는 정보들은 다음과 같다.

  • 테이블의 정규화를 그대로 표현한 방법
  • 외래키 참조 무결성 제약 조건 활용 가능
  • 저장 공간 효율화
  • 조회시 조인을 많이 사용하기에 성능 저하가 올 수 있음
  • 조회 쿼리가 복잡
  • 데이터 저장시 INSERT 2번 호출

단일 테이블 전략

단일 테이블 전략은 하나의 테이블에 모두 때려넣는 방법이다. 위의 조인 전략 코드에서 @Inheritance 어노테이션의 속성값만 변경해주면 되기에 코드는 생략하자. 😋

참고로 단일 테이블 전략에서는 @DiscriminatorColumn 어노테이션이 없어도 동작한다. 하나의 테이블에서 구분할 수 있는 기준이 반드시 필요하기 때문에 자동으로 등록해주기 때문이다.

단 하나의 테이블로 운영한다는 점에서 알 수 있는 정보들은 다음과 같다.

  • 조인이 필요없기에 일반적으로 조회 성능이 빠름
  • 조회 쿼리가 단순
  • 데이터 저장시 INSERT 1번 호출
  • 자식 엔티티가 매핑한 컬럼은 모두 NULL을 허용
  • 단일 테이블에 모든 것을 저장하기에 테이블이 커지면서 상황에 따라 성능 저하가 올 수 있음

클래스별 테이블 전략

클래스별 테이블 전략은 각각의 테이블로 아예 분리시켜 놓은 방법이다. @Inheritance 어노테이션의 속성값만 변경해주면 되며, 각 테이블이 독립적으로 만들어지기 때문에 구분 기준인 @DiscriminatorColumn 어노테이션이 불필요해진다.

클래스별 테이블 전략의 문제점을 코드로 확인해보자.

try {
  // ITEM, MOVIE 테이블에 데이터 저장
  Movie movie = new Movie();
  movie.setDirector("A");
  movie.setActor("B");
  movie.setName("바람과함께사라지다");
  movie.setPrice(10000);

  entityManager.persist(movie);

  entityManager.flush();
  entityManager.clear();

  // 부모 클래스 타입으로 조회
  Item findItem = entityManager.find(Item.class, movie.getId());
  System.out.println("findItem = " + findItem);

  entityTransaction.commit();
} catch (Exception e) {
  entityTransaction.rollback();
} finally {
  entityManager.close();
}

entityManagerFactory.close();

상속 관계에서 상위 클래스 타입으로 조회를 할 경우 UNION으로 엮인 쿼리가 전달되는 것을 확인할 수 있다. 독립적으로 구성된 테이블이기 때문에 원하는 데이터를 찾으려면 떨어져있는 테이블들을 모두 합쳐서 찾아야하기 때문이다. (물론 특정 테이블을 명시하여 조회를 할 경우에는 문제가 없다!)

이 전략의 특징은 다음과 같다.

  • DB 설계자와 ORM 개발자들 모두 비추천하는 전략 (생각만해도 불편하다.. 🤬)
  • 서브 타입을 명확하게 구분하여 처리할 때 효과적
  • NOT NULL 제약 조건 사용 가능
  • 하위 테이블을 함께 조회할 경우 성능 저하가 올 수 있음 (UNION)
  • 하위 테이블을 통합해서 쿼리하기 어려움

김영한님은 주로 조인 전략을 이용하시지만, 비즈니스적으로 복잡하지 않고 데이터가 단순한 형태일 경우에는 단일 테이블 전략을 사용한다고 하셨다. (클래스별 테이블 전략은 아예 거들떠보지도 않으신다고 하셨다. ㅎㅎ)

이 글의 레퍼런스

profile
세상을 현명하게 이끌어갈 나의 성장 일기 📓

0개의 댓글