🗄️ 데이터베이스

Transaction

트랜잭션

논리적 작업 단위. ACID 속성. Commit/Rollback.

📖 상세 설명

트랜잭션이란?

트랜잭션(Transaction)은 데이터베이스의 상태를 변화시키는 하나의 논리적 작업 단위입니다. 여러 개의 연산이 모두 성공하거나, 모두 실패해야 하는 "전부 아니면 전무(All or Nothing)" 특성을 가집니다. 은행 이체, 주문 처리 등 데이터 무결성이 중요한 작업에 필수입니다.

ACID 속성

  • Atomicity (원자성): 트랜잭션 내 모든 연산이 완전히 수행되거나, 전혀 수행되지 않음
  • Consistency (일관성): 트랜잭션 전후로 데이터베이스가 일관된 상태를 유지
  • Isolation (격리성): 동시에 실행되는 트랜잭션들이 서로 영향을 주지 않음
  • Durability (지속성): 커밋된 트랜잭션의 결과는 영구적으로 반영됨

트랜잭션 격리 수준 (Isolation Level)

  • READ UNCOMMITTED: 커밋되지 않은 데이터 읽기 가능 (Dirty Read 발생)
  • READ COMMITTED: 커밋된 데이터만 읽기 (Non-Repeatable Read 발생)
  • REPEATABLE READ: 트랜잭션 중 같은 데이터 재조회 시 동일 결과 (Phantom Read 발생 가능)
  • SERIALIZABLE: 완전한 격리, 순차 실행과 동일 (성능 저하)

동시성 문제

  • Dirty Read: 다른 트랜잭션의 커밋되지 않은 데이터를 읽음
  • Non-Repeatable Read: 같은 쿼리가 다른 결과를 반환 (다른 트랜잭션이 수정)
  • Phantom Read: 범위 쿼리 결과가 달라짐 (다른 트랜잭션이 삽입/삭제)
  • Lost Update: 두 트랜잭션이 같은 데이터를 수정해 하나가 유실됨

분산 트랜잭션

  • 2PC (Two-Phase Commit): Prepare → Commit 단계로 분산 노드 간 원자성 보장
  • Saga 패턴: 각 서비스의 로컬 트랜잭션 + 보상 트랜잭션으로 Eventually Consistent
  • TCC (Try-Confirm-Cancel): 리소스 예약 후 확정/취소

💻 코드 예제

SQL 기본 트랜잭션

SQL
-- 계좌 이체 트랜잭션
BEGIN TRANSACTION;

-- 출금 계좌에서 차감
UPDATE accounts
SET balance = balance - 100000
WHERE account_id = 'A001' AND balance >= 100000;

-- 영향받은 행이 0이면 잔액 부족
-- 입금 계좌에 추가
UPDATE accounts
SET balance = balance + 100000
WHERE account_id = 'A002';

-- 이체 내역 기록
INSERT INTO transfer_log (from_account, to_account, amount, created_at)
VALUES ('A001', 'A002', 100000, NOW());

-- 모든 작업 성공 시 커밋
COMMIT;

-- 실패 시 롤백
-- ROLLBACK;

Python (psycopg2) 트랜잭션

Python
import psycopg2
from psycopg2 import sql

def transfer_money(from_account, to_account, amount):
    conn = psycopg2.connect(database="bank")
    try:
        with conn:
            with conn.cursor() as cur:
                # 출금 계좌 확인 및 차감 (FOR UPDATE로 행 잠금)
                cur.execute("""
                    SELECT balance FROM accounts
                    WHERE account_id = %s
                    FOR UPDATE
                """, (from_account,))

                balance = cur.fetchone()[0]
                if balance < amount:
                    raise ValueError("잔액 부족")

                # 출금
                cur.execute("""
                    UPDATE accounts SET balance = balance - %s
                    WHERE account_id = %s
                """, (amount, from_account))

                # 입금
                cur.execute("""
                    UPDATE accounts SET balance = balance + %s
                    WHERE account_id = %s
                """, (amount, to_account))

                # 로그 기록
                cur.execute("""
                    INSERT INTO transfer_log (from_acc, to_acc, amount)
                    VALUES (%s, %s, %s)
                """, (from_account, to_account, amount))

        # with 블록 종료 시 자동 COMMIT
        print("이체 완료")

    except Exception as e:
        # 예외 발생 시 자동 ROLLBACK
        print(f"이체 실패: {e}")
        raise

    finally:
        conn.close()

