서버는 사람의 요청이든 시스템의 호출이든 동시에 여러 작업을 처리한다. 각 요청은 보통 하나의 스레드를 할당받아 실행되며, 이 과정에서 여러 스레드가 하나의 자원을 공유하게 된다. 공유 자원을 동시에 읽고 수정하는 순간, 우리가 예상하지 못한 결과가 발생할 수 있는데, 이를 동시성 문제라고 부른다.
대표적인 질문은 다음과 같다. “재고가 100개일 때 100명이 동시에 요청하면, 재고는 반드시 0이 되는가?” 단일 스레드 환경에서는 당연한 결과지만, 멀티 스레드 환경에서는 전혀 그렇지 않다. 이 글에서는 동시성 문제가 왜 발생하는지, 그리고 어떤 방식으로 해결할 수 있는지를 비관적 락, 낙관적 락, 단일 스레드 처리 관점에서 정리한다.
동시성 문제 발생 원인과 Race Condition
동시성 문제의 핵심 원인은 “같은 데이터를 여러 스레드가 동시에 수정한다”는 점이다. 대표적인 예가 재고 차감이다. 각 스레드는 재고를 조회하고, 조건을 검사한 뒤, 재고를 감소시키고 결과를 저장한다. 문제는 이 과정이 하나의 원자적 연산이 아니라는 점이다.
두 개의 스레드가 거의 동시에 재고를 조회하면 둘 다 동일한 값을 읽게 된다. 이후 각각 재고를 1 감소시켜 저장하면, 결과적으로 한 번의 감소만 반영된다. 이처럼 실행 순서에 따라 결과가 달라지는 상황을 레이스 컨디션(Race Condition)이라고 한다.
레이스 컨디션은 테스트 환경에서는 잘 드러나지 않다가 트래픽이 증가하거나 서버 인스턴스가 늘어나면 갑자기 발생하는 경우가 많다. 그래서 단순한 버그가 아니라 설계 단계에서 반드시 고려해야 하는 문제다.
비관적 락으로 동시성 문제 해결
비관적 락은 “충돌이 반드시 발생할 것이다”라는 가정에서 출발한다. 따라서 데이터를 수정하기 전에 먼저 잠금을 획득하고, 다른 스레드의 접근을 차단한다. 이 방식은 애플리케이션 수준과 데이터베이스 수준에서 모두 사용할 수 있다.
애플리케이션 수준 비관적 락은 synchronized나 ReentrantLock을 사용한다. 단일 JVM, 단일 인스턴스 환경에서는 비교적 쉽게 동시성 문제를 해결할 수 있다. 하지만 서버가 여러 대로 분산되면 각 인스턴스가 서로 다른 락을 가지게 되어 효과를 잃는다.
데이터베이스 수준 비관적 락은 SELECT FOR UPDATE 같은 방식으로 특정 레코드에 락을 걸어 직렬화한다. 이 방법은 분산 환경에서도 유효하지만, 락을 기다리는 트랜잭션이 늘어나면 처리량이 급격히 감소한다. 또한 여러 자원을 동시에 잠글 경우 데드락 위험도 함께 고려해야 한다.
낙관적 락으로 동시성 문제 해결
낙관적 락은 “대부분의 요청은 충돌하지 않는다”는 가정에 기반한다. 명시적인 락을 사용하지 않고, 데이터를 수정할 때 조건을 함께 비교하여 충돌 여부를 판단한다. 대표적인 방식이 CAS(Compare-And-Set)이다.
낙관적 락은 보통 version 컬럼을 활용한다. 업데이트 시점에 현재 버전이 조회 시점과 동일한 경우에만 수정이 성공하도록 한다. 이 비교는 반드시 한 번의 SQL로 수행되어야 하며, 조건이 맞지 않으면 영향받은 행 수가 0으로 반환된다.
이 방식의 장점은 대기 시간이 없다는 점이다. 실패한 요청은 즉시 응답받고, 재시도 여부를 클라이언트나 서비스 정책으로 유연하게 처리할 수 있다. 분산 환경에서도 추가 인프라 없이 적용 가능해 실무에서 많이 사용된다.
단일 스레드 방식으로 동시성 문제 해결
동시성 문제를 근본적으로 줄이는 방법은 “동시에 실행하지 않게 만드는 것”이다. 단일 스레드 이벤트 루프 방식은 특정 작업을 하나의 스레드에서 순차적으로 처리한다. Redis나 메시지 큐 기반 구조가 대표적이다.
이 방식에서는 각 요청이 바로 자원을 수정하지 않고, 큐에 적재된 뒤 워커가 하나씩 처리한다. 충돌 자체가 발생하지 않기 때문에 락이나 CAS 로직이 단순해진다. 다만 메시지 유실, 멱등 처리, 재처리 전략은 반드시 고려해야 한다.
단일 스레드 방식은 모든 작업을 직렬화하는 것이 아니라 키 단위로 분산 처리하는 것이 핵심이다. 파티션을 늘리면 처리량도 함께 확장할 수 있다. 대규모 트래픽 환경에서 안정성을 우선시할 때 효과적인 선택이 된다.
동시성 문제는 특정 기술의 문제가 아니라 시스템 설계의 문제다. 비관적 락, 낙관적 락, 단일 스레드 방식은 각각 장단점이 명확하다. 중요한 것은 현재 트래픽 규모, 분산 여부, 사용자 경험 요구사항에 맞는 해결 전략을 선택하는 것이다.