설모의 기록

[Spring] 상속 관계 매핑 본문

언어/Spring

[Spring] 상속 관계 매핑

HA_Kwon 2018. 10. 5. 23:27


상속 관계 매핑


여러 클래스를 구현할 때 공통된 기능이나 변수가 있다면 상위 클래스를 만들어 그 곳에 모아두는 '상속' 을 이용하는 경우가 많습니다. 가장 대표적인 예로 Animal 클래스라는 상위 클래스를 만들어 bark() 라는 함수를 만들어두고, 하위 클래스로 Dog, Lion, Cat 등을 만들어 bark() 함수를 오버라이딩하는 경우입니다. 

그렇다면 객체의 상속 관계가 데이터베이스에서는 어떻게 매핑이 될까요? 이에 대한 설명을 아래에 기록하겠습니다.




상속 관계 매핑

사실 RDB에서는 '상속' 개념이 따로 있지 않습니다. 따라서 ORM 에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것입니다.

슈퍼타입-서브타입 논리 모델을 실제 물리 모델인 테이블로 구현하는 방법에는 다음과 같이 3가지 방법이 있습니다.

  • 각각의 테이블로 변환
  • 통합 테이블로 변환
  • 서브타입 테이블로 변환
위의 3가지 방법에 대해 더 자세히 알아보겠습니다.

설명에 앞서 기본이 되는 객체 상속 모델의 형태는 위의 이미지와 같습니다.




(1) 각각의 테이블로 변환


각각의 테이블로 변환하는 것은 JPA 에서 조인전략이라고 합니다. 위의 테이블 형태와 같이 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아 (기본 키 + 외래 키) 로 사용하는 전략입니다. 이 때, Food 테이블에 있는 음식이 Rice 인지, Pasta 인지, Snack 인지를 알아보기 위해 DTYPE 과 같이 타입을 구분하는 컬럼을 추가해야 합니다. 이를 JPA 를 이용해 코드로 나타내면 아래와 같습니다.

import javax.persistence.*;

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract class Food {
@Id @GeneratedValue
@Column(name = "FOOD_ID")
private Long id;

private String name;
private int price;
}

// Food.java

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("P")
public class Pasta extends Food {
private String restaurant;
}

// Pasta.java
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("R")
public class Rice extends Food {
private String region;
}

// Rice.java
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("S")
public class Snack extends Food {
private String company;
private int type;
}

// Snack.java

위의 코드에서 사용한 주요 어노테이션은 다음과 같습니다.

  • @Inheritance(strategy = InheritanceType.JOINED) : 상속 매핑은 부모 클래스에 Inheritance 어노테이션을 사용해야 합니다. 이 때의 전략으로는 조인전략을 사용했습니다.
  • @DiscriminatorColumn : 부모 클래스에 구분 컬럼을 지정합니다. 기본값이 DTYPE 이기 때문에 저는 컬럼 이름을 따로 지정하지 않았습니다.
  • @DiscriminatorValue("S") : 엔티티 저장 시 구분 컬럼에 입력할 값을 지정합니다. "S" 라 지정한다면 엔티티를 저장할 때 부모 클래스인 Food 의 DTYPE 에 S 가 저장됩니다.


위와 같은 방식의 조인 전략을 사용한다면 다음과 같은 특징이 있습니다.

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

  • 단점 - 조회할 때 조인이 많이 사용되기 때문에 성능이 저하될 수 있습니다. - 조회 쿼리가 복잡합니다. - 데이터를 등록할 때 부모 클래스와 자식 클래스 모두 저장해야하기 때문에 INSERT 쿼리가 두 번 실행됩니다.




(2) 단일 테이블 전략


단일 테이블 전략은 말 그대로 테이블을 하나만 사용하는 방식입니다. 이 때, DTYPE 과 같이 구분 컬럼으로 어떤 자식 데이터가 저장되었는지를 구분합니다. 이 전략은 조회할 때 조인을 사용하지 않기때문에 일반적으로 가장 빠릅니다. 그러나 예를 들어 파스타 데이터를 저장하려 할 때 REGION 이나 COMPANY, TYPE 과 같은 컬럼에는 null 값이 저장되게 됩니다. 따라서 자식 엔티티에 매핑된 컬럼은 모두 null 을 허용해야 엔티티 별로 데이터를 저장할 수 있습니다.

이를 JPA 를 이용해 코드로 나타내면 다음과 같습니다.

import javax.persistence.*;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract class Food {
@Id @GeneratedValue
@Column(name = "FOOD_ID")
private Long id;

private String name;
private int price;
}

// Food.java
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("P")
public class Pasta extends Food {
private String restaurant;
}

// Pasta.java
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("R")
public class Rice extends Food {
private String region;
}

// Rice.java
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("S")
public class Snack extends Food {
private String company;
private int type;
}

