2단계 커밋 프로토콜(2PC)이란 무엇이며, 어떻게 동작하나요?

김상욱·2024년 12월 15일
0

2단계 커밋 프로토콜(2PC)이란 무엇이며, 어떻게 동작하나요?

2단계 커밋 프로토콜(2PC, Two-Phase Commit Protocol)은 분산 시스템에서 트랜잭션의 원자성을 보장하기 위해 사용되는 분산 트랜잭션 관리 기법입니다. 주로 여러 데이터베이스나 시스템이 참여하는 트랜재션에서 모든 참여자가 트랜잭션을 성공적으로 완료하거나 모두 취소하도록 조정하는 데 사용됩니다. 2PC는 두 단계로 구성되며, 트랜잭션 코디네이터(Coordinator)와 여러 참여자(Participants) 간의 통신을 통해 동작합니다.

구성 요소

  • Coordinator : 트랜잭션을 관리하고 전체 프로세스를 조정하는 역할
  • Participants : 트랜잭션에 참여하는 각 데이터베이스나 시스템. 각 참여자는 트랜잭션의 일부분을 처리.

동작 방식
1단계 : 준비(Prepare) 단계

  • 클라이언트가 트랜잭션을 시작하면 코디네이터가 트랜잭션을 관리하기 시작.
  • 코디네이터는 모든 참여자에게 Prepare 메시지를 보냅니다. 이 메시지는 트랜잭션을 커밋할 준비가 되었는지 확인하기 위한 것입니다.
  • 각 참여자는 로컬 트랜잭션을 준비하고, 트랜잭션을 준비가 되면 Prepared 메시지를 코디네이터에게 반환합니다. 만약 문제가 발생하여 트랜잭션을 커밋할 수 없으면 Abort 메시지를 보냅니다.
    2단계 : 커밋 또는 중단(Commit or Abort) 단계
  • 코디네이터는 모든 참여자로부터 Prepared 메시지를 받으면 트랜잭션을 커밋하기로 결정합니다. 하나라도 Abort 메시지를 받으면 트랜잭션을 중단하기로 결정.
  • 코디네이터는 모든 참여자에게 Commit 또는 Abort 메시지를 보냅니다.
  • 참여자들은 받은 메시지에 따라 트랜잭션을 최종적으로 커밋하거나 롤백합니다. 이후 완료 메시지를 코디네이터에게 보냅니다.

2단계 커밋의 장점

  • 모든 참여자가 트랜잭션을 일관되게 커밋하거나 중단하도록 보장하여 원자성을 유지합니다.
  • 구현이 비교적 간단하고 이해하기 쉽습니다.

2단계 커밋의 단점과 한계

  • 블로킹 문제 : 코디네이터나 참여자가 실패할 경우, 다른 참여자들이 오랫동안 대기 상태에 머물 수 있습니다.
  • 단일 장애 지점 : 코디네이터가 실패하면 전체 트랜잭션이 중단되거나 복구가 어려울 수 있습니다.
  • 성능 저하 : 모든 참여자 간의 통신이 필요하므로 네트워크 지연이나 오버헤드가 발생할 수 있습니다.
  • 확장성 제한 : 참여자의 수가 많아질수록 관리가 복잡해지고 성능이 저하될 수 있습니다.

2단계 커밋의 사용 사례

  • 분산 데이터베이스 : 여러 데이터베이스 간의 일관된 트랜잭션을 보장해야 하는 경우
  • 마이크로서비스 아키텍쳐 : 여러 서비스가 하나의 트랜잭션에 참여할 때
  • 금융시스템 : 트랜잭션의 일관성과 원자성이 매우 중요한 금융거래 처리

대안 및 개선 방안

  • 3PC : 2PC의 블로킹 문제를 해결하기 위해 추가적이 단계와 타임아웃 메커니즘을 도입한 프로토콜
  • TCC (Try-Confirm/Cancel) : 트랜잭션을 미리 시도해보고, 성공 시 확정하거나 실패 시 취소하는 방식으로 2PC의 한계를 보완
  • 분산 합의 알고리즘 : Paxos나 Raft 같은 합의 알고리즘을 사용하여 트랜잭션의 일관성을 유지.

취업 준비 중인 신입 Java 및 Spring 백엔드 개발자라면, 2단계 커밋 프로토콜(2PC)과 같은 분산 트랜잭션 관리 기법을 이해하고 실습해보는 것은 매우 유익합니다. 이를 통해 분산 시스템에서의 트랜잭션 관리, 데이터 일관성 유지, 그리고 관련 기술 스택에 대한 깊은 이해를 쌓을 수 있습니다. 아래는 실습을 통해 2PC를 학습하고 경험을 쌓을 수 있는 몇 가지 프로젝트와 단계별 가이드입니다.

1. 기본 개념 이해 및 준비

실습을 시작하기 전에 2PC의 기본 개념과 관련 기술에 대한 이해를 확고히 하는 것이 중요합니다. 이미 2PC에 대한 이론적 배경은 알고 계시므로, 이를 실제 코드와 환경에 적용하는 단계로 넘어가겠습니다.

