우주먼지
article thumbnail
Published 2022. 11. 8. 07:27
분산 트랜잭션 Framework/Spring

💡 분산 트랜잭션

 

 

  • 스프링 부트는 Atomikos 임베디드 트랜잭션 매니저를 통해
    여러 XA 리소스에 걸친 분산 JTA 트랜잭션을 지원하며,
    올바른 순서대로 기동,종료 할 수 있도록 스프링 빈에 적절한 depends-on 설정을 적용해준다.
  • JTA 환경을 감지하면 스프링의 'JtaTranscationManager'를 사용해 트랜잭션을 관리한다
  • 자동 설정된 JMS, DataSource, JPA 빈은 XA 트랜잭션을 지원하도록 업그레이드 된다
  • Atomikos 트랜잭션 로그는 어플리케이션의 홈디렉토리안의 transcation-logs 디렉토리에 기록
    이를 위해서 application.properties에 spring.jta.log-dir 프로퍼티를 설정하면 사용디렉토리 custom 가능
  • 2개 이상의 네트워크 상 시스템 간의 트랜잭션
  • 2개의 Phase Commit으로 분산 리소스 간 All or Nothing 보장
  • XA 트랜잭션 - XA 프로토콜을 이용한 분산 트랜잭션

 

JTA란?

JTA(Java Transaction API)은 플랫폼마다 상이한 트랜잭션 매니저들과
어플리케이션들이 상호작용할 수 있는 인터페이스를 정의하고 있다.
 
Java에서 제공되는 대부분의 API와 마찬가지로, JTA는 실제 구현은 다르지만
어플리케이션이 공통적으로 사용할 수 있는 하나의 인터페이스를 제공한다.
 
이 말은 트랜잭션 처리가 필요한 어플리케이션이 (API의 사용 방식 그대로만 사용한다면)
특정 벤더의 트랜잭션 매니저에 의존할 필요가 없음을 의미한다.
 
Atomikos와 같이 JTA 구현체들을 오픈소스로 제공하는 벤더들도 있고,
IBM 같이 JTA 구현체를 어플리케이션 서버의 한 부분으로 제공하는 벤더들도 있다.
JTA의 구현체를 사용할 때에는 주의를 기울여야 한다: 자세히 들여다 보면 뭔가 잘 못 되어 있는 것처럼 보이기 때문이다.
믿기 어렵겠지만, ‘J2EE 호환됨’이라고 검증을 받은 어플리케이션 서버들도
트랜잭션 관리를 제대로 지원하지 않거나 가상적으로만 지원할 수도 있다

 

XA란?

XA(eXtended Architecture)는 동일한 전역 트랜잭션(Global Transaction) 내에서
몇 개의 백엔드 데이터 저장소에 접근하기 위한 X/Open 그룹 표준의 하나이다.


XA 표준 규격은 하나의 트랜잭션 매니저가 어떤 트랜잭션의 한 부분으로
어떤 작업이 수행되고 있는지를 데이터베이스에 통보하는 방식과,
각 트랜잭션이 완료될 때 2단계 커밋(2 Phase Commit)을 수행되는 방식을 관장한다.

데이터 저장소에서 지연되고 있는 트랜잭션을 회복시키는 방법도 포함하고 있다.

예시

  • DB1, 2 가 있다고 가정.
  • WAS가 DB1, 2에 prepare 요청 전송
  • 하나의 DB라도 준비가 되지 않으면 RollBack 실행 -> 트랜잭션의 ACID 충족
  • 모든 DB에서 준비가 될때까지 Commit요청 전송

 

구현

  • Gradle 설정
  • XA 리소스 정보 추가 -> @EnableJpaRepositories 사용
  • 리소스별 설정 -> DB별 데이터 소스 생성 -> DataSource, MysqlXADataSource 클래스 사용
  • JTA TransactionManager 설정
  • JTA Platform 설정

 

요약

> ### ⭐ Distribute 트랜잭션
* 서로 다른 DB를 하나의 트랜잭션으로 묶기 위함

