항플 백엔드에서, 내가 선택한 주제는 콘서트 예약 서비스이다.
콘서트 예약 서비스에서는 아래와 같은 세 가지의 동시성 이슈 발생 가능성이 있다.
- 유저의 잔액 충전
- 유저의 좌석 예약
- 예약된 좌석 결제
동시성 제어와 동시성 제어 방안 분석
동시성 제어에는 여러 방안이 있지만 이번에는 낙관적 락, 비관적 락, Redis pub/sub(분산 락), kafka messaging 기법을 분석해 보았다.
동시성 제어 : 낙관적 락(Optimistic Lock)
트랜잭션 충돌이 적을 것이라 예상하고, 트랙잭션에 잠금을 걸지 않은 상태로 데이터를 조회하여,
수정하는 시점(트랜잭션의 최종 커밋 단계)에 검증해 충돌을 감지하는 동시성 제어 방식이다.
동시 요청 중에 한건만 성공해야 하는 케이스에 적합한 잠금 방식이며, 트랜잭션, Lock 설정 없이 데이터 정합성을 보장할 수 있으므로 성능적으로 우위에 있다.
낙관적 락은 버전 번호나 타임스탬프를 통해 트랜잭션 간 충돌을 확인하는데, 일반적으로 버전 번호를 사용한다.
JPA에서 낙관적 락을 처리하는 방법은 @Version 어노테이션을 이용해 처리한다.

이 어노테이션은 Jpa Entity에 버전 관리용 필드를 추가해 트랜잭션 내에서 처음 조회되었을 때의 버전와 이후 수정 후 커밋되는 시점의 버전을 비교한다.
비교한 결과 버전이 서로 다르다면 이는 충돌이 발생한 것으로 판단하고 ObjectOptimisticLockingFailureException을 발생시킨다. 해당 Exception을 핸들링하여 재시도 로직을 구현할 수 있다.

이 방법은 데이터 충돌 가능성이 적을 때 유용하며, 데이터가 많거나 자주 갱신되는 환경에서 비관적 락처럼 데이터 접근을 제한하지 않기 때문에 성능 이점이 크다. 하지만 충돌이 발생했을 때 충돌을 해결해야만 하는 유스케이스라면 필수적으로 재시도 로직을 구현해야 한다.
이로 인해 충돌이 잦은 경우에는 낙관적 락을 사용할 때 재시도에 따른 성능 트레이드오프를 고려해야 한다.
장점 : 데이터에 대한 접근을 미리 제한하지 않아 동시성 성능 ↑ , Blocking이 발생하지 않아 시스템 자원의 활용 효율 ↑
단점 : 충돌이 발생했을 때 트랜잭션을 롤백하고 재시도 로직이 수행될 수 있으므로, 충돌이 자주 발생하는 환경에서 성능 극 저하됨
구현 복잡도 : 중 (재시도 로직)
동시성 제어 : 비관적 락(Pessimistic Lock)
동시 요청에서 순차적으로 진행될 때 성공할 수 있는 요청이라면 성공시키는 케이스에 적합한 잠금 방식이다.(+ 하지만 순서를 "반드시" 보장하지는 않는다.)
데이터 충돌 가능성이 크거나, 자주 변경되는 데이터에 대한 높은 동시성 요청을 예상할 때 사용된다.
트랜잭션이 데이터를 사용하는 동안 다른 트랜잭션이 해당 데이터에 접근하는것을 제한 하기 때문에, 충돌이 발생할 가능성을 사전에 차단할 수 있다.
비관적 락은 구체적인 기능은 아니고, 트랜잭션의 접근 방지를 위한 데이터 선점 전략을 표현한 개념이다.
이 개념에 속하는 구체적인 구현 기능들이 배타락, 공유락이다.
비관적 락을 이해하기 위해서는 InnoDB의 row-level-lock과 배타락, 공유락에 대한 내용을 확실히 숙지하여야 한다.
(추가+ JPA에서는 table-level-lock을 지원하지 않는다.)
row-level-lock이란 데이터베이스에서 특정 행(row)단위로 락을 거는 방식이다. InnoDB에서는 기본적으로 row-level-lock으로 발생하는데, 특정 행(row)단위로 락을 거는 방식을 뜻한다.
배타락(Exclusive Lock, X-Lock)이란 데이터에 대한 쓰기를 포함한 모든 접근을 제한하는 강력한 잠금 방식이다. 이 락이 걸린 데이터는 오직 하나의 트랜잭션만이 접근할 수 있으며, 다른 트랜잭션은 읽기와 쓰기 모두 불가능하다.
배타락의 특징으로는 단독 접근만 허용한다는 것과, 데이터의 일관성과 무결성을 확실히 보장 가능하다는 것, 그리고 교착상태(DeadLock) 가능성이 있다는 것이다.

