Engineering/Spring

Spring의 빈(Bean) 관리와 AOP(Aspect-Oriented Programming) 프록시의 작동 방식

부스 boos 2025. 6. 9. 15:26
728x90

Spring의 빈(Bean)과 프록시

  1. Spring 컨테이너의 빈: Spring 컨테이너는 애플리케이션 시작 시 @Service, @Repository, @Component 등으로 선언된 클래스의 인스턴스를 생성하고 관리합니다. 이 인스턴스들을 "빈"이라고 부릅니다.
  2. @Async와 AOP 프록시 생성:
    • 어떤 서비스 클래스(예: MyServiceA)의 특정 메소드(예: asyncMethod())에 @Async 어노테이션이 붙어 있고, @EnableAsync가 활성화되어 있다면, Spring 컨테이너는 MyServiceA의 실제 인스턴스(myServiceA_real)를 생성하는 것 외에, MyServiceA의 프록시 객체(myServiceA_proxy)를 생성합니다.
    • 이 프록시 객체는 myServiceA_real을 래핑(wrapping)하고 있으며, myServiceA_real과 동일한 인터페이스를 구현하거나 상속받습니다.
    • Spring 컨테이너는 다른 빈들이 MyServiceA 타입의 빈을 주입받을 때, 실제 myServiceA_real 대신 myServiceA_proxy를 주입해 줍니다.

다른 서비스 코드에서 호출 시 프록시의 역할

이제 MyServiceB라는 다른 서비스 클래스가 MyServiceA를 주입받아 사용하는 상황을 가정해 봅시다.

@Service
public class MyServiceA {

    @Async
    public void asyncMethod() {
        // 비동기적으로 실행될 로직
        System.out.println("MyServiceA.asyncMethod() - Current Thread: " + Thread.currentThread().getName());
    }

    public void syncMethodCallingAsyncInternally() {
        System.out.println("MyServiceA.syncMethodCallingAsyncInternally() - Calling asyncMethod() internally");
        asyncMethod(); // <-- 동일 클래스 내부 호출 (프록시 우회)
    }
}

@Service
public class MyServiceB {

    private final MyServiceA myServiceA; // Spring이 myServiceA_proxy를 주입해 줍니다.

    public MyServiceB(MyServiceA myServiceA) {
        this.myServiceA = myServiceA;
    }

    public void callAsyncMethodFromOtherService() {
        System.out.println("MyServiceB.callAsyncMethodFromOtherService() - Calling MyServiceA.asyncMethod()");
        myServiceA.asyncMethod(); // <-- 다른 서비스 코드에서 호출 (프록시를 통해 호출)
    }
}

 

MyServiceB의 callAsyncMethodFromOtherService() 메소드에서 myServiceA.asyncMethod()를 호출할 때 일어나는 일은 다음과 같습니다:

  1. MyServiceB에 주입된 myServiceA 인스턴스는 실제 MyServiceA 객체가 아니라 Spring이 생성한 MyServiceA 프록시 객체(myServiceA_proxy)입니다.
  2. myServiceA_proxy.asyncMethod()가 호출되면, 이 호출은 프록시 객체를 통해 전달됩니다.
  3. 프록시는 asyncMethod() 호출을 가로챕니다(인터셉션). Spring AOP는 이 호출을 가로채서 @Async 어노테이션이 붙어 있음을 인지합니다.
  4. 프록시는 비동기 로직을 수행합니다. 즉, 새로운 스레드 풀에서 asyncMethod()를 실행하도록 작업을 제출합니다.
  5. 결과적으로 MyServiceA.asyncMethod() 내부의 코드는 호출을 시작한 스레드(MyServiceB가 속한 스레드)와 다른 스레드에서 실행됩니다.

요약

  • 동일 클래스 내부 호출 (MyServiceA.syncMethodCallingAsyncInternally() -> asyncMethod()): this.asyncMethod()와 같이 호출하면, 이는 Spring 프록시를 거치지 않고 실제 MyServiceA 객체의 메소드를 직접 호출하는 것이기 때문에 @Async가 무시되고 동기적으로 실행됩니다. Spring 프레임워크 입장에서는 마치 @Async 어노테이션이 없는 일반 메소드 호출과 동일하게 취급합니다.
  • 다른 서비스 코드에서의 호출 (MyServiceB -> MyServiceA.asyncMethod()): MyServiceB에 주입된 myServiceA는 실제 MyServiceA 객체가 아니라 Spring이 특별히 만든 프록시 객체입니다. myServiceA.asyncMethod() 호출은 이 프록시 객체를 통해 이루어지며, 프록시가 @Async의 비동기 로직을 가로채서 처리하므로 정상적으로 비동기 호출이 발생합니다.

따라서 Spring AOP의 프록시 메커니즘은 @Async, @Transactional, @Cacheable 등과 같은 어노테이션의 핵심 기능을 제공하며, 외부(다른 빈)에서 호출할 때만 그 기능을 발휘합니다.