DB

[RDBMS] 트랜잭션

꾸준함. 2025. 5. 14. 11:11

1. 트랜잭션 개요

  • 트랜잭션은 데이터베이스에서 원자적 (Atomic)이고, 일관적 (Consistent)이며, 격리 (Isolated)되고, 영속적 (Durable)인 작업 단위
  • 실무에서는 동시 사용자 요청, 서버 장애, 네트워크 오류 등 다양한 이슈가 존재하기 때문에 트랜잭션의 안정적 관리가 필수적
  • 트랜잭션을 적용하지 않았을 경우 흔히 발생하는 문제로는, 동시에 여러 인출 요청이 들어와 체크-후-실행 (check-then-act) 로직이 깨져서 음수 잔고나 데이터 불일치가 생기는 케이스
    • i.g. 금융, 암호화폐 등 돈이 오가는 시스템에서는 동시성 제어나 트랜잭션 오류가 심각한 보안사고, 해킹, 자산 손실로 이어질 수 있음을 FlexCoin 해킹 사례

 

2. ACID

 

Atomicity (원자성)

  • 트랜잭션은 전부 실행되거나, 전부 취소되어야 하며, 중간상태가 남아선 안 됨
  • Undo Log가 변경 전 상태를 저장하여 ROLLBACK 시 원상복구

 

Consistency (일관성)

  • 트랜잭션 전후로 데이터 일관성이 보장되어야 함
  • DB의 제약조건(타입, NOT NULL, PK/FK, UNIQUE, CHECK 등)이 이를 보장
  • CAP 이론에서의 일관성과 혼동하지 않아야 하며, CAP에서의 Consistency는 Linearizability( 직렬성)와 더 가까움

 

Isolation (고립성)

  • 여러 트랜잭션이 동시에 실행되어도 결과는 마치 순차적으로 실행된 것과 같아야 함

 

Durability (지속성)

  • 트랜잭션이 커밋되면, 시스템 장애가 나도 데이터는 유실되지 않아야 함
  • Redo Log가 해당 역할을 수행

 

3. Isolation Level과 데이터 무결성 문제 (Phenomena)

  • 표준 격리 수준에서 발생할 수 있는 데이터 이상 현상 (Phenomena)은 다음과 같음
    • Dirty Read: 커밋되지 않은 데이터 읽기 가능 (가장 낮은 수준)
    • Non-repeatable Read: 같은 쿼리라도 실행 시점에 따라 값이 달라짐
    • Phantom Read: 같은 조건의 쿼리를 여러 번 해도 결과 행의 개수가 바뀜

 

 

  • 비표준적이지만 실무에서 더 중요한 동시성 문제는 다음과 같음 
    • Read Skew: 한 트랜잭션이 여러 값을 읽는 동안 다른 트랜잭션이 그중 일부 값을 바꿈
    • Write Skew: 두 트랜잭션이 서로 다른 행을 읽고, 조건을 만족한다고 판단해 동시에 쓰기를 하면 제약조건이 깨짐
    • Lost Update: 두 트랜잭션이 같은 데이터를 읽고 각각 수정, 마지막에 저장하는 쪽의 값만 남는 현상

 

 

  • SQL 표준은 Read Uncommitted, Read Committed, Repeatable Read, Serializable 4가지 격리 수준을 정의
  • SQL 표준의 주요 Isolation Level은 다음과 같음
    • Read Uncommitted: Dirty Read 허용
    • Read Committed: Dirty Read 차단 (Oracle 기본)
    • Repeatable Read: Non-Repeatable Read 차단 (MySQL, PostgreSQL 기본)
    • Serializable: 모든 이상현상 차단, 가장 엄격

 

 

 

 

 

