2025-08-15 14:49

Tags:

데이터 무결성의 수호자, 트랜잭션 핸드북 ACID부터 분산 트랜잭션까지

데이터를 다루는 모든 시스템의 심장부에는 ‘신뢰’라는 가장 중요한 가치가 존재. 사용자는 자신의 데이터가 위조되거나 변조되지 않고, 사라지지 않으며, 언제나 정확하게 유지될 것이라 믿고 시스템을 사용. 만약 은행 앱에서 돈을 이체했는데 내 계좌에선 돈이 빠져나갔지만 상대방 계좌에는 입금되지 않는다면? 온라인 쇼핑몰에서 결제는 완료되었는데 주문은 생성되지 않았다면? 상상만 해도 끔찍한 이런 상황을 막아주는 기술적磐石(반석), 그것이 바로 트랜잭션(Transaction).

이 핸드북은 개발자라면 반드시 알아야 할 트랜잭션의 모든 것을 담은 안내서. 트랜잭션이 왜 탄생했는지, 그 유명한 ACID 원칙은 무엇인지, 그리고 현대적인 분산 시스템에서는 트랜잭션을 어떻게 다루는지까지. 단순한 개념 나열이 아닌, 명확한 비유와 실용적인 예시를 통해 트랜잭션의 핵심을 파헤쳐 볼 것.

1. 트랜잭션의 탄생: 왜 필요한가?

트랜잭션의 필요성을 이해하기 위해 가장 고전적이고 확실한 예시, ‘계좌 이체’를 생각해보자. A가 B에게 10,000원을 이체하는 상황. 이 간단해 보이는 작업은 데이터베이스 내부에서 최소 두 가지 이상의 작업으로 나뉨.

  1. A의 계좌에서 10,000원을 차감한다.

  2. B의 계좌에 10,000원을 추가한다.

만약 1번 작업이 성공적으로 끝난 직후, 데이터베이스 서버에 전원 공급이 끊기거나, 네트워크 오류가 발생하거나, 혹은 알 수 없는 소프트웨어 버그가 발생해 2번 작업이 실패한다면 어떻게 될까? A의 돈은 사라졌지만, B는 받지 못한, 말 그대로 ‘공중분해’된 10,000원이 발생. 데이터의 **정합성(Consistency)**이 깨져버린 것.

이처럼 **논리적으로 더 이상 쪼갤 수 없는 작업의 단위(Logical Unit of Work)**를 안전하게 처리하기 위한 개념이 바로 트랜잭션. 트랜잭션은 “이 작업들은 모두 성공하거나, 하나라도 실패하면 모두 실패한 것으로 되돌려라”라는 단순하지만 강력한 규칙을 데이터베이스 시스템에 부여. 즉, 계좌 이체라는 작업 단위를 하나의 트랜잭션으로 묶음으로써, 1번과 2번 작업이 모두 성공해야만 최종적으로 데이터를 저장(COMMIT)하고, 둘 중 하나라도 실패하면 모든 변경 사항을 이전 상태로 되돌리는(ROLLBACK) 것을 보장.

이것이 트랜잭션이 ‘데이터 무결성의 수호자’라 불리는 이유.

2. 트랜잭션의 4가지 심장: ACID 원칙

트랜잭션이 신뢰성을 보장하기 위해 반드시 지켜야 하는 4가지 성질이 있으며, 이를 묶어 ACID라고 부름. 데이터베이스 시스템의 안정성을 논할 때 빠지지 않고 등장하는 핵심 개념.

A: Atomicity (원자성)

“All or Nothing”. 원자(Atom)가 더 이상 쪼갤 수 없는 물질의 최소 단위인 것처럼, 트랜잭션에 포함된 모든 작업은 전부 성공적으로 실행되거나, 아니면 단 하나도 실행되지 않은 상태, 즉 전부 실패한 상태로 남아야 함.

  • 비유: 레고 블록으로 자동차를 조립하는 과정 전체가 하나의 트랜잭션. 만약 바퀴 하나를 끼우다 실패하면, 완성된 몸체까지 모두 분해해서 조립 시작 전의 상태로 되돌려 놓는 것과 같음. ‘몸체만 완성된 어중간한 상태’는 절대 허용하지 않음.

  • 구현: 데이터베이스는 보통 **로그(Log)**를 사용해 원자성을 보장. 트랜잭션이 진행되는 동안 발생하는 모든 변경 사항을 로그에 기록해두고, 중간에 실패하면 이 로그를 역으로 추적하여 원래 상태로 복원(ROLLBACK).

