Java/Spring

[Spring] 트랜잭션

댕주 2025. 3. 16. 23:43

1. Spring에서 트랜잭션 관리

Spring 프레임워크에서는 @Transactional 어노테이션을 사용해 간단히 트랜잭션을 관리할 수 있다. 트랜잭션은 보통 서비스 계층에서 사용되며, 데이터베이스의 상태를 변경하는 작업을 안전하게 처리한다.

@Service
public class ExampleService {
    @Transactional
    public void insertDatabase() {
        // logic
    }
}

 

위 코드에서 @Transactional 을 메서드에 적용하면, 메서드가 실행되는 동안 트랜잭션이 활성화된다. 만약 메서드 내에서 예외가 발생하면 트랜잭션이 자동으로 롤백된다.

2. 전파 레벨(Propagation)

Spring의 트랜잭션은 다양한 전파 레벨(Propagation)을 제공한다. 전파 레벨은 트랜잭션이 중첩된 상황에서 어떻게 동작할지 결정한다.

  • REQUIRED:
    기본 설정으로, 현재 트랜잭션이 있으면 그 트랜잭션을 사용하고, 없으면 새로운 트랜잭션을 생성한다.

  • REQUIRES_NEW:
    항상 새로운 트랜잭션을 생성하고, 현재 트랜잭션이 존재하면 일시 중단한다.
    외부 트랜잭션과 상관없이 커밋하거나 롤백이 필요할 때 유용하다.

  • NESTED:
    현재 트랜잭션 내에서 중첩된 트랜잭션을 시작한다.

  • SUPPORTS:
    트랜잭션이 존재하면 트랜잭션 내에서 실행되고, 없으면 트랜잭션 없이 실행된다.

  • NOT_SUPPORTED:
    트랜잭션이 존재하면 일시 중단하고, 트랜잭션 없이 실행된다.

  • MANDATORY:
    항상 트랜잭션 내에서 실행되어야 하며, 트랜잭션이 없으면 예외가 발생한다.

  • NEVER:
    트랜잭션 없이 실행되어야 하며, 트랜잭션이 존재하면 예외가 발생한다.

3. 격리 수준(Isolation Level)

트랜잭션은 동시에 여러 사용자가 접근할 때 발생할 수 있는 문제를 해결하기 위해 격리 수준을 설정할 수 있다. 주요 격리 수준은 다음과 같다.

  • READ_UNCOMMITTED: 다른 트랜잭션에서 커밋되지 않은 데이터도 읽을 수 있다.
  • READ_COMMITTED: 다른 트랜잭션에서 커밋한 데이터만 읽을 수 있다.
  • REPEATABLE_READ: 트랜잭션 내에서 동일한 데이터를 여러 번 읽어도 동일한 결과를 보장한다.
  • SERIALIZABLE: 트랜잭션이 완전히 직렬화되며 가장 높은 격리 수준을 제공한다.

4. 트랜잭션 롤백 및 커밋

@Transactional 어노테이션이 적용된 메서드는 실행 중 예외가 발생하면 기본적으로 롤백된다. 일반적으로 RuntimeException 이나 Error 가 발생하면 롤백되고, checked exception 은 롤백되지 않는다. 특정 예외에 대해 롤백하거나 커밋을 제어하고 싶다면 rollbakFor  noRollbackFor 속성을 사용할 수 있다.

@Transactional(rollbackFor = CustomException.class)
public void insertDatabase() {
    // 예외 발생 시 CustomException에 대해서만 롤백
}

 

5. 트랜잭션 관리 전략

Java에서는 트랜잭션을 직접 관리할 수도 있지만, Spring과 같은 프레임워크를 사용하면 코드의 복잡성을 줄이고 일관된 트랜잭션 관리를 쉽게 할 수 있다. 트랜잭션 관리 전략은 다음과 같다.

  • 프로그래밍 방식 관리: 코드 내에서 트랜잭션 시작과 종료를 명시적으로 처리한다. (▶https://yujuharu.tistory.com/40)
  • 선언적 관리: @Transactional 과 같은 어노테이션을 통해 트랜잭션을 선언적으로 설정한다.

같은 서비스 클래스 내 메서드에
@Transactional(propagation = REQUIRES_NEW)를 적용해도
트랜잭션이 분리되지 않는 이유

: AOP(Aspect-Oriented Programming)

 

Spring에서는 @Transactional 이 프록시(Proxy) 패턴을 통해 동작하는데, 이 프록시는 다른 클래스에서 호출될 때만 제대로 작동하기 때문이다.

 

즉, 같은 클래스 내에서 REQUIRES_NEW 트랜잭션을 가진 메서드를 호출하면 프록시를 거치지 않고 바로 호출되기 때문에 전파 레벨 설정이 제대로 되지 않아 새로운 트랜잭션이 생성되지 않는다.

 

구체적 원인

Spring에서는 @Transactional 어노테이션이 설정된 메서드를 실행할 때, 프록시 객체가 트랜잭션 설정을 적용하는 역할을 한다. 하지만 같은 클래스 내부의 메서드를 호출하면 프록시를 통하지 않고 클래스의 인스턴스가 직접 호출하기 때문에 트랜잭션 설정이 무시된다.

 

@Service
public class ExampleService {
    @Transactional
    public void outerMethod() {
        innerMethod(); // 같은 클래스의 메서드를 직접 호출
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void innerMethod() {
        // 새로운 트랜잭션을 생성하려고 하지만 실패
    }
}

 

위 코드에서 outerMethod()  innerMethod() 를 호출할 때, innerMethod()  REQUIRES_NEW 설정이 적용되지 않아서 새로운 트랜잭션이 생성되지 않고 outerMethod()  트랜잭션을 그대로 사용하게 된다.

이렇게 되면 REQUIRES_NEW 로 설정한 목적을 달성할 수 없게 된다.

 

해결 방법

이 문제를 해결하려면 같은 클래스 내부에서 트랜잭션이 분리된 메서드를 호출하지 않고, 다른 클래스에서 해당 메서드를 호출하도록 설계하는 것이 필요하다. 예를 들어, ExampleService 클래스에서 REQUIRES_NEW 가 필요한 메서드를 다른 서비스로 분리할 수 있다.

@Service
@RequiredArgsConstructor
public class ExampleService {
    private final NewTransactionalService newTransactionalService;
    
    @Transactional
    public void outerMethod() {
        newTransactionalService.innerMethod(); // 다른 클래스의 메서드를 호출하므로 프록시가 적용
    }
}

@Service
public class NewTransactionalService {
    @Transactional(propagation = REQUIRES_NEW)
    public void innerMethod() {
        // logic
    }
}

 

 

'Java > Spring' 카테고리의 다른 글

[Spring] 프록시 패턴(Proxy Pattern)  (0) 2025.03.17
[Spring Boot] 의존성 주입(DI)  (0) 2025.03.16