본문 바로가기

백엔드 기술/Spring

Spring MVC - 트랜잭션

트랜잭션(Transaction) ?

여러 개의 작업들하나의 그룹으로 묶어서 처리하는 처리 단위,
물리적으로는 여러 개의 작업이지만 논리적으로는 하나의 작업으로 인식하여
전부 성공하든가, 전부 실패하든가의 둘 중 하나로만 처리되어야 트랜잭션의 의미를 가진다.

 

트랜잭션의 특징 - ACID 원칙

1. 원자성 (Atomicity)

작업을 더 이상 쪼갤 수 없음을 의미.
논리적으로 하나의 작업으로 인식해서 둘 다 성공하든가, 둘 다 실패하든가
중에서 하나로만 처리되는 것이 보장되어야 한다.

2. 일관성 (Consistency)

트랜잭션이 에러 없이 성공적으로 종료될 경우,
비즈니스 로직에서 의도하는 대로 일관성 있게 저장되거나 변경되는 것을 의미

3. 격리성 (Isolation)

CPU의 프로세스 처리과정 처럼, 워드 작업과 동시에 음악을 듣고 있다면,
CPU는 워드용 프로세스, 음악용 프로세스를 번갈아가며 실행하고 있는 것.
즉 격리성은 여러 개의 트랜잭션이 실행될 경우, 각 각 독립적으로 실행이 되어야 함을 의미.

4. 지속성 (Durability)

트랜잭션이 완료되면, 그 결과는 지속되어야 함을 의미.
데이터베이스가 종료되어도 데이터는 물리적인 저장소에 저장되어 지속적으로 유지되어야 한다는 의미.

 

트랜잭션 커밋 (commit)과 롤백 (rollback)

A. 커밋 (commit)

  • 커밋은 모든 작업을 최종적으로 DB에 반영하는 명령어로써 실행 시, 변경된 내용이 DB에 영구적으로 저장
  • commit 명령을 수행하면, 하나의 트랜젝션 과정은 종료하게 된다.

[예시]

BEGIN TRANSACTION;
insert into  MEMBER VALUES 
       (1, now(), now(), 'hgd1@gmail.com', 'MEMBER_ACTIVE', '홍길동1', '010-1111-1111');
COMMIT;

BEGIN TRANSACTION;
insert into  MEMBER VALUES 
       (2, now(), now(), 'hgd2@gmail.com', 'MEMBER_ACTIVE', '홍길동2', '010-2222-2222');

위의 코드대로 실행하게 되면, 1개의 row가 생성됨. 이유는 commit; 명령어가 1번만 실행되었기 때문.

 

B. 롤백 (rollback)

  • 작업 중 문제가 발생했을 때, 트랜잭션 내에서 수행된 작업들을 취소한다.
  • rollback 명령을 수행하면, 트랜잭션 시작 이전의 상태로 돌아간다.

[예시]

BEGIN TRANSACTION;
insert into  MEMBER VALUES 
			(1, now(), now(), 'hgd1@gmail.com', 'MEMBER_ACTIVE', '홍길동1', '010-1111-1111');
ROLLBACK;
COMMIT;

위의 코드대로 실행하게 되면, 0개의 row가 생성됨.
이유는 commit; 명령어 직전에 rollback; 명령어가 수행되었기 때문에 INSERT 수행 이전 상태도 돌아감.

 

Spring Framework에서 트랜잭션 처리

트랜잭션은 로컬 트랜잭션, 분산 트랜잭션으로 분류할 수 있으며,
Spring에서 사용되는 트랜잭션 방식은 선언형 트랜잭션 방식, 프로그래밍 코드 베이스 트랜잭션 방식

하지만 트랜잭션은 애플리케이션의 핵심 로직이 아닌 부가 기능이므로 AOP의 적용 대상 중 하나이다.
따라서 애플리케이션 코드 내에서 프로그래밍 코드 베이스로 트랜잭션을 적용하는 방식은 적절치 않다.

 

Spring에서 사용하는 선언형 방식.

A. 비즈니스 로직에 애너테이션 추가 방식

