본문 바로가기
개인 공부/JPA (자바 ORM 표준 JPA 프로그래밍)

[자바 ORM 표준 JPA 프로그래밍] 고급 매핑 (7장)

by 희조당 2022. 12. 12.
728x90


📌 상속 관계 매핑

관계형 데이터베이스에는 상속이 없다. 그나마 '슈퍼타입-서브타입 관계' 모델링 기법이 상속과 비슷하다.

즉, ORM에서의 상속 관계 매핑은 이 모델링 기법이고 3가지 방법으로 구현할 수 있다.

  • 조인 전략 : 각각을 모두 테이블로 만들고 조회 시 조인을 사용한다.
  • 단일 테이블 전략 : 테이블을 하나만 사용해 통합한다. 
  • 구현 클래스 테이블 전략 : 서브 타입마다 하나의 테이블을 만든다.

1️⃣ 조인 전략

모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아 기본 키 + 외래 키로 사용한다.

테이블은 타입의 개념이 없어 타입을 구분하는 칼럼을 추가해야 한다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 상속의 부모 클래스, 전략 : 조인
@DiscriminatorColumn(name = "DTYPE") // 구분 칼럼 지정
public abstract class Item {
    @Id @GeneratedValue
    @Column(name="item_id")
    private Long id;
	...
}

@Entity
@DiscriminatorValue("A") // 자식 클래스, 구분 칼럼에 "A" 지정
public class Album extends Item {
	...
}

😋 매핑 정보 분석하기

  • @Inheritance(stratege = " ") : 부모 클래스에 사용한다. 매핑 전략을 지정해야 한다.
  • @DiscriminatorColumn(name = " ") : 부모 클래스에 붙여 자식 테이블을 구분하게 해준다.
  • @DiscriminatorValue(" ") : 엔티티를 저장할 때 구분 칼럼에 입력할 값 지정한다.
  • @PrimaryKeyJoinColumn(name = " ") : 자식 테이블의 기본 키 칼럼명을 변경할 때 쓴다.
더보기

🧷 장점 

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

🧷 단점

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

🧷 특징

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

2️⃣ 단일 테이블 전략

이름 그대로 테이블을 단 하나만 사용한다.

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

만약 Book을 저장하면 Actor 같은 다른 칼럼을 사용하지 않기 때문에 매핑한 칼럼 모두 null을 허용해야 한다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 상속의 부모 클래스, 전략 : 단일 테이블
@DiscriminatorColumn(name = "DTYPE") // 구분 칼럼 지정
public abstract class Item {
    @Id @GeneratedValue
    @Column(name="item_id")
    private Long id;
	...
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item { ... }

@Entity
@DiscriminatorValue("B")
public class Movie extends Item { ... }
더보기

🧷 장점 

  • 조인이 필요 없어서 조회가 빠르고 쿼리가 단순하다.

🧷 단점

  • 자식 엔티티가 매핑한 칼럼 모두 null을 허용해야 한다.
  • 단일 테이블에 모든 것을 저장해서 테이블이 커질 수 있다. 즉, 상황에 따라 성능이 떨어진다.

🧷 특징

    • 구분 칼럼을 꼭 사용해야 한다. (@DiscriminatorColumn)
    • 구분 칼럼을 지정하지 않으면 엔티티 이름을 사용한다.

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

자식 엔티티마다 테이블을 만들고, 각 엔티티에 필요한 칼럼을 모두 넣는다.

일반적으로 추천하지 않는 전략이다.

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) // 상속의 부모 클래스, 전략 : 구현 클래스
public abstract class Item {
    @Id @GeneratedValue
    @Column(name="item_id")
    private Long id;
	...
}

@Entity
public class Album extends Item { ... }

@Entity
public class Movie extends Item { ... }
더보기

🧷 장점 

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

🧷 단점

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

🧷 특징

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

📌 @MappedSuperclass

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

물려받은 매핑 정보를 수정하려면 @AttributeOverride 혹은 @AttributeOverrides를 사용한다.

연관관계를 재정의하려면 @AssociationOverride 혹은 @AssociationOverrides를 사용한다.

 