공유락(Shared Lock, S-Lock)이란 데이터에 대한 읽기 작업만 허용하는 락이다.
공유락이 설정된 데이터는 여러 트랜잭션이 동시에 읽을 수 있지만 쓰기 작업은 제한된다.
주로 읽기 작업이 많고, 데이터 변경이 드물거나 변경 시 일관성을 유지해야 할 때 사용된다.
공유락의 특징으로는 다중 접근을 허용한다는 것, 배타락보다 상대적으로 경합이 적다는 것이다.

++ 교착상태(Deadlock)이란??
아래와 같은 케이스일 때 데드락이 발생한다.
- 트랜잭션 A와 B가 자원 1과 자원 2에 대해 공유 락을 설정하여 동시에 읽기 작업을 수행하고 있다.
- 트랜잭션 A가 자원 1에 대해 배타적 락을 요청하여 업데이트를 시도하지만, 자원 2에서 트랜잭션 B의 공유 락이 해제되기를 기다린다.
- 반대로 트랜잭션 B도 자원 2에 대해 배타적 락을 요청하지만, 자원 1에서 트랜잭션 A의 공유 락이 해제되기를 기다린다.
++ 그러면 어떻게 데드락을 방지할까??

장점 : 데이터에 대한 접근을 엄격하게 제어하여 순차적으로 처리하기 때문에 데이터의 일관성과 무결성을 보장 가능하며 Dirty Read상황이 발생할 가능성이 없다.
단점 : 락을 획득하기 위해 대기하는 시간이 길어질 수 있기 땜누에 시스템의 성능에 부정적인 영향을 미칠 수 있으며 교착상태(DeadLock) 발생 가능성이 있다.
구현 복잡도 : 하 (JPA를 통한 쉬운 구현)
동시성 제어 : Redis - pub/sub 락
redis의 pub/sub 구독 기능을 이용한 효과적인 락 제어 방식이다.
분산 환경에서 상태를 일관되게 관리하면서 성능과 효율성을 높일 수 있는 매우 강력한 방식이다.
Lock 획득을 실패했을 시, 이벤트를 publish하고 차례가 될 때까지 대기하도록 하여 효율적인 Lock 관리가 가능하다.
subscriber들 중에 먼저 선점한 작업만 락 해제가 가능하므로 안정적으로 원자적 처리가 가능하다.
직접 구현하거나 라이브러리를 이용할 때 구현이 달라질 수 있으므로 주의해서 사용해야 한다.
Java 진영에서는 Redission이라는 라이브러리를 사용해 구현 가능하다.
Redission이란 Redis를 기반으로 분산 데이터 구조와 동시성 유틸리티 라이브러리로, java에서 RedisClient 기능을 제공할 뿐만 아니라, 분산 환경에서 동시성을 안전하게 관리할 수 있는 다양한 데이터 구조와 유틸리티를 제공한다.
또한, Redis의 Pub/Sub 기능을 활용해 메시지 발행과 구독을 지원한다. RTopic과 같은 클래스를 통해 Redis에 메시지를 발행하고, 여러 인스턴스에서 실시간으로 메시지를 수신할 수 있다.
RTopic과 TTL설정이 가능한 RLock을 활용해 락을 컨트롤하여 동시성 제어를 효율적으로 구현 가능하다.
Redission을 활용한 동시성 제어 방식은 아래와 같은 프로세스를 거친다.
- 락 상태 구독 : 모든 인스턴스는 RTopic을 구독하여, 리소스 락 상태 변화(획득 또는 해제) 메시지를 실시간으로 수신할 준비를 한다.
- 락 획득 시 알림 발행 : 특정 메서드 실행 시 RLock으로 락을 선점하려 한다. 락을 선점하면 RTopic에 "락이 획득되었다"는 메시지를 발행하여 다른 인스턴스들이 이 리소스에 접근하지 못하도록 알린다.
- 락 해제 시 알림 발행 : 작업이 완료되면 락을 해제하고 RTopic에 락 해제 메시지를 발행하여, 대기 중인 인스턴스가 이를 수신하고 다시 락을 시도할 수 있도록 한다.
Redission을 활용하여 락과 트랜잭션을 함께 사용할 때, 락 획득과 해제 순서는 매우 중요하다.