4. Locking과 동시성 제어

  • 트랜잭션 간 충돌 (동일 데이터에 대한 동시에 읽기/쓰기)을 막기 위해 DB는 락을 활용
  • 락의 종류는 다음과 같음
    • Shared (Read) Lock: 여러 트랜잭션이 데이터를 읽을 수 있지만, 쓰기는 불가
    • Exclusive (Write) Lock: 해당 데이터에 대해 읽기/쓰기 모두 차단

 

  • SQL에서 FOR UPDATE, FOR SHARE, WITH (ROWLOCK), WITH (HOLDLOCK) 등으로 특정 행을 락 걸 수 있음

 

 

  • NOWAIT SKIP LOCKED 옵션을 이용해, 이미 잠긴 데이터에 대해 대기하지 않고 곧장 오류를 반환하거나, 잠긴 행을 건너뛸 수 있음 
    • NOWAIT 옵션: 이미 락이 걸려 있으면 즉시 실패
    • SKIP LOCKED 옵션: 락 걸린 행은 건너뛰고 처리 (주로 Job Queue 등에서 충돌 최소화에 사용)

 

 

  • Deadlock은 두 트랜잭션이 서로가 가진 락을 기다리다 영원히 대기하는 현상이며 DBMS는 교착 상태를 자동 감지해 한쪽 트랜잭션을 강제 롤백하여 해소함
    • 각 DBMS마다 데드 락 해소 우선순위, 방식이 다름
    • Oracle: Oracle은 데드 락을 감지하면 가장 먼저 교착 상태를 감지한 세션의 쿼리를 롤백
    • SQL Server: 데드 락이 감지되면 롤백 비용이 가장 낮은 트랜잭션을 우선적으로 종료하며 만약 여러 트랜잭션의 롤백 비용이 같다면, DEADLOCK_PRIORITY 값이 낮은 트랜잭션이 먼저 롤백
    • PostgreSQL: PostgreSQL은 데드 락이 발생할 경우 내부적으로 임의의 트랜잭션 하나를 선택하여 롤백
    • MySQL: InnoDB는 deadlock 시 가장 적은 수의 row를 변경한 트랜잭션을 롤백

 

5. 2PL과 MVCC

  • 관계형 데이터베이스에서 동시성 제어는 여러 트랜잭션이 동일한 데이터에 접근할 때 일관성과 무결성을 보장하는 핵심 기술
  • 대표적인 동시성 제어 방식은 다음과 같음
    • 2PL (Two-Phase Locking)
    • MVCC (Multi-Version Concurrency Control)

 

5.1 2PL (Two-Phase Locking)

  • 데이터에 대한 동시 접근 충돌을 예방하기 위해 트랜잭션이 락을 잡는 시기와 해제하는 시기를 엄격히 관리하는 프로토콜
  • 해당 방식은 트랜잭션이 락을 사용하는 두 단계를 반드시 거치도록 강제함
    • 확장 단계 (Growing Phase): 트랜잭션이 필요로 하는 락(읽기/쓰기 락 등)을 계속 획득할 수 있지만 한 번이라도 락을 해제하면 더 이상 새로운 락을 얻을 수 없음
    • 축소 단계 (Shrinking Phase): 트랜잭션이 잡았던 락을 해제하기만 하고, 더 이상 새로운 락을 얻을 수 없음

 

장점

  • 직렬성 (Serializability) 보장: 모든 트랜잭션의 결과가 실제로 순차 실행한 것과 동일
  • 단순한 개념: 락의 획득과 해제 규칙만 잘 따르면 일관성 보장

 

단점

  • 락 경합 (Bottleneck): 트랜잭션이 동시에 같은 데이터에 접근하면 락 대기가 발생, 전체 처리량 저하
  • 데드락 (Deadlock): 두 트랜잭션이 서로가 잡은 락을 기다리며 영원히 대기할 수 있음
  • 무거운 락 관리 비용: 트랜잭션이 길어질수록 락 보유 시간이 늘어나 시스템 전체가 느려질 수 있음

 

2PL 예시

 

https://vladmihalcea.com/2pl-two-phase-locking/

 

1. Alice의 트랜잭션 시작 (BEGIN TRANSACTION)

  • Alice가 트랜잭션 시작
  • Lock Acquisition Phase (락 획득 단계)에 진입

 

2. Alice가 데이터 읽기 (FOR SHARE 락)

  • Alice는 SELECT * FROM post WHERE id = 1 FOR SHARE 쿼리로 게시글 조회 (확장 단계)
  • FOR SHARE는 공유 락 (읽기 락)으로, 다른 트랜잭션이 동일 행을 읽을 수 있지만, 쓰기는 막음
  • Alice는 게시글 id=1, title='Transactions' 데이터를 읽고, 해당 행에 공유 락 획득

 

3. Bob의 트랜잭션 시작 (BEGIN TRANSACTION)

  • Bob도 트랜잭션을 시작하고, 락 획득 단계에 진입

 

4. Bob도 데이터 읽기 (FOR SHARE 락)

  • Bob 역시 SELECT * FROM post WHERE id = 1 FOR SHARE로 데이터 조회 (확장 단계)
  • 공유 락은 여러 트랜잭션이 동시에 획득 가능하므로, Bob도 게시글 id=1, title='Transactions'를 읽음

 

