트랜잭션 관리

김종준·2023년 3월 7일
0

트랜잭션 관리

스프링 트랜잭션 추상화 레이어 알아보기

트랜잭션을 사용할 때는 먼저 글로벌 트랜잭션을 사용할지 로컬 트랜잭션을 사용할지 선택해야 한다.

로컬 트랜잭션은 단일 트랜잭션 자원에 한정되지만, 글로벌 트랜잭션은 컨테이너가 관리하며 여러 트랜잭션 자원에 걸쳐있을 수 있다.

트랜잭션 타입

로컬 트랜잭션은 관리하기 쉬우며 애플리케이션의 모든 처리가 하나의 트랜잭션 자원만 사용해 이루어진다면 로컬 트랜잭션을 사용해도 충분하다.

하지만 스프링과 같은 애플리케이션 프레임워크를 사용하지 않으면 많은 트랜잭션 관리 코드를 작성해야 하며, 향후 트랜잭션이 다중 트랜잭션 리소스에 걸치도록 확장해야 한다면 로컬 트랜잭션 관리 코드를 제거하고 글로벌 트랜잭션을 사용하도록 다시 개발해야 한다.

자바에서는 JTA글로벌 트랜잭션을 구현한다.

JTA 호환 트랜잭션 매니저는 각 분산 자원에 설치된 리소스 매니저를 이용해 다중 트랜잭션 리소스에 접근한다.

각 리소스 매니저와 통신은 XA 프로토콜을 사용한다.

또한, 2단계 커밋 메커니즘을 사용해 모든 백엔드 데이터 소스가 모두 업데이트되거나 모두 롤백 되도록 보장한다.

백엔드 리소스 중 하나라도 처리가 실패하면 전체 트랜잭션이 롤백 되므로 다른 자원에 대한 수정도 롤백 된다.

아래는 JTA 기반 글로벌 트랜잭션의 개괄적 개요이다.

스크린샷 2023-03-07 오후 5 45 12

첫 번째 부분은 백엔드 리소스로, RDBMS, 메시징 미들웨어, ERP 시스템 등이 있다.

두 번째 부분은 리소스 매니저로, 일반적으로 백엔드 리소스 공급 업체가 제공하며 백엔드 리소스에 접근하는 데 사용된다.

세 번째 부분은 JTA 트랜잭션 매니저로, 트랜잭션에 참여하는 모든 리소스 매니저의 트랜잭션 상태를 관리, 조정, 동기화를 담당한다.

이때 분산 트랜잭션 처리에 널리 사용되는 공개 표준인 XA 프로토콜이 사용된다.

또한 JTA 트랜잭션 관리자는 2PC를 지원하므로 모든 변경 사항은 함께 커밋되며, 하나라도 리소스 수정에 실패하면 전체 트랜잭션이 롤백 되어 어떤 리소스도 수정되지 않는다.

전체 메커니즘은 자바 트랜잭션 서비스(JTS) 사양으로 명시되어 있다.

마지막 부분은 애플리케이션이다.

애플리케이션 자체나 애플리케이션이 실행되는 기본 컨테이너 또는 애플리케이션이 실행되는 스프링 프레임워크가 트랜잭션을 관리한다.

JTA는 모든 JEE 호환 애플리케이션 서버가 지원하며 해당 애플리케이션 서버 내에서는 룩업을 통해서 트랜잭션을 사용할 수 있다.

PlatformTransactionManager 구현체

스프링에서 PlatformTransactionManager 인터페이스는 TransactionDefinition 인터페이스와 TransactionStatus 인터페이스를 사용해 트랜잭션을 생성하고 관리한다.

스프링은 PlatformTransactionManager 인터페이스의 다양한 구현체를 제공한다.

CciLocalTransactionManager 클래스는 JEE, JCA, 공통 클라이언트 인터페이스를 지원한다.

DataSourceTransactionManager 클래스는 일반적인 JDBC 연결을 위한 클래스이다.

ORM과 관련해서는 JPA와 하이버네이트를 포함한 많은 구현체가 있다.

JMS와 관련된 구현체들은 JmsTransactionManager 클래스를 이용해 JMS 2.0을 지원한다.

JTA의 범용 구현체 클래스는 JtaTransactionManager이다.

또한, 스프링은 특정 애플리케이션 서버에 특화된 여러 JTA 트랜잭션 매니저 클래스를 제공한다.

트랜잭션 프로퍼티 살펴보기

트랜잭션은 4개의 잘 알려진 ACID 프로퍼티를 갖고 있으며, 트랜잭션 리소스는 이러한 관점으로 트랜잭션을 관리해야 할 책임이 있다.

