동시성 제어, 왜 필요한가? 로킹·2PL·MVCC의 트레이드오프 정리

2026-09-01 · 테크 · 읽는데 7분

동시성 제어, 왜 필요한가? 로킹·2PL·MVCC의 트레이드오프 정리

동시성 제어는 여러 트랜잭션이 동시에 같은 데이터에 접근할 때 데이터베이스의 일관성과 무결성을 지키기 위해 트랜잭션의 실행 순서를 직렬화하는 기법입니다. DBMS가 다중 사용자 환경에서 동시에 발생하는 요청을 처리할 때, 이를 제어하지 않으면 잘못된 값이 저장되거나 데이터가 손상될 수 있습니다. 대표적인 해결 방법으로는 로킹(Locking), 2단계 로킹 규약(2PL), MVCC(다중 버전 동시성 제어)가 있으며, 각 방식은 트레이드오프가 있어 상황에 맞게 선택해야 합니다.

동시성 제어가 필요한 이유, 데이터 정합성 문제

동시성 제어를 이해하기 위해서는 먼저 트랜잭션의 개념을 짚고 넘어가야 합니다. 트랜잭션은 데이터베이스에서 하나의 논리적 작업 단위로, 여러 개의 쿼리가 하나의 묶음으로 처리됩니다. 온라인 쇼핑몰에서 수백 명이 동시에 결제를 하거나, 은행에서 동시에 계좌이체가 발생하는 상황을 가정해 보겠습니다. 이때 별도의 제어 장치가 없다면 두 트랜잭션이 같은 데이터를 읽고 수정하면서 데이터 정합성이 깨질 수 있습니다.

예를 들어 재고가 10개인 상품이 있을 때, 두 사용자가 동시에 10개씩 주문하면 재고는 0개가 되어야 하지만, 동시성 제어가 없다면 두 트랜잭션 모두 재고를 10으로 읽고 0으로 업데이트하지 못할 수 있습니다. 이런 문제를 방지하기 위해 DBMS는 동시성 제어를 통해 트랜잭션들이 마치 순차적으로 실행된 것과 같은 결과를 보장합니다. 동시성과 일관성은 반비례 관계에 있어서, 동시 접근을 많이 허용할수록 일관성은 낮아지고, 일관성을 높이면 동시성이 떨어지는 구조입니다.

Lock 기반 동시성 제어, 블로킹과 교착 상태 이해하기

Lock은 트랜잭션이 데이터 항목에 대해 독점적인 사용 권한을 설정하는 방식입니다. Lock을 설정한 트랜잭션이 해제(Unlock)할 때까지 다른 트랜잭션은 해당 데이터에 접근할 수 없습니다. Lock에는 공유 Lock(Shared Lock)과 전용 Lock(Exclusive Lock)이 있습니다. 공유 Lock은 읽기 작업에 사용되고, 공유 Lock끼리는 서로 동시 접근이 가능하지만, 공유 Lock과 전용 Lock이 만나면 블로킹이 발생합니다.

블로킹(Blocking)은 Lock 간 경합이 발생해 특정 트랜잭션이 작업을 진행하지 못하고 멈춰선 상태를 말합니다. 예를 들어 트랜잭션 A가 특정 데이터에 전용 Lock을 설정했는데, 트랜잭션 B가 같은 데이터에 공유 Lock을 설정하려고 하면 B는 A가 Lock을 해제할 때까지 기다려야 합니다. 이는 애플리케이션 성능에 좋지 않은 영향을 주기 때문에 최소화해야 합니다.

교착 상태(Deadlock)는 두 트랜잭션이 각각 Lock을 설정하고 서로의 Lock이 해제되기를 영원히 기다리는 상황입니다. 아래 표는 Lock 조합에 따른 블로킹 발생 여부를 정리한 것입니다.

Lock 기반 동시성 제어, 블로킹과 교착 상태 이해하기
Lock 기반 동시성 제어, 블로킹과 교착 상태 이해하기
트랜잭션 A 트랜잭션 B 블로킹 발생
공유 Lock 공유 Lock 없음
공유 Lock 전용 Lock 발생
전용 Lock 공유 Lock 발생
전용 Lock 전용 Lock 발생

교착 상태를 피하기 위해서는 트랜잭션 설계 단계에서 Lock을 획득하는 순서를 일관되게 유지하는 것이 중요합니다. 예를 들어 두 테이블을 항상 같은 순서로 접근하도록 규칙을 정하면 교착 상태 가능성을 줄일 수 있습니다.

