[Spring] AOP, 동적 Proxy와 CGLIB, Adivce, Pointcut, Advisor 그리고 BeanPostProcessor

벼랑 끝 코딩·2025년 3월 22일

Spring

목록 보기
5/16

Spring은 어떤 클래스를 만들어도 이 클래스가 알아서 웹에서 동작할 수 있도록
Java만을 사용해서는 하기 힘들었던 대신 처리해준다.
천쪼가리만 만들어서 던져줘도 어엿한 반팔티가 만들어지는 마법같은 일이다.

덕분에 개발자들은 환경 설정과 같은 부가적인 작업보다
기능 개발 그 자체에 더욱 몰두할 수 있게 됐다.
기능만 개발할 뿐인데 어떻게 이런 마법같은 일이 가능한걸까?

물론 개발 중에도 Spring의 많은 도움을 받지만,
이러한 동작은 개발의 앞단과 뒷단에서 Spring이 다양한 로직을 추가해 처리하기 때문이다.
오늘은 그 마법의 비밀인 AOP에 대해 알아보자.

AOP(Aspect Oriented Programming)

AOP란 관점 지향 프로그래밍을 의미하는 단어로
애플리케이션을 관점 지향적으로, 즉 기능 하나하나의 관점으로 바라보는 것을 의미한다.
여기서의 기능은 개발하고자 하는 기능 자체를 의미하는 것이 아니라,
애플리케이션에 전반적으로 필요한 공통적인 여러가지 기능을 의미한다.

즉 Spring이 마법처럼 처리해주는 일, 그 기능 하나하나를 별도로 바라보는 것이다.

그리고 이것을 구현하기 위해 필요한 기술이 바로 Proxy이다.

먼저 Proxy가 어떤 개념인지 알아야 Spring의 AOP에 대해 이해할 수 있다.

Proxy 단점

모든 문제를 해결할 것만 같은 Proxy에는 단점이 있다.
바로 기능을 추가하는 모든 인터페이스나 클래스에 Proxy를 생성해야 한다는 점이다.

예를 들어 100개의 클래스에 접근을 제어하기 위해 Proxy 클래스를 두고 싶다면
100개의 클래스를 내부에 객체로 두는 100개의 Proxy 클래스를 생성해야 한다.

JDK 동적 프록시

Spring에서는 매번 클래스를 설계해야 하는 번거로움을 해결하기 위해
JDK 동적 프록시와 CGLIB라는 기술을 지원한다.
두 기술은 클래스를 설계하지 않고 코드로 Proxy를 생성할 수 있다.
먼저 JDK 동적 프록시 기술로 어떻게 Proxy를 생성하는지 알아보자.

InvocationHandler

[java.lang.reflect.InvocationHandler]

class RealInvocationHandler implements InvocationHandler {

	private final Object target;
    
    public RealInvocationHandler(Object target) {
    	this.target = target;
    }
	
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    	// 프록시 로직 코드
        Object result = method.invoke(target, args)  // ** 실제 호출 객체 메서드 **
        // 프록시 로직 코드
        
        return result;
    }
}

먼저 JDK 동적 프록시를 생성하기 위해서는 프록시로 적용할 로직을

InvocationHandlerinvoke() 메서드에 구현해야 한다.

사용자가 실제 호출하려는 메서드리플렉션을 사용하여
invoke() 메서드의 파라미터인 method에 전달되고
invoke()에 프록시로 적용할 로직을 추가한 후,
method.invoke()를 통하여 실제 객체의 메서드를 호출하면 된다.
(구현하는 invoke와 호출하는 invoke는 다른 invoke)

리플렉션에 대해 알고있지 않다면 위 포스팅을 참고하자.

java.lang.reflect.Proxy.newProxyInstance()

interface TargetInterface {
	
    void action();
}

class Target implements TargetInterface {
	
    @Override
    public void action() {
    	// 메서드 바디
    }
}


public void clientMethod() {
	
    TargetInterface target = new Target();
    RealInvocationHandler rih = new RealInvocationHandler(target);
    
    // ** JDK 동적 프록시 생성 **
    TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(
    		TargetInterface.class.getClassLoader(),
    		new Class[] {TargetInterface.class}, rih);
    
    proxy.action();
}

InvocationHandler를 구현하여 Proxy 로직을 추가했다면,
java.lang.reflect에 위치한 기능인 Proxy를 통해

Proxy.newProxyInstance() 메서드를 호출하면 된다.

Proxy.newProxyInstance()에는 3가지 파라미터를 전달한다.

  • 클래스로더

