스프링에서의 트랜잭션 관리(Spring Transactional Management)

 

이번 포스팅에서는 스프링 프레임워크에서의 트랜잭션 관리 대해 자세히 알아보고 @Transactional 어노테이션에 대해서 살펴보겠습니다.

 

트랜잭션 관리에 대해서는 매우 방대한 분량이므로 두개의 섹션으로 구분하여 진행하도록 하겠습니다.

  • Spring Transaction Management : 스프링이 트랜잭션 관리하는 방법에 대해 깊게 탐구합니다.
  • @Transactional 어노테이션 : 해당 어노테이션의 사용법과 주의사항에 대해서 탐구합니다.

 


 

Spring Transaction Management

트랜잭션은 완전히 성공하거나 완전히 실패하는 일련의 논리적 작업단위입니다. 은행 계좌이체를 생각해보시면 됩니다. 여기서 작업단위는 A의 계좌에서 출금하는 금액과 B의 계좌에서 입금하는 금액입니다. 둘중 하나라도 실패하게 된다면 전체 프로세스는 실패합니다. 중간에 오류가 발생하면 트랜잭션의 모든 단계를 이전으로 되돌리는 것을 롤백이라고 부릅니다.

 

Global Transactions : 서로 다른 데이터베이스 간에 트랜잭션이 발생할 수 있는 응용프로그램이 있을 수 있습니다.(거의 없긴함.) 이를 분산 트랜잭션(distributed transaction) 처리라고 합니다. 트랜잭션 관리자는 이를 처리하기 위해서 애플리케이션 내에 있을 수 없으며 애플리케이션 서버 수준에 있습니다. JTA 또는 Java Transaction API는 JNDI의 지원과 함께 다른 데이터베이스를 조회하는데 필요하며 트랜잭션 관리자는 분산 트랜잭션의 커밋 또는 롤백을 결정합니다. 이것은 복잡한 프로세스이며 어플리케이션 서버 수준의 지식을 필요로 합니다.

Local Transactions : 로컬 트랜잭션은 간단한 JDBC 연결과 같은 단일 RDBMS와 애플리케이션 사이에서 발생합니다. 로컬 트랜잭션을 사용하면 모든 트랜잭션 코드가 우리 코드 내에 있습니다.

 

global과 local 트랜잭션 모두, 개발자가 스스로 다뤄야하는 영역입니다. 만약 jdbc를 사용한다면 트랜잭션 관리 API는 JDBC용입니다. Hibernate를 사용한다면, 어플리케이션 서버의 hibernate 트랜잭션 API와 JTA는 global 트랜잭션을 위한 것입니다.

 

스프링 프레임워크는 다양한 트랜잭션 API에 대한 추상화를 제공하고 일관된 프로그래밍 모델을 제공하여 위의 모든 문제를 극복하였습니다. 추상화는 org.springframework.transaction.PlatformTransactionManager 인터페이스를 이용하여 해결하였습니다. 해당 인터페이스는 아래와 같이 정의되어 있습니다.

 

public interface PlatformTransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition) 
    	throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;

}

 

아래는 PlatformTransactionManager를 구현하는 다양한 스프링 프레임워크 관련 transaction manager 입니다.

 

  • org.springframework.orm.jpa.JpaTransactionManager — JPA 트랜잭션 관리자
  • org.springframework.jdbc.datasource.DataSourceTransactionManager — JDBC 트랜잭션 관리자
  • org.springframework.orm.hibernate5.HibernateTransactionManager — Hibernate 트랜잭션 관리자이며 SessionFactory 와 바인딩됩니다.
  • org.springframework.transaction.jta.JtaTransactionManager — JTA 트랜잭션 매니저
  • org.springframework.transaction.jta.WebLogicJtaTransactionManager — Oracle Weblogic 트랜잭션 관리자.
  • org.springframework.transaction.jta.WebSphereUowTransactionManager — IBM Websphere Application Server 관리 트랜잭션 관리자.
  • org.springframework.jms.connection.JmsTransactionManager — JMS connection factory 를 바인딩하여 JMS 메시징 트랜잭션 관리자.

 

JpaTransactionManager를 사용하여 2개의 다른 데이터 소스에 연결하고 Oracle 10g을 데이터베이스로 사용하고 Weblogic 서버에서 관리하는 사용 사례를 살펴보겠습니다. 가장 먼저 생성해야 할 것은 아래와 같은 스프링 설정 클래스에 2개의 DataSource Bean입니다.

 

 

Weblogic용 Profile1
@Bean(name = "dataSource")
@Profile("profile1")
public DataSource jndiDataSource() {

	DataSource dataSource = null;
	JndiTemplate jndi = new JndiTemplate();

 	try {
   		dataSource = (DataSource) jndi.lookup("give_JNDI_LOOKUP_NAME");
	} catch (NamingException e) {
    	LOGGER.error("NamingException for " + jndiName, e);
  	}
	return new AbstractDataSource(dataSource);
}

 

 

 