C: Consistency (일관성)

트랜잭션은 데이터베이스의 상태를 하나의 ‘일관된 상태’에서 또 다른 ‘일관된 상태’로 이전시켜야 함. 여기서 ‘일관된 상태’란 데이터베이스에 정의된 여러 규칙이나 제약 조건(예: 계좌 잔고는 음수가 될 수 없다, 모든 주문에는 반드시 고객 ID가 있어야 한다 등)을 위반하지 않는 유효한 상태를 의미.

  • 비유: 체스 게임에서 말을 움직이는 행위가 트랜잭션. 룩(Rook)은 직선으로만, 비숍(Bishop)은 대각선으로만 움직일 수 있다는 ‘규칙’이 존재. 이 규칙을 어겨서 말을 움직이는 것은 허용되지 않음. 트랜잭션 전에도 체스 규칙은 지켜지고 있었고, 트랜잭션(말 이동) 후에도 규칙은 반드시 지켜져야 함.

  • 중요한 점: 일관성은 데이터베이스 시스템 자체보다는 트랜잭션을 실행하는 애플리케이션 개발자의 책임이 더 큰 영역. 데이터베이스는 제약 조건 위반 시 트랜잭션을 중단시키는 방식으로 일관성 유지를 ‘지원’할 뿐, 비즈니스 로직 자체의 일관성을 보장해주지는 않음.

I: Isolation (고립성 또는 격리성)

여러 트랜잭션이 동시에 실행될 때, 각 트랜잭션은 마치 혼자서 독립적으로 실행되는 것처럼 보여야 함. 즉, 하나의 트랜잭션이 실행되는 동안 다른 트랜잭션의 중간 연산 결과를 보거나 영향을 주어서는 안 됨.

  • 비유: 여러 명의 학생이 같은 시험 문제지를 풀고 있는 상황. 각 학생은 다른 학생이 답안지를 얼마나 작성했는지, 무슨 답을 썼는지 볼 수 없음. 모든 학생은 시험이 끝난 후(COMMIT), 채점된 결과만 볼 수 있음. 만약 시험 도중 서로의 답을 엿볼 수 있다면(고립성↓), 부정행위가 발생하고 시험 결과의 신뢰성은 무너짐.

  • 구현: 고립성을 보장하기 위해 데이터베이스는 **잠금(Locking)**이나 MVCC(Multi-Version Concurrency Control) 같은 복잡한 동시성 제어 메커니즘을 사용. (이는 심화 내용에서 자세히 다룸)

D: Durability (지속성)

성공적으로 완료된(COMMIT된) 트랜잭션의 결과는 시스템에 영구적으로 기록되어야 함. COMMIT 이후에 시스템 장애(예: 정전, 서버 다운)가 발생하더라도 데이터는 손실되지 않아야 함.

  • 비유: 중요한 계약서에 서명을 하고 도장을 찍는 행위. 한번 도장을 찍고 나면, 그 계약은 법적 효력을 가지며 되돌릴 수 없음. 이후에 계약서 보관함에 불이 나더라도(시스템 장애), 사본이나 다른 법적 장치를 통해 계약의 효력은 유지되어야 함.

  • 구현: 데이터베이스는 변경 내용을 **비휘발성 메모리(HDD, SSD)**에 확실히 기록하고, WAL(Write-Ahead Logging) 같은 기법을 사용해 로그를 먼저 기록한 뒤 실제 데이터를 변경함으로써 지속성을 보장.

3. 트랜잭션 사용법: SQL 명령어