원본 객체와 동일한 실행 환경을 구성하기 위한 클래스로더를 전달한다.
보통 원본 객체 인터페이스의 클래스로더를 전달한다.

  • 클래스 배열

InvocationHandler의 로직을 더해 Proxy 객체를 생성할 클래스 배열을 전달한다.
전달한 클래스의 한하여 Proxy 객체가 생성된다.

  • InvocationHandler

Proxy 로직을 추가한 InvocationHandler를 전달한다.


proxy 객체의 메서드를 호출하면
호출한 메서드 이름과 파라미터들이 Invocation invoke() 메서드의 파라미터로 전달된다.
invoke()는 자동으로 실행되며 내부에서는 호출한 메서드 이름으로 리플렉션을 통해
파라미터들을 전달하면서 실제 객체의 메서드를 동적으로 호출한다.

JDK 동적 프록시 단점

JDK 동적 프록시는 인터페이스 기반으로 Proxy를 생성한다.

인터페이스 구현이 아닌 경우에는 Proxy를 생성할 수 없으며 예외를 발생시킨다.

또한 인터페이스 기반으로 생성되어 인터페이스 정보만을 가지고 있기 때문에
구체클래스로 타입캐스팅이 불가능하다.
이러한 문제는 Spring에서 구체클래스에 의존관계를 주입하려고 할 때 문제가 발생한다.

CGLIB

인터페이스 기반인 JDK 동적 프록시와는 반대로

구체클래스에 기반하는 것이 바로 CGLIB이다.

MethodInterceptor

[org.springframework.cglib.proxy.MethodInterceptor]


class RealMethodInterceptor implements MethodInterceptor {

	private final Object target;
    
    public RealMethodInterceptor(Object target) {
    	this.target = target;
    }
	
    @Override
    public Object intercept(Object obj, Method method, Object[] args,
    		MethodProxy proxy) throws Throwable {
    
    	// 프록시 로직 코드
        Object result = proxy.invoke(target, args)  // ** 실제 호출 객체 메서드 **
        // 프록시 로직 코드
        
        return result;
    }
}

JDK 동적 프록시가 프록시 로직을 InvocationHandler의 invoke()에 구현했다면,

CGLIB는 프록시 로직을 MethodInterceptor의 intercept()에 구현해야 한다.

intercept() 메서드에는 4가지 파라미터를 전달한다.

  • Object

생성된 proxy 객체를 전달한다. 필요 시 사용한다.

  • Method

생성한 proxy를 통해 호출한 메서드 정보를 전달한다. 필요 시 사용한다.

  • Object[]

호출한 메서드의 파라미터 배열을 전달한다.
실제 객체 메서드를 호출할 때 파라미터로 전달한다.

  • MethodProxy

proxy의 원본 객체를 고성능 호출 기능을 제공하는 객체이다.
CGLIB에서는 method.invoke() 대신 proxy.invoke()를 사용한다.

Enhancer

class Target {
	
    public void action() {
    	// 메서드 바디
    }
}

public void clientMethod() {
	
    Target target = new Target();

	Enhancer enhancer = new Enhancer();
	enhancer.setSuperclass(Target.class);
	enhancer.setCallback(new RealMethodInterceptor(target));
    Target proxy = (Target) enhancer.create();
    
    proxy.action();
}

CGLIB 기술을 사용하여 Proxy 객체를 생성하기 위해서는 Enhancer 객체가 필요하다.

Enhancer의 setSuperclass() 메서드로 Proxy를 생성할 구체클래스를 전달하고,
setCallback() 메서드로 Proxy 로직인 MethodInterceptor 구현체를 전달한다.
이후 create() 메서드를 사용하면 Proxy 객체를 생성할 수 있다.

생성한 Proxy 객체에서 메서드를 호출하면,
MethodInterceptor의 intercept() 메서드를 자동으로 호출하고
파라미터로 생성한 Proxy 객체와 호출한 메서드 정보를 전달한다.
프록시 로직에 더해 MethodProxy를 활용하여 실제 객체 메서드를 호출하면 된다.

CGLIB 단점

CGLIB는 구체클래스를 기반으로 Proxy를 생성하는데,
이때 클래스가 상속 관계에 있는 경우 상속에서 발생하는 단점을 고스란히 가진다.
부모 클래스의 생성자를 필수로 호출해야 하며,
final 클래스나 final 키워드를 가지고 있는 경우 Proxy 객체 생성이 제한된다.

또한 상속 관계의 경우 2번의 생성자를 호출하는 오버헤드가 발생한다.
Proxy 객체를 생성하기 위해 부모의 생성자를 1회 호출하고,
실제 객체를 생성하기 위해 부모의 생성자를 1회 호출한다.