사용자트랜잭션의 원자성, 일관성, 내구성을 제어할 수 없다.

하지만 사용자는 트랜잭션 전파와 시간 초과를 제어할 수 있을 뿐 아니라, 트랜잭션을 읽기 전용으로 구성하고 격리 수준을 지정할 수 있다.

스프링은 이런 모든 설정을 TransactionDefinition 인터페이스에 캡슐화한다.

이 TransactionDefinition 인터페이스는 트랜잭션을 지원하는 스프링의 핵심 인터페이스인 PlatformTransactionManager에서 사용되며, PlatformTransactionManager의 여러 구현체는 JDBC나 JTA와 같은 특정 플랫폼에서 트랜잭션 관리를 수행한다.

핵심 메서드인 PlatformTransactionManager.getTransaction()은 TransactionDefinition 인터페이스를 인수로 전달받고 TransactionStatus 인터페이스를 반환한다.

TransactionStatus 인터페이스는 트랜잭션 실행을 제어할 때 사용되는데, 특히 트랜잭션 결과를 설정하고 트랜잭션의 완료 여부나 새 트랜잭션인지의 여부를 확인하는 데 사용된다.

TransactionDefinition 인터페이스

public interface TransactionDefinition {
  ...
  int getPropagationBehavior();
  int getIsolationLevel();
  int getTimeout();
  boolean isReadOnly();
  String getName();
}

getTimeout() 메서드는 트랜잭션이 완료되어야 하는 시간을 반환한다.

isReadOnly() 메서드는 트랜재션이 읽기 전용인지 여부를 반환한다.

트랜잭션 매니저는 이 값들을 이용해 실행을 최적화하고 트랜잭션이 정확하게 읽기 작업만 수행하는지 확인할 수 있다.

getName() 메서드는 트랜잭션의 이름을 반환한다.

getIsolationLevel() 메서드는 다른 트랜잭션에서 볼 수 있는 데이터 변경 수준을 제어한다.

격리 수준은 TransactionDefinition 인터페이스에 정의된 static 값으로 표시된다.

격리 수준설명
ISOLATION_DEFAULT데이터 저장소의 기본 격리 수준이다.
ISOLATION_READ_UNCOMMITTED가장 낮은 수준의 격리다.
이 트랜잭션은 다른 트랜잭션이 아직 커밋하지 않은 수정 데이터를 볼 수 있으므로 트랜잭션 기능을 거의 수행하지 않는다.
ISOLATION_READ_COMMITTED대부분의 데이터베이스에서 기본 수준이다.
트랜잭션은 다른 트랜잭션이 커밋하지 않은 데이터는 읽을 수 없다.
하지만 어떤 트랜잭션이 특정 데이터를 읽은 상태에서 다른 트랜잭션이 해당 데이터를 수정할 수 있다.
ISOLATION_REPEATABLE_READISOLATION_READ_COMMITTED보다 엄격하다.
트랜잭션 내에서 일단 한 번 데이터를 읽어오면 다시 읽어올 때마다 동일한 데이터를 읽어온다.
한 트랜잭션이 읽은 데이터를 다른 트랜잭션이 수정할 수 없지만 새 데이터를 삽입할 수는 있으며 이렇게 새로 삽입된 데이터는 다른 트랜잭션에서도 읽을 수 있다.
ISOLATION_READ_SERIALIZABLE가장 비용이 크고 신뢰할 수 있는 격리 수준이다.
모든 트랜잭션은 하나씩 차례대로 실행되는 것처럼 처리된다.

getPropagationBehavior() 메서드는 현재 이미 시작돼 진행 중인 트랜잭션이 있는지 없는지에 따라 새 트랜잭션 호출이 발생할 때 어떻게 호출을 처리할지 지정한다.

전파 유형은 TransactionDefinition 인터페이스에 정의된 static 값으로 표시된다.

전파 유형설명
PROPAGATION_REQUIRED이미 존재하는 트랜잭션을 지원한다.
트랜재션이 없으면 새 트랜잭션이 시작된다.
PROPAGATION_SUPPORTS이미 존재하는 트랜잭션을 지원한다.
트랜잭션이 없으면 비트랜잭션으로 실행된다.
PROPAGATION_MANDATORY이미 존재하는 트랜잭션을 지원한다.
진행 중인 트랜잭션이 없으면 예외를 던진다.
PROPAGATION_NOT_SUPPORTED진행 중인 트랜잭션과 함께 실행할 수 없다.
항상 비트랜잭션을 실행하고 기존 트랜잭션을 일시 중단한다.
PROPAGATION_NEVER진행 중인 트랜잭션이 있더라도 항상 비트랜잭션으로 실행된다.
진행 중인 트랜잭션이 존재하는 경우 예외를 던진다.
PROPAGATION_NESTED진행 중인 트랜잭션이 있는 경우 중첩 트랜잭션으로 실행된다.
진행 중인 트랜잭션이 없으며 PROPAGATION_REQUIRED가 설정된 것처럼 실행된다.