5. Bob이 데이터 수정 시도 (UPDATE)

  • Bob은 UPDATE post SET title = 'ACID' WHERE id = 1로 게시글 수정 시도
  • 이때 독점 락 (Exclusive Lock)이 필요하지만 Alice가 아직 공유 락을 보유하고 있기 때문에, Bob의 UPDATE는 락을 기다리게 됨 (Wait For Lock)

 

6. Alice가 트랜잭션 종료 (COMMIT)

  • Alice가 트랜잭션을 커밋하면, 락 해제 단계 (Lock Release Phase)에 들어가고, 공유 락 해제 (축소 단계)

 

7. Bob의 UPDATE 실행

  • Alice의 락이 해제되자, Bob은 기다리던 독점 락을 획득하고, 게시글을 title='ACID'로 수정 
  • 해당 시점에 데이터베이스에는 id=1, title='ACID'가 반영

 

8. Bob도 트랜잭션 종료 (COMMIT)

  • Bob이 트랜잭션을 커밋하면서, 자신의 락도 해제 (축소 단계)

 

9. 이후 Alice가 다시 SELECT (FOR SHARE) 시도

  • Alice가 새 트랜잭션을 시작하고 동일한 쿼리로 SELECT를 시도하지만, 이때는 Bob이 아직 락을 가지고 있으므로 잠시 대기
  • Bob이 커밋 후 락을 해제하면, Alice는 title='ACID'로 바뀐 데이터 조회

 

5.2 MVCC (Multi-Version Concurrency Control)

  • MVCC는 데이터의 버전을 여러 개 관리함으로써, 읽기와 쓰기 작업이 서로 차단하지 않게 (Non-blocking) 만드는 동시성 제어 방식
  • 트랜잭션마다 자신만의 스냅샷을 읽으며, 트랜잭션이 시작된 순간의 데이터 상태를 계속 참조
  • MVCC의 동작 방식은 다음과 같음
    • 읽기 작업 (SELECT): 트랜잭션이 시작된 시점의 데이터 버전 (스냅샷)을 읽으며, 다른 트랜잭션이 데이터를 변경하더라도 자신의 트랜잭션에는 영향이 없음
    • 쓰기 작업 (INSERT/UPDATE/DELETE): 데이터의 기존 버전을 그대로 두고, 새로운 버전을 생성, 다른 트랜잭션이 해당 데이터를 읽고 있다면 해당 트랜잭션은 옛 버전을 계속 읽고 새로운 트랜잭션부터 새 버전을 읽음
    • 커밋/롤백: 트랜잭션이 커밋되면, 새 버전이 확정되고 옛 버전은 더 이상 참조되지 않으면 정리됨, 롤백되면 새 버전은 폐기됨

 

장점

  • 읽기와 쓰기가 서로를 차단하지 않음: 읽기 작업은 항상 Non-blocking, 쓰기만 충돌 시 대기
  • 동시성 극대화: OLTP, 대용량 시스템에서 읽기 성능이 매우 우수
  • 비교적 데드락이 적음: 읽기끼리, 읽기-쓰기끼리 충돌이 없음

 

단점

  • 버전 관리 비용: 데이터의 여러 버전을 메모리/디스크에 보관해야 하므로, 저장 비용과 정리(garbage collection) 부담이 있음
  • 쓰기-쓰기 충돌: 두 트랜잭션이 같은 데이터를 동시에 수정하면, 한쪽이 충돌로 롤백될 수 있음
  • 격리 수준에 따라 이상현상: Repeatable Read, Serializable 등 구현에 따라 Non-repeatable Read, Phantom Read, Write Skew 등이 발생할 수 있음

 

MVCC 예시

 

https://vladmihalcea.com/how-does-mvcc-multi-version-concurrency-control-work/

 

 

1. 트랜잭션 및 트랜잭션 ID

  • Alice와 Bob은 각각 독립적인 트랜잭션을 시작
  • Alice의 현재 트랜잭션 ID(txid_current())는 313413이고, Bob의 트랜잭션 ID는313414
  • PostgreSQL의 MVCC에서는 각 행에 xmin (해당 행을 생성한 트랜잭션 ID), xmax (해당 행을 삭제하거나 수정한 트랜잭션 ID)가 기록됨

 

2. Alice의 데이터 삽입

  • Alice가 INSERT INTO post (title, id) VALUES ('Alice', 1) 쿼리 실행
  • 이로써 post 테이블에 id=1, title='Alice'인 새로운 행 생성
    • xmin = 313413 (Alice의 트랜잭션 ID)
    • xmax = 0 (아직 삭제/수정되지 않았으니 0)

 

  • 중요한 점은 Alice의 트랜잭션은 아직 COMMIT 되지 않았기 때문에, 해당 행은 미확정 (uncommitted) 상태입니다.

 

