[JPA] 엔티티 연관관계 매핑 - 3

Noah-wilson·2025년 1월 6일

JPA

목록 보기
8/10
post-thumbnail

상속관계 매핑

관계형 데이터 베이스는 상속관계가 없다.
하지만 슈퍼타입, 서브타입이라는 모델링 기법이 존재한다.
이를 이용하여 객체의 상속 관계와 RDB의 슈퍼타입, 서브타입 관계를 매핑할 수 있다.

상속 관계 매핑 방법

JPA는 상속 관계를 어떻게 매핑하는지 알아보기전에 기본 매핑전략으로 어떤 전략을 채용하는지 알아보자.

@Entity
public abstract class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;
}
@Entity
public class Book extends Item {

    private String author;
    private String isbn;
}
@Entity
public class Movie extends  Item{

    private String diresctor;
    private String actor;
}
@Entity
public class Album extends  Item{

    private String artist;
}

실행 결과:

Hibernate: 
    create table Item (
        price integer not null,
        id bigint not null,
        DTYPE varchar(31) not null,
        actor varchar(255),
        artist varchar(255),
        author varchar(255),
        diresctor varchar(255),
        isbn varchar(255),
        name varchar(255),
        primary key (id)
    )

JPA의 기본전략은 단일 테이블 방식인것을 확인할 수 있다.
JPA는 상속관계를 조인, 단일 테이블, 구현 클래스별 테이블 전략 3가지로 매핑할 수 있다.
이를 구현 하기 위한 애노테이션들은 다음과 같다.

@Inheritance(strategy=InheritanceType.XXX)
는 슈퍼 타입에서 매핑 전략을 구분하기 위한 애노테이션이다.

  • JOINED: 조인 전략
  • SINGLE_TABLE: 단일 테이블 전략
  • TABLE_PER_CLASS: 구현 클래스마다 테이블 전략

@DiscriminatorColumn()은 구분자 컬럼을 말한다.

@DiscriminatorValue(“XXX”)은 구분자 컬럼의 값을 정할 수 있다.

1. 조인 전략

@Inheritance(strategy = InheritanceType.JOINED)를 사용해서 구현 가능하다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;
}
@Entity
@DiscriminatorValue("M")
public class Movie extends  Item{

    private String diresctor;
    private String actor;

}
            Movie movie = new Movie();

            movie.setDiresctor("aaa");
            movie.setActor("bbb");
            movie.setName("JPA");
            movie.setPrice(10000);

            em.persist(movie);


            em.flush();
            em.clear();
            Movie findMove = em.find(Movie.class, movie.getId());
            System.out.println("findMove = " + findMove.getName());

실행 결과:

Hibernate: 
    create table Album (
        id bigint not null,
        artist 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),
        diresctor varchar(255),
        primary key (id)
    )
Hibernate: 
    create table Item (
        price integer not null,
        id bigint not null,
        DTYPE varchar(31) not null,
        name varchar(255),
        primary key (id)
    )
    
Hibernate: 
    /* insert for
        hellojpa.Movie */insert 
    into
        Item (name, price, DTYPE, id) 
    values
        (?, ?, 'M', ?)
Hibernate: 
    /* insert for
        hellojpa.Movie */insert 
    into
        Movie (actor, diresctor, id) 
    values
        (?, ?, ?)
Hibernate: 
    select
        m1_0.id,
        m1_1.name,
        m1_1.price,
        m1_0.actor,
        m1_0.diresctor 
    from
        Movie m1_0 
    join
        Item m1_1 
            on m1_0.id=m1_1.id 
    where
        m1_0.id=?

조인 전략은 외래키 참조 무결성 제약 조건을 활용하여 테이블을 정규화할 수 있기 때문에 저장공간을 효울적으로 사용할 수 있다.
하지만 데이터 조회시 조인을 하기 떄문에 상황에 따라 성능 문제가 생길수도 있다.(실무에서는 조일할 테이블이 많을것이므로 조회 쿼리가 복잡할수도 있다.)
또한 테이블이 정규화됨에 따라 슈퍼타입 테이블과 서브타입 테이블이 나눠져 있기 때문에 INSERT SQL이 2번 호출되기 성능 문제가 생길 수도 있다.

2. 단일 테이블 전략

@Inheritance(strategy = InheritanceType.SINGLE_TABLE) 사용
단일 테이블 전략에서는 DTYPE가 없으면 어떤 하위 객체인지 확인할 수 없으므로 기본적으로 DTYPE을 생성하므로 @DiscriminatorColumn가 없어도 DTYPE이 생성된다.


@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
//@DiscriminatorColumn
public abstract class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;
}
@Entity
@DiscriminatorValue("A")
public class Album extends  Item{

