트랜잭션 실무 패턴
이론적인 ACID를 이해했다면, 실무에서 트랜잭션을 어떻게 관리하는지 알아야 합니다. 서비스 레이어의 트랜잭션 관리부터 분산 환경까지, 올바른 트랜잭션 설계가 서비스 안정성의 핵심입니다.
서비스 레이어의 트랜잭션 관리
애플리케이션에서 트랜잭션은 보통 서비스 레이어에서 관리합니다. 비즈니스 로직의 단위가 곧 트랜잭션의 단위입니다.
@Service
public class OrderService {
@Transactional // 메서드 전체가 하나의 트랜잭션
public Order createOrder(Long userId, List<OrderItemDto> items) {
// 1. 재고 확인 및 차감
for (OrderItemDto item : items) {
productRepository.decreaseStock(
item.getProductId(), item.getQuantity()
);
}
// 2. 주문 생성
Order order = orderRepository.save(new Order(userId));
// 3. 주문 상세 생성
for (OrderItemDto item : items) {
orderItemRepository.save(new OrderItem(order.getId(), item));
}
return order;
// 정상 종료 → 자동 COMMIT
// 예외 발생 → 자동 ROLLBACK (RuntimeException 기본)
}
}트랜잭션 범위의 원칙
| 원칙 | 설명 | 이유 |
|---|---|---|
| 짧게 유지 | 트랜잭션이 길면 락 점유 시간 증가 | 동시성 저하, 데드락 위험 |
| 외부 호출 제외 | HTTP 호출, 파일 I/O는 트랜잭션 밖에서 | 외부 응답 지연 시 락 장기 보유 |
| 읽기 전용 분리 | 조회만 하는 트랜잭션은 readOnly=true | 데이터 변경 방지, 최적화 힌트 |
| 필요한 곳에만 | 모든 메서드에 트랜잭션을 걸지 말 것 | 불필요한 오버헤드 방지 |
| 예외 처리 주의 | checked 예외 시 롤백 정책 확인 | 프레임워크별 기본 동작 상이 |
트랜잭션 전파 (Propagation)
중첩 메서드 호출 시 트랜잭션을 어떻게 전파할지 결정합니다. Spring에서 가장 중요한 설정 중 하나입니다.
@Service
public class OrderService {
@Transactional
public void createOrder(OrderDto dto) {
Order order = orderRepository.save(dto.toEntity());
// 주문 로그는 주문 롤백과 무관하게 항상 저장
auditService.log(order); // REQUIRES_NEW
// 포인트 적립은 실패해도 주문은 유지
pointService.addPoints(order); // NESTED or try-catch
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(Order order) {
auditRepository.save(new AuditLog(order));
// 독립 트랜잭션: 주문이 롤백되어도 로그는 커밋됨
}
} 트랜잭션 필요?
/ \
Yes No
/ \
기존 트랜잭션에 SUPPORTS 또는
참여해도 되나? NOT_SUPPORTED
/ \
Yes No
/ \
REQUIRED REQUIRES_NEW읽기 전용 트랜잭션
조회만 수행하는 메서드에 readOnly=true를 지정하면 DBMS가 내부 최적화를 수행할 수 있습니다.
@Transactional(readOnly = true)
public List<Order> getOrdersByUser(Long userId) {
return orderRepository.findByUserId(userId);
}분산 트랜잭션과 2PC
마이크로서비스 환경에서는 여러 서비스가 각각의 데이터베이스를 가집니다. 하나의 비즈니스 로직이 여러 DB에 걸치면 분산 트랜잭션이 필요합니다.
Saga 패턴
Saga 패턴은 분산 트랜잭션의 대안입니다. 각 서비스가 로컬 트랜잭션을 실행하고, 실패 시 보상 트랜잭션(Compensating Transaction)을 실행합니다.
Choreography vs Orchestration
| 비교 | 2PC | Saga |
|---|---|---|
| 일관성 | 강한 일관성 (ACID) | 최종적 일관성 (Eventual) |
| 성능 | 락 대기로 느림 | 비동기로 빠름 |
| 복잡도 | 코디네이터 필요 | 보상 로직 필요 |
| 격리성 | 완전한 격리 | 중간 상태 노출 가능 |
| 적합 환경 | 단일 DB 또는 소수 DB | 마이크로서비스 |
Outbox 패턴
Saga 패턴에서 메시지 발행과 DB 업데이트의 원자성을 보장하는 패턴입니다.
멱등성 (Idempotency)
분산 환경에서 메시지가 중복 전달될 수 있으므로, 같은 요청을 여러 번 처리해도 결과가 같아야 합니다.
낙관적 vs 비관적 동시성 제어
트랜잭션이 동시에 같은 데이터를 수정할 때의 충돌 처리 전략입니다.
@Entity
public class Product {
@Id
private Long id;
private int stock;
@Version // 자동으로 version 관리
private int version;
}
// 충돌 시 OptimisticLockException 발생
// → 재조회 후 재시도| 전략 | 방식 | 충돌 처리 | 적합 상황 |
|---|---|---|---|
| 비관적 | SELECT FOR UPDATE | 사전 방지 | 충돌이 빈번한 경우 |
| 낙관적 | 버전 컬럼 비교 | 사후 감지 | 충돌이 드문 경우 |
배치 처리의 트랜잭션
대량 데이터를 처리하는 배치 작업은 적절한 단위로 커밋해야 합니다.
ItemReader → ItemProcessor → ItemWriter
│
chunk 단위 커밋
chunk-size=1000
1,000건 읽기 → 처리 → 쓰기 → COMMIT
1,000건 읽기 → 처리 → 쓰기 → COMMIT
...
→ 실패 시 해당 chunk만 ROLLBACK 후 재시도트랜잭션 실무 체크리스트
□ 트랜잭션 경계가 서비스 레이어에 있는가?
□ 트랜잭션 내에 외부 API 호출이 없는가?
□ 읽기 전용 메서드에 readOnly=true를 설정했는가?
□ 트랜잭션 전파 속성이 적절한가?
□ 장기 트랜잭션 (30초 이상) 가능성은 없는가?
□ 배치 처리 시 적절한 단위로 커밋하고 있는가?
□ 예외 처리 시 롤백 정책을 확인했는가?
□ 분산 환경에서 보상 트랜잭션이 정의되어 있는가?
□ 멱등성이 보장되는가?
□ 데드락 방지를 위해 테이블 접근 순서를 통일했는가?정리
트랜잭션 실무에서 가장 중요한 것은 적절한 범위 설정입니다. 트랜잭션을 너무 넓게 잡으면 동시성이 떨어지고, 너무 좁게 잡으면 데이터 정합성이 깨집니다.
단일 DB 환경에서는 서비스 레이어에서 @Transactional로 관리하고, 전파 속성으로 중첩 호출을 제어합니다. 분산 환경에서는 2PC 대신 Saga 패턴이 표준이며, Outbox 패턴으로 메시지 원자성을, 멱등성으로 중복 처리를 보장합니다.
낙관적/비관적 잠금은 충돌 빈도에 따라 선택합니다. 대부분의 웹 애플리케이션은 읽기가 쓰기보다 압도적으로 많으므로 낙관적 잠금이 기본 선택이고, 금융이나 재고 관리처럼 충돌이 빈번한 경우에 비관적 잠금을 사용합니다.
시험에서는 2PC의 두 단계(Prepare와 Commit)와 블로킹 문제, Saga 패턴의 보상 트랜잭션 개념, Choreography와 Orchestration의 차이, 낙관적 잠금과 비관적 잠금의 구분이 빈출됩니다.
다음 장에서는 동시성 문제의 구체적인 유형과 해결책인 동시성 제어를 다루겠습니다.