Order 생성 시, Cart 엔티티의 cartItems를 Order의 orderMenus에 그대로 할당하여 저장하려고 할 때 detached entity passed to persist
와 같은 JPA 시스템 예외가 발생함.
javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist
주문 전에는 장바구니(Cart)에 아이템(CartItem) 을 넣어 관리하고, 주문(Order)이 시작되면 장바구니를 호출하여 장바구니에 들어있는 아이템을 오더에 그대로 넣어주며, 아이템에서 장바구니와의 연관관계를 끊고 주문과의 관계를 시작하는 구조.
주문이 시작 되기 전 장바구니는 소프트 딜리트를 적용하여, 향후 사용자의 관심메뉴를 제공하는 기능에 활용될 예정.
@Transactional
public OrderResponseDto createOrder(AuthUser authUser, Pageable pageable) {
// ... (유저, 카트 검증 로직)
Cart cart = cartRepository.findCartByUserAndDeletedAtIsNull(user);
// Order 생성 시 detach된 CartItem 문제 발생
Order newOrder = new Order(OrderState.CLIENT_ACCEPT, user, cart);
Order order = orderRepository.save(newOrder);
cart.getCartItems().forEach(cartItem -> cartItem.updateOrder(order));
cart.setDeletedAt();
return new OrderResponseDto(order, pageable);
}
public interface CartRepository extends JpaRepository<Cart, Long> {
Cart findCartByUserAndDeletedAtIsNull(User user);
}
@Getter
@Entity
@NoArgsConstructor
@Table(name = "orders")
public class Order extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private OrderState state;
@Enumerated(EnumType.STRING)
private CancelReason reason;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "shop_id", nullable = false)
private Shop shop;
// 아이템 (CartItem)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CartItem> orderMenus;
private Integer totalPrice;
public Order(OrderState state, User user, Cart cart) {
this.state = state;
this.user = user;
this.shop = cart.getShop();
this.totalPrice = cart.getTotalPrice();
}
}
@Getter
@Entity
@NoArgsConstructor
@Table(name = "carts")
public class Cart extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "shop_id", nullable = false)
private Shop shop;
// 아이템 (CartItem)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CartItem> cartItems;
private Integer totalPrice;
public Cart(User user, Shop shop, List<CartItem> cartItems){
this.user = user;
this.shop = shop;
this.cartItems = cartItems;
}
}
@Getter
@Entity
@NoArgsConstructor
@Table(name = "cart_items")
public class CartItem extends BaseTimeEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne
@JoinColumn(name = "cart_id")
private Cart cart;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "menu_id", nullable = false)
private Menu menu;
private String name;
private Integer price;
private Integer quantity;
public CartItem(Menu menu, Integer quantity){
this.menu = menu;
this.name = menu.getName();
this.price = menu.getPrice();
this.quantity = quantity;
}
public void updateCart(Cart cart) {
this.cart = cart;
}
public void updateOrder(Order order){
this.cart = null;
this.order = order;
}
}
Order를 저장할 때 JPA는 CartItem들이 이미 DB에 존재하는 detached 상태임에도 불구하고 cascade persist를 시도하여 예외 발생함.
Lazy 로딩 문제
fetch = FetchType.LAZY
설정이 되어 있을 경우, Cart 조회 시 CartItem이 즉시 로딩되지 않음. 따라서 Order에 추가할 때 detach된 객체가 될 가능성이 높음.
영속성 컨텍스트의 범위
Cart를 조회한 시점에는 CartItem이 영속 상태가 아닐 수 있으며, 트랜잭션 범위를 벗어나면 detach됨.
CascadeType.PERSIST 설정
Order의 orderMenus에 CascadeType.PERSIST가 적용된 경우, Order를 저장하면서 관련 CartItem도 persist하려고 시도함. 하지만 CartItem은 이미 DB에 존재하는 엔티티이므로 새로 persist할 수 없음.
Detached 엔티티 문제
detach된 CartItem을 Order에 추가한 후 저장하면 JPA가 이를 새로운 엔티티로 간주하여 persist하려고 함. 그러나 기존 CartItem은 식별자를 가지고 있으므로 새로 persist하는 것이 불가능하여 예외가 발생함.
EntityManager.merge() 사용 가능
detached 상태의 엔티티를 영속 상태로 변경하려면 persist가 아닌 merge()를 사용해야 함.
패치 조인(fetch join) 활용 가능
JPQL에서 join fetch
를 사용하면, Cart와 연관된 CartItem을 함께 로딩할 수 있어 detach 문제를 방지할 수 있음.
매핑 테이블을 활용한 해결 가능
CartItem을 Order에 직접 추가하는 대신, 매핑 테이블을 활용하여 관계를 관리하면 detach 문제를 피할 수 있음.
ID를 기반으로 CartItem을 새로 조회하면 문제 해결 가능
detach된 CartItem을 ID로 다시 조회하면 영속 상태로 변경되어 문제 해결됨.
Order 생성 시, Cart에서 가져온 CartItem들이 detach 상태이기 때문에, Order를 persist할 때 JPA가 이들 객체를 새 엔티티로 인식하여 예외 발생.
영속성 컨텍스트에서 관리되지 않는 CartItem
Cascade persist 문제
detached entity passed to persist
예외가 발생하게 됨.Lazy 로딩과 detach 문제
Order에 할당하는 방식의 문제
트랜잭션 범위 문제
(A) CartItem을 새로 조회해서 해결 (가장 안전하며, 실제 프로젝트에 적용된 방식)
방법 설명
detach 상태의 CartItem을 다시 영속 상태로 만들기 위해 ID로 새로 조회해서 해결함.
수정 코드 예시 (서비스 로직)
@Transactional
public OrderResponseDto createOrder(AuthUser authUser, Pageable pageable){
Cart cart = cartRepository.findCartByUserAndDeletedAtIsNull(user);
// 새로운 Order 생성
Order newOrder = new Order(OrderState.CLIENT_ACCEPT, user, cart);
newOrder.setTotalPrice(cart.getTotalPrice()); // totalPrice 설정
// CartItem 복사 및 Order에 연결
List<CartItem> orderMenus = new ArrayList<>();
// 새로운 CartItem (복사본) 생성하여 Order과 연결
for (CartItem cartItem : cart.getCartItems()) {
CartItem newOrderItem = new CartItem(cartItem.getMenu(), cartItem.getQuantity());
newOrderItem.updateOrder(newOrder);
orderMenus.add(newOrderItem);
}
// Order에 새로운 CartItem 목록 설정
newOrder.setOrderMenus(orderMenus);
// Order 저장 (CascadeType.ALL 로 인해 CartItem들도 함께 저장됨)
Order order = orderRepository.save(newOrder);
cart.setDeletedAt();
// Cart soft delete (deletedAt 업데이트)
cartRepository.save(cart);
return new OrderResponseDto(order, pageable);
}
장점
단점
방법 설명
Cart 엔티티의 cartItems 필드를 EAGER로 설정(혹은 특정 쿼리에서 엔티티그래프를 사용)하면, Cart 조회 시에 연관 CartItem들이 즉시 로딩되어 영속성 컨텍스트에 포함됨.
수정 코드 예시 (Order 엔티티)
// Cart 엔티티 - cartItems 필드에 EAGER 설정
@OneToMany(mappedBy = "cart", fetch = FetchType.EAGER)
private List<CartItem> cartItems;
장점
단점
방법 설명
detach된 CartItem들을 EntityManager.merge()를 호출해 재부착한 후 Order에 연결하는 방식.
수정 코드 예시 (서비스 로직)
@Service
@RequiredArgsConstructor
public class OrderService {
// 필요한 Repository와 함께 EntityManager 주입
private final OrderRepository orderRepository;
private final CartRepository cartRepository;
private final UserRepository userRepository;
@PersistenceContext
private EntityManager entityManager;
@Transactional
public OrderResponseDto createOrder(AuthUser authUser, Pageable pageable) {
// ... (유저, 카트 검증 로직)
Cart cart = cartRepository.findCartByUserAndDeletedAtIsNull(user);
Order order = new Order(OrderState.CLIENT_ACCEPT, user, cart);
if (cart.getCartItems() != null) {
for (CartItem cartItem : cart.getCartItems()) {
// merge()를 통해 detach된 CartItem 재부착
CartItem managedCartItem = entityManager.merge(cartItem);
order.addCartItem(managedCartItem);
}
}
Order savedOrder = orderRepository.save(order);
cart.setDeletedAt();
return new OrderResponseDto(savedOrder, pageable);
}
}
장점
단점
방법 설명
JPQL 쿼리에서 패치 조인(join fetch)을 사용해 Cart와 연관된 CartItem을 한 번의 쿼리로 로딩하여, CartItem들이 영속 상태에 포함되도록 함.
수정 코드 예시 (CartRepository, 서비스 로직)
public interface CartRepository extends JpaRepository<Cart, Long> {
@Query("select c from Cart c join fetch c.cartItems where c.user = :user and c.deletedAt is null")
Cart findCartByUserAndDeletedAtIsNull(@Param("user") User user);
}
@Transactional
public OrderResponseDto createOrder(AuthUser authUser, Pageable pageable) {
// ... (유저, 카트 검증 로직)
// 패치 조인을 통해 CartItem들이 함께 로딩됨
Cart cart = cartRepository.findCartByUserAndDeletedAtIsNull(user);
Order order = new Order(OrderState.CLIENT_ACCEPT, user, cart);
if (cart.getCartItems() != null) {
for (CartItem cartItem : cart.getCartItems()) {
order.addCartItem(cartItem);
}
}
Order savedOrder = orderRepository.save(order);
cart.setDeletedAt();
return new OrderResponseDto(savedOrder, pageable);
}
장점
단점
방법 설명
Order 엔티티의 연관관계에 대해 CascadeType.PERSIST 대신 CascadeType.MERGE 등을 사용하면, Order 저장 시 detach된 CartItem을 자동으로 merge하여 재부착할 수 있음.
수정 코드 예시 (Order 엔티티)
@Getter
@Entity
@NoArgsConstructor
@Table(name = "orders")
public class Order extends BaseTimeEntity {
// ... (기존 필드 생략)
// CascadeType.PERSIST를 제거하여 persist 시 detach된 CartItem이 재부착되지 않도록 함
@OneToMany(mappedBy = "order", cascade = {CascadeType.MERGE, CascadeType.REMOVE}, orphanRemoval = true)
private List<CartItem> orderMenus = new ArrayList<>();
// 생성자 및 헬퍼 메서드
public Order(OrderState state, User user, Cart cart) {
this.state = state;
this.user = user;
this.shop = cart.getShop();
this.totalPrice = cart.getTotalPrice();
}
public void addCartItem(CartItem cartItem) {
orderMenus.add(cartItem);
cartItem.updateOrder(this);
}
}
장점
단점
방법 설명
Order와 CartItem 사이의 연관관계를 직접 외래키로 관리하지 않고, 별도의 매핑 테이블을 사용하여 관계 정보를 저장하는 방식임. 이 방식은 Order 저장 시, CartItem 자체는 변경하지 않고 매핑 테이블에 관계만 기록하므로 detach 문제를 피할 수 있음.
수정 코드 예시 (Order 엔티티)
@Getter
@Entity
@NoArgsConstructor
@Table(name = "orders")
public class Order extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private OrderState state;
@Enumerated(EnumType.STRING)
private CancelReason reason;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "shop_id", nullable = false)
private Shop shop;
// 매핑 테이블을 사용한 단방향 OneToMany 관계
@OneToMany(cascade = {CascadeType.MERGE, CascadeType.REMOVE})
@JoinTable(
name = "order_cart_items",
joinColumns = @JoinColumn(name = "order_id"),
inverseJoinColumns = @JoinColumn(name = "cart_item_id")
)
private List<CartItem> orderCartItems = new ArrayList<>();
private Integer totalPrice;
public Order(OrderState state, User user, Cart cart) {
this.state = state;
this.user = user;
this.shop = cart.getShop();
this.totalPrice = cart.getTotalPrice();
}
public void addCartItem(CartItem cartItem) {
orderCartItems.add(cartItem);
}
}
@Transactional
public OrderResponseDto createOrder(AuthUser authUser, Pageable pageable) {
// ... (유저, 카트 검증 로직)
Cart cart = cartRepository.findCartByUserAndDeletedAtIsNull(user);
Order order = new Order(OrderState.CLIENT_ACCEPT, user, cart);
if (cart.getCartItems() != null) {
for (CartItem cartItem : cart.getCartItems()) {
order.addCartItem(cartItem);
}
}
Order savedOrder = orderRepository.save(order);
cart.setDeletedAt();
return new OrderResponseDto(savedOrder, pageable);
}
장점
단점