[springboot] @Transactional

나른한 개발자·2026년 1월 12일

f-lab

목록 보기
29/44

@Transactional

Spring Framework 2.0 이상의 버전에서 지원하는 @Transactional은 선언적 데이터베이스 트랜잭션 관리 방법을 제공합니다. 메서드 레벨 또는 클래스 레벨에서 사용할 수 있으며, 해당 메서드 또는 클래스의 모든 public 메서드에 트랜잭션을 적용합니다.

해당하는 메서드를 실행할 때 스프링은 트랜잭션을 시작하고, 메서드가 정상적으로 종료되면 트랜잭션을 commit하고, 예외가 발생하면 트랜잭션을 rollback합니다. 즉, 비정상적 종료로 인한 rollback이 발생할 경우에는 트랜잭션의 일부 작업만 데이터베이스에 반영되는 것을 방지해 데이터 일관성을 유지할 수 있습니다.

트랜잭션은 ACID 속성을 충족한다

  • Atomicity (원자성): 랜잭션 내에서 실행되는 작업들은 마치 하나의 단위(원자)처럼 취급됩니다. 즉, 모두 성공하거나, 하나라도 실패하면 전부 취소되어야 합니다.
  • Consistency (일관성): 트랜잭션이 성공적으로 완료되면, 데이터베이스는 미리 정의된 모든 규칙(제약 조건)을 만족해야 합니다.
  • Isolation (격리성): 여러 트랜잭션이 동시에 실행될 때, 서로의 중간 작업 과정을 보지 못하도록 격리하는 성질입니다.
  • Durability (지속성): 성공적으로 완료된(Commit) 트랜잭션의 결과는 시스템에 장애가 발생하더라도 영구적으로 보존되어야 합니다.

그리고 아래 간단한 예시 코드처럼 기존의 긴 JDBC 트랜잭션을 짧은 코드를 Service 단 메서드 위에 어노테이션 처리한 두 번째 코드처럼 유지보수가 쉽게 관리할 수 있습니다.

import java.sql.Connection;

Connection conn = dataSource.getConnection();
Savepoint savepoint;

try (connection) {
    conn.setAutoCommit(false);
    Statement stmt = conn.createStatement();
    savepoint = conn.setSavepoint("savepoint");
    String sql = "Insert into User values (16, 'Rosie', 'rosie@gmail.com')"
    stmt.executeUpdate(sql);
    conn.commit(); 
} catch (SQLException e) {
    conn.rollback(savepoint); 
}
public class UserService{
    private final UserRepository userRepository;   

    @Transactional(readOnly = true)
    public List<User> findAll(User user) {
        return userRepository.save(user);
    }
}

@Transactional 동작 원리

스프링 부트가 시작될 때 트랜잭션 처리에 필요한 AOP 엔진(AutoConfiguration)을 자동으로 가동시키고, 이 엔진이 프로젝트 내의 @Transactional을 찾아 프록시를 입혀줍니다. 따라서 클라이언트 request를 보내면 @Transactional이 적용된 메서드에 대한 트랜잭션 처리가 가능해집니다.

그 관계성을 TransactionAutoConfiguration 클래스로 확인해봅시다.

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
  DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {

   @Configuration(proxyBeanMethods = false)
   @ConditionalOnBean(TransactionManager.class)
   @ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
   public static class EnableTransactionManagementConfiguration {

      @Configuration(proxyBeanMethods = false)
      @EnableTransactionManagement(proxyTargetClass = false)
      @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
      public static class JdkDynamicAutoProxyConfiguration {}

      @Configuration(proxyBeanMethods = false)
      @EnableTransactionManagement(proxyTargetClass = true)
      @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
        matchIfMissing = true)
      public static class CglibAutoProxyConfiguration {}

   }
}

위 코드에서 알 수 있듯이 TransactionAutoConfiguration으로 ProxyConfiguration 구성을 활성화하기 위해서는 @ConditionalOnClass에 따른 선행조건으로 PlatformTransactionManager()가 있어야합니다.

일반적으로 spring-jdbc나 spring-data-jpa 등의 의존성을 포함시키면DataSourceTransactionManage나 JpaTransactionManager 등과 같은 클래스가 PlatformTransactionManager 구현체로 사용됩니다. 이렇게 구성된 TransactionManager는 connection 객체를 생성하고, 트랜잭션의 commit 또는 rollback을 가능하게 합니다.

이렇게 TransactionManager가 선정되면 @EnableTransactionManagement가 선언된 ProxyConfiguration 클래스를 통해 트랜잭션 관리 기능을 활성화합니다. ProxyConfiguration 클래스로 두 가지를 제시하고 있는데 바로 JDK Dynamic Proxy와 CGLIB 방식입니다. JDK Dynamic Proxy는 인터페이스를 기반으로 프록시 객체를 생성하며, CGLIB은 바이트 코드 조작을 통해 원본 객체의 서브클래스를 생성한다는 차이점이 있습니다.

