
@Service
public class OrderService {
public void placeOrder() {
System.out.println("주문 시작");
saveOrder(); // 내부 호출
System.out.println("주문 완료");
}
@Transactional
private void saveOrder() {
System.out.println("DB에 주문 저장");
}
}
@Transactional은 private 메서드에서 동작할까? 정답은 “동작하지 않는다.”
그렇다면 왜 @Transactional은 private 메서드에서 작동하지 않을까?
이유를 알기 위해서는 먼저 @Transactional이 어떻게 동작하는지를 이해해야 한다.
@Transactional 이란?
@Transactional은 트랜잭션의 경계를 선언적으로 관리하기 위한 어노테이션이다.
과거에는 트랜잭션을 적용하기 위해 다음과 같은 코드를 모든 비즈니스 로직에 직접 작성해야 했다.
Connection conn = dataSource.getConnection();
try {
conn.setAutoCommit(false);
// 비즈니스 로직
conn.commit();
} catch (Exception e) {
conn.rollback();
}
하지만 @Transactional을 사용하면 Spring이 트랜잭션의 시작, 커밋, 예외 발생 시 롤백까지 자동으로 처리해준다.
이러한 자동 트랜잭션 처리의 핵심은 바로 AOP에서 나온다.
@Transactional의 동작 원리
Spring은 @Transactional이 붙은 메서드를 직접 실행하지 않는다.
대신, 프록시(proxy) 객체를 생성해 원본 객체를 감싸고, 그 프록시가 트랜잭션의 시작,커밋,롤백을 수행한다.
즉, 우리가 orderService.saveOrder()를 호출할 때 실제로 호출되는 것은 OrderService의 프록시 객체이며, 프록시 내부에서 실제 비즈니스 메서드가 실행되는 구조다.
Spring AOP와 프록시
Spring AOP는 프록시 기반 AOP 방식으로 동작한다. 실제 객체를 감싸는 프록시를 만들어 부가기능을 주입하는 구조다.
Spring은 이러한 프록시 클래스를 런타임에 자동 생성하고,프록시 객체를 빈(Bean) 으로 등록한다.
결과적으로 우리는 직접 프록시 클래스를 만들 필요가 없다.
Spring은 빈 생성시, 해당 빈에 AOP 애너테이션이 있는지 검사하고, 있다면 프록시 객체를 생성하여 빈을 대체한다.
AOP 적용 대상인 클래스의 경우, 즉, @Transactional과 같은 AOP 애너테이션이 하나라도 선언된 클래스는 프록시로 감싸진다.
왜 원본 객체 대신 프록시 객체를 Bean으로 등록할까?
1. 호출을 가로채야 하기 때문
AOP의 핵심은 “메서드 호출을 가로채서 부가기능을 실행하는 것” 이다.
그런데 스프링의 컨테이너는 DI(의존성 주입)을 통해 객체를 주입하기 때문에, 다른 클래스가 이 Bean을 호출할 때는 항상 프록시를 거치게 된다.
그 결과 “프록시 객체 → AOP 처리 → 원본 객체”의 플로우가 가능해진다.
따라서 해당 Bean 자체가 프록시여야만 외부에서 어떤 클래스가 해당 Bean의 메서드를 호출할 때마다 프록시객체 → AOP → 원본객체 흐름이 가능해진다.
2. 원본 코드를 수정하지 않기 위해
Spring은 개발자가 작성한 비즈니스 로직 코드를 직접 변경하지 않는다.
코드 내부에 트랜잭션 로직을 삽입하는 것은 유지보수성이 매우 떨어진다.
프록시를 사용하면 원본 객체는 그대로 유지 하면서, 그 위에 가짜 객체(Proxy)를 덧씌워 부가기능을 삽입할 수 있다.
private 메서드에서 동작하지 않는 이유
Spring의 트랜잭션은 프록시(Proxy) 기반 AOP로 동작한다.
이는 외부에서 호출되는 public 메서드만 가로채서 트랜잭션 로직을 추가할 수 있다.
private 메서드는 접근 제어상 프록시가 감쌀 수도, 오버라이드할 수도 없기 때문에 @Transactional이 붙어 있어도 트랜잭션이 적용되지 않는다.
그렇다면 saveOrder()가 public이라면?
@Service
public class OrderService {
public void placeOrder() {
System.out.println("주문 시작");
saveOrder(); // 내부 호출
System.out.println("주문 완료");
}
@Transactional
public void saveOrder() {
System.out.println("DB에 주문 저장");
}
}
위 코드처럼 saveOrder()를 public으로 바꾸면, 프록시가 해당 메서드를 감쌀 수는 있다.
하지만 여전히 placeOrder() 내부에서 saveOrder()를 호출하면 트랜잭션은 동작하지 않는다.
그 이유는 placeOrder()가 saveOrder()를 호출할때는 프록시가 아닌 내부에서 자기 자신(this) 을 통해 saveOrder()를 직접 호출하기 때문이다.
즉, 프록시를 거치지 않으므로 AOP가 호출을 가로챌 수 없다.
@Transactional을 적용하려면?
@Transactional은 반드시 프록시를 통해 호출되는 public 메서드여야 한다. 즉, 외부에서 호출될 때만 프록시를 거쳐 트랜잭션이 적용된다.
즉, @Transactional은 "프록시를 거쳐 외부에서 호출된 public 메서드" 에서만 트랜잭션이 동작한다.
내부 호출이거나 private 메서드에서는 프록시가 개입할 수 없기 때문에 트랜잭션이 적용되지 않는다.
클래스 분리
그렇다면 어떻게 해야 할까? 가장 대표적인 해결 방법은 클래스를 분리하는 것이다.
@Service
public class OrderService {
private final SaveOrderService saveOrderService;
public OrderService(SaveOrderService saveOrderService) {
this.saveOrderService = saveOrderService;
}
public void placeOrder() {
System.out.println("주문 시작");
saveOrderService.saveOrder(); // 외부 Bean 호출 → 프록시 거침
System.out.println("주문 완료");
}
}
@Service
public class SaveOrderService {
@Transactional
public void saveOrder() {
System.out.println("DB에 주문 저장");
}
}
앞서 여러 번 강조했듯이, Spring AOP는 외부에서 프록시 객체를 호출할 때 그 호출을 가로채어 동작한다.
따라서 위와 같이 클래스를 분리하면, placeOrder()는 내부 메서드 호출이 아닌 다른 Bean(SaveOrderService)의 프록시 메서드를 호출하게 된다.
그 결과 saveOrder() 호출 시 AOP가 정상적으로 적용되어 @Transactional이 기대한 대로 동작한다.
'Java , Spring > Spring' 카테고리의 다른 글
| [JSP] JSP의 등장 배경, 동작 원리, 장단점 (0) | 2025.09.07 |
|---|---|
| [MSA] MSA 환경에서 EDA 기반으로 사용자 요청 비동기 처리하기 (0) | 2025.04.17 |
| [MSA] Orchestration 기반 SAGA 패턴을 활용한 분산 트랜잭션 처리 (1) | 2025.03.02 |
| [MSA] MSA 환경에서의 분산 트랜잭션 처리(2PC, SAGA) (3) | 2025.02.23 |
| [Spring Cloud] MSA 서버 간 호출방식 비교 (RestTemplate, FeignClient, WebClient, RestClient) (1) | 2025.02.08 |
댓글