트랜잭션 시작 전에 락을 먼저 선점하여 다른 프로세스가 동일한 리소스에 접근하지 못하게 해야 한다.
이는 락을 선점하기 전에 트랜잭션이 시작되면 다른 트랜잭션도 동일 리소스에 접근할 수 있어, 일관성 문제가 발생하거나 데이터 충돌이 발생할 수 있기 때문이다.
이렇게 트랜잭션 이전에 락을 선점함으로써 데이터 접근데 대한 원자성을 보장하고, 동시에 다른 트랜잭션의 접근을 차단하여 동시성 문제를 방지할 수 있다.
또한, 트랜잭션이 완료된 이후에 락을 해제해야, 데이터 변경이 완료될 때까지 다른 트랜잭션이 해당 리소스에 접근하지 못하도록 보호할 수 있다. 특히 분산 환경에서 여러 인스턴스가 락을 동시에 요청하는 경우, 트랜잭션이 끝나기 전에 락을 해제하면 다른 트랜잭션이 동일 리소스에 접근할 수 있어 데이터 불일치가 발생할 수 있다.
AOP를 활용하면 더욱 깔끔하고 재사용성 높은 락을 구현할 수 있다.
장점 : 락 해제 이벤트를 먼저 받은 경우에만 작업을 수행하게 되어 순서와 원자성이 보장된다.(그렇다고 순서를 100% 보장하지는 않는다. 예외는 항상 존재한다.) 락을 선점한 작업만이 해당 리소스를 점유할 수 있고 TTL설정을 함으로써 예기치 못한 오류나 교착상태(DeadLock), 경쟁상태(Race Condition)를 방지할 수 있으며, DB에 부하를 대폭 감소시킨다.
단점 : 노드 간의 지연이나 네트워크 분할이 발생하면 메시지가 누락되거나 순서가 뒤섞일 가능성이 있다. 또 다른 시스템이 필요하고 장애대응 방생 시 추가적인 물리적, 기술적 비용이 들어갈 것이다.
구현 복잡도 : 상
동시성 제어 : kafka messaging
메시지큐(MQ)와 파티션(파티션 키)을 통해 순서를 보장하면서 여러 인스턴스가 특정 리소스를 안전하게 접근하도록 하는 구조.
이를 통해 효율적이고 안정적인 동시성 처리가 가능하다.
하지만 kafka만 독립적으로 사용할 순 없다. kafka의 클러스터의 상태를 관리하고 안정성을 지원하는 중요한 역할을 담당하는 Zookeeper도 구동되는 환경이어야 한다. Zookeeper는 kafka의 브로커, 토픽, 파티션, 컨슈머 그룹에 대한 메타데이터를 관리하고, 클러스터 내 브로커 간의 조정과 장애 복구를 지원한다.
Kafka Messaging을 활용한 동시성 제어 프로세스는 아래와 같다.
- 메시지 발행 및 파티션 지정 : Producer가 동일한 키로 메시지를 발행하면, kafka는 이를 항상 동일한 파티션에 배치하여 메시지 순서를 보장한다.
- Consumer 순차 처리 및 동시성 제어 : 각 파티션은 하나의 컴슈머가 순차적으로 처리하며, 이를 통해 동일 리소스에 동시 접근을 방지한다.
- SAGA 패턴을 통한 보상 트랜잭션 처리 : 각 단계의 메시지를 처리하는 동안 문제가 발생할 경우, SAGA Pattern을 활용해 이전 단계에서 수행된 작업을 취소하는 보상 트랜잭션을 수행해 데이터 일관성을 유지한다.
- 비동기 결과 확인 및 상태 관리 : kafka의 비동기 특성상 처리 결과를 즉시 확인할 수 없으니, 상태나 발행 메시지를 DB에 기록(Transactional OutBox Pattern)하거나 별도의 완료 메시지를 발행해 처리 상태를 추적해야 한다.
Saga Pattern은 분산 트랜잭션 환경에서 메시지 또는 이벤트를 주고받으며 서비스 간의 데이터 일관성을 지키기 위한 패턴이다. 이 패턴은 로컬 트랜잭션을 사용하며, 트랜잭션이 실패되면 변경된 내용을 취소하는(되돌리는) 보상 트랜잭션을 실행한다.
Saga Pattern을 구현하는 방법에는 보통 Orchestration Pattern과 Choreography Pattern 두 가지 방법이 존재한다.
Orchestration Pattern이란 Orchestrator라는 중앙 컨트롤러가 보상 작업을 트리거하는 방식이다.
이 오케스트레이터는 모든 트랜잭션을 처리하고 수행해야 하는 작업을 메시지를 보내 참여자들과 통신한다.
오케스트레이터는 작업의 상태를 저장 및 해석하고 있어서 분산 트랜잭션의 중앙 집중화가 이루어지고 데이터 일관성을 지킬 수 있다.