개념을 알았으니 실제로 어떻게 사용하는지 살펴보자. 표준 SQL에서는 보통 세 가지 명령어로 트랜잭션을 제어.

  • START TRANSACTION (또는 BEGIN): 트랜잭션의 시작을 알림. 이 명령어 이후의 모든 SQL 문은 하나의 트랜잭션으로 묶임.

  • COMMIT: 트랜잭션 내에서 수행된 모든 변경 사항을 데이터베이스에 영구적으로 저장하고 트랜잭션을 종료.

  • ROLLBACK: 트랜잭션 내에서 수행된 모든 변경 사항을 취소하고, 트랜잭션 시작 이전 상태로 되돌리며 트랜잭션을 종료.

계좌 이체 예시 SQL:

START TRANSACTION;
 
-- 1. A의 계좌(ID: 1)에서 10000원 차감
UPDATE accounts SET balance = balance - 10000 WHERE user_id = 1;
 
-- 2. B의 계좌(ID: 2)에 10000원 추가
UPDATE accounts SET balance = balance + 10000 WHERE user_id = 2;
 
-- 모든 작업이 성공했으므로 영구 저장
COMMIT;

만약 1번 작업 후, B의 계좌가 존재하지 않거나 동결 상태여서 2번 작업이 실패했다면, 애플리케이션 로직은 COMMIT 대신 ROLLBACK을 호출해야 함.

START TRANSACTION;
 
UPDATE accounts SET balance = balance - 10000 WHERE user_id = 1;
 
-- 여기서 B 계좌에 문제가 발생하여 UPDATE가 실패했다고 가정
-- 애플리케이션은 오류를 감지하고 아래 명령을 실행
ROLLBACK;

ROLLBACK이 실행되면, A의 계좌에서 차감되었던 10,000원은 원래대로 복구됨.


4. 심화 내용: 고립성은 어떻게 지켜지는가?

ACID 원칙 중 가장 복잡하고 성능에 큰 영향을 미치는 것이 바로 고립성(Isolation). 데이터베이스는 어떻게 여러 트랜잭션이 서로를 방해하지 않도록 만들까? 주로 두 가지 방식을 사용.

가. 잠금 (Locking) 기반 동시성 제어

가장 직관적인 방법. 하나의 트랜잭션이 특정 데이터에 접근할 때, 다른 트랜잭션이 동시에 접근하지 못하도록 ‘잠금’을 거는 방식.

  • 공유 잠금 (Shared Lock, Read Lock): 데이터를 읽을 때 사용. 다른 트랜잭션도 해당 데이터를 ‘읽는 것’은 허용하지만, ‘변경하는 것(쓰기)‘은 막음. 여러 명이 동시에 책을 읽을 수는 있지만, 아무도 그 책에 낙서할 수는 없는 상황.
  • 배타 잠금 (Exclusive Lock, Write Lock): 데이터를 변경(쓰기)할 때 사용. 이 잠금이 걸리면 다른 어떤 트랜잭션도 해당 데이터를 ‘읽는 것’과 ‘쓰는 것’ 모두 할 수 없음. 한 사람이 책에 글을 쓰는 동안에는 다른 누구도 그 책을 보거나 쓸 수 없는 상황.

문제점: 잠금은 **교착 상태(Deadlock)**를 유발할 수 있음. 트랜잭션 T1이 데이터 A를 잠그고 데이터 B를 기다리는데, 트랜잭션 T2는 데이터 B를 잠그고 데이터 A를 기다리는, 서로가 서로의 작업이 끝나기만을 영원히 기다리는 상황. 데이터베이스는 이런 교착 상태를 감지하고 둘 중 하나의 트랜잭션을 강제로 종료(ROLLBACK)시켜 문제를 해결.

나. MVCC (Multi-Version Concurrency Control) 다중 버전 동시성 제어

