Spring boot & JPA

Spring Boot - 쇼핑몰 프로젝트 05 (연관 관계 매핑 - 2)

로기221 2023. 4. 5. 16:17
728x90
반응형

https://rogi221.tistory.com/174

 

Spring Boot - 쇼핑몰 프로젝트 05 (연관 관계 매핑 - 1)

연관 관계 매핑 - 1 연관 관계 매핑 종류 엔티티들은 대부분 다른 엔티티와 연관 관계를 맺으며, JPA에서는 엔티티에 연관 관계를 매핑해두고 필요할 때 해당 엔티티와 연관된 엔티티를 사용하여

rogi221.tistory.com

 

 


연관 관계 매핑 - 2

 

영속성 전이

  • 영속성 전이 : 엔티티의 상태를 변경할 때 해당 엔티티와 연관된 엔티티의 상태 변화를 전파시키는 옵션
  • 부모는 One에 해당하고 자식은 Many에 해당
  • (ex) Order 엔티티(부모)가 삭제되었을 때 해당 엔티티와 연관되어 있는 OrderItem 엔티티(자식)가 함께 삭제 되거나, Order 엔티티를 저장 할때 Order 엔티티에 담겨있던 OrderItem 엔티티를 한꺼번에 저장

 

 

  • 영속성 전이 옵션

 

  • 영속성 전이 옵션은 단일 엔티티에 완전히 종속적이고 부모 엔티티와 자식 엔
  • 티티의 라이프 사이클이 유사할 때 cascade 옵션을 활용하기를 추천
  • 무분별하게 사용할 경우 삭제되면 안될 데이터도 삭제될 수 있음

 

 

 

주문 영속성 전이 테스트하기

com.shop.repository.OrderRepository.java

// OrderRepository.java

package com.shop.repository;

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

public interface OrderRepository extends JpaRepository<Order, Long> {

}

 

 

com.shop.entity.Order.java

// Order.java

... 기존 임포트 생략 ...

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

... 생략 ...

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

 

 

 

com.shop.entity.OrderTest.java

// OrderTest.java

package com.shop.entity;

import com.shop.constant.ItemSellStatus;
import com.shop.repository.ItemRepository;
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 org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;
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;

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

 

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

 

 

 

 

 

고아 객체 제거

  • 고아 객체 : 부모 엔티티와 연관 관계가 끊어진 자식 엔티티
  • 영속성 전이 기능과 같이 사용하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
  • 고아객체 제거 기능은 참조하는 곳이 하나일 때만 사용
  • OrderItem 엔티티를 Order 엔티티가 아닌 다른 곳에서 사용하고 있다면 이 기능을 사용하면 안됨
  • @OneToOne, @OneToMany 어노테이션에서 “orphanRemoval = true” 옵션 사용

 

 

 

고아 객체 제거 테스트하기

 

com.shop.entity.Order.java

// Order.java

public class Order {

... 생략 ...

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

 

 

com.shop.entity.OrderTest.java

// OrderTest.java

... 기존 임포트 생략 ...

import com.shop.repository.MemberRepository;

public class OrderTest {

... 생략 ...

    @Autowired
    MemberRepository memberRepository;

    public Order createOrder() {
        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);
        }
        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();
    }
}

 

 

  • flush()를 호출하면 콘솔창에 orderItem을 삭제하는 쿼리문이 출력되는 것을 확인 가능
  • 부모 엔티티와 연관 관계가 끊어졌기 때문에 고아 객체를 삭제하는 쿼리문이 실행됨

 

 

 

 

즉시 로딩의 문제점

  • 조인해서 가져오는 엔티티가 많아질수록 쿼리가 어떻게 실행될지 개발자가 예측하기 힘듬
  • 사용하지 않는 엔티티도 한꺼번에 가져오기 때문에 성능상 문제가 발생할 수 있음
  • 즉시 로딩 대신에 지연 로딩 방식 사용

 

주문 엔티티 조회 테스트하기(즉시 로딩)

com.shop.repository.OrderItemRepository.java

// OrderItemRepository.java

package com.shop.repository;

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

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

 

 

com.shop.entity.OrderTest.java

// OrderTest.java

... 생략 ...

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

  • 즉시 로딩 사용시 연관된 모든 객체를 가져오기 때문에 성능상 문제가 발생할 수 있음

 

 

 

com.shop.entity.OrderItem.java

// OrderItem.java

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;
}
  • 즉시 로딩 대신에 지연 로딩 방식으로 변경

 

 

com.shop.entity.OrderTest.java

// OrderTest.java

... 생략 ...

    @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 엔티티만 조회하는 쿼리문이 실행되는 것을 볼 수 있음

 

  • 테스트 코드의 실행 결과 Order 클래스 조회 결과가 HibernateProxy라고 출력되는 것을 볼 수 있음
  • 지연 로딩으로 설정하면 실제 엔티티 대신에 프록시 객체를 넣어둠
  • 프록시 객체는 실제로 사용되기 전까지 데이터 로딩을 하지 않고, 실제 사용 시점에 조회 쿼리문이 실행

 

 

  • Order의 주문일(orderDate)을 조회할 때 select 쿼리문이 실행되는 것을 확인 가능

 

  • 인텔리제이에서 @OneToMany 어노테이션을 <Ctrl>+마우스로 클릭하면 @OneToMany 어노테이션의 경우는 기본 FetchType이 LAZY 방식으로 되어 있는 것을 확인할 수 있음

 

 

  • 어떤 어노테이션은 즉시 로딩이고, 어떤 어노테이션은 지연 로딩인데 사람인 이상 헷갈릴 수 있기 때문에 연관 관계 매핑 어노테이션에 Fetch 전략을 LAZY로 직접 설정

 

엔티티 지연 로딩 설정

com.shop.entity.Cart.java

// Cart.java

... 기존 임포트 생략 ...

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;
}

 

 

 

com.shop.entity.CartItem.java

// CartItem.java

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;
}

 

com.shop.entity.Order.java

// Order.java

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, fetch = FetchType.LAZY)		<< 수정
    private List<OrderItem> orderItems = new ArrayList<>();

    private LocalDateTime regTime;
    private LocalDateTime updateTime;

}

 


https://rogi221.tistory.com/176

 

Spring Boot - 쇼핑몰 프로젝트 05 (연관 관계 매핑 - 3)

https://rogi221.tistory.com/174 Spring Boot - 쇼핑몰 프로젝트 05 (연관 관계 매핑 - 1) 연관 관계 매핑 - 1 연관 관계 매핑 종류 엔티티들은 대부분 다른 엔티티와 연관 관계를 맺으며, JPA에서는 엔티티에 연

rogi221.tistory.com

 

 

728x90
반응형