티스토리 뷰

1. 트랜잭션이란?

트랜잭션은 "모든 작업이 성공하거나, 모든 작업이 실패하거나" 하는 원칙으로 ACID 특성을 갖는다.

  • Atomicity (원자성): 모두 성공하거나 또는 모두 실패
  • Consistency (일관성): 트랜잭션 전후 데이터 무결성 유지
  • Isolation (격리성): 동시 실행되는 트랜잭션들이 서로 간섭 안 줌
  • Durability (지속성): 성공한 트랜잭션은 영구적으로 저장

2. Spring 트랜잭션 구조

Controller
   ↓
Service (프록시 객체)
   ↓
TransactionInterceptor
   ↓
PlatformTransactionManager
   ↓
TransactionSynchronizationManager
   ↓
DataSource / JDBC Connection

각 요소별로 역할을 살피면 다음과 같다.

2.1. @Transactional (선언부)

개발자가 트랜잭션 경계를 선언적으로 정의하는 수단으로 메소드나 클래스 단위로 적용이 가능하다.

@Transactional 은 메소드 자체에 트랜잭션을 거는 것이 아닌, 프록시 객체를 통해 메소드 호출을 감싸는 방식으로 동작한다.

@Transactional
public void order() {
	// ...
}

2.2. 프록시 객체 (AOP Proxy)

스프링은 @Transactional이 붙은 빈에 대해 프록시 객체를 생성한다.

이 때, 트랜잭션은 프록시를 통해 호출될 때만 동작 한다.

내부 메소드 호출은 트랜잭션이 적용되지 않는다.

@Service
public class OrderService {
	@Transactional
	public void order() {
		saveOrder();
	}
	
	@Transactional(propagation = Propagation.REQUIRES_NEW)
	public void saveOrder() {
		// ...
	}
}

2.2.1. 내부 메소드 호출은 트랜잭션이 적용되지 않는다?

처음에 이 말을 보면서 한참동안 이해가 되지 않아 이것저것 찾아보며, 정리해두었다.

위 OrderService 기준으로 다시 보면 런타임 호출 구조는 다음과 같다.

Client
  ↓
OrderServiceProxy.order()   ← @Transactional 적용
  ↓
OrderService.order()
  ↓
this.saveOrder()            ← 프록시 미통과

같은 빈이자 같은 클래스에 있으므로 this 로 호출하는 격이고, 이는 내부 메소드 호출로 규정하는 것이다.

다만 트랜잭션이 적용되지 않는 것에 대해 정확히 구분해서 이해해야 될 필요가 있다.

 

먼저 order 메소드 내부에서 saveOrder 메소드가 호출된다고 가정을 했을 때, Order 메소드 내부에서 오류 발생할 경우 흐름은 다음과 같다.

[트랜잭션 시작 - order()]
   ├─ saveOrder()
   ├─ DB 작업
   └─ 예외 발생 → rollback

order 에서 예외가 발생 할 경우, saveOrder 메소드에서 한 DB 작업도 모두 롤백을 진행한다.

🚨 트랜잭션이 적용되지 않는다는 것은 saveOrder 메소드에 @Transactional 으로 선언된
propagation, isolation, rollbackFor 와 같은 속성이 무시된다는 것이다.

이렇게 무시되는 이유는 프록시를 통과하지 않았기 때문에, TransactionInterceptor가 실행되지 않은 탓이다.

만약, 외부에서 saveOrder 메소드만을 호출한다면, 해당 메소드의 @Transactional이 정상적으로 적용되며, propagation, rollbackFor이 적용될 것이다.

만약 다음과 같은 케이스에서 위와 같은 트랜잭션이 적용되지 않아 사고가 발생하는 케이스 중 하나다.

@Transactional
public void order() {
	saveOrder(); // REQUIRES_NEW 기대
	throw new RuntimeException();
}

개발자는 saveOrder는 별도 트랜잭션으로 REQUIRES_NEW로 커밋을 기대하였지만, 내부 메소드 호출이여서 전부 롤백이 되어 버린다.

이렇게 발생하는 문제를 해결하는 방식은 두 가지 방법 정도가 있다.

첫 번째로는 빈을 분리하는 것이다.

@Service
public class OrderService {
	private final OrderSaveService saveService;

	@Transactional
	public void order() {
		saveService.saveOrder(); // 프록시 통과
	}
}

두 번째로는 추천하지 않지만 명시적으로 프록시 사용이 있다.

((OrderService) AopContext.currentProxy()).saveOrder();

정리하면 “내부 메소드 호출은 트랜잭션이 적용되지 않는다”는 것은, 내부 메소드에서 정의한 트랜잭션 정책이 적용이 되지 않는다는 뜻이다.

2.3. TransactionInterceptor

프록시 내부에서 실제 트랜잭션 처리를 담당하는 AOP 인터셉터로, 동작하는 흐름의 순서는 다음과 같다.

  1. @Transactional 메타데이터 해석
  2. 트랜잭션 시작
  3. 타겟 메소드 실행
  4. 정상 종료 → commit OR 예외 발생 → rollback 판단
  5. 리소스 정리

2.4. PlatformTransactionManager