SQLAlchemy 트랜잭션

Python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine('postgresql://localhost/bank')
Session = sessionmaker(bind=engine)

def process_order(user_id, product_id, quantity):
    session = Session()
    try:
        # 재고 확인 및 감소
        product = session.query(Product).filter_by(id=product_id).with_for_update().one()

        if product.stock < quantity:
            raise ValueError("재고 부족")

        product.stock -= quantity

        # 주문 생성
        order = Order(
            user_id=user_id,
            product_id=product_id,
            quantity=quantity,
            total_price=product.price * quantity
        )
        session.add(order)

        # 결제 처리
        payment = Payment(order=order, status='completed')
        session.add(payment)

        session.commit()
        return order.id

    except Exception as e:
        session.rollback()
        raise

    finally:
        session.close()

격리 수준 설정

SQL
-- 세션 격리 수준 설정
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 트랜잭션별 설정
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- PostgreSQL: 현재 격리 수준 확인
SHOW transaction_isolation;

-- MySQL: 격리 수준 확인
SELECT @@transaction_isolation;

Saga 패턴 예시 (보상 트랜잭션)

Python
class OrderSaga:
    def __init__(self):
        self.compensations = []

    def execute(self, order_data):
        try:
            # Step 1: 재고 예약
            self.reserve_stock(order_data)
            self.compensations.append(lambda: self.release_stock(order_data))

            # Step 2: 결제 처리
            self.process_payment(order_data)
            self.compensations.append(lambda: self.refund_payment(order_data))

            # Step 3: 배송 요청
            self.request_shipping(order_data)
            self.compensations.append(lambda: self.cancel_shipping(order_data))

            # 모든 단계 성공
            return {"status": "success"}

        except Exception as e:
            # 실패 시 보상 트랜잭션 역순 실행
            for compensation in reversed(self.compensations):
                try:
                    compensation()
                except Exception as comp_error:
                    log.error(f"보상 실패: {comp_error}")

            raise SagaFailedException(str(e))

💬 현업 대화 예시

주니어 개발자
"트랜잭션 안에서 외부 API 호출해도 되나요? 결제 API 먼저 호출하고 DB 저장하려고요."
시니어 개발자
"절대 안 돼. 트랜잭션은 최대한 짧게. 외부 API는 트랜잭션 밖에서 호출하고, 결과에 따라 DB 작업 시작해. 트랜잭션 중에 타임아웃 나면 락 오래 잡아서 전체 시스템 마비될 수 있어."
백엔드 개발자
"동시에 같은 상품 주문이 들어오면 재고가 마이너스 되는 버그가 있어요."
DBA
"Race Condition이야. 재고 조회할 때 SELECT FOR UPDATE로 행 락 걸어. 아니면 UPDATE문에서 WHERE stock >= quantity 조건 추가해서 원자적으로 처리해."
아키텍트
"MSA에서 여러 서비스에 걸친 트랜잭션은 어떻게 해요? 2PC는 느리다던데..."
시니어 아키텍트
"2PC는 분산 환경에서 병목이야. Saga 패턴 써서 각 서비스별 로컬 트랜잭션 + 보상 로직으로 Eventually Consistent하게 가는 게 일반적이야. 이벤트 기반으로 조율하고."

⚠️ 주의사항

⚠️ 트랜잭션은 짧게
트랜잭션 중에는 락이 걸립니다. 오래 잡고 있으면 다른 트랜잭션이 대기하게 되어 성능 저하와 데드락 위험이 증가합니다. 외부 API 호출, 복잡한 계산은 트랜잭션 밖에서 처리하세요.
⚠️ 데드락 방지
여러 테이블/행을 잠글 때는 항상 같은 순서로 잠그세요. A→B 순서로 잠그는 트랜잭션과 B→A 순서의 트랜잭션이 동시에 실행되면 데드락 발생합니다. 타임아웃 설정도 필수.
⚠️ 격리 수준 선택
높은 격리 수준은 데이터 정합성을 보장하지만 동시성이 떨어집니다. 대부분의 경우 READ COMMITTED가 적당하며, 금융 등 정합성이 중요한 곳만 SERIALIZABLE을 고려하세요.
⚠️ 분산 트랜잭션의 한계
마이크로서비스에서 강력한 ACID는 달성하기 어렵습니다. Saga, TCC 등 Eventually Consistent 패턴을 사용하고, 멱등성(Idempotency)을 보장하는 설계가 중요합니다.

🔗 관련 용어

📚 더 배우기