2025-08-24 13:28
원자성 완벽 정복 핸드북 데이터 무결성의 수호자
데이터베이스의 ACID 원칙 중 하나인 원자성은 ‘모두 아니면 전무(All-or-Nothing)‘를 보장하여 데이터의 일관성과 무결성을 지키는 핵심 개념입니다. 이 핸드북은 원자성의 탄생 배경부터 동작 원리, 실제 사용 사례와 심화 주제까지 깊이 있게 다룹니다. 이를 통해 개발자와 데이터베이스 관리자는 신뢰할 수 있는 시스템을 구축하는 데 필요한 핵심 지식을 얻을 수 있습니다.
1. 원자성은 왜 세상에 나왔을까? (탄생 배경)
컴퓨터가 비즈니스에 막 도입되던 시절을 상상해 보세요. 은행에서 계좌 이체를 처리하는 간단한 작업을 예로 들어보겠습니다. A의 계좌에서 1만 원을 빼서 B의 계좌로 1만 원을 더하는 작업입니다.
이 작업은 사실 두 단계로 이루어집니다.
-
출금(Debit): A의 계좌에서 1만 원을 차감한다.
-
입금(Credit): B의 계좌에 1만 원을 더한다.
만약 1번 단계(출금)는 성공했는데, 바로 그 순간 정전이 되거나 시스템 오류가 발생해서 2번 단계(입금)가 실패하면 어떻게 될까요? A의 계좌에서는 1만 원이 사라졌지만, B의 계좌에는 돈이 들어오지 않은, 말 그대로 ‘공중으로 사라진 돈’이 발생합니다. 이는 데이터의 정합성이 깨진 심각한 문제입니다.
이러한 문제점을 해결하기 위해 **“관련된 작업들은 하나의 묶음(Transaction)으로 처리되어야 하며, 이 묶음은 전부 성공하거나 전부 실패해야 한다”**는 개념이 필요해졌습니다. 이것이 바로 **원자성(Atomicity)**의 탄생 배경입니다. 이름처럼, 더 이상 쪼갤 수 없는 하나의 원자(Atom)처럼 취급하겠다는 의미입니다. 계좌 이체라는 작업은 ‘출금’과 ‘입금’으로 쪼개서 볼 수 없고, ‘하나의 계좌 이체’라는 원자적 단위로만 존재해야 한다는 철학입니다.
2. 원자성의 구조와 동작 원리
원자성은 어떻게 ‘모두 아니면 전무’를 보장할 수 있을까요? 그 비밀은 데이터베이스의 트랜잭션(Transaction) 관리와 로그(Log) 시스템에 있습니다.
트랜잭션 (Transaction)
트랜잭션은 원자성을 보장하기 위한 작업의 논리적 단위입니다. 데이터베이스는 BEGIN TRANSACTION (또는 START TRANSACTION)이라는 명령어로 트랜잭션의 시작을 알리고, 관련된 모든 작업을 수행한 뒤 COMMIT 또는 ROLLBACK으로 트랜잭션을 마무리합니다.
-
COMMIT: 트랜잭션 내의 모든 작업이 성공적으로 완료되었음을 데이터베이스에 알립니다.
COMMIT이 실행되면 변경 사항이 영구적으로 데이터베이스에 반영됩니다. -
ROLLBACK: 트랜잭션 내의 작업 중 하나라도 실패하거나 문제가 발생했을 때, 트랜잭션 시작 이전의 상태로 모든 것을 되돌립니다. 즉, 지금까지의 모든 변경 사항을 취소합니다.
계좌 이체 예시를 트랜잭션으로 표현하면 다음과 같습니다.
BEGIN TRANSACTION;
-- 1. A 계좌에서 10000원 차감
UPDATE accounts SET balance = balance - 10000 WHERE user_id = 'A';
-- 2. B 계좌에 10000원 추가
UPDATE accounts SET balance = balance + 10000 WHERE user_id = 'B';
-- 모든 작업이 성공했으므로 영구 반영
COMMIT;만약 1번 작업 후 시스템에 문제가 생겨 2번 작업이 실행되지 못하면, 데이터베이스 관리 시스템(DBMS)은 이 트랜잭션이 COMMIT되지 않았음을 인지하고, 시스템이 복구되었을 때 자동으로 ROLLBACK을 실행하여 A의 계좌 잔액을 원래대로 되돌려 놓습니다.
로그 (Log) 시스템
데이터베이스는 모든 변경 사항을 실제로 디스크에 쓰기 전에, **로그(Log)**라는 특별한 파일에 먼저 기록합니다. 이를 Write-Ahead Logging (WAL) 정책이라고 합니다.
-
변경 시도: 트랜잭션이 데이터 변경을 시도합니다.
-
로그 기록: “A 계좌의 잔액을 100만 원에서 99만 원으로 변경할 예정”이라는 내용을 로그 버퍼(메모리)에 기록합니다.
-
로그 디스크 저장: 로그 버퍼의 내용이 디스크에 있는 로그 파일에 안전하게 저장됩니다.
-
실제 데이터 변경: 로그가 성공적으로 저장된 후에야 실제 데이터 파일(테이블)의 변경 작업이 메모리에서 이루어지고, 나중에 디스크에 반영됩니다.
만약 실제 데이터 변경 중에 시스템이 다운되더라도, 재시작 시 로그 파일을 보고 “이 트랜잭션은 COMMIT되지 않고 중간에 멈췄구나”를 파악할 수 있습니다. 그리고 로그에 기록된 이전 값을 바탕으로 데이터를 완벽하게 원상 복구(ROLLBACK)할 수 있습니다. COMMIT이 성공하면 로그에 “이 트랜잭션은 성공적으로 완료됨”이라는 기록을 남기고, 이 기록을 바탕으로 시스템 장애가 발생해도 변경 사항을 확실히 복구(REDO)할 수 있습니다.
| 단계 | 동작 | 설명 |
|---|---|---|
| 시작 | BEGIN TRANSACTION | 작업 묶음의 시작을 선언합니다. |
| 수행 | UPDATE, INSERT 등 | 데이터 변경 작업을 수행합니다. 변경 내용은 로그에 먼저 기록됩니다. |
| 문제 발생 | 시스템 장애, 제약 조건 위반 등 | 작업이 중간에 실패합니다. |
| 복구 (실패 시) | ROLLBACK | 로그를 참조하여 트랜잭션 시작 전 상태로 모든 데이터를 되돌립니다. |
| 완료 (성공 시) | COMMIT | 모든 작업이 성공했음을 로그에 기록하고, 변경 사항을 영구적으로 확정합니다. |
3. 원자성은 언제, 어떻게 사용될까?
원자성은 데이터의 무결성이 중요한 거의 모든 애플리케이션에서 필수적으로 사용됩니다.
대표적인 사용 사례
-
금융 시스템: 계좌 이체, 주식 거래, 결제 처리 등 돈과 관련된 모든 작업은 원자성이 보장되어야 합니다.
-
전자상거래: 주문 처리 시 재고 감소, 주문 내역 생성, 결제 정보 기록은 하나의 원자적 트랜잭션으로 묶여야 합니다. 재고는 줄었는데 주문이 생성되지 않으면 ‘유령 재고’가 발생합니다.
-
예약 시스템: 항공권이나 공연 티켓 예약 시, 좌석 선택과 결제는 동시에 성공하거나 실패해야 합니다. 좌석만 점유되고 결제가 안 되면 다른 사람이 그 좌석을 예약할 수 없게 됩니다.
-
콘텐츠 관리 시스템 (CMS): 블로그 글을 발행할 때, 글 내용 저장, 태그 정보 저장, 발행 상태 변경 등의 작업이 하나의 트랜잭션으로 처리되어야 합니다.
간단한 코드 예시 (Python + SQLAlchemy)
파이썬에서 데이터베이스를 다룰 때 ORM(Object-Relational Mapping) 라이브러리인 SQLAlchemy를 사용하면 원자성을 쉽게 구현할 수 있습니다.
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# 데이터베이스 연결 설정
engine = create_engine('postgresql://user:password@host:port/database')
Session = sessionmaker(bind=engine)
session = Session()
try:
# 트랜잭션 시작 (세션이 시작될 때 암묵적으로 시작됨)
# 1. A 계좌 객체 조회 및 잔액 차감
account_a = session.query(Account).filter_by(user_id='A').first()
if account_a.balance < 10000:
raise ValueError("잔액이 부족합니다.")
account_a.balance -= 10000
# 2. B 계좌 객체 조회 및 잔액 추가
account_b = session.query(Account).filter_by(user_id='B').first()
account_b.balance += 10000
# 모든 작업이 성공하면 COMMIT
session.commit()
print("계좌 이체 성공!")
except Exception as e:
# 예외 발생 시 ROLLBACK
session.rollback()
print(f"오류 발생: {e}. 계좌 이체가 취소되었습니다.")
finally:
# 세션 종료
session.close()
위 코드에서 try...except 블록이 원자성을 보장하는 핵심적인 역할을 합니다. try 블록 안의 모든 작업이 성공적으로 끝나야 session.commit()이 호출됩니다. 만약 잔액 부족, 데이터베이스 연결 끊김 등 어떤 종류의 예외라도 발생하면 except 블록으로 넘어가 session.rollback()이 호출되어 모든 변경 사항이 취소됩니다.
4. 더 깊이 알아보기 (심화 내용)
분산 시스템에서의 원자성 (2PC: Two-Phase Commit)
만약 계좌 이체가 서로 다른 은행, 즉 서로 다른 데이터베이스에서 이루어진다면 어떻게 될까요? A는 신한은행, B는 국민은행에 계좌가 있는 상황입니다. 신한은행 DB에서 출금은 성공했는데, 국민은행 DB에 입금하려니 네트워크 문제로 실패했습니다. 이 경우 단순한 ROLLBACK으로는 해결이 어렵습니다.
이런 분산 트랜잭션의 원자성을 보장하기 위해 2PC (Two-Phase Commit) 프로토콜이 사용됩니다. 조정자(Coordinator)가 참여자(Participants, 각 데이터베이스)들의 트랜잭션을 조율합니다.
-
1단계: 준비 (Prepare Phase)
-
조정자는 모든 참여자에게 “트랜잭션을 커밋할 준비가 되었는가?”라고 묻습니다.
-
각 참여자는 트랜잭션을 실행하고, 성공적으로 완료할 수 있으면 “준비 완료(Ready)“라고 응답하고 로그에 기록합니다. 만약 실패하면 “거절(Abort)“이라고 응답합니다.
-
-
2단계: 커밋 (Commit Phase)
-
조정자가 모든 참여자로부터 “준비 완료” 응답을 받으면, 모든 참여자에게 “커밋하라”는 명령을 보냅니다. 참여자들은 명령을 받고 트랜잭션을 영구적으로 커밋합니다.
-
만약 참여자 중 한 명이라도 “거절” 응답을 보내거나 시간 내에 응답이 없으면, 조정자는 모든 참여자에게 “롤백하라”는 명령을 보냅니다.
-
2PC는 모든 참여자가 동의해야만 트랜잭션을 완료함으로써 분산 환경에서도 원자성을 보장합니다. 하지만 조정자에 장애가 발생하면 전체 시스템이 멈추는 ‘블로킹’ 문제가 발생할 수 있어, 최근에는 Paxos, Raft 같은 합의 알고리즘이나 보상 트랜잭션(Saga 패턴) 등 다른 접근 방식도 많이 사용됩니다.
원자성과 격리 수준 (Isolation Level)
원자성은 주로 단일 트랜잭션의 성공/실패에 초점을 맞추지만, 다른 트랜잭션과의 상호작용을 다루는 **격리성(Isolation)**과도 밀접한 관련이 있습니다. 예를 들어, 격리 수준이 낮으면 A가 B에게 송금하는 트랜잭션이 아직 COMMIT되지 않았음에도 불구하고, 다른 트랜잭션이 B의 (아직 확정되지 않은) 증가된 잔액을 읽어가는 더티 리드(Dirty Read) 문제가 발생할 수 있습니다.
데이터베이스는 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE 같은 다양한 격리 수준을 제공하여 원자적으로 실행 중인 트랜잭션의 중간 결과를 다른 트랜잭션으로부터 얼마나 보호할지 결정할 수 있습니다.
5. 결론: 신뢰의 초석
원자성은 단순히 기술적인 개념을 넘어, 우리가 디지털 시스템을 신뢰할 수 있게 만드는 근본적인 약속입니다. “당신의 계좌 이체는 중간에 멈추지 않습니다. 성공하거나, 아니면 아예 없던 일이 되거나, 둘 중 하나입니다.”라는 약속 말입니다.
개발자로서 우리는 트랜잭션의 범위를 신중하게 설계하여 원자성을 올바르게 적용해야 합니다. 너무 작은 단위로 트랜잭션을 나누면 데이터 정합성이 깨질 수 있고, 반대로 너무 많은 작업을 하나의 트랜잭션으로 묶으면 시스템 성능에 부하를 줄 수 있습니다.
이 핸드북을 통해 원자성의 중요성을 이해하고, 여러분이 만드는 시스템이 더욱 견고하고 신뢰할 수 있게 되기를 바랍니다. 데이터의 세계에서 원자성은 질서와 안정을 유지하는 보이지 않는 수호자입니다.