목록으로 돌아가기

Spring의 @Transactional과 Propagation옵션

2025년 12월 31일
3개 태그
AOP
JPA
Spring

Spring의 @Transactional 어노테이션을 완전히 이해하기 위한 가이드입니다. Propagation 옵션부터 내부 구현 원리까지, 실제 프로젝트 코드와 함께 살펴봅니다.

목차

  1. @Transactional이란?
  2. Propagation 옵션 완전 정리
  3. 실제 프로젝트에서의 활용
  4. 트랜잭션 내부 구현 원리
  5. 성능 최적화 팁

1. @Transactional이란?

1.1. 기본 개념

@Transactional은 Spring에서 제공하는 **선언적 트랜잭션 관리(Declarative Transaction Management)**를 위한 어노테이션입니다.

트랜잭션이란?

  • 데이터베이스 작업의 논리적 단위
  • 모든 작업이 성공하거나 모두 실패해야 함 (ACID 원칙)
  • BEGIN → 작업 수행 → COMMIT 또는 ROLLBACK

선언적 관리란?

  • 코드에 직접 트랜잭션 관리 코드를 작성하지 않음
  • 어노테이션으로 선언만 하면 Spring이 자동으로 처리

1.2. 사용 방법

// 클래스 레벨: 모든 public 메서드에 적용
@Service
@Transactional
class CreditPaymentService {
    // 모든 public 메서드가 트랜잭션 내에서 실행됨
}

// 메서드 레벨: 특정 메서드에만 적용
@Transactional
fun approvePayment(...) {
    // 이 메서드만 트랜잭션 내에서 실행됨
}

2. Propagation 옵션 완전 정리

propagation트랜잭션이 이미 존재할 때 새로운 트랜잭션이 어떻게 동작할지를 결정합니다.

2.1. REQUIRED (기본값) ⭐ 가장 많이 사용

동작:

  • 트랜잭션이 있으면 → 기존 트랜잭션에 참여
  • 트랜잭션이 없으면 → 새로운 트랜잭션 생성

특징:

  • 같은 트랜잭션 내에서 실행
  • 하나라도 실패하면 전체 롤백
  • 가장 일반적으로 사용
@Transactional  // propagation = REQUIRED (기본값)
fun approvePayment(...) {
    // 기존 트랜잭션에 참여하거나 새로 생성
    paymentRepository.save(...)
}

2.2. REQUIRES_NEW ⭐⭐ 독립적인 트랜잭션 필요 시

동작:

  • 항상 새로운 트랜잭션 생성
  • 기존 트랜잭션이 있어도 독립적인 트랜잭션으로 실행
  • 기존 트랜잭션은 일시 중지(suspend)

언제 사용하나?

  • 각 단계를 독립적으로 커밋해야 할 때
  • Saga 패턴 구현 시
  • 부분 커밋이 필요한 경우
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun markAsPgApproved(...) {
    // 항상 새로운 트랜잭션에서 실행
    // 즉시 커밋됨!
    paymentRepository.save(...)
}

시각적 설명:

다이어그램 로딩 중...

2.3. SUPPORTS

동작:

  • 트랜잭션이 있으면 → 참여
  • 트랜잭션이 없으면 → 트랜잭션 없이 실행

사용 예시: 읽기 전용 작업

@Transactional(propagation = Propagation.SUPPORTS)
fun readOnlyOperation() {
    // 트랜잭션이 있으면 참여, 없으면 그냥 실행
}

2.4. MANDATORY

동작:

  • 반드시 기존 트랜잭션이 있어야 함
  • 없으면 IllegalTransactionStateException 발생

사용 예시: 트랜잭션 내에서만 호출되어야 하는 메서드

@Transactional(propagation = Propagation.MANDATORY)
fun mustBeInTransaction() {
    // 반드시 트랜잭션 내에서만 호출되어야 함
}

2.5. NOT_SUPPORTED

동작:

  • 트랜잭션을 사용하지 않음
  • 기존 트랜잭션이 있으면 일시 중지

사용 예시: 로깅, 캐시 작업 등

@Transactional(propagation = Propagation.NOT_SUPPORTED)
fun nonTransactionalOperation() {
    // 트랜잭션 없이 실행
}

2.6. NEVER

동작:

  • 트랜잭션을 사용하지 않음
  • 트랜잭션이 있으면 예외 발생

사용 예시: 트랜잭션 내에서 호출되면 안 되는 메서드

@Transactional(propagation = Propagation.NEVER)
fun mustNotBeInTransaction() {
    // 트랜잭션 내에서 호출되면 안 됨
}

2.7. NESTED

동작:

  • 중첩 트랜잭션 생성 (Savepoint 사용)
  • 부모 트랜잭션이 롤백되면 중첩 트랜잭션도 롤백
  • 중첩 트랜잭션이 롤백되어도 부모는 계속 진행 가능

사용 예시: 부분 롤백이 필요한 경우