최근 많은 RDBMS(PostgreSQL, Oracle, MySQL의 InnoDB 엔진 등)가 채택하는 더 정교한 방식. 데이터를 변경할 때마다 기존 데이터를 덮어쓰는 대신, 새로운 버전의 데이터를 생성하고 이전 버전의 데이터는 따로 보관.

  • 동작 방식: 각 트랜잭션은 자신이 시작된 시점을 기준으로, 그 시점의 데이터 ‘스냅샷’을 읽음. 트랜잭션이 진행되는 동안 다른 트랜잭션이 데이터를 변경하고 COMMIT하더라도, 현재 트랜잭션은 자신이 처음 봤던 버전의 데이터를 계속 보게 됨. 이로 인해 읽기 작업은 쓰기 작업을, 쓰기 작업은 읽기 작업을 막지 않음. (Non-blocking reads)
  • 장점: 잠금을 사용하는 것보다 동시성 처리 성능이 훨씬 좋음. ‘읽기’를 위해 기다릴 필요가 없기 때문.
  • 단점: 오래된 버전의 데이터를 계속 유지해야 하므로 저장 공간이 더 필요하고, 시스템이 주기적으로 불필요한 버전의 데이터를 정리(Vacuum)해야 하는 부담이 있음.

5. 현대 시스템과 트랜잭션: 분산 환경의 도전

전통적인 트랜잭션은 하나의 데이터베이스 시스템 내에서 동작하는 것을 전제로 함. 하지만 MSA(마이크로서비스 아키텍처)와 같이 시스템이 여러 개로 분산된 환경에서는 문제가 복잡해짐.

예를 들어, ‘주문’ 서비스와 ‘결제’ 서비스가 각각 별도의 데이터베이스를 가지고 있다고 가정. 주문 생성 트랜잭션은 ‘주문 DB’에 주문 정보를 기록하고, ‘결제 DB’에 결제 정보를 기록해야 함. 이 두 작업은 어떻게 원자적으로 묶을 수 있을까?

가. 2단계 커밋 (Two-Phase Commit, 2PC)

분산 트랜잭션을 위한 고전적인 프로토콜. 중앙의 **코디네이터(Coordinator)**가 모든 참여자(Participant)(각 서비스의 데이터베이스)에게 트랜잭션을 처리할 준비가 되었는지 묻고(1단계: Prepare), 모든 참여자가 ‘준비 완료’라고 응답하면 최종적으로 ‘커밋’하라고 명령(2단계: Commit).

  • 문제점: 코디네이터에 장애가 발생하면 전체 시스템이 멈출 수 있고, 모든 참여자가 응답할 때까지 잠금이 유지되어 성능이 저하됨. 현대적인 대규모 분산 시스템에서는 잘 사용되지 않음.

나. 사가 (Saga) 패턴

ACID를 완벽하게 보장하는 대신, 최종적인 일관성(Eventual Consistency)을 목표로 하는 패턴. 각 서비스는 자신의 로컬 트랜잭션을 수행하고, 성공하면 다음 서비스를 호출하는 이벤트를 발행. 만약 중간에 어느 서비스에서 작업이 실패하면, 이전 단계의 서비스들이 수행했던 작업을 취소하는 **보상 트랜잭션(Compensating Transaction)**을 실행.

  • 예시: 주문 서비스가 주문을 생성(로컬 트랜잭션) 결제 서비스 호출 결제 실패 주문 서비스가 ‘주문 취소’(보상 트랜잭션)를 실행.
  • 장점: 서비스 간 결합도가 낮아지고, 2PC보다 성능적으로 유리.
  • 단점: 구현이 복잡하고, 트랜잭션이 진행되는 중간에는 데이터가 일시적으로 비일관적인 상태에 놓일 수 있음.

결론: 신뢰할 수 있는 시스템의 초석

트랜잭션은 단순히 COMMITROLLBACK이라는 명령어에 국한된 개념이 아님. 그것은 데이터의 신뢰성을 지키기 위한 수십 년간의 고민과 기술적 진화가 담긴 철학. 원자성, 일관성, 고립성, 지속성이라는 네 개의 기둥이 받치고 있는 ACID 원칙은 오늘날에도 안정적인 시스템을 구축하는 데 있어 가장 기본적이고 중요한 원리.

개발자로서 우리는 트랜잭션의 동작 원리를 깊이 이해함으로써, 동시성 문제나 데이터 불일치와 같은 까다로운 버그를 예방하고, 사용자가 믿고 사용할 수 있는 견고한 애플리케이션을 만들 수 있음. 데이터가 존재하는 한, 트랜잭션의 중요성은 결코 변하지 않을 것.

레퍼런스(References)

트랜잭션 ACID 원칙