MVCC, 읽기 성능을 높이는 다중 버전 동시성 제어

MVCC(Multi Version Concurrency Control)는 데이터에 접근하는 사용자가 접근 시점의 데이터베이스 스냅샷을 읽는 방식입니다. 변경 사항이 커밋되기 전까지는 다른 사용자가 그 변경 내역을 볼 수 없고, 데이터를 업데이트하면 이전 데이터를 덮어쓰는 대신 UNDO 영역에 새로운 버전을 생성합니다. 이후 여러 버전의 데이터 중 현재 실행 중인 스케줄의 직렬 가능성이 보장되는 적절한 버전을 선택해 접근하도록 합니다.

MVCC의 가장 큰 장점은 트랜잭션의 대기 시간이 존재하지 않는다는 점입니다. 읽기 작업이 쓰기 작업을 기다리지 않아도 되므로 빠르게 동작하며, 데이터를 읽기 시작한 시점 이후에 다른 트랜잭션이 그 데이터를 수정하거나 삭제해도 조회가 가능합니다. 다만, UNDO 영역에 이전 버전의 데이터가 계속 쌓이기 때문에 저장 공간이 추가로 필요하고, 오래된 스냅샷을 유지하는 동안에는 정리 작업이 지연될 수 있습니다.

방식 특징 장점 단점
Lock 데이터 접근 시 잠금 설정 구현이 단순 블로킹·교착 발생
MVCC 버전을 여러 개 유지 읽기 대기 없음 저장 공간 추가
2PL Lock 확장·축소 2단계 직렬성 보장 동시성 저하

트랜잭션 격리 수준과 @Transactional 한계

트랜잭션 격리 수준(Isolation Level)은 동시성 제어를 어느 정도로 할지 결정하는 기준입니다. MySQL 기준으로 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE 네 가지 레벨이 있습니다. 격리 수준이 높을수록 데이터 일관성은 좋아지지만 동시성은 낮아집니다.

일부 개발자는 @Transactional(isolation = Isolation.SERIALIZABLE)처럼 트랜잭션 단위로 격리 수준을 높이면 성능 저하 없이 동시성 문제를 해결할 수 있을 것이라고 생각합니다. 하지만 이 방식은 쓰기 작업만 막을 뿐 읽기 작업은 막지 못합니다. 단순 SELECT 문은 테이블을 읽어버리기 때문에 동시성 제어가 되지 않으며, 오히려 전체적인 성능만 낮아질 수 있습니다. 따라서 격리 수준을 높이는 것보다는 비즈니스 로직에 맞는 적절한 Lock 전략이나 MVCC를 조합하는 것이 더 효과적입니다.

동시성 제어 해결 방법, 상황별 선택 기준

동시성 제어 기법을 선택할 때는 데이터의 특성과 서비스 요구사항을 함께 고려해야 합니다. 재고 차감처럼 동시에 많은 요청이 몰리면서 정확한 값이 중요한 경우에는 전용 Lock이나 낙관적 Lock을 사용하는 것이 적합합니다. 반면 조회가 많은 게시판이나 상품 목록 같은 경우에는 MVCC를 활용하면 읽기 성능을 유지하면서 일관성을 확보할 수 있습니다.

분산 환경에서는 Redis를 활용한 분산락(Distributed Lock)을 사용하기도 합니다. Spring Boot 환경에서는 spring-boot-starter-data-redis와 redisson-spring-boot-starter 같은 의존성을 추가해 구현할 수 있습니다. 분산락은 여러 서버 인스턴스가 하나의 데이터에 접근할 때 동일한 Lock을 공유해야 하는 상황에서 유용합니다.

동시성 제어 해결 방법, 상황별 선택 기준
동시성 제어 해결 방법, 상황별 선택 기준
상황 권장 기법 고려 사항
재고 차감 전용 Lock 동시 요청 많음
조회 중심 MVCC 읽기 성능 우선
분산 서버 분산락 Redis 필요
정합성 최우선 2PL 동시성 희생

동시성 제어는 단일 기술만으로 해결되는 문제가 아닙니다. Lock 기반의 확실한 정합성 보장이 필요한 경우와 MVCC의 빠른 읽기 성능이 필요한 경우를 구분해서 적용하는 것이 중요합니다. 트랜잭션 격리 수준과 각 기법의 특성을 이해하고 나면, 서비스 특성에 맞는 적절한 동시성 제어 전략을 설계할 수 있습니다.