본문 바로가기
Spring/Spring JPA

영속성 전이

by chogigang 2023. 3. 6.

영속성 전이 즉, 'cascade'의 사전적 정의는 '작은 폭포', '폭포처럼 흐르다'라는 뜻이 있습니다. 영속성 전이랑 엔티티의 상태를 변경할 때 헤당 엔티티와 연관된 엔티티의 상태 변화를 전파시키는 옵션입니다. 이때 부모는 One에 해당하고 자식은 Many에 해당합니다 예를 들어 Order 엔티티가 삭제되었을 때 해당 엔티티와 여관 되어 있는 OrderItem 엔티티가 함께 삭제되거나, Order 엔티티를 저장할 때 Order 엔티티에 담겨있던 OrderItem 엔티티를 한꺼번에 저장할 수 있습니다. 상태가 전파되는 모습을 폭포가 흐르는 모습으로 상상하면 떠올리기가 쉽습니다.

CASCADE종류 설명
PERSIST 부모 엔티티가 영속화될 때 자식 엔티티도 영속화
MERGE 부모 엔티티가 병합될 때 자식 엔티티도 병합
REMOVE 부모 엔티티가 삭제될 때 연관된 자식 엔티티도 삭제
REFRESH 부모 엔티티가 refresh 되면 연관된 자식 엔티티도 detach 상태로 변경
DETACH 부모 엔티티가 detach 되면 연간된 자식 엔티티도 detach 상태로 변경
ALL 부모 엔티티의 영속성 상태 변화를 자식 엔티티에 모두 전이

 

영속성 전이 옵션을 무분별하게 사용할 경우 삭제되지 말아야 할 데이터가 삭제될 수 있으므로 조심히 사용해야 합니다.

영속성 전이 옵션은 단일 엔티티에 완전히 종속적이고 부모 엔티티와 자식 엔티티의 라이프 사이클이 유사할 때 cascade옵션을 활용하시기를 추천드립니다.

 

 

 

예제 코드들은 쇼핑물 기준으로 작성을 할 겁니다.

 

http://www.yes24.com/Product/Goods/103453774

 

스프링 부트 쇼핑몰 프로젝트 with JPA - YES24

스프링 부트와 JPA를 활용하여 실제 이커머스 업계에서 활용되는 쇼핑몰 기술들을 직접 구현해볼 수 있게 구성하였다. JPA와 Thymeleaf에 대한 간단한 예제로 기본 개념과 사용법을 익히고 그 후 쇼

www.yes24.com

 

 

주문 엔티티를 저장하기 위해 JpaRepository를 상속받는 OrderRepository 인터페이스를 생성합니다.

 

OrderRepository

package com.shop.repository;

import com.shop.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {

}

@OneToMany 어노테이션에 cascade 옵션을 설정합니다.

 

package com.shop.entity;


import com.shop.constant.OrderStatus;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {


    @Id
    @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    private LocalDateTime orderDate;//주문일

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus; //주문 상태


    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL,//<- 영속성전이 추가 
            orphanRemoval = true)
    private List<OrderItem> orderItems = new ArrayList<>();
    private LocalDateTime regTime;

    private LocalDateTime updateTime;




}

 

1. 부모 엔티티의 영속성 상태 변화를 자식 엔티티에 모두 전이하는 CascadeTypeAll 옵션을 설정하겠습니다.

 

영속성 전이 옵션 설정이 완료됐습니다. 실제로 주문 엔티티를 저장할 때 영속성 전이가 일어나 지는 테스트 코드를 통해 알아보겠습니다. 고객이 주문할 상품을 선택하고 주문할 때 주문 엔티티를 저장하면서 주문 상품 엔티티도 함께 저장되는 경우라고 생각하시면 됩니다.

 

 

OrderTest

package com.shop.entity;

import com.shop.constant.ItemSellStatus;
import com.shop.repository.ItemRepository;
import com.shop.repository.MemberRepository;
import com.shop.repository.OrderItemRepository;
import com.shop.repository.OrderRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
import java.time.LocalDateTime;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@TestPropertySource(locations="classpath:application-test.properties")
@Transactional
public class OrderTest {

    @Autowired
    OrderRepository orderRepository;

    @Autowired
    ItemRepository itemRepository;
    @Autowired
    OrderItemRepository orderItemRepository;