장점
- 참여자가 많거나 추가되는 상황 같이 복잡한 워크플로에 적합
- 활동 흐름의 제어 기능
- 오케스트레이터가 존재하여 순환 종속성이 발생되지 않음
- 각 참여자는 다른 참여자의 명령어를 알지 않아도 됨
단점
- 중앙에서 관리를 위한 복잡한 로직 구현 필요
- 모든 워크플로를 관리하기 때문에 실패 지점이 될 수 있음
Choreography Pattern이란 중앙 제어 없이 서비스끼리 이벤트로 통신하는 방법이다.
서비스들은 특정 동작을 수행하면 도메인 이벤트를 발행하고, 이를 구독하고 있던 서비스가 그에 따른 트랜잭션을 수행한다.
이벤트는 kafka와 같은 메시지 큐를 이용해 비동기로 전달한다.
- 트랜잭션이 실패한다면 보상 이벤트를 발행하여 롤백
- 메시지를 발행하는 동작도 트랜잭션에 포함되어야 함

장점
- 참여자가 적고 중앙 제어가 필요없는 경우 적합
- 추가 서비스 구현이 필요없음(구현이 간편하다)
- 역할이 분산되어 단일 실패 지점이 존재하지 않음
- 참여자는 서로 직접 알지 못하기 때문에 느슨한 결합
단점
- 명령 추적이 어렵기 때문에 워크플로 파악이 어려움
- Saga 참가자 간에 순환 종속성 발생 가능
- 통합테스트가 매우 어려움
Transactional Outbox Pattern이란 분산 시스템에서 데이터 일관성을 보장하기 위한 디자인 패턴이다.
보상 트랜잭션을 수행하는 SAGA패턴과 함께 쓰이기도 한다.
이 패턴은 하나의 서비스가 로컬 데이터베이스에 데이터를 변경하고, 그 변경 사항을 메시지브로커(kafka)로 안전하게 전파하기 위해 사용된다.
데이터 변경과 메시지 발행을 하나의 트랜잭션 안에서 수행함으로써 메시지의 손실이나 중복 전송을 방지한다.
동작 원리는 아래와 같다.
- 애플리케이션에서 비즈니스 이벤트(1. 주문 생성)가 발생하면 로컬 데이터베이스에 데이터 변경 및 Outbox Table에 기록한다. (이 두 작업은 하나의 트랜잭션으로 묶여 있다)
- 메시지 발행을 담당하는 별도의 프로세스가 주기적으로 Outbox Table을 조회하고, 아직 메시지 브로커로 전송되지 않은 이벤트를 가져와, kafka에 발행한다(2. 이벤트 게시).
- 메시지 발행에 성공하면, 해당 메시지는 특정 비즈니스 로직을 수행하고(3.상품 수량 감소) Outbox Table에서 제거하거나 처리완료로 표시한다.
- 메시지는 kafka를 통해 다른 마이크로서비스로 전송된다. 메시지를 수신한 서비스는 이를 처리하고 필요한 작업(4. 배송목록 추가)을 처리한다.

