JPA 톺아보기 - 상속, @MappedSuperClass, 식별관계 매핑

Janek·2023년 1월 14일
0

JPA 톺아보기

목록 보기
4/10
post-thumbnail
post-custom-banner

해당 포스팅은 인프런에서 제공하는 김영한 님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편'을 수강한 후 정리한 글입니다. 유료 강의를 정리한 내용이기에 제공되는 예제나 몇몇 내용들은 제외하였고, 정리한 내용을 바탕으로 글 작성자인 저의 언어로 다시 작성한 글이기에 서술이 부족하거나 잘못된 내용이 있을 수 있습니다. 그렇기에 해당 글은 개념에 대한 참고 정도만 해주시고, 강의를 통해 학습하시기를 추천합니다.

상속 관계 매핑

RDB에서는 객체지향과 같은 상속 개념이 없지만 슈퍼타입서브타입 관계라는 모델링 기법이 존재한다. ORM에서의 상속 관계 매핑은 객체의 상속 구조를 RDB의 슈퍼타입-서브타입 관계로 매핑하는 것이다.

상속 관계 매핑 시 선택할 수 있는 방법은 다음과 같은 세가지 방법이 있다.

  • 각각의 테이블로 변환(조인 전략) : 슈퍼타입 테이블과 서브타입 테이블들을 각각 만들고, 조회할 때 JOIN을 사용
  • 통합 테이블로 변환(단일 테이블 전략) : 테이블 하나만을 사용해 구분 컬럼으로 구분
  • 서브타입 테이블로 변환(구현 클래스마다 테이블 전략) : 서브 타입마다 하나의 테이블로 생성

다음은 상속 관계 매핑시 사용하는 어노테이션과 그 속성 값이다.
1. @Inheritance(stragey = "매핑 전략")
2. @Discriminator(name = 구분 컬럼명)
3. @DiscriminatorValue("구분 컬럼 값")

조인 전략(Joined Strategy)

슈퍼타입과 서브타입, 즉 부모 엔티티와 자식 엔티티를 모두 테이블로 만들고 자식 테이블에 부모 테이블의 기본키를 받아 기본 키 + 외래 키로 사용하는 전략이다. 때문에 조회 시 조인을 자주 사용하며, 타입으로 구분할 수 있는 객체와 달리 타입의 개념이 없는 테이블을 구분하기 위해 구분을 위한 컬럼(DTYPE)을 추가해야 한다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Parent {
	@Id @GeneratedValue
    @Column(name = "PARENT_ID")
	...
}

@Entity
@DiscriminatorValue("A")	// 생략시 엔티티 이름 기본 사용
public class ChildA extends Parent {
	...
}

@Entity
@DiscriminatorValue("B")
public class ChildB extends Parent {
	...
}

기본 값으로 자식 테이블은 부모 테일블의 ID 컬럼명을 그대로 사용한다. 자식 테이블의 기본 키 컬럼명을 변경하고 싶은 경우 @PrimaryKeyJoinColumn을 사용하면 된다.

@Entity
@DiscriminatorValue("A")
@PrimaryKeyJoinColumn(name = "CHILD_A_ID")
public class ChildA extends Parent {
	...
}

조인 전략의 장점은 테이블이 정규화 되며, 외래 키 참조 무결성 제약조건을 활용할 수 있고 저장공간을 효율적으로 사용한다는 점이 있다.

그러나 조회할 때 조인이 많이 사용되어 성능이 저하될 수 있고, 조회 쿼리가 복잡하며, 데이터를 등록할 때 INSERT SQL을 두 번 실행한다는 단점도 존재한다.

단일 테이블 전략(Single-Table Strategy)

테이블을 하나만 사용하되 구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분하는 전략이다. 조회할 때 조인을 사용하지 않기 때문에 일반적으로 가장 빠르다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")	// 필수
public abstract class Parent {
	@Id @GeneratedValue
    @Column(name = "PARENT_ID")
	...
}

@Entity
@DiscriminatorValue("A")	// 생략시 엔티티 이름 기본 사용
public class ChildA extends Parent {
	...
}

주의해야할 점은 저장되는 자식 데이터의 컬럼이 아닌 경우 해당 컬럼에 null이 들어가기 때문에 자식 엔티티의 모든 컬럼이 null을 허용해야 한다는 점과 구분 컬럼을 필수로 사용해야 한다는 점이 있다.

단일 테이블 전략의 장점은 조인이 필요 없기에 조회 성능이 가장 빠르다는 점과 조회 쿼리가 단순하다는 점이 있지만 자식 엔티티가 매핑한 컬럼은 모두 null을 허용한다는 점과 단일 테이블에 자식 객체의 데이터를 모두 저장하기에 테이블이 커질 수 있어 상황에 따라 조회 성능이 오히려 느려질 수 있다는 단점도 존재한다.

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