2. 분산 트랜잭션 환경 구축

A. 필요한 도구 및 기술 스택

  • Java 17+: 최신 Java 버전을 사용하여 최신 기능과 호환성을 확보합니다.
  • Spring Boot: 빠른 개발과 설정을 위해 사용합니다.
  • JTA (Java Transaction API): 분산 트랜잭션 관리를 위한 표준 API입니다.
  • Atomikos 또는 Bitronix: JTA 구현체로, 2PC를 지원합니다.
  • 두 개 이상의 데이터베이스: 예를 들어, 두 개의 MySQL 데이터베이스 또는 하나의 MySQL과 하나의 PostgreSQL 등.
  • Maven 또는 Gradle: 프로젝트 관리 도구로 사용합니다.
  • Docker: 데이터베이스 컨테이너를 쉽게 관리하기 위해 사용합니다.

B. 프로젝트 설정

  1. Spring Boot 프로젝트 생성

    • Spring Initializr를 사용하여 Spring Boot 프로젝트를 생성합니다.
    • 필요한 의존성:
      • Spring Web
      • Spring Data JPA
      • Atomikos (또는 Bitronix)
      • JDBC 드라이버 (예: MySQL, PostgreSQL)
  2. 데이터베이스 설정

    • Docker를 사용하여 두 개 이상의 데이터베이스 컨테이너를 실행합니다.

    • 예시 (MySQL 두 개):

      docker run --name db1 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=testdb1 -p 3306:3306 -d mysql:latest
      docker run --name db2 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=testdb2 -p 3307:3306 -d mysql:latest
    • 각 데이터베이스에 테이블을 생성합니다.

      -- db1.test_table1
      CREATE TABLE test_table1 (
          id INT PRIMARY KEY AUTO_INCREMENT,
          name VARCHAR(50)
      );
      
      -- db2.test_table2
      CREATE TABLE test_table2 (
          id INT PRIMARY KEY AUTO_INCREMENT,
          description VARCHAR(100)
      );

3. 분산 트랜잭션 구현

A. Atomikos 설정 (JTA 구현체)

  1. 의존성 추가 (pom.xml)

    <dependencies>
        <!-- Other dependencies -->
        <dependency>
            <groupId>com.atomikos</groupId>
            <artifactId>transactions-jta</artifactId>
            <version>5.0.8</version>
        </dependency>
        <dependency>
            <groupId>com.atomikos</groupId>
            <artifactId>transactions-jdbc</artifactId>
            <version>5.0.8</version>
        </dependency>
        <!-- JDBC 드라이버 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
    </dependencies>
  2. 데이터 소스 설정 (application.properties 또는 application.yml)

    # DB1 설정
    spring.jta.atomikos.datasource.db1.unique-resource-name=db1
    spring.jta.atomikos.datasource.db1.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
    spring.jta.atomikos.datasource.db1.xa-properties.url=jdbc:mysql://localhost:3306/testdb1
    spring.jta.atomikos.datasource.db1.xa-properties.user=root
    spring.jta.atomikos.datasource.db1.xa-properties.password=root
    spring.jta.atomikos.datasource.db1.min-pool-size=5
    spring.jta.atomikos.datasource.db1.max-pool-size=10
    
    # DB2 설정
    spring.jta.atomikos.datasource.db2.unique-resource-name=db2
    spring.jta.atomikos.datasource.db2.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
    spring.jta.atomikos.datasource.db2.xa-properties.url=jdbc:mysql://localhost:3307/testdb2
    spring.jta.atomikos.datasource.db2.xa-properties.user=root
    spring.jta.atomikos.datasource.db2.xa-properties.password=root
    spring.jta.atomikos.datasource.db2.min-pool-size=5
    spring.jta.atomikos.datasource.db2.max-pool-size=10
    
    # JPA 설정
    spring.jpa.properties.hibernate.transaction.factory_class=com.atomikos.icatch.jta.hibernate4.AtomikosJTATransactionFactory
    spring.jpa.properties.hibernate.transaction.jta.platform=org.hibernate.engine.transaction.jta.platform.internal.AtomikosJtaPlatform
    spring.jpa.hibernate.ddl-auto=update
  3. JPA 엔티티 및 리포지토리 생성

    • DB1용 엔티티

      @Entity
      @Table(name = "test_table1")
      public class TestTable1 {
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Long id;
      
          private String name;
      
          // Getters and Setters
      }
    • DB2용 엔티티

      @Entity
      @Table(name = "test_table2")
      public class TestTable2 {
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Long id;
      
          private String description;
      
          // Getters and Setters
      }
    • 리포지토리 인터페이스

      public interface TestTable1Repository extends JpaRepository<TestTable1, Long> {}
      public interface TestTable2Repository extends JpaRepository<TestTable2, Long> {}
  4. 서비스 계층에서 분산 트랜잭션 구현

    @Service
    public class DistributedService {
    
        @Autowired
        private TestTable1Repository table1Repository;
    
        @Autowired
        private TestTable2Repository table2Repository;
    
        @Transactional
        public void performDistributedTransaction(String name, String description) {
            TestTable1 entity1 = new TestTable1();
            entity1.setName(name);
            table1Repository.save(entity1);
    
            TestTable2 entity2 = new TestTable2();
            entity2.setDescription(description);
            table2Repository.save(entity2);
    
            // 예외 발생 시 트랜잭션 롤백 확인
            if (name.equals("rollback")) {
                throw new RuntimeException("Forced rollback");
            }
        }
    }
  5. 컨트롤러 생성

    @RestController
    @RequestMapping("/api")
    public class DistributedController {
    
        @Autowired
        private DistributedService distributedService;
    
        @PostMapping("/transaction")
        public ResponseEntity<String> executeTransaction(@RequestParam String name, @RequestParam String description) {
            try {
                distributedService.performDistributedTransaction(name, description);
                return ResponseEntity.ok("Transaction Successful");
            } catch (Exception e) {
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Transaction Failed: " + e.getMessage());
            }
        }
    }

