본문 바로가기

Backend/Spring

다양한 연관 관계 매핑

연관 관계 매핑 시 다음 사항들을 고려해야 한다.
- 다중성
- 단방향, 양방향
- 연관 관계의 주인
 

다중성

jpa에서 제공하는 다중성 관련 애노테이션은 다음과 같다.
- 다대일: @ManyToOne
- 일대다: @OneToMany
- 일대일: @OneToOne
- 다대다: @ManyToMany
 

단방향, 양방향과 연관 관계의 주인

테이블객체는 유사해 보이지만, 단방향, 양방향 관계에 있어서 차이가 있다.
 
테이블의 경우, 외래 키 하나로 양쪽 조인이 가능해서 사실상 방향이라는 개념이 없다.
그래서, 외래 키만으로 두 테이블이 연관 관계를 맺게 된다.
 
하지만, 객체는 참조용 필드가 있는 쪽으로만 참조가 가능하다.
한 쪽만 참조하면 단방향, 양쪽이 서로 참조하면 양방향이 되는 것이다.
객체의 양방향 관계는 a->b, b->a 이렇게 참조가 두 군데라 외래 키를 어디서 관리할지를 정해야 한다.
연관 관계의 주인이 외래 키를 관리하게 되고, 주인이 아닌 쪽은 단순 조회만 하게 된다.
 

다대일 (N:1)

다대일에서 다쪽에 외래 키가 있어야 한다.
아래 사진을 보면 알 수 있다시피, 다쪽인 Member 엔티티에 외래 키(FK)가 있다.

 

일대다 (1:N)

일대다 관계에서는 1 쪽이 연관 관계의 주인이 된다.
테이블 일대다 관계에서는 항상 다쪽에 외래 키가 있다.
그래서, 객체와 테이블 간 차이 때문에 반대편 테이블의 외래 키를 관리하게 되는 구조이다.

일대다 연관 관계에서는 @JoinColumn이라는 어노테이션을 사용해서 매핑할 외래 키 컬럼명을 지정한다.
그런데, 일대다 단방향 매핑은 외래 키가 다른 테이블에 있어 연관 관계 관리를 위해 추가로 UPDATE SQL을 실행해야 한다.
그래서 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자.
 
아래 사진은 일대다 양방향을 나타낸다.
이런 매핑은 공식적으로는 존재하지 않지만, 읽기 전용 필드를 사용해서 양방향처럼 사용할 수는 있다.
다만, 다대일 양방향을 사용하는 것이 권장된다.

 

일대일 (1:1)

일대일 관계에서는 주 테이블이나 대상 테이블 중 어디에 외래 키를 넣을 지 선택할 수 있다.
그리고 외래 키에 데이터베이스 유니크(UNI) 제약 조건을 추가해야 한다.
 
아래 사진은 주 테이블에 외래 키 단방향 매핑을 한 것을 나타냈다.
이 경우 다대일 단방향 매핑과 유사하다.

 
아래 사진은 주 테이블에 외래 키 양방향 매핑을 나타냈다.

이 경우도 다대일 양방향처럼 외래 키가 있는 곳이 연관 관계의 주인이다.
 
아래 사진은 대상 테이블에 외래 키 단방향 매핑을 나타낸다.

대상 테이블에 외래 키를 넣는 경우 단방향 관계는 JPA에서 지원하지 않고, 양방향은 지원한다.
 
아래 사진은 대상 테이블에 외래 키 양방향을 나타낸다.

이 경우 일대일 주 테이블에 외래 키 양방향과 매핑 방법은 동일하다.
 
주 테이블에 외래 키를 두는 경우 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인이 가능하지만, 값이 없다면 외래 키에 null을 허용하게 된다.
반면, 대상 테이블에 외래 키를 두는 경우 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조를 그대로 유지할 수 있다는 장점도 있지만 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩된다는 단점을 가지고 있다.
 

다대다 (N:M)

다대다 연관 관계는 실무에서 사용하면 안 된다.
관계형 DB는 정규화된 테이블 2개만으로 다대다 관계를 표현할 수 없어서, 두 테이블 중간에 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.
이 연결 테이블을 엔티티로 만들어주면 된다.

 
하지만 객체의 경우, 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하기는 하다.

 
다대다 연관 관계에서는 @ManyToMany 어노테이션을 사용하고 @JoinTable 어노테이션을 통해서 연결 테이블을 지정한다.
 
이제 예제로 알아보자.
 

실전 예제

이전에 살펴봤던 예제에 배송, 카테고리를 추가했다.

 
ERD 다이어그램과 엔티티 상세 정보는 다음과 같다.

 
코드는 다음과 같다.

package jpabook.jpashop.domain;

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

@Entity
public class Member {
	@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 {
	@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 {
	@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.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public 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;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Delivery {
	@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 {
	@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<>();
}

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

프록시와 연관 관계 관리  (0) 2023.08.16
고급 매핑  (0) 2023.08.16
엔티티 매핑과 연관 관계 매핑  (3) 2023.08.09
영속성 컨텍스트  (0) 2023.08.09
빈 스코프  (1) 2023.08.03