[구현]
* DB 접속 정보 생성
  * DataSource 타입의 메소드를 사용해 @Primary, @Bean 등록
  * MysqlXADataSource 객체 생성
  * AtomikosDataSourceBean 객체 생성, 이 객체를 이용하여 DB 접속 정보 생성

* JPA의 EntityManager를 얻기위한 LocalContainerEntityManagerFactoryBean 사용
  * LocalContainerEntitymanagerFactoryBean 타입의 메소드를 사용해 @Primary, @Bean 등록
  * LocalContainerEntityManagerFactoryBean 객체 생성
  * HibernateJpaVendorAdapter 객체 생성
  * JTA 플랫폼 이름 설정 - LocalContainerEntityManagerFactoryBean
  * 어댑터 설정 - HibernateJpaVendorAdapter

💡 구현


DB 설정

// (1) JpaRepository 활성화
@EnableJpaRepositories(
        basePackages = {"com.solo.member",
                "com.solo.stamp",
                "com.solo.order",
                "com.solo.coffee"},
        entityManagerFactoryRef = "coffeeOrderEntityManager"
)
@Configuration
public class XaCoffeeOrderConfig {
		// (2) 데이터소스 생성
    @Primary
    @Bean
    public DataSource dataSourceCoffeeOrder() {
        MysqlXADataSource mysqlXADataSource = new MysqlXADataSource();
        mysqlXADataSource.setURL("jdbc:mysql://localhost:3306/coffee_order" +
                "?allowPublicKeyRetrieval=true" +
                "&characterEncoding=UTF-8");
        mysqlXADataSource.setUser("guest");
        mysqlXADataSource.setPassword("guest");

        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSource(mysqlXADataSource);
        atomikosDataSourceBean.setUniqueResourceName("xaCoffeeOrder");

        return atomikosDataSourceBean;
    }

		// (3) EntityManagerFactoryBean 설정
    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean coffeeOrderEntityManager() {
        LocalContainerEntityManagerFactoryBean emFactoryBean =
                new LocalContainerEntityManagerFactoryBean();
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setDatabase(Database.MYSQL);
        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "create");
        properties.put("hibernate.show_sql", "true");
        properties.put("hibernate.format_sql", "true");

				// (4)
        properties.put("hibernate.transaction.jta.platform", 
                                             AtomikosJtaPlatform.class.getName());
        properties.put("javax.persistence.transactionType", "JTA");

        emFactoryBean.setDataSource(dataSourceCoffeeOrder());
        emFactoryBean.setPackagesToScan(new String[]{
                "com.solo.member",
                "com.solo.stamp",
                "com.solo.order",
                "com.solo.coffee"
        });
        emFactoryBean.setJpaVendorAdapter(vendorAdapter);
        emFactoryBean.setPersistenceUnitName("coffeeOrderPersistenceUnit");
        emFactoryBean.setJpaPropertyMap(properties);

        return emFactoryBean;
    }
}


백업용 회원 정보 DB 설정

@EnableJpaRepositories(
        basePackages = {"com.solo.backup"},
        entityManagerFactoryRef = "backupEntityManager"
)
@Configuration
public class XaBackupConfig {
    @Bean
    public DataSource dataSourceBackup() {
				// (2)
        MysqlXADataSource mysqlXADataSource = new MysqlXADataSource();
        mysqlXADataSource.setURL("jdbc:mysql://localhost:3306/backup_data" +
                "?allowPublicKeyRetrieval=true" +
                "&characterEncoding=UTF-8");
        mysqlXADataSource.setUser("backup");
        mysqlXADataSource.setPassword("backup");

        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSource(mysqlXADataSource);
        atomikosDataSourceBean.setUniqueResourceName("xaMySQLBackupMember");

        return atomikosDataSourceBean;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean backupEntityManager() {
        LocalContainerEntityManagerFactoryBean emFactoryBean =
                new LocalContainerEntityManagerFactoryBean();
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setDatabase(Database.MYSQL);
        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "create");
        properties.put("hibernate.show_sql", "true");
        properties.put("hibernate.format_sql", "true");
        properties.put("hibernate.transaction.jta.platform",  
                                             AtomikosJtaPlatform.class.getName());
        properties.put("javax.persistence.transactionType", "JTA");

        emFactoryBean.setDataSource(dataSourceBackup());

				// (3)
        emFactoryBean.setPackagesToScan(new String[]{"com.codestates.backup"});
        emFactoryBean.setJpaVendorAdapter(vendorAdapter);
        emFactoryBean.setPersistenceUnitName("backupPersistenceUnit");
        emFactoryBean.setJpaPropertyMap(properties);

        return emFactoryBean;
    }
}


