2025-08-24 13:28

원자성 완벽 정복 핸드북 데이터 무결성의 수호자

데이터베이스의 ACID 원칙 중 하나인 원자성은 ‘모두 아니면 전무(All-or-Nothing)‘를 보장하여 데이터의 일관성과 무결성을 지키는 핵심 개념입니다. 이 핸드북은 원자성의 탄생 배경부터 동작 원리, 실제 사용 사례와 심화 주제까지 깊이 있게 다룹니다. 이를 통해 개발자와 데이터베이스 관리자는 신뢰할 수 있는 시스템을 구축하는 데 필요한 핵심 지식을 얻을 수 있습니다.

1. 원자성은 왜 세상에 나왔을까? (탄생 배경)

컴퓨터가 비즈니스에 막 도입되던 시절을 상상해 보세요. 은행에서 계좌 이체를 처리하는 간단한 작업을 예로 들어보겠습니다. A의 계좌에서 1만 원을 빼서 B의 계좌로 1만 원을 더하는 작업입니다.

이 작업은 사실 두 단계로 이루어집니다.

  1. 출금(Debit): A의 계좌에서 1만 원을 차감한다.

  2. 입금(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) 정책이라고 합니다.

  1. 변경 시도: 트랜잭션이 데이터 변경을 시도합니다.

  2. 로그 기록: “A 계좌의 잔액을 100만 원에서 99만 원으로 변경할 예정”이라는 내용을 로그 버퍼(메모리)에 기록합니다.

  3. 로그 디스크 저장: 로그 버퍼의 내용이 디스크에 있는 로그 파일에 안전하게 저장됩니다.

  4. 실제 데이터 변경: 로그가 성공적으로 저장된 후에야 실제 데이터 파일(테이블)의 변경 작업이 메모리에서 이루어지고, 나중에 디스크에 반영됩니다.

만약 실제 데이터 변경 중에 시스템이 다운되더라도, 재시작 시 로그 파일을 보고 “이 트랜잭션은 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)

    1. 조정자는 모든 참여자에게 “트랜잭션을 커밋할 준비가 되었는가?”라고 묻습니다.

    2. 각 참여자는 트랜잭션을 실행하고, 성공적으로 완료할 수 있으면 “준비 완료(Ready)“라고 응답하고 로그에 기록합니다. 만약 실패하면 “거절(Abort)“이라고 응답합니다.

  • 2단계: 커밋 (Commit Phase)

    1. 조정자가 모든 참여자로부터 “준비 완료” 응답을 받으면, 모든 참여자에게 “커밋하라”는 명령을 보냅니다. 참여자들은 명령을 받고 트랜잭션을 영구적으로 커밋합니다.

    2. 만약 참여자 중 한 명이라도 “거절” 응답을 보내거나 시간 내에 응답이 없으면, 조정자는 모든 참여자에게 “롤백하라”는 명령을 보냅니다.

2PC는 모든 참여자가 동의해야만 트랜잭션을 완료함으로써 분산 환경에서도 원자성을 보장합니다. 하지만 조정자에 장애가 발생하면 전체 시스템이 멈추는 ‘블로킹’ 문제가 발생할 수 있어, 최근에는 Paxos, Raft 같은 합의 알고리즘이나 보상 트랜잭션(Saga 패턴) 등 다른 접근 방식도 많이 사용됩니다.

원자성과 격리 수준 (Isolation Level)

원자성은 주로 단일 트랜잭션의 성공/실패에 초점을 맞추지만, 다른 트랜잭션과의 상호작용을 다루는 **격리성(Isolation)**과도 밀접한 관련이 있습니다. 예를 들어, 격리 수준이 낮으면 A가 B에게 송금하는 트랜잭션이 아직 COMMIT되지 않았음에도 불구하고, 다른 트랜잭션이 B의 (아직 확정되지 않은) 증가된 잔액을 읽어가는 더티 리드(Dirty Read) 문제가 발생할 수 있습니다.

데이터베이스는 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE 같은 다양한 격리 수준을 제공하여 원자적으로 실행 중인 트랜잭션의 중간 결과를 다른 트랜잭션으로부터 얼마나 보호할지 결정할 수 있습니다.

5. 결론: 신뢰의 초석

원자성은 단순히 기술적인 개념을 넘어, 우리가 디지털 시스템을 신뢰할 수 있게 만드는 근본적인 약속입니다. “당신의 계좌 이체는 중간에 멈추지 않습니다. 성공하거나, 아니면 아예 없던 일이 되거나, 둘 중 하나입니다.”라는 약속 말입니다.

개발자로서 우리는 트랜잭션의 범위를 신중하게 설계하여 원자성을 올바르게 적용해야 합니다. 너무 작은 단위로 트랜잭션을 나누면 데이터 정합성이 깨질 수 있고, 반대로 너무 많은 작업을 하나의 트랜잭션으로 묶으면 시스템 성능에 부하를 줄 수 있습니다.

이 핸드북을 통해 원자성의 중요성을 이해하고, 여러분이 만드는 시스템이 더욱 견고하고 신뢰할 수 있게 되기를 바랍니다. 데이터의 세계에서 원자성은 질서와 안정을 유지하는 보이지 않는 수호자입니다.

레퍼런스(References)

원자성