본문 바로가기
프로그래밍 기법

2PC(Two Phase Commit)와 2PL(Two Phase Locking) 차이점

by Marco Backman 2025. 10. 7.

 

해당 책의 7장, Transaction에 대해 읽으면서 2PC(Two Phase Commit)와 2PL(Two Phase Locking)에 대한 용어를 접했지만 확 와닿지 않아서 다시 정리해 올려봅니다.

 

이름은 비슷해 보여도 DB 시스템에서 각기 다른 문제를 해결하기 위해 존재하는 프로토콜입니다.


Two-Phase Locking (2PL): 병렬 제어

 

목표: DB의 Serializability를 충족하기 위해 사용

활용: 병렬 Transaction에서 동시성 Read-Write으로 인해 데이터 불일치 방지

설명: 다수의 db transaction 이 병렬적으로 실행되어도 최종 결과물은 순차적으로 실행 된 것 처럼 항상 같아야 합니다. 결국 병렬이 아닌것과 같습니다.

 

단일 DB 서버 환경에서 2PL은 이름과 같이 두개의 단계를 적용합니다.

1. 확장 단계 (Growing Phase): 초기 단계. Transaction은 접근하려는 데이터에 락(lock)을 걸 수 있습니다.

2. 축소 단계 (Shrinking Phase): Transaction이 초기 락을 해제하면 축소 단계에 들어갑니다. 해당 단계에서는 lock을 해제만 하며 새로운 lock은 축소 단계가 끝날 때 까지 하지 않습니다( deadlock 방지 ).

 

특징: Select에서 For Update를 사용합니다.

예시: 가장 대표적인 예시로 은행 계좌의 돈을 다른 계좌로 전송할 때의 예시 코드입니다.

-- ===================================================
-- Transaction 1: $100의 금액을 일반 예금을 적금으로 보낼 때
-- ===================================================

BEGIN TRANSACTION;

-- ### 확장 단계 시작 ###
-- transaction 이 필요한 데이터에 lock을 겁니다.
-- Smith의 적금 계좌 데이터 행에 락을 겁니다. (Account ID 123)
SELECT balance FROM accounts WHERE account_id = 123 FOR UPDATE; -- Lock #1

-- 추가 비즈니스 로직 실행

-- Smith의 예금 계좌 데이터 행에 락을 겁니다. (Account ID 456)
SELECT balance FROM accounts WHERE account_id = 456 FOR UPDATE; -- Lock #2

-- 데이터 업데이트
UPDATE accounts SET balance = balance - 100 WHERE account_id = 123;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 456;

-- ### 축소 단계 시작 ###
-- COMMIT 명령이 모든 lock을 동시에 해제합니다. 해제 중에는 해당 Transaction은 lock을 걸지 못합니다.
COMMIT; -- All locks are released.

 

단점: Deadlock의 가능성 존재, 만약 우연찮게 동시에 두개의 Transaction이 같은 프로세스를 실행 했을때 각각 Transaction이 사용해야 할 data가 release될 때까지 대기해야할 때 서로 무한히 기다리는 위험성이 존재합니다. 그리고 이를 해결하기 위해 순차적프로세스를 적용하면 (Serailizability) 당연히 병렬성을 포기하는 것이라 동시 처리의 이점이 사라집니다.

 

적용 사례: 처리 속도와 병렬성을 포기하더라도 데이터의 무결성이 반드시 보장되어야 하는 상황에 사용.


Two-Phase Commit (2PC):  분산 Transaction 독립 적용 (원자성)

목표: 분산 시스템(다수의 독립적 DB)에서 각 Transaction의 원자성을 보장하기위해 사용.

활용: 해당 기능은 다른 위치나에 상주하는 DB나 파티셔닝 된 DB의 데이터 무결성을 보장

설명: 각 Transaction은 완전히 성공하거나 완전히 실패(Rollback) 적용. 그러나 중재자가 필요하므로 Application 단에서 제어를 해주어야 합니다. (Script 만으로 처리 불가)

 

2PC는 다음과 같은 단계를 거칩니다.

 

1. 투표(준비) 단계 (Prepare Phase): 초기 단계에서 중재자는 모든 관련된 참여자(독자적 DB transactions)에게 준비를 요청합니다. 그러면 모든 참여자가 요청한 실행문에 대해 변경사항이나 실행 사항을 로그에 저장하고 준비에 대한 응답을 성공적으로 합니다. 하나라도 응답이 없거나 실패를 보내면 모든 Transation이 취소됩니다.

 