    private String artist;
}
            Album album = new Album();

            album.setArtist("JPA");
            em.persist(album);

            em.flush();
            em.clear();

            Album findAlum = em.find(Album.class, album.getId());
            System.out.println("findAlum.getArtist() = " + findAlum.getArtist());

실행결과:


Hibernate: 
    create table Item (
        price integer not null,
        id bigint not null,
        DTYPE varchar(31) not null,
        actor varchar(255),
        artist varchar(255),
        author varchar(255),
        diresctor varchar(255),
        isbn varchar(255),
        name varchar(255),
        primary key (id)
    )
Hibernate: 
    /* insert for
        hellojpa.Album */insert 
    into
        Item (name, price, artist, DTYPE, id) 
    values
        (?, ?, ?, 'A', ?)
Hibernate: 
    select
        a1_0.id,
        a1_0.name,
        a1_0.price,
        a1_0.artist 
    from
        Item a1_0 
    where
        a1_0.DTYPE='A' 
        and a1_0.id=?
findAlum.getArtist() = JPA

단일 테이블 전략은 기본적으로 조회할때 조인을 하지않으므로 조회 성능이 빠르고 단순하다.(조회 성능 임계점을 초과하지 않을떄까지) 하지만 자식 엔티티가 매핑한 컬럼중 쓰이지 않는 자식 엔티티들은 모두 null을 허용하는 문제가 있다.

3. 클래스별 테이블 전략

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 사용


@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {

    private String author;
    private String isbn;
}

```log
Hibernate: 
    create table Book (
        price integer not null,
        id bigint not null,
        author varchar(255),
        isbn varchar(255),
        name varchar(255),
        primary key (id)
    )
Hibernate: 
    /* insert for
        hellojpa.Book */insert 
    into
        Book (name, price, author, isbn, id) 
    values
        (?, ?, ?, ?, ?)
    Hibernate: 
    select
        b1_0.id,
        b1_0.name,
        b1_0.price,
        b1_0.author,
        b1_0.isbn 
    from
        Book b1_0 
    where
        b1_0.id=?
findBook.getAuthor() = aaa

클래스별 테이블 전략은 서브타입을 명확하게 구분해서 처리할수 있기때문에 null을 허용하지 않고 사용할 수 있다.
하지만 자식 테이블들을 통합해서 조회할 상황이 온다면 조회 성능도 느리고 통합해서 쿼리를 사용하기도 힘들다.
간단하게 생각해봐도 서브타입 테이블들에 대한 집계 연산을 할떄 어려움이 있으므로 실무에서 사용하기 어려운 전략인 것 같다.

@MappedSuperclass

@MappedSuperclass
public abstract class BaseEntity {

    private String createBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
}
@Entity
public class Member extends BaseEntity{
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;
}
            Member member = new Member();
            member.setUsername("JPA");
            member.setCreateBy("kim");
            member.setCreatedDate(LocalDateTime.now());

            em.persist(member);
            em.flush();
            em.clear();

            Member findMember = em.find(Member.class, member.getId());
            System.out.println("findMember.getCreateBy() = " + findMember.getCreateBy());

실행 결과:

Hibernate: 
    create table Member (
        MEMBER_ID bigint not null,
        TEAM_ID bigint,
        createdDate timestamp(6),
        lastModifiedDate timestamp(6),
        team_TEAM_ID bigint,
        USERNAME varchar(255),
        createBy varchar(255),
        lastModifiedBy varchar(255),
        primary key (MEMBER_ID)
    )
    Hibernate: 
    /* insert for
        hellojpa.Member */insert 
    into
        Member (createBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) 
    values
        (?, ?, ?, ?, ?, ?)
    Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.createBy,
        m1_0.createdDate,
        m1_0.lastModifiedBy,
        m1_0.lastModifiedDate,
        t1_0.TEAM_ID,
        t1_0.name,
        m1_0.USERNAME 
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.team_TEAM_ID 
    where
        m1_0.MEMBER_ID=?
findMember.getCreateBy() = kim

@MappedSuperclass은
엔티티의 반복되는 속성을 같이 쓰고 싶을때 사용한다.(공통 속성은 상속받은 엔티티의 테이블에 추가된다)

테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는매핑 정보를 모으는 역할을 한다.
주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.

조회, 검색(em.find(BaseEnitity.class))이 불가능하다.
직접 생성해서 사용할 일이 없지만 누군가는 실수할 수 있으니 미리 방지하기 위해 추상 클래스 권장한다.

단일 테이블 전략과 유사하므로 헷갈릴수도 있지만 @MappedSuperclass는 테이블과 관련이 없다는것만 기억하자.

참고:@Entity 클래스는 @Entity,@MappedSuperclass만 상속 가능하다.

0개의 댓글