Engineering Note

[SW Engineering]동시성 제어를 위한 Lock 비교 본문

SW Engineering/선착순 이벤트

[SW Engineering]동시성 제어를 위한 Lock 비교

Software Engineer Kim 2026. 2. 4. 17:16

**조회 시점에 실제로 데이터베이스 수준의 락을 거는 방식은 '비관적 락(Pessimistic Lock)'**입니다. 낙관적 락은 그 이름과 달리 실제로는 '락'을 걸지 않는다는 점이 가장 큰 차이점이에요.
혼동하기 쉬운 두 개념을 명확하게 정리해 드릴게요.


1. 비관적 락 (Pessimistic Lock)


"데이터 수정이 빈번할 거야. 충돌이 날 게 뻔하니 미리 문을 잠가두자!"
* 작동 방식: 데이터를 조회할 때 SELECT ... FOR UPDATE 같은 구문을 사용하여 데이터베이스 로우(Row)에 직접 락을 겁니다.
* 특징: 다른 트랜잭션은 해당 락이 풀릴 때까지 대기해야 합니다.
* 장점: 데이터 무결성을 강력하게 보장하며, 충돌이 잦은 환경에서 재시도 로직이 필요 없습니다.
* 단점: 성능(동시성)이 저하될 수 있고, 자칫하면 데드락(Deadlock)에 빠질 위험이 있습니다.


2. 낙관적 락 (Optimistic Lock)


"충돌이 자주 안 날 거야. 일단 그냥 두고, 나중에 저장할 때 확인만 하자!"
* 작동 방식: 조회 시점에 락을 걸지 않습니다. 대신 버전(Version) 번호나 타임스탬프를 활용합니다.
   * 데이터를 읽을 때 버전 번호(예: 1)도 함께 읽습니다.
   * 수정 후 업데이트할 때 WHERE version = 1 조건을 붙여 쿼리를 날립니다.
   * 그사이 다른 사용자가 업데이트해서 버전이 2가 되었다면, 내 쿼리는 실패하게 됩니다.
* 특징: DB 레벨의 락이 아니라 애플리케이션 레벨에서 처리하는 논리적 격리 방식입니다.
* 장점: 실제로 락을 잡지 않으므로 비관적 락보다 성능상 이점이 큽니다.
* 단점: 충돌이 발생하면 개발자가 직접 예외 처리를 하거나 재시도 로직을 구현해야 합니다.
핵심 비교 요약
| 구분 | 비관적 락 (Pessimistic) | 낙관적 락 (Optimistic) |
|---|---|---|
| 락 시점 | 데이터 조회 시 즉시 | 업데이트(Commit) 시점에 확인 |
| 자원 점유 | 트랜잭션 동안 점유 (DB 락) | 점유하지 않음 |
| 충돌 대응 | 발생 전 예방 (대기 발생) | 발생 후 처리 (예외 발생) |
| 적합한 상황 | 충돌이 잦고 데이터 정합성이 최우선일 때 | 충돌이 적고 성능이 중요할 때 |
따라서 질문하신 **"조회 시점에 락을 걸어 충돌을 방지하는 것"**은 비관적 락의 설명이며, 낙관적 락은 조회 시점에는 누구나 접근 가능하게 열어두되 수정 시점에 버전 일치 여부를 따지는 방식이라고 이해하시면 완벽합니다.
혹시 지금 개발 중인 서비스에서 어떤 락을 적용할지 고민 중이신가요? 상황을 말씀해 주시면 적절한 방식을 추천해 드릴 수 있습니다.



선착순 주문 이벤트라면 재고 정합성이 생명이죠! 수량이 한정되어 있고 짧은 시간에 트래픽이 몰리기 때문에, 락의 선택이 서비스의 생사(?)를 결정할 수도 있습니다.
앞서 살펴본 두 가지 방식 중 선착순 이벤트에는 보통 어떤 것이 유리할까요?


1. 선착순 이벤트에서 '낙관적 락'의 한계


낙관적 락은 충돌이 발생하면 애플리케이션에서 예외를 던지고 재시도를 해야 합니다.
* 상황: 재고가 100개인데 1,000명이 동시에 주문 버튼을 누름.
* 결과: 1명만 성공하고 나머지 999명은 버전이 맞지 않아 오류가 발생합니다. 이들이 다시 재시도를 하면 또 충돌이 발생하죠.
* 결론: 트래픽이 몰리는 선착순 이벤트에서는 DB 부하만 가중시키고 사용자 경험은 최악이 될 수 있습니다.


2. 선착순 이벤트에서 '비관적 락'의 적용