@Transactional 이라는 애너테이션을 추가하는 방식

 

#1. 클래스 레벨에 @Transactional 애너테이션.

import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional   // (1)
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());

        return memberRepository.save(member);
    }
}

위와 같이 @Transactional 애너테이션을 클래스 레벨에 추가하면,
기본적으로 해당 클래스에서 MemberRepository의 기능을 이용하는 모든 메서드에 트랜잭션이 적용된다.


애플리케이션을 실행시키기 전, 트랜잭션이 어떻게 적용되는지 로그를 통해 확인하도록
JPA의 로그 레벨을 수정

spring:
  h2:
    console:
      enabled: true
      path: /h2
  datasource:
    url: jdbc:h2:mem:test
  jpa:
    hibernate:
...
...

logging:         // 로그 레벨 설정
  level:
    org:
      springframework:
        orm:
          jpa: DEBUG


[애플리케이션 실행 후 postMember 메서드 실행 시 출력되는 로그]

2022-06-20 15:00:22.806 DEBUG 24368 --- o.s.orm.jpa.JpaTransactionManager: 

// (1) 트랜잭션 생성
Creating new transaction with name [com.codestates.member.service.MemberService.
createMember]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

...
...

2022-06-20 15:00:23.004 DEBUG 24368 --- o.s.orm.jpa.JpaTransactionManager: 
Initiating transaction commit

// (2) 커밋
2022-06-20 15:00:23.004 DEBUG 24368 --- o.s.orm.jpa.JpaTransactionManager: 
Committing JPA transaction on EntityManager [SessionImpl(1508768151<open>)]
2022-06-20 15:00:23.017 DEBUG 24368 --- o.s.orm.jpa.JpaTransactionManager: 
Not closing pre-bound JPA EntityManager **after transaction** // (3) 트랜잭션 종료

// (4) EntityManager 종료
2022-06-20 15:00:23.075 DEBUG 24368 --- o.j.s.OpenEntityManagerInViewInterceptor: 
Closing JPA EntityManager in OpenEntityManagerInViewInterceptor

1. createMember 호출되며, 새로운 트랜잭션이 생성됨.
2. 트랜잭션에서 commit이 발생.
3. 트랜잭션이 종료.
4. JPA의 EntityManager를 종료.

[rollback 동작 유무 확인]

import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());
        Member resultMember = memberRepository.save(member);

				
        if (true) {    // (1)
            throw new RuntimeException("Rollback test");
        }
        return resultMember;
    }
}

createMember() 메서드에서 회원 정보를 저장하고 메서드가 종료되기 전 강제로 RuntimeException 처리.

[결과 로그]

2022-06-20 17:20:18.429 DEBUG 21312 --- o.s.orm.jpa.JpaTransactionManager : 
Initiating transaction rollback  // (1)

// (2)
2022-06-20 17:20:18.429 DEBUG 21312 --- o.s.orm.jpa.JpaTransactionManager : 
Rolling back JPA transaction on EntityManager [SessionImpl(1632113004<open>)]

2022-06-20 17:20:18.431 DEBUG 21312 --- o.s.orm.jpa.JpaTransactionManager : 
Not closing pre-bound JPA EntityManager after transaction
...
...

java.lang.RuntimeException: Rollback test
	...
  ...

2022-06-20 17:20:18.471 DEBUG 21312 --- o.j.s.OpenEntityManagerInViewInterceptor : 
Closing JPA EntityManager in OpenEntityManagerInViewInterceptor

(1), (2) 로그를 통해 rollback이 정상적으로 동작하는 것을 확인.

#2. 메서드 레벨에 @Transactional 사용.

import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional  // (1)
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

		// (2)
    @Transactional(readOnly = true)
    public Member findMember(long memberId) {
        return findVerifiedMember(memberId);
    }
}

@Transactional(readOnly = true) 이유.
commit 절차가 진행되긴 하지만, 영속성 컨텍스트가 flush되고, 변경 감지를 위한 스냅샷 생성도 진행 X.
즉, 조회 메서드에는 readOnly 속성을 true로 지정해서
JPA가 자체적으로 성능 최적화 과정을 거치도록 하는 것이 좋다.

 

