본문 바로가기
Java 웹 프로그래밍

스프링 @Transactional 어노테이션을 사용하는 가장 좋은 방법

by irerin07 2024. 2. 9.
728x90

Spring Transactional annotation

스프링은 1.0 버전부터 AOP 기반의 트랜잭션을 지원했고, 이를 사용하여 개발자들은 트랙잭션 범위를 선언적으로 지정할 수 있었습니다.

 

얼마 지나지 않아 1.2 버전에서 스프링은 @Transactional 어노테이션을 추가했고, 이로써 트랜잭션 범위를 지정하는것이 훨씬 더 쉬워졌습니다.

 

@Transactional 어노테이션은 다음 속성들로 설정이 가능합니다.

  • value와 transactionManager - value는 transactionManager와 동일(alias)하게 사용됩니다. 이를 사용해 @Transactional 어노테이션이 사용된 블록에서 사용될 TransactioinManager의 참조를 제공합니다.
  • propagation - @Transactional 어노테이션이 사용된 블록에서 직접적으로 혹은 간접적으로 호출 된 다른 메서드들에 트랜잭션 범위가 어떻게 전파될지 정의합니다. 기본값은 REQUIRED이며 이미 사용 가능한 트랜잭션이 없는 경우 트랜잭션이 시작됨을 의미합니다. 그렇지 않은 경우에는 이미 사용되고 있는 트랜잭션이 사용 됩니다.

Propagation REQUIRED

  • timeout과 timeoutString - TransactionTimedOutException을 발생시키기 전까지 메소드가 실행될 수 있는 시간을 초 단위로 설정합니다.
  • readOnly - 현재 트랜잭션이 읽기 전용인지 아니면 읽기-쓰기까지 가능한지 정의합니다.
  • rollbackFor와 rollbackForClassName - 현재 트랙잰셕이 롤백 되는 하나 이상의 Throwable 클래스를 정의 합니다. 기본적으로 트랜잭션은 RuntimeException이나 Error가 발생하면 롤백이 되고 checked Exception이 발생하면 롤백되지 않습니다.
간혹 많은 블로그에서 RuntimeException과 CheckedException의 차이로 Transaction에서 롤백을 한다 하지 않는다로 정리하고 있는데 백기선님의 다음 영상을 보시면 조금 더 명확하게 이해할 수 있습니다.
https://www.youtube.com/watch?v=_WkMhytqoCc
영상 내용을 요약하자면 Transaction은 기본적으로 예외처리를 어떻게 한다는 규칙이 없습니다.
정확하게는 어떤 Transaction을 뜻하는 것인지 알아야 합니다.
예를 들어 DB Transaction의 경우 checked/unchecked exception 발생시 롤백 여부를 정하는 것은 개발자입니다.
많은 블로그에서 인용하는 표의 기원은 스프링의 트랜잭션 처리에서 나온 것인데, 스프링은 기본적으로 Runtime 계열을 바로 롤백을 하고 check exception은 롤백을 하지 않지만, 이 역시도 개발자가 설정할 수 있습니다.
  • noRollBackFor와 noRollbackForClassName - 현재 트랜잭션이 롤백 되지 않는 하나 이상의 Throwable 클래스를 정의 합니다. 보통 이 속성에는 해당 트랜잭션에서 발생할 수 있으나 트랜잭션이 롤백되지 않아야 하는 하나 이상의 RuntimeException을 정의합니다.

스프링 Transactional 어노테이션은 어느 레이어에 속할까?

@Transactional 어노테이션은 서비스 레이어에 속하는데, 트랜잭션의 범위를 정의하는 것은 서비스 레이어의 책임이기 때문입니다.

 

웹 레이어에 @Transactional 어노테이션을 사용하는 경우 데이터베이스 트랜잭션 응답 시간을 증가 시킬 수 있고, deadlock이나 optimistic locking 같은 데이터베이스 트랜잭션 오류 발생시 명확한 메시지를 제공하기 어려워질 수 있기 때문입니다.

 

DAO 혹은 Repository 레이어는 애플리케이션 레벨 트랜잭션을 요구하지만 이 트랜잭션은 서비스 레이어에서 전파되어야 합니다.

 

스프링 Transactional 어노테이션을 사용하는 가장 좋은 방법

서비스 레이어에는 데이터베이스와 관련이 있거나 혹은 관련이 없는 서비스가 함께 있을 수 있습니다. 만약 어떤 비즈니스 유스케이스가 두 형태의 서비스를 모두 사용한다면 (예를 들어 전달 받은 데이터를 분석하고, 리포트를 만든 뒤, 결과를 데이터 베이스에 저장하는 서비스 같은) 데이터베이스 트랜잭션은 최대한 늦게 시작되는 것이 가장 좋습니다.

