
"트랜잭션이 일어나지 않는 경우"에 대해서 설명해달라는 질문을 받았다.
트랜잭션 메서드 안에 트랜잭션 메서드가 있는 경우에는 작동하지 않는다는 건 알고 있었는데, 알고 있는 것조차 대답을 못할정도로 충분히 숙지되어있지 않다는 걸 느껴 다시금 정리해보려고 한다.
1. 자기 자신을 호출하는 경우 (self-invocation)
@Service
public class MyService {
@Transactional
public void methodA() {
this.methodB(); // 프록시를 거치지 않아 @Transactional 무시됨
}
@Transactional
public void methodB() {
userRepository.save(new User("Key"));
throw new RuntimeException("에러 발생");
}
}
동일 클래스 내부에서 `this.methodB()`처럼 호출하면 프록시를 거치지 않기 때문에 `methodB()`에 붙은 `@Transactional`은 동작하지 않는다. 하지만 `methodA()` 자체에 트랜잭션이 적용되어 있다면, `methodB()`도 같은 트랜잭션 내에서 실행되므로 롤백은 함께 일어난다.
정리하면, `this.methodB()`처럼 자기 자신을 호출하면 Spring의 AOP 트랜잭션 프록시가 적용되지 않기 때문에, methodB()에 붙은 @Transactional의 속성은 반영되지 않는다. 하지만 이 호출이 이미 트랜잭션 안에서 수행되면, 그 트랜잭션의 범위 안에 포함되어 롤백은 함께 된다.
2. private 메서드에 @Transactional을 붙인 경우
@Transactional
private void someMethod() {
// 트랜잭션 적용 안 됨
}
Spring AOP는 기본적으로 public 메서드만 프록시를 통해 트랜잭션 적용이 가능하다.
`@Transactional`은 Spring AOP 기반으로 동작하는데, Spring AOP는 프록시 객체를 통해 메서드를 가로채서 트랜잭션 시작/커밋/롤백을 처리한다. 그런데 `private` 메서드는 자바 언어 레벨에서 외부 클래스는 물론, 프록시 객체에서도 접근이 불가능하므로, `private` 메서드에 `@Transactional`을 붙여도 프록시가 호출할 수 없어서 트랜잭션이 적용되지 않는다.
3. 예외가 잡히는 경우 (예외 전파 실패)
@Transactional
public void process() {
try {
// 예외 발생
throw new RuntimeException("error");
} catch (Exception e) {
// rollback 되지 않음
}
}
`@Transactional`은 기본적으로 런타임 예외가 발생해야 rollback 되기 때문에, 예외가 `try-catch`로 잡히면 rollback 되지 않는다.
`try-catch`로 예외를 잡아버리면, Spring은 “예외가 발생하지 않았다”고 판단해 트랜잭션을 정상 종료시키기 때문에, catch 블록 안에서 rollback 처리를 명시적으로 하지 않으면 커밋된다.
그러면 "`@Transactional`을 사용할 땐 `try-catch`를 쓰면 안되는건가?"라는 생각이 들 수도 있다.
@Transactional
public void registerUser() {
try {
// 실패해도 괜찮은 외부 API 호출
externalNotificationService.sendWelcomeMessage();
} catch (Exception e) {
// 실패해도 DB는 롤백되지 않아야 하므로 try-catch로 감쌈
log.warn("알림 전송 실패", e);
}
// 아래는 반드시 성공해야 하므로 예외 발생 시 트랜잭션 롤백
userRepository.save(new User("haeun")); // 예외 나면 롤백됨
}
이렇게 예외 여부가 트랜잭션에 영향을 주지 않는 로직의 경우는 `try-catch`를 사용하고 그 외에는 사용하지 않으면 된다.
@Transactional
public void process() {
try {
saveUser();
} catch (Exception e) {
log.error("에러 발생", e);
// 수동 롤백 지시
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
`try-catch` 부분에도 롤백이 필요하다면 위와 같이 `setRollbackOnly()`를 명시하면 된다.
`@Transactional에서` `try-catch`는 필요한 부분에서만 사용하도록 하자.
4. 프록시 기반이 아닌 경우 (예: 내부 호출, 직접 인스턴스 생성 등)
new 키워드로 직접 객체를 생성하거나 AOP 프록시를 거치지 않으면 트랜잭션 적용 안 된다.
위에서 한 번 언급했다시피, `@Transactional`은 Spring AOP 기반으로 동작하는데, Spring AOP는 프록시 객체를 통해 메서드를 가로채서 트랜잭션 시작/커밋/롤백을 처리하기 때문에 AOP 프록시를 반드시 거쳐야한다.
프록시를 거치는 방법
@Service
public class InnerService {
@Transactional
public void innerMethod() {
System.out.println("inner 시작");
}
}
1. 다른 빈에서 호출
@Component
public class Caller {
private final InnerService innerService;
public Caller(InnerService innerService) {
this.innerService = innerService;
}
public void doSomething() {
innerService.innerMethod(); // 프록시 타고 호출
}
}
2. 강제로 프록시에서 다시 호출
import org.springframework.aop.framework.AopContext;
@Transactional
public void outerMethod() {
((MemberService) AopContext.currentProxy()).innerMethod(); // 프록시를 직접 호출
}
AopContext.currentProxy()를 쓸 때 반드시 exposeProxy = true가 켜져 있어야 한다. 이 옵션 없이는 IllegalStateException: Cannot find current proxy 오류 발생
5. 메서드가 final인 경우
CGLIB 프록시는 final 메서드를 오버라이드할 수 없기 때문에 AOP 적용이 되지 않는다.
CGLIB 프록시란?
클래스를 상속받아서 새로운 클래스를 런타임에 동적으로 생성해 AOP 기능을 수행하는 프록시 방식
// 클래스
public class MyService {
public void doSomething() { ... }
}
// CGLIB가 감산 클래스
public class MyService$$EnhancerBySpringCGLIB extends MyService {
@Override
public void doSomething() {
try {
// AOP before advice: 트랜잭션 시작
TransactionManager.begin();
// 원래 로직 실행
super.doSomething();
// AOP after advice: 트랜잭션 커밋
TransactionManager.commit();
} catch (Exception e) {
// AOP after-throwing advice: 트랜잭션 롤백
TransactionManager.rollback();
throw e;
}
}
}
`final`이면 CGLIB가 `@Override` 할 수 없다.
6. Checked Exception일 경우
@Transactional
public void save() throws IOException {
throw new IOException("실패"); // 롤백 안 됨
}
`@Transactional`은 RuntimeException은 자동으로 롤백하지만, Checked Exception은 롤백하지 않는다.
@Transactional(rollbackFor = IOException.class)
public void save() throws IOException {
throw new IOException("파일 오류 발생"); // 이젠 롤백됨
}
Checked 예외로 롤백하고 싶다면, 이렇게 예외를 명시해주면 된다.
'IT' 카테고리의 다른 글
| [SpringBoot/JSP] 다국어 처리 (1) | 2025.08.08 |
|---|---|
| [Spring Batch] Cursor vs Paging (2) | 2025.07.25 |
| [Cloudflare Tunnel] 우리집 IP 노출 없이 노트북을 연결해보자 (1) | 2025.07.16 |
| [Oracle Cloud] 인스턴스 생성 자동화 (Out of host capacity) (1) | 2025.07.03 |
| [Oracle Cloud] 오라클 클라우드 프리티어(Free Tier) 가입 (0) | 2025.07.02 |