정리하자면 Kafka Messaging은 대규모 분산 시스템과 순서 보장이 필요한 환경에 적합하며, 확장성 및 고가용성이 뛰어나다. 그러나 비동기 처리의 복잡성과 지연 문제를 고려해야 한다.
동시성 제어 방법 총 정리 요약 표
Optimistic Lock | Pessimistic Lock | Redis Pub/Sub(Distributed Lock) | Kafka Messaging | |
장점 | 빠른 속도 | 데이터 일관성, 순서 보장 | 확장성, 분산 시스템에 적합, DB부하 낮음 | 확장성(Scale-out), 고가용성, 분산 시스템에 적합, DB부하 낮음 |
단점 | 재시도 로직으로 느려질 수 있음, 순서보장x | 교착상태(데드락)가능성 | 구현 복잡, 네트워크 지연 발생 가능성 | 구현 복잡, 운영 복잡, 메시지 지연 |
구현복잡도 | 중(재시도 로직) | 하(JPA 지원) | 상 | 상 |
성능 | 중 | 하 | 상 | 상 |
효율성 | 중 | 하 | 상 | 상 |
동시성 이슈 발생 가능성포인트에서 각각 동시성 제어 방안을 도입해보고 성능 분석
낙관적 락, 비관적 락, Redis pub/sub 을 각각 동시성이 터질 수 있는 유스케이스들에 도입해보고, 성능 분석을 해 보자.
(kafka messaging 동시성 제어는 높은 구현 난이도와 러닝커브로 이번 주차에는 성능 테스트가 힘들었다.)
Junit으로 테스트 시, CompletableFuture를 활용하여 비즈니스 로직의 동시 실행을 테스트하고, startTime과 endTime의 차이를 통해 실제 모든 비즈니스 로직이 완료되는 데 걸린 시간을 ms단위로 측정했다.
각 유스케이스 별 동시성 제어로 [낙관적 락] 도입 시 성능 테스트
구현 깃허브 브랜치 https://github.com/wn1331/Concert-Reservation-Service/tree/feature/STEP-11-OptimisticLock
GitHub - wn1331/Concert-Reservation-Service: 콘서트 예약 시스템 구현(대기열 시스템)
콘서트 예약 시스템 구현(대기열 시스템). Contribute to wn1331/Concert-Reservation-Service development by creating an account on GitHub.
github.com
평균 JUnit테스트 소요 시간(1000회의 동시 비동기 요청)
좌석 예약 3회 테스트 (재시도 X) : 124ms , 124ms, 106ms -> 평균 118ms
잔액 충전 3회 테스트 (재시도 X) : 257ms, 249ms, 268ms -> 평균 258ms
좌석 결제 3회 테스트 (재시도 O) : 412ms, 370ms, 405ms -> 평균 395ms
각 유스케이스 별 동시성 제어로 [비관적 락] 도입 시 성능 테스트
구현 깃허브 브랜치 https://github.com/wn1331/Concert-Reservation-Service/tree/feature/STEP-11-PessimisticLock
GitHub - wn1331/Concert-Reservation-Service: 콘서트 예약 시스템 구현(대기열 시스템)
콘서트 예약 시스템 구현(대기열 시스템). Contribute to wn1331/Concert-Reservation-Service development by creating an account on GitHub.
github.com
평균 JUnit테스트 소요 시간(1000회의 동시 비동기 요청)
좌석 예약 3회 테스트 : 227ms, 206ms, 205ms -> 평균 212ms
잔액 충전 3회 테스트 : 528ms, 555ms, 524ms -> 평균 535ms
좌석 결제 3회 테스트 : 264ms, 254ms, 264ms -> 평균 260ms
각 유스케이스 별 동시성 제어로 [Redis pub/sub 락] 도입 시 성능 테스트
구현 깃허브 브랜치 https://github.com/wn1331/Concert-Reservation-Service/tree/feature/STEP-11-RedisPubSub
GitHub - wn1331/Concert-Reservation-Service: 콘서트 예약 시스템 구현(대기열 시스템)
콘서트 예약 시스템 구현(대기열 시스템). Contribute to wn1331/Concert-Reservation-Service development by creating an account on GitHub.
github.com
평균 JUnit테스트 소요 시간(1000회의 동시 비동기 요청)
좌석 예약 3회 테스트 : 1546ms, 1683ms, 1528ms -> 평균 1585ms
잔액 충전 3회 테스트 : 2495ms, 2773ms, 2059ms -> 평균 2442ms
좌석 결제 3회 테스트 : 2354ms, 2442ms, 2216ms -> 평균 2337ms
비즈니스 유스케이스별 총 정리 및 요약표
낙관락 | 비관락 | redis pub/sub 락 | |
좌석 예약 평균 소모시간 | 118ms | 212ms | 1585ms |
잔액 충전 평균 소모시간 | 258ms | 535ms | 2422ms |
좌석 결제 평균 소모시간 | 395ms | 260ms | 2377ms |
nGrinder를 Docker로 세팅하고, 각각의 유스케이스에 위 3가지 락을 적용하여 아래와 같이 세팅 후 테스트를 적용해보았다.
VUser 수 : 400명
프로세스 수 : 10개
쓰레드 수 : 40개

