상속 관계 매핑

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

조인 전략

엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다.

타입을 구분하는 컬럼을 추가해야 한다.

조인 전략 매핑
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@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 {

  private String artist;
  ...
}

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

  private String director;
  private String actor;
}
  • @Inheritance(strategy = InheritanceType.JOINED)
    • 상속 매핑은 부모 클래스에 @Inheritance를 사용해야 한다.
    • 그리고 매핑 전략을 지정한다.
  • @DiscriminatorColumn(name = "DTYPE")
    • 부모 클래스에 구분 컬럼을 지정한다. 이 컬럼으로 저장된 자식 테이블을 구분할 수 있다.
    • 기본값은 DTYPE 이다.
  • @DiscriminatorValue("M")
    • 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.
    • 만약 영화 엔티티를 저장하면 구분 컬럼인 DTYPE에 값 M이 저장된다.
  • @PrimaryKeyJoinColumn(name = "BOOK_ID")
    • 자식 테이블의 기본 키 컬럼명을 변경하고 싶을 때 사용

장점

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

단점

  • 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
  • 조회 쿼리가 복잡하다.
  • 데이터를 등록할 INSERT SQL을 두 번 실행한다.

특징

  • JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼 없이도 동작한다.

단일 테이블 전략

구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분한다.

단일 테이블 전략 매핑
...
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
...

장점

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

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 그러므로 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.

특징

  • 구분 컬럼을 꼭 사용해야 한다.
  • @DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용한다.

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

자식 엔티티마다 테이블을 만든다. 일반적으로 추천하지 않는 전략이다.

장점

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

단점

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

특징

  • 구분 컬럼을 사용하지 않는다.

@MappedSuperclass

부모 클래스가 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶을 때 사용한다.

@MappedSuperclass의 특징

  • 추상 클래스와 비슷하다.
  • 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용한다.
  • 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장한다.

@AttributeOverrides, @AttributeOverride

  • 부모로부터 물려받은 매핑 정보를 재정의할 때 사용

@AssociationOverrides, @AssociationOverride

  • 부모로부터 물려받은 연관관계를 재정의할 때 사용

참고

  • 엔티티는 엔티티이거나 @MappedSuperclass로 지정한 클래스만 상속받을 수 있다.

복합 키와 식별 관계 매핑

식별 관계 vs 비식별 관계

데이터베이스를 설계할 때 식별 관계나 비식별 관계 중 하나를 선택해야 한다.

최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세다.

복합키: 비식별 관계 매핑

JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용한다. 그리고 식별자를 구분하기 위해 equals와 hashCode를 사용해서 동등성 비교를 한다.

식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에 equals와 hashCode를 구현해야 한다.

JPA는 복합 키를 지원하기 위해 2가지 방법을 제공한다.

IdClass

// 6장에서 설명

자식 클래스
  ...
  @ManyToOne
  @JoinColumns({
          @JoinColumn(name = "PARENT_ID1",
              referencedColumnName = "PARENT_ID1"),
          @JoinColumn(name = "PARENT_ID1",
              referencedColumnName = "PARENT_ID1")
  })
  ...
  • 부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합키다.
  • @JoinColumn의 name 속성과 referencedColumnName 속성의 값이 같으면 referencedColumnName은 생략해도 된다.

@EmbeddedId

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

  ...
  @EmbeddedId
  private ParentId id;
  ...
@Embeddable
public class ParentId implements Serializable {

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

  // equals and hashCode 구현
}

@EmbeddedId를 적용한 식별자 클래스는 다음 조건을 만족해야 한다.

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

복합키와 equals(), hashCode()

  • 자바의 모든 클래스는 기본으로 Object 클래스를 상속받는데 이 클래스가 제공하는 기본 equals()는 인스턴스 참조 값 비교인 == 비교를 하기 때문이다.
  • 식별자 객체의 동등성(equals 비교)이 지켜지지 않으면 심각한 문제가 발생한다.

@IdClass vs @EmbeddedId

  • 각각 장단점이 있으므로 본인의 취향에 맞는 것을 일관성 있게 사용하면 된다.
  • @EmbeddedId가 더 객체지향적이고 중복도 없어서 좋아보이긴 하지만 특정 상황에 JPQL이 조금 더 길어질 수 있다.

참고

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

복합 키: 식별 관계 매핑

