본문 바로가기

WEB개발/Spring

@Transactional (Propagation)

트랜잭션(Transaction) 추상화 

Spring은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있다. 이를 이용함으로써 애플리케이션에 각 기술마다(JDBC, JPA, Hibernate 등) 종속적인 코드를 이용하지 않고도 일관되게 트랜잭션을 처리할 수 있도록 해주고 있다.

 

 

Spring이 제공하는 트랜잭션 경계 설정을 위한 추상 인터페이스는 PlatformTransactionManager 이다. 예를 들어 만약 JDBC의 로컬 트랜잭션을 이용한다면 DataSourceTxManager를 이용하면 된다.

이제 우리는 사용하는 기술과 무관하게 PlatformTransactionManager를 통해 다음의 코드와 같이 트랜잭션을 공유하고, 커밋하고, 롤백할 수 있게 되었다.

public Object invoke(MethodInvoation invoation) throws Throwable {
	TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
	
	try {
		Object ret = invoation.proceed();
		this.transactionManager.commit(status);
		return ret;
	} catch (Exception e) {
		this.transactionManager.rollback(status);
		throw e;
	}
}

 

하지만 위와 같은 트랜잭션 관리 코드들이 비지니스 로직 코드와 결합되어 2가지 책임을 갖고 있다. Spring에서는 AOP를 이용해 이러한 트랜잭션 부분을 핵심 비지니스 로직과 분리하였다.

 

 

 

AOP를 이용한 트랜잭션(Transaction) 분리 

 

예를 들어 다음과 같이 트랜잭션 코드와 비지니스 로직 코드가 복잡하게 얽혀있는 코드가 있다고 하자.

public void addUsers(List<User> userList) {
	TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
	
	try {
		for (User user: userList) {
			if(isEmailNotDuplicated(user.getEmail())){
				userRepository.save(user);
			}
		}

		this.transactionManager.commit(status);
	} catch (Exception e) {
		this.transactionManager.rollback(status);
		throw e
	}
}

 

위의 코드는 여러 책임을 가질 뿐만 아니라 서로 성격도 다르고 주고받는 것도 없으므로 분리하는 것이 적합하다.

하지만 위의 코드를 어떻게 분리할 것인지에 대한 고민을 해야 한다. 흔히 떠올릴 수 있는 방법으로는 내부 메소드로 추출하거나 DI로 합성을 이용해 해결하거나 상속을 이용할 수 있을 것이다.


하지만 위의 어떠한 방법을 이용하여도 트랜잭션을 담당하는 기술 코드를 완전히 분리시키는 것이 불가능하였다. 그래서 Spring에서는 마치 트랜잭션 코드와 같은 부가 기능 코드가 존재하지 않는 것 처럼 보이기 위해 해당 로직을 클래스 밖으로 빼내서 별도의 모듈로 만드는 AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)를 고안 및 적용하게 되었고, 이를 적용한 트랜잭션 어노테이션(@Transactional)을 지원하게 되었다. 

 

이를 적용하면 위와 같은 코드를 핵심 비지니스 로직만 다음과 같이 남길 수 있다.

 

@Service
@RequiredArgsConstructor
@Transactional
public class UserService {

    private final UserRepository userRepository;

    public void addUsers(List<User> userList) {
        for (User user : userList) {
            if (isEmailNotDuplicated(user.getEmail())) {
                userRepository.save(user);
            }
        }
    }
}

 

 

Spring 트랜잭션의 세부 설정

 

Spring의 DefaultTransactionDefinition이 구현하고 있는 TransactionDefinition 인터페이스는 트랜잭션의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의하고 있다. 해당 4가지 속성은 트랜잭션을 세부적으로 이용할 수 있게 도와주며, @Transactional 어노테이션에도 공통적으로 적용할 수 있다. 

  • 트랜잭션 전파
  • 격리수준
  • 제한시간
  • 읽기전용

 

트랜잭션 전파

 

트랜잭션 전파란 트랜잭션의 경계에서 이미 진행중인 트랜잭션이 있거나 없을 때 어떻게 동작할 것인가를 결정하는 방식을 의미한다. 예를 들어 어떤 A 작업에 대한 트랜잭션이 진행중이고 B 작업이 시작될 때 B 작업에 대한 트랜잭션을 어떻게 처리할까에 대한 부분이다.

 

특정 서비스 메소드에서 다른 서비스 메소드를 호출하고 각각의 메소드에서 트랜잭션을 관리해야한다고 가정해봅시다. 이때, 각각의 메소드는 서로 독립적인 트랜잭션으로 실행되어야하며, 하나의 메소드가 실패하여도 다른 메소드의 트랜잭션에는 영향을 미치지 않아야 합니다.

 

1. A의 트랜잭션에 참여(PROPAGATION_REQUIRED)

B의 코드는 새로운 트랜잭션을 만들지 않고 A에서 진행중이 트랜잭션에 참여할 수 있다. 이 경우 B의 작업이 마무리 되고 나서, 남은 A의 작업(2)을 처리할 때 예외가 발생하면 A와 B의 작업이 모두 취소된다. 왜냐하면 A와 B의 트랜잭션이 하나로 묶여있기 때문이다.


2. 독립적인 트랜잭션 생성(PROPAGATION_REQUIRES_NEW)

