
안녕하세요, 이력서 기반 면접 준비를 도와드리는 QueryDaily 팀입니다.
.
.
.
이 질문에 답변하실 수 있나요?
"트랜잭션 처리는 어떻게 하셨나요?"
"@Transactional 붙였습니다!"
여기까지는 자신 있게 대답하시죠. 그런데 면접관이 고개를 끄덕이며 이렇게 묻는다면요?
"그러면 @Transactional을 붙이면 내부적으로 어떤 일이 일어나는지 설명해주실 수 있나요?"
혹시 머릿속이 하얘지는 경험, 없으신가요? 괜찮습니다. 많은 개발자들이 @Transactional의 편리함에 익숙해져서, 정작 그 내부 동작은 블랙박스로 남겨두곤 합니다. 오늘은 Spring의 @Transactional이 마법처럼 동작하는 비밀을 파헤쳐 보겠습니다.
Spring의 @Transactional은 AOP(Aspect-Oriented Programming) 를 기반으로 동작합니다. 그리고 AOP의 핵심 구현 방식이 바로 프록시 패턴 입니다.
쉽게 비유하자면, 프록시는 '대리인' 또는 '매니저'와 같습니다. 여러분이 유명 연예인이라면, 모든 일정과 계약을 직접 처리하지 않고 매니저를 통해 처리하죠. 마찬가지로 Spring은 @Transactional이 붙은 클래스의 가짜 객체(프록시) 를 만들어서, 실제 메서드 호출 전후에 트랜잭션 관련 로직을 끼워 넣습니다.
// 우리가 작성한 서비스 클래스
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 주문 생성 로직
orderRepository.save(order);
}
}
위 코드가 실행될 때 실제로 일어나는 일을 그림으로 표현하면 이렇습니다:
[Controller]
↓ orderService.createOrder() 호출
[Proxy] ← Spring이 만든 가짜 객체
├─ 1. 트랜잭션 시작 (BEGIN)
├─ 2. 실제 OrderService.createOrder() 호출
├─ 3-1. 정상 완료 시 → 커밋 (COMMIT)
└─ 3-2. 예외 발생 시 → 롤백 (ROLLBACK)
[실제 OrderService]
핵심 포인트: @Transactional은 마법이 아닙니다. Spring이 프록시를 통해 메서드 호출을 가로채고, 트랜잭션의 시작/커밋/롤백을 대신 처리해주는 것입니다.
프록시의 동작 원리를 이해하면, @Transactional의 가장 유명한 함정도 이해할 수 있습니다. 바로 같은 클래스 내에서 메서드를 호출하면 트랜잭션이 적용되지 않는 문제입니다.
@Service
public class OrderService {
// 트랜잭션 없음
public void processOrder(Order order) {
// 같은 클래스의 메서드를 직접 호출
this.createOrder(order); // ❌ 트랜잭션이 적용되지 않음!
}
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
}
}
왜 이런 일이 발생할까요?
processOrder()는 this.createOrder()를 호출합니다. 여기서 this는 프록시가 아닌 실제 객체입니다. 프록시를 거치지 않기 때문에 트랜잭션 로직이 끼어들 틈이 없는 것이죠.
[Controller]
↓ orderService.processOrder() 호출
[Proxy]
└─ 실제 OrderService.processOrder() 호출
└─ this.createOrder() ← 프록시를 거치지 않음! ❌
@Autowired로 자기 자신을 주입받아 프록시를 통해 호출합니다. (권장하지 않음)// 권장: 클래스 분리
@Service
@RequiredArgsConstructor
public class OrderFacade {
private final OrderService orderService;
public void processOrder(Order order) {
orderService.createOrder(order); // ✅ 프록시를 통해 호출
}
}
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
}
}
@Transactional의 또 다른 함정은 롤백 조건입니다. 기본적으로 Spring은 Unchecked Exception(RuntimeException과 그 하위 클래스) 에서만 롤백합니다.
@Transactional
public void createOrder(Order order) throws IOException {
orderRepository.save(order);
// Checked Exception 발생 → 롤백되지 않음! ❌
throw new IOException("파일 처리 실패");
}
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
// Unchecked Exception 발생 → 롤백됨 ✅
throw new RuntimeException("처리 실패");
}
rollbackFor 속성을 사용합니다.
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) throws IOException {
orderRepository.save(order);
throw new IOException("파일 처리 실패"); // 이제 롤백됨 ✅
}
자, 이제 면접관의 날카로운 질문들에 대비해 봅시다.
Q1. "@Transactional은 내부적으로 어떻게 동작하나요?"
A: "Spring AOP의 프록시 패턴을 기반으로 동작합니다. @Transactional이 붙은 클래스의 프록시 객체를 생성하여, 메서드 호출 전에 트랜잭션을 시작하고, 정상 완료 시 커밋, 예외 발생 시 롤백하는 로직을 메서드 전후에 끼워 넣습니다."
Q2. "@Transactional이 적용되지 않는 경우는 언제인가요?"
A: "대표적으로 세 가지 경우가 있습니다. 첫째, 같은 클래스 내에서 내부 메서드를 호출할 때(Self-Invocation). 둘째, private 메서드에 적용했을 때. 셋째, final 클래스나 메서드에 적용했을 때입니다. 모두 프록시가 제대로 동작할 수 없는 상황입니다."
Q3. "Checked Exception이 발생하면 롤백되나요?"
A: "기본적으로 롤백되지 않습니다. Spring의 기본 롤백 정책은 RuntimeException과 Error에 대해서만 롤백합니다. Checked Exception에서도 롤백하려면@Transactional(rollbackFor = Exception.class)처럼 명시적으로 지정해야 합니다."
Q4. "프록시 방식의 한계는 무엇인가요?"
A: "내부 호출 시 트랜잭션이 적용되지 않는 것이 가장 큰 한계입니다. 이를 해결하기 위해 클래스를 분리하거나, AspectJ의 컴파일 타임 위빙/로드 타임 위빙 방식을 사용할 수 있습니다."
rollbackFor로 명시해야 합니다.오늘 다룬 내용처럼, 단순히 "사용해봤다"를 넘어 "왜 그렇게 동작하는지"까지 설명할 수 있어야 면접에서 차별화됩니다. 혼자 준비하다 보면 어떤 부분이 약점인지 파악하기 어렵지만, QueryDaily를 활용해서 꼬리 질문에 대비해 보세요.
#Spring #Transactional #스프링 #트랜잭션 #백엔드면접 #AOP #프록시