동시성 처리기 2

문정현·2024년 1월 19일
0


모든 문제를 해결하고 보니 임시저장해둔 글에 수많은 사진속 이 사진이 있는데 왜 주의깊게 보지 않았을까

결과부터 얘기하자면 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을 적용해서 해당 문제를 해결하고자 한다.
다음글로 고고싱

profile
기록 == 성장

0개의 댓글