본문으로 건너뛰기

1. 분산 트랜잭션의 이해

  • 분산 트랜잭션은 2개 그 이상의 네트워크 상의 시스템 간의 트랜잭션입니다.
  • 일반적으로 시스템은 트랜잭션 리소스의 역할을 하고, 트랜잭션 매니저는 이러한 리소스에 관련된 모든 동작에 대해 트랜잭션의 생성 및 관리를 담당합니다.

트랜잭션 리소스(Transaction Resource)

  • 데이터베이스, 메시지 큐 등 실제 데이터를 저장하고 처리하는 시스템
  • 로컬에서 커밋이나 롤백 등의 트랜잭션 연산을 수행할 수 있는 능력 보유
  • 예: MySQL 데이터베이스, RabbitMQ 메시지 브로커, MongoDB 등

트랜잭션 매니저(Transaction Manager)

  • 여러 트랜잭션 리소스들을 조율하여 하나의 논리적 트랜잭션으로 관리
  • 전체 트랜잭션의 시작, 커밋, 롤백을 결정하고 실행
  • 각 리소스의 상태를 모니터링하고 문제 발생 시 복구 처리
  • 예: Java의 JTA 트랜잭션 매니저, 분산 트랜잭션 코디네이터

1.1 마이크로서비스 아키텍처와 분산 트랜잭션

  • 마이크로서비스 아키텍처(MSA)는 대표적인 분산 트랜잭션 환경입니다.
  • MSA의 주요 특징 중 하나인 'Database per Service' 패턴으로 인해 다음과 같은 장점을 제공합니다:
    • 각 서비스는 독립적인 데이터베이스에서 자신의 데이터를 관리
    • 서비스별로 최적화된 데이터베이스 선택 가능 (예: 주문 이력은 MongoDB, 결제는 MySQL)
    • 다른 서비스의 장애로부터 데이터 보호
    • 서비스 간 낮은 결합도 유지
  • 하지만 이러한 데이터베이스 분리는 비즈니스 트랜잭션 처리를 복잡하게 만듭니다.

2. 분산 트랜잭션의 어려움

2.1 ACID 속성 유지의 어려움

  • 마이크로서비스 환경에서 트랜잭션은 두 가지 관점에서 볼 수 있습니다:

로컬 트랜잭션 (각 서비스 내부)

  • 각 서비스는 자신의 데이터베이스에 대해 ACID 특성을 보장할 수 있음
  • 예: 주문 서비스 내에서 주문 테이블에 데이터를 추가하는 트랜잭션은 ACID 보장

비즈니스 트랜잭션 (여러 서비스에 걸친 전체 흐름)

  • 여러 서비스의 로컬 트랜잭션들이 모여 하나의 논리적인 트랜잭션을 구성
  • 각각의 로컬 트랜잭션은 ACID를 보장하지만, 전체 비즈니스 트랜잭션은 ACID 보장이 어려움
  • 예: 주문 생성 → 결제 처리 → 재고 감소 → 배송 정보 생성 과정에서 중간에 실패하면 일부 데이터만 반영되는 문제 발생

2.2 실제 사례로 보는 문제점

상품 주문 프로세스를 예로 들어보겠습니다:

  1. 여러 서비스가 관여하며 서비스 별로 Database가 분리되어 있습니다
    • 주문 서비스: 주문 정보 생성
    • 재고 서비스: 상품 재고 감소
    • 결제 서비스: 결제 처리
    • 배송 서비스: 배송 정보 생성
  2. 이 모든 작업은 하나의 '주문' 프로세스에서 전부 성공하거나 전부 실패해야 합니다
    • 결제가 실패하면 재고도 원복되어야 함
    • 배송 생성이 실패하면 결제도 취소되어야 함
    • 어느 한 단계라도 실패하면 모든 것이 처음 상태로 돌아가야 함
  • 이처럼 여러 서비스에 걸친 데이터 일관성을 보장하기 위해 분산 트랜잭션이 필요합니다.
  • 단일 데이터베이스였다면 하나의 트랜잭션으로 해결될 문제가, 데이터베이스가 분리되면서 더 복잡한 분산 트랜잭션 처리가 요구되는 것입니다.

3. 2단계 커밋(2-Phase Commit)

3.1 2단계 커밋 개요

  • 2단계 커밋(2PC)은 분산 트랜잭션 환경에서 데이터 일관성을 보장하기 위한 프로토콜입니다.
  • X/Open의 분산 트랜잭션 표준인 XA를 따르며, Java에서는 JTA(Java Transaction API)를 통해 구현됩니다.

대표적인 JTA 구현체로는 Atomikos와 Bitronix가 있습니다.

3.2 동작 프로세스

1단계 - Prepare Phase (투표 단계)

  1. 코디네이터가 모든 참여자(데이터베이스 노드)에게 커밋 준비 요청을 전송합니다.
  2. 각 참여자는 다음 작업을 수행합니다:
    • 트랜잭션을 실행하여 커밋 가능한 상태까지 준비
    • undo log와 redo log를 기록
    • 필요한 락(lock)을 획득하여 다른 트랜잭션의 간섭 방지
  3. 각 참여자는 코디네이터에게 응답을 전송합니다:
    • 성공 시: 'Yes' 응답 (커밋 가능)
    • 실패 시: 'No' 응답 (커밋 불가능)

2단계 - Commit Phase (완료 단계)

성공 시나리오 (모든 참여자가 'Yes' 응답)

  1. 코디네이터가 모든 참여자에게 commit 메시지 전송
  2. 각 참여자는:
    • 트랜잭션을 최종 커밋
    • 모든 락과 리소스 해제
    • 코디네이터에게 완료 응답 전송
  3. 코디네이터는 모든 완료 응답을 받으면 트랜잭션 종료

실패 시나리오 (하나라도 'No' 응답 또는 타임아웃 발생)

  1. 코디네이터가 모든 참여자에게 rollback 메시지 전송
  2. 각 참여자는:
    • undo log를 사용하여 트랜잭션 롤백
    • 모든 락과 리소스 해제
    • 코디네이터에게 완료 응답 전송
  3. 코디네이터는 모든 완료 응답을 받으면 트랜잭션 종료
노트

각 참여자는 코디네이터의 최종 결정(commit/rollback)을 수신할 때까지 리소스를 점유한 상태로 대기합니다. 네트워크 장애 등으로 결정을 수신하지 못한 경우, 코디네이터에게 직접 질의하여 최종 결정을 확인할 수 있습니다.

3.3 MSA 환경에서의 한계점

위험

2단계 커밋은 다음과 같은 이유로 MSA 환경에서 실용적이지 않습니다:

  • 코디네이터에 대한 과도한 의존성
    • 단일 실패 지점(Single Point of Failure) 위험
    • 빈번한 통신으로 인한 성능 저하
  • 전체 시스템 성능이 가장 느린 노드에 종속
  • NoSQL 데이터베이스와의 호환성 문제

4. 결론

  • 분산 트랜잭션은 마이크로서비스 아키텍처에서 피할 수 없는 도전 과제입니다.
  • 2단계 커밋과 같은 전통적인 해결책은 현대적인 마이크로서비스 환경에서 실용적이지 않을 수 있으며, 이는 새로운 패턴과 해결책의 필요성을 시사합니다.