스프링 트랜잭션의 추상화 핵심으로, 트랜잭션 기술(JDBC, JPA 등)에 종속되지 않도록 설계되어 있다.

public interface PlatformTransactionManager {
	TransactionStatus getTransaction(...);
	void commit(TransactionStatus status);
	void rollback(TransactionStatus status);
}

2.5. TransactionSynchronizationManager

트랜잭션 상태 저장소로 ThreadLocal 기반으로 현재 트랜잭션 정보를 관리한다. 즉, 같은 스레드 내에서는 항상 동일한 Connection을 사용할 수 있다.

관리의 대상은 다음과 같다.

  • 현재 트랜잭션 활성 여부
  • Connection / EntityManager 바인딩
  • 트랜잭션 동기화 콜백

3. 실제 트랜잭션 동작 흐름 (JDBC 기준)

3.1. 트랜잭션 시작

  1. 프록시가 메소드 호출 가로챔
  2. TransactionInterceptor 실행
  3. PlatformTransactionManager.getTransaction()
  4. DataSource에서 Connection 획득
  5. setAutoCommit(false)
  6. Connection을 ThreadLocal에 바인딩

3.2. 비즈니스 로직 실행

  • MyBatis / JDBC Template 등에서 Connection 요청
  • → ThreadLocal에 바인딩된 동일한 Connection 반환
  • 여러 DAO 호출이 하나의 트랜잭션으로 묶임

3.3. 커밋 / 롤백

  • 정상 종료
    • commit() → Connection.commit()
  • 예외 발생
    • rollback() → Connection.rollback()

3.4. 트랜잭션 종료

  • Connection ThreadLocal 해제
  • autoCommit 원복
  • Connection 반환

4. @Transactional 옵션

@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.DEFAULT,
    timeout = -1,
    readOnly = false,
    rollbackFor = {},
    noRollbackFor = {}
)

4.1. propagation (전파 옵션)

이미 트랜잭션이 존재할 때, 현재 메소드를 어떻게 실행할 것이지 결정하는 옵션이다.

  • REQUIRED (기본값)
    • 기존 트랜잭션에 참여하고, 없을 경우 생성
  • REQUIRES_NEW
    • 항상 새 트랜잭션 생성
    • 커넥션을 점유하므로 남용 금지
  • SUPPORTS
    • 트랜잭션이 있으면 참여
  • NOT_SUPPORTED
    • 트랜잭션 없이 실행
  • MANDATORY
    • 트랜잭션이 없으면 예외
  • NEVER
    • 트랜잭션이 있으면 예외
  • NESTED
    • 중첩 트랜잭션

4.2. isolation (격리 수준)

동시에 실행되는 트랜잭션 간 데이터 접근 허용 수준을 설정하며, 특별한 케이스 외에 해당 옵션을 사용할 일은 별로 없다.

즉, 정합성과 성능 사이에서의 선택이다.

옵션 DB 현상
DEFAULT DB 기본값 사용
READ_UNCOMMITTED Dirty Read
READ_COMMITTED Non-repeatable Read
REPEATABLE_READ Phantom Read
SERIALIZABLE 완전 격리

4.3. readOnly

@Transactional(readOnly = true)

해당 트랜잭션이 조회 전용임을 명시하는 것으로, JDBC/MyBatis 에서는 DB 최적화 힌트가 되고, JPA 에서는 Dirty Checking 을 비활성화 한다.

4.4. timeout

@Transactional(timeout = 5)

트랜잭션의 최대 수행 시간을 초 단위로 지정하는 옵션이다.

지정 시간이 초과될 경우, TransactionTimedOutException이 발생하고, DB 쿼리 타임아웃과 별개이다.

배치, 대량 처리에 제한적으로 사용할 수 있다.

4.5. rollbackFor / rollbackForClassName

@Transactional(rollbackFor = Exception.class)

기본 롤백 대상이 아닌 예외도 롤백 대상으로 지정하기 위한 옵션이다.

4.5.1. 기본 롤백 규칙

  • RuntimeException → Rollback
  • Error → Rollback
  • Checked Exception → NOT Commit

4.6. noRollbackFor / noRollbackForClassName

@Transactional(noRollbackFor = IllegalStateException.class)

기본적으로 롤백되는 예외를 롤백하지 않도록 지정하기 위한 옵션이다.

4.7. name / value

@Transactional("orderTransaction")
@Transactional(value = "orderTransaction")

트랜잭션에 이름을 지정하는 옵션으로 모니터링 용도로 작성한다.

4.8. qualifier

@Transactional(transactionManager = "orderTxManager")

여러 TransactionManager 중 어떤 것을 사용할지 지정하는 것으로, 멀티 데이터소스 환경에서만 사용한다.

즉, 단일 DB 환경에서는 필요하지 않다.

4.9. 옵션 우선순위

트랜잭션 설정의 우선순위는 다음과 같은 순서를 따른다.

  1. 메소드 레벨
  2. 클래스 레벨
  3. 인터페이스
  4. 기본 설정
@Transactional(readOnly = true)
public class UserService {

    @Transactional
    public void save() {} // readOnly = false
}
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
글 보관함