Spring Boot에서 @Transactional 어노테이션이 작동하지 않을 때 해결 방법

서론

트랜잭션이 필요한 서비스 메소드에 아무리 @Transactional 어노테이션을 적용해도 트랜잭션이 발생하지 않는 상황에 처할 수 있습니다. 이런 경우, 문제의 원인을 파악하고 해결하기 위해 여러 가지 요소를 점검해야 합니다. @Transactional은 Spring 프레임워크에서 트랜잭션 관리를 위해 매우 중요한 어노테이션이지만, 설정이나 코드 구조에 따라 예상과 다르게 동작할 수 있습니다. 이 글에서는 @Transactional이 작동하지 않는 일반적인 원인과 그 해결 방법을 구체적인 예시와 함께 자세히 살펴보겠습니다.


1. 메소드 접근 제어자 확인하기

@Transactional이 적용된 메소드는 public 접근 제어자를 가져야 합니다. Spring의 AOP는 기본적으로 프록시 기반으로 동작하며, public 메소드에만 적용됩니다. 따라서 private, protected, default(package-private) 접근 제어자를 가진 메소드에는 트랜잭션이 적용되지 않습니다.

@Service
public class OrderService {

    @Transactional
    void processOrder() {
        // 주문 처리 로직
    }
}

위의 코드에서 processOrder() 메소드는 접근 제어자가 명시되지 않았으므로 default입니다. 이 경우, @Transactional이 효과를 발휘하지 않습니다. 때문에 메소드의 접근 제어자를 public으로 사용해 줍니다.

@Service
public class OrderService {

    @Transactional
    public void processOrder() {
        // 주문 처리 로직
    }
}

Spring AOP는 기본적으로 프록시 패턴을 사용하여 대상 객체를 감싸서 부가 기능을 제공합니다. 이때, 프록시는 대상 객체의 public 메소드만 오버라이드하여 기능을 추가할 수 있습니다. 따라서 public이 아닌 메소드는 프록시의 영향을 받지 않아 트랜잭션이 적용되지 않습니다.


2. 자기 자신 호출 (Self-invocation) 문제 검토하기

클래스 내에서 자신의 메소드를 호출할 때, @Transactional이 적용된 메소드라도 프록시를 거치지 않기 때문에 트랜잭션이 적용되지 않습니다.

@Service
public class UserService {

    @Transactional
    public void createUser(User user) {
        // 사용자 생성 로직
    }

    public void registerUser(User user) {
        // 기타 처리 로직
        createUser(user); // 자기 자신 메소드 호출
    }
}

registerUser() 메소드에서 createUser()를 호출할 때 트랜잭션이 적용되지 않습니다. 때문에 외부에서 호출하도록 구조적으로 createUser() 메소드를 다른 클래스에서 호출하도록 변경합니다.

@Service
public class UserRegistrationService {

    private final UserService userService;

    public UserRegistrationService(UserService userService) {
        this.userService = userService;
    }

    public void registerUser(User user) {
        // 기타 처리 로직
        userService.createUser(user); // 프록시를 통해 호출
    }
}

@Service
public class UserService {

    @Transactional
    public void createUser(User user) {
        // 사용자 생성 로직
    }
}

혹은 자신을 빈으로 주입받아서 사용하게 할 수도 있습니다.

@Service
public class UserService {

    private final UserService self;

    @Autowired
    public UserService(UserService self) {
        this.self = self;
    }

    public void registerUser(User user) {
        // 기타 처리 로직
        self.createUser(user); // 프록시를 통해 호출
    }

    @Transactional
    public void createUser(User user) {
        // 사용자 생성 로직
    }
}

다만, 자기 자신을 주입받을 때 순환 참조 문제가 발생할 수 있으므로 @Lazy 어노테이션을 사용하거나 구조를 재설계하는 것이 좋습니다.


3. 프록시 대상 객체 타입 확인하기

프록시 객체를 직접 캐스팅하거나 구체적인 클래스 타입에 의존하면 트랜잭션이 예상대로 동작하지 않을 수 있습니다. paymentService는 프록시 객체이기 때문에 instanceof 검사가 예상대로 동작하지 않을 수 있습니다.

@Service
public class PaymentService {

    @Transactional
    public void processPayment() {
        // 결제 처리 로직
    }
}

public class PaymentController {

    @Autowired
    private PaymentService paymentService;

    public void initiatePayment() {
        if (paymentService instanceof PaymentService) {
            // ...
        }
        paymentService.processPayment();
    }
}