[두 개의 다른 클래스의 메서드를 하나의 트랜잭션으로 묶어서 처리하는 예시]

Order Class

@Transactional  // (1)
@Service
public class OrderService {
    private final MemberService memberService;
    private final OrderRepository orderRepository;
    private final CoffeeService coffeeService;

    public OrderService(MemberService memberService,
                        OrderRepository orderRepository,
                        CoffeeService coffeeService) {
        this.memberService = memberService;
        this.orderRepository = orderRepository;
        this.coffeeService = coffeeService;
    }
		
    public Order createOrder(Order order) {
        verifyOrder(order);
        Order savedOrder = saveOrder(order);
        updateStamp(savedOrder);
				
				// (2)
        throw new RuntimeException("rollback test");
//        return savedOrder;
    }

    private void updateStamp(Order order) {
        Member member = memberService.findMember(order.getMember().getMemberId());
        int stampCount = calculateStampCount(order);
        
        Stamp stamp = member.getStamp();
        stamp.setStampCount(stamp.getStampCount() + stampCount);
        member.setStamp(stamp);

        memberService.updateMember(member);
    }

    private int calculateStampCount(Order order) {
        return order.getOrderCoffees().stream()
                .map(orderCoffee -> orderCoffee.getQuantity())
                .mapToInt(quantity -> quantity)
                .sum();
    }

    private Order saveOrder(Order order) {
        return orderRepository.save(order);
    }
}

Member Class

@Transactional
@Service
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

     // (1)
    @Transactional(propagation = Propagation.REQUIRED)
    public Member updateMember(Member member) {
        Member findMember = findVerifiedMember(member.getMemberId());

        Optional.ofNullable(member.getName())
                .ifPresent(name -> findMember.setName(name));
        Optional.ofNullable(member.getPhone())
                .ifPresent(phone -> findMember.setPhone(phone));
        Optional.ofNullable(member.getMemberStatus())
                .ifPresent(memberStatus -> findMember.setMemberStatus(memberStatus));

        return memberRepository.save(findMember);
    }

    @Transactional(readOnly = true)
    public Member findMember(long memberId) {
        return findVerifiedMember(memberId);
    }
}

(1)과 같이 메서드에 @Transactional의 애트리뷰트propagation = Propagation.REQUIRED를 지정하면,
현재 진행 중인 트랜잭션이 존재하면, 해당 트랜잭션을 사용하고, 
현재 진행 중인 트랜잭션이 존재하지 않으면, 새 트랜잭션을 생성하도록 해준다.

따라서 OrderService에서 createOrder() 메서드를 호출하면 트랜잭션이 하나 생성되며,
createOrder() 메서드 내에서 updateMember() 메서드를 호출하면
현재 OrderService에서 진행 중인 트랜잭션에 참여한다.

 

<트랜잭션 전파: Transaction Propagation>

더보기

트랜잭션 전파란, 트랜잭션의 경계에서 진행 중인 트랜잭션이 존재할 때, 존재하지 않을 때,
어떻게 동작할 것인지 결정하는 방식을 의미한다.

애트리뷰트를 통해서 설정할 수 있으며, 대표적으로 아래와 같은 유형이 있다.

1. Propagation.REQUIRED
propagation유형의 Default 값.

진행 중인 트랜잭션이 없으면 새로 시작하고, 진행 중인 트랜잭션이 있다면 해당 트랜잭션에 참여.

2. Propagation.REQUIRES_NEW
이미 진행 중인 트랜잭션과 무관하게 새로운 트랜잭션이 시작된다.
기존에 진행 중이던 트랜잭션은 새로 시작된 트랜잭션이 종료할 때 까지 중지된다.

 

3. Propagation.MANDATORYPropagation.REQUIRED는 진행 중인 트랜잭션이 없으면, 새로운 트랜잭션이 시작되는 반면,
Propagation.MANDATORY는 진행 중인 트랜잭션이 없으면, 예외를 발생시킨다.

 

