[트러블 슈팅] 영속성과 JpaAuditing과 멀티스레드 테스트

김지현·2024년 1월 18일
0

Spring Boot 프로젝트

목록 보기
11/20

오늘의 트러블 슈팅은 또 다시 돌아온 영속성이다...

 		// 구매 인스턴스 생성
        Purchase purchase = PurchaseMapper.INSTANCE.toEntity(requestDto, member);
        purchaseRepository.save(purchase);

        // 총 금액 업데이트
        purchase.updateTotalPrice(calTotalPrice(purchaseProductList));

위의 로직에서 purchase 엔티티에 JpaAuditing을 사용하여 TimeStamp로 createdAt과 modifiedAt을 현재 시간으로 넣어주도록 설정하였는데 이런 오류가 발생하는 것이다.

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 값을 허용하지 않는 데이터베이스에서 update 쿼리를 실행할 때, 해당 컬럼이 null로 전달되어 발생한 것이다.

save와 동시에 auditing으로 createdAt을 넣어주는 것으로 알고 있는데 왜 null인지 알 수 없었다.. 원인 분석을 하자면 하나의 트랜잭션이 설정된 로직에서 해당 트랜잭션이 끝나기 전에 save와 update가 실행되어 그런 것이 아닐까라고 생각했고 save 트랜잭션이 끝난 후 update가 실행될 수 있도록 하기 위해 메서드를 분리하도록 해보았다.

private Purchase createPurchase(){
	Purchase purchase = PurchaseMapper.INSTANCE.toEntity(requestDto, member);
    return purchaseRepository.save(purchase);
}

이렇게 save한 객체를 리턴하도록 해주었더니 테스트를 통과하였다!
그런데 save한 객체를 purchase에 다시 저장하면 굳이 다른 메서드로 분리하지 않아도되지 않을까하여 맨 처음 메서드에서 다음과 같이 수정하였다.

 		// 구매 인스턴스 생성
        Purchase purchase = PurchaseMapper.INSTANCE.toEntity(requestDto, member);
        purhcase = purchaseRepository.save(purchase);

        // 총 금액 업데이트
        purchase.updateTotalPrice(calTotalPrice(purchaseProductList));

save한 객체를 반환하여 purchase에 다시 변수에 저장하였더니 이번에도 통과되었다.

찾아보니 이렇게 save한 객체를 반환해줘야 변경 감지가 가능하고 특히 특정 필드가 자동으로 생성된 값으로 업데이트되는 경우 해당 값을 확인하려면 이러한 방식으로 사용해야 한다고 한다.


그렇게 잘 해결하였다고 생각했으나, 비관적락을 이용한 멀티 스레드 테스트 코드 환경에서 동일한 오류가 다시 발생하게 된다.....

		@Test
        void purchaseHotdealWithPessimisticLock() throws InterruptedException {
            // given
            ...

            ExecutorService service = Executors.newFixedThreadPool(numberOfThreads);
            CountDownLatch latch = new CountDownLatch(numberOfThreads);

            for (int i = 0; i < numberOfThreads; i++) {
                service.execute(() -> {
                    try {
                        hotdealService.purchaseHotdeal(member, requestDto);
                    } finally {
                        latch.countDown();
                    }
                });
            }

			...
        }
        (팀원이 쓴 코드)

멀티스레드 환경을 테스트하기 위해 ExecutorService를 사용하여 스레드 풀을 생성하고 여러 스레드에서 동시에 hotdealService.purchaseHotdeal 메서드를 호출한다. 이때 첫 번째 스레드에서만 'created_at' cannot be null 오류가 발생하지 않았고 이후 모든 스레드에서 해당 오류가 발생하였다. 로그로 확인해보니 실제로 save 이후에도 created_at에 값이 들어가지 않아 null이 찍히는 것을 볼 수 있었다.

해당 오류의 원인을 계속 찾지 못하다가.... 테스트 코드로 멀티스레드를 만들어서 테스트하는 경우, auditing이 제대로 동작하지 않을 수 있다 는 사실을 알게 되었다. 그러므로 실제 환경에서는 제대로 동작할 수 있다는 것이다. 이 사실을 확인해보기 위해 curl을 사용하여 API 엔드포인트를 호출하는 방식으로 테스트를 진행해보았더니 성공하였다.

결국 실제 코드 로직에는 문제가 없었다... 그러나 한 가지 문제는 우리 프로젝트는 CI/CD 환경을 구축해놓았고 해당 멀티스레드 생성 테스트가 실패라고 뜨기 때문에 배포가 불가능하다는 것이다. 그렇다고 해당 테스트 코드를 제외할 순 없고 createdAt의 속성값을 nullable true로 설정하면 테스트가 통과되기는 하지만 테스트 통과를 위해서 실제 코드를 변경할 수는 없는 노릇이다.
CI 환경에서도 curl을 사용하도록 할 수 있다고 한다. 이 방식은 실제 환경과 유사하기 때문에 테스트의 정확도를 높여줄 수 있지만 인프라를 따로 구축해야하며 테스트 시간이 굉장히 오래걸린다는 단점이 존재한다.

코드를 테스트 후 배포하는 것은 중요한 일이지만 테스트 코드나 테스트 환경 구축에 문제가 발생하면 되려 로직 코드와 개발 프로세스에 영향을 미칠 수 있다는 점에서 괴리감이 조금 느껴졌다. 오류를 해결하려다가 새로운 깨달음을 얻은 기분... 해당 메서드를 어떤 방식으로 테스트할지는 조금 더 생각해보아야겠다.

0개의 댓글