    @Autowired
    MemberRepository memberRepository;

    @PersistenceContext
    EntityManager em;

    public  Item createItem(){
        Item item =new Item();
        item.setItemNm("테스트상품");
        item.setPrice(10000);
        item.setItemDetail("상세설명");
        item.setItemSellStatus(ItemSellStatus.SELL);
        item.setStockNumber(100);
        item.setRegTime(LocalDateTime.now());
        item.setUpdateTime(LocalDateTime.now());
        return item;
    }
@Test
    @DisplayName("영속성 전이 테스트")
    public void cascadeTest(){

        Order order = new Order();

        for(int i=0;i<3; i++){
            Item item =this.createItem();
            itemRepository.save(item);
            OrderItem orderItem = new OrderItem();
            orderItem.setItem(item);
            orderItem.setCount(10);
            orderItem.setOrderPrice(1000);
            orderItem.setOrder(order);
            order.getOrderItems().add(orderItem);//<- 추가

        }
            orderRepository.saveAndFlush(order);//<- 추가
            em.clear();// <- 추가


        Order savedOrder = orderRepository.findById(order.getId())//<- 추가
                .orElseThrow(EntityNotFoundException::new);
                assertEquals(3,savedOrder.getOrderItems().size());

    }

 

  1. 아직 영속성 컨텍스트에 저장되지 않은 orderItem 엔티티를 order 엔티티에 담아줍니다.
  2. order 엔티티를 저장하면서 강제로 flush를 호출하여 영속성 컨텍스트에 있는 객체들을 데이터베이스에 반영합니다.
  3. 영속성 컨텍스트의 상태를 초기화합니다.
  4. 영속성 컨텍스트를 초기화했기 때문에 데이터베이스에서 주문 엔티티를 조회합니다. select 쿼리문이 시행되는 것을 콘솔창에서 확인할 수 있습니다.
  5. itemOrder 엔티티 3개가 실제로 데이터베이스에 저장되었는지 검사합니다.

 

(2) 코드 실행 시 flush를 호출하면서 콘솔창에 insert 쿼리문이 출력되는 것을 확인할 수 있습니다. 주문 데이터가 먼저 데이터베이스에 반영됩니다.

 

그 후 영속성이 전이되면서 order에 담아 두었던 orderItem이 insert 되는 것을 확인할 수 있습니다. 총 3개의 orderItem을 담아두었으므로 3번의 insert 쿼리문이 실행됩니다.

 

테스트 코드 실행 결과 실제 조회되는 orderItem이 3개이므로 테스트가 정상적으로 통과합니다.

 

 

고아 객체 제거하기

다음으로 알아볼 내용은 고아 객체 제거입니다. 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 고아 객체라고 합니다.

영속성 전이 기능과 같이 사용하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있습니다.

 

영속성 전이 기능과 마찬가지로 고아 객체 제거 기능을 사용하기 위해서 주의사항이 있습니다. 고아객체 제거 기능은 참조하는 곳이 하나일 때만 사용해야 합니다. 다른 곳에서도 참조하고 있는 엔티티인데 삭제하면 문제가 생길 수 있습니다.

OrderItem엔티티를 Order 엔티티가 아닌 다른 곳에서 사용하고 있다면 이 기능을 사용하면 안 됩니다.

 

@OneToOne,@OneToMany 어노테이션에서 옵션으로 사용하시면 됩니다. 고아 객체 제거를 사용하기 위해서 @OneToMany 어노테이션에 "orphanRemoval = true" 옵션을 추가합니다.

 

Order

package com.shop.entity;


import com.shop.constant.OrderStatus;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {


    @Id
    @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    private LocalDateTime orderDate;//주문일

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus; //주문 상태


    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL,
            orphanRemoval = true)//고아 객체 제거 추가
    private List<OrderItem> orderItems = new ArrayList<>();
    private LocalDateTime regTime;

    private LocalDateTime updateTime;




}

 

주문 엔티티 (부모 엔티티)에서 주문 상품(자식 엔티티)를 삭제했을 때 orderItem 엔티티가 삭제되는지 테스트 코드를 작성해 보겠습니다.

 

 

OrderTest

 

@SpringBootTest
@TestPropertySource(locations="classpath:application-test.properties")
@Transactional
public class OrderTest {

    @Autowired
    OrderRepository orderRepository;

