Self-Invocation
Self-invocation (자기 호출)은 같은 클래스 안에서, 자신의 메서드를 직접 호출하는 경우 발생하는 문제이다. 스프링에서 `@Transactional` , `@Async` , AOP 기반의 기능은 프록시를 통해 동작을 한다. 그런데, `this.someMethod()` 처럼 내부 호출을 하면 프록시를 거치지 않고 실제 객체의 메서드가 실행된다. 따라서 프록시가 개입하지 못하니 AOP 기능이 적용되지 않는다.
외부에서 호출할 때 : 클라이언트 -> 프록시 -> 실제 메서드 실행
내부에서 자기호출할 때 : `this.innerMethod()` -> 바로 실제 메서드 실행 (프록시를 거치지 않음)
이게 왜 문제인거냐 ?
개발자는 `@Transactional` , `@Async` , `@Cacheable` 등을 메서드에 붙이면 항상 적용될 것이라고 기대한다. 그런데 내부에서 자기 메서드를 호출하면 프록시가 안 타서 어노테이션이 무시된다. 즉, 코드와 실행 결과가 달라져서 버그로 이어지기 쉽다.
예시
@Service
public class OrderService {
public void outerMethod() {
// 같은 클래스 내부에서 innerMethod 호출
// self-invocation (프록시 안탐)
innerMethod();
}
@Transactional
public void innerMethod() {
// DB 저장 로직
throw new RuntimeException("예외 발생");
}
}
- 기대 : `innerMethod` 가 트랜잭션을 시작해서, 예외 발생 시 rollback 될 것
- 실제 : `outerMethod()` → `innerMethod()` 호출 시 프록시를 안 거침 → 트랜잭션 시작 안 됨 → rollback 안 됨 → 데이터가 그대로 db에 저장됨 → 큰 문제 발생
해결 방법
1) 메서드를 다른 빈으로 분리한다.
AOP는 프록시를 통과해야 켜진다. 같은 클래스 안에서 호출하면 프록시를 거치지 않기 때문에 안 먹히는 상황이다. 그래서 프록시를 무조건 거치도록 메서드를 다른 클래스(빈)으로 떼어내는 방법이 가장 깔끔하다.
@Service
@RequiredArgsConstructor
public class OrderFacade {
private final OrderService orderService;
public void placeOrder() {
// 트랜잭션 안 걸린 작업들
// 다른 빈 호출 → 프록시 통과 → @Transactional 적용
orderService.saveOrder();
}
}
@Service
public class OrderService {
@Transactional
public void saveOrder() {
// DB 저장
throw new RuntimeExpetion("예외 발생");
}
}
이렇게 하면 `saveOrder()` 는 반드시 프록시를 거쳐 호출되기 때문에 `@Transactional` 이 100% 동작한다. 이 방식이 가장 많이 권장되는 방식이다.
2) 자기 자신(프록시)을 주입
스프링이 관리하는 '나 자신'의 프록시 객체를 직접 불러서 내부 호출도 프록시를 거치도록 만드는 방법이다.
@Service
@RequiredArgsConstructor
public class UserService {
private final @Lazy UserService self; // 자기 자신 프록시 주입 (@Lazy로 순환 방지)
public void signup() {
// 프록시를 거쳐서 호출 → @Transactional 동작함
self.createUser();
}
@Transactional
public void createUser() {
// 여기서는 트랜잭션이 제대로 시작됨
}
}
`self.createUser()` 는 단순히 `this.createUser()` 가 아니라 스프링이 대신 만들어둔 프록시를 거친다. 그 덕분에 트랜잭션이 적용된다. 그러나 단점이라면 `@Lazy`를 붙여야 순환참조 문제가 안 생기고, 코드만 보면 왜 self 를 쓰는지 이해가 어려울 수 있다. 그래서 빠르게 해결해야 할 때만 주로 사용한다.
'Spring' 카테고리의 다른 글
| [Spring] 객체지향 쿼리 언어 (0) | 2025.10.02 |
|---|---|
| [Spring] 즉시 로딩과 지연 로딩 (0) | 2025.09.21 |
| [Spring] Spring Events 사용해 이벤트 발행하기 (0) | 2025.09.07 |
| [Spring] Setter 사용을 지양해야하는 이유 (0) | 2025.08.29 |
| [Spring] DTO class를 record로 사용하는 이유 (0) | 2025.08.29 |