@Transactional(propagation = Propagation.NESTED)
fun nestedOperation() {
    // Savepoint 생성
    // 실패하면 이 지점까지만 롤백
}

2.8. Propagation 옵션 비교

다이어그램 로딩 중...

2.9. Propagation 옵션 비교표

Propagation기존 트랜잭션동작사용 빈도
REQUIRED (기본)있음참여⭐⭐⭐⭐⭐
없음생성
REQUIRES_NEW있음새로 생성 (기존 일시 중지)⭐⭐⭐⭐
없음생성
SUPPORTS있음참여⭐⭐
없음트랜잭션 없이 실행
MANDATORY있음참여
없음예외 발생
NOT_SUPPORTED있음일시 중지
없음트랜잭션 없이 실행
NEVER있음예외 발생
없음트랜잭션 없이 실행
NESTED있음중첩 트랜잭션⭐⭐
없음생성

3. 실제 프로젝트에서의 활용

3.1. 문제 상황: REQUIRED의 한계

결제 승인 프로세스를 생각해봅시다:

  1. PG 승인
  2. 상태를 PG_APPROVED로 저장
  3. Credit 발급
  4. 최종 완료 만약 REQUIRED를 사용하면?
@Transactional
fun approvePayment(...) {
    // 같은 트랜잭션 내에서 실행
    creditPaymentStatusService.markAsPgApproved(...)  // REQUIRED

    // Credit 발급 실패
    supportersServiceClient.issueCredit(...)  // ❌ 예외

    // 전체 롤백 → PG_APPROVED 상태도 롤백됨! ❌
    // 문제: PG는 승인되었는데 DB에는 기록이 없음!
}

문제점: PG는 이미 승인되었는데, DB에는 기록이 없어서 추적이 불가능합니다.

다이어그램 로딩 중...

3.2. 해결책: REQUIRES_NEW 사용

// BasePaymentStatusService.kt
@Transactional(propagation = Propagation.REQUIRES_NEW)
protected open fun markStatus(...) {
    // 항상 새로운 트랜잭션에서 실행
    val updated = changeStatus(payment)
    return savePayment(updated)  // 즉시 커밋됨
}

// CreditPaymentService.kt
@Transactional  // 메인 트랜잭션
fun approvePayment(...) {
    try {
        // Step 1: PG 승인
        nicePayService.approvePayment(...)

        // Step 2: 별도 트랜잭션에서 상태 저장 (REQUIRES_NEW)
        currentPayment = creditPaymentStatusService.markAsPgApproved(payment, tid)
        // ↑ 이 시점에 이미 DB에 저장되고 커밋됨!

        // Step 3: Credit 발급
        supportersServiceClient.issueCredit(...)

        // Step 4: 최종 완료
        return paymentRepository.saveCreditPayment(updated)
    } catch (e: Exception) {
        // Credit 발급 실패 시
        // 이미 PG_APPROVED 상태는 별도 트랜잭션에서 커밋되어 있음
        // 따라서 롤백되지 않음!

        // 보상 트랜잭션 실행
        compensatePgApproval(currentPayment, tid)  // PG 취소
        creditPaymentStatusService.markAsIssueCreditFailed(currentPayment)
    }
}

장점:

  • ✅ PG 승인 후 상태가 즉시 저장됨
  • ✅ Credit 발급 실패해도 상태는 유지됨
  • ✅ 보상 트랜잭션으로 PG 취소 가능
다이어그램 로딩 중...

3.3. Saga 패턴과 REQUIRES_NEW

Saga 패턴이란?

  • 분산 트랜잭션을 여러 단계로 나누어 처리
  • 각 단계를 독립적으로 커밋
  • 실패 시 보상 트랜잭션(Compensation) 실행
다이어그램 로딩 중...

4. 트랜잭션 내부 구현 원리

4.1. AOP 프록시 패턴

Spring의 @Transactional은 **AOP (Aspect-Oriented Programming)**를 기반으로 동작합니다.

다이어그램 로딩 중...

프록시 생성 과정

// 원본 클래스
@Service
@Transactional
class CreditPaymentService {
    fun approvePayment(...) {
        // 비즈니스 로직
    }
}

Spring이 내부적으로 생성하는 프록시 (의사 코드):

class CreditPaymentServiceProxy(
    private val target: CreditPaymentService,
    private val transactionInterceptor: TransactionInterceptor
) : CreditPaymentUseCase {

    override fun approvePayment(...) {
        // 1. 트랜잭션 시작
        val transactionInfo = transactionInterceptor.createTransactionIfNecessary(...)

        try {
            // 2. 실제 메서드 호출
            val result = target.approvePayment(...)

            // 3. 트랜잭션 커밋
            transactionInterceptor.commitTransactionAfterReturning(transactionInfo)
            return result
        } catch (e: Exception) {
            // 4. 예외 발생 시 롤백
            transactionInterceptor.completeTransactionAfterThrowing(transactionInfo, e)
            throw e
        }
    }
}

프록시 확인 방법

