고급 매핑(공부중)

김준석·2021년 4월 19일
0
post-thumbnail

1. 상속 관계 매핑

관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없다.

ORM에서 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

지금부터 위 그림의 객체 상속 모델을 3가지 방법으로 매핑해보자.

조인 전략

Table

객체는 타입으로 구분할 수 있지만, 테이블은 타입의 개념이 없기 때문에 구분하는 컬럼(DTYPE)을 추가해야 한다.

Entity

@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 상속 매핑은 부모 클래스에 사용, 매핑 전략 JOINED 사용
@DiscriminatorColumn(name = "DTYPE") // 구분 컬럼 지정, 저장된 자식 테이블 구분, 기본값이 DTYPE
public abstract class Item {
    
    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    
    private String name;
    private int price;
    
    // ...
}

@Entity
@DiscriminatorValue("A") // 엔티티를 저장할 때 구분 컬럼에 입력할 값 지정, DTYPE에 "A" 저장
public class Album extends Item {
    
    private String artist;
    
    // ...
}

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

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID") // 부모 테이블의 ID 컬럼명을 변경하고 싶으면 사용
public class Book extends Item {
    
    private String acthor;
    private String isbn;
    
    // ...
}

장점

  • 테이블 정규화
  • 외래 키 참조 무결성 제약조건 활용
  • 저장공간 효율적 사용

단점

  • 조회 시 조인이 많아 성능 저하 우려
  • 조회 쿼리 복잡
  • 데이터 등록할 INSERT SQL 두 번 실행

단일 테이블 전략

Table

말 그대로 테이블 하나만 사용한다. 주의점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다는 점이다. (ALBUM 테이블은 ARTIST 컬럼만 사용하기 때문에 나머지 컬럼은 null이 입력된다.)

Entity

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 매핑 전략 SINGLE_TABLE 사용
@DiscriminatorColumn(name = "DTYPE") // 꼭 사용
public abstract 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 {

    // ...
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
    
    // ...
}

@Entity
@DiscriminatorValue("B")
public class Book extends Item {

    // ...
}

장점

  • 조인이 필요 없으므로 일반적으로 조회 성능이 좋음
  • 조회 쿼리 단순

단점

  • 자식 엔티티가 매핑한 컴럼은 모두 null 허용
  • 테이블이 커질 수 있음 → 조회 성능이 오히려 느려질 수 있음

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

Table

자식 테이블에 필요한 컬럼을 모두 넣는다.

Entity

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PRE_CLASS) // 매핑 전략 TABLE_PRE_CLASS 사용
public abstract class Item {
    
    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    
    private String name;
    private int price;
    
    // ...
}

@Entity
public class Album extends Item {

    // ...
}

@Entity
public class Movie extends Item {
    
    // ...
}

@Entity
public class Book extends Item {

    // ...
}

구분 컬럼을 사용하지 않는다. 일반적으로 추천하지 않는 전략이라고 한다.

장점

  • 서브 타입을 구분해서 처리할 때 효과적
  • not null 제약조건 사용 가능

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느림
  • 자식 테이블을 통합해서 쿼리하기 힘듦

2. @MappedSuperclass

@MappedSuperclass를 사용하면 부모 클래스는 테이블과 매핑하지 않고 자식 클래스에게 매핑 정보를 제공할 수 있다.

Entity

@MappedSuperclass
public abstract class BaseEntity {
    
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    
    // ...
}

@Entity
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")) // 매핑 정보 재정의
public class Member extends BaseEntity {
    
    // ID 상속
    // NAME 상속
    private String email;
    
    // ...
}

@Entity
@AttributeOverrides({
    @AttributeOverride(name = "id", column = @Column(name = "SELLER_ID")),
    @AttributeOverride(name = "name", column = @Column(name = "SELLER_NAME"))
}) // 복수로 사용 가능
public class Seller extends BaseEntity {
    
    // ID 상속
    // NAME 상속
    private String shopName;
    
    // ...
}

@MappedSuperclass 특징

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아님(em.find()나 JPQL 사용 못함)
  • 추상 클래스로 만드는 것을 권장

@MappedSuperclass를 사용하면 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.

