이전글[https://techforme.tistory.com/34]
이전 글에서 프록시객체를 자동으로 생성해주는 ProxyFactoryBean 에 대해서 썼는데, 이로써 인터페이스로 추상화하고 구현하는 지루한 과정을 하지 않아도 되게 되었다. 그러나 이 방식에는 한계가 있었는데, 또 다른 인터페이스에 부가기능을 추가하고 싶은 경우 다른 ProxyFactoryBean 을 다시 추가해야 한다는 점이었다.
이번 글에서는 한번에 여러가지 클래스, 여러 메서드를 맵핑하여 일괄적으로 부가기능을 부여하는 BeanPostProcessor (빈 후처리) 기술에 대해서 소개하고, 나아가 Spring 에서 제공하는 Spring AOP 를 통해 구현하는 것 까지 이야기하여 주제를 마치려고 한다.
1. 개념 정리
1) BeanPostProcessor
저번글에서 내가 지정한 대상의 <내가 원하는 메서드에>, <원하는 부가기능을>, <자동으로> 추가해주는 ProxyFactoryBean 을 구현했는데, BeanPostProcessor 는 여기서 한발 더 나아가 <원하는 대상> 에 부가기능을 추가해 주는 기술이다. BeanPostProcess 는 빈 후처리 라는 뜻으로, bean 이 생성된 후 추가적인 작업을 하고 빈을 되돌려주는 스프링의 AOP 기술이다. ProxyFactoryBean 은 빈이 생성되는 방식을 다르게 하여 빈을 새로 생성하는 것이라면, BeanPostProcessor 에서는 프록시의 대상이 되는 빈을 찾아내어 빈에 부가기능을 부여하고 되돌려준다는데서 차이가 있다.
여기서 <원하는 대상> 을 고르는 기준은 어떻게 주어야 할까? 이 또한 포인트컷의 역할이다. 사실 포인트 컷은 단순히 클래스 내의 메서드를 매치해주는 역할만 하는 것이 아니라 클래스를 솎아 내는 작업도 할 수 있다. 포인트 컷 + 어드바이스를 통해 클래스와 메서드를 맵핑하고 기능을 부여해 보자.
2. 프로젝트 적용
1) 스프링 AOP
Advisor 클래스
@Configuration
@RequiredArgsConstructor
@Profile("TestBeanPostProcessor")
public class TestBeanPostProcessor {
private final ApplicationContext ap;
@Bean
public DefaultPointcutAdvisor testAdvisor(){
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("@annotation(com.example.naejango.global.aop.TransactionTest)");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setPointcut(pointcut);
advisor.setAdvice((MethodInterceptor) invocation -> {
TransactionTest annotation = AnnotationUtils.findAnnotation(invocation.getMethod(), TransactionTest.class);
int pos = annotation.pos();
String value = annotation.value();
if(String.valueOf(invocation.getArguments()[pos]).equals(value)) throw new TestException();
return invocation.proceed();
});
return advisor;
}
@Bean
@Primary
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
}
TransactionTest 어노테이션
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TransactionTest {
String value() default "";
int pos() default 0;
}
어노테이션이 붙은 코드
@Override
@TransactionTest(pos = 1, value = "unsubscribe")
public void deleteSubscriptionIdBySessionId(String subscriptionId, String sessionId) {
String key = SESSION_SUBSCRIPTION + sessionId;
redisTemplate.opsForSet().remove(key, subscriptionId);
포인트 컷은 여러 형태가 있다. 직접 클래스 명과 메서드를 입력해 주거나 특정 패턴으로 필터링 할 수도 있다. 나는 AspectJ 라는 AOP 프레임워크의 표현시 포인트 컷을 이용했다. 이는 AspectJ 에서 제시하는 방법에 따라 표현식을 작성하여, 아주 경제적으로 클래스와 메서드를 솎아내주는 방법이다. 내 경우에는 트랜잭션테스트에서 예외를 반환해야하는 클래스가 아무래도 일정한 규칙을 따르지 않고, 대상 클래스의 정보 또한 이용해야 하기 때문에 어노테이션 표현식을 이용해서 대상 메서드를 선택했다. 어노테이션은 특정 아규먼트가 특정 값을 가지고 있을 때 예외를 반환할 것을 지정하기 위해 pos, value 값을 선언했다.
DefaultAdvisorAutoProxyCreator 는 BeanPostProcessor 의 구현체다. 별도로 Advisor 를 지정해주지 않아도 자동으로 탐색하여 빈 후처리를 진행한다. 사실 위 코드에는 명시적으로 Bean 을 등록하였지만, 등록하지 않아도 이미 빈으로 자동 등록이 되는 클래스다. 해당 코드를 주석처리해도 잘 동작한다.
이로서 내가 원하는 클래스의 원하는 메서드에 부가기능을 자동으로 추가하는 프록시 객체를 만들 수 있었다. 이번 포스팅에서 은근슬쩍 AOP 라는 단어를 이와 혼용하였다. AOP 란 Aspect Oriented Programming 의 약자로, 관점 지향 프로그래밍 이라는 뜻이다. 지금까지 확장에는 열려있고, 변경에는 닫혀있는 완성도 있는 설계를 위하여 객체 지향 설계 기법을 중심으로 추상화, 캡슐화, DI 등을 이용하였다. AOP 또한 객체 지향과 목표는 같으나, DI, IOC 등의 기존 방식으로는 해결할 수 없는 문제(특정 관심사를 여러 객체들을 횡단하여 적용)를 해결하기 위해 고안된 기법니다. 사람들은 이러한 접근법을 객체지향과는 다르게 보았고 관점 지향 이라는 이름을 붙이게 되었다고 한다.
2) Spring AOP
Spring 프레임 워크에서는 앞서 이야기한 BeanPostProcess 같은 기술을 직접 다루지 않고 그냥 포인트 컷만 제공하면 해당 부분을 가로채서 프록시 해주는 강력한 AOP 기술을 제공해준다. 그 많던 코드가 아래 처럼 확 줄어버린다.
@Aspect
@Component
@Profile("AspectJTestAOP")
public class AspectJTestAOP {
@Around("@annotation(com.example.naejango.global.aop.TransactionTest)")
public Object proxyTestStub(final ProceedingJoinPoint joinPoint) throws Throwable {
TransactionTest annotation = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(TransactionTest.class);
String targetArg = String.valueOf(joinPoint.getArgs()[annotation.pos()]);
if(targetArg.equals(annotation.value())) throw new TestException();
return joinPoint.proceed();
}
}
지금까지 프록시, 데코레이터 패턴부터 AOP 까지 단계적으로 확장해가면서 알아보았다. 솔직히 몇 문장 되지도 않는 글인데도, 지식이 짧으니 책을 여러번 들춰보고 안되는 코드 해결하려고 한참을 끙끙 대기도 했다. 아마 이 시리즈의 다음번 타자는 AspectJ 일 것인데 아직 그쪽 까지 배우려면 한참을 더 가야 할 것 같다. 화이팅...