그렇기에 우리는 다음과 같이 Non-Transactional한 게이트웨이 역할의 서비스를 사용할 수 있습니다.

@Service
public class RevolutStatementService {
 
    @Transactional(propagation = Propagation.NEVER)
    public TradeGainReport processRevolutStocksStatement(
            MultipartFile inputFile,
            ReportGenerationSettings reportGenerationSettings) {
        return processRevolutStatement(
            inputFile,
            reportGenerationSettings,
            stocksStatementParser
        );
    }
     
    private TradeGainReport processRevolutStatement(
            MultipartFile inputFile,
            ReportGenerationSettings reportGenerationSettings,
            StatementParser statementParser
    ) {
        ReportType reportType = reportGenerationSettings.getReportType();
        String statementFileName = inputFile.getOriginalFilename();
        long statementFileSize = inputFile.getSize();
 
        StatementOperationModel statementModel = statementParser.parse(
            inputFile,
            reportGenerationSettings.getFxCurrency()
        );
        int statementChecksum = statementModel.getStatementChecksum();
        TradeGainReport report = generateReport(statementModel);
 
        if(!operationService.addStatementReportOperation(
            statementFileName,
            statementFileSize,
            statementChecksum,
            reportType.toOperationType()
        )) {
            triggerInsufficientCreditsFailure(report);
        }
 
        return report;
    }
}

 

processRevolustionStocksStatement 메서드는 non-transactional 하기 때문에 Propagation.NEVER 전략을 사용해 processRevolustionStocksStatement 메서드가 활성화된 트랜잭션에서 호출되는 일이 절대 없도록 만들 수 있습니다.

 

statementParser.parse와 generateReport 메서드는 데이터베이스 커넥션을 만들고 유지하지 않아도 되는 애플리케이션 레벨의 처리만 하기 때문에  non-transactional 컨텍스트에서 실행되도 문제가 없습니다.

 

operationService.addStatementReportOperation 메서드만 transactional 컨텍스트에서 실행될 필요가 있기 때문에 @Transactional 어노테이션을 사용합니다.

@Service
@Transactional(readOnly = true)
public class OperationService {
 
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public boolean addStatementReportOperation(
        String statementFileName,
        long statementFileSize,
        int statementChecksum,
        OperationType reportType) {
         
        ...
    }
}

 

addStatementReportOperation 메서드는 격리수준이 SERIALIZABLE인 데이터베이스 트랜잭션에서 실행되도록 설정 되어있는것을 확인 할 수 있습니다.

 

또 한가지 특이한 것은 OperationService 클래스에 @Transactional(readOnly = true)가 붙어 있는 것인데, 기본적으로 해당 클래스 내부의 모든 서비스 메소드들은 별도로 transaction 설정을 오버라이드 하지 않는 이상 readOnly로 동작하게 됩니다.

 

transactional한 서비스들은 클래스 레벨에서 Transaction을 readOnly로 설정하고, 데이터베이스에 쓰기 작업등이 필요시 해당 메서드에서 설정을 override 하여 사용하는 것이 좋습니다.

 

@Service
@Transactional(readOnly = true)
public class UserService implements UserDetailsService {
 
    @Override
    public UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException {
        ...
    }
     
    @Transactional
    public void createUser(User user) {
        ...
    }
}

 

위 코드에서 loadUserByUsername메서드는 readOnly로 설정된 트랜잭션을 사용하고, 하이버네이트를 사용중이기 때문에 스프링은 이를 바탕으로 읽기전용 최적화를 진행합니다.

 

createUser 메서드는 데이터베이스에 쓰는 작업을 진행하는데, 이를 위해 readOnly 설정을 오버라이드 하여 read-write으로 사용하게 됩니다.

 

이렇게 read-only 메서드와 read-write 메서드를 분리하여 얻을 수 있는 이점은 이 메서드들을 서로 다른 데이터베이스 노드로 라우팅 할 수 있다는 점에 있습니다.

 

이 방식을 사용하면 DB의 복제 노드를 사용하여 read-only 트래픽을 확장할 수 있게 됩니다.

 

출처 : https://vladmihalcea.com/spring-transactional-annotation/

          https://www.youtube.com/watch?v=_WkMhytqoCc

          https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html#transaction-declarative-attransactional-settings

728x90