Spring은 AOP 프레임워크를 이용하여 프록시를 생성하고, 특정 메소드 호출을 가로채서 추가 동작을 수행합니다. 여기서 ‘가로챈다’는 것은 Spring AOP의 어드바이스가 수행하는 것으로 각 Jointpoint에서 동작하는 횡단 관심의 공통 기능을 수행합니다.

@Transactional 어노테이션의 속성

@Transactional 어노테이션에는 여러 가지 속성이 있습니다. 그 중에서도 대표적인 속성인 propagation, isolation에 대해 알아보겠습니다.

propagation

propagation은 여러 트랜잭션이 관련된 경우 트랜잭션 전파 방식을 결정합니다. 총 7가지 유형이 있다,

  • REQUIRED: 트랜잭션이 있는 경우 참여하고 없으면 새 트랜잭션을 생성하며 propagation 설정이 없는 경우의 기본값입니다.
  • REQUIRES_NEW: 항상 새 트랜잭션을 만들고 트랜잭션이 있다면 끝날 때까지 일시중지합니다.
  • NESTED: 기존 트랜잭션과 중첩된 트랜잭션을 생성하고, 없다면 새로 트랜잭션을 생성합니다.
  • SUPPORTS: 존재하는 트랜잭션이 있다면 지원하고, 없으면 트랜잭션 없이 메서드만 실행합니다.
  • MANDATORY: 반드시 트랜잭션이 존재해야 하는 유형으로 없으면 예외(ThrowIllegalTransactionStateException)가 발생합니다.
  • NOT_SUPPORTED: 트랜잭션이 있어도 중단되며, 트랜잭션을 지원하지 않습니다.
  • NEVER: 트랜잭션이 존재하면 예외(ThrowIllegalTransactionStateException)가 발생합니다.
@Transactional(propagation = Propagation.REQUIRED)       // Default
@Transactional(propagation = Propagation.REQUIRES_NEW) 
@Transactional(propagation = Propagation.NESTED) 
@Transactional(propagation = Propagation.SUPPORTS) 
@Transactional(propagation = Propagation.MANDATORY) 
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Transactional(propagation = Propagation.NEVER)

isolation

트랜잭션 격리 수준을 설정한다. 격리 수준이란 동시에 여러 트랜잭션이 실행될 때 한 트랜잭션이 다른 트랜잭션의 작업 내용을 어느 정도까지 볼 수 있도록 할건지 정의하는 기준입니다. isolation 속성을 설정하지 않은 경우에는 기본값으로 연결된 DB의 격리수준을 따릅니다.

  • READ_UNCOMMITTED: 가장 낮은 격리수준으로 다른 트랜잭션에서 commit되지 않은 상태의 데이터까지 읽어옵니다. (Dirty read)
  • READ_COMMITTED: UNCOMMITTED와 반대로 commit된 내용만 읽어옵니다. 하지만 트랜잭션들이 동시에 수행되고 있다면 commit 이후의 데이터가 다른 동시성 문제가 발생할 수 있습니다. 같은 쿼리를 두 번 실행할때 결과가 다를수 있습니다.(Nonrepeatable read)
  • REPEATABLE_READ: 하나의 트랜잭션에 하나의 스냅샷을 이용하기 때문에 트랜잭션 내에서 같은 데이터를 여러 번 읽어도 항상 같은 결과를 보장합니다. 또한 다시 데이터를 조회하는 과정에서 새로 추가되거나 제거된 값을 가져올 수 있습니다. (Phantom read)
  • SERIALIZABLE: 가장 높은 격리수준으로 Read시에 DML 작업이 불가능하기 때문에 동시성이 낮습니다.
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Transactional(isolation = Isolation.READ_COMMITTED) 
@Transactional(isolation = Isolation.REPEATABLE_READ) 
@Transactional(isolation = Isolation.SERIALIZABLE)

그 외에도 읽기전용 여부를 표시하는 readOnly, 트랜잭션 타임아웃 시간을 설정하는 timeout과 같은 속성들이 존재합니다.

사용시 주의점

1. 트랜잭션을 적용하려는 메서드는 반드시 public으로 선언되어야 합니다.
프록시 객체로 외부에서 접근 가능한 인터페이스를 제공해야 하기 때문입니다. 만약 해당 메서드가 private이나 protected로 선언되어 있다면, 프록시 객체가 생성될 때 해당 메서드에 접근할 수 없으므로 @Transactional 어노테이션을 사용한 트랜잭션 관리가 불가능합니다.

2. 다른 AOP 기능과의 충돌을 고려해야 합니다.
예를 들어 @Secured를 통해 권한이 있는 사용자 여부를 확인하는데 @Transactional이 먼저 수행된다면 권한 검사가 무의미해집니다.
이를 방지하기 위해서는 @Order를 이용해 적용 순서를 정하거나 적용범위를 조정해서 해결할 수 있습니다. 이외에도 @Transactional proxyTargetClass 속성을 true로 설정하여 강제로 클래스를 대상으로 프록시를 사용하도록 지정할 수도 있습니다. 하지만 성능저하가 발생할 수 있다는 단점이 있습니다.

