트랜잭션은 데이터베이스의 논리적 작업 단위로 하나의 작업을 수행하기 위해 실행되는 일련의 연산들을 의미한다.
트랜잭션은 일관성 유지를 위해 반드시 완료되거나 실패 시 모든 작업이 취소되어야 한다.
트랜잭션은 ACID 원칙을 준수해야 한다.
Atomicity(원자성)
Consistency(일관성)
Isolation(격리성)
Durability(지속성)
Spring에서는 @Transactional 어노테이션을 통해 트랜잭션을 쉽게 관리할 수 있다.
@Service
public class OrderService {
@Transactional
public void placeOrder(Long userId, Long productId) {
// 주문 생성
orderRepository.save(new Order(userId, productId));
// 결제 처리 (여기서 예외 발생 시 롤백됨)
paymentService.processPayment(userId, productId);
}
}
@Transactional을 메서드에 적용하면 예외 발생 시 자동으로 롤백됨Spring에서는 트랜잭션이 어떻게 전파될지를 결정할 수 있다.
@Transactional(propagation = Propagation.REQUIRED)
| 옵션 | 설명 |
|---|---|
| REQUIRED (기본값) | 기존 트랜잭션이 있으면 참여, 없으면 새 트랜잭션 생성 |
| REQUIRES_NEW | 항상 새로운 트랜잭션을 생성 |
| SUPPORTS | 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행 |
| MANDATORY | 반드시 기존 트랜잭션이 있어야 함 (없으면 예외 발생) |
| NOT_SUPPORTED | 트랜잭션 없이 실행 |
| NEVER | 트랜잭션이 있으면 예외 발생 |
트랜잭션이 동시에 실행될 때 데이터 정합성을 보장하기 위해 격리 수준을 설정할 수 있다.
@Transactional(isolation = Isolation.SERIALIZABLE)
| 격리 수준 | 설명 | 발생할 수 있는 문제 |
|---|---|---|
| READ_UNCOMMITTED | 다른 트랜잭션의 커밋되지 않은 변경 내용을 읽을 수 있음 | Dirty Read |
| READ_COMMITTED | 커밋된 데이터만 읽음 | Non-Repeatable Read |
| REPEATABLE_READ | 한 트랜잭션에서 같은 데이터를 여러 번 조회할 때 결과가 동일하게 유지됨 | Phantom Read |
| SERIALIZABLE | 가장 높은 격리 수준, 동시 실행을 막고 순차적으로 처리 | 성능 저하 |
Spring의 @Transactional은 기본적으로 Unchecked Exception(RuntimeException)이 발생하면 롤백된다.
@Transactional(rollbackFor = Exception.class)
RuntimeException과 Error만 롤백 대상이므로 Exception까지 롤백하려면 명시적으로 지정해야 함프록시 방식이므로 같은 클래스 내에서 메서드 호출 시 트랜잭션이 적용되지 않을 수 있음
@Transactional
public void methodA() {
methodB(); // 같은 클래스 내 호출 → 트랜잭션 적용되지 않음
}
Lazy Loading과 트랜잭션 범위
@Transactional이 종료되면 영속성 컨텍스트도 닫히므로 Lazy Loading을 사용하려면 주의해야 함트랜잭션 범위를 최소화
Spring에서 트랜잭션을 관리할 때 @Transactional을 사용하면 편리하지만 전파 옵션, 롤백 조건, 격리 수준 등을 잘 이해하고 사용해야 함을 배웠다.
Lazy Loading과 트랜잭션 범위를 고려하지 않으면 예상치 못한 에러가 발생할 수도 있다.
또한 트랜잭션을 최소한의 범위에서 사용해야 성능을 최적화할 수 있다는 점도 중요하다는 걸 다시금 느꼈습니다.