    @Autowired
    ItemRepository itemRepository;
    @Autowired
    OrderItemRepository orderItemRepository;

    @Autowired
    MemberRepository memberRepository;

    public Order crateOrder() {
        Order order = new Order();

        for (int i = 0; i < 3; i++) {
            Item item = createItem();
            itemRepository.save(item);
            OrderItem orderItem = new OrderItem();
            orderItem.setItem(item);
            orderItem.setCount(10);
            orderItem.setOrderPrice(1000);
            orderItem.setOrder(order);
            order.getOrderItems().add(orderItem);
        }
        Member member = new Member();
        memberRepository.save(member);

        order.setMember(member);
        orderRepository.save(order);

        return order;
    }

    @Test
    @DisplayName("고아객체 제거 테스트")
    public void orphanRemovalTest() {
        Order order = this.crateOrder();
        order.getOrderItems().remove(0);
        em.flush();
    }
}
  1. 주문 데이터를 생성해서 저장하는 메소드를 만듭니다.
  2. order 엔티티에서 관리하고 있는 orderItem 리스트의 0번째 인덱스 요소를 제거합니다.

flush()를 호출하면 콘솔창에 orderItem을 삭제하는 쿼리문이 출력되는 것을 확인할 수 있습니다. 즉, 부모 엔티티와 연관 관계가 끊어졌기 때문에 고아 객체를 삭제하는 쿼리문이 실행되는 것입니다.

 

 

OrderTest

 

package com.shop.entity;

import com.shop.constant.ItemSellStatus;
import com.shop.repository.ItemRepository;
import com.shop.repository.MemberRepository;
import com.shop.repository.OrderItemRepository;
import com.shop.repository.OrderRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
import java.time.LocalDateTime;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@TestPropertySource(locations="classpath:application-test.properties")
@Transactional
public class OrderTest {

    @Autowired
    OrderRepository orderRepository;

    @Autowired
    ItemRepository itemRepository;
    @Autowired
    OrderItemRepository orderItemRepository;

    @Autowired
    MemberRepository memberRepository;

    @PersistenceContext
    EntityManager em;

    public Item createItem() {
        Item item = new Item();
        item.setItemNm("테스트상품");
        item.setPrice(10000);
        item.setItemDetail("상세설명");
        item.setItemSellStatus(ItemSellStatus.SELL);
        item.setStockNumber(100);
        item.setRegTime(LocalDateTime.now());
        item.setUpdateTime(LocalDateTime.now());
        return item;
    }

    @Test
    @DisplayName("영속성 전이 테스트")
    public void cascadeTest() {

        Order order = new Order();

        for (int i = 0; i < 3; i++) {
            Item item = this.createItem();
            itemRepository.save(item);
            OrderItem orderItem = new OrderItem();
            orderItem.setItem(item);
            orderItem.setCount(10);
            orderItem.setOrderPrice(1000);
            orderItem.setOrder(order);
            order.getOrderItems().add(orderItem);

        }
        orderRepository.saveAndFlush(order);
        em.clear();


        Order savedOrder = orderRepository.findById(order.getId())
                .orElseThrow(EntityNotFoundException::new);
        assertEquals(3, savedOrder.getOrderItems().size());

    }

    public Order crateOrder() {
        Order order = new Order();

        for (int i = 0; i < 3; i++) {
            Item item = createItem();
            itemRepository.save(item);
            OrderItem orderItem = new OrderItem();
            orderItem.setItem(item);
            orderItem.setCount(10);
            orderItem.setOrderPrice(1000);
            orderItem.setOrder(order);
            order.getOrderItems().add(orderItem);
        }
        Member member = new Member();
        memberRepository.save(member);

        order.setMember(member);
        orderRepository.save(order);

        return order;
    }

    @Test
    @DisplayName("고아객체 제거 테스트")
    public void orphanRemovalTest() {
        Order order = this.crateOrder();
        order.getOrderItems().remove(0);
        em.flush();
    }

}

 

 

지연 로딩 

 

연관된 엔티티를 사용할 때 조회하는 지연 로딩과 지연 로딩을 사용해야 하는 이유를 알아보겠습니다.

 

엔티티를 조회할 때 연관된 엔티티를 함께 조회하는 즉시 로딩을 알아보겠습니다. 즉시 로딩 이외에도 지연로딩이라는 Fetch 전략이 있습니다. 지연 로딩을 배우기 전에 주문 데이터 저장 후 OrderItem 엔티티를 조회해 보겠습니다.

 