하지만 Spring은 내부에 CGLIB 라이브러리
spring-core.org.springframework에 함께 패키징하는데,
여기에 objenesis 라이브러리를 통해 부모 클래스 생성자 호출,
그리고 2번 생성자가 호출되는 문제를 해결한다.

final 키워드의 경우 대부분 상속 관계에서 사용할 일이 없기 때문에 문제가 되지 않아
CGLIB의 단점은 없다고 보면 된다!
(CGLIB가 함께 패키징 되는 것은 Spring 버전에 따라 다름)

따라서 Spring은 AOP 기술로 CGLIB를 기본으로 사용한다.

ProxyFactory

인터페이스 기반일 때에는 JDK 동적 프록시를 생성하고,
구체클래스 기반일 때에는 CGLIB 프록시를 생성하여 구분하는 것이 번거롭다.

Proxy를 생성해야할 객체가 무엇이든 하나로 통일하는 것이 ProxyFactory다.

Advice(MethodInterceptor)

객체의 타입에 상관 없이 프록시 생성을 추상화하는 것의 핵심은
InvokationHadnler, MethodInterceptor로 나누어 구현하던
프록시 로직 생성 기능을 추상화해야 한다.

ProxyFactory에서는 프록시 로직을 공통적으로 Advice에 구현한다.

Advice에 구현하는 방법은 MethodInterceptor의 invoke() 메서드를 구현하면 된다.

[org.aopalliance.intercept.MethodInterceptor]

class RealAdvice implements MethodInterceptor {

	@Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
    	// 프록시 로직
        Object result = invocation.proceed();
        // 프록시 로직
        
        return result;
    }
}

MethodInvocation의 proceed() 메서드를 호출하면
Advice는 인터페이스 기반 프록시 객체라면 InvocationHandler의 invoke()를,
구체클래스 기반 프록시 객체라면 MethodInterceptor의 invoke()를 호출한다.

CGLIB의 MethodInterceptor와 이름이 같을 뿐 전혀 다른 클래스이다!
Advice인 MethodInterceptor은 이전과는 다르게 파라미터로 target을 전달하지 않는데,
ProxyFactory에 target을 파라미터로 전달하여 생성하기 때문에
이미 target에 대한 정보가 내부에 있기 때문이다.

MethodInvocation에 target, 메서드 정보가 모두 담겨있어 더욱 편리하게 사용 가능하다.

Proxy 생성

class Target {
	
    public void action() {
    	// 메서드 바디
    }
}


Target target = new Target();

ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new RealAdvice());  // ** MethodInterceptor 추가 **

// ** 프록시 생성 **
// 인터페이스면 JDK 동적 프록시, 구체클래스면 CGLIB 프록시 생성
Target proxy = (Target) proxyFactory.getProxy();

proxy.action();

ProxyFactory 객체를 생성하면서 target 객체를 전달한다.
addAdvice()로 프록시 로직을 추가하기 위해 MethodInterceptor를 추가해야 한다.

getProxy() 메서드로 프록시 객체를 생성할 수 있다.
이 때 인터페이스를 대상으로 프록시를 생성하는 경우 JDK 동적 프록시를,
구체클래스 대상으로 프록시를 생성하는 경우 CGLIB 프록시를 자동으로 생성한다.
더 이상 프록시를 생성하려는 클래스에 따라 기술을 구분할 필요가 없다!

Proxy 객체가 메서드를 호출하면
MethodInterceptorinvoke() 메서드가 자동으로 호출된다.
실제 객체 메서드는 파라미터인 MethodInvocationproceed() 메서드로 호출한다.

proxyFactory.setProxyTargetClass(true);

setProxyTargetClass(true) 메서드를 사용하면
인터페이스 대상으로 프록시 객체를 생성해도 CGLIB 프록시를 생성해준다.

Pointcut

ProxyFactory덕분에 인터페이스, 구체클래스에 관계 없이
클래스를 생성하지 않고 프록시를 생성하는 것이 가능해졌다.
이제 메서드 호출 시 간편하게 프록시 기능을 추가하여 메서드를 호출할 수 있다.

그렇다면 이제 제약이 필요한 시점이다.
현재까지의 기능은 매우 훌륭하지만, 호출하는 메서드마다 모두 프록시 기능이 적용된다.
어떤 메서드는 그냥 호출하고, 특정 메서드에만 프록시 기능을 더하고 싶다.

이렇게 프록시 기능을 적용할 대상을 선택할 수 있는 기능이 Pointcut이다.

interface Pointcut

Pointcut을 만들기 위해서는 Pointcut 인터페이스를 구현해야 한다.

