elevne's Study Note

JPA 고급매핑 본문

Backend/JPA 프로그래밍

JPA 고급매핑

elevne 2023. 7. 9. 21:01

상속관계 매핑

 

관계형 데이터베이스에는 객체지향 언어와는 다르게 상속이라는 개념이 없다. 대신 슈퍼타입 서브타입 관계라는 모델링 기법이 객체의 상속 개념과 가장 유사하다. ORM 에서 이야기하는 상속관계 매핑은 객체의 상속 구조와 데이터베이스 슈퍼타입 서브타입 관계를 매핑하는 것이다. 슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현할 때에는 3 가지 방법 중 하나를 택할 수 있다.

 

 

1. 조인 전략

 

엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다. 조인 전략을 사용할 때 주의할 점이 있다. 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없다. 따라서 타입을 구분하는 컬럼을 추가해주어야 한다.

 

 

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

    @Id @GeneratedValue @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
}

 

 

@Entity
@DiscriminatorValue("A")
@Getter
@Setter
public class Album extends Item{

    private String artist;
}


@Entity
@DiscriminatorValue("M")
@Getter
@Setter
public class Movie extends Item {

    private String director;
    private String actor;
}

 

 

상속 매핑은 부모 클래스에 @Inheritance 를 사용하게 된다. 그 후 매핑전략을 지정해야 하는데, 위의 경우에는 조인전략을 사용하기에 strategy=Inheritance.JOINED 가 들어간 것이다. 그 다음 @DiscriminatorColumn 으로 부모 클래스에 구분 컬럼을 지정한다. 이 컬럼으로 저장된 자식 테이블을 구분할 수 있다. 자식클래스에는 @DiscriminatorValue 로 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다. (위의 경우에는 만약 영화 엔티티를 저장하면 구분 컬럼 DTYPE 에 값 M 이 저장되는 것)

 

 

item table

 

 

movie table

 

 

 

위 전략에서 자식 테이블은 기본값으로 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 만약 자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 @PrimaryKeyJoinColumn 을 사용하면 된다.

 

 

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
public class Book extends Item{

    private String author;
    private String isbn;
}

 

 

book table

 

 

조인전략을 사용하면 테이블이 정규화되고, 외래 키 참조 무결성 제약조건을 활용할 수 있으며 저장공간이 효율적으로 사용된다. 하지만, 조회할 때 조인이 많아서 성능이 저하될 수 있으며 조회 쿼리가 복잡해진다. 또, 데이터를 등록할 때 INSERT 가 두 번 실행된다.

 

 

 

2. 단일테이블 전략

 

단일테이블 전략은 이름 그대로 테이블을 하나만 사용하여 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분한다. 조인을 사용하지 않으므로 일반적으로 가장 빠르다.

 

 

 

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class SingleTableItem {

    @Id @GeneratedValue @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;

}

 

 

@Entity
@DiscriminatorValue("A")
public class SingleTableAlbum extends SingleTableItem {

    private String albumName;

}

@Entity
@DiscriminatorValue("M")
public class SingleTableMovie extends SingleTableItem {

    private String movieName;

}

 

 

SingleTable~~ table

 

 

이 전략을 사용할 때에는 자식 엔티티가 매핑한 컬럼은 모두 null 을 허용해주어야 한다. 위와 같이 InheritanceType.SINGLE_TABLE 로 지정하면 단일테이블 전략이 사용된다. 이는 조인 필요 없이 조회 성능이 빠르며 조회쿼리가 단순하다. 하지만 자식 엔티티가 매핑한 컬럼은 null 을 허용해야하는 점과 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있어 오히려 성능이 나빠질 수 있다는 단점이 있다.

 

 

 

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

 

이는 각 구현클래스, 자식 엔티티마다 테이블을 따로 만드는 전략이다. 그리고 자식 테이블 각각에 필요한 컬럼이 모두 들어간다.

 

 

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

    @Id @GeneratedValue @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;

}

 

 

@Entity
public class EachAlbum extends EachItem {

    private String albumName;
}

@Entity
public class EachMovie extends EachItem{

    private String movieName;
}

 

 