3. Service 계층에서 사용하자.
Service 계층에서 @Transactional을 사용하므로써 여러 데이터베이스 작업들을 원자적으로 처리할 수 있습니다. 그리고 Spring에서 단일 책임원칙에 따라 Database 계층에서 비즈니스 로직과 관련 없는 역할을 담당해 코드의 유지보수와 확장성이 높이기 위함입니다.

4. Exception을 고려하자.

트랜잭션은 RuntimeException과 Error에서는 롤백되지만, Checked exceptions에서는 롤백되지 않습니다. Checked exceptions는 예측가능한 에러를 말하는데, 아래와 같이 @Transactional에 rollbackFor 속성을 두어 롤백처리가 되도록 할 수도 있습니다.

@Transactional(rollbackFor={Exception.class})

트랜잭션 Checked exception이 발생했을 때 Java와 Kotlin
Java에서는 롤백되지 않고 Checked exception을 try-catch, throw 방식으로 처리하고 있지만 Kotlin에서는 트랜잭션이 롤백되는 것을 볼 수 있습니다. 하지만 Java에서처럼 롤백이 되지 않도록 @Throws를 사용해서 처리할 수도 있습니다.
따라서 개발자는 어떤 언어를 스프링과 사용하는지 그리고 Custom Exception 설정을 어떻게 하는지에 따라 다양한 Exception 처리방법을 고려해야 합니다.

5. 트랜잭션과 DeadLock
실제로 DeadLock 이슈가 발생해서 리펙토링을 하면서 가장 유심히 본 부분 중 하나입니다. Service 계층에서 설정한 메서드들이 데이터베이스에서 어떤 방향으로 리소스를 점유하는지는 매우 중요합니다.

실제 복잡한 서비스에 트랜잭션 순서를 일치시키거나 최적화시키는 것은 쉬운 일이 아닙니다. 하지만 잘못된 트랜잭션으로 DeadLock이 발생하면 위 사진의 spid 76번처럼 희생양 프로세스가 생기거나 반복적인 DeadLock 이슈로 서버 성능에 문제가 발생할 수도 있습니다.

JPA는 왜 SimpleJpaRepository에 @Transactional을 사용할까

만약 Service 계층에서 @Transactional을 깜빡했더라도, 리포지토리의 save()를 호출하면 SimpleJpaRepository에 걸려 있는 트랜잭션이 작동합니다.

Mixed Transaction이 발생해도 일반적으로 Service 계층의 @Transactional이 우선순위를 갖습니다.

따라서 SimpleJpaRepository에 @Transactional이 있는 이유는 어떤 상황에서도(서비스에서 트랜잭션을 잊었더라도) JPA의 영속성 컨텍스트가 정상적으로 동작하고 데이터를 안전하게 보호하기 위함입니다.

왜 같은 클래스 내에서 @Transactional 메서드를 호출하면 작동하지 않을까

프록시는 외부 호출을 가로채는 방식인데, 내부 호출은 프록시를 거치지 않고 this를 통해 직접 메서드를 호출하기 때문에 트랜잭션 부가 기능이 적용되지 않습니다.


참고 링크

@Transactional이란 트랜잭션을 관리하기 위해 스프링에서 제공하는 어노테이션이다. 메서드 레벨 또는 클래스 레벨에서 사용할 수 있으며 해당 메서드 또는 해당 클래스 전체 public 메서드에 트랜잭션을 적용한다. 만약 예외가 발생하면 rollback 시켜서 여러 작업들을 안전하게 묶어 처리할 수 있도록 한다. @Transactional은 스프링 AOP를 통해 프록시 방식으로 동작한다. 애플리케이션 로딩 시점에 해당 어노테이션이 붙은 빈을 감싸는 프록시 객체가 생성된다. 클라이언트가 메서드를 호출하면 실제 객체가 아닌 이 프록시가 대신 호출된다. 프록시는 내부적으로 Transaction Advisor를 통해 트랜잭션을 시작(begin)하고, 실제 비즈니스 로직(Target)을 호출한다. 로직이 정상 종료되면 Commit을, 예외가 발생하면 Rollback을 수행한 뒤 트랜잭션을 종료한다. 이를 통해 개발자는 비즈니스 로직에만 집중하고, 트랜잭션 처리라는 부가 기능은 선언적으로 관리할 수 있다는 장점이 있다. 이를 통해 데이터베이스에서 일어나는 여러 작업들을 안전하게 묶어서 처리할 수 있으며, 데이터 일관성을 유지하고 문제가 발생해도 데이터의 무결성을 보장할 수 있다.

profile
Start fast to fail fast

0개의 댓글