JPA

고급 매핑

No-ah98 2022. 4. 5. 00:54

목차

    • 상속 관계 매핑
    • @MappedSuperclass
    • 실전 예제 - 4. 상속관계 매핑

상속관계 매핑

  • 객체는 상속관계 존재 O, 관계형 데이터베이스는 상속 관계 존재X
  • 그나마 슈퍼타입 서브타임 관계라는 모델링 기법이 객체 상속과 유사
  • 상속관계 매핑 : 객체의 상속과 구조와 DB의 슈퍼타입 서브타입 관계를 매핑 

논리 모델 VS 물리 모델

슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법

  • 객체는 상속을 지원하므로 모델링과 구현이 같지만, DB는 상속을 지원하지 않기 때문에 논리 모델을 물리 모델로 구현할 방법이 필요하다.
  • 구현 방법 
    • 주요 어노테이션
      • @Inheritance(stategy=InheritanceType.xxxx)의 stategy를 부모 클래스에서 설정해주면 됨
        • InheritanceType 종류
          • JOINED 
          • SINGLE_TABLE (default)
          • TABLE_PER_CLASS 
      • @DiscriminatorColumn(name ="DTYPE")
        • 부모 클래스에 선언한다. 하위 클래스를 구분하기 위해 사용한다.
        • @DiscriminatorValue("XXX")
          • 하위클래스에 선언한다. 엔티티 저장 시, 슈퍼타입 구분 컬럼에 저장할 값을 지정한다.
          • 어노테이션을 선언하지 않으면 클래스 이름이 기본값이다.
    • 참고※) JPA에서는 아래 방법 3가지 중 어느 방식을 선택하든 매핑이 가능하도록 지원한다.
    • 각각 테이블로 변환 -> 조인 전략
    • 통합 테이블로 변환 -> 단일 테이블 전략
    • 서브타입 테이블로 변환 -> 구현 클래스마다 테이블 전략 ​

객체의 상속관계 구현 

Item(부모 클래스)

@Entity
@Inheritance(strategy = InheritanceType.XXX) // 상속 구현 전략 선택
public class Item {

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

    private String name;
    private int price;
}

Album(자식 클래스)

@Entity
public class Album extends Item {

    private String artist;
}

 

Moive(자식 클래스)

@Entity
public class Movie extends Item {

    private String director;
    private String actor;
}

Book(자식 클래스)

@Entity
public class Book extends Item {

    private String author;
    private String isbn;
}

조인 전략

  • 상속관계 매핑에서 "정석"이라고 생각하면 된다. 보통 비즈니스적으로 중요하고 복잡할 때 사용한다.
  • NAME, PRICE가 공통 속성으로 ITEM 테이블에만 저장되고, 자식 테이블은 각자 데이터만을 저장한다.
  • 장점
    • 테이블 정규화
    • 외래 키 참조 무결성 제약조건 활용가능
    • 저장공간 효율화
  • 단점
    • 조회시 조인을 많이 사용, 성능 저하
    • 조회 쿼리가 복잡함
    • 데이터 저장 시 INSERT SQL 2번 호출
  • Item 엔티티 - @Ingeritance(strategy = InheritanceType.JOINED) 전략
    • 하이버네이트의 조인 전략에서는 @DiscriminatorColumn을 선언하지 않으면 DTYPE 칼럼이 생성되지 않는다.
    • 하지만 조인하면 앨범인지 무비인지 알 수 있다. 그래도 DTYPE을 넣어서 명확하게 해주는게 낫다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn // 하위 테이블의 구분 컬럼 생성(default = DTYPE)
public class Item {

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

    private String name;
    private int price;
}

실제 실행된 DDL 

  • 테이블 4개 생성 (ITEM, ALBUM, BOOK, MOVIE)
  • 하위 테이블에 FK 생성, 하위 테이블은 ITEM_ID가 PK이면서 FK로 잡아야 한다.
  • 조인 전략에 맞는 테이블들이 생성됨.
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 Item (
       DTYPE varchar(31) not null,
        id bigint generated by default as identity,
        name varchar(255),
        price integer not null,
        primary key (id)
    )
Hibernate: 
    create table Movie (
       actor varchar(255),
        director varchar(255),
        id bigint not null,
        primary key (id)
    )
    
    