때문에 인터페이스를 사용하여 의존성을 주입하고, 구현 클래스에 의존하지 않게 해야 합니다. 프록시 객체는 실제 구현 클래스가 아닌 인터페이스를 기반으로 생성되기 때문에, 인터페이스를 사용하면 이러한 문제를 회피할 수 있습니다. 또한, CGLIB 프록시를 사용하도록 설정하면 클래스 기반 프록시를 사용할 수 있지만, 이는 추가 설정이 필요합니다.

public interface PaymentService {
    void processPayment();
}

@Service
public class PaymentServiceImpl implements PaymentService {

    @Override
    @Transactional
    public void processPayment() {
        // 결제 처리 로직
    }
}

public class PaymentController {

    @Autowired
    private PaymentService paymentService;

    public void initiatePayment() {
        paymentService.processPayment();
    }
}

4. 예외 처리 방식 확인하기

Spring은 기본적으로 RuntimeException 또는 Error가 발생할 때만 트랜잭션을 롤백합니다. 체크 예외(Exception)는 트랜잭션 롤백을 유발하지 않습니다.

@Transactional
public void updateAccount() throws Exception {
    // 계좌 업데이트 로직
    if (someCondition) {
        throw new Exception("예외 발생");
    }
}

 

위의 코드에서 Exception이 발생해도 트랜잭션이 롤백되지 않습니다. 때문에 롤백 대상 예외를 명시적으로 지정할 수 있는 rollbackFor 속성을 사용하여 예외를 명시적으로 지정합니다.

@Transactional(rollbackFor = Exception.class)
public void updateAccount() throws Exception {
    // 계좌 업데이트 로직
    if (someCondition) {
        throw new Exception("예외 발생");
    }
}

또는, 예외를 RuntimeException으로 변경하거나 새로운 커스텀 예외를 만들어 RuntimeException을 상속받게 할 수 있습니다.

public class CustomException extends RuntimeException {
    public CustomException(String message) {
        super(message);
    }
}

@Transactional
public void updateAccount() {
    // 계좌 업데이트 로직
    if (someCondition) {
        throw new CustomException("예외 발생");
    }
}

@Transactional 어노테이션의 rollbackFor 속성을 사용하여 롤백할 예외를 지정할 수 있습니다. 반대로, noRollbackFor 속성을 사용하여 롤백하지 않을 예외를 지정할 수도 있습니다.


5. 트랜잭션 매니저 설정 확인하기 (2개 이상의 데이터 소스 사용시 필수확인)

여러 데이터 소스를 사용하는 경우 또는 Spring Boot의 자동 설정을 사용하지 않는 경우, 적절한 트랜잭션 매니저가 설정되지 않으면 트랜잭션이 작동하지 않습니다. 여러 데이터 소스를 사용하는 경우 각 데이터 소스에 대한 트랜잭션 매니저를 설정하고, @Transactional에서 사용할 트랜잭션 매니저를 지정합니다.

@Bean
public PlatformTransactionManager firstTransactionManager() {
    return new DataSourceTransactionManager(firstDataSource());
}

@Bean
public PlatformTransactionManager secondTransactionManager() {
    return new DataSourceTransactionManager(secondDataSource());
}

@Transactional("firstTransactionManager")
public void methodUsingFirstDataSource() {
    // ...
}

@Transactional("secondTransactionManager")
public void methodUsingSecondDataSource() {
    // ...
}

 

Spring Boot를 사용하면 기본적으로 하나의 데이터 소스와 트랜잭션 매니저가 자동으로 설정됩니다. 그러나 여러 데이터 소스를 사용하거나 커스텀 설정이 필요한 경우에는 명시적으로 설정해야 합니다.


6. Spring AOP 설정 확인하기 (XML 기반의 Spring 사용시 유의)

Spring의 AOP 설정이 누락되거나 잘못되면 @Transactional이 적용되지 않습니다. 특히 XML 설정을 사용하는 경우 tx:annotation-driven 태그가 누락될 수 있습니다. Spring Boot를 사용하는 경우는 자동으로 설정되므로 특별한 설정이 필요 없습니다. @SpringBootApplication이 있는지 확인만 해주면 됩니다.

Spring 프레임워크를 사용하는 경우 @EnableTransactionManagement 어노테이션을 추가합니다.

@Configuration
@EnableTransactionManagement
public class AppConfig {
    // 기타 설정
}

 

XML 설정을 사용하는 경우 Bean 등록을 아래와 같이 해줍니다.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:tx="http://www.springframework.org/schema/tx"
       ...>
    <tx:annotation-driven />
    <!-- 기타 빈 설정 -->
</beans>

 

@EnableTransactionManagement 어노테이션은 트랜잭션 어노테이션을 활성화하며, Spring의 AOP 기능을 사용하여 트랜잭션을 적용할 수 있게 합니다