OrderItem을 조회하기 위해서 JpaRepository를 상속받는 OrderItemRepository 인터페이스를 생성합니다.

 

 

 

OrderItemRepository 

package com.shop.repository;

import com.shop.entity.OrderItem;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderItemRepository extends JpaRepository<OrderItem,Long> {
}

 

주문 데이터를 먼저 데이터베이스에 저장하고, 저장한 주문 상품 데이터를 조회해 보겠습니다.

 

OrderTest

 

package com.shop.entity;

import com.shop.constant.ItemSellStatus;
import com.shop.repository.ItemRepository;
import com.shop.repository.MemberRepository;
import com.shop.repository.OrderItemRepository;
import com.shop.repository.OrderRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
import java.time.LocalDateTime;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@TestPropertySource(locations="classpath:application-test.properties")
@Transactional
public class OrderTest {

    @Autowired
    OrderRepository orderRepository;

    @Autowired
    ItemRepository itemRepository;
    @Autowired
    OrderItemRepository orderItemRepository;

    @Autowired
    MemberRepository memberRepository;

    @PersistenceContext
    EntityManager em;

    public  Item createItem(){
        Item item =new Item();
        item.setItemNm("테스트상품");
        item.setPrice(10000);
        item.setItemDetail("상세설명");
        item.setItemSellStatus(ItemSellStatus.SELL);
        item.setStockNumber(100);
        item.setRegTime(LocalDateTime.now());
        item.setUpdateTime(LocalDateTime.now());
        return item;
    }
@Test
    @DisplayName("영속성 전이 테스트")
    public void cascadeTest(){

        Order order = new Order();

        for(int i=0;i<3; i++){
            Item item =this.createItem();
            itemRepository.save(item);
            OrderItem orderItem = new OrderItem();
            orderItem.setItem(item);
            orderItem.setCount(10);
            orderItem.setOrderPrice(1000);
            orderItem.setOrder(order);
            order.getOrderItems().add(orderItem);

        }
            orderRepository.saveAndFlush(order);
            em.clear();


        Order savedOrder = orderRepository.findById(order.getId())
                .orElseThrow(EntityNotFoundException::new);
                assertEquals(3,savedOrder.getOrderItems().size());

    }

    public Order crateOrder(){
        Order order = new Order();

        for(int i=0;i<3; i++){
            Item item = createItem();
            itemRepository.save(item);
            OrderItem orderItem = new OrderItem();
            orderItem.setItem(item);
            orderItem.setCount(10);
            orderItem.setOrderPrice(1000);
            orderItem.setOrder(order);
            order.getOrderItems().add(orderItem);
        }
    Member member = new Member();
        memberRepository.save(member);

        order.setMember(member);
        orderRepository.save(order);

        return order;
    }

    @Test
    @DisplayName("고아객체 제거 테스트")
    public void orphanRemovalTest(){
        Order order = this.crateOrder();
        order.getOrderItems().remove(0);
        em.flush();
    }

    @Test
    @DisplayName("지연 로딩 테스트")//<- 추가 
    public void lazyLoadingTest(){
        Order order = this.crateOrder();
        Long orderItemId = order.getOrderItems().get(0).getId();
        em.flush();
        em.clear();

        OrderItem orderItem = orderItemRepository.findById(orderItemId)
                .orElseThrow(EntityNotFoundException::new);
        System.out.println("Order class : " + orderItem.getOrder().getClass());
 


    }
 }
  1. 기존에 만들었던 주문 생성 메소드를 이용하여 주문 데이터를 저장합니다.
  2. 영속성 컨텍스트의 상태 초기화 후 order 엔티티에 저장했던 주문 상품 아이디를 이용하여 orderItem을 데이터베이스에서 다시 조회합니다.
  3. orderItem 엔티티에 있는 order 객체의 클래스를 출력합니다. Order 클래스가 출력되는 것을 확인할 수 있습니다.

2 코드에서 orderItem 데이터를 조회하면 콘솔창에서 엄청나게 긴 쿼리문을 볼 수 있습니다. orderItem 엔티티 하나를 조회했을 뿐인데 order_item 테이블과 item, orders, member 테이블을 조인해서 한꺼번에 가지고 오고 있습니다.

 

 