3. Alice의 SELECT

Alice가 자신의 트랜잭션 내에서 해당 행을 조회하면 자신이 생성한 (아직 커밋 전) 데이터도 볼 수 있음

  • 결과: id=1, title='Alice', xmin='313413', xmax='0'

 

4. Bob의 SELECT (커밋 전, Read Committed)

  • Bob이 자신의 트랜잭션에서 동일한 행을 조회
  • Read Committed 격리 수준에서는 커밋되지 않은 (미확정) 타 트랜잭션의 데이터는 조회할 수 없음
    • Alice가 만든 데이터는 아직 커밋 전이므로, Bob은 아무 결과도 조회할 수 없음

 

5. Alice의 COMMIT

  • Alice가 트랜잭션을 커밋
  • 이제 Alice가 추가한 행은 모든 트랜잭션에서 확정된 (committed) 버전으로 인식됨

 

6. Bob의 SELECT (커밋 후)

  • Bob이 다시 같은 쿼리를 실행하면 Alice가 커밋한 새 행(id=1, title='Alice', xmin='313413', xmax='0')을 정상적으로 조회할 수 있음

 

5.3 2PL vs MVCC

 

  2PL MVCC
동시성 낮음 (락 경합 발생) 높음 (읽기-쓰기 차단 없음)
데드 락 자주 발생 거의 없음 (쓰기-쓰기만 충돌)
읽기 성능 트랜잭션 대기 및 락 필요 Non-blocking, 매우 빠름
쓰기 성능 락 경합 시 느릴 수 있음 버전 관리 비용 소모
일관성/격리 직렬성 보장 (Serializable) 구현에 따라 Serializable ~ Read Committed
관리 복잡성 락 관리가 단순하지만 데드 락 주의 필요 버전 관리, GC 등 시스템 부하 가능성 존재

 

6. Optimistic Locking

  • 충돌이 거의 없다고 가정하고, 변경 시점에만 버전 체크로 충돌을 감지
  • 각 레코드에 version 컬럼 (숫자 또는 타임스탬프) 사용
  • UPDATE/DELETE 시 version 값이 일치하는지 체크, 불일치하면 롤백 및 재시도
  • JPA/Hibernate 등 ORM에서 @Version 어노테이션 사용 시 자동 적용
  • 대량 데이터 일괄 처리 (Bulk Update)에서도 버전 체크로 Lost Update 방지 가능

 

6.1 Optimistic Locking 동작 원리

  • 데이터 읽기: 사용자는 데이터(행)를 읽어온다. 이때, 해당 데이터의 버전 정보 (예: version 컬럼, 타임스탬프 등)도 함께 조회
  • 업데이트 준비: 사용자는 데이터 값을 로컬에서 수정
  • 업데이트 시도 (커밋): 사용자가 변경 사항을 DB에 반영할 때, WHERE 조건에 원래 읽어온 버전 정보도 함께 포함
    • i.g. UPDATE table SET ... , version = version + 1 WHERE id = ? AND version = ?
  • 충돌 검증: 데이터베이스는 WHERE 조건에 맞는 행이 있으면 (즉, 버전이 일치하면) 업데이트 수행
    • 만약 WHERE 조건에 맞는 행이 없으면 (즉, 다른 트랜잭션이 먼저 값을 변경해 버전이 달라졌다면), 아무 행도 갱신되지 않음
  • 충돌 감지 및 처리: 업데이트된 행 수가 1이면 성공 (충돌 없음)
    • 0이면 충돌 발생 → 애플리케이션은 예외를 발생시키거나, 사용자가 데이터를 다시 읽고 재시도할 수 있도록 처리

 

6.2 구현 방법

  • Optimistic Locking은 주로 버전 컬럼이나 타임스탬프를 통해 구현
    • 버전 컬럼 방식: 테이블에 version이라는 정수 컬럼을 정의한 뒤 데이터가 수정될 때마다 version 값이 1씩 증가.
    • 타임스탬프 방식:데이터가 수정될 때마다 last_modified 컬럼 등을 현재 시각으로 갱신하며 WHERE 절에서 이 타임스탬프가 기존 값과 같은지 확인


 

참고

인프런 - JPA (ORM) 개발자를 위한 고성능 SQL

반응형

'DB' 카테고리의 다른 글

[RDBMS] 페이징 (Pagination)  (0) 2025.05.12
[RDBMS] 파생 테이블과 공통 테이블 표현식  (0) 2025.05.12
[RDBMS] JOIN 성능  (0) 2025.05.12
LATERAL JOIN 개요  (0) 2025.04.30