하지만 좌석 예약 성능테스트 중, 위 성능 결과 그래프를 보면 처음 1회만 성공하고 그 후의 테스트들은 전부 실패하는 것을 볼 수 있다.
이는 비즈니스 로직상 당연한 결과로, 1회만 성공할 수 밖에 없는 좌석결제와 좌석예약의 경우에 nGrinder를 통한 성능 테스트는 무의미하다고 판단했다.
그리하여, 중복해서 성공할 수 있는 잔액 충전 유스케이스를 중점으로 낙관, 비관, pub/sub 성능 테스트를 수행하였다.
낙관적 락(재시도x)에서의 잔액 충전

위 그래프를 보면, 초기에는 성공과 실패를 빠르게 반복하다가, 도중에 테스트가 멈추어 버렸다. 이는 낙관적 락 충돌시 과도한 롤백으로 테스트가 중단되는것으로 예상이 된다.
낙관적 락(재시도o)에서의 잔액 충전

재시도 로직으로 인해 테스트 결과 중 성공 횟수가 매우 많다. 실패의 경우에는 재시도 설정값 중 재시도 횟수가 고갈될때까지 정상처리가 되지 않은 경우인 것이라 예상이 된다.
비관적 락에서의 잔액 충전

위 성능 테스트 그래프에서는, 에러가 10000건 이상 나타나는 것을 볼 수 있는데 이는 굉장히 많은 트랜잭션이 대기상태에 있어 이런 현상이 나타나게 되는 것이라 예상이 된다. 그리고 타임아웃을 걸어놓았기 때문에 아래로 쭉 꺼진 것 같다.
PUB/SUB 락에서의 잔액 충전

