2단계 커밋 프로토콜(2PC, Two-Phase Commit Protocol)은 분산 시스템에서 트랜잭션의 원자성을 보장하기 위해 사용되는 분산 트랜잭션 관리 기법입니다. 주로 여러 데이터베이스나 시스템이 참여하는 트랜재션에서 모든 참여자가 트랜잭션을 성공적으로 완료하거나 모두 취소하도록 조정하는 데 사용됩니다. 2PC는 두 단계로 구성되며, 트랜잭션 코디네이터(Coordinator)와 여러 참여자(Participants) 간의 통신을 통해 동작합니다.
구성 요소
동작 방식
1단계 : 준비(Prepare) 단계
2단계 커밋의 장점
2단계 커밋의 단점과 한계
2단계 커밋의 사용 사례
대안 및 개선 방안
취업 준비 중인 신입 Java 및 Spring 백엔드 개발자라면, 2단계 커밋 프로토콜(2PC)과 같은 분산 트랜잭션 관리 기법을 이해하고 실습해보는 것은 매우 유익합니다. 이를 통해 분산 시스템에서의 트랜잭션 관리, 데이터 일관성 유지, 그리고 관련 기술 스택에 대한 깊은 이해를 쌓을 수 있습니다. 아래는 실습을 통해 2PC를 학습하고 경험을 쌓을 수 있는 몇 가지 프로젝트와 단계별 가이드입니다.
실습을 시작하기 전에 2PC의 기본 개념과 관련 기술에 대한 이해를 확고히 하는 것이 중요합니다. 이미 2PC에 대한 이론적 배경은 알고 계시므로, 이를 실제 코드와 환경에 적용하는 단계로 넘어가겠습니다.
Spring Boot 프로젝트 생성
데이터베이스 설정
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)
);
의존성 추가 (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>
데이터 소스 설정 (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
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> {}
서비스 계층에서 분산 트랜잭션 구현
@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");
}
}
}
컨트롤러 생성
@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());
}
}
}
성공적인 분산 트랜잭션 실행
POST /api/transaction?name=John&description=Test
트랜잭션 롤백 시나리오
POST /api/transaction?name=rollback&description=Test
Prepare 단계 시뮬레이션
application.properties
에 로그 레벨을 설정하여 상세 로그를 확인합니다.logging.level.com.atomikos=DEBUG
logging.level.org.hibernate=DEBUG
장애 상황 시뮬레이션
서비스 분리
API 게이트웨이 및 서비스 디스커버리
로그 분석
재시도 메커니즘 구현
2단계 커밋 프로토콜(2PC)은 분산 시스템에서 데이터 일관성을 유지하는 중요한 기법입니다. 위에서 제시한 실습 과정을 통해 Java와 Spring 환경에서 2PC를 직접 구현하고 동작 방식을 깊이 이해할 수 있습니다. 이러한 실습 경험은 면접 시 분산 트랜잭션 관리에 대한 이해도를 효과적으로 어필하는 데 큰 도움이 될 것입니다. 꾸준한 실습과 이론 학습을 통해 탄탄한 백엔드 개발자로 성장하시길 바랍니다.