@Transactional

parkrootseok·2025년 2월 8일

스프링

목록 보기
9/12
post-thumbnail

@Transactional이란?

@Transactional은 Spring에서 트랜잭션을 자동으로 관리할 수 있도록 제공하는 어노테이션입니다. 이 어노테이션을 메소드에 붙이면 해당 메소드 내의 모든 데이터베이스 접근 작업을 하나의 트랜잭션으로 관리할 수 있습니다.

사용 방법

@Transactional를 선언하여 사용할 수 있으며, 선언 위치에 따라 클래스 레벨과 메서드 레벨로 구분할 수 있습니다. 두 개가 동시에 적용된 경우, 메서드 레벨이 적용됩니다.

클래스 레벨

클래스에 선언하는 방식으로 클래스 내부에 있는 모든 메서드에 트랜잭션을 적용할 수 있습니다.

@Service
@Transactional
public class OrderServiceImpl implements OrderService {

	@Override
    public void placeOrder() { ... }  // 트랜잭션 적용
    
	@Override
    public void cancelOrder() { ... } // 트랜잭션 적용
    
}

메서드 레벨

메서드에 선언하는 방식으로 해당 메서드만 트랜잭션을 적용할 수 있습니다.

@Service
public class OrderServiceImpl implements OrderService {

	@Override
    @Transactional
    public void placeOrder() { ... }  // 트랜잭션 적용
    
	@Override
    public void cancelOrder() { ... }
    
}

기본 동작

@Transactional이 선언된 메소드는 다음과 같이 동작합니다.

  1. @Transactional이 선언된 메서드를 호출하면 프록시가 개입
  2. 프록시가 트랜잭션 시작
  3. 메서드 실행 후 정상 종료되면 commit() / 예외 발생은 rollback()
    • 런타임 예외는 자동 롤백되지만, 체크 예외는 기본적으로 롤백되지 않음
      • 이에 따라, 체크 예외를 롤백하기 위해선 rollbackFor = Exception.class 설정 필요
  4. 트랜잭션 종료

내부 동작 원리

Spring은 @Transactional을 프록시 기반의 AOP로 구현하여 트랜잭션을 관리합니다. @Transactional이 선언된 메서드가 호출되면 다음 예시와 같은 프록시 객체를 통해 이를 관리합니다.

public class TransactionalProxy implements OrderService {

    private final OrderService target;
    private final PlatformTransactionManager transactionManager;

    public TransactionalProxy(OrderService target, PlatformTransactionManager transactionManager) {
        this.target = target;
        this.transactionManager = transactionManager;
    }

    @Override
    public void placeOrder() {
    
        // 0. 트랜잭션 시작
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            // 1. 비즈니스 로직 실행
            target.placeOrder();	
            
            // 1-1. commit (정상 실행)
            transactionManager.commit(status);
        } catch (RuntimeException e) {
            // 1-2. rollback (예외 발생)
            // 단, RuntimeException만 롤백
            transactionManager.rollback(status);
            throw e;
        }
        
    }
    
}

전파 옵션

전파 옵션을 통해 트랜잭션 내에서 다른 트랜잭션을 호출할 때 동작 방식을 결정할 수 있습니다. 제공하는 옵션은 다음과 같습니다.

  • REQUIRED (기본값)
    • 기존 트랜잭션이 있으면 참여, 없으면 생성
  • REQUIRES_NEW
    • 기존 트랜잭션을 무시하고 생성
    • 기존 트랜잭션과 독립적으로 실행되므로 기존 트랜잭션 롤백에 영향 받지 않음
  • NESTED
    • 기존 트랜잭션 내에서 중첩 트랜잭션 실행

외부가 REQUIRED, 내부는 REQUIRES_NEW / NESTED

REQUIRES_NEW일 때

아래와 같이 REQUIRED 전파 수준인 트랜잭션 내부에 REQUIRES_NEW가 실행이 된다면 어떻게 될까요?

@Transaction
public void required() {
	requires_new();
}

@Transactional(propagation = REQUIRES_NEW)
public void requires_new() {
	// 예외 처리 발생
}

REQUIRES_NEW의 경우 기존 트랜잭션과 독립된 트랜잭션을 생성하게 됩니다. 이로 인해, 내부 트랜잭션에서 롤백이 발생해도 외부 트랜잭션에는 영향을 주지 않습니다. 또한, 외부 트랜잭션이 실패할 경우에도 내부 트랜잭션은 정상적으로 수행됩니다.

NESTED일 때

아래와 같이 REQUIRED 전파 수준인 트랜잭션 내부에 NESTED가 실행이 된다면 어떻게 될까요?

@Transaction
public void required() {
	nested();
}