tomcat 서버용 Profile2
@Bean(name = "dataSource")
@Profile({"profile2"})
public DataSource jdbcDataSource() {
	DriverManagerDataSource dataSource = new DriverManagerDataSource();
	dataSource.setDriverClassName("oracle.jdbc.OracleDriver");
	dataSource.setUrl("give db url here ");
	dataSource.setUsername("give username");
	dataSource.setPassword("give password");
	return new AbstractDataSource(dataSource);
}
public class AbstractDataSource extends DelegatingDataSource {
	public AbstractDataSource(DataSource delegate) {
		super(delegate);
	}

	@Override
    public Connection getConnection() throws SQLException {
		return super.getConnection();
	}
}

 

EntityManagerFactory 용 Bean을 생성합니다.

@Bean(name = "entityManagerFactory")
public EntityManagerFactory getEntityManagerFactory() {
	LocalContainerEntityManagerFactoryBean em 
    	= new LocalContainerEntityManagerFactoryBean();
	em.setPersistenceUnitName("ORM_Model");
	em.setPersistenceXmlLocation("META-INF/persistence.xml");
	em.setDataSource(dataSource);
	em.setJpaVendorAdapter(getJpaHibernateVendorAdapter());
	em.afterPropertiesSet();
	return em.getObject();
}

 

JPA 트랜잭션 관리자를 정의합니다.

@Bean(name = "transactionManager")
public JpaTransactionManager getTransactionManager() {
	JpaTransactionManager tm = new JpaTransactionManager();
	tm.setEntityManagerFactory(getEntityManagerFactory());
	tm.setDataSource(dataSource);
	return tm;
}

 


 

 

 

@Transactional 어노테이션

스프링의 트랜잭션은 프로그래밍 방식선언적 방식의 두가지 방식으로 구분할 수 있습니다.

 

프로그래밍 방식

스프링에서는 프로그래밍 방식으로 두가지를 제공하고 있습니다.

  • TransactionTemplate
  • 직접 PlatformTransactionManager 구현하기.

트랜잭션 관리가 비즈니스 로직과 함께 사용되기 때문에 프로그래밍 방식으로는 널리 사용되지는 않습니다. 하지만 몇가지의 CRUD 밖에 없는 어플리케이션에서는 트랜잭션 프록시를 통한 부하가 과중한 작업으로 느껴질 수 있어서 프로그래밍 방식이 사용될 수 있습니다.

 

선언적 방식(@Transactional)

트랜잭션 관리가 비즈니스 로직과 별도로 분리되어 있기 때문에 널리 사용하고 있습니다. Spring AOP 프록시를 활용하여 적절한 TransactionManager로 메서드 호출 주변의 트랜잭션을  구동합니다. XML과 어노테이션을 활용하여 수행할 수 있습니다. 그러나 요즘에는 대부분의 응용프로그램이 어노테이션 기반이므로 어노테이션과 함께 작동하는 방법에 대해 다루도록 하겠습니다.

 

  • @Configuration 어노테이션이 있는 설정 클래스 상단에 @EnableTransactionManagement를 사용합니다. 이것은 아래의 XML 태그와 동일합니다.
<tx:annotation-driven transaction-manager="txManager"/>
@Configuration
@EnableTransactionmanagement
public class SpringConfiguration{

}

 

 

  • DataSource와 TransactionManager를 정의합니다.
@Bean
public FooRepository fooRepository() {
	// @Transactional 메서드가 있는 클래스를 설정하고 반환합니다.
	return new JdbcFooRepository(dataSource());
}

@Bean
public DataSource dataSource() {
	// 필요한 JDBC DataSource 설정 및 반환
}

 

 

메소드 혹은 구현체 클래스 상단에 @Transactional 어노테이션을 사용합니다. 클래스 수준에서 사용되는 경우 메소드 전체에 전역으로 적용됩니다.

 

@Transactional 어노테이션 호출 원리

간단한 예를 통해 @Transactional 어노테이션이 어떻게 작동하는지 알아보겠습니다.  우선 SampleService를 만들어 보겠습니다.

public class SampleService {
   @Transactional
   public void serviceMethod(){
       //call to dao layer 
   }
}

 

SampleService가 다른 클래스에 주입되면 Spring은 내부적으로 아래와 같은 클래스를 생성하여 대신 주입합니다.

class ProxySampleService extends SampleService {
  private SampleService sampleService;

  public ProxySampleService(SampleService s){
    this.sampleService=s;
  }

  @Override
  public void sampleMethod(){
    try{
      // 트랜잭션 시작
      sampleService.sampleMethod();
      // 트랜잭션 커밋
    } catch(Exception e){
	  // 트랜잭션 롤백
    }
  }
}