3. 복합 키와 식별 관계 매핑

식별 관계 vs 비식별 관계

식별 관계

부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계

비식별 관계

부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계

  • 필수적 비식별 관계: 외래 키에 NULL을 허용하지 않음
  • 선택적 비식별 관계: 외래 키에 NULL을 허용

복합 키: 비식별 관계 매핑

@IdClass

부모 클래스

@Entity
@IdClass(ParentId.class)
public class Parent {
    
    @Id
    @Column(name = "PARENT_ID1")
    private String id1;
    
    @Id
    @Column(name = "PARENT_ID2")
    private String id2;
    
    private name;
    
    // ...
}

식별자 클래스

public class ParentId implements Serializable {
    
    private String id1;
    private String id2;
    
    public ParentId() {
    }
    
    public ParentId(String id1, String id2) {
        this.id1 = id1;
        this.id2 = id2;
    }
    
    // equals, hashCode 구현
}

@IdClass를 사용할 때 식별자 클래스가 만족해야 하는 조건

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 함.
  • Serializable 인터페이스를 구현해야 함
  • 기본 생성자가 있어야 함
  • 식별자 클래스는 public이여야 함

식별자 클래스는 어떻게 사용될까?

// Parent 엔티티 생성
Parent parent = new Parent();
parent.setId1("myId1");
parent.setId2("myId2");
parent.setName("parentName");
em.persist(parent);

// 조회
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);

자식 클래스

@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;
}

만약 @JoinColumnnamereferencedColunmName의 값이 같다면 생략할 수 있다.

EmbeddedId

@IdClass가 데이터베이스에 맞춘 방법이라면 EmbeddedId는 좀 더 객체지향적인 방법이다.

부모 클래스

@Entity
public class Parent {
    
    @EmbeddedId
    private ParentId id;
    
    private String name;
    
    // ...
}

식별자 클래스

@Embeddable
public class ParentId implements Serializable {
    
    @Column(name = "PARENT_ID1")
    private String id1;
    
    @Column(name = "PARENT_ID2")
    private String id2;
    
    // equals, hashCode 구현
}

@EmbeddedId를 사용할 때 식별자 클래스가 만족해야 하는 조건

  • @Embeddable 어노테이션을 붙여야 함
  • Serializable 인터페이스를 구현해야 함
  • equals, hashCode를 구현해야 함
  • 기본 생성자가 있어야 함
  • 식별자 클래스는 public이어햐 함

식별자 클래스는 어떻게 사용될까?

// Parent 엔티티 생성
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1", "myId2"); // ParentId를 사용한다.
parent.setId(parentId);
parent.setName("parentName");
em.persist(parent);

// 조회
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);

복합 키와 equals(), hashCode()

지금까지 구현한 식별자 클래스를 보면 equals()hashCode()를 구현했다. 왜일까?

다음 코드를 보자.

ParentId id1 = new ParentId();
id1.setId1("myId1");
id1.setId2("myId2");

ParentId id2 = new ParentId();
id2.setId1("myId1");
id2.setId2("myId2");

id1.equals(id2) // Override를 했다면 true, 아니면 false

둘은 인스턴스가 다르다. 영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 관리한다. 그리고 식별자를 비교할 때 equals()hashCode()를 사용한다. 따라서 동등성이 지켜지지 않으면 영속성 컨텍스트가 엔티티를 관리하는 데 심각한 문제를 발생시킬 수 있다.

@IdClass vs @EmbeddedId

@EmbeddedIdIdClass와 비교해서 더 객체지향적이고 중복도 없어서 좋아보이긴 하지만 특정 상황에서 JPQL이 조금 더 길어질 수 있다.

em.createQuery("select p.id.id1, p.id.id2 from Parent p"); // @EmbeddedId
em.createQuery("select p.id1, p.id2 from Parent p"); // @IdClass

참고: 복합 키에는 @GenerateValue를 사용할 수 없다. 복합 키를 구성하는 여러 컬럼 중 하나에도 사용할 수 없다.

복합 키: 식별 관계 매핑

식별 관계는 다음 그림과 같다.

@IdClass와 식별관계

부모

@Entity
public class Parent {
    