4. Propagation.NOT_SUPPORTED트랜잭션을 필요로 하지 않음을 의미한다. 진행 중인 트랜잭션이 있으면 메서드 실행이 종료될 때 까지진행 중인 트랜잭션은 중지되며, 메서드 실행이 종료되면 트랜잭션을 계속 진행한다.

 

5. Propagation.NEVER트랜잭션을 필요로 하지 않음을 의미하며, 진행 중인 트랜잭션이 존재할 경우에는 예외를 발생시킨다.

 

B. AOP 방식으로 비즈니스 로직에서 아예 트랜잭션 적용 코드 자체를 감추는 방식

비효율적임.

 

[핵심 포인트]

1. 트랜잭션 관련 설정은 Spring Boot가 내부적으로 해주기 때문에 개발자가 설정할 필요는 없다.

2. 일반적으로 @Transactional 애너테이션 방식으로 사용할 수 있다.

3. 체크 예외 (Checked Exception)은 @Transactional 애너테이션만 추가해서는 rollback X.
@Transactional(rollbackFor = {SQLException.class, DataFormatException.class}) 처럼
해당 체크 예외를 직접 지정해주거나, 언체크 예외(unchecked exception)로 감싸야 rollback 가능.
4. 트랜잭션 전파 : 진행 중인 트랜잭션이 있거나, 없을 때 어떻게 동작할지를 결정하는 방식.

 

 

[심화]

JTA를 이용한 분산 트랜잭션 적용

일반적인 트랜잭션 방식은 단일 데이터베이스에 대한 트랜잭션 적용 방식을 의미하는 로컬 트랜잭션이다.

하지만 서로 다른 데이터소스를 사용하는
한 개 이상의 데이터베이스를 하나의 트랜잭션으로 묶어서 처리해야 할 경우가 있는데,
이를 분산 트랜잭션이라고 한다.

 

>> 두 개의 MySQL 데이터베이스를 사용하는 데이터 액세스 작업을 하나의 트랜잭션으로 묶어보자.

분산 트랜잭션 적용 예시

회원 정보와 백업용 회원 정보 모두 database 명만 다르게 해서 MySQL DB에 저장해보도록 해보자.

백업용 회원 정보 데이터베이스는 기존 회원 정보의 백업 데이터 역할.

 

백업을 위해 특정 데이터베이스의 데이터를 다른 데이터베이스로 복제하는 방법은 여러가지가 있다.
같은 종류의 데이터베이스일 경우, 복제(Replication) 기능을 이용해서 데이터를 백업할 수 있다.
다른 종류의 데이터베이스일 경우, 애플리케이션의 스케쥴링 기능을 통해
주기적으로 원본 데이터베이스의 데이터다른 데이터베이스로 백업하는 기능을 구현할 수 있다.

 

[테스트]

분산 트랜잭션을 적용해보기 위해서는 두 개의 database와 user가 필요.

1. 커피 주문 정보를 위한 DB 정보.

mysql> create database coffee_order;
mysql> create user guest@localhost identified by 'guest';
mysql> grant all privileges on *.* to guest@localhost;
mysql> flush privileges;

 

2. 백업 회원 정보를 위한 데이터베이스 정보

mysql> create database backup_data;
mysql> create user backup@localhost identified by 'backup';
mysql> grant all privileges on *.* to backup@localhost;
mysql> flush privileges;

 

3. 의존 라이브러리 추가

dependencies {
	...
	...
	implementation 'mysql:mysql-connector-java'
	implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos'
}

MySQL DB를 사용하기 때문에 MySQL 커넥터와 분산 트랜잭션 오픈 소스 atomikos 추가

 

4. 두 개의 데이터베이스 설정

두 개의 데이터베이스에 분산 트랜잭션을 적용하려면 두 개의 데이터베이스에 모두 다음과 같은 설정이 필요.

A. 해당 데이터베이스에 맞는 데이터소스를 생성
B. 각 데이터소스를 JPA의 EntityManager가 인식하도록 설정C. JTA TransactionManager 설정D. JTA Platform 설정

 