Hibernate: 
    alter table Album 
       add constraint FKcve1ph6vw9ihye8rbk26h5jm9 
       foreign key (id) 
       references Item
Hibernate: 
    alter table Book 
       add constraint FKbwwc3a7ch631uyv1b5o9tvysi 
       foreign key (id) 
       references Item
Hibernate: 
    alter table Movie 
       add constraint FK5sq6d5agrc34ithpdfs0umo9g 
       foreign key (id) 
       references Item

Movie 객체 생성했을 때

  • Insert 쿼리가 2개 나간다.
  • Item 테이블, Movie 테이블 저장.
  • DTYPE에 클래스 이름이 디폴트로 저장 
  • 참고) 조회할 때는 innert join을 통해서 조회한다.
Movie movie = new Movie();
movie.setDirector("DirectorA");
movie.setActor("ActorB");
movie.setName("title");
movie.setPrice(10000);

em.persist(movie); //영속성

tx.commit(); // 트랙잭션 커밋
Hibernate: 
    /* insert advancedmapping.Movie
        */ insert 
        into
            Item
            (id, name, price, DTYPE) 
        values
            (null, ?, ?, 'Movie')
Hibernate: 
    /* insert advancedmapping.Movie
        */ insert 
        into
            Movie
            (actor, director, id) 
        values
            (?, ?, ?)

단일 테이블 전략(SINGLE_TABLE)

  • 논리 모델을 한 테이블로 합쳐버림
    • "DTYPE"으로 구분 
    • -> @DiscriminatorColumn 필수 (디폴트로 설정 되어있음)
  • 서비스 규모가 크지 않고, 굳이 조인 전략을 선택해서 복잡하게 갈 필요 없이 테이블을 단순하게 할 때 사용
  • 장점
    • 한 테이블에 다 저장하기 때문에 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다
    • 조회 쿼리가 단순하다.
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 null 허용
      • 데이터 무결성 위반
    • 한 테이블에 모두 저장하므로 테이블이 커질 수 있다. 상황에 따라 조회 성능이 느려질 수 있다.
@Entity
@DiscriminatorColumn
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Item {

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

    private String name;
    private int price;
}

실행된 DDL

  • 통합 테이블이 하나 생성된다.
Hibernate: 
    create table Item (
       DTYPE varchar(31) not null,
        id bigint generated by default as identity,
        name varchar(255),
        price integer not null,
        artist varchar(255),
        author varchar(255),
        isbn varchar(255),
        actor varchar(255),
        director varchar(255),
        primary key (id)
    )

조인 전략에서 실습했던 Movie 저장, 조회 예제를 그대로 돌려보면

  • 한 테이블에 있으므로 Item 테이블을 조회한다. 이때, DTYPE을 검색 조건으로 추가해서 조회한다.
Hibernate: 
    select
        movie0_.id as id2_0_0_,
        movie0_.name as name3_0_0_,
        movie0_.price as price4_0_0_,
        movie0_.actor as actor8_0_0_,
        movie0_.director as director9_0_0_ 
    from
        Item movie0_ 
    where
        movie0_.id=? 
        and movie0_.DTYPE='Movie'

구현 클래스마다 테이블 전략(TABLE_PER_CLASS)

  • 이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천 X
  • 슈퍼 타입의 컬럼들을 서브 타입으로 내린다. 즉, NAME, PRICE 컬럼이 중복되도록 허용하는 전략이다.
  • 구현 클래스마다 테이블을 생성한다. (슈퍼타입 클래스 생성 X)
  • 슈퍼타입 클래스는 실제 생성되는 테이블이 아니므로 abstract 클래스여야 하고, 테이블을 구분해줄 DTYPE이 필요 없기 때문에 @DiscriminatorColumn도 필요가 없어진다.
  • 참고) @Inheritance의 TABLE_PER_CLASS와 @Id의 생성 전략 GenerationType.IDENTITY를 같이 사용하면 에러 발생
    • @Id의 GenerationType을 TABLE 타입으로 변경 적용해서 해결. 그리고 시퀀스 테이블 생성을 방지하기 위해 이것에 대한 매핑까지 추가해주자
  • 문제점
    • 객체지향 프로그래밍에서는 MOVIE, ALBUM, BOOK 객체를 ITEM 타입으로도 조회할 수 있기 때문에 조회 시 union all로 전체 하위 테이블을 다 찾는다. 굉장히 비효율적으로 동작.