public interface Pointcut {
	ClassFilter getClassFilter();
    MethodMatcher getMethodMatcher();
}

public interface ClassFilter {
	boolean matches(Class<?> clazz);
}

public interface MethodMatcher {
	boolean matches(Method method, Class<?> targetClass);
}

Pointcut 인터페이스를 구현하려면
ClassFilter getClassFilter()과
MethodMatcher getMethodMatcher()을 각각 구현해야 한다.
반환값이 ClassFilter와 MethodMatcher이기 때문에,
ClassFilter와 MethodMatcher도 마찬가지로 구현해야 한다.

이러한 과정은 매우 복잡하다.
실제로 Pointcut을 직접 구현해서 사용하는 경우는 드물고,

Spring에서 제공하는 Pointcut을 주로 사용한다.

Spring 제공 Pointcut

Pointcut을 직접 구현하는 작업은 매우 번거롭기 때문에,
주로 Spring이 제공하는 Pointcut을 사용한다.

  • TruePointcut : 항상 Advice 적용
  • NamedMethodMatchPointcut : 지정한 이름만 Advice 적용
  • AnnotationMatchingPointcut : 지정한 애노테이션을 보유한 경우 Advice 적용
  • AspectJExpressionPointcut : AspectJ 표현식에 근거하여 Advice 적용
    보통 AspectJExpressionPointcut을 사용한다.

Advisor

Advisor은 프록시 로직인 Advice와 적용 대상인 Pointcut을
한 곳에 담아 캡슐화하여 관리하는 클래스이다.
관점 지향 프로그래밍의 Aspect가 의미하는 것이 바로 Advisor이다.
Advisor라는 개념을 통해서 AOP를 더욱 직관적으로 관리한다.

Advisor = 1 Advice + 1 Pointcut

DefaultPointcutAdvisor

Advisor 인터페이스의 가장 일반적인 구현체로는 DefaultPointcutAdvisor가 있다.

DefaultPointcutAdvisor에 Pointcut과 Advice를 파라미터로 전달하여 사용한다.

class Target {
	
    public void action() {
    	// 메서드 바디
    }
}

public void proxyMethod() {
	Target target = new Target;
	ProxyFactory proxyFactory = new ProxyFactory(target);

	// ** Advisor 생성 및 등록 **
	DefaultPointcutAdvisor dpa = new DefaultPointcutAdvisor(
			Pointcut.TRUE, new RealAdvice());
	proxyFactory.addAdvisor(dpa);

	Target proxy = (Target) proxyFactory.getProxy();
	proxy.action();
}

Advice의 개념만 있을 땐 Advice를 등록해서 프록시를 생성했지만,
Pointcut이 추가되어 Advisor가 Aspect의 개념이라는 것을 알게된 지금부터는
addAdvisor() 메서드를 통해 생성한 Advisor을 등록해서 생성한다.

1 Proxy N Advisor

Spring의 Proxy 기술인 JDK 동적 프록시와 CGLIB에 대해 알아보고
이를 통합하는 ProxyFacotry에 대해 알아봤다.
ProxyFactory에서는 Advice에 프록시 로직을 작성했다.
여기에 적용 대상을 구분하는 Pointcut이 추가되었고,
Advice와 Pointcut을 이 합쳐져 Advisor이라는 Aspect 개념이 탄생했다.

이 과정이 없었다면, 프록시 기능마다 Proxy 객체가 생성된다.
100개의 추가 기능이 있다면 100개의 Proxy가 생성되는 것이다.

하지만 Advisor의 등장으로 여러 프록시 로직을 하나의 프록시 객체에 등록하여
향상된 성능으로 Proxy 기능을 사용할 수 있게 됐다.

BeanPostProcessor (빈후처리기)

클래스를 생성하지 않고 메서드를 통해 편리하게 프록시 객체를 생성할 수 있게 됐지만
target을 전달하고, Advisor를 등록하여 생성하는 과정이 너무 번거롭다.

결정적으로 ProxyFactory로 프록시 객체를 생성하고 사용하려면
런타임에 동적으로 생성해야 하기 때문에,
우리가 현재 우리가 주로 사용하는 편리한 애플리케이션 시작 시 빈을 등록하는
Component Scan 방식에서는 사용할 수가 없다!

BeanPostProcessor는 이런 번거로운 작업을 쉽게 처리해준다.

Spring은 스프링 컨테이너 생성 후 Bean 등록 전에 빈후처리기에 Bean을 전달한다.

빈후처리기에서는 Bean을 등록하기 전에 Bean에 적용할 로직을 수행 후 등록한다.

