메서드 실행을 트랜잭션으로 감싸는 애너테이션. AOP 프록시로 동작한다는 사실이 모든 함정의 뿌리다.
호출이 실제로 어떻게 흐르나
컨테이너는 원본 빈이 아니라 프록시 객체를 주입한다.
다이어그램 로딩 중…
트랜잭션의 시작·종료는 프록시 경계에서만 일어난다. 이 경계를 통과하지 않으면 애너테이션은 아무 일도 하지 않는다.
함정 ① 자기 호출 — 가장 많이 나온다
@Service
class OrderService {
public void outer() {
this.inner(); // ❌ this 는 원본 객체 → 프록시를 안 거침
}
@Transactional
public void inner() { … }
}
this는 프록시가 아니라 원본 인스턴스다. 그래서 트랜잭션이 시작되지 않는다.
→ 다른 빈으로 분리하거나, 자기 자신을 주입받아 호출한다. 분리가 더 깔끔하다.
함정 ② checked 예외는 롤백되지 않는다
기본 롤백 대상은 RuntimeException과 Error뿐이다. IOException 같은 checked
예외가 나가면 커밋된다. 필요하면 rollbackFor = Exception.class.
함정 ③ 예외를 삼키면 롤백되지 않는다
try { risky(); } catch (Exception e) { log.warn("무시"); } // 커밋됨
프록시는 예외가 경계 밖으로 나가야 롤백을 판단한다.
함정 ④ private·final 메서드
프록시가 오버라이드할 수 없어 적용되지 않는다. public에 붙인다.
함정 ⑤ 트랜잭션 안에서 외부 호출
@Transactional
void order() {
save(); // DB 작업
httpClient.call(); // ❌ 응답이 늦으면 그동안 커넥션을 계속 쥔다
}
커넥션 풀이 고갈되는 전형적인 경로다. 외부 호출은 트랜잭션 밖으로 빼거나
커밋 후 이벤트(@TransactionalEventListener(AFTER_COMMIT))로 처리한다.
커넥션은 언제 잡히나
기본 설정에서는 실제 DB 접근이 필요한 시점에 커넥션을 가져오고 트랜잭션이 끝날 때 반납한다. 그래서 트랜잭션 길이가 곧 커넥션 점유 시간이다 — 짧게 유지하는 것이 성능의 핵심이다.