데이터베이스의 동시성 문제 해결 방안

dddingzong·2026년 4월 20일

이론

목록 보기
14/14

0. 들어가며

서비스를 개발하다 보면 하나의 데이터에 여러 사용자가 동시에 접근하는 상황을 자주 마주하게 됩니다. 대표적으로는 다음과 같은 경우가 있습니다.

  • 여러 사용자가 동시에 같은 상품의 재고를 차감하는 경우
  • 같은 좌석을 여러 사용자가 거의 동시에 예약하는 경우
  • 하나의 계좌 잔액을 동시에 조회하고 수정하는 경우

이러한 상황에서 데이터베이스가 단순히 요청을 순서대로만 처리한다면 큰 문제가 없어 보일 수 있습니다. 그러나 실제 서비스 환경에서는 수많은 요청이 동시에 들어오고, 각각의 트랜잭션이 서로의 처리 과정에 영향을 줄 수 있습니다. 이때 적절한 제어가 없다면 데이터 정합성이 깨지고, 사용자는 잘못된 결과를 보게 됩니다.

즉, 데이터베이스는 데이터를 저장하는 역할만 수행하는 것이 아니라, 동시에 실행되는 여러 작업 사이에서 일관성을 유지하는 역할도 함께 수행해야 합니다.

이 글에서는 데이터베이스의 동시성 문제가 왜 발생하는지, 어떤 유형이 있는지, 그리고 이를 제어하기 위한 대표적인 기법에는 무엇이 있는지 정리하고자 합니다.


1. 동시성 문제란 무엇인가

여러 트랜잭션이 동시에 실행되면서 서로의 작업 결과에 영향을 주어 예상하지 못한 결과가 발생하는 문제

트랜잭션은 데이터베이스에서 하나의 논리적 작업 단위를 의미합니다. 예를 들어 계좌 이체는 잔액 조회, 출금, 입금과 같은 여러 쿼리로 구성되더라도 하나의 작업으로 취급되어야 합니다. 이처럼 트랜잭션은 보통 모두 성공하거나 모두 실패해야 하는 단위로 다뤄집니다.

문제는 여러 트랜잭션이 동시에 실행될 때 발생합니다. 하나의 트랜잭션이 작업을 끝내기 전에 다른 트랜잭션이 같은 데이터를 읽거나 수정하면, 각 트랜잭션이 의도한 결과와 실제 데이터베이스의 결과가 달라질 수 있습니다. 결국 동시성 문제는 여러 트랜잭션의 실행 순서가 뒤섞이면서 데이터의 정합성이 깨지는 현상입니다.


2. 대표적인 동시성 문제

2.1 Dirty Read

Dirty Read는 한 트랜잭션이 아직 커밋하지 않은 데이터를 다른 트랜잭션이 읽는 문제입니다.

예를 들어 트랜잭션 A가 상품 재고를 10개에서 5개로 수정했지만 아직 커밋하지 않은 상태라고 가정하겠습니다. 이때 트랜잭션 B가 해당 값을 읽어 재고가 5개라고 판단할 수 있습니다. 그런데 이후 트랜잭션 A가 롤백되면 실제 재고는 다시 10개가 됩니다. 결국 트랜잭션 B는 실제로 확정되지 않은 값을 기준으로 동작한 셈입니다.

이는 서비스 로직에 큰 혼선을 주며, 잘못된 판단을 기반으로 후속 처리가 일어날 수 있다는 점에서 위험합니다.

2.2 Non-repeatable Read

Non-repeatable Read는 하나의 트랜잭션이 같은 데이터를 두 번 읽었을 때 결과가 달라지는 문제입니다.

예를 들어 트랜잭션 A가 어떤 상품의 가격을 조회했을 때 10,000원이었는데, 그 사이 트랜잭션 B가 가격을 12,000원으로 수정하고 커밋했다고 가정하겠습니다. 이후 트랜잭션 A가 같은 데이터를 다시 읽으면 값이 12,000원으로 바뀌어 있습니다.