로직의 형태에는 한계가 없다.
등록 전에 알림 기능이 추가될 수도 있고, 등록할 Bean을 바꿔버릴 수도 있다.

.. 등록할 Bean을 바꿔버린다고?
그렇다.

등록할 Bean이 프록시 로직 적용 대상이라면 Proxy 객체를 Bean으로 등록할 수도 있다.
이 방법이 바로 Component Scan 방식도 Proxy 객체 생성을 가능하게 만들어준다!

BeanPostProcessor 인터페이스

빈후처리기를 사용하기 위해서는 BeanPostProcessor 인터페이스를 구현해야 한다.

public interface BeanPostProcessor {
	
    Object postProcessBeforeInitialization(Object bean, String beanName) 
    		throws BeansException;
    
    Object postProcessAfterInitialization(Object bean, String beanName)
    		throws BeanException;
}

Spring은 스프링 컨테이너 생성 후 빈을 등록하고 초기화 메서드를 실행한다.
postProcessBeforeInitialization() 메서드는 초기화 메서드 호출 전에,
postProcessAfterInitialization() 메서드는 초기화 메서드 호출 후에 호출된다.

개발자가 등록한 Bean을 포함하여
Spring에서 등록하는 모든 Bean에 대해 메서드가 1회 호출된다.
따라서 Spring에서 등록하는 Bean은 적용을 제외하도록 적절한 필터링이 필요하다.

BeanPostProcessor를 구현한 클래스 역시 Bean으로 등록해야 사용할 수 있다.
기존에 Proxy 객체를 생성하려면 ProxyFactory를 활용하여
모든 객체마다 코드를 작성해주어야 했지만,
이제는 빈후처리기에 한번만 작성하면
모든 Bean이 등록되기 전에 프록시 객체가 생성되어 컨테이너에 등록된다!

AnnotationAwareAspectJAutoProxyCreator

Spring이 얼마나 많은 일을 처리해주는지 긴 과정으로부터 뼈저리게 느낄 것이다.
Spring은 빈후처리기를 직접 구현하는 것 조차도 용납하지 않는다
구현 없이 빈후처리기를 사용할 수 있도록 지원해준다!

build.gradle 라이브러리 추가

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-aop'
}

위 라이브러리를 추가하면,

AnnotationAwareAspectJAutoProxyCreator 빈후처리기가 Bean으로 자동 등록된다.

이 빈후처리기는 Bean으로 등록된 Advisor를 탐색하여
자동으로 초기화 메서드 호출 후에 Proxy 객체를 생성해준다.

이젠 BeanPostProcessor를 구현하지도,
메서드를 오버라이딩하여 Proxy 객체를 직접 생성하지 않아도 된다.

AppConfig에 Advisor를 반환하는 메서드를 구현하여 Bean으로 등록만 하면 된다!

마무리

Spring에서 벌어지는 마법같은 작업의 핵심인 Proxy 기술을 알아봤다.
Spring은 Proxy 기술을 클래스를 만들지 않고 편리하게 사용하기 위해
JDK 동적 프록시, CGLIB 프록시를 사용하고 있었다.

하지만 JDK 동적 프록시는 인터페이스 기반으로,
CGLIB 프록시는 구체클래스 기반으로 사용되어 상황에 따라 구분짓는 것이 번거로웠다.
그래서 프록시 객체 생성 방법을 추상화한 ProxyFactory를 사용해
기술을 구분짓지 않고 편리하게 프록시 객체를 생성할 수 있게 됐다.

ProxyFactory에서는 프록시 로직을 Advice에 구현했다.
그리고 프록시 객체의 적용 대상을 구분하기 위해 Pointcut도 만들었다.
Advice와 Pointcut을 한 곳에서 관리하는 Advisor를 생성하여
ProxyFactory에 등록해 1 Proxy N Advisor 구조로 사용할 수 있게 됐다.

ProxyFactory로 정말 편리해졌지만,
객체마다 프록시 생성 코드를 작성하는 점이 힘들었다.
BeanPostProcessor 구현하여 한 번의 코드로 프록시 객체를 생성할 수 있었다.

하지만 Spring은 빈후처리기 마저도 자동으로 구현 후 Bean으로 등록하여
Advisor만 만들면 프록시 생성 코드 없이도 Proxy 객체를 만들 수 있게 해주었다.


우리는 Advisor만 만들면 된다.
Advice에 프록시 로직을 작성하고, 적용할 대상인 Pointcut만 전달해주면 된다.
이제 편리하게 Advisor를 만드는 방법에 대해 알아보자.

profile
복습에 대한 비판과 지적을 부탁드립니다

0개의 댓글