본문 바로가기

Backend/Spring

고급 매핑

상속 관계 매핑

관계형 DB에는 상속 관계라는 것은 없다.
하지만 슈퍼 타입, 서브 타입 관계라는 모델링 기법이 있는데, 객체 상속과 유사하다.
상속 관계 매핑이라는 것은 객체의 상속 구조와 DB의 슈퍼 타입 서브 타입 관계를 매핑하는 것이다.

 
상속 관계 매핑에는 다음 세 가지 방법이 있다.
- 조인 전략: 각각 테이블로 변환
- 단일 테이블 전략: 통합 테이블로 변환
- 구현 클래스마다 테이블 전략: 구현 클래스마다 테이블로 변환
 

조인 전략

조인 전략의 경우, 다음 장점과 단점을 가지고 있다.
 
장점
- 테이블 정규화
- 외래 키 참조 무결성 제약 조건 활용 가능
- 저장 공간 효율화
 
여기서 외래 키 참조 무결성 제약 조건이라는 것은 외래 키는 참조할 수 없는 값을 가질 수 없다는 것이다.
 
단점
- 조회 시 조인을 많이 사용, 성능 저하
- 조회 쿼리가 복잡하다.
- 데이터 저장 시 INSERT SQL 2번 호출
 

단일 테이블 전략

단일 테이블 전략은 다음과 같은 장점과 단점을 가지고 있다.
 
장점 
- 조인이 필요 없기 때문에 일반적으로 조회 성능이 빠르다.
- 조회 쿼리가 단순하다.
 
단점
- 자식 엔티티가 매핑한 컬럼은 모두 null 허용
- 단일 테이블에 모든 것을 저장하기 때문에 테이블이 커질 수 있다.
- 상황에 따라 조회 성능이 오히려 느려질 수 있다.
 

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

이 전략은 사용하지 않는 것이 좋다.
 
장점과 단점은 다음과 같다.
 
장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적이다.
- not null 제약 조건 사용 가능
 
단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다. (UNION SQL)
- 자식 테이블을 통합해서 쿼리하기 어렵다.
 

어노테이션

@Inheritance(strategy=InheritanceType.XXX)
- XXX에는 JOINED, SINGLE_TABLE, TABLE_PER_CLASS가 들어갈 수 있다.
JOINED는 조인 전략, SINGLE_TABLE은 단일 테이블 전략, TABLE_PER_CLASS는 구현 클래스마다 테이블 전략이다.
 

Mapped Superclass - 매핑 정보 상속

@Mapped Superclass 어노테이션은 아래 사진과 같이 공통 매핑 정보가 필요한 경우 사용한다.

이 MappedSuperclass는 상속 관계 매핑도, 엔티티도 아니고, 테이블과 매핑도 되지 않는다.
그냥 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공하는 것이다.
직접 조회, 검색이 불가하고, 생성해서 사용할 일이 없기 때문에 추상 클래스로 사용하는 것이 좋다.
추가로, @Entity 어노테이션이 붙은 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속이 가능하다.
 
이제 예제로 알아보자.
 

실전 예제

다음과 같이 도서, 음반, 영화 같은 세부 상품도 추가했다.

 
도메인 모델과 테이블 설계는 아래와 같다.

package jpabook.jpashop.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

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

	private String name;
	private int price;
	private int stockQuantity;

	@ManyToMany(mappedBy = "items")
	private List<Category> categories = new ArrayList<>();

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getPrice() {
		return price;
	}

	public void setPrice(int price) {
		this.price = price;
	}

	public int getStockQuantity() {
		return stockQuantity;
	}

	public void setStockQuantity() {
		this.stockQuantity = stockQuantity;
    }
}
package jpabook.jpashop.domain;

import javax.persistence.Entity;

@Entity
public class Album extends Item {
	private String artist;
	private String etc;

	public String getArtist() {
		return artist;
	}

	public void setArtist(String artist) {
		this.artist = artist;
	}

	public String getEtc() {
		return etc;
	}

	public void setEtc(String etc) {
		this.etc = etc;
	}
}
package jpabook.jpashop.domain;

import javax.persistence.Entity;

@Entity
public class Book extends Item {
	private String author;
	private String isbn;

	public String getAuthor() {
		return author;
	}

	public void setAuthor(String author) {
		this.author = author;
	}

	public String getIsbn() {
		return isbn;
	}

	public void setIsbn(String isbn) {
		this.isbn = isbn;
	}
}
package jpabook.jpashop.domain;

import javax.persistence.Entity;

@Entity
public class Movie extends Item {
	private String director;
	private String actor;

	public String getDirector() {
		return director;
	}

	public setDirector(String director) {
		this.director = director;
	}

	public String getActor() {
		return actor;
	}

	public void setActor(String actor) {
		this.actor = actor;
	}
}

상속 관계 매핑을 추가해보았다.
Album, Movie, Book 엔티티를 추가했는데, Item 엔티티를 상속받게 해서 진행했다.
전략은 단일 테이블 전략을 사용했다.
 
이번에는 BaseEntity도 한 번 만들어보자.

package jpabook.jpashop.domain;

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

@MappedSuperclass
public abstract class BaseEntity {
	private String createdBy;
	private LocalDateTime createDate;
	private String lastModifiedBy;
	private LocalDateTime lastModifiedDate;