@Transactional(propagation = NESTED)
public void nested() {
	// 예외 처리 발생
}

NESTED의 경우 기존 트랜잭션과 중첩된 트랜잭션을 생성하게 됩니다. 이 경우는 내부 트랜잭션에 예외가 발생할 경우, 외부 트랜잭션까지 전파되어 모두 롤백 될 수 있습니다.

하지만, 아래와 같이 내부 트랜잭션에 대한 예외를 잡을 수 있다면, 외부 트랜잭션은 정상적으로 수행합니다.

@Transaction
public void required() {
	try {
    	nested();
    } catch (RuntimeException e) {
    }
}

@Transactional(propagation = NESTED)
public void nested() {
	// 예외 처리 발생
}

반대로, 외부 트랜잭션이 실패할 경우에는 내부 트랜잭션도 함께 실패하게 됩니다.

트랜잭션 격리 수준

격리 수준이란?

격리 수준은 트랜잭션에서 일관성 없는 데이터를 허용하도록 하는 수준을 말하며, 동시에 실행되는 트랜잭션 간의 데이터 일관성을 보장하는 방법을 결정할 수 있습니다.

종류

격리 수준의 종류는 다음과 같습니다.

  • READ_UNCOMMITTED
    • 커밋하지 않은 데이터를 읽을 수 있음
    • Dirty Read, Non-Repeatable Read, Phantom Read 발생 가능
  • READ_COMMITTED (PostgreSQL, Oracle 등)
    • 커밋한 데이터만 읽을 수 있음
    • Non-Repeatable Read, Phantom Read 발생 가능
  • REPEATABLE_READ (MySQL)
    • 동일한 데이터를 여러 번 조회해도 값이 변하지 않음
    • Phantom Read 발생 가능
    • 단, MySQL의 InnoDB는 Next-Key Lock(Gap Lock + Record Rock)을 사용해 Phatom Read도 막음
  • SERIALIZABLE
    • 트랜잭션을 순차적으로 실행하여 동시성을 완전히 차단

격리 수준에 따라 발생할 수 있는 현상

격리 수준에 따라 다음과 같은 현상이 발생할 수 있습니다.

  • Dirty Read
    • 어떤 트랜잭션에서 아직 실행이 끝나지 않은 다른 트랜잭션에 의한 변경사항을 볼 수 있는 현상
  • Non-Repatable Read
    • 한 트랜잭션에서 같은 쿼리를 두 번 수행할 때, 두 쿼리의 결과가 상이하게 나타나는 현상
  • Phantom Read
    • 일정 범위의 레코드를 읽었을 때, 없던 레코드가 두 번째 쿼리에서 나타나는 현상

주의 사항

@Transactional을 사용하여 트랜잭션을 관리할 때 주의 사항은 다음과 같습니다.

내부 메서드 호출 시 트랜잭션 적용 X

다음 코드와 같이 내부 메서드 호출은 프록시를 거치지 않아 트랜잭션이 동작하지 않습니다.

@Service
public class SomeService {

    @Transactional
    public void methodA() {
    	// 트랜잭션이 적용되지 않음
        methodB();
    }

    @Transactional
    public void methodB() {
    }
    
}

이를 해결하기 위해, 프록시 객체를 사용하기 위해 다음과 같이 코드를 작성해야 합니다.

@Service
public class AnotherService {

    @Transactional
    public void methodB() {
    }
}

@Service
public class SomeService {
	
    private final AnotherService anotherService;

    public SomeService(AnotherService anotherService) {
        this.anotherService = anotherService;
    }

    @Transactional
    public void methodA() {
        anotherService.methodB();
    }
}

private 메소드는 적용 X

@Transcational은 AOP를 사용하여 구현한다. 즉, 프록시 객체가 메서드 호출을 가로채 대신 수행하는 구조이다. private는 같은 클래스 내부에서만 접근할 수 있도록 설정하기 위해 사용하는 접근 제한자이다. 즉, 프록시 객체가 감싸고 있는 객체의 메서드에 접근을 할 수 없다는 것 이다. 이로 인해, private 메소드는 @Transcational 적용이 불가하다.

@transcational(readOnly=true)

해당 메서드가 단순 데이터 조회를 수행하기 위한 목적일 경우, readOnly=true을 사용합니다. 이를 설정할 경우 다음과 같이 다릅니다.

  • JPA 동작
    • flush() 메서드 호출 생략
      • save() 메서드 호출 시 반영하지 않음
    • Dirty Checking 생략
  • DB 레벨
    • 읽기 전용 트랜잭션 적용
profile
동료들의 시간과 노력을 더욱 빛내줄 수 있는 개발자가 되고자 노력합니다.

0개의 댓글