같은 트랜잭션 안에서 같은 행을 읽었음에도 결과가 달라지는 것은, 일관된 작업 흐름을 기대한 애플리케이션 입장에서 문제가 될 수 있습니다.

2.3 Phantom Read

Phantom Read는 같은 조건으로 조회했을 때 조회되는 행의 개수가 달라지는 문제입니다.

예를 들어 트랜잭션 A가 WHERE status = 'READY' 조건으로 주문 목록을 조회했는데 5건이 나왔다고 가정하겠습니다. 이후 트랜잭션 B가 같은 조건을 만족하는 새 주문을 추가하고 커밋하면, 트랜잭션 A가 같은 조건으로 다시 조회했을 때는 6건이 조회될 수 있습니다.

즉, 특정 행의 값이 바뀐 것이 아니라 조회 대상 집합 자체가 달라지는 현상이기 때문에 Phantom Read라고 부릅니다.

2.4 Lost Update

Lost Update는 둘 이상의 트랜잭션이 같은 데이터를 읽고 각각 수정한 뒤 저장하는 과정에서, 먼저 수행된 수정 결과가 나중의 수정에 의해 덮어씌워져 사라지는 현상입니다.

예를 들어 어떤 계좌의 잔액이 100,000원이라고 하겠습니다.

  1. 트랜잭션 A가 잔액 100,000원을 읽고 10,000원을 출금하려고 합니다.
  2. 동시에 트랜잭션 B도 잔액 100,000원을 읽고 20,000원을 출금하려고 합니다.
  3. 트랜잭션 A가 90,000원을 저장합니다.
  4. 이후 트랜잭션 B가 80,000원을 저장합니다.

겉보기에는 문제가 없어 보일 수 있지만, 실제로는 두 트랜잭션이 독립적으로 처리되면서 A의 변경이 충분히 반영되지 않았을 수 있습니다. 경우에 따라 최종 값이 의도한 결과와 달라질 수 있으며, 더 복잡한 비즈니스 로직에서는 심각한 정합성 오류로 이어질 수 있습니다.

Lost Update는 재고 차감, 예약 시스템, 포인트 사용, 계좌 이체와 같이 동시 수정이 자주 발생하는 기능에서 특히 치명적입니다.


3. 동시성 제어가 필요한 이유

여러 트랜잭션이 동시에 실행되더라도, 마치 순차적으로 실행된 것과 유사한 결과를 얻도록 만드는 것입니다.

이를 위해 데이터베이스는 다음과 같은 목표를 달성해야 합니다.

  • 잘못된 중간 상태를 다른 트랜잭션이 읽지 못하도록 해야 한다.
  • 동시에 같은 데이터를 수정할 때 충돌을 제어해야 한다.
  • 가능한 한 많은 요청을 병렬로 처리하면서도 정합성을 유지해야 한다.

결국 동시성 제어는 정합성을 지키기 위한 안전장치이면서, 동시에 성능과 처리량을 고려해야 하는 설계 문제이기도 합니다.


4. 대표적인 동시성 제어 기법

4.1 Lock 기반 제어

가장 전통적인 방식은 락을 사용하는 것입니다. 락은 특정 데이터에 대해 다른 트랜잭션의 접근을 제한하는 장치입니다.

  • 공유 락(Shared Lock): 읽기는 허용하지만 쓰기는 제한합니다.
  • 배타 락(Exclusive Lock): 읽기와 쓰기 모두를 제한하고, 해당 트랜잭션만 수정할 수 있게 합니다.

예를 들어 어떤 트랜잭션이 특정 행에 배타 락을 획득하면, 다른 트랜잭션은 그 행을 수정할 수 없게 됩니다. 이를 통해 Lost Update와 같은 문제를 방지할 수 있습니다.

