토비의 스프링 6장 AOP를 읽고 배우고 학습한 것들을 정리합니다. 특별히 최신 AOP 기술이 등장하기 까지 스프링이 걸어온 집요한 문제 해결 과정에 집중해 봅니다.
Hello 인터페이스
public interface Hello {
String sayHello(String name);
String sayHi(String name);
String sayThankYou(String name);
}
타깃 클래스
public class HelloTarget implements Hello {
@Override
public String sayHello(String name) {
return "Hello " + name;
}
@Override
public String sayHi(String name) {
return "Hi " + name;
}
@Override
public String sayThankYou(String name) {
return "Thank You " + name;
}
}
프록시 클래스
public class HelloUppercase implements Hello {
private Hello hello;
public HelloUppercase(Hello hello) {
this.hello = hello;
}
@Override
public String sayHello(String name) {
return hello.sayHello(name).toUpperCase();
}
@Override
public String sayHi(String name) {
return hello.sayHi(name).toUpperCase();
}
@Override
public String sayThankYou(String name) {
return hello.sayThankYou(name).toUpperCase();
}
}
클라이언트 역할의 테스트
@Test
public void manualProxy() {
Hello hello = new HelloUppercase(new HelloTarget());
Assertions.assertEquals("HELLO TOBY", hello.sayHello("Toby"));
Assertions.assertEquals("HI TOBY", hello.sayHi("Toby"));
Assertions.assertEquals("THANK YOU TOBY", hello.sayThankYou("Toby"));
}
데코레이터 패턴은 타깃의 코드를 손대지 않고, 클라이언트가 호출하는 방법도 변경하지 않은 채로 새로운 기능을 추가할 수 있다.
목 오브젝트를 만드는 불편함을 목 프레임워크를 사용해 편리하게 바꿧던 것처럼 프록시도 일일이 모든 인터페이스를 구현해서 클래스를 새로 정의하지 않고도 편리하게 만들어서 사용할 방법은 없을까?
독립적인 부가기능 (InvocationHandler)
public class UpperCaseHandler implements InvocationHandler {
private final Object target;
private final String pattern;
public UpperCaseHandler(Object target, String pattern) {
this.target = target;
this.pattern = pattern;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object ret = method.invoke(target, args);
if (ret instanceof String && method.getName().startsWith(pattern)) {
return ((String) ret).toUpperCase();
} else {
return ret;
}
}
}
다이나믹 프록시와 InvocationHandler를 사용하는 테스트
@Test
public void dynamicProxy() {
HelloTarget helloTarget = new HelloTarget();
Hello hello = (Hello) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { Hello.class }, // 프록시가 구현할 타겟 인터페이스
new UpperCaseHandler(helloTarget, "say") // 부가기능 구현
);
Assertions.assertEquals("HELLO TOBY", hello.sayHello("Toby"));
Assertions.assertEquals("HI TOBY", hello.sayHi("Toby"));
Assertions.assertEquals("THANK YOU TOBY", hello.sayThankYou("Toby"));
}
Date now = (Date) Class.forName("java.util.Datte").newInstance()'
팩토리빈
@Setter
public class UppercaseProxyFactoryBean implements FactoryBean<Object> { // 범용적으로 사용하기 위해 Object 타입 지정
private Object target;
private Class<?> targetInterface; // 타겟과 타겟의 인터페이스를 함께 설정해야 한다.
private String pattern; // 부가기능에서 필요로 하는 값에 의존한다.
@Override
public Object getObject() throws Exception {
return Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { targetInterface },
new UpperCaseHandler(target, pattern)); // 팩토리빈과 부가기능 객체가 결합된다.
}
@Override
public Class<?> getObjectType() { return targetInterface; }
@Override
public boolean isSingleton() { return false; }
}
팩토리 빈 설정
<bean id="helloImpl" class="study.proxy.target.HelloImpl" />
<bean id="hello" class="study.proxy.factory.UppercaseProxyFactoryBean">
<property name="target" ref="helloImpl" />
<property name="pattern" value="say" />
<property name="targetInterface" value="study.proxy.target.Hello" />
</bean>
팩토리 빈 테스트
public class FactoryBeanTest {
@Autowired
Hello hello;
@Test
public void setupFactoryBean() {
Assertions.assertEquals("HELLO TOBY", hello.sayHello("Toby"));
Assertions.assertEquals("HI TOBY", hello.sayHi("Toby"));
Assertions.assertEquals("THANK YOU TOBY", hello.sayThankYou("Toby"));
}
}
한 번 부가기능을 가진 프록시를 생성하는 팩토리 빈을 만들어두면 타깃의 타입에 상관없이 재사용할 수 있다.
<bean id="helloImpl" class="study.proxy.target.HelloImpl" />
<bean id="hello" class="study.proxy.factory.UppercaseProxyFactoryBean">
<property name="target" ref="helloImpl" />
<property name="pattern" value="say" />
<property name="targetInterface" value="study.proxy.target.Hello" />
</bean>
<bean id="introduceImpl" class="study.proxy.target.IntroduceImpl" />
<bean id="introduce" class="study.proxy.factory.UppercaseProxyFactoryBean">
<property name="target" ref="introduceImpl" />
<property name="pattern" value="introduce" />
<property name="targetInterface" value="study.proxy.target.Introduce" />
</bean>
스프링의 ProxyFactoryBean을 이용한 다이나믹 프록시 테스트
@Test
public void springProxyFactoryBean() {
// 스프링의 ProxyFactoryBean 생성
ProxyFactoryBean factoryBean = new ProxyFactoryBean();
// 팩토리빈에 타겟 설정
factoryBean.setTarget(new HelloImpl());
// 포인트컷과 어디바이스 생성과 설정
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("sayH*");
UppercaseAdvice advice = new UppercaseAdvice();
// 어드바이저 팩토리에 추가
factoryBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, advice));
// 프록시 생성
Hello hello = (Hello) factoryBean.getObject();
Assertions.assertEquals("HELLO TOBY", hello.sayHello("Toby"));
Assertions.assertEquals("HI TOBY", hello.sayHi("Toby"));
Assertions.assertEquals("Thank You Toby", hello.sayThankYou("Toby"));
}
설정파일
<bean id="helloImpl" class="study.proxy.target.HelloImpl" />
<bean id="uppercaseAdvice" class="study.proxy.advice.UppercaseAdvice" />
<bean id="uppercasePointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedName" value="say" />
</bean>
<bean id="uppercaseAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="pointcut" ref="uppercasePointcut" />
<property name="advice" ref="uppercaseAdvice" />
</bean>
<bean id="hello" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="helloImpl" />
<property name="interceptorNames">
<list>
<value>uppercaseAdvisor</value>
</list>
</property>
</bean>
부가기능 적용이 필요한 타겟 객체마다 거의 비슷한 내용의 ProxyFactoryBaen 빈 설정정보가 반복되고 있다.
반복적인 프록시의 메서드 구현을 코드 자동생성 기법을 이용해 해결했다면 반복적인 ProxyFactoryBean 설정 문제는 설정 자동등록 기법으로 해결할 수 없을까? 일정한 타겟 빈의 목록을 제공하면 자동으로 각 타깃 빈에 대한 프록시를 만들어주는 방법이 있다면 ProxyFactoryBean 타입 빈 설정을 매번 추가해서 프록시를 만들어내는 수고를 덜 수 있을 것 같다.
DefaultAdvisorAutoProxyCreator 통한 자동 프록시 생성 설정
<!--Target-->
<bean id="helloImpl" class="study.proxy.target.HelloImpl" />
<!--Advice (부가기능)-->
<bean id="uppercaseAdvice" class="study.proxy.advice.UppercaseAdvice" />
<!--Pointcut (메서드 선정 알고리즘)-->
<bean id="uppercasePointcut" class="org.springframework.aop.aspectj.AspectJExpressionPointcut">
<property name="expression" value="execution(* *..*Impl.*(..))" />
</bean>
<!--자동 프록시 생성자가 참조하는 직접적인 정보-->
<bean id="uppercaseAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="uppercaseAdvice" />
<property name="pointcut" ref="uppercasePointcut" />
</bean>
<!--자동 프록시 생성자-->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
스프링에서는 AOP를 위해 기계적으로 적용하는 빈들(Advice, Pointcut, Advisor)을 간편한 방법으로 등록할 수 있다. 스프링은 AOP와 관련된 태그를 정의해둔 aop 스키마를 제공한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
>
<!--Target-->
<bean id="helloImpl" class="study.proxy.target.HelloImpl" />
<!--Advice (부가기능)-->
<bean id="uppercaseAdvice" class="study.proxy.advice.UppercaseAdvice" />
<!-- aop 네임스페이스 -->
<aop:config>
<aop:advisor advice-ref="uppercaseAdvice" pointcut="execution(* *..*Impl.*(..))" />
</aop:config>
</beans>