#1. 커피 주문을 위한 데이터베이스 설정 코드.

// (1) JpaRepository 활성화
@EnableJpaRepositories(
        basePackages = {"com.codestates.member",
                "com.codestates.stamp",
                "com.codestates.order",
                "com.codestates.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.codestates.member",
                "com.codestates.stamp",
                "com.codestates.order",
                "com.codestates.coffee"
        });
        emFactoryBean.setJpaVendorAdapter(vendorAdapter);
        emFactoryBean.setPersistenceUnitName("coffeeOrderPersistenceUnit");
        emFactoryBean.setJpaPropertyMap(properties);

        return emFactoryBean;
    }
}

로컬 트랜잭션의 경우, Spring Boot의 자동 구성을 이용했기 때문에 개발자가 해야 하는 일이 없었지만,
분산 트랜잭션의 경우 위와 같이 별도의 설정이 필요하다.

 

(1) 데이터베이스를 사용하기 위한 JpaRepository가 위치한 패키지와 entityManagerFactory 빈 참조 설정.
<basePackages>

기존에 사용하던 JpaRepository를 그대로 사용하도록 해당 Repository가 있는 패키지 경로를 적어준다.
<entityManagerFactoryRef>
(3)의 Bean 생성 메서드 명을 작성.

 

(2) 데이터베이스에 대한 데이터소스를 생성하기 위해 데이터베이스 접속 정보들을 설정

 

(3) JPA의 EntityManager를 얻기 위해서 LocalContainerEntityManagerFactoryBean을 사용하고 있다.

LocalContainerEntityManagerFactoryBean에서 사용하는 어댑터 중, 우리가 사용하는
HibernateJpaVendorAdapter를 설정해주고, Hibernate에서 필요한 설정 정보를 Map으로 설정.
EntityManager가 사용할 Entity 클래스가 위치한 패키지 경로를 지정.

(4)와 같이 JTA Platform의 이름을 추가해주어야 한다.
서블릿 컨테이너 환경에서 분산 트랜잭션을 적용하기 위해서는 별도의 JTA 트랜잭션 매니저가 필요한데,
예시에서는 가장 많이 사용하는 오픈 소스 JTA 트랜잭션 매니저 플랫폼인 Atomikos를 사용.

 

 

#2. 백업용 회원 정보 데이터베이스 설정.

// (1)
@EnableJpaRepositories(
        basePackages = {"com.codestates.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;
    }
}

분산 트랜잭션을 적용하기 위한 백업용 회원 정보 데이터베이스를 위한 설정 코드


백업용 회원 정보 데이터베이스의 설정 방법은 커피 주문을 위한 데이터베이스 설정과 동일하지만
백업용 회원 정보 데이터베이스에 필요한 정보로 적절하게 수정해 주면 된다.

<수정 내용>
1. 데이터베이스에서 사용할 JpaRepository가 위치한 패키지 경로를 (1)과 같이 변경.
2. MySQL 접속 정보를 (2)과 같이 입력
3. EntityManager가 사용할 Entity 클래스(BackupMember)가 위치한 패키지 경로를 (3)같이 변경.

 

#3. 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);
    }
}

Atomikos와 관련된 두 개의 BeanuserTransactionatomikosTransactionManager이며,
이 두 개의 Bean을 (4)과 같이 JtaTransactionManager의 생성자로 넘겨주면
Atomikos의 분산 트랜잭션을 사용할 수 있다.

(1)의 UserTransaction은 애플리케이션이 트랜잭션 경계에서 관리되는 것을 명시적으로 정의.
(2)와 같이 UserTransaction을 관리하는 UserTransactionManager를 생성한 후에
(3)과 같이 AtomikosJtaPlatform의 트랜잭션 매니저로 설정.

 

#4. JTA Platform ( AtomikosJtaPlatform ) 설정

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

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

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

AbstractJtaPlatform을 상속한 후에 트랜잭션 매니저의 위치와 UserTransaction의 위치를 지정하면 되는데
이는 JTA TransactionManager(JtaConfig) 설정에서 이루어 진다.

 

