트러블슈팅

포인트 차감 동시성 문제 해결 - 비관적 락 적용

sudaruuu 2026. 4. 1. 15:42

2026.03.27 - [Spring 2기 과제] - 포인트 기반 커피 주문 결제 시스템 (도메인 설계)

 

포인트 기반 커피 주문 결제 시스템 (도메인 설계)

https://github.com/Banhklo2/coffee-order-system.git GitHub - Banhklo2/coffee-order-system: Spring Boot 기반 포인트 기반 커피 주문 결제 시스템Spring Boot 기반 포인트 기반 커피 주문 결제 시스템. Contribute to Banhklo2/coffee-o

sudaruuu.tistory.com

 

문제 상황

포인트 차감 로직을 구현하면서

동시에 여러 요청이 들어올 경우 데이터 정합성이 깨질 수 있는 문제를 예상하였다.


동시 요청 시 포인트 정합성 문제

사용자 포인트: 5000
주문 요청 1: 3000
주문 요청 2: 3000

 

두 요청이 동시에 들어오면

  1. 두 요청 모두 현재 포인트를 5000으로 조회
  2. 둘 다 결제가 가능하다고 판단
  3. 각각 포인트 차감 수행

그 결과

  • 실제로는 6000 포인트가 차감되는 문제가 발생하거나
  • 마지막 요청이 값을 덮어써 데이터가 손실될 수 있다.

즉, 사용자의 포인트가 잘못 차감되는 심각한 정합성 문제가 발생하다.


해결 방향

동시성 제어에는 대표적인 두 가지 방법이 있다.

1️⃣ 비관적 락 (Pessimistic Lock)

  • 데이터를 조회할 때 DB에서 락을 걸어버림
  • 다른 트랜잭션은 대기
  • SELECT ... FOR UPDATE

2️⃣ 낙관적 락 (Optimistic Lock)

  • 충돌이 없을 것이라 가정
  • 버전 충돌 시 예외 발생
  • @Version 사용

왜 비관적 락을 선택했을까?

포인트 차감 로직은

  • 동시에 요청이 발생할 수 있고
  • 데이터 정합성이 매우 중요하며
  • 실패 후 재시도 로직을 추가하기 복잡하다

따라서 충돌을 사전에 방지하는 비관적 락을 선택하였다.


적용 방법

1. 사용자 조회 시 락 적용

public interface UserRepository extends JpaRepository<User, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select u from User u where u.id = :userId")
    Optional<User> findByIdWithPessimisticLock(@Param("userId") Long userId);
}

2. 서비스 로직 수정

private User findUserWithLock(Long userId) {
    return userRepository.findByIdWithPessimisticLock(userId)
            .orElseThrow(() -> new ServiceException(ErrorCode.USER_NOT_FOUND));
}

 

기존

User user = findUser(userId);

 

변경

User user = findUserWithLock(userId);

3. 포인트 차감

user.use(menu.getPrice());

 

이제 해당 로직은

  • 하나의 트랜잭션만 접근 가능
  • 다른 요청은 대기 상태로 전환된다

동작 흐름

동시에 두 요청이 들어올 경우

요청 A → 락 획득
요청 B → 락 대기

요청 A → 포인트 차감 후 커밋
요청 B → 이후 실행

 

⭐ 항상 최신 데이터 기준으로 검증된다.


테스트 코드로 검증

동일 사용자에 대해 동시에 2건의 주문 요청을 수행하였다.

테스트 조건

  • 초기 포인트: 5000
  • 메뉴 가격: 3000
  • 동시 요청 수: 2

기대 결과

  • 1건 성공
  • 1건 실패
  • 최종 포인트: 2000

테스트 결과

동일 사용자에 대해 동시에 2건의 요청을 수행한 결과,

  • successCount = 1
  • failCount = 1
  • final point = 2000

으로 확인되었다.


결과 해석

  • 하나의 요청만 성공했고
  • 다른 하나는 락 대기 후 실행되며
  • 이미 차감된 포인트 기준으로 검증되어 포인트 부족으로 실패하였다

따라서 포인트는 1회만 정상 차감되었다.


결론

동시에 2건의 요청이 발생하더라고

하나의 트랜잭션만 성공하며

 

최종 포인트가 2000으로 유지됨을 통해

⭐ 동시 요청 상황에서도 데이터 정합성이 보장됨을 검증하였다.


정리

  • 동시 요청 시  Lost Update 문제 발생 가능
  • 비관적 락을 통해 동시 접근 자체를 차단
  • 테스트를 통해 정합성 보장 확인

느낀 점

포인트와 같이 정합성이 중요한 데이터는 단순히 빠르게 처리하는 것보다 정확하게 처리하는 것이 더 중요하다는 것을 느꼈다.

 

또한, 충돌이 발생한 이후에 처리하는 낙관적 락 방식보다

상황에 따라서는 충돌 자체를 사전에 차단하는 비관적 락이 더 적합할 수 있다는 점을 이해하게 되었다.

 

무엇보다도, 단순히 기능을 구현하는 것에서 끝나는 것이 아니라 테스트 코드를 통해 실제 동시성 상황까지 검증하는 과정이 굉장히 중요하다는 것을 직접 경험할 수 있었다.