[Spring Boot] 쇼핑몰 프로젝트 (8) - 영속성 전이, 지연 로딩

YulHee Kim·2021년 12월 9일
0

✏️ 영속성 전이란?

영속성 전이란 엔티티의 상태를 변경할 때 해당 엔티티와 연관된 엔티티의 상태 변화를 전파시키는 옵션입니다.

이때 부모는 One에 해당하고 자식은 Many에 해당합니다. 예를들어, Order엔티티를 저장할 때 Order엔티티에 담겨있던 OrderItem 엔티티를 함께 저장할 수 있습니다.

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

이전 코드를 이어서 OrderRepository 인터페이스를 생성 후, Order entity에 cascade 옵션을 설정하겠습니다.

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

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

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

실제로 영속성 전이가 일어나는지 테스트 코드를 실행해보겠습니다.

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

    @Autowired
    OrderRepository orderRepository;

    @Autowired
    ItemRepository itemRepository;

    @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());
    }

}

itemOrder 엔티티 3개가 실제로 데이터베이스에 저장이됩니다.

✏️ 고아 객체 제거하기

부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 고아 객체라고 합니다. 영속성 전이 기능과 같이 사용하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있습니다.

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

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

주문 엔티티에서 주문 상품 엔티티를 삭제했을 때 orderItem 엔티티가 삭제되는지도 테스트코드로 보게씁니다.

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

    @Autowired
    OrderRepository orderRepository;

    @Autowired
    ItemRepository itemRepository;

    @PersistenceContext
    EntityManager em;

    @Autowired
    MemberRepository memberRepository;

    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 createOrder(){
        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.createOrder();
        order.getOrderItems().remove(0);
        em.flush();
    }

}

주문 데이터를 생성해서 저장하는 메소드를 만들고 order엔티티에서 관리하고 있는 orderItem 리스트의 0번째 인덱스 요소를 제거합니다.

flush()를 호출하면 콘솔창에 orderItem을 삭제하는 쿼리문이 출력되는 것을 확인할 수 있습니다.

✏️ 지연 로딩

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


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

    @Autowired
    OrderItemRepository orderItemRepository;
    
    @Test
    @DisplayName("지연 로딩 테스트")
    public void lazyLoadingTest(){
        Order order = this.createOrder();
        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());
        
    }

}

영속성 컨텍스트의 상태 초기화 후 order 엔티티에 저장했던 주문 상품 아이디를 이용하여 orderItem을 데이터베이스에서 다시 조회합니다. orderItem엔티티에 있는 Order 객체의 클래스를 출력합니다. order클래스가 출력되는 것을 확인할 수 있습니다.
콘솔창에서 엄청나게 긴 쿼리문을 볼 수 있습니다. orderItem 엔티티 하나를 조회했을 뿐인데 order_item 테이블과 item, orders, member 테이블을 조인해서 한꺼번에 가지고 오고 있습니다.

일대일, 다대일로 매핑할 경우 기본 전략인 즉시 로딩을 통해 엔티티를 함게 가지고 옵니다. 작성하고 있는 비즈니스 로직에서 사용하지 않을 데이터도 한꺼번에 들고 옵니다.

개발자는 쿼리가 어떻게 실행될지 예측할 수 없고 또한 성능에 문제가 있으므로 즉시 로딩 대신, 지연 로딩 방식을 사용해야합니다. FetchType.LAZY 방식으로 설정하겠습니다.


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

    @Autowired
    OrderItemRepository orderItemRepository;
    
    @Test
    @DisplayName("지연 로딩 테스트")
    public void lazyLoadingTest(){
        Order order = this.createOrder();
        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 엔티티만 조회하는 쿼리문이 실행되는 것을 볼 수 있습니다.
다른 엔티티 설계 코드에도 LAZY로 수정해주겠습니다.


다음 글은 상품 등록, 조회로 넘어가보겠습니다~!

profile
백엔드 개발자

0개의 댓글