이를 프록시(Proxy) 패턴이라고 합니다. 실제 호출되는 메소드는 프록시이며 내부적으로 target(본체)을 호출합니다.

 


@Transactional 어노테이션 속성

이번에는 @Transactional 어노테이션 속성 설정을 변경하여 트랜잭션 설정을 하는 방법을 살펴보겠습니다.

 

propagation

트랜잭션 전파를 위한 설정입니다.(Optional) 이것은 트랜잭션 동작을 설정하는 데 매우 중요한 속성입니다. 아래에서 사용 사례를 다루겠습니다.
  • REQUIRED (default) — 현재 트랜잭션 지원, 존재하지 않는 경우 새 트랜잭션 생성
  • REQUIRES_NEW — 새로운 트랜잭션을 생성하고 존재하지 않는 경우 현재 트랜잭션을 일시 중단합니다.
  • MANDATORY — 현재 트랜잭션을 지원하고 존재하지 않는 경우 예외를 던집니다.
  • NESTED — 현재 트랜잭션이 있는 경우 중첩된 트랜잭션 내에서 실행
  • SUPPORTS — 현재 트랜잭션을 지원하지만 존재하지 않는 경우 비트랜잭션으로 실행
isolation
트랜잭션 격리 수준. 트랜잭션이 다른 트랜잭션과 격리되어야 하는 수준을 결정합니다.
  • DEFAULT — 데이터 소스의 기본 격리 수준
  • READ_UNCOMMITTED — Dirty Read, Non-Repeatable Read 및 Phantom Read가 발생할 수 있음을 나타냅니다. 다른 트랜잭션의 커밋되지 않은 데이터도 읽을 수 있음.
  • READ_COMMITTED — Dirty Read를 방지하고 반복할 수 없으며 Phantom Read가 발생할 수 있음을 나타냅니다. 커밋된 데이터만 읽음. 반복조회시 커밋 시점에 따라 데이터 상이.
  • REPEATABLE_READ — Dirty Read와 Non-Repeatable Read가 방지되지만 Phantom Read가 발생할 수 있음을 나타냅니다. 반복적으로 조회하여도 동일한 데이터를 보장.
  • SERIALIZABLE — Dirty Read와 Non-Repeatable Read, Phantom Read가 방지될 수 있음을 나타냅니다. 데이터 처리의 직렬화를 보장.

readOnly : 트랜잭션이 읽기 전용인지 또는 읽기/쓰기인지 여부
timeout : 트랜잭션 타임아웃(처리 시간초과)
rollbackFor : 트랜잭션의 롤백을 발생시켜야 하는 예외(Exception) 클래스의 배열
rollbackForClassName : 트랜잭션의 롤백을 발생시켜야 하는 예외 클래스 이름의 배열
noRollbackFor : 트랜잭션 롤백을 유발하지 않아야 하는 예외 클래스 개체의 배열
noRollbackForClassName : 트랜잭션 롤백을 유발하지 않아야 하는 예외 클래스 이름의 배열

  •  

이 모든 것이 어떻게 작동하는지 이해하기 위해 예제를 살펴보겠습니다. 직원 목록이 있고 직원별로 여러 테이블을 업데이트해야 하는 경우,  직원 데이터를 처리하는 동안 예외가 발생할 경우 해당 직원의 업데이트된 모든 데이터를 롤백해야 하지만 다른 직원은 계속 유지되어야 합니다.

SampleController 클래스
public class SampleController {
  @Autowired
  EmployeeService empService;

  // 직원 목록에 대한 여러 작업 수행.
  public void execute (){
    public List<Integer> empIdList; //직원 아이디 리스트

     for (Integer empId:empIdList){
       try{
         // 각 직원 ID에 대해 직원을 업데이트하여 작업 집합 실행
          empService.updateEmployee();
       }
       catch(Exception e){
         // 실패한 직원에 대한 로깅
       }
     }
  }
 }
EmployeeService 클래스 (직원마다 트랜잭션 적용됨)
  •  
@Transactional
public class EmployeeService {

  @Transactional(
    rollbackFor=Exception.class,
    propagation= Propagation.REQUIRES_NEW)
  public void updateEmployee(){
        //dao.task1
        //dao2.task2
        //dao3.task3  -- 두 번째 직원에 대해 예외가 발생했습니다.
        //dao4.task4
  }
}

직원 2에 대한 태스크 3에서 예외가 발생하면 직원 1에 대한 모든 태스크가 성공적으로 커밋되고, 직원 2에 대한 태스크 1 및 태스크 2는 태스크 3에 오류가 발생하므로 정확하지 않습니다. 따라서 직원 2의 작업은 롤백됩니다. 트랜잭션 설정이 있는 트랜잭션 관리를 통해 처리할 수 있습니다.

 

해당 포스팅은 번역문입니다. 원문은 여기에서  확인하실 수 있습니다.

댓글

Designed by JB FACTORY