모든 문제를 해결하고 보니 임시저장해둔 글에 수많은 사진속 이 사진이 있는데 왜 주의깊게 보지 않았을까
결과부터 얘기하자면 Mapper를 미숙하게 사용하다가 동시성 처리를 의심하고 있었다
애초에 Mapping 로직에서 문제가 있었고 객체를 완전히 구성하지 않은 상태에서 save를 하고 있으니 트랜잰션이 롤백되면서 created_at을 생성해줘야 하는 JPAAuditing이 되지 않았던 것이다..
Code With Me로 같이 로직을 짰는데 서로 충분한 소통없이 내 메서드에의 RequestDto가 다르다는 점을 인지하지 못해서 발생했던 문제인것...
그래도 기록으로 남기기 위한 나의 삽질들
ERROR 34196 --- [nio-8080-exec-7] o.h.engine.jdbc.spi.SqlExceptionHelper : Column 'created_at' cannot be null
org.springframework.dao.DataIntegrityViolationException: could not execute statement [Column 'created_at' cannot be null]update purchase set
created_at이 null값이면 안되는데 지금 null이라고 에러가 나오고 있다
근데 이해가 안되는게 created_at은 JPAAuditing으로 트랜잭션에서 save할 때 자동 생성하고 있는데 null값이라는게 말도 안되는 것...
여기서 부터 모든 원흉의 시작 애초에 잘못된 로직인데 curl로 실행했을 때 200을 반환 받아버렸다 그것도 모든 쓰레드가..
그래서 생각 했다 테스트 환경에서만 JPAAuditing이 잘못 동작하는 건 아닐까?
StackOverFlow에서 Test클래스에 @Import를 붙여주라고 하지만 안됨
@Test
@DisplayName("데이터 정합성 테스트 - 핫딜의 한정 수량이 정확히 감소하는지 확인")
void purchaseHotdealWithPessimisticLock() throws InterruptedException {
// given
Product product = productRepository.save(TEST_PRODUCT);
Hotdeal hotdeal = hotdealRepository.save(
HotdealTestUtil.createHotdeal(TEST_START_DAY, TEST_DUE_DAY, TEST_DEAL_QUANTITY,
TEST_SALE, product));
int beforeDealQuantity = hotdeal.getDealQuantity();
int purchaseQuantity = 3;
int numberOfThreads = 10;
PurchaseHotdealRequestDto requestDto = HotdealTestUtil.createTestPurchaseHotdealRequestDto(
hotdeal.getId(), purchaseQuantity);
CountDownLatch countDownLatch = new CountDownLatch(numberOfThreads);
List<HotdealBuyer> workers = Stream.generate(
() -> new HotdealBuyer(buyer, countDownLatch, requestDto))
.limit(numberOfThreads)
.toList();
// when
List<Thread> threads = workers.stream().map(Thread::new).toList();
threads.forEach(Thread::start);
countDownLatch.await(60, TimeUnit.SECONDS);
// then
Hotdeal foundHotdeal = hotdealRepository.findById(hotdeal.getId())
.orElseThrow(() -> new AssertionError("핫딜을 찾을 수 없음"));
assertEquals(beforeDealQuantity - numberOfThreads * purchaseQuantity,
foundHotdeal.getDealQuantity()); // 100 - 10 * 3 = 70
}
private class HotdealBuyer implements Runnable {
private Member buyer;
private CountDownLatch countDownLatch;
private PurchaseHotdealRequestDto requestDto;
public HotdealBuyer(Member buyer, CountDownLatch countDownLatch,
PurchaseHotdealRequestDto requestDto) {
this.buyer = buyer;
this.countDownLatch = countDownLatch;
this.requestDto = requestDto;
}
@Override
public void run() {
hotdealService.purchaseHotdeal(member, requestDto);
countDownLatch.countDown();
}
}
쓰레드 환경이 잘못된건 아닐까? 하는 마음에 옆동네에서 문제 없단 코드를 참고해서 Runnable을 상복 받아서 구현하는 방법으로 수행해 보았다
진짜 이해 안가는점 : 이 방법으로 돌리면 두 번째 쓰레드까지 created_at이 생성된다
글 작성 초반부터 언급했지만 사실 범인은 mapper였기에 비즈니스 로직을 손보고 나서는 created_at을 잘 만들어 내서 test를 통과 했다
하지만 왜 created_at이 특정 쓰레드에선 생성이 되었고 그 이후의 트랜잭션에서는 롤백이 이루어져서 created_at이 생성되지 않았던 것 인지는 모른다. 주변 지인한테 물어보고 꼭 알아내고 말 것이다..
또한 아직 완벽하지 않은 처리이다
사진을 보면 쓰레드가 순서별로 구매를 하고 있지 않다 동기적으로 수행하고 있기 때문에 만약 10개 남은 매물에 9번 쓰레드가 구매를 해도 구매를 하지 못할 수도 있는 것 이다
이건 Redis의 SortedSet을 적용해서 해당 문제를 해결하고자 한다.
다음글로 고고싱