Skip to main content
트랜잭션을 올바르게 사용하면 데이터 무결성을 보장하면서도 성능을 최적화할 수 있습니다.

핵심 원칙

ACID 보장

원자성, 일관성, 격리성, 지속성 데이터 무결성 유지

짧은 트랜잭션

필요한 만큼만 락 대기 최소화

적절한 격리 수준

요구사항에 맞게 성능과 일관성 균형

에러 처리

명확한 롤백 전략 일관된 상태 유지

트랜잭션 범위

✅ 좋은 패턴: 짧고 명확한 범위

❌ 나쁜 패턴: 긴 트랜잭션

트랜잭션 안에 포함하지 말아야 할 것: - 외부 API 호출 - 파일 I/O - 이메일/SMS 발송 - 복잡한 계산 - 느린 쿼리

격리 수준 선택

용도별 권장 격리 수준

에러 처리 전략

패턴 1: Try-Catch로 명확한 처리

패턴 2: 부분 롤백으로 복구

동시성 제어

낙관적 락 (Optimistic Lock)

비관적 락 (Pessimistic Lock)

데드락 방지

일관된 순서로 락 획득

타임아웃 설정

성능 최적화

배치 처리

읽기/쓰기 분리

테스트 전략

트랜잭션 롤백 테스트

동시성 테스트

체크리스트

트랜잭션 설계 시

  • 트랜잭션 범위가 최소화되어 있는가?
  • 외부 API 호출이 트랜잭션 밖에 있는가?
  • 적절한 격리 수준을 선택했는가?
  • 에러 처리가 명확한가?
  • 데드락 가능성을 고려했는가?

코드 리뷰 시

  • 중첩 트랜잭션이 필요한가?
  • 락 순서가 일관되는가?
  • 롤백 시나리오가 명확한가?
  • 성능 영향을 고려했는가?
  • 테스트 코드가 있는가?

안티패턴

1. 트랜잭션 남용

2. 에러 무시

3. 불필요한 중첩

실전 복잡한 시나리오

실무에서 자주 마주치는 복잡한 트랜잭션 패턴과 해결 방법입니다.

시나리오 1: 다중 Model 간 트랜잭션 공유

여러 Model의 메서드를 하나의 트랜잭션으로 묶어야 할 때
트랜잭션 컨텍스트 공유:
  • createOrderWithPayment가 최상위 트랜잭션 시작
  • InventoryModel.reserveStock, PaymentModel.processPayment는 같은 트랜잭션 컨텍스트 공유
  • 어느 단계에서든 에러 발생 시 전체 롤백 (재고 차감도 취소됨)

시나리오 2: 부분 롤백 with Savepoint

일부 작업만 롤백하고 나머지는 유지하고 싶을 때
Savepoint 활용:
  • 주문 생성은 반드시 성공
  • 쿠폰 적용 실패 시 쿠폰 관련 작업만 롤백
  • 전체 트랜잭션은 계속 진행
Savepoint 주의사항: - Savepoint는 수동으로 생성/관리해야 합니다 (자동 생성 안 됨) - 중첩 레벨이 깊어질수록 복잡도 증가 - 대부분의 경우 전체 롤백이 더 안전하고 단순합니다

시나리오 3: 동시성 제어 - 비관적 락

여러 사용자가 동시에 같은 데이터를 수정할 때
동시성 제어 비교:

시나리오 4: 데드락 재시도 패턴

데드락 발생 시 자동 재시도
재시도 전략:
  1. 데드락 에러만 재시도 (다른 에러는 즉시 실패)
  2. 지수 백오프로 재시도 간격 증가 (100ms → 200ms → 400ms)
  3. 최대 재시도 횟수 제한

시나리오 5: 외부 API 호출과 트랜잭션 분리

외부 API는 트랜잭션 밖에서 호출
패턴 설명:
  1. 외부 API 호출은 트랜잭션 밖에서
    • 외부 API는 느리고 롤백 불가능
    • 트랜잭션 시간을 최소화해야 함
  2. 보상 트랜잭션(Saga 패턴)
    • 외부 API 성공 후 DB 업데이트 실패 시
    • 외부 API 취소 호출 (보상 작업)
  3. 에러 처리
    • 각 단계별로 명확한 에러 처리
    • 실패 지점에 따라 다른 복구 전략
분산 트랜잭션 Best Practice: - 외부 시스템과 통신할 때는 Saga 패턴 사용 - 각 단계마다 실패 시 보상 작업 정의 - 멱등성(Idempotency) 보장으로 재시도 안전성 확보

다음 단계

@transactional

데코레이터 사용법

수동 트랜잭션

transaction() 직접 사용

UpsertBuilder

트랜잭션 내 데이터 저장

에러 처리

API 에러 처리하기