InheritanceType.TABLE_PER_CLASS 를 선택하면 구현클래스마다 테이블 전략을 사용한다. 이는 일반적으로 추천되지 않는 방법이다. 서브타입을 구분해서 처리할 때 효과적이기도 하고 not null 제약조건을 사용할 수 있다는 장점이 있긴 하지만, 여러 자신 테이블을 함께 조회할 때 성능이 좋지 않다. (UNION 을 사용해서 느림) 또 자식 테이블을 통합해서 쿼리하기도 어렵다. (조인이나 단일테이블 전략을 사용한다!)

 

 

 

@MappedSuperclass

 

부모클래스는 테이블과 매핑하지 않고 부모클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고자 한다면 @MappedSuperclass 를 사용할 수 있다. @MappedSuperclass 는 실제 테이블과는 매핑되지 않는다. 단순히 매핑 정보를 상속할 목적으로만 사용된다.

 

 

@MappedSuperclass
public class BaseEntity {

    @Id @GeneratedValue
    private Long id;

    private String name;

}

 

 

@Entity
public class EntityWithSuperclass extends BaseEntity{

    private String email;

}

 

 

EntityWithSuperclass table

 

 

BaseEntity 에 객체들이 주로 사용하는 공통 매핑 정보를 정의하고, 자식 엔티티들은 상속을 통해 매핑 정보를 물려받는다. BaseEntity 는 테이블과 매핑하지 않고 자식 엔티티에게 공통으로 사용되는 매핑정보만 제공한다. 부모에게 물려받은 매핑 정보를 재정의하려면 @AttributeOverrides 혹은 @AttributeOverride 를 사용하고, 연관관계를 재정의하려면 @AssociationOverrides 혹은 @AssociationOverride 를 사용한다.

 

 

@Entity
@AttributeOverrides({
        @AttributeOverride(name = "id", column = @Column(name = "NEW_ID")),
        @AttributeOverride(name = "name", column = @Column(name = "NEW_NAME"))
})
public class EntityWithSuperclassEdit extends BaseEntity{
}

 

 

EntityWithSuperclassEdit table

 

 

@MappedSuperclass 는 테이블과는 관계가 없고 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할을 한다. 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상클래스로 만드는 것이 권장된다.

 

 

 

복합 키와 식별관계 매핑

 

이전 시간에 @IdClass 를 활용한 복합 키 생성 방법에 대해서는 알아보았다. 만약 복합 키를 사용한 테이블의 키를 외래 키로 사용하는 테이블을 만들고자 한다면 아래와 같이 @JoinColumns 애노테이션을 사용한다.

 

 

@Entity
public class Child {

    @Id
    private String id;

    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "parent_id1", referencedColumnName = "parent_id1"),
            @JoinColumn(name = "parent_id2", referencedColumnName = "parent_id2")
    })
    private Parent parent;
}

 

 

@IdClass 를 사용하는 방법 말고도 @EmbeddedId 를 사용하는 방법이 있다. 이는 좀 더 객체지향적인 방법이라고 한다.

 

 

@Entity
public class Parent {

    @EmbeddedId
    private ParentId parentId;

    private String name;

}


@Embeddable
@EqualsAndHashCode
public class ParentId implements Serializable {

    @Column(name = "PARENT_ID1")
    private String id1;

    @Column(name = "PARENT_ID2")
    private String id2;

}

 

 

이는 @IdClass 와 다르게 식별자 클래스에 기본키를 직접 매핑한다. @EmbeddedId 에 사용되는 클래스는 @Embeddable 애노테이션이 붙어있어야 하며, Serializable 을 구현, equals/hashCode 를 구현해야한다. 기본 생성자가 있어야 하며 식별자 클래스는 public 이어야 한다.

 

 

@EmbeddedId 로 식별관계를 구성할 때는 @MapsId 를 사용한다. 이는 외래키와 매핑한 연관관계를 기본키에도 매핑하겠다는 뜻이다. @MapsId 의 속성 값은 @EmbeddedId 를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.

 

 

@Entity
public class Child {

    @EmbeddedId
    private ChildId id;

    @MapsId("parentId")
    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "PARENT_ID1"),
            @JoinColumn(name = "PARNET_ID2")
    })
    public Parent parent;

    private String name;

}


@Embeddable
@EqualsAndHashCode
public class ChildId implements Serializable {

    private ParentId parentId;

    @Column(name = "CHILD_ID")
    private String id;
}

 

 