TransactionStatus 인터페이스

트랜잭션 매니저는 TransactionStatus 인터페이스를 사용해 트랜잭션을 제어할 수 있다.

TransactionStatus 인터페이스 메서드는 트랜잭션이 새로운 트랜잭션인지의 여부나 트랜잭션이 읽기 전용인지 여부를 확인할 수 있으며, 롤백을 시작할 수도 있다.

public interface TransactionStatus {
  boolean isNewTransaction();
  boolean hasSavepoint();
  void setRollbackOnly();
  boolean isRollbackOnly();
  void flush();
  boolean isCompleted();
}

setRollbackOnly() 메서드는 롤백을 일으키고 진행 중인 트랜잭션을 종료한다.

hasSavepoint() 메서드는 트랜잭션 내부적으로 저장 지점을 전달하는지 여부를 반환한다.

flush() 메서드는 가능하다면 기본 세션을 데이터 저장소에 플러시한다.

isCompleted() 메서드는 트랜잭션이 종료되었는지 여부를 반환한다.

예제 코드를 위한 데이터 모델과 인프라

전문적인 환경에서는 퍼시스턴스 구성(DAO)트랜잭션 구성(서비스)분리하는 것이 일반적이다.

그렇기에 DataJpaConfig 클래스에는 데이터 접근 빈만 포함하고 ServiceConfig 클래스에는 트랜잭션 관리와 관련된 빈만 포함한다.

@Configuration
@EnableJpaRepositories(basePackages = {"..."})
public class DataJpaConfig {
  @Bean
  public DataSource dataSource() {
    ...
  }
  
  @Bean
  public JpaVendorAdapter jpaVendorAdapter() {
    return new HibernateJpaVendorAdapter();
  }
  
  private Properties hibernateProperties() {
    ...
	}
  
  @Bean
  public EntityManagerFactory entityManagerFactory() {
    ...
  }
}
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "...")
public class ServicesConfig {
  @Autowired
  EntityManagerFactory entityManagerFactory;
  
  @Bean
  public PlatformTransactionManager transactionManager() {
    return new JpaTransactionManager(entityManagerFactory);
  }
}

애너테이션 기반으로 트랜잭션을 관리할 때 적용해야 하는 유일한 애너테이션은 @Transactional 이다.

@Transactional 애너테이션은 클래스 수준에 적용될 때 스프링은 클래스 내의 각 메서드 실행 전에 트랜잭션이 존재함을 보장한다.

@Transactional 애너테이션은 기본 동작을 재정의하는 데 사용할 수 있는 많은 애트리뷰트를 제공한다.

propagation, Isolation, timeout, readOnly, rollbackFor, rollbackForClassName, noRollbackFor, noRollbackForClassName, value

만약 애트리뷰트를 적용하지 않고 @Transactional 애너테이션만 적용하면 트랜잭션 전파는 required, 격리는 default, 타임아웃은 default, 모드는 read-write가 된다.

@Transactional(readOnly = true)

readOnly = true 애트리뷰트는 모든 finder 메서드에 적용되어야 한다.

주된 이유는 대부분의 퍼시스턴스 제공자가 read-only 트랜잭션에 대해 일정한 수준으로 최적화를 수행하기 때문이다.

예를 들어 하이버네이트는 read-only 상태에서 데이터베이스에서 조회한 관리대상 인스턴스의 스냅샷을 유지하지 않는다.

트랜잭션 관리에 AOP 사용하기

선언적으로 트랜잭션을 관리하는 또 다른 일반적인 접근법은 스프링의 AOP를 사용하는 것이다.

물론, 애너테이션을 적용하였다면 AOP 방식의 트랜잭션 관리를 권장하지 않는다.

하지만 프로젝트 코드가 아닌 외부 코드를 트랜잭션 코드로 래핑하고 싶을 때 소스를 수정하지 못해서 트랜잭션 애너테이션을 적용하지 못하는 경우, AOP 방식 트랜잭션 관리 방법을 알고 있다면 유용하다.

프로그래밍으로 트랜잭션 사용하기

프로그래밍으로 트랜잭션을 제어하는 방식은 두 가지가 있다.

