본문 바로가기

Backend/Spring

프록시와 연관 관계 관리

프록시

이전 예제 기준으로 프록시에 대해서 살펴보자.
 
Member를 조회할 때 Team도 함께 조회해야 할까? 라는 의문이 들 수도 있다.
이 부분은 프록시, 즉시 로딩과 지연 로딩을 이해하면 해결할 수 있다.
 
엔티티 객체를 조회하는 방법은 다음 두 방법이 있다.
- em.find(): DB를 통해 실제 엔티티 객체 조회
- em.getReference(): DB 조회를 미루는 가짜, 즉 프록시 엔티티 객체 조회
 
프록시의 특징은 다음과 같다.
- 실제 클래스를 상속 받아서 만들어진다.
- 실제 클래스와 겉 모양이 같다.
- 사용자 입장에서는 실제 엔티티 객체인지, 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체의 참조를 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

 
이제 프록시 객체의 초기화에 대해서 알아보자.
다음 코드를 한 번 살펴보자.

Member member = em.getReference(Member.class, "id1");
member.getName();

먼저, 클라이언트가 getName() 메서드를 호출하게 되면, 영속성 컨텍스트에 프록시 객체의 초기화를 요청한다.
이후, DB 조회를 통해 실제 엔티티를 생성하게 되고, 실제 엔티티의 getName() 메서드를 호출하게 되는 과정으로 이루어진다.
 
프록시 객체는 처음 사용할 때 한 번만 초기화된다.
프록시 객체 초기화 후, 실제 엔티티로 바뀌는 것이 아니라, 프록시 객체를 통해서 실제 엔티티에 접근이 가능해지는 것이다.
만약, 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
그리고 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때 프록시를 초기화하면 문제가 발생할 수 있으니 주의하자.
 
다음 코드를 통해 조금 더 자세히 알아보자.

Member member1 = new Member();
member1.setUsername("member1");
em.persist(member);

em.flush();
em.clear();

Member m1 = em.getReference(Member.class, member1.getId());
Member reference = em.getReference(Member.class, member1.getId());

tx.commit();

이 경우, m1, reference 모두 프록시 객체가 된다.
 

Member member1 = new Member();
member1.setUsername("member1");
em.persist(member);

em.flush();
em.clear();

Member m1 = em.getReference(Member.class, member1.getId());
Member reference = em.find(Member.class, member1.getId());

tx.commit();

이 경우도 둘 다 프록시 객체가 된다.
 

Member member1 = new Member();
member1.setUsername("member1");
em.persist(member);

em.flush();
em.clear();

Member m1 = em.getReference(Member.class, member1.getId());

em.detach(m1);

tx.commit();

이 경우, 준영속 상태로 만들었는데, getUsername 메서드를 호출하면 초기화 문제가 발생할 수 있다.
 

즉시 로딩과 지연 로딩

만약, member 정보만 사용하는 비즈니스 로직으로 코드가 전개된다면, 굳이 team 정보를 같이 불러올 필요는 없다.
 
이제부터 지연 로딩을 사용해서 프록시로 조회하는 방법을 한 번 알아보자.
 
다음과 같이 지연 로딩 LAZY를 사용하면 Member 조회 시 Member 객체만 가져오고, Team의 경우 프록시로 가져오게 된다. 

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @Column(name = "USERNAME")
    private String name;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

그래서, 실제로 team을 사용할 때 DB에 쿼리가 나가게 되고, 프록시 객체의 초기화가 이루어지는 것이다.
 
만약, Member와 Team을 자주 함께 사용하면 다음과 같이 즉시 로딩 EAGER를 사용해서 조회하면 된다.

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @Column(name = "USERNAME")
    private String name;
    
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

 
하지만, 즉시 로딩을 적용하면 예상치 못한 SQL이 발생할 수 있다.
즉시 로딩은 JPQL에서 N+1 문제를 일으킨다고 하는데, 1이 필요한 쿼리고 N은 추가 쿼리가 나가는 것을 의미한다고 한다.
그래서 최초 쿼리 하나 날리면 쿼리가 N개가 추가적으로 나가서 N+1 문제를 일으킬 수 있다는 것이다.
 
그냥 지연 로딩만 사용하자.
 
@ManyToOne, @OneToOne 어노테이션은 기본이 즉시 로딩이니 직접 LAZY, 즉 지연 로딩으로 설정해주어야 한다.
 

영속성 전이 (CASCADE)와 고아 객체

영속성 전이는 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.
예를 들어서 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하는 경우가 있겠다.
 
다음과 같이 작성해주면 parent 객체를 저장할 때 child 객체도 같이 저장된다.

@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)

 
그런데 연관 관계가 많이 얽혀 있는 경우에는 영속성 전이를 사용하지 않는게 좋다.
단일 엔티티에 종속적인 경우 사용하는 것을 권장한다.
 
고아 객체를 제거한다는 것은 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제한다는 것이다.
 
고아 객체 또한 참조하는 곳이 하나일 때 사용해야 한다.
 
이제 예제로 알아보자.
 

실전 예제

이제 @XToOne 어노테이션들은 모두 지연 로딩으로 변경하고, Order -> Delivery, Order -> OrderItem을 영속성 전이 ALL로 설정해주자.

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(fetch = FetchType.LAZY)
	@JoinColumn(name = "MEMBER_ID")
	private Member member;
	
	@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
	@JoinColumn(name = "DELIVERY_ID")
	private Delivery delivery;
		
	@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
	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(fetch = FetchType.LAZY)
	@JoinColumn(name = "ORDER_ID")
	private Order order;

//	  @Column(name = "ITEM_ID")
//	  private Long itemId;
		
	@ManyToOne(fetch = FetchType.LAZY)
	@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", fetch = FetchType.LAZY)
	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(fetch = FetchType.LAZY)
	@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<>();
}
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;
	}
}

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

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