2. 적용 단계 (Commit Phase): 모든 응답을 성공적으로 받으면 모든 참여자에게 Commit 요청을 보냅니다. Commit 단계에서도 실패가 하나라도 돌아오게 되면 log에 저장된 이전 기록을 토대로 Rollback 단계에 돌입하게 됩니다.

// 해당 Transaction은 고객의 주문정보와 운송정보를 DB에 업데이트합니다
// 여기서 중재자'coordinator' 역활은 어플리케이션의 Transaction Manager 코드가 담당합니다.

// --- Databases (참여자) ---
const ordersDB = new Database("orders_db_connection");
const shippingDB = new Database("shipping_db_connection");

// ===================================
// 중재자 논리
// ===================================

async function executeDistributedTransaction() {
  try {
    console.log("--- PHASE 1: PREPARE ---");
    // 1. 중재자가 각 DB에 준비단계를 선언합니다.
    const vote1 = await ordersDB.prepare("UPDATE orders SET status = 'paid' WHERE order_id = 789;");
    const vote2 = await shippingDB.prepare("UPDATE shipping SET status = 'ready' WHERE order_id = 789;");

    console.log("Votes received:", vote1, vote2);

    // 2. 중재자는 각 응답을 확인합니다.
    if (vote1 === "VOTE_COMMIT" && vote2 === "VOTE_COMMIT") {
      console.log("--- PHASE 2: COMMIT ---");
      // 만약 모든 것이 성공적이면 commit 실행
      await ordersDB.commit();
      await shippingDB.commit();
      console.log("✅ Transaction committed successfully across all databases!");
    } else {
      throw new Error("A participant voted to abort.");
    }

  } catch (error) {
    console.error("🚨 Transaction failed:", error.message);
    console.log("--- PHASE 2: ABORT ---");
    // 만약 한곳이라도 이상이 생기면 모든 작업 rollback
    await ordersDB.rollback();
    await shippingDB.rollback();
    console.log("Transaction rolled back on all databases.");
  }
}

executeDistributedTransaction();

 

단점: 2PC는 Blocking 프로토콜 입니다. 따라서 중재자가 commit을 전송하기 전에 모든 참여자가 준비 완료 상태를 보내도 이때 네트워크 문제로 인해 응답을 받지 못하게 되면 계속 참여자의 응답을 기다리고 결국 timeout조건을 부여해도 속도가 전반적으로 느려집니다. 결국 중재자 단일 실패 지점의 문제점이 존재합니다.

 

적용 사례: 다른 DB 혹은 테이블의 수정이 병렬적으로 동시에 일어나는 처리를 요구할 때

 


여담: 2PC는 SAGA 패턴인가?

2PC를 작성하면서 느끼는것은 Rollback 기능과 작동 방식이 SAGA 패턴과 유사하여 혹시 똑같은 것인가 하고 생각했었다.

 

하지만 자료 조사를 한 결과 서로 다른것으로 판정이 났다. 목적과 방식 자체가 다르기 때문이다. 

 

  • 2PC: 원자성(Atomicity)를 목표. Transactional ACID를 모든 DB transaction에 관련된 데이터를 lock을 사용하여 달성.
  • SAGA: 분산 시스템에서 Atomicity를 보장하는것이 이론상 어렵움. 따라서 Enventual Consistency를 지향.

 

더 쉽게 말해 2PC는 데이터 무결성을 원천에 차단하는 것이고 SAGA는 논리적으로 무결성을 명령 처리 이후에 이루는것이다.

 

기술적으로 설명하면 2PC는 동기적 Blocking 방식의 프로토콜이고 SAGA는 비동기식, non-blocking형 패턴이다.


Reference

 

 

해당 블로그 내용도 참고하면 좋을 것 같습니다.

https://velog.io/@ch200203/MSA-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EB%B6%84%EC%82%B0-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B4%80%EB%A6%AC2PC-SAGA-%ED%8C%A8%ED%84%B4

 

MSA 환경에서의 분산 트랜잭션 관리: 2PC & SAGA 패턴(이론편)

MSA 환경을 경험해보면서 여러개로 분산되어진 DB들이 어떻게 트랜잭션을 관리하고 데이터 일관성을 유지 할 수 있을까? 라는 생각이 들어 내용을 찾아보고 정리해보고자 합니다. 분산 트랜잭션

velog.io