// Snack.java

이 때, @Inheritance 의 전략을 InheritanceType.SINGLE_TABLE 로 지정하면 단일 테이블 전략을 사용합니다. 

위와 같은 방식의 단일 테이블 전략을 사용한다면 다음과 같은 특징이 있습니다.

  • 장점 - 조인이 필요하지 않기 때문에 일반적으로 조회 성능이 빠릅니다. - 조회 쿼리가 단순합니다.

  • 단점 - 자식 엔티티가 매핑한 컬럼은 모두 null 을 허용해야 한다. - 단일 테이블에 모든 것을 저장하기 때문에 테이블이 커질 수 있습니다. 따라서 어떤 상황에서는 조인 성능이 오히려 느려질 수도 있습니다.

  • 주의 - 구분 컬럼을 꼭 사용해야 합니다. 즉 @DiscriminatorColumn 을 꼭 설정해야 합니다.




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


구현 클래스마다 테이블 전략은 위의 그림과 같이 각각의 테이블을 만드는 전략입니다. 그리고 자식 테이블 각각에 필요한 컬럼이 모두 존재합니다. 이 방법은 일반적으로 추천하지 않는 전략입니다. JPA 를 이용해 코드로 나타내면 아래와 같습니다.

import javax.persistence.*;

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn
public abstract class Food {
@Id @GeneratedValue
@Column(name = "FOOD_ID")
private Long id;

private String name;
private int price;
}

// Food.java

나머지 자식 클래스 코드는 위와 똑같아서 생략하겠습니다. 

@Inheritance 의 전략을 InheritanceType.TABLE_PER_CLASS 로 지정하시면 각 음식별로 테이블이 생성됩니다. 이 전략의 장단점은 아래와 같습니다.

  • 장점 - 서브 타입을 구분해서 처리할 때 효과적이다. - 단일테이블과 비교해보면 not null 제약조건을 사용할 수 있습니다.

  • 단점 - 여러 자식 테이블을 함께 조회할 때 SQL 의 union 을 사용해야하기 때문에 성능이 느립니다. - 자식 테이블을 통합해 쿼리를 적용하기가 어렵다.






@MappedSuperclass

위의 상속 관계 매핑은 부모 클래스(Food) 와 자식 클래스(Rice, Pasta, Snack) 가 모두 데이터베이스 테이블과 매핑되었습니다. 즉 각각의 클래스가 데이터베이스 테이블이 되는 구조였습니다. 이에 반해 @MappedSuperclass 는 부모크래스를 데이터베이스 테이블로 매핑하지 않고 자식 클래스에게 매핑 정보만 제공하고 싶을 때 사용합니다. 

즉, 위의 예제들처럼 Food 클래스에 @Entity 를 붙이면 테이블이 생성되지만, @MappedSuperclass 는 실제 테이블과 매핑되지 않는다는 것입니다. 

아래의 예제를 보겠습니다. 



위의 테이블을 보면 전혀 관계가 없는 Board, Product 테이블이 있습니다. 하지만 id, created_at, updated_at 이 세 개의 속성은 공통으로 가지고 있습니다. 이제 @MappedSuperclass 를 이용해 공통 속성을 부모 클래스로 모은 후에 Board, Product 클래스와 상속 관계를 만들어보겠습니다.


import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue
private Long id;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;

}
import javax.persistence.Entity;

@Entity
public class Board extends BaseEntity {
private String writer;
private String title;
private String contents;
}
import javax.persistence.Entity;

@Entity
public class Product extends BaseEntity {
private String name;
private Long ownerId;
private int count;
}

위의 코드와 같이 BaseEntity 에는 공통 매핑 정보를 정의했습니다. 자식 엔티티들은 상속을 통해 BaseEntity 의 매핑 정보를 물려받았습니다. 

위의 예제에서는 BaseEntity 가 데이터베이스 테이블로 매핑될 필요가 없기 때문에 @MappedSuperclass 를 사용했습니다. 이 때, 자식 엔티티에서 부모로부터 물려받은 매핑 정보를 재정의하려면 @AttributeOverrides 또는 @AttributeOverride 를 이용해 속성 이름을 변경할 수 있습니다. 

@MappedSuperclass 방식의 특징은 아래와 같습니다.

  • 특징 - 테이블과 매핑되지 않고 자식 클래스에게 매핑 정보를 상속하기 위해 사용합니다. - @MappedSuperclass 로 지정한 클래스는 엔티티가 아니기 때문에 em.find() 또는 JPQL 에서 사용할 수 없습니다. - @MappedSuperclass 로 지정한 클래스를 이용해 객체를 생성해 사용할 일은 거의 없기 때문에 추상 클래스로 만드는 것을 권장합니다.


위의 모든 내용의 출처는 자바 ORM 표준 JPA 프로그래밍 서적입니다.

Comments