데이터 무결성을 지키는 트랜잭션과 동시성 제어
트랜잭션의 ACID 속성, 격리 수준, 동시성 제어와 분산 트랜잭션 관리 방식을 실무 관점에서 정리합니다.
2026-08-14 · 최초 발행 2025-08-10
한 작업으로 묶어야 데이터가 어긋나지 않는다
트랜잭션(Transaction)은 데이터베이스 상태를 바꾸는 논리적 작업 단위다. 여러 작업을 하나로 묶어 모두 실행하거나 전혀 실행하지 않도록(All or Nothing) 처리하며, 데이터베이스의 일관성과 무결성을 지키는 핵심 메커니즘이다.
ATM에서 현금을 인출할 때 계좌 잔액 차감과 현금 지급은 함께 성공해야 한다. 한쪽만 실행되면 데이터 불일치가 생긴다. 금융, 예약, 재고 관리처럼 상태 변경의 정확성이 중요한 업무에서 트랜잭션이 필요한 이유다.
ACID가 보장하는 범위
원자성은 중간 상태를 남기지 않는다
원자성(Atomicity)은 트랜잭션 안의 연산이 모두 반영되거나 전혀 반영되지 않아야 한다는 속성이다. 일부 명령만 수행된 상태를 남기지 않으며, 로그 기반 복구나 섀도우 페이징(Shadow paging) 등이 구현 방식으로 사용된다.
일관성은 제약조건을 지킨다
일관성(Consistency)은 실행 전후 데이터베이스가 일관된 상태를 유지하도록 요구한다. 제약조건, 규칙, 관계가 위반되어서는 안 된다. 계좌 이체 뒤 총 잔액이 변하지 않아야 한다는 조건이 한 사례다.
격리성은 실행 중인 작업을 분리한다
격리성(Isolation)은 동시에 수행되는 트랜잭션이 서로의 연산에 영향을 주지 않게 한다. 실행 중인 작업 사이에 다른 트랜잭션이 끼어들지 않도록 하며, 격리 수준(Isolation Level)으로 동시성과 일관성의 균형을 조정한다.
지속성은 커밋 결과를 보존한다
지속성(Durability)은 성공적으로 끝난 트랜잭션의 결과가 영구적으로 반영됨을 뜻한다. 시스템 장애가 일어나더라도 커밋된 내용은 보존되어야 하며, 로그(WAL: Write-Ahead Logging)와 체크포인트 등이 이를 뒷받침한다.
실행부터 롤백까지의 상태 변화
트랜잭션은 실행 중인 활동(Active) 상태에서 마지막 연산을 마친 부분 완료(Partially Committed) 상태로 이동할 수 있다. 커밋이 끝나면 완료(Committed)가 되고, 오류로 더 진행할 수 없으면 실패(Failed)를 거쳐 철회(Aborted)된다. 철회된 경우 데이터베이스는 트랜잭션 시작 전 상태로 환원된다.
격리 수준이 허용하거나 막는 문제
Read Uncommitted(레벨 0)는 가장 낮은 격리 수준으로, 다른 트랜잭션이 커밋하지 않은 데이터를 읽을 수 있다. 롤백될 수 있는 변경 데이터를 읽는 Dirty Read가 발생할 수 있지만 성능은 좋다.
Read Committed(레벨 1)는 Oracle, SQL Server 등 대부분 DBMS의 기본 격리 수준이다. 커밋된 데이터만 읽을 수 있어 Dirty Read는 막지만, 같은 쿼리를 두 번 실행했을 때 결과가 달라질 수 있는 Non-repeatable Read는 발생할 수 있다.
Repeatable Read(레벨 2)는 MySQL InnoDB의 기본 격리 수준이다. 트랜잭션 안에서 같은 데이터를 여러 번 읽어도 같은 결과를 보장해 Non-repeatable Read를 방지한다. 다만 다른 트랜잭션이 조건에 맞는 새 행을 추가하면 Phantom Read가 발생할 수 있다.
Serializable(레벨 3)은 가장 높은 격리 수준으로, 트랜잭션을 완전히 분리해 순차 실행과 같은 결과를 보장한다. Dirty Read, Non-repeatable Read, Phantom Read를 모두 방지하지만 성능 저하가 발생하므로 필요한 경우에 사용한다.
동시에 접근할 때 생기는 읽기 오류
Dirty Read는 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 현상이다. 트랜잭션 A가 데이터를 바꾼 뒤 커밋하기 전에 트랜잭션 B가 이를 읽는 경우가 해당한다.
Non-repeatable Read는 하나의 트랜잭션에서 같은 쿼리를 다시 실행했을 때 다른 결과를 얻는 경우다. 트랜잭션 A가 데이터를 읽은 뒤 트랜잭션 B가 이를 변경하고 커밋하면, A의 다음 읽기 결과가 달라질 수 있다.
Phantom Read는 같은 조건으로 다시 조회했을 때 이전에 없던 레코드가 나타나는 현상이다. A가 조건에 맞는 레코드를 조회한 뒤 B가 새 레코드를 삽입하고 커밋하면, A는 다음 조회에서 새 레코드를 발견한다.
충돌을 관리하는 방식
락킹(Locking)은 읽기 작업에 공유 락(Shared Lock)을, 쓰기 작업에 배타적 락(Exclusive Lock)을 사용한다. 공유 락이 걸린 데이터는 다른 트랜잭션도 읽을 수 있지만 쓸 수 없고, 배타적 락은 다른 트랜잭션의 읽기와 쓰기를 모두 막는다. 2단계 락킹(2PL)은 확장 단계와 수축 단계로 나누어 락을 관리한다.
타임스탬프 기반 프로토콜은 트랜잭션마다 고유한 타임스탬프를 부여하고 그 순서에 따라 실행 순서를 정한다. 충돌이 나면 더 이른 타임스탬프를 가진 트랜잭션이 우선한다.
낙관적 동시성 제어(Optimistic Concurrency Control)는 충돌이 드물다는 가정 아래 검증 단계에서 충돌을 확인한다. 읽기 단계, 검증 단계, 쓰기 단계로 구성되며 충돌이 생기면 트랜잭션을 재시도한다.
다중 버전 동시성 제어(MVCC)는 데이터 항목의 여러 버전을 유지해 읽기 작업의 격리성을 확보한다. 읽기와 쓰기 사이의 충돌을 줄이며 MySQL의 InnoDB, PostgreSQL 등에서 사용된다.
여러 시스템에 걸친 변경을 다루는 방법
분산 트랜잭션은 여러 데이터베이스나 시스템에 걸친 작업을 관리한다.
2단계 커밋(2PC: Two-Phase Commit)은 준비 단계(Prepare Phase)와 커밋 단계(Commit Phase)로 진행된다. 모든 참여자가 준비 완료 메시지를 보내야 최종 커밋이 가능하다. 원자성을 보장하지만 코디네이터 장애 시 블로킹 문제가 발생한다.
3단계 커밋(3PC: Three-Phase Commit)은 2PC의 블로킹 문제를 개선하기 위한 방식이다. CanCommit, PreCommit, DoCommit으로 구성되며 타임아웃 메커니즘으로 무한 대기를 방지한다.
보상 트랜잭션(Compensating Transaction)은 마이크로서비스 아키텍처에서 주로 사용된다. Saga 패턴에서는 각 트랜잭션을 로컬에서 커밋하고, 실패하면 보상 트랜잭션으로 롤백한다. 이 방식은 최종 일관성(Eventual Consistency)을 보장한다.
업무 흐름에서의 선택
금융 시스템의 계좌 이체는 출금 계좌 차감과 입금 계좌 추가가 원자적으로 처리되어야 한다. 데이터 일관성을 최우선으로 두기 위해 대부분 Serializable 격리 수준을 사용한다.
전자상거래의 주문 처리는 재고 확인, 결제 처리, 주문 생성 등을 하나의 트랜잭션으로 다룬다. 동시 주문에서도 정확한 재고를 유지하려면 업무 특성에 맞춰 격리 수준을 정해야 한다.
항공권 예약에서는 동시 요청에도 중복 좌석 예약이 일어나지 않아야 한다. 예약, 결제, 마일리지 시스템 간의 일관성을 유지해야 할 때는 분산 트랜잭션이 필요하다.
처리량을 해치지 않기 위한 운영 지점
트랜잭션에는 필요한 작업만 넣고 실행 시간도 최소화한다. 불필요한 작업은 트랜잭션 밖으로 분리한다.
모든 작업에 Serializable을 적용하면 성능 저하가 생긴다. 데이터 특성과 요구사항에 맞는 격리 수준을 선택해야 한다.
효율적인 인덱스 설계는 락 경합을 줄이고 트랜잭션 처리 시간을 단축해 동시성을 높인다. 락 세분화(Lock Granularity)도 함께 검토한다. 테이블 락 대신 행 락을 사용해 동시성을 높일 수 있으며, 필요에 따라 페이지 락과 테이블 락을 선택한다.
로그와 지표로 병목을 찾는다
트랜잭션 로그에서는 실행 쿼리, 소요 시간, 락 대기 시간을 모니터링해 문제 트랜잭션을 식별하고 최적화한다.
데드락이 발생하면 자동 감지와 롤백 전략이 필요하다. 데드락 그래프를 분석하면 근본 원인을 파악할 수 있다.
TPS(Transaction Per Second), 평균 트랜잭션 시간, 롤백 비율 같은 성능 메트릭을 추적하면 운영 중인 트랜잭션의 상태를 확인할 수 있다.