@SpringBootTest
class TransactionProxyTest {

    @Autowired
    private lateinit var creditPaymentService: CreditPaymentService

    @Test
    fun checkProxy() {
        // 프록시인지 확인
        println(creditPaymentService.javaClass.name)
        // 출력: com.sclass.paymentservice.application.service.CreditPaymentService$$SpringCGLIB$$0

        // AOP 프록시인지 확인
        val isProxy = AopUtils.isAopProxy(creditPaymentService)
        println("Is Proxy:$isProxy")  // true
    }
}

4.2. TransactionSynchronizationManager

트랜잭션 상태를 ThreadLocal에 저장하여 관리합니다.

내부 동작 원리

// TransactionSynchronizationManager 내부 (의사 코드)
object TransactionSynchronizationManager {

    // ThreadLocal로 각 스레드별 트랜잭션 정보 저장
    private val resources = ThreadLocal<Map<Any, Any>>()
    private val synchronizations = ThreadLocal<MutableList<TransactionSynchronization>>()
    private val currentTransactionName = ThreadLocal<String>()
    private val currentTransactionReadOnly = ThreadLocal<Boolean>()
    private val actualTransactionActive = ThreadLocal<Boolean>()

    fun bindResource(key: Any, value: Any) {
        val map = resources.get() ?: mutableMapOf()
        map[key] = value
        resources.set(map)
    }

    fun getResource(key: Any): Any? {
        return resources.get()?.get(key)
    }
}

실제 사용 예시

@Service
class CreditPaymentService {

    fun approvePayment(...) {
        // 현재 트랜잭션 확인
        val isActive = TransactionSynchronizationManager.isActualTransactionActive()
        println("Transaction Active:$isActive")  // true

        // 트랜잭션 이름 확인
        val name = TransactionSynchronizationManager.getCurrentTransactionName()
        println("Transaction Name:$name")
        // 출력: com.sclass...CreditPaymentService.approvePayment
    }
}

4.3. REQUIRES_NEW의 내부 구현

REQUIRES_NEW는 어떻게 독립적인 트랜잭션을 생성할까요?

내부 동작 흐름

// AbstractPlatformTransactionManager 내부 (의사 코드)
class AbstractPlatformTransactionManager {

    fun getTransaction(definition: TransactionDefinition): TransactionStatus {
        val transaction = doGetTransaction()  // 현재 트랜잭션 확인

        if (definition.propagation == Propagation.REQUIRES_NEW) {
            // 기존 트랜잭션이 있으면 일시 중지
            if (transaction != null) {
                val suspendedResources = suspend(transaction)
                // ThreadLocal에서 트랜잭션 정보 제거
                // 새로운 트랜잭션 시작
                return startTransaction(definition, suspendedResources)
            } else {
                // 트랜잭션이 없으면 새로 생성
                return startTransaction(definition, null)
            }
        }
        // ... REQUIRED 등의 처리
    }

    private fun suspend(transaction: Any): SuspendedResourcesHolder {
        // 1. 현재 트랜잭션 정보 저장
        val name = TransactionSynchronizationManager.getCurrentTransactionName()
        val readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly()
        val wasActive = TransactionSynchronizationManager.isActualTransactionActive()

        // 2. ThreadLocal에서 트랜잭션 정보 제거
        TransactionSynchronizationManager.unbindResource(dataSource)
        TransactionSynchronizationManager.clear()

        // 3. 저장된 정보를 SuspendedResourcesHolder에 보관
        return SuspendedResourcesHolder(
            transaction = transaction,
            name = name,
            readOnly = readOnly,
            wasActive = wasActive
        )
    }

    private fun resume(holder: SuspendedResourcesHolder) {
        // 일시 중지된 트랜잭션 재개
        TransactionSynchronizationManager.bindResource(dataSource, holder.transaction)
        TransactionSynchronizationManager.setCurrentTransactionName(holder.name)
        TransactionSynchronizationManager.setCurrentTransactionReadOnly(holder.readOnly)
        TransactionSynchronizationManager.setActualTransactionActive(holder.wasActive)
    }
}

REQUIRES_NEW 실행 흐름 시각화

다이어그램 로딩 중...

ThreadLocal 상태 변화 요약:

다이어그램 로딩 중...

4.4. Connection 관리

같은 트랜잭션 내에서의 Connection 재사용

@Transactional
fun approvePayment(...) {
    // 첫 번째 DB 작업
    val payment1 = paymentRepository.findCreditPaymentByPgOrderId(...)
    // Connection1 획득

    // 두 번째 DB 작업
    val payment2 = paymentRepository.findCreditPaymentById(...)
    // Connection1 재사용 (같은 트랜잭션)

    // 세 번째 DB 작업
    paymentRepository.saveCreditPayment(...)
    // Connection1 재사용
}
// Connection1.commit() 또는 rollback()
다이어그램 로딩 중...

댓글 (0)

댓글 수정 시 필요합니다. 최소 4자 이상 입력해주세요.

아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요!