노트

자기 호출 문제

Self-Invocation

백엔드#spring · 연결된 개념 4개

쉽게 말하면

자기 호출 문제는 비서를 거쳐 들어온 일에만 기록과 결재가 붙는데, 사장이 자기 방 안에서 다음 일을 스스로 처리해 버려 비서가 모르는 상황이에요. 트랜잭션 같은 기능이 오류도 없이 조용히 빠져요.

비유가 깨지는 곳 사장에게 '비서를 거쳐요'라고 부탁할 수는 없어요. 실제로는 그 메서드를 다른 빈으로 옮기거나 자기 프록시를 주입받아 부르고, AspectJ 위빙을 쓰면 바이트코드에 직접 들어가 이 문제가 없어요.

Spring AOP(Aspect-Oriented Programming)에서 같은 객체 안의 메서드가 this로 다른 메서드를 부르면, 그 메서드에 붙은 부가 기능(@Transactional, @Async, @Cacheable 등)이 실행되지 않는 현상. 부가 기능은 프록시가 실행하는데, 내부 호출은 프록시를 거치지 않기 때문이다.

@Service
public class OrderService {
    public void placeAll(List<Order> orders) {
        orders.forEach(this::place);   // this = 실제 객체, 프록시가 아님
    }
 
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void place(Order order) { /* 새 트랜잭션이 열리지 않는다 */ }
}
컨트롤러 → [프록시] → OrderService.placeAll()
                         └ this.place()  ← 프록시를 건너뜀

밖에서 주입받은 빈(bean)은 프록시이므로 orderService.place()는 정상 동작한다. 같은 메서드를 내부에서 부를 때만 부가 기능이 조용히 빠지고 오류도 나지 않아 찾기 어렵다. @Transactional이 적용되지 않는 흔한 이유다.

해결

Spring 문서가 권하는 순서대로:

  1. 내부 호출을 없앤다(권장): 부가 기능이 필요한 메서드를 다른 빈으로 옮기고 그 빈을 주입받아 부른다. 일이 조금 들지만 가장 덜 침습적이다
  2. 자기 자신을 주입한다(self injection): 자기 빈 참조(프록시)를 주입받아 this 대신 그 참조로 부른다. 자기 자신에 대한 순환 참조이므로 생성자 주입이나 순환 참조를 막은 설정에서는 @Lazy로 늦춰 주입해야 할 수 있다. Spring 문서는 이것도 최후의 수단으로 보고 별도 빈 분리를 먼저 권한다
  3. AopContext.currentProxy(): 현재 프록시를 꺼내 부른다. 코드가 Spring AOP에 강하게 묶이고 프록시 노출(exposeProxy) 설정이 필요해서 Spring 문서는 가장 권하지 않는다

AspectJ 컴파일 시점·로드 시점 위빙(weaving)은 프록시가 아니라 바이트코드에 부가 기능을 넣으므로 이 문제가 없다. @Transactional도 @EnableTransactionManagement(mode = AdviceMode.ASPECTJ)로 기본인 프록시 모드 대신 AspectJ 모드를 쓰면(spring-aspects와 위빙 설정 필요) 내부 호출에도 트랜잭션이 적용된다. AOP의 개념 자체는 스프링 AOP에 있다.

출처: Spring Framework — Understanding AOP Proxies · Spring Framework — Self Injection · Spring Framework — Using @Transactional

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 스프링에서 외부 API 호출

    스프링 앱에서 다른 서버의 REST API를 부르는 도구는 여러 세대가 있다. 지금 새 코드라면 동기 호출은 RestClient, 리액티브·비동기는 WebClient, 인터페이스 선언 방식은 HTTP Interface나 OpenFeign이 흔한 선택이다(2026 기준).

  • 의존성 주입 (DI)

    객체가 필요한 협력 객체를 직접 new로 만들지 않고, 바깥(스프링 컨테이너)에서 넣어 받는 방식. 무엇을 언제 만들고 어떻게 연결할지의 제어가 내 코드에서 컨테이너로 넘어가므로 제어의 역전(Inversion of Control, IoC)이라고도 부른다.

  • 스프링 빈과 IoC 컨테이너

    스프링 빈은 스프링 IoC(Inversion of Control, 제어의 역전) 컨테이너가 만들고, 의존성을 연결하고, 생명주기(lifecycle)를 관리하는 평범한 자바 객체(POJO, Plain Old Java Object)다. 컨테이너(ApplicationContext)는 이 빈들을 담는 공간이고, 내가 new로 만든 객체는 컨테이너가 모른다.

  • 스프링과 장고 비교

    스프링과 장고는 같은 문제(요청 처리, 의존성 관리, 부가 기능 분리, DB 접근)를 각 언어의 성격에 맞게 다르게 푼다. 스프링은 명시적인 IoC(Inversion of Control) 컨테이너와 프록시 기반 AOP(Aspect-Oriented Programming)를, 장고는 설정 파일·import·데코레이터·미들웨어 같은 파이썬다운 장치를 쓴다.

  • 트랜잭셔널 아웃박스 패턴

    DB 변경과 이벤트 발행을 안전하게 함께 처리하는 패턴이다. 실제 데이터 변경과 "이 이벤트를 내보내라"는 기록(outbox 행)을 같은 DB 트랜잭션에 넣고, 별도 워커가 outbox를 읽어 메시지 브로커로 보낸다.

보기 옵션