[Spring] 분산락에서 발생하는 레이스 컨디션을 해결..!.part2

2025. 4. 8. 02:26카테고리 없음

개요

 

지난 블로그까지의 스토리

 

테스트 코드상으로 맥북에서는 문제가 없고, 기존 리눅스에서는 문제가 생기는 것을 발견

 

하지만 해당 문제를 좀 더 찾아보고 테스트를 해보았다.

 


 

nGrinder 로 과부화 테스트 및 동시성 테스트 진행

 

기존 테스트 코드를 조금 수정을 하긴 했지만 가장 큰 문제인 부분을 수정하지 않았기에 nGrinder로 레이스 컨디션 상태를 만들어 보았다.

 

테스트 상에서는 전부 통과를 하는 모습을 확인을 할 수 있다.

 

하지만 API를 호출을 하여서 값이 얼마나 남았는지를 확인해보자.

 

 

Stock이 남은 값을 의미하는데 100번을 반복을 하였는데 144번이나 남은 모습을 확인할 수 있었다.

사실 리눅스에서는 Stock이 200~300번까지 확인을 하였다.

 

이제 해결한 코드를 같이 봐보자.

 


문제 원인 파악

@Transactional
	public void payment(/*Long userId, */Long bookId, Long buyStock, Long money, PayType payType) {
//		UserResponse user = userService.getMyInfo(userId);

		Book book = bookRepository.findById(bookId).orElseThrow(()->new NotFoundException("Book not found"));

		RLock lock = redissonClient.getFairLock("book:"+bookId);

		try {
			boolean acquired = lock.tryLock(100L, 10L, TimeUnit.SECONDS);
			if (!acquired) {
				throw new InterruptedException();
			}
			
			// 책이 재고가 0개인 경우 || 구매하려는 개수 만큼 없는 경우
			if( book.getStock() == 0 || book.getStock() < buyStock ) {
				throw new InvalidRequestException(ErrorMessage.ZERO_BOOK_STOCK.getMessage());
			}

			// 돈이 부족한 경우
			if( money < buyStock * book.getPrePrice() ) {
				throw new InvalidRequestException(ErrorMessage.SHORT_ON_MONEY.getMessage());
			}

			// buyStock 만큼 구매
			book.updateStock(book.getStock() - buyStock);

			bookRepository.save(book);

			// todo : 오더 로직

		} catch (InterruptedException ex) {
			throw new InvalidRequestException(ErrorMessage.REDIS_ERROR.getMessage());
		} catch ( InvalidRequestException ex ) {
			throw new InvalidRequestException(ex.getMessage());
		} catch (Exception ex) {
			throw new InvalidRequestException(ErrorMessage.ERROR.getMessage());
		} finally {
			lock.unlock();
		}
	}

 

핵심 문제는 2개였는데, 

  • book을 조회를 가져오는 부분을 락을 걸지 않았던 점
  • 트랜잭션을 사용한 부분

일단 book 조회를 하는 것은 로직상 큰 실수 이기 때문에 반성을 하고 넘어간다.

 

핵심 문제인 트랜잭션을 사용하는 부분인데, 트랜잭션의 동작 원리를 알면 왜 문제인지 알 것이라고 생각이 든다.

 

자주 참고하는 블로그

 

[Spring] 트랜잭션에 대한 이해와 Spring이 제공하는 Transaction(트랜잭션) 핵심 기술 - (1/3)

1. Transaction(트랜잭션)에 대한 이해 [ 트랜잭션(Transaction)의 필요성 ] 만약 데이터베이스의 데이터를 수정하는 도중에 예외가 발생된다면 어떻게 해야 할까? DB의 데이터들은 수정이 되기 전의 상

mangkyu.tistory.com

 

자주 참고하는 분의 링크를 남기도록 하겠다.

 

단순하게 생각을 해본다면,

트랜잭션이 반영이 되기 전에 다른 쓰래드 및 다른 사용자가 접근을 하기 때문에 생기는 문제 같다.

 


문제 해결

 

트랜잭션과 book 찾는 순서를 변경을 하니 레이스 컨디션이 해결된 모습을 확인을 할 수 있었다.

 

최종 코드

public void payment(Long userId, Long bookId, Long buyStock, Long money, PayType payType) {
		UserResponse user = userService.getMyInfo(userId);

		RLock lock = redissonClient.getFairLock("book:"+bookId);

		try {
			boolean acquired = lock.tryLock(100L, 10L, TimeUnit.SECONDS);
			if (!acquired) {
				throw new InterruptedException();
			}
			Book book = bookRepository.findById(bookId).orElseThrow(()->new NotFoundException("Book not found"));
			// 책이 재고가 0개인 경우 || 구매하려는 개수 만큼 없는 경우
			if( book.getStock() == 0 || book.getStock() < buyStock ) {
				throw new InvalidRequestException(ErrorMessage.ZERO_BOOK_STOCK.getMessage());
			}

			// 돈이 부족한 경우
			if( money < buyStock * book.getPrePrice() ) {
				throw new InvalidRequestException(ErrorMessage.SHORT_ON_MONEY.getMessage());
			}

			// buyStock 만큼 구매
			book.updateStock(book.getStock() - buyStock);

			bookRepository.save(book);

			// todo : 오더 로직

		} catch (InterruptedException ex) {
			throw new InvalidRequestException(ErrorMessage.REDIS_ERROR.getMessage());
		} catch ( InvalidRequestException ex ) {
			throw new InvalidRequestException(ex.getMessage());
		} catch (Exception ex) {
			throw new InvalidRequestException(ErrorMessage.ERROR.getMessage());
		} finally {
			lock.unlock();
		}
	}

 


마치며

여러 시스템을 사용을 하다보니 문제점에 대해서 민감하게 반응하게 되는 점은 정말로 강점이라고 생각을 한다.

 

그리고 단순하게 지나칠뻔 했던 문제도 다시 돌아보면서 해결을 하니 상당히 느낌이 좋았다.

 

이후 시간이 된다면, 레디스도 최적화를 한번 진행을 해보는 것이 좋을 것 같다.

 

전체적인 응답 시간이 56ms 정도인데 한 40 정도 까지는 줄여볼만 하지 않을까 싶다.

 

그리고 코드를 좀더 꼼꼼하게 읽는 습관을 가지도록 해야겠다.

남들의 코드를 확인을 해보아도 다른 점을 찾지 못한점은 정말로 반성을 해야겠다고 생각이 든다.