@IdClass와 식별 관계

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

@EmbeddedId와 식별 관계

  • @MapsId를 사용해야 한다.

  • @Entity
    public class Parent {
      @Id @Column(name = "PARENT_ID")
      private String id;
    
      ...
    }
    
    @Entity
    public class Child {
    
      @EmbeddedId
      private ChildId id;
    
      @MapsId("parentId") // ChildId.parentId 매핑
      @ManyToOne
      @JoinColumn(name = "PARENT_ID")
      public Parent parent;
      ...
    }
    
    @Embeddable
    public class ChildId implements Serializable {
    
      private String parentId;  // @MapsId("parentId")로 매핑
      @Column(name = "CHILD_ID")
      private String id2;
    
      // equals and hashCode 구현
    }
  • @IdClass와 다른 점은 @Id 대신에 @MapsId를 사용한 점이다.

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

  • @MapsId의 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.

비식별 관계로 구현

매핑도 쉽고 코드도 단순하다. 그리고 복합 키가 없으므로 복합 키 클래스를 만들지 않아도 된다.

일대일 식별 관계

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

@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;
  ...
}
  • BoardDetail처럼 식별자가 단순히 컬럼 하나면 @MapsId를 사용하고 속성 값은 비워두면 된다.
  • 이때 @MapsId는 @Id를 사용해서 식별자로 지정한 BoardDetail.boardId와 매핑된다.

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

데이터베이스 설계 관점에서 보면 다음과 같은 이유로 식별 관계보다는 비식별 관계를 선호한다.

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 결국 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
  • 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다. 비즈니스 요구사항은 시간이 지남에 따라 언젠가는 변한다.
  • 비식별 관계보다 테이블 구조가 유연하지 못하다.

객체 관계 매핑의 관점에서 보면 다음과 같은 이유로 비식별 관계를 선호한다.

  • 컬럼이 하나인 기본 키를 매핑하는 것보다 많은 노력이 필요하다.
  • JPA는 @GenerateValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.

식별 관계가 가지는 장점

  • 기본 키 인덱스를 활용하기 좋다.
  • 상위 테이블들의 기본 키 컬럼을 자식, 손자 테이블들이 가지고 있으므로 특정 상황에 조인 없이 하위 테이블만으로 검색을 완료할 수 있다.

식별 관계가 꼭 필요한 곳에는 적절하게 사용하는 것이 좋을 수도 있다.

선택적 비식별 관계보다는 필수적 비식별 관계를 사용하자.

  • 내부 조인만 사용해도 된다.

조인 테이블

데이터베이스 테이블의 연관관계를 설계하는 방법은 크게 2가지다.

조인 컬럼 사용(외래 키)

  • 회원이 사물함을 사용하기 전까지는 MEMBER 테이블의 LOCKER_ID 외래 키에 null을 입력해두어야 한다.
  • 선택적 비식별 관계는 외래 키에 null을 허용하므로 조인할 때 외부 조인을 사용해야 한다.

조인 테이블 사용

  • 가장 큰 단점은 테이블을 하나 추가해야 한다는 점이다.
  • 두 테이블을 조인하려면 MEMBER_LOCKER 테이블까지 추가로 조인해야 한다.

따라서 기본은 조인 컬럼을 사용하고 필요하다고 판단되면 조인 테이블을 사용하자.

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

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

@Entity
@Table(name="BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
    pkJoinColumns = @PrimaryKeyJoinColunn(name = "BOARD_DETAIL_ID"))
public class Board {

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

  private String title;

  @Column(table = "BOARD_DETAIL")
  private String content;
  ...
}
  • @SecondaryTable.name
    • 매핑할 다른 테이블의 이름
  • @SecondaryTable.pkJoinColumns
    • 매핑할 다른 ㅔㅌ이블의 기본 키 컬럼 속성
  • content 필드는 @Column(table = "BOARD_DETAIL")을 사용해서 BOARD_DETAIL 테이블의 컬럼에 매핑했다.
  • title 필드처럼 테이블을 지정하지 않으면 기본 테이블인 BOARD에 매핑된다.

@SecondaryTable을 사용해서 두 테이블을 하나의 엔티티에 매핑하는 방법보다는 테이블당 엔티티를 각각 만들어서 일대일 매핑하는 것을 권장한다.

+ 따끈한 최근 게시물