본문 바로가기
Spring

SpringBoot @Transactional이 동작하지 않는 이유와 해결법

by 긍고 2024. 10. 2.
반응형

개요


예전에 개발 중 트랜잭션 처리가 자동으로 되지 않는 문제를 경험한 적이 있었다. 처음에 @Transactional 어노테이션을 사용하면 모든 트랜잭션이 알아서 처리될 것이라고 생각했었다. 하지만 예상대로 트랜잭션이 작동하지 않고, 데이터가 반영되지 않는 문제가 발생해 한참을 헤맸다. 결국, 트랜잭션이 동작하지 않는 다양한 이유를 알게 되었고, 그 해결 방법을 찾게 되었다. 이 글에서는 @Transactional이 왜 동작하지 않는지, 그리고 그 해결법을 예시와 함께 설명해보려고 한다.

 

@Transactional 기본 개념


@Transactional은 스프링 프레임워크에서 제공하는 어노테이션으로, 데이터베이스 작업에서 트랜잭션을 관리하는 데 사용된다. 트랜잭션이란 작업의 일련의 과정을 하나로 묶어주는 개념으로, 작업이 모두 성공해야만 데이터베이스에 반영된다. 반대로 중간에 실패하면 모든 작업이 원복(rollback)된다. @Transactional을 사용하면 이 과정을 자동으로 처리할 수 있다. 하지만 이 어노테이션이 항상 동작하지 않는 경우가 있어, 그 이유를 잘 이해하는 것이 중요하다.

 

자주 발생하는 문제 상황


많은 개발자들이 @Transactional을 사용하면서 자주 겪는 문제는 트랜잭션이 기대한 대로 동작하지 않는 것이다. 예를 들어, 데이터가 제대로 커밋되지 않거나, 오류가 발생해도 롤백되지 않는 경우가 있다. 이러한 문제는 종종 @Transactional의 기본적인 동작 원리를 오해하거나 잘못된 설정 때문일 수 있다.

 

@Transactional이 동작하지 않는 이유와 해결법


1. 프록시 기반 트랜잭션 관리

스프링에서 @Transactional은 기본적으로 프록시를 사용해 동작한다. 이는 클래스나 메서드를 호출할 때 프록시가 트랜잭션을 관리하는 방식이다. 하지만 트랜잭션을 관리하는 프록시가 예상대로 동작하지 않는 경우가 종종 있다. 그 이유 중 하나는 자기 자신을 호출하는 메서드 때문이다. @Transactional이 적용된 메서드가 같은 클래스 내에서 다른 메서드를 호출하면 프록시가 동작하지 않는다.

 

해결법: 자기 자신을 호출하지 않고, 별도의 서비스로 분리하거나, AOP 구조로 설계하는 것이 필요하다.

@Service
public class MyService {

    @Transactional
    public void outerMethod() {
        // 내부 메서드 호출
        innerMethod();  // 트랜잭션이 적용되지 않음
    }

    @Transactional
    public void innerMethod() {
        // DB 작업
    }
}

 

위 예시에서 outerMethod()가 내부적으로 innerMethod()를 호출할 때, 트랜잭션이 예상대로 적용되지 않는다. 이 경우, innerMethod()는 프록시를 통해 호출되지 않기 때문이다.


2. 메서드 접근 제어자 문제

@Transactional은 기본적으로 public 메서드에만 동작한다. 따라서 메서드가 private 또는 protected로 선언되어 있으면 트랜잭션이 적용되지 않는다.

 

해결법: 트랜잭션을 적용할 메서드를 반드시 public으로 선언해야 한다.

@Service
public class MyService {

    @Transactional
    public void validTransactionMethod() {
        // 정상적으로 트랜잭션이 적용됨
    }

    @Transactional
    private void invalidTransactionMethod() {
        // 트랜잭션이 적용되지 않음
    }
}

3. 읽기 전용 트랜잭션 설정

@Transactional에는 readOnly 속성을 설정할 수 있다. 이를 true로 설정하면 데이터베이스 읽기 전용 작업에 최적화되지만, 쓰기 작업을 시도하면 오류가 발생할 수 있다.

 

해결법: 데이터 변경이 발생하는 작업에서는 readOnly를 false로 설정하거나, 생략해야 한다.

@Transactional(readOnly = true)
public void readData() {
    // 읽기 전용 작업
}

@Transactional
public void writeData() {
    // 쓰기 작업
}

 


4. 예외 처리 문제

기본적으로 @Transactional은 Unchecked Exception(런타임 예외)만 롤백 처리를 한다. 만약 Checked Exception(체크 예외)을 던질 경우, 트랜잭션이 롤백되지 않는다.

 

해결법: 특정 예외에 대해 롤백이 필요하다면 rollbackFor 속성을 추가해야 한다.

@Transactional(rollbackFor = Exception.class)
public void saveData() throws Exception {
    // 체크 예외가 발생해도 롤백 처리됨
}

 

예시 코드


다음은 @Transactional이 적용된 서비스 클래스의 예시이다.

@Service
public class TransactionalService {

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void createUser(User user) {
        userRepository.save(user);
        if (user.getName() == null) {
            throw new RuntimeException("이름이 null일 수 없습니다.");  // 롤백 처리됨
        }
    }
}

 

위 코드에서는 @Transactional이 적용되어 있어, 만약 사용자 이름이 null이면 예외가 발생하고 데이터가 롤백된다. 반대로 예외가 발생하지 않으면 데이터가 정상적으로 저장된다.

 

정리


@Transactional은 스프링 애플리케이션에서 중요한 역할을 하는 어노테이션이다. 하지만 트랜잭션이 동작하지 않는 상황이 발생할 수 있으므로, 그 원인과 해결 방법을 미리 알고 있어야 한다. 자기 자신을 호출하는 문제, 메서드 접근 제어자, 예외 처리 방식 등 다양한 원인이 존재하기 때문에 이를 잘 이해하고 사용하는 것이 중요하다.

 

댓글