개요
스프링에서 제공하는 AOP의 기능과 원리를 알아보겠습니다. 리플렉션, JDK 동적 프록시와 CGLIB 소개를 시작으로 스프링이 지원하는 프록시와 빈 후기처리를 알아보겠습니다.
리플렉션
리플렉션은 런타임에 클래스와 메서드의 메타정보를 사용해 애플리케이션을 동적으로 유연하게 만드는 기술입니다. 리플렉션은 스프링 프록시의 기본이 되는 기술이므로 개념을 알아야 합니다. 리플렉션은 런타임에 원하는 동작을 할 수 있다는 장점이 있지만 컴파일 시점의 오류를 잡을 수 없으므로 특별한 경우를 제외하고는 사용하면 안 됩니다.
JDK 동적 프록시와 CGLIB
스프링에서 리플렉션 기반으로 동적 프록시를 사용하면 런타임 시 개발자를 대신하여 프록시를 생성해주고 다양한 동작을 할 수 있습니다. JDK 동적 프록시와 CGLIB 2가지를 알아보겠습니다.
- JDK 동적 프록시
JDK 동적 프록시는 인터페이스를 기반으로 동적 프록시를 생성하는 기술입니다.
기존의 런타임 객체에서 client는 aProxy를 통해 aImpl을 호출합니다.
동적 프록시를 적용하면 아래와 같이 프록시가 timeInvocationHandler를 통해 aImpl을 호출합니다.
- CGLIB(Code Generator Library)
CGLIB는 인터페이스를 사용하지 않고 구체 클래스만을 이용하여 동적 프록시를 생성하는 기술입니다. CGLIB는 바이트코드를 조작하여 동적으로 클래스를 생성하는 외부 라이브러리로 현재는 스프링 내부에 포함되어 별도의 추가를 하지 않아도 사용이 가능합니다.
cglib proxy가 생성되어 timeMethodInterceptor가 target을 호출합니다.
CGLIB는 단점이 있습니다.
1. CLIGB는 자식 클래스를 동적으로 생성하므로 부모 클래스의 생성자를 체크합니다.
2. 클래스에 final 키워드가 있으면 CGLIB를 적용할 수 없습니다.
3. 마찬가지로 메서드에 final 키워가 있으면 오버라이딩이 불가능하므로 CGLIB를 적용 할 수 없습니다.
(CGLIB의 문제는 최근 스프링 AOP 라이브러리에서 기본으로 해결이 되었습니다.)
직접적으로 JDK 동적 프록시와 CGLIB를 사용하면 각각 Handler와 Interceptor를 만들어야 하며 동작하는 코드도 정의해야 합니다. 스프링 프록시를 사용하면 이 과정을 자동화하고 통합 할 수 있습니다.
스프링 프록시
스프링에서 제공하는 ProxyFactory를 사용하면 직접 프록시를 정의할 필요 없이 프록시 기술에 따라서 자동으로 생성합니다. 위의 JDK 동적 프록시와 CGLIB는 사진에서 본대로 별도의 프록시를 생성해서 사용하지만 이 과정이 자동화됩니다.
또한 어떻게 동작하는지 각각 정의해주어야 했지만, 이제는 Advice를 정의하고 스프링 프록시에 추가하면 됩니다.
스프링 프록시를 사용하면 부가 기능을 별도의 기술에 종속적으로 정의하지 않고 Advice로 통일해서 사용할 수 있는 장점이 있습니다. 스프링 프록시는 내부적으로 JDK 동적 프록시의 InvocationHandler가 Advice를 호출하도록 만들어졌으며, MethodInterceptor가 Advice를 호출하도록 만들어져 있습니다.
스프링 프록시는 ProxyFactory이며 핵심 작동 로직은 아래와 같습니다.
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
Pointcut(포인트컷), Advice(어드바이스), Advisor(어드바이저)
스프링 AOP를 이해하기 위해서는 Pointcut, Advice, Advisor 3가지 용어를 알아야 합니다.
- Pointcut
부가 기능을 어디에 적용할지, 어디에 적용하지 않을지 판단하며 주로 클래스와 메서드 이름으로 필터링합니다.
- Advice
프록시가 호출하는 부가 기능입니다.
- Advisor
1개의 Pointcut과 1개의 Advice를 가지고 있습니다.
3개로 구분한 이유는 역할과 책임을 분리하기 위해서입니다. Pointcut은 적용 부분을 담당하고 Advice는 로직에 집중하며 2개를 합치면 하나의 Advisor가 됩니다.
client의 요청과 스프링 프록시의 동작 흐름은 아래와 같습니다.
스프링 프록시인 ProxyFactory는 Advisor에 있는 Pointcut과 Advice를 검사하여 적용합니다.
- 1개의 프록시가 여러 개의 Advisor를 가질 수 있다!!
프록시 1개당 Advisor를 1개만 가지는 구조라고 착각하는 사람들이 많은데, 프록시 1개에 여러개의 Advisor를 가질 수 있습니다.
코드는 아래와 같습니다. ProxyFactory에 addAdvisor()를 통해 2개의 Advisor를 추가합니다.
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
proxyFactory1.addAdvisor(advisor2);
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
즉, 하나의 target에 여러 AOP가 동시에 적용되더라도 스프링 AOP는 target마다 1개의 프록시만 생성합니다.
빈 후기처리
빈 후기처리는 기존에 스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 저장소에 등록하기 이전에 조작할 수 있는 방법입니다. BeanPostProcessor를 사용하며 말 그대로 빈을 생성한 이후에 후처리를 할 수 있습니다.
- 객체 바꿔치기
아래처럼 A객체를 B객체로 바꿔치기해서 빈 저장소에 등록할 수 있습니다.
아래 코드에서 ApplicationContext로 beanA를 획득해서 B 객체를 빈으로 바꿔서 등록합니다.
ApplicationContext applicationContext = new
AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
//beanA 이름으로 B 객체가 빈으로 등록된다.
B b = applicationContext.getBean("beanA", B.class);
b.helloB();
Assertions.assertThrows(NoSuchBeanDefinitionException.class,
() -> applicationContext.getBean(A.class));
- 객체를 프록시 객체로 바꿔치기
일반적으로 스프링 컨테이너가 컴포넌트 스캔으로 등록하는 모든 빈을 중간에 조작할 수 있습니다. 바꿔 말해서빈 객체를 프록시로 교체하는 것도 가능하며 스프링 AOP의 적용방식입니다.
메서드명은 postProcessAfterInitialization으로 빈이 생성된 이후 초기화를 하는 작업입니다. pointcut은 프록시 적용 여부를 체크해서 적용이 되어 있지 않다면 그대로 객체를 반환하고, 프록시 대상이면 프록시 객체를 만들어서 반환하여 바꿔치기합니다.
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("param beanName={} bean={}", beanName, bean.getClass());
//프록시 적용 대상 여부 체크
//프록시 적용 대상이 아니면 원본을 그대로 반환
String packageName = bean.getClass().getPackageName();
if (!packageName.startsWith(basePackage)) {
return bean;
}
//프록시 대상이면 프록시를 만들어서 반환
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvisor(advisor);
Object proxy = proxyFactory.getProxy();
log.info("create proxy: target={} proxy={}", bean.getClass(),proxy.getClass());
return proxy;
}
- 빈후처리에서 자동 프록시 생성기로 프록시 등록하기
스프링의 AutoProxyCreater를 사용하면 이 빈 후처리기는 스프링 빈으로 등록된 advisor를 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용합니다. advisor안에 advice와 pointcut이 있기 때문에 advisor만 알고 있으면 적용할 위치와 부가 기능도 당연히 알고 있습니다. 따라서 개발자는 advisor만 빈으로 잘 등록하면 나머지는 스프링 AOP가 자동으로 처리합니다.
자동 프록시 생성기는 pointcut을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 검사합니다. 만약 조건이 맞는 하나라도 있으면 프록시를 생성하고 조건에 맞는것이 하나라도 없으면 프록시를 생성하지 않습니다.
또한 프록시가 호출되었을 때 pointcut을 사용해서 advice를 적용할지 말지 검사합니다. 조건에 만족하면 프록시는 advice를 적용하고 나서 target을 호출합니다.(프록시는 advisor를 검사하여 advice, pointcut을 알 뿐만 아니라 실제 호출해야 하는 target도 알고 있습니다.)
- 1개의 프록시, 여러개의 advisor
advisor의 pointcut을 만족하는 개수가 2개 이상이더라도 프록시는 무조건 1개만 생성됩니다.
@Aspect 프록시 적용
@Aspect는 advisor를 스프링 빈으로 등록하는 역할을 합니다. 스프링 애플리케이션에서 프록시를 적용하려면 pointcut과 advice로 구성되어있는 advisor를 스프링 빈으로 등록하면 됩니다. 그러면 나머지는 프록시 자동 생성기에서 자동으로 등록하고 활용합니다. (@Aspect는 관점지향 프로그래밍을 가능하게 하는 AspectJ 프로젝트에서 제공하는 에노테이션입니다.)
스프링 AOP의 간단한 적용 예제는 아래와 같습니다.
@Aspect를 사용한다면 프록시 생성 과정은 아래와 같습니다.
1. 생성 : 스프링에서 빈 대상 객체들을 생성합니다.
2. 전달 : 생성된 객체들이 스프링 컨테이너에 등록되기 전에 빈 후처리기에 전달됩니다.
3-1. Advisor 빈 조회 : 스프링 컨테이너에서 모든 Advisor 빈을 조회합니다.
3-2. @Aspect Advisor 조회 : 스프링 컨테이너에 @Aspect로 등록된 모든 Advisor 빈을 조회합니다
4. 프록시 적용 대상 체크 : pointcut을 하나씩 검사해서 프록시 대상 여부를 확인합니다.
5. 프록시 생성 : 프록시 적용 대상이면 프록시를 생성하고 프록시를 반환합니다. 프록시가 빈으로 등록됩니다. 프록시 적용 대상이 아니면 원본 객체를 반환합니다.
6. 빈 등록 : 반환되는 객체를 빈으로 등록합니다
스프링 AOP 개념은 스프링 AOP (2) 에서 이어집니다.
참고
'학습 > Java' 카테고리의 다른 글
Value Object 패턴 (0) | 2023.07.07 |
---|---|
Entity vs Value Object 차이점 (0) | 2023.06.30 |
mutable vs immutable (0) | 2022.11.01 |
StringBuffer vs StringBuilder (0) | 2022.11.01 |
Java 8에 등장한 Optional이란? (0) | 2022.10.29 |