    @Id
    @Column(name = "PARENT_ID")
    private String id;
    
    private String name;
    
    // ...
}

자식

@Entity
@IdClass(ChildId.class)
public class Child {
    
    @Id
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    
    @Id
    @Column(name = "CHILD_ID")
    private String childId;
    
    private String name;
    
    // ...
}

자식 ID

public class ChildId implements Serializable {
    
    private String parent; // Child.parent 매핑
    private String childId; // Child.childId 매핑
    
    // equals, hashCode
    // ...
}

손자

@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
    
    @Id
    @ManyToOne
    @JoinColumns({
        @JoinColumn(name = "PARENT_ID"),
        @JoinColumn(name = "CHILD_ID")
    })
    private Child child;
    
    @Id
    @Column(name = "GRANDCHILD_ID")
    private String id;
    
    private String name;
    
    // ...
}

손자 ID

public class GrandChildId implements Serializable {
    
    private ChildId childId; // GrandChild.child 매핑
    private String id; // GrandChild.id 매핑
    
    // equals, hashCode
    // ... 
}

식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다. 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne을 같이 사용하면 된다.

@EmbeddedId와 식별 관계

@EmbeddedId로 식별 관계를 구성할 때는 @MapsId 사용

부모

@Entity
public class Parent {
    
    @Id
    @Column(name = "PARENT_ID")
    private String id;
    
    private String name;
    
    // ...
}

자식

@Entity
public class Child {
    
    @EmbeddedId
    private ChildId id;
    
    @MapsId("parentId") // ChildId.parentId 매핑
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    
    private String name;
    
    // ...
}

자식 ID

@Embeddable
public class ChildId implements Serializable {
    
    private String parentId; // @MapsId("parentId")로 매핑
    
    @Column(name = "CHILD_ID")
    private String id;
    
    // equals, hashCode
    // ...
}

손자

@Entity
public class GrandChild {
    
    @EmbeddedId
    private GrandChildId id;
    
    @MapsId("childId") // GrandChildId.childId 매핑
    @ManyToOne
    @JoinColumns({
        @JoinColumn(name = "PARENT_ID"),
        @JoinColumn(name = "CHILD_ID")
    })
    private Child child;
    
    private String name;
    
    // ...
}

손자 ID

@Embeddable
public class GrandChildId implements Serializable {
    
    private ChildId childId; // @MapsId("childId")로 매핑
    
    @Column(name = "GRANDCHILD_ID")
    private String id;
    
    // equals, hashCode
    // ... 
}

@MapsId는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다.

비식별 관계로 구현

식별 관계를 비식별 관계로 변경한 그림이다.

이번에는 비식별 관계로 구현해보자.

부모

@Entity
public class Parent {
    
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    
    private String name;
    
    // ...
}

자식

@Entity
public class Child {
    
    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;
    
    // ...
}

손자

@Entity
public class GrandChild {
    
    @Id
    @GeneratedValue
    @Column(name = "GRANDCHILD_ID")
    private Long id;
    
    private String name;

	@ManyToOne
    @JoinColumn(name = "CHILD_ID")
    private Child child
}

식별 관계와 비교할 때 복합 키를 사용하지 않아 매핑도 쉽고 코드도 간단한 것을 볼 수 있다.

일대일 식별 관계

일대일 식별 관계는 특별하다. 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다. 그래서 자식 테이블에서 부모 테이블의 기본 키 값만 사용하고 있다.

부모

@Entity
public class Board {
    
    @Id
    @GeneratedValue
    @Column(name = "BOARD_ID")
    private Long id;
    
    private String title;
    
    @OneToOne(mappedBy = "board")
    private BoardDetail boardDetail;
    
    // ...
}

자식

@Entity
public class BoardDetail {
    
    @Id
    private Long boardId;
    
    @MapsId // BoardDetail.boardId 매핑, 식별자가 단순히 컬럼 하나면 속성 값은 비워두면 된다.
    @OneToOne
    @JoinColumn(name = "BOARD_ID")
    private Board board;
    
    private String content;
    
    // ...
}

식별, 비식별 관계의 장단점

profile
내 몸에는 꼰대의 피가 흐른다.

0개의 댓글