일대일 식별관계에서는, 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용한다. 그래서 부모 테이블의 기본 키가 복합 키가 아니면 자식테이블의 키는 복합키로 구성하지 않아도 된다. 아래처럼 식별자가 단순히 컬럼 하나면 @MapsId 를 속성 값을 비운 채로 사용한다. (@Id 를 사용해서 식별자를 지정한 PostDetail.postId 와 매핑된다)

 

 

@Entity
public class Post {

    @Id @GeneratedValue @Column(name = "POST_ID")
    private Long id;

    private String title;

    @OneToOne(mappedBy = "post")
    private PostDetail postDetail;
}



@Entity
public class PostDetail {

    @Id
    private Long postId;

    @MapsId
    @OneToOne
    @JoinColumn(name = "POST_ID")
    private Post post;

    private String content;

}

 

 

PostDetail table

 

 

 

 

조인 테이블

 

회원과 사물함이 있는데 각각 테이블에 데이터를 등록했다가 회원이 원할 때 사물함을 택할 수 있는 경우를 생각해본다. 회원이 사물함을 사용하기 전까지는 아직 둘 사이에 관계가 없으므로 MEMBER 테이블의 LOCKER_ID 외래 키에 null 을 입력해두어야 한다. 이렇게 외래 키에 null 을 허용하는 관계를 선택적 비식별 관계라고 한다. 선택적 비식별 관계는 외래 키에 null 을 허용하므로 회원과 사물함을 조인할 때 OUTER JOIN 을 사용해야 한다. INNER JOIN 을 하면 사물함과 관계가 없는 회원은 조회되지 않는다. (회원과 사물함이 아주 가끔 관계를 맺는다면 외래 키 값 대부분이 null 로 저장된다는 단점이 있다)

 

 

위와 같은 방법 대신에 별도의 조인 테이블을 생성해서 연관관계를 관리할 수 있다. (다대다 관계처럼) 조인 컬럼을 사용하는 방법은 단순히 외래 키 컬럼만 추가해서 연관관계를 맺지만 조인 테이블을 사용하는 방법은 연관관계를 관리하는 조인테이블을 추가로 두고 여기서 두 테이블의 외래 키를 가지고 연관관계를 관리한다. (따라서 회원, 사물함 테이블에는 연관관계를 관리하기 위한 외래 키 컬럼이 없다) 조인 테이블을 사용할 때에는 이전에 N:M 관계에서 사용했던 @JoinTable 애노테이션을 사용하면 된다.

 

 

 

엔티티 하나에 여러 테이블 매핑

 

잘 사용하지는 않지만 @SecondaryTable 을 사용하면 한 엔티티에 여러 개의 테이블을 매핑할 수 있다.

 

 

@Entity
@Table(name = "MT")
@SecondaryTable(name = "MT_DETAIL", pkJoinColumns = @PrimaryKeyJoinColumn(name = "MT_DETAIL_ID"))
public class MultipleTables {

    @Id @GeneratedValue @Column(name = "MT_ID")
    private Long id;

    private String title;

    @Column(table = "MT_DETAIL")
    private String content;
}

 

 

@SecondaryTable 을 이용하여 MT_DETAIL 테이블을 추가로 매핑한다. name 속성은 매핑할 다른 테이블의 이름, pkJoinColunmns 는 매핑할 다른 테이블의 기본 키 컬럼 속성이다. (더 많은 테이블을 매핑하려면 @SecondaryTables 를 사용하면 된다)

 

 

@SecondaryTable 을 사용해서 두 테이블을 하나의 엔티티에 매핑하는 것보다 테이블 당 엔티티를 각각 만들어서 일대일 매핑하는 것이 권장된다. 위 방법은 항상 두 테이블을 조회하므로 최적화하기 어렵다.

 

 

 

 

 

 

 

 

Reference:

자바 ORM 표준 JPA 프로그래밍

'Backend > JPA 프로그래밍' 카테고리의 다른 글

JPA - Embedded / Collection Value Type  (0) 2023.07.12
JPA - Proxy, Cascade, Orphan  (0) 2023.07.11
JPA N:M 관계  (0) 2023.07.06
JPA 연관관계 매핑  (0) 2023.07.05
JPA Entity 매핑  (0) 2023.07.04