💬 사용해야 하는 이유
이번 개인 프로젝트의 구현 내용 중 배송 상태를 체크하여 상품을 취소하거나 상품의 반품을 진행하기 위해 주문한 내용의 배송 상태를 확인해보아야 한다.
이번 조건은 시간 단위가 아닌 일일단위로 결제를 한 당일부터 하루하루가 지나가며 조건이 생긴다
그렇다면 상태가 하루에 한 번씩 바뀌면서 체크를 해서 취소 혹은 반품을 할 수 있는가에 대한 상태를 반환받고 그 내용에 따라 진행이 되어야 하는데 이때 굉장히 번거로운 일이 발생한다
하루에 한번씩 배송 상태를 체크하는 요청을 보내야 하는가?
이는 굉장히 귀찮은 방법에 속한다. 그래서 이걸 자동으로 하는 방법이 없을까? 라는 생각이 들었고 바로 방법을 찾게 되었다.
💬 @Scheduling
그래서 Scheduling 을 사용하게 되었다. 이걸로 서버가 돌아가면 일정 시간마다 상태를 체크하여 조건에 따라 상태가 변경되게 하고 싶었다. 간단한 Scheduling에 대해서는 따로 블로그에 작성하였다.
https://latewalk.tistory.com/228
🌲 Spring 의 @Scheduling
🌠 @Schduling Java 에서 스케쥴링을 구현하는 방법 중 가장 일반적인 방법은 Spring Framework의 @Scheduling 어노테이션을 사용해서 일정한 시간 혹은 간격으로 또는 특정 시간에 코드가 실행되도록 설정
latewalk.tistory.com
💬 @Scheduling 의 도입
일단 조건을 보자면 하루마다 배송 상태가 변경되어야 하는데 테스트를 하기엔 너무 긴 시간이다 때문에 이를 1분 단위로 바꿔보고자 한다.
그럼 우선 조건을 먼저 알아보자
- 1분마다 상태가 변경된다. 순서는 결제 → 배송 중 → 배송 완료 → 반품 여부 → 완료 or 취소
- 취소는 결제에서 넘어가버리면 불가능하다
- 반품은 조금 특이하다 반품 여부를 확인하고 반품을 한다고 하면 누른 뒤 하루가 지나고 상품의 갯수 내용이 변동되어 실제 상품에도 적용된다.
이걸 조건으로 스케줄러를 사용해보려 한다
💬 완성본
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderStatusScheduling {
private final OrderRepository orderRepository;
private final OrderProductRepository orderProductRepository;
private final ProductRepository productRepository;
@Transactional
@Scheduled(fixedRate = 10000, initialDelay = 3000)
public void changeStatus() {
List<Order> orderAll = orderRepository.findAll();
for (Order order : orderAll) {
// 0. DONE 이거나 CANCEL 상태면 더이상 상태변경 불가
if (order.getStatus().equals(Status.DONE) || order.getStatus().equals(Status.CANCEL)) continue;
// 1. createAt와 updateAt의 시간 차이 계산
long createMinute = Duration.between(order.getCreateAt(), LocalDateTime.now()).toMinutes();
long updateMinute = Duration.between(order.getUpdateAt(), LocalDateTime.now()).toMinutes();
// 2. 반품이 된 상품인지 확인, 결제가 D-day
if (!order.getStatus().equals(Status.REFUNDING)) {
// D + 1 에 SHIPPING 으로 상태 변화
if (createMinute >= 1) order.transferStatus(1);
// D + 2 에 배달 완료
if (createMinute >= 2) order.transferStatus(2);
// D + 3 에 더 이상 상태 변경 불가
if (createMinute >= 3) order.transferStatus(4);
}
// 3. 반품이 된 상품이라면 UpdateTime 으로 체크
if (order.getStatus().equals(Status.REFUNDING)) {
// 반품한지 1일이 지나면 취소 상태로 변경하고
// 해당 주문에 묶인 물건의 재고를 원상 복구
if (updateMinute >= 1) {
order.transferStatus(5);
updateProduct(order);
}
}
orderRepository.save(order);
}
}
@Transactional
public void updateProduct(Order order) {
List<OrderProduct> orderProductList = orderProductRepository.findByOrder(order);
for (OrderProduct orderProduct : orderProductList) {
Product fixProduct = productRepository.findByProductUUID(orderProduct.getProduct().getProductUUID());
fixProduct.increaseStock(orderProduct.getUnitCount());
productRepository.save(fixProduct);
}
}
}
💬 발생한 오류
Unable to evaluate the expression Method threw 'org.hibernate.LazyInitializationException' exception.
이 오류는 지연 로딩이 적용된 Entity의 프록시를 초기화하면서 발생할 수 있는 오류라고 한다.
- 지연로딩 (LAZY) - 연관된 Entity를 쿼리할 때 해당 엔티티를 즉시 로드하는 것이 아닌 엔티티가 실제로 필요한 시점에 로드하는 방식을 이야기 한다.
이유
초기에 @Transactional을 붙여주지 않았는데 이로 인해 재고를 업데이트한 후에 해당 영속성 컨텍스트를 닫지 않고 order를 save하면서 문제가 발생하게 된것이다. ☞ 그렇다 나는 바보다...
현재 스케쥴링 작업은 주문을 가져와 상태 즉, 재고를 변경하고 저장하는 과정을 수행한다. 이때 영속성 컨텍스트가 닫히거나 엔티티가 detached 상태가 될 수 있다. 그러면서 order의 save와 product 의 save가 부딫히는 것을 볼 수 있는 것이다.
이를 해결하기 위해서는 엔티티가 수정된 이후에도 영속성 컨텍스트를 활성화 상태로 유지해주어야한다. 이때 사용하는 것이 바로 @Transactional이다. 스케쥴링 작업 하나를 전체적인 하나의 과정으로 생각하고 작업을 할 수 있게 해주기 때문에 이 설정을 해주어야 하는 것이다. 그리고 updateProduct 메소드에도 transactional 을 붙여주면서 해결이 되는 것이다
결론
트랜잭션 내에서 실행되는 작업은 모두 하나의 논리적 작업으로 간주된다. 작업 중에 발생한 모든 변경 사항이 DB에 반영되고 이렇게 DB의 일관성이 유지되며 발생한 문제를 해결할 수 있게 된다.
'프로젝트 > 항해99 개인 프로젝트' 카테고리의 다른 글
🚢 재고 관리를 위한 동시성 제어 (Monolithic Architecture) (2) | 2024.04.27 |
---|---|
🚢 WishList가 장바구니? (Redis 의 Hash타입 사용) (0) | 2024.04.26 |
🚢 이메일 인증 코드, Session 에서 Redis로 (0) | 2024.04.26 |
🚢 JPA 의 AttributeConverter (1) | 2024.04.26 |
🚢 로그아웃, RefreshToken?? Redis?? (feat. 비밀번호 변경) (1) | 2024.04.26 |