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
두 요청이 동시에 들어오면
- 두 요청 모두 현재 포인트를 5000으로 조회
- 둘 다 결제가 가능하다고 판단
- 각각 포인트 차감 수행
그 결과
- 실제로는 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 문제 발생 가능
- 비관적 락을 통해 동시 접근 자체를 차단
- 테스트를 통해 정합성 보장 확인
느낀 점
포인트와 같이 정합성이 중요한 데이터는 단순히 빠르게 처리하는 것보다 정확하게 처리하는 것이 더 중요하다는 것을 느꼈다.
또한, 충돌이 발생한 이후에 처리하는 낙관적 락 방식보다
상황에 따라서는 충돌 자체를 사전에 차단하는 비관적 락이 더 적합할 수 있다는 점을 이해하게 되었다.
무엇보다도, 단순히 기능을 구현하는 것에서 끝나는 것이 아니라 테스트 코드를 통해 실제 동시성 상황까지 검증하는 과정이 굉장히 중요하다는 것을 직접 경험할 수 있었다.
'트러블슈팅' 카테고리의 다른 글
| Request DTO에 @Builder를 쓰면 안 되는 이유 (0) | 2026.04.01 |
|---|---|
| 인기 메뉴 조회 성능 개선 - 인덱스 도입 (0) | 2026.04.01 |
| 주문 완료 후 외부 전송을 이벤트 기반으로 분리하며 겪은 문제 (0) | 2026.03.31 |
| 외부 API 호출 비동기 처리 적용 (@Async) (0) | 2026.03.31 |
| 외부 플랫폼 주문 전송 기능 - Mock API 구현 (0) | 2026.03.31 |