Spring @Transactional 동작 원리
트랜잭션 경계에 관한 이야기
여는 글
Spring Framework를 다루시는 분이라면, @Transactional 어노테이션을 많이 사용하실겁니다. @Transactional 어노테이션을 붙이면, 해당 메소드의 트랜잭션을 보장해주는 것으로 알고 있는데요. 저는 이 어노테이션의 동작원리를 설명할 줄 모르고 머릿속에 떠다니는 상상으로 사용했던 것 같습니다.
면접에서 “@Transactional이 어떻게 동작하나요?“라는 질문을 받았을 때, 제대로 대답하지 못했던 창피한 기억이 있습니다. 이번 포스팅을 통해 동작 원리를 확실히 정리하고, 다시는 그런 실수를 반복하지 않으려 합니다.
핵심 답변: 프록시(Proxy) 패턴
@Transactional의 동작 원리를 한 문장으로 요약하면:
Spring은 프록시 객체를 생성하여, 실제 메소드 호출 전후에 트랜잭션 시작/커밋/롤백 로직을 끼워넣는다.
이게 핵심입니다. 이제 이 한 문장을 깊이 파헤쳐 보겠습니다.
1. 프록시가 뭔데?
프록시(Proxy)는 대리인이라는 뜻입니다. 실제 객체를 감싸는 래퍼(Wrapper) 객체라고 생각하면 됩니다.
[클라이언트] → [프록시 객체] → [실제 객체]
Spring이 @Transactional이 붙은 클래스를 만나면
- 실제 객체를 생성합니다
- 그 객체를 감싸는 프록시 객체를 생성합니다
- 스프링 컨테이너에는 프록시 객체가 빈(Bean)으로 등록됩니다
따라서 우리가 @Autowired로 주입받는 것은 실제 객체가 아니라 프록시 객체입니다.
2. 프록시가 하는 일
프록시 객체는 메소드 호출을 가로채서 다음과 같이 동작합니다:
// 우리가 작성한 코드
@Service
public class UserService {
@Transactional
public void createUser(User user) {
userRepository.save(user);
}
}
// Spring이 생성한 프록시의 동작 (의사 코드)
public class UserService$$Proxy {
private UserService target; // 실제 객체
public void createUser(User user) {
// 1. 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction();
try {
// 2. 실제 메소드 호출
target.createUser(user);
// 3. 커밋
transactionManager.commit(status);
} catch (RuntimeException e) {
// 4. 롤백
transactionManager.rollback(status);
throw e;
}
}
}
이렇게 AOP(Aspect-Oriented Programming) 방식으로 트랜잭션 로직이 끼워지는 것입니다.
3. 프록시 생성 방식: JDK Dynamic Proxy vs CGLIB
Spring은 두 가지 방식으로 프록시를 생성합니다:
| 방식 | 조건 | 특징 |
|---|---|---|
| JDK Dynamic Proxy | 인터페이스가 있을 때 | 인터페이스를 구현한 프록시 생성 |
| CGLIB | 인터페이스가 없을 때 | 클래스를 상속한 프록시 생성 |
Spring Boot 2.0부터는 기본적으로 CGLIB를 사용합니다.
// JDK Dynamic Proxy - 인터페이스 필요
public interface UserService { ... }
@Service
public class UserServiceImpl implements UserService { ... }
// CGLIB - 인터페이스 없이 클래스만
@Service
public class UserService { ... } // 이 클래스를 상속한 프록시 생성
4. 면접 단골 질문: Self-Invocation 문제
“같은 클래스 내에서 @Transactional 메소드를 호출하면 트랜잭션이 적용될까요?”
답: 아니요!
@Service
public class UserService {
public void outer() {
// 같은 클래스 내부에서 호출
this.inner(); // ❌ 트랜잭션 적용 안 됨!
}
@Transactional
public void inner() {
// DB 작업
}
}
왜 안 될까?
this.inner()는 프록시를 거치지 않고 실제 객체를 직접 호출하기 때문입니다.
외부에서 호출: [클라이언트] → [프록시] → [실제 객체] ✅ 트랜잭션 적용
내부에서 호출: [실제 객체] → [실제 객체] ❌ 프록시를 안 거침
해결 방법
@Service
public class UserService {
@Autowired
private UserService self; // 자기 자신을 주입 (프록시 객체가 주입됨)
public void outer() {
self.inner(); // ✅ 프록시를 통해 호출
}
@Transactional
public void inner() {
// DB 작업
}
}
또는 별도의 클래스로 분리하는 것이 더 깔끔합니다.
5. private 메소드에 @Transactional을 붙이면?
동작하지 않습니다.
CGLIB 프록시는 클래스를 상속해서 만들어지는데, private 메소드는 상속이 불가능하기 때문입니다.
@Transactional
private void doSomething() { // ❌ 컴파일은 되지만, 트랜잭션 적용 안 됨
// ...
}
6. 트랜잭션 전파(Propagation)
@Transactional에서 가장 많이 물어보는 속성입니다.
@Transactional(propagation = Propagation.REQUIRED) // 기본값
| 전파 옵션 | 설명 |
|---|---|
| REQUIRED (기본값) | 트랜잭션이 있으면 참여, 없으면 새로 생성 |
| REQUIRES_NEW | 항상 새 트랜잭션 생성 (기존 트랜잭션 일시 정지) |
| NESTED | 중첩 트랜잭션 생성 (savepoint 활용) |
| SUPPORTS | 트랜잭션이 있으면 참여, 없으면 없이 실행 |
| NOT_SUPPORTED | 트랜잭션 없이 실행 (기존 트랜잭션 일시 정지) |
| MANDATORY | 트랜잭션 필수 (없으면 예외 발생) |
| NEVER | 트랜잭션이 있으면 예외 발생 |
자주 쓰는 예시
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
// 로그 저장은 별도 트랜잭션 (메인 트랜잭션 실패해도 로그는 남김)
logService.saveLog(order);
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(Order order) {
logRepository.save(new Log(order));
}
}
7. 롤백 규칙
기본적으로 Unchecked Exception (RuntimeException)에서만 롤백됩니다.
@Transactional
public void method() {
throw new RuntimeException(); // ✅ 롤백됨
}
@Transactional
public void method() throws Exception {
throw new Exception(); // ❌ 롤백 안 됨 (Checked Exception)
}
Checked Exception도 롤백하려면?
@Transactional(rollbackFor = Exception.class)
public void method() throws Exception {
throw new Exception(); // ✅ 이제 롤백됨
}
정리: 면접에서 이렇게 답하자
“@Transactional은 Spring AOP를 기반으로 동작합니다. Spring은 해당 클래스의 프록시 객체를 생성하고, 메소드 호출 시 프록시가 이를 가로채서 트랜잭션 시작, 커밋, 롤백 로직을 수행합니다. 프록시는 CGLIB을 사용해 클래스를 상속하여 생성되며, 이 때문에 private 메소드나 같은 클래스 내부 호출(self-invocation)에서는 트랜잭션이 적용되지 않는 한계가 있습니다.”
이 질문을 받았을 때, 위 정도로만 답했다면 좋았을텐데. 무지함이 아쉽네요.
마치며
이번 포스팅을 정리하면서 “아, 이걸 면접 때 이렇게 말했어야 했는데…“라는 후회가 밀려왔습니다. 하지만 이제는 확실히 이해했으니, 다음에는 자신있게 설명할 수 있을 것 같습니다.
면접 준비하시는 분들, 화이팅입니다!