Named query 사용시 영속성 컨텍스트와의 DB간의 데이터 정합성 문제

seungwon·2023년 12월 3일
0
post-custom-banner

앞 글에서 동시성 문제/데드락 해결을 위해 named query를 사용했습니다. 그 과정에서 발생한 영속성 컨텍스트와 db간의 데이터 정합성 문제를 해결한 과정을 작성했습니다. (관련글 링크 : 링크)

🧩 문제의 코드

@Modifying
@Query("UPDATE Item i SET i.stock = i.stock - :quantity WHERE i.id = :itemId AND i.stock > 0 AND i.stock >= :quantity")
int updateStock(Long itemId, int quantity);

-> 고객이 주문시 상품의 재고에서 수량만큼 위의 쿼리문을 통해 차감하는데 DB에는 반영이 되었는데 상품 재고를 조회하여 테스트 코드에서 검증시 반영이 안 되어 있는 문제가 발생했습니다.

👉🏻 코드로 보면

@DisplayName("고객이 주문한 상품을 취소하면 재고가 다시 늘어난다.")
@Test
@Transactional
void cancel_order() {
		// given
		
                ....
                
		// when
		// then
		assertThat(item.getStock(), is(originalStock));  // 재고 갱신 전 검증

		OrderRes orderRes = orderService.create(orderCreateReq, member.getProviderId()); // named query를 포함한 부분
		long orderId = orderRes.orderId();
		Order order = orderRepository.findById(orderId).get();

		orderRepository.flush();
		itemRepository.flush();

		assertThat(item.getStock(), is(originalStock - quantity)); // 재고 갱신 (1) : 상품 주문량만큼 재고 차감

		orderService.updateStatus(orderId, new OrderStatusUpdateReq("CANCELED"), member.getProviderId()); // 주문 취소 -> 재고 갱신 (2)
		assertThat(order.getOrderStatus(), is(CANCELED));

		assertThat(item.getStock(), is(originalStock)); // 재고가 다시 늘어났는지 검증
	}
}

원했던 재고 변화는 원했던 재고 변화는 3 →(상품 구매) → 1 →(주문 취소)→ 3 였지만

실제로는 3 →(구매) → 3 →(취소)→ 5 와 같은 변화를 보였습니다.

재고 감소는 안 되는데 주문을 취소하였을 때 다시 늘어나는 것을 보고 아이디어를 얻었습니다.

🧩 원인 파악

차이점은 재고 차감은 위의 named query를 활용하지만 주문 취소 후에 수량이 늘어나는 것은
영속성 컨텍스트에 의해 관리되고 있는 상품을 가져와 dirty checking에 의해 상품 재고가 수정됩니다.

즉, named query에 의한 결과가 정상적으로 조회되지 않고 dirty checking에 의한 결과는 제대로 반영되었습니다.

DB에는 반영이 잘 되고 있었기 때문에 영속성 컨텍스트의 문제라고 보고

entitymanager의 clear() 메서드를 호출해 영속성 컨텍스트를 지워보고자 했습니다.

@DisplayName("고객이 주문한 상품을 취소하면 재고가 다시 늘어난다.")
@Test
@Transactional
void cancel_order() {
		...

		OrderRes orderRes = orderService.create(orderCreateReq, member.getProviderId()); // named query를 포함한 부분

		entityManager.clear();

		...
        
	}
}

그렇게 하니 재고가 3 →(구매) → 3 →(취소)→ 3 로 변화했습니다. 즉, dirty checking을 통한 재고 갱신도 이루어지지 않았습니다.


그래서 아래처럼 named query 수행 전 후로 각각 entity manager의flush(), clear() 메소드를 호출해 주니 정상 작동했습니다.

@DisplayName("고객이 주문한 상품을 취소하면 재고가 다시 늘어난다.")
@Test
@Transactional
void cancel_order() {
		...

		entityManager.flush();

		OrderRes orderRes = orderService.create(orderCreateReq, member.getProviderId()); //named query를 포함한 부분

		entityManager.clear();

		...


	}
}

원인은 다음과 같습니다.

jpa로 조회를 실행하면 1차 캐시를 확인해 해당 엔티티가 1차 캐시에 존재한다면 DB에 접근하지 않고 1차 캐시에 있는 엔티티를 반환합니다.

그런데 named query는 영속성 컨텍스트를 통하지 않고 바로 쿼리를 실행해 DB에 접근하기 때문에 DB와 영속성 컨텍스트 간의 데이터 정합성이 깨진 것 입니다. 그래서 named query 수행 전 영속성 컨텍스트에 있는 내용들을 DB에 반영(flush())하고, named query 수행 후에 영속성 컨텍스트에 있는 내용을 모두 지워(clear()) 다시 DB에 접근하여 가져오도록 하면 문제를 해결할 수 있습니다.

🧩 결론

위의 코드에서

entityManager.flush(); 

OrderRes orderRes = orderService.create(orderCreateReq, member.getProviderId());// named query를 포함한 부분

entityManager.clear();

flush() 메소드와 같은 역할을 하는 것이 flushAutomatically = true 옵션,
clear() 메소드와 같은 역할을 하는 것이 clearAutomatically = true 옵션입니다


그래서 named query 에 @Modifying(clearAutomatically = true, flushAutomatically = true) 와 같이 clearAutomatically, flushAutomatically 옵션을 통해 해결할 수 있습니다.

post-custom-banner

0개의 댓글