Akashic Records

Spring Data JPA - 분산 트랜잭션 본문

Spring.io

Spring Data JPA - 분산 트랜잭션

Andrew's Akashic Records 2024. 11. 26. 15:08
728x90

Spring Data JPA(Spring Data Java Persistence API)

 

분산 트랜잭션 

JPA에서 2단계로 구성된 데이터베이스 트랜잭션 설정은 일반적으로 2단계 커밋 (Two-Phase Commit) 또는 분산 트랜잭션 (Distributed Transaction)이라고 부르며, 하나 이상의 데이터베이스 또는 시스템 간의 트랜잭션을 안전하게 관리하기 위한 방식입니다. 이 방식은 트랜잭션이 여러 데이터 소스(예: 서로 다른 데이터베이스, 메시지 큐 등)에 걸쳐 있는 상황에서 전체 트랜잭션의 일관성을 보장하는 데 사용됩니다. JPA와 Spring 환경에서는 이를 통해 복잡한 분산 환경에서도 데이터 무결성을 유지할 수 있습니다.

  • 2단계 트랜잭션 설정은 여러 데이터 소스 간의 일관성 있는 트랜잭션 처리를 위해 JTA를 사용하여 JtaTransactionManager를 설정합니다.
  • Atomikos와 같은 JTA 구현체를 사용하여 분산 트랜잭션을 관리합니다.
  • 트랜잭션의 복잡성과 성능 비용을 고려해야 하며, 잠재적인 데드락과 분산 트랜잭션 관리의 어려움에 주의해야 합니다.
  • 성능 요구사항에 따라 단일 트랜잭션 사용이나 사가 패턴과 같은 대안도 고려해야 합니다.

1. 2단계 커밋(Two-Phase Commit)의 이해

2단계 커밋은 다음 두 단계로 나누어져 데이터베이스 트랜잭션을 관리합니다.

  • 1단계: 준비 단계(Prepare Phase)
    • 각 참여자(데이터베이스, 메시지 브로커 등)에 트랜잭션을 준비하도록 요청합니다.
    • 각 참여자는 트랜잭션이 성공할 수 있는지 여부를 확인하고 "OK" 또는 "FAIL"을 응답합니다.
  • 2단계: 커밋 단계(Commit Phase)
    • 모든 참여자가 준비 완료(OK)를 응답했다면 실제로 트랜잭션을 커밋합니다. 만약 준비가 안 된 참여자가 있다면 모든 참여자에게 롤백을 요청합니다.

이 방식을 통해 데이터베이스나 다른 시스템에서 트랜잭션의 일관성을 유지할 수 있습니다.

2. JPA와 Spring에서의 2단계 트랜잭션 설정

JPA는 JTA(Java Transaction API)를 통해 분산 트랜잭션을 지원합니다. Spring에서는 여러 데이터 소스에 걸친 트랜잭션을 관리하기 위해 JtaTransactionManager를 사용합니다.

2.1 의존성 설정

Maven pom.xml에 JTA를 사용하기 위한 의존성을 추가합니다. 일반적으로 Spring Boot에서는 Atomikos, Bitronix와 같은 JTA 구현체를 사용합니다.

예를 들어, Atomikos의 의존성을 추가하려면 다음과 같습니다:

<dependency>
    <groupId>com.atomikos</groupId>
    <artifactId>transactions-jta</artifactId>
    <version>5.0.9</version>
</dependency>

2.2 JTA 트랜잭션 매니저 설정

Spring Boot 애플리케이션에서 JPA와 JTA를 함께 사용하는 경우, 다음과 같이 트랜잭션 매니저를 설정할 수 있습니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.TransactionManager;

@Configuration
public class TransactionManagerConfig {

    @Bean
    public PlatformTransactionManager transactionManager(TransactionManager transactionManager) {
        return new JtaTransactionManager(transactionManager);
    }
}

JtaTransactionManager는 여러 데이터 소스에 걸친 트랜잭션을 안전하게 관리할 수 있는 기능을 제공합니다. 이 설정을 통해 여러 데이터 소스에 대해 원자적인 트랜잭션을 수행할 수 있습니다.

728x90

2.3 데이터 소스 설정

분산 트랜잭션을 사용할 경우 여러 데이터 소스를 설정해야 합니다. 각 데이터 소스는 별도의 트랜잭션 매니저를 통해 관리될 수 있어야 합니다. 예를 들어, 두 개의 서로 다른 데이터 소스를 다음과 같이 설정할 수 있습니다.

import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.Properties;

@Configuration
public class DataSourceConfig {

