Spring
@Transactional은 어떻게 동작하나요? — 프록시 원리부터 self-invocation까지
Spring @Transactional이 내부적으로 어떻게 프록시를 생성하고 트랜잭션을 관리하는지, self-invocation 문제와 checked/unchecked 예외 롤백 차이를 인터랙티브 시각화로 이해합니다.
2026년 3월 21일 · 약 12분 읽기
Q. "Spring의 @Transactional 어노테이션이 내부적으로 어떻게 동작하는지 설명해주세요. 그리고 self-invocation 문제가 무엇인지, 왜 발생하는지 말씀해주세요."
예상 꼬리질문
답변 가이드
"Spring의 @Transactional은 AOP 기반의 프록시(Proxy) 패턴으로 구현됩니다. Spring IoC 컨테이너가 @Transactional이 붙은 빈을 등록할 때, 원본 객체를 그대로 쓰지 않고 이를 감싸는 프록시 객체를 대신 빈으로 등록합니다. 이 프록시가 메서드 호출을 가로채어 트랜잭션 시작, 커밋, 롤백을 자동 처리합니다."
"프록시 구조를 이해하면 self-invocation 문제가 왜 발생하는지 자연스럽게 설명할 수 있습니다. 외부에서 서비스 메서드를 호출하면 프록시를 통해 진입하지만, 같은 클래스 내부에서 this.메서드()로 호출하면 this는 원본 객체를 가리키므로 프록시를 전혀 거치지 않습니다. TransactionInterceptor가 개입하지 않아 트랜잭션이 시작되지 않습니다."
"실무에서 중요한 포인트는 세 가지입니다. 첫째, @Transactional은 public 메서드에만 동작합니다. CGLIB 프록시는 상속 방식으로 메서드를 오버라이드하므로 private 메서드는 가로챌 수 없습니다. 둘째, unchecked 예외(RuntimeException, Error)는 롤백되지만 checked 예외는 기본적으로 커밋됩니다. 셋째, try-catch로 예외를 삼키면 Spring이 예외를 인식하지 못해 롤백하지 않습니다."
면접에서 "@Transactional을 써봤나요?"라고 물으면 "네, 써봤습니다"로 끝나지 않습니다. 면접관이 정말 듣고 싶은 건 "그게 어떻게 동작하는지 설명해주세요"입니다.
프록시 구조를 모르면 self-invocation 버그를 만나도 원인을 찾지 못하고, 예외 처리 실수로 데이터가 롤백되지 않는 상황을 겪게 됩니다. 이 아티클에서 프록시 동작부터 흔한 함정까지, 인터랙티브 시각화로 직접 체험하며 이해합니다.
1. 프록시가 트랜잭션을 어떻게 감싸는가
꼬리질문: "CGLIB 프록시와 JDK Dynamic Proxy의 차이가 무엇인가요?"
@Transactional은 마법이 아닙니다. Spring IoC 컨테이너가 @Transactional이 붙은 빈을 생성할 때 원본 객체를 프록시로 감싸서 등록합니다. 클라이언트(컨트롤러 등)는 실제로 원본 객체가 아닌 프록시 객체와 대화합니다.
프록시는 TransactionInterceptor를 통해 메서드 호출 전후에 트랜잭션을 제어합니다. Spring Boot 2.x부터는 인터페이스 유무와 무관하게 CGLIB 프록시를 기본으로 사용합니다.
"다음 단계" 버튼으로 트랜잭션이 처리되는 각 단계를 순서대로 확인하세요.
클라이언트
Controller / 외부 코드
프록시(Proxy)
CGLIB 생성 서브클래스
TransactionInterceptor
AOP 어드바이스
Target 객체
원본 Service 클래스
PlatformTransactionManager
JpaTransactionManager 등
DataSource / Connection
ThreadLocal 바인딩
@Transactional이 붙은 클래스를 final로 선언하면 CGLIB이 상속으로 프록시를 만들 수 없어 컨텍스트 로딩 시 오류가 납니다. Kotlin에서는 클래스가 기본적으로 final이므로 open 키워드나 kotlin-spring 플러그인이 필요합니다.
2. self-invocation — 가장 자주 빠지는 함정
꼬리질문: "같은 클래스 안에서 @Transactional 메서드를 호출하면 왜 트랜잭션이 적용 안 되나요?"
프록시 구조를 이해하면 이 함정을 바로 예측할 수 있습니다. 외부에서 서비스를 호출하면 반드시 프록시를 통해 진입합니다. 그런데 같은 클래스 내에서 this.saveOrder()처럼 자기 자신의 메서드를 호출하면, this는 프록시가 아닌 원본 객체를 가리킵니다.
원본 객체에는 트랜잭션 로직이 없습니다. TransactionInterceptor가 개입하지 않으므로 @Transactional이 붙어 있어도 트랜잭션이 시작되지 않습니다.
탭을 전환하여 외부 호출과 self-invocation의 흐름 차이를 확인하세요.
호출 흐름
Controller
클라이언트 코드
프록시(Proxy)
CGLIB 프록시 — 트랜잭션 시작 ✅
OrderService.placeOrder()
원본 객체 메서드
OrderService.saveOrder()
@Transactional — 트랜잭션 적용 ✅
코드
// Controller에서 호출 — 프록시를 통해 진입
@RestController
public class OrderController {
@Autowired
private OrderService orderService; // 실제론 프록시 객체
@PostMapping("/orders")
public void create() {
orderService.placeOrder(); // ✅ 프록시 경유
}
}
@Service
public class OrderService {
public void placeOrder() { ... }
@Transactional
public void saveOrder() { ... } // ✅ 트랜잭션 적용
}해결 방법 중 가장 권장하는 방식은 메서드를 별도 빈으로 분리하는 것입니다. self-injection은 순환 참조가 생길 수 있어 Spring Boot 2.6 이후 기본 설정에서 오류가 발생할 수 있습니다.
3. public 메서드 제약 — 왜 private에는 안 되는가
꼬리질문: "@Transactional이 private 메서드에 붙으면 어떻게 되나요?"
@Transactional을 private 메서드에 붙여도 아무 효과가 없습니다. IDE가 경고도 안 해주기 때문에 모르고 지나치기 쉬운 함정입니다.
CGLIB 프록시는 상속(오버라이드) 방식으로 메서드를 가로챕니다. private 메서드는 상속이 불가능하고, protected/package-private도 Spring AOP 기본 동작에서는 가로채지 않습니다.
각 접근 제한자를 클릭하여 @Transactional 적용 여부를 확인하세요.
CGLIB 프록시가 서브클래스를 생성할 때 public 메서드는 오버라이드 가능합니다. @Transactional이 정상 동작합니다.
@Transactional
public void createOrder() {
// ✅ 트랜잭션 정상 적용
}실무 원칙: @Transactional은 반드시 public 메서드에만 사용하세요. Spring 6.x 이상에서도 안전한 방법은 public 메서드입니다.
4. checked vs unchecked 예외 롤백 차이
꼬리질문: "checked 예외가 발생했을 때 왜 기본적으로 롤백이 안 되나요?"
@Transactional의 기본 롤백 정책은 Java의 예외 설계 철학을 그대로 따릅니다. RuntimeException(unchecked)은 프로그래밍 오류로 보아 롤백하고, Exception(checked)은 예상 가능한 비즈니스 상황으로 보아 기본적으로 커밋합니다.
실무에서 자주 하는 실수는 try-catch로 예외를 삼켜버리는 것입니다. Spring은 메서드 밖으로 예외가 던져져야만 롤백 여부를 판단합니다. catch 블록에서 예외를 잡아 정상 반환하면 Spring은 "성공"으로 인식해 커밋합니다.
시나리오를 선택하여 각 경우의 커밋/롤백 결과를 확인하세요.
시나리오 선택
코드
@Transactional
public void process() {
save(); // DB 저장
// RuntimeException (unchecked)
throw new IllegalArgumentException("잘못된 입력");
// → 자동 ROLLBACK ↩
// save() 결과도 취소됨
}unchecked 예외 — Spring 기본 롤백 정책
롤백 정책 요약
예외를 삼켜야 하는 상황이라면 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()를 호출해 수동으로 롤백 마킹을 할 수 있습니다.
자주 발생하는 문제
마무리
이 항목들을 자신 있게 설명할 수 있다면 @Transactional 질문은 준비 완료입니다.
- - 프록시 기반: CGLIB/JDK Dynamic Proxy로 원본 빈 대신 프록시 등록, TransactionInterceptor가 트랜잭션 제어
- - self-invocation: this가 프록시를 우회하여 트랜잭션 미적용 — 별도 빈 분리로 해결
- - public 메서드만: CGLIB 상속 오버라이드 방식의 한계, private은 완전 무시
- - 롤백 정책: unchecked 예외 → 롤백, checked 예외 → 기본 커밋 (rollbackFor로 변경 가능)
- - try-catch 주의: 예외를 삼키면 트랜잭션이 커밋됨
더 알아볼 주제
- -
@Transactional(propagation = ...)전파 속성 7가지 - -
readOnly = true의 성능 최적화 원리 (더티 체킹 생략) - - AspectJ 위빙 방식 — self-invocation 없이 private 메서드도 적용 가능
- - 분산 트랜잭션 (2PC, Saga 패턴)
퀴즈로 확인하기
개념을 제대로 이해했는지 확인해 보세요.
프록시를 통하는 호출인가?
@Service
public class PaymentService {
@Transactional
public void pay() {
charge(); // this.charge()와 동일
}
@Transactional
public void charge() {
// DB 결제 처리
}
}checked 예외가 발생하면?
// InsufficientBalanceException extends Exception (checked)
@Transactional
public void transfer(Long from, Long to, int amount)
throws InsufficientBalanceException {
debit(from, amount); // DB 출금
credit(to, amount); // DB 입금
if (amount > balance) {
throw new InsufficientBalanceException("잔액 부족");
}
}
// debit() 결과는?private 메서드의 @Transactional
@Service
public class OrderService {
public void createOrder(OrderRequest req) {
saveOrder(req); // 내부 호출
}
@Transactional
private void saveOrder(OrderRequest req) {
// DB 저장 로직
orderRepository.save(req.toEntity());
}
}
// saveOrder()에 @Transactional이 적용되는가?예외를 잡으면 롤백되는가?
@Transactional
public void processOrder(Long orderId) {
try {
updateInventory(orderId); // DB 업데이트
sendNotification(orderId); // RuntimeException 발생 가능
} catch (RuntimeException e) {
log.error("알림 실패", e);
// 예외를 다시 던지지 않음
}
}
// updateInventory() 결과는?CGLIB 프록시 생성이 실패하는 경우
@Service
@Transactional
public final class UserService {
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow();
}
}
// 이 코드는 어떻게 되는가?참고 자료
- 망나니 개발자 — Spring @Transactional의 이해 및 사용법 — @Transactional의 동작 원리와 옵션을 단계별로 친절하게 설명하는 한국어 블로그
- 우아한 형제들 기술 블로그 — Spring @Transactional의 이해 — 실무에서 자주 만나는 @Transactional 문제들을 사례 중심으로 설명
- Baeldung — @Transactional Self-Invocation — self-invocation 문제와 해결 방법을 코드 예시와 함께 상세히 설명 (영어)
의견을 들려주세요
서비스 개선에 큰 도움이 됩니다. 익명으로 자유롭게 남겨주세요.