Spring @Transactional 완전 정리: Propagation 옵션과 내부 구현 원리
Spring의 @Transactional 어노테이션은 선언적 트랜잭션 관리 기능을 제공하며, Propagation 옵션을 통해 트랜잭션의 동작 방식을 설정할 수 있다. 주요 Propagation 옵션으로는 REQUIRED, REQUIRES_NEW, SUPPORTS, MANDATORY, NOT_SUPPORTED, NEVER, NESTED가 있으며, 각 옵션은 트랜잭션의 참여 및 생성 방식에 따라 다르게 작동한다. 실제 프로젝트에서의 활용 예시와 성능 최적화 팁도 포함되어 있으며, AOP 프록시 패턴을 기반으로 내부 구현 원리를 설명한다.
Spring의 @Transactional 어노테이션을 완전히 이해하기 위한 가이드입니다. Propagation 옵션부터 내부 구현 원리까지, 실제 프로젝트 코드와 함께 살펴봅니다.
목차
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(...)
}
시각적 설명:
sequenceDiagram
participant Client
participant Service as CreditPaymentService<br/>(REQUIRED)
participant StatusService as CreditPaymentStatusService<br/>(REQUIRES_NEW)
participant DB as Database
Client->>Service: approvePayment()
Service->>DB: BEGIN Transaction (MainTx)
Service->>Service: PG 승인 API 호출
Service->>StatusService: markAsPgApproved()
Note over Service,DB: 메인 트랜잭션 일시 중지
StatusService->>DB: BEGIN Transaction (NewTx)
StatusService->>DB: UPDATE status = PG_APPROVED
StatusService->>DB: COMMIT (NewTx) ✅ 즉시 커밋!
Note over Service,DB: 메인 트랜잭션 재개
Service->>Service: Credit 발급 API 호출
Service->>DB: UPDATE status = COMPLETED
Service->>DB: COMMIT (MainTx)
Service->>Client: 반환
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 옵션 비교
graph TD
Start{트랜잭션<br/>호출} --> HasTx{기존 트랜잭션<br/>존재?}
HasTx -->|있음| Required[REQUIRED:<br/>참여]
HasTx -->|있음| RequiresNew[REQUIRES_NEW:<br/>새로 생성<br/>기존 일시 중지]
HasTx -->|있음| Supports[SUPPORTS:<br/>참여]
HasTx -->|있음| Mandatory[MANDATORY:<br/>참여]
HasTx -->|있음| NotSupported[NOT_SUPPORTED:<br/>일시 중지]
HasTx -->|있음| Never[NEVER:<br/>예외 발생 ❌]
HasTx -->|있음| Nested[NESTED:<br/>중첩 트랜잭션]
HasTx -->|없음| Required2[REQUIRED:<br/>생성]
HasTx -->|없음| RequiresNew2[REQUIRES_NEW:<br/>생성]
HasTx -->|없음| Supports2[SUPPORTS:<br/>트랜잭션 없이 실행]
HasTx -->|없음| Mandatory2[MANDATORY:<br/>예외 발생 ❌]
HasTx -->|없음| NotSupported2[NOT_SUPPORTED:<br/>트랜잭션 없이 실행]
HasTx -->|없음| Never2[NEVER:<br/>트랜잭션 없이 실행]
HasTx -->|없음| Nested2[NESTED:<br/>생성]
style Required fill:#c8e6c9
style RequiresNew fill:#fff9c4
style Never fill:#ffcdd2
style Mandatory2 fill:#ffcdd2
2.9. Propagation 옵션 비교표
| Propagation | 기존 트랜잭션 | 동작 | 사용 빈도 |
|---|---|---|---|
| REQUIRED (기본) | 있음 | 참여 | ⭐⭐⭐⭐⭐ |
| 없음 | 생성 | ||
| REQUIRES_NEW | 있음 | 새로 생성 (기존 일시 중지) | ⭐⭐⭐⭐ |
| 없음 | 생성 | ||
| SUPPORTS | 있음 | 참여 | ⭐⭐ |
| 없음 | 트랜잭션 없이 실행 | ||
| MANDATORY | 있음 | 참여 | ⭐ |
| 없음 | 예외 발생 | ||
| NOT_SUPPORTED | 있음 | 일시 중지 | ⭐ |
| 없음 | 트랜잭션 없이 실행 | ||
| NEVER | 있음 | 예외 발생 | ⭐ |
| 없음 | 트랜잭션 없이 실행 | ||
| NESTED | 있음 | 중첩 트랜잭션 | ⭐⭐ |
| 없음 | 생성 |
3. 실제 프로젝트에서의 활용
3.1. 문제 상황: REQUIRED의 한계
결제 승인 프로세스를 생각해봅시다:
- PG 승인
- 상태를
PG_APPROVED로 저장 - Credit 발급
- 최종 완료
만약
REQUIRED를 사용하면?
@Transactional
fun approvePayment(...) {
// 같은 트랜잭션 내에서 실행
creditPaymentStatusService.markAsPgApproved(...) // REQUIRED
// Credit 발급 실패
supportersServiceClient.issueCredit(...) // ❌ 예외
// 전체 롤백 → PG_APPROVED 상태도 롤백됨! ❌
// 문제: PG는 승인되었는데 DB에는 기록이 없음!
}
문제점: PG는 이미 승인되었는데, DB에는 기록이 없어서 추적이 불가능합니다.
sequenceDiagram
participant Client
participant Service as CreditPaymentService<br/>(REQUIRED)
participant StatusService as CreditPaymentStatusService<br/>(REQUIRED)
participant DB as Database
Client->>Service: approvePayment()
Service->>DB: BEGIN Transaction
Service->>Service: PG 승인 API 호출 ✅
Service->>StatusService: markAsPgApproved()
StatusService->>DB: UPDATE status = PG_APPROVED
Note over StatusService,DB: 아직 커밋 안 됨
Service->>Service: Credit 발급 API 호출 ❌ 실패!
Service->>DB: ROLLBACK
Note over Service,DB: 전체 롤백!<br/>PG_APPROVED 상태도 사라짐
Service->>Client: 예외 발생
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 취소 가능
sequenceDiagram
participant Client
participant Service as CreditPaymentService<br/>(REQUIRED)
participant StatusService as CreditPaymentStatusService<br/>(REQUIRES_NEW)
participant DB as Database
Client->>Service: approvePayment()
Service->>DB: BEGIN Transaction (MainTx)
Service->>Service: PG 승인 API 호출 ✅
Service->>StatusService: markAsPgApproved()
Note over Service,DB: 메인 트랜잭션 일시 중지
StatusService->>DB: BEGIN Transaction (NewTx)
StatusService->>DB: UPDATE status = PG_APPROVED
StatusService->>DB: COMMIT (NewTx) ✅ 즉시 커밋!
Note over Service,DB: 메인 트랜잭션 재개
Service->>Service: Credit 발급 API 호출 ❌ 실패!
Note over Service,DB: 메인 트랜잭션만 롤백<br/>PG_APPROVED는 유지됨
Service->>DB: ROLLBACK (MainTx)
Service->>Service: 보상 트랜잭션 실행<br/>(PG 취소)
Service->>DB: UPDATE status = ISSUE_CREDIT_FAILED
Service->>Client: 예외 발생
3.3. Saga 패턴과 REQUIRES_NEW
Saga 패턴이란?
- 분산 트랜잭션을 여러 단계로 나누어 처리
- 각 단계를 독립적으로 커밋
- 실패 시 보상 트랜잭션(Compensation) 실행
flowchart TD
Start([결제 승인 시작]) --> Step1[Step 1: PG 승인]
Step1 --> Step2[Step 2: 상태 저장<br/>REQUIRES_NEW]
Step2 --> Commit2[커밋 ✅]
Commit2 --> Step3[Step 3: Credit 발급]
Step3 -->|성공| Step4[Step 4: 최종 완료]
Step3 -->|실패 ❌| Compensate[보상 트랜잭션:<br/>PG 취소]
Compensate --> UpdateStatus[상태 업데이트:<br/>ISSUE_CREDIT_FAILED]
Step4 --> End([완료])
UpdateStatus --> End
style Step2 fill:#e1f5ff
style Commit2 fill:#c8e6c9
style Compensate fill:#ffcdd2
style UpdateStatus fill:#fff9c4
4. 트랜잭션 내부 구현 원리
4.1. AOP 프록시 패턴
Spring의 @Transactional은 **AOP (Aspect-Oriented Programming)**를 기반으로 동작합니다.
graph TB
subgraph "클라이언트 관점"
Client[클라이언트]
end
subgraph "Spring이 생성하는 프록시"
Proxy[CreditPaymentServiceProxy]
Interceptor[TransactionInterceptor]
end
subgraph "실제 구현"
Target[CreditPaymentService<br/>실제 객체]
end
subgraph "데이터베이스"
DB[(Database)]
end
Client -->|메서드 호출| Proxy
Proxy -->|트랜잭션 시작| Interceptor
Interceptor -->|실제 메서드 호출| Target
Target -->|DB 작업| DB
DB -->|결과 반환| Target
Target -->|결과 반환| Interceptor
Interceptor -->|트랜잭션 커밋/롤백| DB
Interceptor -->|결과 반환| Proxy
Proxy -->|결과 반환| Client
style Proxy fill:#e1f5ff
style Interceptor fill:#fff9c4
style Target fill:#c8e6c9
프록시 생성 과정
// 원본 클래스
@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 실행 흐름 시각화
sequenceDiagram
participant App as Application
participant TM as TransactionManager
participant TSM as TransactionSynchronizationManager
participant DB as Database
App->>TM: approvePayment() 호출
TM->>TSM: BEGIN Transaction (MainTx)
Note right of TSM: ThreadLocal 상태<br/>MainTx, Connection1, active
TM->>DB: BEGIN Transaction (MainTx)
App->>TM: markAsPgApproved() 호출 (REQUIRES_NEW)
Note over TM,TSM: 1. 일시 중지 (Suspend)
TM->>TSM: suspend() 실행
TSM->>TSM: ThreadLocal 정보 저장<br/>SuspendedResourcesHolder에 보관
TSM->>TSM: ThreadLocal.clear()
Note right of TSM: ThreadLocal 상태<br/>null, null, false
Note over TM,DB: 2. 새 트랜잭션 시작
TM->>TSM: BEGIN Transaction (NewTx)
Note right of TSM: ThreadLocal 상태<br/>NewTx, Connection2, active
TM->>DB: BEGIN Transaction (NewTx)
TM->>DB: UPDATE status = PG_APPROVED
TM->>DB: COMMIT (NewTx) ✅ 즉시 커밋!
TM->>TSM: ThreadLocal.clear()
Note right of TSM: ThreadLocal 상태<br/>null, null, false
Note over TM,TSM: 3. 메인 트랜잭션 재개 (Resume)
TM->>TSM: resume() 실행
TSM->>TSM: SuspendedResourcesHolder에서<br/>트랜잭션 정보 복원
Note right of TSM: ThreadLocal 상태<br/>MainTx, Connection1, active
TM->>DB: UPDATE status = COMPLETED
TM->>DB: COMMIT (MainTx)
TM->>App: 반환
ThreadLocal 상태 변화 요약:
flowchart TD
Start([메인 트랜잭션 시작]) --> State1[ThreadLocal 상태 1:<br/>transaction: MainTx<br/>connection: Connection1<br/>active: true]
State1 --> Suspend[REQUIRES_NEW 호출]
Suspend --> State2[ThreadLocal 상태 2:<br/>transaction: null<br/>connection: null<br/>active: false<br/><br/>SuspendedResourcesHolder에<br/>MainTx 정보 보관]
State2 --> NewTx[새 트랜잭션 시작]
NewTx --> State3[ThreadLocal 상태 3:<br/>transaction: NewTx<br/>connection: Connection2<br/>active: true]
State3 --> Commit[Connection2.commit<br/>즉시 커밋 ✅]
Commit --> State4[ThreadLocal 상태 4:<br/>transaction: null<br/>connection: null<br/>active: false]
State4 --> Resume[메인 트랜잭션 재개]
Resume --> State5[ThreadLocal 상태 5:<br/>transaction: MainTx<br/>connection: Connection1<br/>active: true<br/><br/>복원됨]
State5 --> End([메인 트랜잭션 커밋])
style State1 fill:#e3f2fd
style State2 fill:#fff9c4
style State3 fill:#c8e6c9
style State4 fill:#fff9c4
style State5 fill:#e3f2fd
style Commit fill:#4caf50
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()
graph LR
A[메서드 시작] --> B[Connection1 획득]
B --> C[DB 작업 1]
C --> D[DB 작업 2<br/>Connection1 재사용]
D --> E[DB 작업 3<br/>Connection1 재사용]
E --> F[Connection1.commit]
style B fill:#c8e6c9
style D fill:#c8e6c9
style E fill:#c8e6c9
style F fill:#4caf50
REQUIRES_NEW에서의 별도 Connection
@Transactional
fun approvePayment(...) {
// 메인 트랜잭션: Connection1
val payment = paymentRepository.findCreditPaymentByPgOrderId(...)
// REQUIRES_NEW: Connection2 (새로운 Connection!)
creditPaymentStatusService.markAsPgApproved(...)
// Connection2.commit() ← 즉시 커밋
// 메인 트랜잭션 재개: Connection1
paymentRepository.saveCreditPayment(...)
// Connection1.commit() (나중에)
}
graph TD
A[메서드 시작] --> B[Connection1 획득<br/>메인 트랜잭션]
B --> C[DB 작업 1]
C --> D[REQUIRES_NEW 호출]
D --> E[Connection1 일시 중지]
E --> F[Connection2 획득<br/>새 트랜잭션]
F --> G[DB 작업 2]
G --> H[Connection2.commit<br/>즉시 커밋 ✅]
H --> I[Connection2 반환]
I --> J[Connection1 재개]
J --> K[DB 작업 3]
K --> L[Connection1.commit]
style B fill:#e3f2fd
style E fill:#fff9c4
style F fill:#c8e6c9
style H fill:#4caf50
style J fill:#e3f2fd
style L fill:#4caf50
5. 성능 최적화 팁
5.1. 트랜잭션 범위 최소화
// ❌ 나쁜 예: 트랜잭션이 너무 길게 유지됨
@Transactional
fun processPayment() {
// 외부 API 호출 (느림)
val pgResult = nicePayService.approvePayment(...) // 2초 소요
// DB 작업
paymentRepository.save(...) // 0.01초 소요
}
// 트랜잭션이 2초 동안 유지됨 → Connection이 오래 점유됨
// ✅ 좋은 예: 트랜잭션 범위 최소화
fun processPayment() {
// 외부 API 호출 (트랜잭션 밖)
val pgResult = nicePayService.approvePayment(...) // 2초 소요
// 트랜잭션 내에서만 DB 작업
savePaymentInTransaction(pgResult) // 0.01초만 트랜잭션 유지
}
@Transactional
private fun savePaymentInTransaction(pgResult: Any) {
paymentRepository.save(...)
}
5.2. 읽기 전용 트랜잭션
@Transactional(readOnly = true)
fun findPayment(id: String) {
// 읽기 전용 트랜잭션
// 성능 최적화 (쓰기 작업 방지)
return paymentRepository.findCreditPaymentById(id)
}
5.3. Connection Pool 설정
# application.properties
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
5.4. 트랜잭션 로그 활성화 (개발 환경)
# application.properties
logging.level.org.springframework.transaction=DEBUG
logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.hibernate.SQL=DEBUG
6. 주의사항
6.1. 같은 클래스 내 메서드 호출
class CreditPaymentService {
@Transactional
fun publicMethod() {
// ❌ 트랜잭션이 적용되지 않음!
privateMethod() // 프록시를 거치지 않음
}
@Transactional
private fun privateMethod() {
// 트랜잭션 미적용
}
}
해결 방법:
@Transactional은 public 메서드에만 적용- 다른 클래스의 메서드를 호출하거나
self.method()호출 (권장하지 않음)
6.2. 예외 처리
@Transactional
fun mayFail() {
try {
// 예외 발생
throw RuntimeException()
} catch (e: Exception) {
// 예외를 잡으면 롤백되지 않음!
// 기본적으로 RuntimeException만 롤백
}
}
해결 방법:
@Transactional(rollbackFor = [Exception::class])
fun mayFail() {
// 모든 예외에 대해 롤백
}
마무리
Spring의 @Transactional은 강력한 도구이지만, 내부 동작을 이해하지 못하면 예상치 못한 문제가 발생할 수 있습니다.
핵심 포인트:
- REQUIRED: 기본값, 대부분의 경우에 사용
- REQUIRES_NEW: 독립적인 트랜잭션이 필요할 때 (Saga 패턴 등)
- AOP 프록시:
@Transactional은 프록시를 통해 동작 - ThreadLocal: 트랜잭션 상태는 ThreadLocal에 저장
- Connection 관리: 같은 트랜잭션은 같은 Connection, REQUIRES_NEW는 별도 Connection
이러한 원리를 이해하면 트랜잭션 관련 문제를 더 쉽게 디버깅하고 최적화할 수 있습니다!
참고 자료: