시작

Spring JPA로 CRUD를 사용하는 경우 Repository 메소드를 이용하게 될텐데 기본적으로 @Transaction이 적용되어 있다. Spring에서 트랜잭션 처리를 지원해서 어노테이션 방식으로  @Transactional로 선언하여 사용할 수 있다. 이번 포스팅에서는 Transaction에 대해 알아보고 Spring에서 어떻게 사용할 수 있는지 정리해보았다.


Transaction 설정하기

Transaction 설정하는 방법에는 크게 3가지 방법이 있다. 

1. Spring 3.1부터 @EnableTransactionManagement 어노테이션을 이용해 @Configuration이 붙은 클래스에 트랜잭션 설정이 가능한 방법이 있다. 

@Configuration
@EnableTransactionManagement
public class PersistenceJPAConfig{

   @Bean
   public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
       //...
   }

   @Bean
   public PlatformTransactionManager transactionManager() {
      JpaTransactionManager transactionManager = new JpaTransactionManager();
      transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
      return transactionManager;
   }
}

2. xml방식을 이용해서 @Transaction 설정 방식을 활성화 할 수 있다. 

<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
   <property name="entityManagerFactory" ref="myEmf" />
</bean>
<tx:annotation-driven transaction-manager="txManager" />

3. 클래스나 메소드 레벨 위에서 @Transactional Annotation을 이용해 설정할 수 있다.

@Service
@Transactional
public class FooService {
    //...
}

@Transactional Annotation이 지원하는 기능이 몇 가지 있는데 다음과 같다. 

  • Isolation Level
  • Propagation Type
  • Timeout
  • ReadOnly Flag
  • Rollback Rules

Isolation Level

트랜잭션에서 일관성이 없는 데이터를 어떻게 허용할지 허용 수준을 설정할 수 있는 옵션이다. 단, 격리 수준이 높아지면 성능에 대한 저하의 우려가 있기 때문에 적절한 속성을 사용해줘야 한다. 

  • Default
    • 기본 격리 수준이다. 
  • Read Uncommited (Level 0)
    • Commit 되지 않은 데이터에 대해 읽기를 허용하는 방식이다. 이렇게 설정되어 있으면 Dirty Read 문제가 발생할 수 있다. 어떤 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안에 다른 사용자는 B라는 완료되지 않은 데이터를 읽어갈 수 있다. 
  • Read Commited (Level 1)
    • Commit 된 데이터를 읽기를 허용하는 방식이다. 특정 사용자가 데이터를 변경하는 동안에 다른 사용자는 해당 데이터에 접근할 수 없다. 이런 경우에 발생할 수 있는 문제는 Non-Repeatable Read 문제가 발생할 수 있다. 사용자 A가 데이터를 조회하고 있을 때 사용자 B가 해당 데이터를 수정하고 Commit해버리면 사용자 A는 데이터를 다시 조회했을 때 수정된 데이터가 조회된 상태를 의미한다. 
  • Repeatable Read (Level 2)
    • 트랜잭션이 완료될 때까지 shared lock이 걸려서 다른 사용자가 해당 데이터에 접근이 불가능한 상태를 의미한다. 즉, 하나의 트랜잭션이 수행되고 있으면 다른 트랜잭션의 갱신이나 삭제가 불가능하기 때문에 데이터의 일관성을 보장할 수 있다. 그러나 트랜잭션 범위에서 레코드를 두 번 읽을 때 데이터가 불일치하는 경우가 발생할 수 있다. Phantom Read 문제가 발생할 수 있다. 
  • Serializable (Level 3)
    • Repeatable Read의 경우에는 데이터 Insert 작업이 가능했기 때문에 Pahntom Read가 가능했다. 트랜잭션이 완료될 때까지 다른 사용자는 해당 영역에서 데이터에 대한 수정 또는 입력이 불가능하다. 

Spring 4.1에서부터 코드 내에서 Isolation Level은 다음과 같이 적용할 수 있다.

@Transactional(isolation = Isolation.SERIALIZABLE)

Propagation Type

propagation 옵션의 경우에는 트랜잭션이 동작할 때 다른 트랜잭션이 호출되면 어떻게 처리해야할지 정하는 옵션이다. 아무 설정을 하지 않고 사용하게 되면 Default는 Required 로 동작하게 된다. 

  • REQUIRED (default) : 이미 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다.
  • REQUIRES_NEW : 항상 새로운 트랜잭션을 시작한다. 이미 진행 중인 트랜잭션이 있으면 트랜잭션을 잠시 보류시킨다.
  • SUPPORTS : 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 트랜잭션없이 진행한다.
  • NESTED : 중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않는다. 메인 트랜잭션이 롤백되면 중첩된 로그 트랜잭션도 같이 롤백되지만, 반대로 중첩된 로그 트랜잭션이 롤백돼도 메인 작업에 이상이 없다면 메인 트랜잭션은 정상적으로 커밋된다.
  • MANDATORY : REQUIRED와 비슷하게 이미 시작된 트랜잭션이 있으면 참여한다. 반면에 트랜잭션이 시작된 것이 없으면 새로 시작하는 대신 예외를 발생시킨다. 혼자서는 독립적으로 트랜잭션을 진행하면 안 되는 경우에 사용한다.
  • NOT_SUPPORTED : 트랜잭션을 사용하지 않게 한다. 이미 진행 중인 트랜잭션이 있으면 보류시킨다.
  • NEVER : 트랜잭션을 사용하지 않도록 강제한다. 이미 진행 중인 트랜잭션도 존재하면 안된다 있다면 예외를 발생시킨다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void doSomething() { ... }

Timeout

제한된 시간 내에 메서드 수행이 완료되지 못하면 Rollback 하는 옵션이다. Default 값은 -1이고 No Timeout을 의미한다. 

@Transactional(timeout = 10)

어노테이션 외에도 전역으로 TimeOut을 설정할 수 있다. (application.yml 혹은 application.properties)

spring:
  transaction:
    default-timeout: 10

ReadOnly Flag

ReadOnly Flag가 설정되어 있다고 하더라도 삽입 혹은 업데이트가 발생하지 않을 것이라고 확신할 수 없다. 일부 트랜잭션 매니저의 경우에는 읽기전용 속성을 무시하고 삽입 혹은 업데이트를 허용할 수 있기 때문에 주의해야한다.

@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)

Rollback Rules

트랜잭션 런타임 예외가 발생하면 롤백한다. 롤백하는 옵션으로는 rollbackFor, rollbackForClassName 속성을 사용 해 트랜잭션을 롤백하고 noRollbackFor, noRollbackForClassName 속성을 사용해 예외에서 롤백을 방지한다. 또, 외부 요인으로 발생할 수 있는 것이 Checked Exception 인데, transaction 처리가 되지 않아서 예외가 발생해도 Rollback 처리가 되지 않는다. 

예를들어 SQLException이 예외가 발생할 때 트랜잭션을 rollback 한다. 

@Transactional(rollbackFor = { SQLException.class })
public void createCourseDeclarativeWithCheckedException(Course course) throws SQLException {
    courseDao.create(course);
    throw new SQLException("Throwing exception for demoing rollback");
}

 


정리

Java, Xml, 선언적인 방법을 이용해 트랜잭션을 이용하는 것에 대해 확인해볼 수 있었다. 특히, 선언적인 방법을 이용하면 트랜잭션 옵션으로 제공하는 여러 특징들을 살펴볼 수 있었다. 


참고

  • https://goddaehee.tistory.com/167
  • https://www.baeldung.com/transaction-configuration-with-jpa-and-spring