[JPA] 고급 매핑

Updated:

들어가며

해당 글은 자바 ORM 표준 프로그래밍을 정리한 글입니다.

상속 관계 매핑, @MappedSuperclass, 복합키와 식별관계 매핑에 대해서 알아본다.

상속 관계 매핑

조인 전략

  • 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없기에 type을 구분할 수 있는 컬럼(default = “DTYPE”)을 사용해야 한다.
@Inheritance(strategy = InheritanceType.JOINED)
@Entity
@DiscriminatorColumn(name = "DTYPE")
public class Item {
    @Id @GeneratedValue
    @Column(name = "item_id")
    private Long id;

    private String name;
    private int price;
}


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

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
    private String director;
    private String actor;
}

    create table item (
       dtype varchar(31) not null,
        item_id bigint not null,
        name varchar(255),
        price integer not null,
        primary key (item_id)
    )
    
    create table album (
       artist varchar(255),
        item_id bigint not null,
        primary key (item_id)
    )
    
    create table movie (
       actor varchar(255),
        director varchar(255),
        item_id bigint not null,
        primary key (item_id)
    )    

@Inheritance(strategy = InheritanceType.JOINED)

  • 상속 매핑은 부모 클래스에 @Inheritance를 사용 해야 한다.
  • 매핑 전략을 지정해야 하는데 조인 전략은 JOINED를 사용한다.

@DiscriminatorColumn(name = “DTYPE”)

  • 부모 클래스에 구분 컬럼을 지정한다.
  • 이 컬럼으로 저장된 자식 테이블을 구분할 수 있다.
  • 기본값이 DTYPE이다.

@DiscriminatorValue(“M”)

  • 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.
  • 기본값은 엔티티 이름으로 사용한다.

장점

  • 테이블이 정규화된다.
  • 외래 키 참조 무결성 제약조건을 활용할 수 있다.
  • 저장공간을 효율적으로 사용한다.

단점

  • 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
  • 조회 쿼리가 복잡하다
  • 데이터를 등록할 INSERT SQL을 두 번 실행한다.
@Test
void insertTest() {
  Movie movie = new Movie();

  em.persistAndFlush(movie);
  em.clear();

  Movie result = em.find(Movie.class, movie.getId());
  System.out.println(result);
}
    insert 
    into
        item
        (name, price, dtype, item_id) 
    values
        (?, ?, 'M', ?)
        
    insert 
    into
        movie
        (actor, director, item_id) 
    values
        (?, ?, ?)
        
    select
        movie0_.item_id as item_id2_1_0_,
        movie0_1_.name as name3_1_0_,
        movie0_1_.price as price4_1_0_,
        movie0_.actor as actor1_5_0_,
        movie0_.director as director2_5_0_ 
    from
        movie movie0_ 
    inner join
        item movie0_1_ 
            on movie0_.item_id=movie0_1_.item_id 
    where
        movie0_.item_id=?                

단일 테이블 전략

  • InheritanceType.SINGLE_TABLE을 사용한다.
  • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
@Data
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 해당 부분만 변경
@Entity
@DiscriminatorColumn(name = "DTYPE")
public class Item {
    @Id @GeneratedValue
    @Column(name = "item_id")
    private Long id;

    private String name;
    private int price;
}
    create table item (
       dtype varchar(31) not null,
        item_id bigint not null,
        name varchar(255),
        price integer not null,
        artist varchar(255),
        actor varchar(255),
        director varchar(255),
        primary key (item_id)
    )

장점

  • 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
  • 조회 쿼리가 단순하다.
  • 추가적인 테이블 생성이 없다.

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있어, 상황에 따라 조회 성능이 오히려 느려질 수 있다.
    insert 
    into
        item
        (name, price, actor, director, dtype, item_id) 
    values
        (?, ?, ?, ?, 'M', ?)
        
    select
        movie0_.item_id as item_id2_0_0_,
        movie0_.name as name3_0_0_,
        movie0_.price as price4_0_0_,
        movie0_.actor as actor6_0_0_,
        movie0_.director as director7_0_0_ 
    from
        item movie0_ 
    where
        movie0_.item_id=? 
        and movie0_.dtype='M'
        

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

  • 추천하지 않는 전략