매핑 정보를 모아주는 역할일 뿐 ORM에서 이야기하는 진정한 상속은 슈퍼-서브타입 매핑이다.


📌 복합 키와 식별 관계 매핑

1️⃣ 식별 관계 vs 비식별 관계

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

2️⃣ 복합 키 : 비식별 관계 매핑

JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 한다.

식별자를 equals와 hashCode로 구분하기 때문에 구현한 식별자 클래스에 두 메소드를 구현해야 한다. 

🌀IdClass

관계형 데이터베이스에 가까운 복합키 방식이다.

@Entity
@IdClass(ParentId.class) // ParentId라는 식별자 클래스로 구분
public class Parent { // 부모 엔티티
  @Id
  @Column(name = "PARENT_ID1")
  private String id1; // ParentId.id1과 연결
  
  @Id
  @Column(name = "PARENT_ID2")
  private String id2; // ParentId.id2과 연결
}

부모 엔티티에 @IdClass로 사용할 식별자 클래스를 지정해준다.

 

public class ParentId implements Serializable {
  private String id1;
  private String id2;
  
  public ParentId(String id1, String id2) {
    this.id1 = id1;
    this.id2 = id2;
  }
  
  @Override
  public boolean equals(Object o) {...}
  
  @Override
  public int hashCode() {...}
}

@IdClass를 사용하는 식별자 클래스는 다음 조건을 꼭 만족해야 한다.

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

자식 클래스 추가는 다음과 같다.

@Entity
public Class Childe {
  @Id
  private String id;
  
  @ManyToOne
  @JoinColumns({ // 부모의 키를 모두 외래 키로 등록
    @JoinColumn(name = "PARENT_ID1",
      referencedColumnName = "PARENT_ID1"),
    @JoinColumn(name = "PARENT_ID2",
      referencedColumnName = "PARENT_ID2")})
  private Parent parent;
}

🌀 EmbeddedId

좀 더 객체지향적으로 복합 키를 매핑하는 방법이다.

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

부모 엔티티에 식별자 클래스를 직접 사용하고 @Embedded 어노테이션을 적어주면 된다.

 

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

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

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

🧐 왜 equals()와 hashCode()를 구현해야 할까??

영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리한다.

키를 비교할 때 equals()와 hashCode()를 사용하는데, 객체를 대상으로 equals()를 사용하면 동일성 비교(참조 비교)가 이루어지기 때문이다.

따라서, 오버라이딩을 하지 않으면 예상과 다른 엔티티를 찾게 되는 등의 문제가 발생한다.

3️⃣ 복합 키 : 식별 관계 매핑

비식별 관계와 마찬가지로 @IdClass와 @EmbeddedId를 사용해서 식별자를 매핑해야 한다. 

🌀 IdClass

@Entity // 부모 엔티티
public class Parent {
  @Id @Colum(name = "PARENT_ID")
  private String id;
}

 

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

// 자식 식별자 클래스
public class ChildId implements Serializable {
  private String parent;
  private String childId;
  
  // equals, hashCode
}

Childe 엔티티는 @Id, @ManyToOne, 그리고 @JoinColumn을 통해서 기본 키와 외래 키를 같이 매핑한다.

 

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

// 손자 식별자 클래스
public class GrandChildId implements Serializable {
  private Child child;
  private String id;
  
  // equals, hashCode
}

🌀 EmbeddedId

@Entity // 부모 클래스
public class Parent {
  @Id @Column(name = "PARENT_ID")
  private String id;
}

 

@Entity // 자식 엔티티
public class Child {
  @EmbeddedId
  private ChildId id;
  
  @MapsId("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 id;
  
  // equals, hashCode
}

 

@Entity // 손자 엔티티
public class GrandChild {
  @EmbeddedId
  private GrandChildId id;
  
  @MapsId("childId")
  @ManyToOne
  @JoinColumns({{
    @JoinColumn(name = "PARENT_ID"),
    @JoinColumn(name = "CHILD_ID")})
  private Child child;
}

@Embeddable // 손자 식별자 클래스
public class GrandChildId implements Serializable {
  private ChildId childId;
  
  @Column(name = "GRANDCHILD_ID")
  private String id;
  
  // equals, hashCode
}

 

 

댓글