자식 엔티티마다 테이블을 만들며, 자식 테이블 각각에 필요한 모든 컬럼이 존재한다. 따라서 구분 컬럼을 사용하지 않는다.

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Parent {
	@Id @GeneratedValue
    @Column(name = "PARENT_ID")
	...
}

@Entity
public class ChildA extends Parent {
	...
}

서브 타입을 구분해서 처리할 때 효과적이며, not null 제약조건을 사용할 수 있지만 여러 자식 테이블을 함께 조회할 때 UNION을 사용해야 하기에 성능이 느리며, 자식 테이블을 통합해서 쿼리하기 어렵다는 점 때문에 일반적으로 추천하지 않는 전략이다.

@MappedSuperclass

부모 클래스는 테이블과 매핑하지 않고 자식 클래스에게 매핑 정보만 제공하고 싶을 경우 @MappedSuperclass를 사용한다. 주로 객체들에서 공통적으로 사용되는 공통 매핑 정보를 정의하고 이를 상속을 통해 사용하고 싶은 경우 사용한다.

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

@Entity
public class One extends BaseEntity {
	// ID 상속
    // NAME 상속
    ...
}

@Entity
public class AnotherOne extends BaseEntity {
	// ID 상속
    // NAME 상속
    ...
}

부모 객체로 부터 물려받은 매핑 정보를 재정의하려면 @AttributeOverrides@AttributeOverride를 사용하고 연관관계를 재정의하려면 @AssociantionOverrides@AssociantionOverride를 사용한다.

// 하나의 컬럼 정보 재정의
@Entity
@AttributeOvverride(name = "id", column = @Column(name = "ONE_ID"))
public class One extends BaseEntity {...}

// 여러 개의 컬럼 정보 재정의
@Entity
@AttributeOvverrides({
	AttributeOvverride(name = "id", column = @Column(name = "ONE_ID")),
    AttributeOvverride(name = "name", column = @Column(name = "ONE_NAME"))
})
public class One extends BaseEntity {...}

@MappedSuperclass의 특징은 다음과 같다.

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아니며, 엔티티매니저.find()나 JPQL에서 사용할 수 없다.
  • 직접 생성해서 사용할 일이 거의 없으며, 추상 클래스로 만드는 것을 권장
  • ORM에서 이야기하는 진정한 상속 매핑은 아니다.

복합 키와 식별 관계 매핑

데이터베이스에서 테이블 사이의 관계는 외래 키가 기본 키에 포함되었는지 여부에 따라 식별 관계비식별 관계로 구분된다.

식별 관계와 비식별 관계

식별 관계(Identifying Relationship)

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

비식별 관계(Non-Identifying Relationship)

비식별 관계는 부모 테이블의 기본 키자식 테이블에서 외래 키로만 사용하는 관계이다. 외래 키에 null을 허용하는지 여부에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나뉜다.

최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세이지만 JPA는 이러한 식별 관계와 비식별 관계를 모두 지원한다.

복합키 : 비식별 관계 매핑

JPA에서 둘 이상의 컬럼으로 구성된 복합 기본 키를 매핑할 경우 별도의 식별자 클래스를 만들어야 한다.

JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용하며, 이를 구분하기 위해 eaualshashCode를 사용해 동등성 비교를 한다. 따라서 식별자 필드가 하나일 때 보통 자바의 기본 타입을 사용하므로 문제가 없으나 식별자 필드가 두 개 이상이면 별도의 식별자 클래스를 만들고 eaualshashCode를 구현해야 한다.

JPA는 복합 키를 지원하기 위해 좀 더 관계형 데이터베이스에 가까운 방법인 @IdClass와 객체지향에 가까운 방법인 @EmbeddedId 두 가지를 제공한다.

@IdClass

@IdClass를 사용할 때 식별자 클래스는 다음과 같은 조건들을 만족해야 한다.

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

이를 토대로 부모 클래스, 식별자 클래스, 자식 클래스를 구현하면 다음과 같다.

// 부모 클래스
@Entity
@IdClass(ParentId.class)
public class Parent {
	@Id
    @Column(name = "PARENT_ID1")
    private Long id1;
    
    @Id
    @Column(name = "PARENT_ID2")
    private Long id2;
    ...
}

// 식별자 클래스
public class ParentId implements Serializable {	
// public 이면서 Serializable 인터페이스 구현
	private Long id1;	// 엔티티의 식별자 속성명과 동일
    private Long id2;	// 엔티티의 식별자 속성명과 동일
    
    public ParentId() {}	// 기본 생성자 존재
    
    public ParentId(Long id1, Long id2) {
    	this.id1 = id1;
        this.id2 = id2;
    }
    