	public String getCreatedBy() {
		return createdBy;
	}

	public void setCreatedBy(String createdBy) {
		this.createdBy = createdBy;
	}

	public LocalDateTime getCreatedDate() {
		return createdDate;
	}

	public void setCreatedDate(LocalDateTime createdDate) {
		this.createdDate = createdDate;
	}

	public String getLastModifiedBy() {
		return lastModifiedBy;
	}

	public void setLastModifiedBy(String lastModifiedBy) {
		this.lastModifiedBy = lastModifiedBy;
	}

	public LocalDateTime getLastModifiedDate() {
		return lastModifiedDate;
	}

	public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
		this.lastModifiedDate = lastModifiedDate;
	}
}
package jpabook.jpashop.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

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

	private String name;
	private int price;
	private int stockQuantity;

	@ManyToMany(mappedBy = "items")
	private List<Category> categories = new ArrayList<>();

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getPrice() {
		return price;
	}

	public void setPrice(int price) {
		this.price = price;
	}

	public int getStockQuantity() {
		return stockQuantity;
	}

	public void setStockQuantity() {
		this.stockQuantity = stockQuantity;
    }
}
package jpabook.jpashop.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Member extends BaseEntity {
	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;

	private String name;
	private String city;
	private String street;
	private String zipcode;
	
	@OneToMany(mappedBy = "member")
	private List<Order> orders = new ArrayList<>();

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public String getStreet() {
		return street;
	}

	public void setStreet(String street) {
		this.street = street;
	}

	public String getZipcode() {
		return zipcode;
	}

	public void setZipcode(String zipcode) {
		this.zipcode = zipcode;
	}
}
package jpabook.jpashop.domain;

import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.time.LocalDateTime;

@Entity
@Table(name = "ORDERS")
public class Order extends BaseEntity {
	@Id @GeneratedValue
	@Column(name = "ORDER_ID")
	private Long id;

//	  @Column(name = "MEMBER_ID")
//	  private Long memberId;

	@ManyToOne
	@JoinColumn(name = "MEMBER_ID")
	private Member member;
	
	@OneToOne
	@JoinColumn(name = "DELIVERY_ID")
	private Delivery delivery;
		
	@OneToMany(mappedBy = "order")
	private List<OrderItem> orderItems = new ArrayList<>();

	private LocalDateTime orderDate;
		
	@Enumerated(EnumType.STRING)
	private OrderStatus status;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public Member getMember() {
		return member;
	}

	public void setMember(Member member) {
		this.member = member;
	}

	public LocalDateTime getOrderDate() {
		return orderDate;
	}

	public void setOrderDate(LocalDateTime orderDate) {
		this.orderDate = orderDate;
	}

	public OrderStatus getStatus() {
		return status;
	}

	public void setStatus(OrderStatus status) {
		this.status= status;
	}
}
package jpabook.jpashop.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class OrderItem extends BaseEntity {
	@Id @GeneratedValue
	@Column(name = "ORDER_ITEM_ID")
	private Long id;

//	  @Column(name = "ORDER_ID")
//	  private Long orderId;
	
	@ManyToOne
	@JoinColumn(name = "ORDER_ID")
	private Order order;

//	  @Column(name = "ITEM_ID")
//	  private Long itemId;
		
	@ManyToOne
	@JoinColumn(name = "ITEM_ID")
	private Item item;
		
	private int orderPrice;
	private int count;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public Order getOrder() {
		return order;
	}

	public void setOrder(Order order) {
		this.order = order;
	}

	public Item getItem() {
		return item;
   	}

	public void setItem(Item item) {
		this.item = item;
	}

	public int getOrderPrice() {
		return orderPrice;
	}

	public void setOrderPrice(int orderPrice) {
		this.orderPrice = orderPrice;
	}

	public int getCount() {
		return count;
	}

	public void setCount(int count) {
		this.count = count;
	}
}
package jpabook.jpashop.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Delivery extends BaseEntity {
	@Id @GeneratedValue
	private Long id;

	private String city;
	private String street;
	private String zipcode;
	private DeliveryStatus status;

	@OneToOne(mappedBy = "delivery")
	private Order order;
}
package jpabook.jpashop.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Category extends BaseEntity {
	@Id @GeneratedValue
	private Long id;

	private String name;

	@ManyToOne
	@JoinColumn(name = "PARENT_ID")
	private Category parent;

	@OneToMany(mappedBy = "parent")
	private List<Category> child = new ArrayList<>();

	@ManyToMany
	@JoinTable(name = "CATEGORY_ITEM",
					joinColumns = @JoinColumn(name = "CATEGORY_ID"),
					inverseJoinColumns = @JoinColumn(name = "ITEM_ID")
	)
	private List<Item> items = new ArrayList<>();
}

알다시피 Item의 자식 클래스들에는 BaseEntity를 적용할 필요 없다.
Item 클래스를 통해 상속받았기 때문이다.

'Backend > Spring' 카테고리의 다른 글

값 타입  (0) 2023.08.24
프록시와 연관 관계 관리  (0) 2023.08.16
다양한 연관 관계 매핑  (0) 2023.08.16
엔티티 매핑과 연관 관계 매핑  (3) 2023.08.09
영속성 컨텍스트  (0) 2023.08.09