
실무에서 @Transactional은 꽤나 자주 사용된다.
선언 하나로 트랜잭션을 관리하는 아주 편리한 어노테이션이지만 정확히 알고 쓰는게 좋다.
왜 @Transactional인가?
Spring 프로젝트에서 @Transactional은 서비스 레이어 어디서나 볼 수 있고 코드 한 줄로 트랜잭션의 시작·커밋·롤백을 자동 처리해주기 때문에 편리하지만, 바로 그 편리함 때문에 실무에서 가장 많은 오해와 버그를 낳는 기능이기도 하다.
원자성 (Atomicity)
모든 작업이 성공하거나, 하나라도 실패하면 전부 롤백됩니다.
격리성 (Isolation)
다른 트랜잭션의 중간 결과로부터 보호합니다.
지속성 (Durability)
커밋된 데이터는 장애가 발생해도 유지됩니다.
일관성 (Consistency)
DB가 항상 유효한 상태를 유지하도록 보장합니다.
내부 동작 원리
— AOP 프록시
@Transactional은 스프링 AOP(Aspect-Oriented Programming) 프록시를 통해 동작한다.
스프링 컨테이너는 해당 빈을 직접 반환하지 않고, 프록시 객체를 생성하여 반환하며 메서드 호출이 프록시를 통과할 때 트랜잭션 인터셉터가 끼어들어 트랜잭션을 시작·종료한다.
**
호출자 (Controller)
│
▼
┌─────────────────────────────────┐
│ TransactionInterceptor (Proxy) │
│ 1. 트랜잭션 시작 │
│ 2. 실제 메서드 호출 ──────► │──► OrderService.createOrder()
│ 3-a. 성공 → COMMIT │
│ 3-b. 예외 → ROLLBACK │
└─────────────────────────────────┘
같은 클래스 내부에서 this.method()로 호출하면 프록시를 우회하므로 @Transactional이 적용되지 않습니다. 실무에서 가장 자주 발생하는 실수입니다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryRepository inventoryRepository;
private final PaymentClient paymentClient;
/**
* 주문 생성: 재고 차감 + 주문 저장이 하나의 트랜잭션으로 처리
* 둘 중 하나라도 실패하면 전부 롤백
*/
@Transactional
public OrderResponse createOrder(OrderRequest request) {
// 재고 차감
Inventory inventory = inventoryRepository.findByProductId(request.productId())
.orElseThrow(() -> new ProductNotFoundException());
inventory.decrease(request.quantity()); // Dirty Checking으로 UPDATE 발생
// 주문 저장
Order order = Order.create(request);
orderRepository.save(order);
return OrderResponse.from(order);
}
// 조회 전용: readOnly = true로 성능 최적화
@Transactional(readOnly = true)
public List<OrderResponse> getOrders(Long userId) {
return orderRepository.findByUserId(userId)
.stream()
.map(OrderResponse::from)
.toList();
}
}
조회 메서드에는 @Transactional(readOnly = true)와 같이
readOnly 를 붙이면 좋다.
Dirty Checking 비활성화 스냅샷을 저장하지 않아 메모리 절약, 플러시 생략
DB 읽기 전용 힌트 전달 MySQL Replication 환경에서 Slave DB로 자동 라우팅 가능
의도 명확화 코드 리뷰 시 "이 메서드는 데이터를 수정하지 않음"을 즉시 전달
전파 속성 (Propagation) — 실무 핵심
전파 속성은 트랜잭션이 이미 존재할 때 새로운 트랜잭션을 어떻게 처리할지 결정합니다. 실무에서 자주 쓰는 3가지만 제대로 알아두세요.
속성 동작 주요 사용처
REQUIRED : 기존 트랜잭션 참여, 없으면 새로 생성 (기본값) 일반적인 서비스 로직
REQUIRES_NEW : 항상 새 트랜잭션 생성, 기존 트랜잭션 일시 중단 로그 저장, 알림 발송 (본 트랜잭션과 분리)
NOT_SUPPORTED : 트랜잭션 없이 실행, 기존 트랜잭션 일시 중단 트랜잭션 불필요한 단순 조회
REQUIRES_NEW 예시 - 로그를 항상 남겨야 할 때
@Service
public class AuditLogService {
/**
* REQUIRES_NEW: 호출한 트랜잭션과 독립적으로 실행됩니다.
* 외부 트랜잭션이 롤백되더라도 이 로그는 커밋됩니다.
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(String action, Long userId) {
auditLogRepository.save(AuditLog.of(action, userId, LocalDateTime.now()));
}
}
@Service
public class OrderService {
@Transactional
public void createOrder(OrderRequest req) {
// 별도 트랜잭션으로 로그 먼저 저장
auditLogService.saveAuditLog("ORDER_ATTEMPT", req.userId());
// 아래 로직이 실패해서 롤백되어도 위 로그는 유지됨
inventory.decrease(req.quantity());
orderRepository.save(Order.create(req));
}
}
— 롤백 규칙을 지키지 않으면 데이터가 썩는다
🚨 Spring은 기본적으로 Unchecked 예외(RuntimeException, Error)에서만 롤백하며
Checked 예외(Exception)는 롤백하지 않는다
// ❌ 잘못된 예
@Transactional
public void processFile() throws IOException {
orderRepository.save(order);
fileService.write(file); // IOException 발생 → 롤백 안 됨!
}
// ✅ 올바른 예
@Transactional(rollbackFor = Exception.class)
public void processFile() throws IOException {
orderRepository.save(order);
fileService.write(file); // IOException → 이제 롤백됨 ✓
}
// ✅ 또는 Checked Exception을 RuntimeException으로 래핑
@Transactional
public void processFile() {
try {
fileService.write(file);
} catch (IOException e) {
throw new FileProcessingException("파일 처리 실패", e);
}
}
@Transactional 사용 전 확인사항
✅ 조회 메서드에는 항상 readOnly = true 붙이기
✅ Checked Exception 롤백 필요 시 rollbackFor 명시
✅ 같은 클래스 내부 this 호출은 @Transactional 무효 → 클래스 분리
✅ 외부 API / 이메일 발송은 트랜잭션 밖으로 분리 (또는 REQUIRES_NEW)
✅ 트랜잭션 범위를 최대한 짧게 — 락 보유 시간 최소화
✅ @Transactional은 public 메서드에만 적용됨 (private 무효)
✅ Lazy Loading은 트랜잭션 내부에서 처리 (LazyInitializationException 방지)
핵심을 요약하면 조회엔 readOnly, 수정엔 REQUIRED, 독립 실행엔 REQUIRES_NEW, Checked 예외엔 rollbackFor. 이 네 가지만 제대로 이해해도 실무에서 발생하는 트랜잭션 버그의 80%는 예방할 수 있다.