위 성능 테스트 그래프에서는, 처리 속도는 생각보다 느리지만 정상적으로 모든 요청이 성공적으로 수행된 결과를 볼 수 있다.
내가 선택한 동시성 방안
여러 동시성 방안들에 대해 공부해 본 결과 각 유스케이스 별로 구현해야 할 동시성 방안들에 대한 나의 판단은 아래와 같다.
좌석 예약 : 다른 동시성 대응 방안에 비해 확장성을 고려하고, DB의 부하를 최소한으로 줄일 수 있으며, kafka보다 러닝커브가 낮고 구현 난이도가 낮은 동시성 대응 방안으로 Redis pub/sub 락이 적합하다 판단된다.
잔액 충전 : 유저 간 잔액 충전이 가능한 상황이 아니며, 실수로 엄청 빠른 속도로 따닥 클릭했을 경우, 두 번째 요청은 실패로 처리해야 하는 상황(재시도 로직이 필요 없는)을 가정하면서 높은 성능을 가져갈 수 있는 동시성 대응 방안인 낙관적 락이 적합하다 판단된다.
좌석 결제 : 잔액 차감의 경우, 따닥 클릭했을 경우에는 멱등성으로 처리하여 Early return을 하고, 클라이언트가 여러 창을 띄워놓고 동시에 좌석 결제를 수행했을 때 모든 창이 정상적인 요청이라면 전부 성공해야 한다는 케이스를 고려한다면 비관적 락이 가장 적합할 것이라 판단된다.
JPA를 사용하고 있는데
유저 테이블에는 point라는 잔액 칼럼이 있다.
잔액을 충전하는 API 에는 비관적 잠금을,
잔액을 결제하는 API 에는 재시도 로직이 없는 낙관적 잠금을 걸었다고 가정하는데 (내가 선택한 동시성 방안에는 비관적 락(결제)에서 무조건 1번만 성공하기 때문에 예시가 적절하지 않다. 한번 결제 성공한건 또 성공할 수 없음)
그렇게 되면 충전 시 유저 조회를 하는 쿼리에는 비관적 배타락이 걸려있고, 유저 Entity에는 @Version(낙관락)이 있다.

- 유저가 충전 API를 (심각하게 빠른속도로) 여러번 따다다다다닥 호출한다.
- 첫 번째 트랜잭션이 열린다. 두번째 트랜잭션도 열리긴 했지만 유저 조회 직전에 계속 대기(비관락)한다. 세번째도 대기… 네번째도….
- 첫 번째 트랜잭션에서 조회한 유저의 버전은 0.
- 첫 번째 트랜잭션에서 정상적으로 로직(잔액차감)을 수행하고 커밋할때 버전 검사(낙관락)를 한다.
- 현재 버전이 0이고 db의 버전이 0이라 첫 번째 트랜잭션은 무사 통과. 버전을 하나 올린다.
- 대기타던 두 번째 트랜잭션이 잔액조회를한다.
- 두 번째 트랜잭션에서 조회한 유저의 버전은 1.
- 로직을 수행하고 커밋할 때 버전 검사를 한다.
- 현재 버전이 1이고 db의 버전이 1이라 두번째 트랜잭션도 무사 통과. 버전을 하나 올린다.
- 계속반복
결론 : 이전 트랜잭션이 끝나야 다음 트랜잭션에서 유저조회를 수행할 수 있기 때문에 낙관락 버전체크에서 터지는 경우는 없을 것 같다.
결론
이번 콘서트 예약 서비스에서 동시성 제어를 위해 낙관적 락, 비관적 락, Redis pub/sub, kafka Messaging 방식을 분석하고 성능 테스트를 수행했다.
각 유스케이스에 적합한 동시성 대응 방안을 선택하는 것이 중요했는데, 좌석 예약에는 확장성과 DB 부하 감소를 고려해 Redis pub/sub 락을, 잔액 충전에는 높은 성능을 위해 낙관적 락을, 좌석 결제에는 비관적 락을 적용했다.
성능 테스트를 통해 각 락의 특성과 트레이드오프를 작게나마 체감할 수 있었으며, 상황별로 적절한 선택이 중요하다는 생각이 들었다.
이번 보고서을 통해 동시성 제어에 대해 많은 공부를 하게 되어, 실무에도 적용할 수 있는 기회가 있었으면 하는 바램이 있다.
'교육 | 외부활동 > 항해99 플러스 백엔드' 카테고리의 다른 글
Index를 통한 쿼리 성능개선 - 8주차 (1) | 2024.11.12 |
---|---|
캐시 및 Redis 대기열 이관 - 7주차 WIL (0) | 2024.11.10 |
항해99 플러스 백엔드 Chapter2(3,4,5주차) 회고 및 WIL (8) | 2024.10.25 |
항해99 플러스 백엔드 2주차 WIL - 클린아키텍처와 동시성 (0) | 2024.10.04 |
항해99 플러스 백엔드 1주차 WIL - TDD 와 동시성 (0) | 2024.09.28 |