[Spring] @Transactional - 동작원리

코코코딩을 합시다·2024년 8월 27일

Spring CS

목록 보기
3/6

@Transactional이란?

스프링에서는 @Transactional 어노테이션을 제공한다. 이러한 어노테이션을 사용하는 트랜잭션 방식을 선언적 트랜잭션이라 불리며 스프링 AOP가 이를 인식하고 트랜잭션 프록시 객체가 생성해 자동으로 commit 혹은 rollback을 시행한다. 메서드에 붙여도 되고 클래스에 붙여도 되지만 클래스에 붙으면 외부에서 호출 가능한 public 메서드가 AOP 적용 대상이 된다.

스프링부트의 자동 리소스 등록

스프링부트는 데이터소스와 트랜잭션 매니저를 자동으로 등록한다.

  • 데이터소스
    • application.properties에 있는 속성을 사용해서 DataSource를 생성하고 스프링 빈에 등록한다.
  • 트랜잭션 매니저
    • 적절한 트랜잭션 매니저를 자동으로 스프링 빈에 등록한다.
    • 어떤 트랜잭션 매니저를 선택할지는 현재 등록된 라이브러리로 판단. JPA를 사용한다면 JpaTransactionManager를 빈으로 등록한다.

트랜잭션 AOP 적용 전체 흐름

  • 스프링부트가 등록한 트랜잭션 매니저는 데이터소스를 통해 Connection을 만들고 트랜잭션을 시작한다.
  • 트랜잭션 매니저는 트랜잭션이 시작된 Connection을 트랜잭션 동기화 매니저에 보관한다.
  • Repository는 트랜잭션 동기화 매니저에 보관된 Connection을 꺼내서 사용한다. (파라미터로 Connection 전달 필요 X)
  • 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 Connection을 종료한다.

트랜잭션 AOP - 프록시 내부 호출

앞서 말했듯이 @Transactional이 달려있으면 트랜잭션 AOP는 트랜잭션 프록시 객체를 만들어 스프링 빈에 등록한다. 스프링 부트의 디폴트 프록시 생성 라이브러리인 CGLIB가 담당한다.

당연히 주입 받을 때도 실제 객체 대신 프록시 객체가 주입된다.

프록시 객체가 요청을 가로채 트랜잭션을 처리하고 실제 객체를 호출한다.

따라서 트랜잭션을 적용하려면 항상 프록시를 통해서 타깃을 호출해야 한다. 부가 기능을 담당하는 것이다.

  • @Transactional 어노테이션이 붙어있으므로 클라이언트는 트랜잭션 프록시를 호출하게 된다.
  • 트랜잭션 적용 후 실제 타깃의 internal() 메서드를 호출한다.
  • 타깃 메서드가 처리 완료되면 프록시로 돌아와 트랜잭션을 종료한다.

AOP 트랜잭션의 한계

만약 타깃 메서드를 내부 호출한다면 @Transactional이 달려있어도 트랜잭션이 적용되지 않는다. 이는 프록시 방식의 AOP의 한계다.

다음 코드를 보자.

@Service
public class CallService {

    @Transactional
    public void external() {
        internal();  // 내부 메서드 호출
    }

    @Transactional
    public void internal() {
        // 트랜잭션이 적용되지 않는다. 
    }
}

external()를 호출하면 internal()도 함께 호출되지만, 트랜잭션은 external()에만 적용된다.

프록시 방식에서 external()를 호출하면 프록시 객체가 트랜잭션을 관리하고 이 메서드를 가로채서 트랙잭션을 적용한다. 하지만 external()가 internal()를 호출하는 순간, 프록시가 관여하지 않는 직접적인 메서드 호출이 되어 internal()에는 트랜잭션이 적용되지 않는다.

해결 방법

  • 메서드 분리
  • AspectJ 사용
    • 프록시 기반 AOP 대신 바이트코드 조작 방식을 사용하면 클래스 내부 호출에도 부가기능을 적용할 수 있다.

가장 간단한 방법은 내부 호출을 피하기 위해 internal() 메서드를 별도의 클래스로 분리해 내부 호출을 외부 호출로 만들어주는 것이다.

@Service
public class CallService {

    @Autowired
    private InternalService internalService;

    @Transactional
    public void external() {
        internalService.internal();  // 외부 클래스 메서드 호출
    }
}
@Service
public class InternalService {

    @Transactional
    public void internal() {
        // 트랜잭션 적용
    }
}