락 기반 제어의 장점은 구조가 직관적이고 충돌 방지에 효과적이라는 점입니다. 반면 단점도 분명합니다.

  • 락 대기로 인해 응답 시간이 길어질 수 있습니다.
  • 동시에 처리할 수 있는 요청 수가 줄어들 수 있습니다.
  • 잘못 설계하면 데드락이 발생할 수 있습니다.

따라서 락은 매우 강력한 도구이지만, 사용 범위와 지속 시간을 신중히 설계해야 합니다.

4.2 비관적 락과 낙관적 락

락을 활용하는 방식은 크게 비관적 락과 낙관적 락으로 나눌 수 있습니다.

비관적 락

비관적 락은 충돌이 자주 발생할 것이라고 가정하고, 데이터를 읽거나 수정하는 시점부터 미리 락을 걸어 충돌을 차단하는 방식입니다.

대표적으로는 SELECT ... FOR UPDATE와 같은 방식이 사용됩니다. 이 방식은 재고 차감, 좌석 예약처럼 충돌이 발생했을 때 비용이 큰 경우에 적합합니다.

다만 락 점유 시간이 길어지면 병목이 생기기 쉽고, 처리량이 감소할 수 있습니다.

낙관적 락

낙관적 락은 충돌이 자주 발생하지 않을 것이라고 가정하고, 실제 수정 시점에만 충돌 여부를 검사하는 방식입니다. 보통 버전(version) 컬럼이나 타임스탬프를 함께 저장해 두고, 수정 시점에 이전 값과 일치하는지 확인합니다.

예를 들어 다음과 같은 방식으로 동작할 수 있습니다.

  1. 트랜잭션 A가 버전 3인 데이터를 읽습니다.
  2. 수정 시 WHERE id = 1 AND version = 3 조건으로 업데이트를 시도합니다.
  3. 업데이트가 성공하면 버전을 4로 증가시킵니다.
  4. 이미 다른 트랜잭션이 먼저 수정하여 버전이 바뀌었다면, 현재 트랜잭션의 수정은 실패합니다.

낙관적 락은 락 점유 시간이 거의 없기 때문에 성능상 유리합니다. 반면 충돌이 잦은 환경에서는 재시도가 반복되어 오히려 비효율적일 수 있습니다.

4.3 MVCC

MVCC(Multi-Version Concurrency Control)는 많은 현대 DBMS에서 사용하는 대표적인 동시성 제어 방식입니다. 이름 그대로 데이터의 여러 버전을 관리하여, 읽기와 쓰기 작업이 서로 직접 충돌하지 않도록 돕습니다.

예를 들어 어떤 트랜잭션이 데이터를 수정하더라도, 동시에 다른 트랜잭션은 기존 버전을 읽을 수 있습니다. 이를 통해 읽기 작업이 쓰기 작업 때문에 과도하게 대기하지 않게 됩니다.

MVCC의 장점은 다음과 같습니다.

  • 읽기 성능이 우수합니다.
  • 읽기와 쓰기의 충돌을 줄일 수 있습니다.
  • 대량의 조회가 많은 서비스에서 유리합니다.

반면 주의할 점도 있습니다.

  • 버전 관리를 위해 추가적인 저장 공간과 관리 비용이 필요합니다.
  • 특정 상황에서는 여전히 업데이트 충돌을 별도로 처리해야 합니다.
  • 개념이 락 기반 제어보다 직관적이지 않아 처음 이해하기 어렵습니다.

5. 트랜잭션 격리 수준과 동시성 문제

동시성 문제를 제어하는 또 하나의 중요한 축은 트랜잭션 격리 수준입니다. 격리 수준은 동시에 실행되는 트랜잭션 간에 어느 정도까지 서로의 작업을 볼 수 있게 허용할지를 결정합니다.

일반적으로 SQL 표준에서는 네 가지 격리 수준을 정의합니다.

격리 수준Dirty ReadNon-repeatable ReadPhantom Read
Read Uncommitted발생 가능발생 가능발생 가능
Read Committed방지발생 가능발생 가능
Repeatable Read방지방지발생 가능 가능성 존재
Serializable방지방지방지