#5. 회원 백업 정보 엔티티 클래스 정의

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

    @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 MemberStatus memberStatus = MemberStatus.MEMBER_ACTIVE;

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

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

    public enum MemberStatus {
        MEMBER_ACTIVE("활동중"),
        MEMBER_SLEEP("휴면 상태"),
        MEMBER_QUIT("탈퇴 상태");

        @Getter
        private String status;

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

 

 

다른 엔티티 클래스와의 연관 관계를 제거한 것을 제외하면, Member 클래스와 동일.

 

#6. 회원 백업 정보 저장을 위한 Repository 인터페이스

import com.codestates.backup.entity.BackupMember;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BackupMemberRepository extends JpaRepository<BackupMember, Long> {
}

회원 백업 정보 저장을 위한 BackupMemberRepository는
클래스 명이 Member -> BackupMember로 변경되었다는 것 외에
MemberRepository와 동일

 

#7. MemberService에서 회원 정보와 회원 백업 정보 등록

@Transactional
@Service
public class MemberService {
    private final BackupMemberService backupMemberService;
    private final MemberRepository memberRepository;
    private final BackupMemberRepository backupMemberRepository;

    public MemberService(BackupMemberService backupMemberService, // (1)
                         MemberRepository memberRepository) {
        this.backupMemberService = backupMemberService;
        this.memberRepository = memberRepository;
        this.backupMemberRepository = backupMemberRepository;
    }

    @Transactional
    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());
        Member savedMember = memberRepository.save(member);

				// (2)
        backupMemberService.createBackupMember(new BackupMember(member.getEmail(),
                member.getName(), member.getPhone()));

        return savedMember;
    }

		...
		...
}

(1)에서 백업용 회원 정보를 저장할 BackupMemberService를 DI.
(2)에서 회원 정보를 등록한 후, (2)에서 백업용 회원 정보를 저장.

 

#8. MemberService에서 회원 정보와 회원 백업 정보 등록

@Service
public class BackupMemberService {
    private final BackupMemberRepository backupMemberRepository;

    public BackupMemberService(BackupMemberRepository backupMemberRepository) {
        this.backupMemberRepository = backupMemberRepository;
    }

    @Transactional
    public void createBackupMember(BackupMember backupMember) {
        backupMemberRepository.save(backupMember);
				
				// (1)
        throw new RuntimeException("multi datasource rollback test");
    }
}

createBackupMember() 메서드에 @Transactionl 애너테이션을 사용해
MemberService에서 사용하는 createMember() 메서드에 같은 트랜잭션이 되도록 설정.

(1)에서는 MemberService와는 다르게 의도적으로 RuntimeException 예외 발생을 시킴.

 

[테스트 결과]

createMember() 메서드를 실행하면 MemberService에서는 제대로 저장되지만,
BackupMemberService 에서는 의도적으로 예외를 발생시켰으므로,
트랜잭션의 일관성 규칙에 의해 두 개의 데이터베이스에는 모두 데이터가 저장되지 않는다.

 

이처럼 분산 트랜잭션을 적용하면,
서로 다른 리소스에 대한 작업을 수행하더라도, 하나의 작업처럼 관리하기 때문에 원자성을 보장할 수 있다.

 

[핵심 포인트]

1. 분산 트랜잭션
서로 다른 데이터소스를 사용하는 한 개 이상의 데이터베이스를 하나의 트랜잭션으로 묶어서 처리하는 것.

2. Atomikos
Spring Boot에서 스타터로 지원하는 가장 인기 있는 오픈 소스 트랜잭션 관리자.

'백엔드 기술 > Spring' 카테고리의 다른 글

JDBC, Spring Data JDBC, JPA, Spring JPA  (0) 2023.04.25
DTO  (0) 2023.04.23
Spring MVC에서 제공하는 CSR, SSR 방식  (0) 2023.04.21
Spring MVC 프레임워크 요청 처리 과정  (0) 2023.04.21
Spring (POJO) VS Spring Boot  (0) 2023.04.21