장점

  • 서브 타입을 구분해서 처리할 때 효과적이다.
  • not null 제약조건을 사용할 수 있다.

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느리다.(SQL에 UNION을 사용해야 한다.)
  • 자식 테이블을 통합해서 쿼리하기 어렵다.

@MappedSuperclass

  • 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶을 때 사용
  • @AttributeOverrides, @AttributeOverride를 사용하여 부모로부터 물려받은 매핑 정보를 재정의 가능
  • 테이블에 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용
  • em.find()나 JPQL에서 사용할 수 없다.
  • 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장
@MappedSuperclass
public abstract class AbstractEntity {
    @Id
    private Long id;

    private OffsetDateTime createdTime;
    private OffsetDateTime modifiedTime;
    private OffsetDateTime removedTime;
}

@Entity
public class Book extends AbstractEntity {
}


create table book (
       id bigint not null,
        created_time timestamp,
        modified_time timestamp,
        removed_time timestamp,
        primary key (id)
    )
  • 부모로 부터 상속 받은 값을 변경하기 위해서는 @AttributeOverrides를 이용해서 변경할 수 있다.
@Entity
@AttributeOverrides(value = {@AttributeOverride(name = "id", column = @Column(name = "book_id"))})
public class Book extends AbstractEntity {
    // Id와 time 상
}
    
    create table book (
       book_id bigint not null, // id에서 book_id 
        created_time timestamp,
        modified_time timestamp,
        removed_time timestamp,
        primary key (book_id)
    )

복합 키와 식별 관계 매핑

식별관계

  • 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계
  • 자식 테이블로 내려갈 수록 기본 키 컬럼이 늘어나 조인시 복잡해지고 기본키 인덱스가 불필요하게 커질 수 있음
  • 자연 키 컬럼들로 만들게 되면 나중에 비지니스 요구사항이 변경됨에 따라 영향을 줄 수 있음
  • 식별 관계의 경우 @GenerateValue를 사용하지 못 한다.

비식별 관계

  • 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용
  • 필수적 비식별 관계
    • 외래 키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야 한다.
  • 선택적 비식별 관계
    • 외래 키에 NULL을 허용한다. 연관관계를 맺을지 말지 선택할 수 있다.

@IdClass

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
    • Parent.id1과 ParentId.id1
  • Serializable 인터페이스를 구현해야 한다.
  • equals, hashCode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.
@Entity
@IdClass(ParentId.class)
public class Parent {
    @Id
    private String id1;

    @Id
    private String id2;
}

public class ParentId implements Serializable {
    private Long id1;
    private Long id2;
}
    create table parent (
       id1 varchar(255) not null,
        id2 varchar(255) not null,
        primary key (id1, id2)
    )

@EmbeddedId

  • @Embeddable 어노테이션을 붙여주어야 한다.
  • Serializable 인터페이스를 구현해야 한다.
  • equals, hashCode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.
  • 식별 관계에서 사용하는 경우 @MapsId를 사용해야 한다.
@Entity
public class Parent {
    @EmbeddedId
    private ParentId id;
}

@Embeddable
public class ParentId implements Serializable {
    private Long id1;
    private Long id2;
}
    create table parent (
       id1 varchar(255) not null,
        id2 varchar(255) not null,
        primary key (id1, id2)
    )

마치며

상속 관계일 때 매핑을 하는 방법에 대해서 알아보았다. 실무에서도 상속관계일 때 JOINEDSINGLE_TABLE 중 에 무엇을 쓸지는 상황에 따라 다르겠지만 복잡도를 고려해볼 때는 SINGLE_TABLE이 낮아 적용하기에는 쉬워 보인다고 생각한다.

@MappedSuperClass 어노테이션은 실무에서도 많이 사용하고 있기 때문에 잘 설계된 엔티티의 경우 상당히 많은 부모로 부터 라이프 사이클까지 관리될 수 있게 사용되니 잘 기억해두기를 추천한다.👍



Leave a comment