    // equals와 hashCode 구현
    @Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}

// 자식 클래스
@Entity
public class child {
	@Id
    private Long id;
    
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "PARENT_ID1", referencedColumns = "PARENT_ID1"),
        @JoinColumn(name = "PARENT_ID2", referencedColumns = "PARENT_ID2")
    })
    private Parent parent;
}

부모 테이블의 기본 키 컬럼이 복합 키이기 때문에 자식 테이블의 외래 키도 복합 키다. 따라서 외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns 어노테이션을 사용하고, 각각의 외래 키 컬럼을 @JoinColumn으로 매핑한다. @JoinColumnname 속성과 referencedColumnName 속성의 값이 같을 경우 referencedColumnName은 생략이 가능하다.

@EmbeddedId

좀 더 객체지향적인 방법이다. @IdClass와는 달리 식별자 클래스에 기본 키를 직접 매핑하며, 다음과 같은 조건을 만족해야 한다.

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

이를 토대로 부모 클래스, 식별자 클래스, 자식 클래스를 구현하면 다음과 같다.

// 부모 클래스
@Entity
public class Parent {
	@EmbeddedId
    private ParentId id;
    ...
}

// 식별자 클래스
@Embeddable
public class ParentId implements Serializable {	
// public 이면서 Serializable 인터페이스 구현
	@Column(name = "PARENT_ID1")
	private Long id1;
    @Column(name = "PARENT_ID2")
    private Long id2;
    
    public ParentId() {}	// 기본 생성자 존재
    ...
    // equals와 hashCode 구현
    @Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}

// 자식 클래스
@Entity
public class child {
	@Id
    private Long id;
    
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "PARENT_ID1", referencedColumns = "PARENT_ID1"),
        @JoinColumn(name = "PARENT_ID2", referencedColumns = "PARENT_ID2")
    })
    private Parent parent;
}

복합키 : 식별 관계 매핑

식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로 @IdClass@EmbeddedId를 사용해서 식별자를 매핑해야 한다.

@IdClass

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

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

// 자식 ID
public class ChildId implements Serializable {
	private Long parent;	// Child.parent 매핑
   	private Long childId;	// Child.childId 매핑
    
    // equals, hashCode...
}

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

@EmbeddedId

@EmbeddedId로 구성할 때는 @MapsId를 사용한다.

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

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

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

@IdClass와 다른 점은 @Id 대신에 @MapsId를 사용한다는 점이며, 이는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 의미이다. 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.

일대일 식별 관계

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

// 부모 클래스
@Entity
public class Sample {
	@Id	@GeneratedValue	
    @Column(name = "SAMPLE_ID")
    private Long id;
    
    @OneToOne(mappedBy = "smaple")
    private SampleDetail sampleDetail;
}

// 자식 클래스
@Entity
public class SampleDetail {
	@Id
    private Long id;
    
	@MapsId
    @OneToOne
    @JoinColumn(name = "SAMPE_ID")
    public Sample sample;
    ...
}

식별자가 단순히 컬럼 하나일 경우 @MapsId를 사용하고 속성 값을 사용하지 않으면 되며, @Id를 사용해서 식별자로 지정한 부모 테이블의 기본 키와 매핑된다.

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

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

  1. 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 결국 JOIN할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
  2. 두 개 이상의 컬럼을 합해서 복합 기본 키로 만들어야 하는 경우가 많다.
  3. 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다. 반면 비식별 관계의 기본 키는 비즈니스와 전혀 관계 없는 대리 키를 주로 사용한다. 비즈니스 요구사항은 언젠간 변할 수 있기 때문에 해당 식별 관계의 자연 키 컬럼들이 전파되며 변경하기 힘들어진다.
  4. 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 비식별 관계에 비해 테이블 구조가 유연하지 못하다.
  5. 일대일 관계를 제외하고 식별 관계는 두 개 이상의 컬럼을 묶은 복합 기본 키를 사용한다. JPA에서 이를 구현하기 위해서는 별도의 복합 키 클래스를 만들어서 사용해야 하며, 한 개의 대리 키 사용에 비해 많은 리소스가 필요하다.
  6. JPA는 @GeneratedValue와 같이 편리하게 대리 키를 생성하기 위한 방법을 제공해주기 때문에 비식별 관계로 생성하기가 보다 쉽다.

반면 식별 관계가 가지는 장점은 기본 키 인덱스를 활용하기 좋고, 상위 테이블의 기본 키 컬럼을 자식 테이블들이 가지고 있기 때문에 특정 상황에서 조인 없이 하위 테이블만으로 검색을 완료할 수 있다는 점이 있다.

profile
만들고 나누며, 세상을 이롭게 하고 싶습니다.
post-custom-banner

0개의 댓글