첫 번째는 빈에 PlatformTransactionManager 인스턴스를 주입해 트랜잭션 매니저에 직접 접근하는 것이다.

다른 방식은 스프링이 제공하는 TransactionTemplate 클래스를 사용하는 것이다.

@Configuration
@ComponentScan(basePackages = "...")
public class ServicesConfig {
  @Autowired
  EntityManagerFactory entityManagerFactory;
  
  @Bean
  public TransactionTemplate transactionTemplate() {
    TransactionTemplate tt = new TransactionTemplate();
    tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER);
    tt.setTimeout(30);
    tt.setTransactionManager(transactionManager());
    return tt;
  }
  
  @Bean
  public PlatformTransactionManager transactionManager() {
    return new JpaTransactionManager(entityManagerFactory);
  }
}

@EnableTransactionManagement 애너테이션이 제거되고 transactionTemplate 빈을 정의하였다.

이렇게 정의한 transactionTemplate는 아래와 같이 사용한다.

@Service
@Repository
public class SingerServiceImpl implements SingerService {
  @Autowired
  private SingerRepository singerRepository;
  @Autowired
  private TransactionTemplate transactionTemplate;
  @PersistenceContext
  private EntityManager em;
  
  public long countAll() {
    return transactionTemplate.execute(
      transactionStatus -> 
      	em.createNamedQuery(Singer.COUNT_ALL, Long.class)
      		.getSingerResult()
    );
  }
}

트랜잭션 관리 시 고려사항

선언적 방식은 모든 경우에 권장되며 가능한 한 코드 내에서 트랜잭션 관리 로직을 구현하지 말아야 한다.

애플리케이션에서 트랜잭션 제어 로직을 작성할 일이 생긴다면 대부분 설계를 잘못한 것이므로 관리가 가능하게 로직을 리팩터링하고 트랜잭션 요구사항을 선언적으로 정의해야 한다.

스프링과 글로벌 트랜잭션

자바 애플리케이션은 대부분 다중 백엔드 리소스에 접근해야 한다.

글로벌 트랜잭션의 주된 특징은 원자성의 보장으로, 이는 관련된 리소스가 모두 수정되거나 하나도 수정되지 않는다는 것을 의미한다.

여기에는 트랜잭션 매니저가 처리해야 하는 복잡한 조정과 동기화 로직이 포함된다.

자바에서 JTA는 글로벌 트랜잭션을 구현하는 사실상의 표준이다.

스프링은 JTA 트랜잭션을 로컬 트랜잭션과 동일한 방식으로 지원하며 비즈니스 코드에서 트랜잭션 관련 로직을 숨긴다.

JTA로 글로벌 트랜잭션 구현하기

@Configuration
@EnableJpaRepositories
public class XAJpaConfig {
  @SuppressWarnings("unchecked")
  @Bean(initMethod = "init", destroyMethod = "close")
  public DataSource dataSourceA() {
    try {
      AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean(); // XA 호환
      dataSource.setUniqueREsourceName("XADBMSA");
      dataSource.setXaDataSourceClassName(
      	"com.mysql.cj.jdbc.MysqlXADataSouorce");
      dataSource.setXaProperties(xaAproperties());
      dataSource.setPoolSize(1);
    } catch (Exception e) {
      ...
    }
  }
  @Bean
  public Properties xaAProperties() {
    Properties xaProp = new Properties();
    xaProp.put("databaseName", "...");
    xaProp.put("user", "...");
    xaProp.put("password", "...");
    return xaProp;
  }
  
  @SuppressWarnings("unchecked")
  @Bean(initMethod = "init", destroyMethod = "close")
  public DataSource dataSourceB() {
    try {
      AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean(); // XA 호환
      dataSource.setUniqueREsourceName("XADBMSA");
      dataSource.setXaDataSourceClassName(
      	"com.mysql.cj.jdbc.MysqlXADataSouorce");
      dataSource.setXaProperties(xaAproperties());
      dataSource.setPoolSize(1);
    } catch (Exception e) {
      ...
    }
  }
  @Bean
  public Properties xaBProperties() {
    Properties xaProp = new Properties();
    xaProp.put("databaseName", "...");
    xaProp.put("user", "...");
    xaProp.put("password", "...");
    return xaProp;
  }
  
