웹 프로그래밍/Spring JPA

[Spring JPA] 상속 ( JOINED 전략을 중심으로 )

빅토리_ 2018. 4. 29. 14:52


객체지향 설계

JPA는 데이터베이스 모델링을 할 때 객체 지향적으로 설계를 합니다.

예를들어 Movie , Music , Book 이라는 테이블이 있을 때 3개의 테이블에는 공통적으로 고유 번호( no ), 이름( name ) , 가격( price )가 있을 수 있습니다.

공통적인 속성은 Item이라는 테이블로 만들어서 상속 받도록 하면 객체 지향적으로 설계가 이루어진 것입니다.



이렇게 객체지향 설계가 이루어졌을 때 JPA에서는 상속이라는 관계를 해결할 수 있어야 합니다.

JPA는 상속을 해결하기 위해 3가지의 방식을 지원합니다.



1) JOINED

JOINED 방식은 no, name, price 속성을 갖는 Item이라는 테이블을 생성하여 Movie , Music , Book 테이블이 Item의 PK를 외래키로 갖는 방식입니다.

정규화가 된 모델링을 사용하기 때문에 데이터의 중복이 없으므로 가장 많이 사용되는 방법입니다.


2) SINGLE_TABLE

SINGLE_TABLE 방식은 Movie , Music , Book 각 테이블의 속성들을 Item 테이블의 속성으로 합치는 방식입니다.

이름 그대로 하나의 테이블로 처리하겠다는거죠.


3) TABLE_PER_CLASS

TABLE_PER_CLASS 방식은 부모의 속성들을 Movie , Music , Book 테이블의 속성으로 갖는 방식입니다.

이 경우 join을 사용하지 않고 union을 사용하는데, union 쿼리는 사용하지 않는 것이 좋으므로 이 전략은 잘 사용하지 않기 때문에 예제도 생략할 것입니다.





1. JOINED 전략

@Entity
@Inheritance(strategy = InheritanceType.JOINED)	// 상속 전략
@DiscriminatorColumn(name="type")		// 구분 하는 칼럼	
public abstract class Item {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="no")
	private Integer no;
	
	@Column(name="name")
	private String name;
	
	@Column(name="price")
	private Integer price;
}


@Entity
@DiscriminatorValue("movie")
public class Movie extends Item{
	private String actor;
}


@Entity
@DiscriminatorValue("music")
public class Music extends Item{
	private String artist;
}


@Entity
@DiscriminatorValue("book")
public class Book extends Item{
	private String writer;
}

@Inheritance 어노테이션으로 어떤 상속 전략을 사용 할 것인지 명시합니다.


@DiscriminatorColumn 어노테이션은 어떤 자식인지 구분하는 역할을 하는 칼럼입니다.

JOINED 전략은 이름 그대로 조인을 자주 사용하는 방식입니다.

객체는 타입으로 어떤 객체인지 구분할 수 있지만 테이블은 타입의 개념이 없기 때문에,

부모 엔티티는 자식 엔티티를 구분 할 수 있도록 타입을 구분하는 컬럼을 추가해야 합니다.


@DiscriminatorValue 어노테이션은 부모가 자식을 구분할 때 자신이 어떤 값을 갖는지 지정하는 역할을 합니다.

( 한글 값도 가능합니다. )


이렇게 어노테이션을 작성하면 JPA가 수행하는 쿼리는 다음과 같습니다.



테이블이 총 4개가 생성됩니다.

Item 테이블에는 자식 테이블을 구분할 수 있는 type 칼럼을 자동으로 생성합니다.

( 스프링 MVC에서는 Item 클래스에 String 타입으로 type 멤버 변수를 정의하고 getter, setter를 추가해야 type 칼럼에 접근할 수 있습니다. )


Book , Movie , Music 테이블은 자신의 PK를 외래키로서 Item의 PK를 갖습니다.

즉 식별 관계임을 알 수 있습니다.



테이블을 생성했으니, 이제 데이터를 추가해보겠습니다.
private static void insert(EntityManager em) {
	Book book = new Book();
	book.setName("자바의 정석");
	book.setPrice(30000);
	book.setWriter("남궁성");
	em.persist(book);
	
	Movie movie = new Movie();
	movie.setName("어벤져스");
	movie.setPrice(12000);
	movie.setActor("크리스 헴스워스");
	em.persist(movie);
}