    @Bean(name = "dataSourceOne")
    public DataSource dataSourceOne() {
        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        Properties properties = new Properties();
        properties.setProperty("user", "user1");
        properties.setProperty("password", "password1");
        properties.setProperty("URL", "jdbc:mysql://localhost:3306/db_one");

        dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        dataSource.setUniqueResourceName("dataSourceOne");
        dataSource.setXaProperties(properties);

        return dataSource;
    }

    @Bean(name = "dataSourceTwo")
    public DataSource dataSourceTwo() {
        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        Properties properties = new Properties();
        properties.setProperty("user", "user2");
        properties.setProperty("password", "password2");
        properties.setProperty("URL", "jdbc:mysql://localhost:3306/db_two");

        dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        dataSource.setUniqueResourceName("dataSourceTwo");
        dataSource.setXaProperties(properties);

        return dataSource;
    }
}

위 코드에서는 두 개의 데이터 소스 (dataSourceOne, dataSourceTwo)를 Atomikos를 통해 설정하였습니다. 각 데이터 소스는 XADataSource를 사용하며, Atomikos를 통해 JTA 트랜잭션을 지원합니다.

2.4 분산 트랜잭션을 위한 서비스 설정

JTA를 사용한 트랜잭션 매니저 설정 후, 트랜잭션을 걸어야 할 서비스 레이어에 @Transactional 어노테이션을 사용합니다. JTA 트랜잭션 매니저는 여러 데이터 소스를 포함한 트랜잭션을 처리할 수 있습니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Service
public class UserService {

    @PersistenceContext(unitName = "dataSourceOne")
    private EntityManager entityManagerOne;

    @PersistenceContext(unitName = "dataSourceTwo")
    private EntityManager entityManagerTwo;

    @Transactional
    public void transferData(Long userId, String newInfo) {
        // 데이터 소스 1에서 엔티티 업데이트
        User user = entityManagerOne.find(User.class, userId);
        user.setInfo(newInfo);
        entityManagerOne.persist(user);

        // 데이터 소스 2에 새로운 엔티티 저장
        AnotherEntity anotherEntity = new AnotherEntity();
        anotherEntity.setSomeData(newInfo);
        entityManagerTwo.persist(anotherEntity);

        // 두 데이터 소스에 걸친 트랜잭션 처리 - 오류 발생 시 전체 롤백
    }
}

위의 transferData() 메소드는 두 데이터 소스를 대상으로 데이터를 수정하는 예시입니다. @Transactional을 사용하여 두 데이터 소스에서의 작업을 하나의 트랜잭션으로 관리합니다. 만약 하나라도 실패하면 전체 트랜잭션이 롤백됩니다.

3. 2단계 트랜잭션 설정 시 주의 사항

  1. 분산 트랜잭션의 비용: 2단계 커밋은 모든 참여자가 준비 상태에 대한 응답을 보내고 커밋하는 과정을 거치기 때문에, 성능 비용이 매우 높습니다. 가능한 한 여러 데이터 소스를 동시에 업데이트하는 경우를 줄이고, 단일 데이터 소스에서의 트랜잭션을 고려하는 것이 좋습니다.
  2. JTA와 데이터베이스 지원: JTA 트랜잭션을 사용하려면 XA 프로토콜을 지원하는 데이터베이스가 필요합니다. 각 데이터베이스 벤더가 제공하는 XA DataSource를 사용해야 합니다.
  3. 잠재적 데드락: 여러 데이터 소스에 걸친 트랜잭션에서는 데드락의 위험이 더 커질 수 있습니다. 각 시스템의 자원을 예약하는 과정에서 다른 시스템의 리소스를 기다리게 되는 경우가 발생할 수 있으므로, 이러한 상황을 주의 깊게 테스트하고 잠재적인 교착 상태를 방지할 수 있는 전략을 마련해야 합니다.
  4. 트랜잭션 관리의 복잡성: 분산 트랜잭션에서는 모든 참여자가 트랜잭션에 동의하지 않는 경우 복잡한 롤백 절차가 발생할 수 있습니다. 가능한 한 단순하게 트랜잭션을 구성하고, 데이터베이스 간 동기화가 필요 없는 설계를 고려해 보세요.
  5. 분산 트랜잭션 대안: 분산 트랜잭션의 복잡성 때문에, 최근에는 사가 패턴(Saga Pattern) 또는 이벤트 기반 비동기 처리와 같은 대안을 사용하는 경우도 많습니다. 각 단계별 트랜잭션을 독립적으로 처리하고 보상 트랜잭션을 활용하여 실패 시 복구하는 방법을 사용하는 것도 고려할 수 있습니다.

이 설정을 통해 여러 데이터 소스에 걸친 트랜잭션을 원자적으로 처리할 수 있지만, 이와 관련된 성능 및 복잡성 문제를 항상 염두에 두고 설계하는 것이 중요합니다.

728x90
Comments