JTA TransactionManager 설정

@Configuration
public class JtaConfig {
	// (1)
    @Bean(name = "userTransaction")
    public UserTransaction userTransaction() throws Throwable {
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        userTransactionImp.setTransactionTimeout(10000);
        return userTransactionImp;
    }

    @Bean(name = "atomikosTransactionManager")
    public TransactionManager atomikosTransactionManager() throws Throwable {
	// (2)
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(false);

	// (3)
        AtomikosJtaPlatform.transactionManager = userTransactionManager;

        return userTransactionManager;
    }

    @Bean(name = "transactionManager")
    @DependsOn({ "userTransaction", "atomikosTransactionManager" })
    public PlatformTransactionManager transactionManager() throws Throwable {
        UserTransaction userTransaction = userTransaction();

        AtomikosJtaPlatform.transaction = userTransaction;

        TransactionManager atomikosTransactionManager = atomikosTransactionManager();

	// (4)
        return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
    }
}


JTA Platform 설정

public class AtomikosJtaPlatform  extends AbstractJtaPlatform {
    static TransactionManager transactionManager;
    static UserTransaction transaction;

    @Override
    protected TransactionManager locateTransactionManager() {
        return transactionManager;
    }

    @Override
    protected UserTransaction locateUserTransaction() {
        return transaction;
    }
}


백업 Entity 클래스 생성

@NoArgsConstructor
@Getter
@Setter
@Entity
public class BackupUser extends Auditable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    @Column(nullable = false, updatable = false, unique = true)
    private String email;

    @Column(length = 100, nullable = false)
    private String name;

    @Column(length = 13, nullable = false, unique = true)
    private String phone;

    // 추가된 부분
    @Enumerated(value = EnumType.STRING)
    @Column(length = 20, nullable = false)
    private UserStatus userStatus = UserStatus.USER_ACTIVE;

    public BackupUser(String email) {
        this.email = email;
    }

    public BackupUser(String email, String name, String phone) {
        this.email = email;
        this.name = name;
        this.phone = phone;
    }

    public enum MemberStatus {
        USER_ACTIVE("활동중"),
        USER_SLEEP("휴면 상태"),
        USER_QUIT("탈퇴 상태");

        @Getter
        private String status;

        UserStatus(String status) {
           this.status = status;
        }
    }
}


Service 클래스에서 백업 Entity 정보 등록

[Service]
- BackupUser DI

create() 메소드 내부
        backupUserService.createBackupUser(new BackupUser(user.getEmail(),
                user.getName(), user.getPhone()));


백업 서비스 클래스에 정보 등록

@Service
public class BackupUserService {
    private final BackupUserRepository backupUserRepository;

    public BackupUserService(BackupUserRepository backupUserRepository) {
        this.backupUserRepository = backupUserRepository;
    }

    @Transactional
    public void createBackupUser(BackupUser backupUser) {
        backupUserRepository.save(backupUser);
				
				// (1)
        throw new RuntimeException("multi datasource rollback test");
    }
}

'Framework > Spring' 카테고리의 다른 글

API Documentation (Spring RestDocs)  (0) 2022.11.12
Testing (Mockito)  (0) 2022.11.08
선언형 트랜잭션  (0) 2022.11.04
JPA & Hibernate (Java Persistence API)  (0) 2022.11.01
Spring Data JDBC 기반 도메인 엔티티 & 테이블 설계  (0) 2022.10.27
profile

우주먼지

@o귤o

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그