에러 로그
org.hibernate.TransientPropertyValueException
object references an unsaved transient instance - save the transient instance before flushing
문제 로직
@Transactional
public String addItem(ItemFormDto itemFormDto) {
// 사용자 조회및 권한 체크
Member member = memberRepository.findById(itemFormDto.getSellerId())
.orElseThrow(() -> new BusinessException(ErrorCode.SELLER_NOT_FOUND));
if(!ValidUtil.isSeller(member)){
// 판매자가 아닌 경우 상품 등록 불가
throw new BusinessException(ErrorCode.UNAUTHORIZED_REQUEST);
}
// 상품 생성
Item item = Item.makeItem(itemFormDto);
// 재고 정보 생성
Stock stock = Stock.makeStock(itemFormDto.getStockNumber());
item.setStock(stock);
// 상품 저장시 재고도 같이 저장 됨(OneToOne 단방향 맺어둠)
itemRepository.save(item);
return "상품 등록이 완료되었습니다!";
}
@Entity
public class Item{
...
@OneToOne
@JoinColumn(name = "stock_id")
private Stock stock;
}
혹시 제가 짠 로직에서 왜 문제가 발생하셨는지 아시겠나요??? 모르시겠다면 저랑 함께 알아봅시다 :)
원인
원인에 대해 알아보기전 간단한 용어들에 대해 살펴보겠습니다!
JPA(Hibernate)에서 객체의 상태는 크게 3가지로 나뉩니다.
- Transient : JPA가 아예 인지를 하지 못하는 상태. 즉 객체는 메모리에 있지만 아직 데이터베이스에 저장되지 않았으며, Persistence Context에도 존재하지 않는 상태
- Persistent : 객체가 영속성 컨텍스트에 저장되어 있어 JPA의 관리 하에 있는 상태입니다. 이 상태의 객체에 대한 변경사항은 자동으로 DB에 반영됨
- Detached : 객체가 데이터베이스에 저장되어 있지만 현재 영속성 컨텍스트에서 관리받지 못하고 있는 상태
기존에 제가 작성한 코드에서는 Item 객체와 Stock 객체를 생성하고 item.setStock(stock)으로 두 객체를 연결합니다.
그런데 이때 주의할 점은 Item과 Stock 둘다 Transient 상태란 것입니다.
이 상태에서 itemRepository.save(item)을 호출하면 item객체는 Persistent 상태, 즉 영속성 컨텍스트에 저장되어 JPA의 관리를 받는 상태가 됩니다.
그런데 문제는 item 객체가 Persistent 상태가 되었을때 연관된 stock 객체는 여전히 Transient 상태라는 점입니다!
JPA는 Persistent 상태의 엔티티가 Transient 상태의 엔티티를 참조하고 있을 때 문제를 인식합니다. 이러한 상황은 데이터베이스에 반영해야 하는 Persistent 상태의 엔티티와 아직 DB에 반영되지 않은 Transient 상태의 엔티티 간의 참조 관계 때문에 발생하는 것입니다.
데이터베이스는 Persistent 상태의 엔티티만을 인식하기 때문에, 아직 Transient 상태인 엔티티를 참조하고 있는 상태는 데이터베이스에 정상적으로 반영될 수 없어 "object references an unsaved transient instance - save the transient instance before flushing"라는 에러 메시지가 발생하게 됩니다
이는 데이터베이스의 일관성을 유지하기 위한 JPA의 기능으로 이해하시면됩니다.
데이터베이스에 반영되지 않은 Transient 상태의 엔티티를 참조하고 있는 Persistent 상태의 엔티티는 이 둘 사이의 관계가 데이터베이스에 아직 반영되지 않은 상태이기 때문에, 이러한 상황을 허용한다면 데이터 무결성 문제가 발생할 수 있습니다. 따라서 JPA는 이러한 문제를 미리 인식하고 에러 메세지를 통해 개발자에게 알려주는 것이죠.
해결법
1. CaseCadeType.PERSIST 옵션 사용하기
@Entity
public class Item{
...
@OneToOne(casecade = CascadeType.PERSIST)
@JoinColumn(name = "stock_id")
private Stock stock;
}
이 옵션은 부모 엔티티가 Persistent 상태가 될때 연관된 자식 엔티티도 함께 Persistent 상태가 되게 하라는 의미입니다!
따라서 itemRepository.save(item)을 호출할때 item 객체뿐만 아니라 연관된 stock 객체도 함꼐 Persistent 상태가 됩니다. 이렇게 되면 item 객체가 참조하고 있는 stock 객체가 이제는 Transient 상태가 아니기 떄문에 에러가 발생하지 않습니다.
2. 기존 로직에서 stock을 저장하기
@Transactional
public String addItem(ItemFormDto itemFormDto) {
...
// 상품 생성
Item item = Item.makeItem(itemFormDto);
// 재고 정보 생성
Stock stock = Stock.makeStock(itemFormDto.getStockNumber());
// 재고 직접 저장
stockRepository.save(stock);
item.setStock(stock);
itemRepository.save(item);
return "상품 등록이 완료되었습니다!";
}
로직상에서 Stock을 먼저 저장하여 Persistent 상태로 만들고 그 다음에 Item을 저장하는 방식 또한 에러를 해결할 수 있습니다.
'이커머스 프로젝트' 카테고리의 다른 글
상품 주문 로직 구현해보기(with 동시성 제어 테스트 및 트러블 슈팅) (1) | 2023.08.05 |
---|---|
상품의 수정 및 삭제에 대해 정책적으로 다루어보자! (0) | 2023.08.03 |
재고 추가하기 - POST(멱등성 x), 비관적 락,낙관적 락 (0) | 2023.07.28 |
판매자의 상품 검색 기능 - 동적 쿼리 생성을 위해 JPA Specification 적용 (0) | 2023.07.24 |