Hibernate:
select
orderitem0_.order_item_id as order_it1_4_0_,
orderitem0_.count as count2_4_0_,
orderitem0_.item_id as item_id6_4_0_,
orderitem0_.order_id as order_id7_4_0_,
orderitem0_.order_price as order_pr3_4_0_,
orderitem0_.reg_time as reg_time4_4_0_,
orderitem0_.update_time as update_t5_4_0_,
item1_.item_id as item_id1_2_1_,
item1_.item_detail as item_det2_2_1_,
item1_.item_nm as item_nm3_2_1_,
item1_.item_sell_status as item_sel4_2_1_,
item1_.price as price5_2_1_,
item1_.reg_time as reg_time6_2_1_,
item1_.stock_number as stock_nu7_2_1_,
item1_.update_time as update_t8_2_1_,
order2_.order_id as order_id1_5_2_,
order2_.member_id as member_i6_5_2_,
order2_.order_date as order_da2_5_2_,
order2_.order_status as order_st3_5_2_,
order2_.reg_time as reg_time4_5_2_,
order2_.update_time as update_t5_5_2_,
member3_.member_id as member_i1_3_3_,
member3_.address as address2_3_3_,
member3_.email as email3_3_3_,
member3_.name as name4_3_3_,
member3_.password as password5_3_3_,
member3_.role as role6_3_3_
from
order_item orderitem0_
left outer join
item item1_
on orderitem0_.item_id=item1_.item_id
left outer join
orders order2_
on orderitem0_.order_id=order2_.order_id
left outer join
member member3_
on order2_.member_id=member3_.member_id
where
orderitem0_.order_item_id=?

 

일대일, 다대일로 매핑할 경우 기본 전략인 즉시 로딩을 통해 엔티티를 함께 가지고 옵니다. 심지어 Order 엔티티는 자신과 다대일로 매핑된 Member 엔티티도 가지고 오고 있습니다. 작성하고 있는 비즈니스 로직에서 사용하지 않을 데이터도 한꺼번에 들고 오는 것입니다.

 

지금 예제에서는 4개의 테이블을 조인해서 가지고 오지만, 실제 비즈니스를 하고 있다면 매핑되는 엔티티의 개수는 훨씬 많습니다. 그렇게 되면 개발자는 쿼리가 어떻게 실행될지 예층할 수 없습니다. 또한 사용하지 않는 데이터도 한꺼번에 조회하므로 성능 문제도 있을 수 있습니다. 따라서 즉시 로딩은 실무에서는 사용하기 힘듭니다.

 

즉시 로딩을 사용하는 대신 지연 로딩 방식을 사용해야 합니다. FetchType.LAZY방식으로 설정하겠습니다.

 

 

OrderItem

package com.shop.entity;


import lombok.Getter;
import lombok.Setter;

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

@Entity
@Getter
@Setter
public class OrderItem {

    @Id
    @GeneratedValue
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)//<- 추가
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne(fetch = FetchType.LAZY)//<- 추가
    @JoinColumn(name = "order_id")
    private Order order;

    private int orderPrice;//주문가격

    private int count;// 수량

    private LocalDateTime regTime;
    private LocalDateTime updateTime;

}

 

지연 로딩으로 변경 후 기존 테스트 코드를 수정 후 다시 실행해 보겠습니다.

 

 

OrderTest

package com.shop.entity;

import com.shop.constant.ItemSellStatus;
import com.shop.repository.ItemRepository;
import com.shop.repository.MemberRepository;
import com.shop.repository.OrderItemRepository;
import com.shop.repository.OrderRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
import java.time.LocalDateTime;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
@TestPropertySource(locations="classpath:application-test.properties")
@Transactional
public class OrderTest {

    @Autowired
    OrderRepository orderRepository;

    @Autowired
    ItemRepository itemRepository;
    @Autowired
    OrderItemRepository orderItemRepository;

    @Autowired
    MemberRepository memberRepository;

    @PersistenceContext
    EntityManager em;

