@Transactional
Spring Framework에서 제공하는 선언적 트랜잭션 관리를 위한 어노테이션
- 클래스 또는 메서드 레벨에 적용 가능
- 선언적 트랜잭션 관리 방식 제공
- AOP 기반으로 동작
💡동작방식
1. Spring 컨테이너가 @Transactional이 붙은 빈을 감지
2. 해당 빈에 대한 프록시 객체 생성
3. 메서드 호출 시 트랜잭션 관리자(PlatformTransactionManager)를 통해 트랜잭션 처리
4. 정상 종료 시 커밋, 예외 발생 시 롤백
💡주의사항
private 메서드 제약
내부 호출 문제
public void test1() {
test2();
}
@Transactional
public void test2() {
// 상위 메서드인 test1이 타깃 오브젝트가 되어 트랜잭션 적용 안됨
}
@Service
public class UserService {
public void external() {
// 직접 호출 - 트랜잭션 적용 안됨
this.internal();
}
@Transactional
public void internal() {
// 트랜잭션이 필요한 로직
}
}
// 자기 자신 의존성 주입
@Service
public class UserService2 {
@Autowired
private UserService userService2;
public void external() {
// 프록시를 통한 호출
userService2.internal();
}
}
// 클래스 분리 등
롤백 처리
✍️ 트랜잭션 전파
기존 트랜잭션이 있을 때 새로운 트랜잭션을 어떻게 처리할지 결정하는 방식
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void someMethod() {
// 메서드 로직
}
✍️트랜잭션 격리수준(Transaction Isolation Level)
동시에 여러 트랜잭션이 처리될 때, 트랜잭션끼리 얼마나 서로 고립되어 있는지를 나타냄
- Read Uncommitted (가장 낮은 격리수준)
- 다른 트랜잭션에 의해 커밋되지 않은 데이터를 읽을 수 있음
- Dirty Read, Non-Repeatable Read, Phantom Read 모두 발생 가능
- 성능은 가장 좋지만, 데이터 일관성이 가장 낮음
- Read Committed
- 커밋된 데이터만 읽을 수 있음
- Dirty Read 방지, 하지만 Non-Repeatable Read와 Phantom Read는 여전히 발생 가능
- 대부분의 데이터베이스 시스템의 기본 격리수준
- Repeatable Read
- 트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회 가능
- Dirty Read, Non-Repeatable Read 방지, 하지만 Phantom Read는 여전히 발생 가능
- 동일한 쿼리를 실행했을 때 항상 같은 결과를 보장
- Serializable (가장 높은 격리수준)
- 가장 엄격한 격리수준으로, 완벽한 읽기 일관성 제공
- Dirty Read, Non-Repeatable Read, Phantom Read 모두 방지
- 성능은 가장 낮지만, 데이터 일관성이 가장 높음
- Dirty Read: 커밋되지 않은 데이터를 읽는 현상
- Non-Repeatable Read: 같은 쿼리를 여러 번 실행했을 때 결과가 다른 현상
- Phantom Read: 같은 쿼리를 여러 번 실행했을 때 이전에 없던 레코드가 나타나는 현상
@Transactional(isolation = Isolation.READ_COMMITTED)
public void someMethod() {
// 메서드 로직
}
@Transactional(rollbackFor = Exception.class)
@Transactional(noRollbackFor = RuntimeException.class)
@Transactional(readOnly = true)
@Transactional(timeout = 10)
@Transactional(
propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.SERIALIZABLE,
timeout = 10,
rollbackFor = Exception.class
)
public void criticalOperation() {
// 중요 로직
}
// 트랜잭션의 수는 ?
@Service
public class OrderService {
@Transactional
public void createOrder() {
saveOrder();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveOrder() {
// 주문 저장 로직
}
}
// 트랜잭션이 롤백 되는가?
@Service
public class PaymentService {
@Transactional
public void processPayment() {
try {
// 결제 로직
throw new RuntimeException("결제 오류");
} catch (RuntimeException e) {
log.error("에러 발생");
}
}
}
@Service
public class FileService {
@Transactional
public void processFile() throws IOException {
throw new IOException("파일 처리 오류");
}
}
// 트랜잭션이 롤백 되는가?
@Service
public class PaymentService {
@Transactional
public void processPayment() {
try {
// 결제 로직
throw new RuntimeException("결제 오류");
} catch (RuntimeException e) {
log.error("에러 발생");
throw CustomException("에러발생", e);
}
}
}
public class CustomException extends Exception {
public PaymentException(String message, Throwable cause) {
super(message, cause);
}
}
// 실행결과 ?
@Service
public class UserService {
@Transactional(readOnly = true)
public void updateUser(User user) {
userRepository.save(user);
}
}
REFERENCE