🔍 검색 키워드: Spring Boot @Transactional 작동 안 함, @Transactional self-invocation 에러, Spring AOP 프록시 문제, @Transactional 같은 클래스 호출, Spring 트랜잭션 무시
Spring Boot로 개발하다가 @Transactional을 붙였는데도 롤백이 안 되거나 트랜잭션 자체가 적용이 안 되는 상황을 겪는다. 코드를 아무리 봐도 문제가 없어 보이는데 예외가 발생해도 DB에 그대로 저장된다.
이 문제의 80%는 self-invocation(자기 자신을 직접 호출)이 원인이다.
증상
@Transactional을 붙였는데 예외 발생 후에도 DB가 롤백되지 않음- 같은 클래스 안에서
@Transactional메서드를 호출할 때만 이 현상 발생 - 다른 빈(Bean)에서 호출하면 정상적으로 동작함
- 로그에 트랜잭션 관련 에러는 없고 조용히 문제가 생김
원인: Spring AOP 프록시 메커니즘
Spring의 @Transactional은 AOP 프록시로 동작한다. 외부에서 빈을 호출하면 프록시가 먼저 받아서 트랜잭션을 시작하고 실제 메서드를 호출한 뒤 트랜잭션을 종료한다.
외부 호출 → [Spring Proxy] → 트랜잭션 시작 → [실제 객체.method()] → 트랜잭션 종료
this.method() → [실제 객체.method()] (프록시 미통과, 트랜잭션 없음)
@Service
public class OrderService {
public void createOrder(OrderDto dto) {
// ... 주문 생성 로직
this.sendNotification(dto); // ❌ self-invocation — 트랜잭션 적용 안 됨
}
@Transactional
public void sendNotification(OrderDto dto) {
// 이 메서드의 @Transactional은 위에서 직접 호출하면 무시됨
notificationRepository.save(new Notification(dto));
}
}
해결방법
방법 1: 별도 빈으로 분리 (가장 권장)
@Service
public class OrderService {
private final NotificationService notificationService;
public OrderService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void createOrder(OrderDto dto) {
// ... 주문 생성 로직
notificationService.sendNotification(dto); // ✅ 외부 빈 호출 → 프록시 통과
}
}
@Service
public class NotificationService {
@Transactional
public void sendNotification(OrderDto dto) {
notificationRepository.save(new Notification(dto));
}
}
방법 2: Self-Injection
@Service
public class OrderService {
@Autowired
@Lazy // 순환 참조 방지
private OrderService self;
public void createOrder(OrderDto dto) {
self.sendNotification(dto); // ✅ 프록시를 통해 호출
}
@Transactional
public void sendNotification(OrderDto dto) {
notificationRepository.save(new Notification(dto));
}
}
방법 3: AopContext.currentProxy() 사용
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // 필수 설정
public class Application { ... }
@Service
public class OrderService {
public void createOrder(OrderDto dto) {
((OrderService) AopContext.currentProxy()).sendNotification(dto);
}
@Transactional
public void sendNotification(OrderDto dto) {
notificationRepository.save(new Notification(dto));
}
}
@Transactional의 다른 흔한 함정들
함정 1: private 메서드에 @Transactional
// ❌ private 메서드는 프록시 오버라이딩 불가 — @Transactional 무시됨
@Transactional
private void updateStock(Long productId, int quantity) { ... }
// ✅ public으로 변경
@Transactional
public void updateStock(Long productId, int quantity) { ... }
함정 2: Checked Exception은 기본 롤백 안 됨
// ❌ IOException(Checked Exception)은 기본 롤백 대상이 아님
@Transactional
public void processFile(String path) throws IOException {
fileRepository.save(new FileRecord(path));
throw new IOException("파일 처리 실패"); // 롤백 안 됨!
}
// ✅ rollbackFor로 명시
@Transactional(rollbackFor = Exception.class)
public void processFile(String path) throws IOException {
fileRepository.save(new FileRecord(path));
throw new IOException("파일 처리 실패"); // 롤백 됨
}
함정 3: try-catch로 예외를 삼켜버리기
// ❌ 예외를 catch하고 아무것도 안 하면 롤백 안 됨
@Transactional
public void saveData(Data data) {
try {
repository.save(data);
externalApiCall();
} catch (Exception e) {
log.error("에러 발생", e); // 트랜잭션 매니저가 롤백 대상인지 모름
}
}
// ✅ 롤백이 필요하면 명시적 처리
@Transactional
public void saveData(Data data) {
try {
repository.save(data);
externalApiCall();
} catch (Exception e) {
log.error("에러 발생", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
점검 체크리스트
| 항목 | 확인 |
|---|---|
| self-invocation 여부 | 같은 클래스 내 직접 호출인지 확인 |
| 메서드 접근자 | public 메서드인지 확인 |
| 예외 타입 | Checked Exception이면 rollbackFor 설정 |
| try-catch 위치 | 예외를 너무 일찍 삼키지 않는지 |
| 빈 분리 | 트랜잭션 경계를 별도 서비스로 분리했는지 |
정리
@Transactional이 작동 안 한다면 첫 번째로 확인할 것은 호출 방식이다. 같은 클래스 안에서 this.method()로 호출하면 Spring 프록시를 거치지 않아 트랜잭션이 적용되지 않는다.
가장 깔끔한 해결은 트랜잭션이 필요한 로직을 별도 서비스 빈으로 분리하는 것이다. Self-injection이나 AopContext.currentProxy() 방식은 동작은 하지만 코드가 복잡해지므로 리팩토링이 가능하다면 분리를 우선한다.
댓글 없음:
댓글 쓰기