상속관계 매핑 정리

  • 조인 전략 
    • 장점
      • 테이블 정규화 되어있음
      • 외래 키 참조 무결성 제약조건 활용가능
      • 저장공간 효율화
    • 단점
      • 조회시 조인을 많이 사용, 성능 저하 (실제로는 크게 차이 없음)
      • 조회 쿼리가 복잡함
      • 데이터 저장시 INSERT SQL 2번 호출 
    • 정리
      • 단점으로 성능 저하가 있지만 실제로 크게 차이가 없다.
      • 저장공간이 효율화 된다.
      • 기본적으로 조인 정략이 정석이다. 설계가 깔끔하게 나온다
      • 하지만 테이블이 단순 + 데이터 양이 적음 + 확장 가능성 X -> 단일 테이블 전략이 더 좋은 선택이다.
  • 단일 테이블 전략 
    • 장점
      • 테이블이 하나기 때문에 조인이 필요 없으므로 조회 성능이 빠르다
      • 조회 쿼리가 단순하다.
    • 단점
      • 자식 엔티티가 매핑한 컬럼은 모두 NULL 허용 -> 데이터 무결성 위반
      • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
      • 상황에 따라서 조회 성능이 오히려 느질 수 있다.
        • 이 상황에 해당하는 임계점을 넘을 일이 많지 않다. 
  • 구현 클래스마다 테이블 전략
    • 데이터 베이스 설계자와 ORM 전문가 둘다 추천 X -> 쓰지 말자.
    • 장점 
      • 서브 타입을 명확하게 구분해서 처리할 때 효과적이다.
      • NOT NULL 제약조건 사용할 수 있다.
    • 단점
      • 여러 자식 테이블을 조회할 때 성능이 느리다(UNION SQL 필요)
      • 자식 테이블을 통합해서 쿼리하기 어려움

@MappedSuperclass

  • 객체 입장에서 공통 매핑 정보가 필요할 때 사용 ( ex. id, name은 객체 입장에서 계속 나옴)
    • 공통 속성을 상속받아서 사용하고싶을 때 사용
  • 상속관계 매핑으로 착각할 수 있는데 아니다.
  • 엔티티 X, 테이블과 매핑 X 
  • 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공
  • 조회, 검색 불가(em.find(BaseEntity)불가)
  • 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장
  • 겍체 입장에서 나온 어노테이션 이기 때문에 DB 테이블과는 상관없다. 
    • 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할
  • 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용
  • 참고: @Entity 클래스는 엔티티(상속 관계 매핑)나 @MappedSuperclass로 지정한 클래스만 상속 가능
  • 참고2 : 실무에서는 누가 수정했는지 파악하기 위해 기본적으로 사용한다.

코드로 이해하기

  • 상황 : 생성자, 생성시간, 수정자, 수정시간을 모든 엔티티에 공통으로 포함
  • 아래와 같이 BaseEntity를 정의해서 활용 
  • 매핑정보만 상속받는 Superclass라는 의미의 @MappedSuperclass 어노테이션 선언

BaseEntity

@Getter
@Setter
@MappedSuperclass
public abstract class BaseEntity {

    private String createdBy;

    private LocalDateTime createdDate;

    private String lastModifiedBy;

    private LocalDateTime lastModifiedDate;
}

Member (BaseEntity 상속)

@Entity
public class Member extends BaseEntity {
    ...
}

실행된 DDL ( BaseEntity에 선언된 컬럼들이 생성 )

Hibernate: 
    create table Member (
       id bigint generated by default as identity,
        createdBy varchar(255),
        createdDate timestamp,
        lastModifiedBy varchar(255),
        lastModifiedDate timestamp,
        age integer,
        description clob,
        roleType varchar(255),
        name varchar(255),
        locker_id bigint,
        team_id bigint,
        primary key (id)
    )
Hibernate: 
    create table Team (
       id bigint generated by default as identity,
        createdBy varchar(255),
        createdDate timestamp,
        lastModifiedBy varchar(255),
        lastModifiedDate timestamp,
        name varchar(255),
        primary key (id)
    )
...