Spring에서는 @Transactional 어노테이션을 통해 트랜잭션을 선언적으로 처리할 수 있으며, 트랜잭션의 전파(Propagation) 옵션을 설정해 트랜잭션 간의 흐름을 제어할 수 있다.
inventoryService
@Transactional // propagation = REQUIRED (기본값)
public void decraseStock(...) {
}
@Transactional // propagation = REQUIRED (기본값)
public Order createOrder(...) {
inventoryService.decreaseStock(...); // 같은 트랜잭션에 참여하므로 롤백
orderRepository.save(...); // save 메서드같은 경우도 REQUIRED
// 예외 발생 시 둘 다 롤백됨
}
@Transactional
public void processPayment() {
Payment payment = new Payment();
payment.setOrderId(orderId);
payment.setAmount(amount);
payment.setStatus(PaymentStatus.PROCESSING);
Payment savedPayment = paymentRepository.save(payment); // 예외 발생 시 롤백됨
try {
// 외부 결제 게이트웨이 호출 (이니시스, 토스같은거)
// 예외 발생 가능성 있는 로직
boolean success = callExternalPaymentGateway(orderId, amount);
} catch (Exception e) {
// 예외가 발생하면 rollback 발생
throw e; // throw가 없고 return으로 정상종료된다면 롤백 X
} finally {
auditLogService.logActivity(...);
// REQUIRES_NEW로 별도 트랜잭션이므로 processPayment에서 예외가 터져도롤백되지 않음.
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // 예외 발생해도 상위 트랜잭션과 분리되어 반영됨
public void logActivity(...) {
auditLogRepository.save(...);
}
그 외에도 SUPPORT, MANDATORY, NEVER, NESTED, NOT_SUPPORTED등의 설정이 있지만, 잘 사용하지 않는다.

catch (Exception e) {
payment.setStatus(ERROR);
paymentRepository.save(...); // 이 시점에서 트랜잭션은 rollbackOnly 상태
return; //예외를 던지지 않으면 트랜잭션은 커밋됨
}
catch (Exception e) {
payment.setStatus(ERROR);
paymentRepository.save(...);
throw e; // 트랜잭션은 롤백됨
}
public void noTransactionalMethod() {
MyEntity saved = repository.save(new MyEntity("test")); // 여기서 트랜잭션 시작 → 커밋
Optional<MyEntity> result = repository.findById(saved.getId()); // DB에서 조회 가능
System.out.println(result.isPresent()); // true
}
@Transactional
public void noTransactionalMethod() {
MyEntity saved = repository.save(new MyEntity("test")); // 여기서 트랜잭션 시작 → 커밋
Optional<MyEntity> result = repository.findById(saved.getId()); // DB에서 조회 가능
System.out.println(result.isPresent()); // true
}

트랜잭션 격리 수준은 여러 트랜잭션이 동시에 실행될 때, 각 트랜잭션이 다른 트랜잭션에서 변경한 데이터를 어느 정도까지 읽을 수 있는지를 제어하는 설정이다.
격리 수준이 높을수록 데이터 정합성이 올라가지만, 성능과 병렬성은 떨어지는 trade-off가 존재함!
특징
사용 예
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public List<StockPrice> getCurrentPrices() {
return stockRepository.findLatest();
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public Order getOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void processOrder(Long id) {
Order order1 = orderRepository.findById(id).orElseThrow();
// 외부에서 update & commit 가능
Order order2 = orderRepository.findById(id).orElseThrow();
// order1 ≠ order2 가능성 있음 (Non-repeatable Read)
}
@Transactional(isolation = Isolation.REPEATABLE_READ)
public FinancialReport generateReport(...) {
// 동일한 조건으로 여러 쿼리 실행해도 결과 일관됨
}
주의: 범위 조회에서는 Phantom Read 발생 가능
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void processNewCustomers() {
long count = customerRepository.countNewCustomers();
List<Customer> list = customerRepository.findAllNewCustomers();
// 두 결과가 다를 수 있음
}
특징
=> A트랜잭션이 커밋/롤백될 때 까지, B트랜잭션은 Lock을 획득하지 못하고 A트랜잭션의 Lock이 끝날때까지 대기
사용 예
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transferFunds(...) {
// from → to 계좌로 안전하게 자금 이체
}
교착 상태 발생 가능 예시
트랜잭션 A: 계좌 X → Y 이체
트랜잭션 B: 계좌 Y → X 이체
서로 잠금 획득을 기다리다가 Deadlock 발생
(트랜잭션 A에서는 X를 먼저 Lock을 걸고 Y의 Lock을 획득해야하는데, 트랜잭션 B에서는 Y를 먼저 Lock을 걸었으므로 획득이 불가능. 트랜잭션 B의 경우에도 X의 Lock을 획득 불가능 -> 무한 대기)
해결 방법
@Transactional(isolation = Isolation.SERIALIZABLE, timeout = 10)
public void criticalOperation() {
// 제한 시간 내 실행, 교착 상태 방지
}

=> 격리 수준 선택은 성능 ↔ 정합성 트레이드오프를 고려하여,
비즈니스 요구사항과 시스템 특성에 따라 신중하게 결정해야 합니다.