B. 실습 시나리오

  1. 성공적인 분산 트랜잭션 실행

    • POST /api/transaction?name=John&description=Test
    • 결과: 두 데이터베이스에 각각 데이터가 정상적으로 저장됩니다.
  2. 트랜잭션 롤백 시나리오

    • POST /api/transaction?name=rollback&description=Test
    • 결과: 예외가 발생하여 두 데이터베이스 모두에 데이터가 저장되지 않습니다.

4. 2PC 동작 이해를 위한 시뮬레이션

A. 트랜잭션 준비 및 커밋/롤백 단계 시뮬레이션

  • Prepare 단계 시뮬레이션

    • Atomikos는 내부적으로 2PC를 관리하지만, 로깅을 통해 각 단계의 동작을 확인할 수 있습니다.
    • application.properties에 로그 레벨을 설정하여 상세 로그를 확인합니다.
      logging.level.com.atomikos=DEBUG
      logging.level.org.hibernate=DEBUG
  • 장애 상황 시뮬레이션

    • 트랜잭션 중간에 데이터베이스를 중지시켜 블로킹 문제를 관찰합니다.
    • 예를 들어, 트랜잭션 실행 중 Docker 컨테이너를 중지시켜 코디네이터의 반응을 확인합니다.

5. 추가 실습 및 확장

A. 마이크로서비스 아키텍처 적용

  • 서비스 분리

    • 두 개 이상의 Spring Boot 애플리케이션을 만들어 각각 다른 데이터베이스에 접근하도록 설정합니다.
    • 서비스 간의 트랜잭션을 관리하기 위해 2PC를 적용합니다.
  • API 게이트웨이 및 서비스 디스커버리

    • Spring Cloud Netflix Eureka와 Spring Cloud Gateway를 사용하여 서비스 간의 통신을 관리합니다.
    • 분산 트랜잭션이 여러 서비스에 걸쳐 올바르게 작동하는지 확인합니다.

B. 장애 복구 및 트랜잭션 로그 분석

  • 로그 분석

    • Atomikos의 로그를 분석하여 트랜잭션의 각 단계를 이해합니다.
    • 장애 상황 시 로그를 통해 문제를 진단하고 복구 방법을 학습합니다.
  • 재시도 메커니즘 구현

    • 트랜잭션 실패 시 자동 재시도 로직을 추가하여 시스템의 견고성을 높입니다.

6. 학습 자료 및 참고 링크

  • 공식 Atomikos 문서: Atomikos Documentation
  • Spring JTA 트랜잭션 관리 가이드: Spring Documentation on JTA
  • 분산 트랜잭션 튜토리얼: 여러 블로그와 유튜브 튜토리얼에서 2PC 및 JTA 구현 사례를 찾아보세요.
  • 책 추천: "Spring in Action"이나 "Java Concurrency in Practice" 등 관련 서적을 통해 심화 학습.

7. 면접 대비 팁

  • 이론과 실습 병행: 2PC의 이론적 이해뿐만 아니라 실제 구현 경험을 바탕으로 설명할 수 있도록 준비합니다.
  • 문제 해결 경험 공유: 실습 중 겪은 문제와 이를 어떻게 해결했는지에 대한 경험을 정리해 두세요.
  • 관련 용어 숙지: XA, JTA, 분산 트랜잭션, 코디네이터, 참여자 등 관련 용어를 정확히 이해하고 사용할 수 있도록 합니다.
  • 대안 기술 이해: 2PC의 한계와 이를 보완하는 3PC, TCC, 분산 합의 알고리즘 등에 대한 기본적인 이해도 갖추세요.

결론

2단계 커밋 프로토콜(2PC)은 분산 시스템에서 데이터 일관성을 유지하는 중요한 기법입니다. 위에서 제시한 실습 과정을 통해 Java와 Spring 환경에서 2PC를 직접 구현하고 동작 방식을 깊이 이해할 수 있습니다. 이러한 실습 경험은 면접 시 분산 트랜잭션 관리에 대한 이해도를 효과적으로 어필하는 데 큰 도움이 될 것입니다. 꾸준한 실습과 이론 학습을 통해 탄탄한 백엔드 개발자로 성장하시길 바랍니다.

0개의 댓글

관련 채용 정보