반대로 B의 트랜잭션은 A의 트랜잭션과 무관하게 만들 수 있다. 이 경우 B의 트랜잭션 경계를 빠져나오는 순간 B의 트랜잭션은 독자적으로 커밋 또는 롤백되고, 이것은 A에 어떠한 영향도 주지 않는다. 즉, 이후 A가 (2)번 작업을 하면서 예외가 발생해 롤백되어도 B의 작업에는 영향을 주지 못한다.


3. 트랜잭션 없이 동작(PROPAGATION_NOT_SUPPORTED)

B의 작업에 대해 트랜잭션을 걸지 않을 수 있다. 만약 B의 작업이 단순 데이터 조회라면 굳이 트랜잭션이 필요 없을 것이다

 

 

PROPAGATION_REQUIRED

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    @Transactional
    public void methodA() {
        // A의 트랜잭션 시작
        System.out.println("A 트랜잭션 시작");

        // B 실행 (A의 트랜잭션에 참여)
        serviceB.methodB();

        // A의 추가 작업 수행
        System.out.println("A의 추가 작업 수행 중...");
        
        // 예외 발생
        throw new RuntimeException("A에서 예외 발생!");
    }
}

@Service
public class ServiceB {
    @Transactional(propagation = Propagation.REQUIRED)
    public void methodB() {
        // B 실행 (A의 트랜잭션에 참여)
        System.out.println("B 트랜잭션 실행");

        // 데이터베이스 작업 수행 (Commit 보류)
    }
}

.

  1. methodA()가 실행되면서 트랜잭션이 시작됨.
  2. methodB()가 실행될 때 새로운 트랜잭션을 만들지 않고 A의 트랜잭션을 공유.
  3. methodB()는 정상적으로 완료되지만, 아직 커밋되지 않음 (A의 트랜잭션이 끝나야 커밋됨).
  4. methodA()의 나머지 코드에서 예외가 발생함.
  5. A와 B가 동일한 트랜잭션을 공유하고 있으므로, 예외가 발생하면 전체 트랜잭션이 롤백됨.
  6. 결과적으로 B에서 수행한 작업도 모두 취소됨.

 

 

PROPAGATION_REQUIRES_NEW

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    @Transactional
    public void methodA() {
        // A의 트랜잭션 시작
        System.out.println("A 트랜잭션 시작");

        // B 실행 (새로운 트랜잭션 시작)
        serviceB.methodB();

        // A의 추가 작업 수행 중 예외 발생
        System.out.println("A의 추가 작업 수행 중...");
        throw new RuntimeException("A에서 예외 발생!");
    }
}

@Service
public class ServiceB {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // 새로운 트랜잭션이 시작됨 (A와 별개)
        System.out.println("B의 별도 트랜잭션 실행");

        // 데이터베이스 작업 수행 (B의 트랜잭션은 독립적)
    }
}

 

  1. methodA()가 실행되면서 A의 트랜잭션이 시작됨
  2. methodB()가 실행되면서 새로운 트랜잭션이 생성됨 (A와 별도)
  3. methodB()가 정상적으로 완료되면 즉시 커밋됨 (독립 트랜잭션이므로 A와 무관)
  4. methodA()의 추가 코드에서 예외 발생
  5. A의 트랜잭션은 롤백되지만, B의 트랜잭션은 롤백되지 않음 (B는 독립적으로 커밋됨)

PROPAGATION_NOT_SUPPORTED

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    @Transactional
    public void methodA() {
        // A의 트랜잭션 시작
        System.out.println("A 트랜잭션 시작");

        // B 실행 (트랜잭션 없이 실행)
        serviceB.methodB();

        // A의 추가 작업 수행 중 예외 발생
        System.out.println("A의 추가 작업 수행 중...");
        throw new RuntimeException("A에서 예외 발생!");
    }
}

@Service
public class ServiceB {
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void methodB() {
        // 기존 트랜잭션이 일시 중단됨 (트랜잭션 없이 실행됨)
        System.out.println("B 트랜잭션 없이 실행");

        // 데이터베이스 작업 수행 (즉시 auto-commit됨)
    }
}

 

  1.  methodA()가 실행되면서 A의 트랜잭션이 시작됨.
  2.  methodB()가 실행될 때, A의 트랜잭션이 일시 중단(suspend)되고 트랜잭션 없이 실행됨.
  3.  methodB()의 데이터베이스 작업이 즉시 반영(auto-commit).
  4.  methodA()의 추가 코드에서 예외 발생.
  5.  A의 트랜잭션은 롤백되지만, B의 작업은 롤백되지 않음 (이미 auto-commit되었기 때문).

 

 

 

주의사항

 

@Transactional은 AOP기반임으로 )같은 클래스 내부에서 메서드를 직접 호출할 때 (self-invocation)

주의해야한다.

 

@Service public class MyService {
	@Transactional // AOP 적용하고 싶음 
    public void outer() { 
    	inner(); // ← 같은 클래스 내부 직접 호출 → AOP 적용되지 않음 
    } 
    
    @Transactional public void inner() {
    // 트랜잭션이 적용되지 않음 
    }
 }


Spring AOP는 프록시(proxy) 기반이기 때문에, A가 A의 메서드를 호출하면 프록시를 거치지 않고 직접 호출됨 → AOP가 적용 안 됨.

 

 

해결 방법

  • inner()를 다른 빈으로 분리
  • 또는 같은 클래스 내에서 AOP 적용하고 싶으면 AopContext.currentProxy() 사용
    (단 exposeProxy=true 설정 필요)

 

참고

 

1. rollback

@Transactional
public void someService() {
    try {
        // 작업
        if (fail) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return;
        }
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}