    public  Item createItem(){
        Item item =new Item();
        item.setItemNm("테스트상품");
        item.setPrice(10000);
        item.setItemDetail("상세설명");
        item.setItemSellStatus(ItemSellStatus.SELL);
        item.setStockNumber(100);
        item.setRegTime(LocalDateTime.now());
        item.setUpdateTime(LocalDateTime.now());
        return item;
    }
@Test
    @DisplayName("영속성 전이 테스트")
    public void cascadeTest(){

        Order order = new Order();

        for(int i=0;i<3; i++){
            Item item =this.createItem();
            itemRepository.save(item);
            OrderItem orderItem = new OrderItem();
            orderItem.setItem(item);
            orderItem.setCount(10);
            orderItem.setOrderPrice(1000);
            orderItem.setOrder(order);
            order.getOrderItems().add(orderItem);

        }
            orderRepository.saveAndFlush(order);
            em.clear();


        Order savedOrder = orderRepository.findById(order.getId())
                .orElseThrow(EntityNotFoundException::new);
                assertEquals(3,savedOrder.getOrderItems().size());

    }

    public Order crateOrder(){
        Order order = new Order();

        for(int i=0;i<3; i++){
            Item item = createItem();
            itemRepository.save(item);
            OrderItem orderItem = new OrderItem();
            orderItem.setItem(item);
            orderItem.setCount(10);
            orderItem.setOrderPrice(1000);
            orderItem.setOrder(order);
            order.getOrderItems().add(orderItem);
        }
    Member member = new Member();
        memberRepository.save(member);

        order.setMember(member);
        orderRepository.save(order);

        return order;
    }

    @Test
    @DisplayName("고아객체 제거 테스트")
    public void orphanRemovalTest(){
        Order order = this.crateOrder();
        order.getOrderItems().remove(0);
        em.flush();
    }

    @Test
    @DisplayName("지연 로딩 테스트")
    public void lazyLoadingTest(){
        Order order = this.crateOrder();
        Long orderItemId = order.getOrderItems().get(0).getId();
        em.flush();
        em.clear();

        OrderItem orderItem = orderItemRepository.findById(orderItemId)
                .orElseThrow(EntityNotFoundException::new);
        System.out.println("Order class : " + orderItem.getOrder().getClass());//추가
        System.out.println("===========================");
        orderItem.getOrder().getOrderDate();//추가
        System.out.println("===========================");


    }
 }

 

테스트 코드 실행 결과 orderItem 엔티티만 조회하는 쿼리문이 실행되는 것을 볼 수 있습니다.

 

 

또한 1 코드의 실행 결과 Order 클래스 조회 결과가 HibernateProxy라고 출력되는 것을 볼 수 있습니다. 지연 로딩으로 설정하면 실제 엔티티 대신에 프록시 객체를 넣어둡니다.

 

 

프록사 객체는 실제로 사용되기 전까지 데이터 로딩을 하지 않고, 실제 사용 시점에 조회 쿼리문이 실행됩니다.

2 코드에서 Order의 주문일(orderDate)을 조회할 때 select 쿼리문이 실행되는 것을 확인할 수 있습니다. 디버깅 모드로 실행 후 코드를 한 줄씩 실행해 보면 쉽게 이해할 수 있습니다.

 

어떤 어노테이션은 즉시 로딩이고, 어떤 어노테이션은 지연로딩이면 헷갈릴 수 있습니다.

연관 매핑 어노테이션에 Fetch 전략을 LAZY로 직접 설정하겠습니다.

 

 

Cart

package com.shop.entity;


import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;

@Entity
@Table(name = "cart")
@Getter
@Setter
@ToString
public class Cart {

    @Id
    @Column(name = "cart_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;


}

 

CartItem

 

package com.shop.entity;


import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Getter
@Setter
@Table(name = "cart_item")
public class CartItem {
    @Id
    @GeneratedValue
    @Column(name = "cart_item_id")
    private Long id;


    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "cart_id")
    private Cart cart;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    private int count;

}

 

Order

 

package com.shop.entity;


import com.shop.constant.OrderStatus;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {


    @Id
    @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    private LocalDateTime orderDate;//주문일

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus; //주문 상태


    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL,
            orphanRemoval = true)
    private List<OrderItem> orderItems = new ArrayList<>();
    private LocalDateTime regTime;

    private LocalDateTime updateTime;




}

 

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

Auditing을 이용한 엔티티 공통 속성 공통화  (0) 2023.03.06
연관 관계 매핑 종류  (1) 2023.03.05
@Query  (0) 2023.03.02
엔티티 매핑 관련 어노테이션  (0) 2023.02.28
JPA  (0) 2023.02.28