7. 빈(Bean) 등록 여부 확인하기

@Transactional이 적용된 클래스가 Spring 컨테이너에서 관리되지 않으면 어노테이션이 효과가 없습니다.

public class NotificationService {

    @Transactional
    public void sendNotification() {
        // 알림 발송 로직
    }
}

위의 클래스는 아무런 어노테이션이 없으므로 Spring의 빈으로 등록되지 않습니다. 클래스에 적절한 어노테이션을 추가하여 빈으로 등록해 줘야 합니다.

@Service
public class NotificationService {

    @Transactional
    public void sendNotification() {
        // 알림 발송 로직
    }
}
// xml 기반 spring의 경우
<bean id="notificationService" class="com.example.NotificationService" />

 

Spring 컨테이너에서 관리되지 않는 객체는 @Transactional 뿐만 아니라 다른 Spring의 기능들도 사용할 수 없습니다. 반드시 빈으로 등록되어야 합니다.


8. 데이터베이스 설정 및 JDBC URL 확인하기.

데이터베이스 연결 설정이 잘못되었거나 자동 커밋 모드로 설정되어 있으면 트랜잭션이 적용되지 않을 수 있습니다.

// properties setting file
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?autoCommit=true

autoCommit=true로 설정되어 있으면 트랜잭션이 즉시 커밋되어 롤백이 불가능합니다. 때문에 autoCommit을 false로 설정하거나 기본값을 사용해야 합니다.

// properties setting file
spring.datasource.url=jdbc:mysql://localhost:3306/mydb

일부 데이터베이스는 자동 커밋 모드가 기본값으로 설정되어 있을 수 있습니다. DataSource 설정 시 autoCommit 옵션을 명시적으로 설정하는 것이 좋습니다.


9. 테스트 코드에서의 트랜젝션 적용 확인하기.

JUnit이나 테스트 프레임워크에서 트랜잭션이 예상과 다르게 동작할 수 있습니다.

@RunWith(SpringRunner.class)
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    public void testCreateUser() {
        userService.createUser(new User());
        // 데이터베이스에 커밋되지 않을 수 있음
    }
}

 

이럴 경우 테스트 메소드에 @Transactional 어노테이션을 추가하거나 @Rollback 어노테이션을 사용할 수 있습니다.

@Test
@Transactional
public void testCreateUser() {
    userService.createUser(new User());
    // 테스트 종료 시 롤백됨
}

@Test
@Transactional
@Rollback(false)
public void testCreateUser() {
    userService.createUser(new User());
    // 테스트 종료 시 커밋됨
}

 

테스트 환경에서는 기본적으로 각 테스트 메소드마다 트랜잭션이 적용되고, 테스트가 끝나면 롤백됩니다. 따라서 데이터베이스 상태를 확인하려면 @Rollback(false)를 사용하여 커밋해야 확인이 가능하지만 테스트케이스에서 실제 사용하는 RDB로의 CRUD는 권장하지는 않습니다.


10. 트랜잭션 전파(propagation) 설정 확인하기.

트랜잭션 전파 속성이 예상과 다르게 설정되어 있으면 트랜잭션이 제대로 적용되지 않을 수 있습니다.

@Transactional(propagation = Propagation.NEVER)
public void method() {
    // 트랜잭션을 사용하지 않음
}

때문에 적절한 전파 속성을 사용해야 하고, 기본값인 Propagation.REQUIRED를 사용하거나 필요한 전파 속성을 설정합니다.

@Transactional(propagation = Propagation.REQUIRED)
public void method() {
    // 트랜잭션이 필요한 로직
}

트랜잭션 전파 속성은 메소드 간 트랜잭션의 전파 방식을 결정합니다. 상황에 맞는 전파 속성을 사용하여 트랜잭션이 올바르게 적용되도록 해야 합니다.


결론

@Transactional 어노테이션이 작동하지 않을 때는 여러 가지 원인이 있을 수 있으며, 그 중 일부는 미묘한 차이로 인해 쉽게 놓칠 수 있습니다. 접근 제어자, 자기 자신 호출, 예외 처리 방식, 트랜잭션 매니저 설정, AOP 설정, 빈 등록 여부, 데이터베이스 설정 등을 꼼꼼히 점검해야 합니다. 

트랜잭션은 데이터의 무결성과 일관성을 유지하는 데 핵심적인 역할을 하므로, 정확한 설정과 이해가 필수적입니다. 위에서 소개한 각 예시와 해결 방법이 @Transactional 관련 문제를 해결하는 데 도움이 되기를 기원합니다.