실제 추가하려는 데이터는 Item이 아닌, 책과 영화이므로 Book 객체, Movie 객체를 생성해서 영속화했습니다.

수행되는 쿼리는 먼저 Item 테이블에 name, price, type의 값을 추가하고, Book 테이블에 writer 값을 Movie 테이블에 actor 값을 추가합니다.

이 때 type은 실제 테이블의 값으로 고정된 것을 확인할 수 있습니다.



다음으로 Item의 모든 데이터를 조회해보겠습니다.

private static void findAll(EntityManager em) {
	String jpql = "SELECT i FROM Item i";
	TypedQuery<Item> query = em.createQuery(jpql, Item.class);
	List<Item> itemList = query.getResultList();
	
	for(Item item : itemList){
		System.out.println(item.getName() + " " + item.getClass().getName());
	}
}


수행되는 쿼리를 보니 Item 엔티티의 자식 엔티티들과 join이 일어난 것을 확인할 수 있습니다.

JOINED 전략은 이처럼 자식 엔티티들을 조회할 때 join을 사용합니다.



이상으로 JOINED 전략으로 상속 관계를 해결하는 방법에 대해 알아보았습니다.

JOINED 방식은 정규화된 테이블을 사용하므로 데이터의 중복 문제 해결 및 무결성이 보장됩니다.

하지만 SELECT 쿼리 시 join이 많이 일어나기 때문에 성능상의 문제가 발생할 수 있으며, 데이터를 추가할 때 INSERT 쿼리가 2번 실행됩니다.





2. SINGLE TABLE 전략

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)	// 상속 전략
@DiscriminatorColumn(name="d_type")		// 구분 하는 칼럼	
public abstract class Item {

}

JOINED 전략에서 사용했던 코드를 그대로 사용하며, Item 클래스에서 상속 전략 부분만 수정하면 됩니다.

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)



결과를 보시면 Music의 artist 칼럼, Book의 wrtier 칼럼 , Movie의 actor 칼럼이 Item 테이블의 칼럼으로 추가되는 것을 확인할 수 있습니다.



SINGLE_TABLE 방식은 SELECT 쿼리를 수행할 때 join을 사용할 필요가 없으므로 성능이 가장 좋고, 쿼리가 단순합니다.

그런데 필드가 많아질수록 테이블의 크기가 커지게 되는데, 이 경우 오히려 성능이 나빠질 수 있습니다.





3. @MappedSuperclass

Movie와 Book은 객체 지향적으로 볼 때 Item 객체에서 no, name, price 속성을 받지만, 테이블로서 매핑이 되지 않기를 바랄 때 사용하는 방식입니다.

객체지향적으로 상속 관계에는 있지만 엔티티에는 영향을 주기 싫은 경우 사용합니다.

사용하는 방식은 부모 클래스에서 @Entity 대신에 @MappedSuperclass를 달아주면 됩니다.

@MappedSuperclass
public abstract class Item {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name = "no")
	private Integer no;
	
	@Column(name = "name")
	private String name;
	
	@Column(name = "price")
	private Integer price;
}


@Entity
@AttributeOverride(name="no", column=@Column(name="actor_no"))
public class Movie extends Item{
	private String actor;
}


@Entity
@AttributeOverrides({
	@AttributeOverride(name="no", column=@Column(name="book_no")),
	@AttributeOverride(name="name", column=@Column(name="book_name"))
})
public class Book extends Item{
	private String writer;
}

부모 클래스의 특정 칼럼을 오버라이딩 하고 싶을 경우 @AttributeOverride 어노테이션을 사용합니다.

부모 클래스의 여러 개 칼럼을 오버라이딩 하고 싶을 경우 @AttributeOverrides 어노테이션을 사용합니다.



결과를 보시면 부모 클래스의 모든 필드를 물려 받았고, 이름도 변경되었습니다.

그리고 부모 클래스인 Item 테이블은 생성되지 않았습니다.





이상으로 객체지향 설계에서 JPA가 상속을 해결하여 테이블을 정의하는 방법에 대해 알아보았습니다.