5.1 Read Uncommitted

가장 낮은 수준입니다. 커밋되지 않은 변경도 읽을 수 있기 때문에 Dirty Read가 발생할 수 있습니다. 정합성이 중요한 서비스에서는 거의 사용되지 않습니다.

5.2 Read Committed

커밋된 데이터만 읽을 수 있도록 하여 Dirty Read를 방지합니다. 다만 같은 트랜잭션 안에서 다시 조회했을 때 값이 달라질 수 있으므로 Non-repeatable Read는 여전히 발생할 수 있습니다.

실무에서 기본 격리 수준으로 많이 사용되며, 성능과 정합성 사이에서 균형을 맞추는 선택으로 볼 수 있습니다.

5.3 Repeatable Read

같은 트랜잭션 안에서 동일한 행을 반복해서 읽어도 같은 결과를 보장하려는 수준입니다. 따라서 Non-repeatable Read를 방지할 수 있습니다. 다만 조회 범위 전체에 대해서는 Phantom Read를 어떻게 처리하는지가 DBMS 구현 방식에 따라 달라질 수 있습니다.

5.4 Serializable

가장 높은 수준입니다. 여러 트랜잭션이 동시에 실행되더라도 결과적으로는 순차 실행과 동일한 결과를 보장하도록 제어합니다. 정합성 측면에서는 가장 안전하지만, 그만큼 락 경합과 대기가 증가하여 성능 저하가 발생할 수 있습니다.

즉, 격리 수준은 높을수록 더 안전하지만, 동시에 처리할 수 있는 양은 줄어들 가능성이 큽니다.


6. 실무에서는 무엇을 선택해야 하는가

실무에서는 단순히 가장 높은 격리 수준이나 가장 강한 락을 사용하는 것이 정답이 아닙니다. 중요한 것은 비즈니스 요구에 맞는 수준의 정합성과 성능을 함께 고려하는 것입니다.

예를 들어 조회가 매우 많은 서비스에서는 MVCC 기반의 읽기 성능이 큰 장점이 됩니다. 반면 재고 차감, 좌석 예약, 계좌 이체처럼 잘못된 한 번의 갱신이 큰 문제로 이어지는 기능에서는 더 강한 제어가 필요합니다.

다음과 같은 기준으로 접근할 수 있습니다.

6.1 충돌 비용이 큰 기능

  • 재고 차감
  • 예약 시스템
  • 결제 처리
  • 계좌 이체

이러한 기능은 잘못된 한 번의 업데이트가 직접적인 손실로 이어질 수 있으므로, 비관적 락이나 높은 수준의 격리, 혹은 명시적인 충돌 제어가 필요할 수 있습니다.

6.2 조회 비중이 큰 기능

  • 게시글 목록 조회
  • 통계 화면
  • 대시보드
  • 검색 결과 조회

이러한 기능은 쓰기보다 읽기가 훨씬 많으므로, MVCC와 적절한 격리 수준을 통해 성능을 확보하는 것이 중요합니다.

6.3 충돌 가능성은 낮지만 정합성은 필요한 기능

  • 사용자 프로필 수정
  • 설정 정보 변경
  • 관리자 화면 일부 수정 기능

이 경우에는 낙관적 락이 적합할 수 있습니다. 평소에는 성능 부담을 줄이면서, 충돌이 발생한 경우에만 재시도 또는 사용자 안내를 통해 처리할 수 있기 때문입니다.


7. 한 줄 요약

데이터베이스의 동시성 제어는 여러 트랜잭션이 동시에 실행되는 환경에서 데이터 정합성을 지키기 위한 장치이며, 실무에서는 일관성과 성능 사이의 균형을 고려해 락, MVCC, 격리 수준 등을 선택해야 합니다.

profile
공부 노트 & 회고

0개의 댓글