  @Bean
  public Properties hibernateProperties() {
    Properties hibernateProp = new Properties();
    hibernateProp.put("hibernate.transaction.factory_class", "org.hibernate.transaction.JTATransactionFactory");
    hibernateProp.put("hibernate.transaction.jta.platform", "com.atomikos.icatch.hibernate4.AtomikosPlatform");
    hibernateProp.put("hibernate.transaction.coordinator_class", "jta");
    hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect");
    hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
    hibernateProp.put("hibernate.format_sql", true);
    hibernateProp.put("hibernate.max_fetch_depth", 3);
    hibernateProp.put("hibernate.jdbc.batch_size", 10);
    hibernateProp.put("hibernate.jdbc.fetch_size", 50);
    return hibernateProp;
  }
  
  @Bean
  public EntityManagerFactory emfA() {
    ...
  }
  @Bean
  public EntityManagerFactory emfB() {
    ...
  }
}

Properties의 factory_class 그리고 platform 프로퍼티는 하이버네이트가 기본 UserTransaction과 TransactionManager 빈을 룩업해 글로벌 트랜잭션으로 관리하는 퍼시스턴스 컨텍스트에 참여하는 데 사용되기 때문에 매우 중요하다.

또한, coordinator_class는 하이버네이트 4를 지원하는 Atomikos 클래스를 하이버네이트 5에서도 동작하게 하는 데 필요하다.

이렇게 설정한 글로벌 트랜잭션을 관리하는 데 사용될 빈을 선언하는 ServicesConfig 코드는 아래와 같다.

@Configuration
@ComponentScan(basePackages = "...")
public class ServicesConfig {
  @Autowired
  EntityManagerFactory entityManagerFactory;
  
  @Bean(initMethod = "init", destoryMethod = "shutdownForce")
  public UserTransactionService userTransactionService() {
    Properties atProps = new Properties();
    atProps.put("com.atomikos.icatch.service",
                "com.atomikos.icatch.standalone.UserTransactionServiceFactory");
    return new UserTransactionServiceImpl(atProps);
  }
  
  @Bean(initMethod = "init", destoryMethod = "close")
  @DependsOn("userTransactionService")
  public UserTransactionManager atomikosTransactionManager() {
    UserTransactionManager utm = new UserTransactionManager();
    utm.setStartupTransactionService(false);
    utm.setForceShutdown(true);
    return utm;
  }
  
  @Bean
  @DependsOn("userTransactionService")
  public UserTransaction userTransaction() {
    UserTransactionImp ut = new UserTransactionImp();
    try {
      ut.setTransactionTimeout(300);
    } catch (SsystemException se) {
      ..
    }
    return ut;
  }
  
  @Bean 
  public PlatformTransactionManager transactionManager() {
    JtaTransactionManager ptm = new JtaTransactionManager();
    ptm.setTransactionManager(atomikosTransactionManager());
    ptm.setUserTransaction(userTransaction());
    return ptm;
  }
}

Atomikos와 관련돼 정의된 두 개의 빈은 atomikosTransactionManageruserTransaction이다.

Atomikos가 제공하는 이 빈의 구현체 클래스는, javax.transaction.UserTransaction 인터페이스 및 스프링이 제공하는 org.springframework.transaction.PlatformTransactionManager 인터페이스를 각각 구현한다.

이러한 빈은 JTA가 필요로 하는 트랜잭션 조정과 동기화 서비스를 제공하고, 2PC를 지원하는 XA 프로토콜을 통해 리소스 매니저와 통신한다.

그런 다음 스프링의 transactionManager 빈을 정의하며 Atomikos가 제공하는 두 개의 트랜잭션 빈을 주입한다.

이로써 스프링은 트랜잭션 관리에 Atomikos JTA를 사용한다.

@Service
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
  @PersistenceContext(unitName = "emfA")
  private EntityManager emA;
  
  @PersistenceContext(unitName = "emfB")
  private EntityManager emB;
  
  ....
}

위에서 Atmoikos는 복합 트랜잭션을 생성하고, XA DataSource와 통신하고, 동기화를 수행하는 트랜잭션을 커밋한다.

즉 A와 B 둘 중 하나에서 오류가 발생해 데이터를 저장하지 못한다면 Atomikos는 전체 트랜잭션을 롤백할 것이다.

JTA 트랜잭션 매니저 사용 시 고려사항

글로벌 트랜잭션 관리에 JTA를 사용해야 하는지에 대해서는 논쟁이 있다.

예를 들어 스프링 개발팀은 일반적으로 글로벌 트랜잭션에 JTA를 사용하지 않는 것을 권장한다.

반면에, 일반적으로 애플리케이션이 본격적으로 JEE 애플리케이션 서버에 배포될 때, JEE 애플리케이션 서버 공급 업체들이 각자의 플랫폼용에 최적화된 JTA 구현체를 만들었으므로 JTA를 사용하지 않을 이유가 없다.

0개의 댓글