예외와 트랜잭션 커밋, 롤백

@Transactional의 기본적인 롤백 원칙은 다음과 같다.

  1. 체크되지 않은 예외 발생시 롤백
    • RuntimeException와 그 하위 클래스들에서 발생하는 예외는 기본적으로 롤백한다.
    • 체크되지 않은 예외란 컴파일시 체크되지 않고 런타임 중 발생하는 예외를 뜻한다.
  2. 체크된 예외 발생시 커밋
    • 기본적으로 체크된 예외 (Exception, IOException 등)이 발생하면 트랜잭션은 롤백되지 않고 커밋된다.
    • 체크된 예외란 Exception 클래스와 그 하위 클래스 중 RuntimeException을 제외한 클래스로 컴파일시 체크되는 예외를 뜻한다.

롤백을 지정하고 싶다면 rollbackFor을 사용해서 지정해주면 된다.

@Transactional(rollbackFor = MyException.class) 
public void rollbackFor() throws MyException {
    throw new MyException();
}

그렇다면 왜 스프링은 체크(컴파일) 예외는 커밋하고 체크되지 않은(런타임) 예외는 롤백할까?

스프링은 기본적으로 체크 예외는 비즈니스 의미가 있을 때 사용하고, 런타임(언체크) 예외는 복구 불가능한 예외로 가정 한다. 

💡

만약 결제 잔고가 부족해 NotEnoughMoneyException 라는 체크 예외가 발생했다 가정하자.

이 예외는 시스템 상의 문제가 있어서 발생하는 예외인가? 아니다!
오히려 시스템이 문제 없이 동작했기 때문에 발생하는 비즈니스적 예외인 것이다.
따라서 이러한 체크 예외는 시스템이 정상이라는 증거이기도 하기 때문에, 커밋되는 것이 맞다.
롤백하면 오히려 결제 요청 자체가 사라지는 이상한 상황이 된다.
만약 그래도 롤백하고 싶을 경우를 위해 스프링은 친절히 rollbackFor 옵션을 제공한다.

@Transactional 옵션

  • isolation : 트랜잭션의 격리 수준 설정. 여러 트랜잭션이 동시에 실행될 때 데이터의 일관성을 유지하는 방법을 정의한다.
    • DEFAULT: 데이터베이스의 기본 격리 수준을 사용합니다.
    • READ_UNCOMMITTED: 다른 트랜잭션이 커밋되지 않은 데이터를 읽을 수 있습니다.
    • READ_COMMITTED: 다른 트랜잭션이 커밋한 데이터만 읽을 수 있습니다.
    • REPEATABLE_READ: 트랜잭션 동안 읽은 데이터가 변경되지 않도록 보장합니다.
    • SERIALIZABLE: 가장 높은 격리 수준으로, 트랜잭션 간에 완전한 직렬화를 보장합니다.

  • propagation : 트랜잭션 전파 수준 설정. 메소드가 트랜잭션 내에서 호출될 때 트랜잭션을 어떻게 처리할지 정의한다.
    • REQUIRED: 기본값으로, 메소드가 트랜잭션 내에서 실행되어야 하며 기존 트랜잭션이 없으면 새로운 트랜잭션을 생성합니다.
    • REQUIRES_NEW: 항상 새로운 트랜잭션을 시작하며 기존 트랜잭션이 있으면 일시 중단됩니다.
    • MANDATORY: 반드시 기존 트랜잭션이 존재해야 하며, 그렇지 않으면 예외를 발생시킵니다.
    • SUPPORTS: 트랜잭션이 있으면 그 안에서 실행하고, 없으면 트랜잭션 없이 실행합니다.
    • NOT_SUPPORTED: 트랜잭션 없이 실행합니다. 기존 트랜잭션이 있으면 일시 중단됩니다.
    • NEVER: 트랜잭션이 있으면 예외를 발생시킵니다.
    • NESTED: 중첩 트랜잭션을 허용합니다.

  • timeout: 트랜잭션 지속 시간 설정, 시간 내에 트랜잭션이 완료되지 않으면 롤백된다.

  • readOnly: 읽기 전용 트랜잭션 명시. 주로 데이터 조회용으로만 사용하는 트랜잭션에 설정한다.

  • rollBackFor : 특정 예외 발생시 롤백할지 여부 설정한다.

  • noRollBackFor: 특정 예외 발생시 롤백하지 않도록 설정한다.
profile
좋아하는 걸로 밥 벌어먹기

0개의 댓글