비관적 락은 순례자가 줄을 서듯 데이터베이스 로우에 락을 획득할 때까지 대기시킵니다.
* 장점: 재고 수량이 딱 떨어지는 것을 보장하기 가장 쉽습니다.
* 단점: 대기열이 길어지면 DB 커넥션이 부족해져 시스템 전체가 느려질 수 있습니다.
🚀 실무에서 권장하는 '선착순' 구현 전략
사실 데이터베이스 락만으로는 수만 명의 동시 접속을 버티기 어렵습니다. 그래서 실무에서는 보통 다음과 같은 단계로 발전시킵니다.
* Level 1: 비관적 락 (Pessimistic Lock)
   * 트래픽이 아주 많지 않을 때 가장 확실하고 단순한 방법입니다. SELECT ... FOR UPDATE로 재고를 하나씩 차감합니다.
* Level 2: Redis를 활용한 분산 락 (Distributed Lock)
   * DB까지 요청이 가기 전에 Redis 같은 고성능 저장소에서 락을 획득하게 합니다. DB 부하를 획기적으로 줄일 수 있습니다. (Redisson 라이브러리 등이 주로 사용됩니다.)
* Level 3: Redis 원자적 연산 (Incr/Decr) + 메시지 큐
   * 락 자체를 걸지 않고 Redis의 decr 명령어로 재고를 먼저 깎은 뒤, 성공한 요청만 Kafka나 RabbitMQ에 담아 비동기로 DB에 저장합니다. 가장 고성능을 내는 방식입니다.

 

비관적락 분산락 낙관적락 비교

 
비관적락과 Redis의 분산락은 기본적으로 대기열 큐를 사용하여 동작합니다. 한 번에 하나의 트랜잭션(쓰레드)만 데이터를 읽을 수 있는데, 큐에 대기중인 트랜잭션들은 락이 해제되면 차례대로 데이터를 조회하고 쓸 수 있습니다. 반면 낙관적락은 큐잉 모델을 따르지 않고 실질적으로 락도 사용하지 않는다. 동시 요청이 오게 되면 여러 트랜잭션들이 자유롭게 데이터를 읽어가고, 애플리케이션 로직을 거친후 쓰기시점에 처음 읽어간 버전과 다르면, 재시도 로직을 구현하거나 실패처리를 통해 동시성 문제를 해결합니다.
선착순 이벤트에서 데이터 정합성을 위해 재시도 로직이 추가 되는 낙관적락은 일반적인 동시성 문제 해결에서 낙관적락을 사용하는 때와 달리 성능이 저하 될 수 있습니다.



최초 구현 방법
낙관적락 비관적락 비교

재고 정합성이 깨질 경우 비즈니스 손실이 크다고 판단했습니다. 낙관적 락은 충돌 시 재시도 비용이 발생해 초고밀도 트래픽에선 성능 저하가 우려되었고, 따라서 데이터 수준에서 원자성을 보장하는 비관적 락을 선택해 안정성을 확보했습니다


최종
비관적락에서 Redis 분산락으로 마이그레이션

단일 DB 락은 커넥션 점유 문제로 인해 전체 시스템 병목이 될 수 있다는 점을 경계했습니다. 시스템의 확장성(Scalability)을 고려했을 때, 애플리케이션 외부에서 락을 관리하는 분산 락 방식이 DB 부하를 줄이면서도 동시성을 제어할 수 있는 최적의 아키텍처라고 판단했습니다.

 

 

 

동시성 제어를 위해 여러가지 락의 트레이드 오프를 고려하며 알게된 점

낙관적 락이 일반적으로 성능상 유리해서 선택한다고 하지만, 선착순 이벤트처럼 특별한 비즈니스 상황에서는 오히려 성능이 저하될 수 있습니다. 사실상 낙관적 락이 성능 상 유리한 조건은 동시에 트래픽이 몰리지 않는 상황에서만 성능이 유리하다. 이유는 실질적으로 락을 걸지 않기 때문이다. 

그런데 선착순 이벤트 상황에서는 충돌시 실패처리를 하면 사용자에게 다시 시도하라는 메세지를 보내고 실제 사용자가 다시 시도해야하는 상황으로 UX가 저하 될 수 있고, 시스템상에서 재시도로직을 추가하면 오히려 CPU 점유시간이 길어지고, DB에 다시 SELECT 쿼리를 날려야 하므로 커넥션이 낭비되고, 불필요한 네트워크 통신으로 인한 네트워크 부하가 발생합니다.

이렇게 CS 관점에서 트레이드 오프를 고려하고 적절한 기술을 선택할 수 있는 능력이 Software Engineer에게 필요한 능력이라고 생각합니다.


 

 

Comments