Spring Boot에서 효율적인 트랜잭션 관리 방법
Spring Boot 애플리케이션에서 트랜잭션 관리는 데이터 일관성을 유지하는 핵심 요소입니다. 올바르게 설정하지 않으면 데이터 손실이나 불일치 문제가 발생할 수 있습니다. 이번 글에서는 Spring의 트랜잭션 관리 전략과 효율적인 사용 방법을 정리해보겠습니다.
1. 트랜잭션 관리 방식
Spring Boot에서 트랜잭션을 관리하는 방법은 크게 두 가지로 나뉩니다.
1) 선언적 트랜잭션 관리 (@Transactional)
가장 일반적인 방식으로, 메서드나 클래스에 @Transactional 어노테이션을 사용하여 트랜잭션을 관리합니다.
- 해당 메서드 실행 중 예외가 발생하면 자동으로 롤백
- 커밋 또는 롤백을 신경 쓰지 않아도 되므로 유지보수가 용이
2) 프로그래밍 방식 트랜잭션 관리 (TransactionTemplate, PlatformTransactionManager)
트랜잭션을 직접 제어해야 할 때 사용합니다.
@Service
public class PaymentService {
private final TransactionTemplate transactionTemplate;
private final PaymentRepository paymentRepository;
public PaymentService(PlatformTransactionManager transactionManager, PaymentRepository paymentRepository) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
this.paymentRepository = paymentRepository;
}
public void processPayment(Payment payment) {
transactionTemplate.executeWithoutResult(status -> {
paymentRepository.save(payment); // 추가적인 비즈니스 로직 수행
});
}
}
- 세밀한 트랜잭션 제어가 필요할 때 유용
- 코드가 길어지고 복잡해질 수 있음
2. @Transactional 사용 시 주의할 점
1) 프록시 기반이므로 같은 클래스 내에서 호출하면 동작하지 않음
Spring의 @Transactional은 기본적으로 프록시 방식을 사용하기 때문에, 같은 클래스 내에서 트랜잭션 메서드를 호출하면 적용되지 않습니다.
잘못된 예:
@Service
public class UserService {
@Transactional
public void createUser(User user) {
saveUser(user);
}
public void saveUser(User user) {
// 같은 클래스 내 호출 → 트랜잭션이 적용되지 않음
userRepository.save(user);
}
}
해결 방법:
- 외부에서 호출하도록 설계하거나, self-invocation 문제를 해결하기 위해 AOP를 활용합니다.
- @Transactional을 사용하는 메서드를 다른 빈(Bean)에서 호출하도록 설계합니다.
2) readOnly 옵션을 적절히 활용하기
데이터 조회 시 @Transactional(readOnly = true)를 사용하면 성능 최적화에 도움이 됩니다.
@Transactional(readOnly = true)
public User getUser(Long id) {
return userRepository.findById(id).orElseThrow();
}
- Hibernate는 readOnly = true일 때 더티 체킹(DIRTY CHECKING)을 수행하지 않으므로 성능이 향상됩니다.
- 단, 읽기 전용 트랜잭션 내에서 save()를 호출하면 예외(Exception)가 발생할 수 있으므로 주의해야 합니다.
3. 트랜잭션 전파 옵션 (Propagation)
Spring은 트랜잭션이 중첩될 때 동작을 제어할 수 있도록 여러 가지 전파(Propagation) 옵션을 제공합니다.
- 전파 옵션 종류
REQUIRED | 기본값. 기존 트랜잭션이 있으면 참여, 없으면 새 트랜잭션 생성 |
REQUIRES_NEW | 기존 트랜잭션을 무시하고 항상 새로운 트랜잭션 시작 |
NESTED | 기존 트랜잭션 내에서 중첩 트랜잭션 실행 |
SUPPORTS | 트랜잭션이 있으면 참여, 없으면 트랜잭션 없이 실행 |
NOT_SUPPORTED | 트랜잭션 없이 실행 |
NEVER | 트랜잭션이 있으면 예외 발생 |
MANDATORY | 기존 트랜잭션이 없으면 예외 발생 |
@Service
public class NotificationService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotification(Notification notification) {
notificationRepository.save(notification);
}
}
- REQUIRES_NEW를 사용하면 부모 트랜잭션과 별개로 커밋/롤백이 가능합니다.
- 전파 옵션을 잘 활용하면 한쪽 로직이 실패해도 다른 트랜잭션이 영향을 받지 않도록 할 수 있습니다.
4. 트랜잭션 롤백 전략
1) 기본 롤백 동작
- @Transactional은 체크 예외(Checked Exception) 발생 시 롤백하지 않고, 런타임 예외(Runtime Exception) 발생 시 롤백합니다.
@Transactional
public void processOrder() throws IOException {
// 체크 예외 throw new IOException("파일 처리 오류");
// 롤백되지 않음
}
@Transactional public void processPayment() {
throw new RuntimeException("결제 오류");
// 롤백됨
}
2) 특정 예외에 대해 롤백 설정
- rollbackFor 옵션을 사용하여 특정 예외가 발생해도 롤백되도록 설정할 수 있습니다.
@Transactional(rollbackFor = IOException.class)
public void processFile() throws IOException {
throw new IOException("파일 처리 오류"); // 롤백됨
}
- 반대로, 특정 예외에서 롤백되지 않도록 noRollbackFor 옵션을 사용할 수도 있습니다.
@Transactional(noRollbackFor = IllegalArgumentException.class)
public void updateUser(User user) {
throw new IllegalArgumentException("잘못된 입력"); // 롤백되지 않음
}
Spring Boot에서 트랜잭션을 적절히 관리하면 데이터 무결성을 보장하고 성능을 최적화할 수 있습니다.
✅ @Transactional을 적극 활용하되, 내부 호출(self-invocation) 문제를 주의
✅ 읽기 전용 트랜잭션(readOnly = true)을 적절히 활용하여 성능을 최적화
✅ Propagation 옵션을 이해하고, 비즈니스 로직에 맞게 선택
✅ 체크 예외와 런타임 예외를 rollbackFor, noRollbackFor를 통해 롤백 여부 제어
'SpringBoot > Spring 기초' 카테고리의 다른 글
[Spring] Spring Rest Docs 사용을 위한 설정 (0) | 2022.08.16 |
---|---|
[SpringBoot] JPA ddl-auto 옵션 정리 (0) | 2022.07.19 |