스프링 인 액션에 나와 있는 예제를 통해 스프링의 AOP 구현에 대해 알아보기로 하자.
1. 사전 충고
사전 충고는 아래와 같은 MethodBeforeAdvice 인터페이스를 이용하여 만든다.
public interface MethodBeforeAdvice {
void before(Method method, Object[] args, Object target) throws Throwable;
}
이러한 before 메소드를 구현하는 welcomeAdvice라는 클래스를 작성한다. welcomeAdvice는 상점에 손님이 들어오면 반갑게 인사하는 것을 나타내는 클래스인데, 이는 상점에 들어와서 물건을 사기 전에 수행해야 하기 때문에 다른 메소드가 실행되기 전에 호출되어야 한다.
package com.springinaction.chapter03.store;
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
public class WelcomeAdvice implements MethodBeforeAdvice {
public void before(Method arg0, Object[] arg1, Object arg2)
throws Throwable {
Customer customer = (Customer)arg1[0]; // 첫번째 인자 캐스팅
System.out.println("Hello " + customer.getName() + ". How are you doing?");
}
}
작성된 welcomeAdvice 를 가지고 빈에 묶어보자. 빈에 묶는 스프링 설정 파일이 아래에 나타나 있다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
<bean id="kwikEMartTarget"
class="com.springinaction.chapter03.store.ApuKwikEMart" />
<bean id="welcomeAdvice"
class="com.springinaction.chapter03.store.WelcomeAdvice" />
<bean id="kwikEMart"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>com.springinaction.chapter03.store.KwikEMart</value>
</property>
<property name="interceptorNames">
<list>
<value>welcomeAdvice</value>
</list>
</property>
<property name="target">
<ref bean="kwikEMartTarget" />
</property>
</bean>
</beans>
여기에는 ProxyFactoryBean 클래스가 나오는데, 이 클래스는 프록시 생성을 위하여 BeanFactory와 ApplicationContext 객체에 의해 사용된다. ProxyFactoryBean 클래스는 BeanFactory 안에서 프록시화된 객체를 명시적으로 생성시키기 위해 필요한 중심 클래스이다.
스프링 설정파일까지 작성하였으므로, 마지막으로 실제로 예제를 돌려보는 클래스를 작성한다.
package com.springinaction.chapter03.store;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.FileSystemResource;
public class Main {
public static void main(String[] args) throws KwikEMartException {
BeanFactory factory = new XmlBeanFactory(new FileSystemResource("kwikemart.xml"));
KwikEMart mart = (KwikEMart)factory.getBean("kwikEMart");
mart.buySquishee(new Customer());
}
}
실행결과는 아래와 같다.
2. 사후 충고
사후 충고는 사전 충고와 마찬가지로 AfterReturningAdvice라는 인터페이스를 이용하여 만들 수 있다.
public interface AfterReturningAdvice {
public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable;
}
이번에는 welcomeAdvice와는 반대로 손님이 나갈때(메소드 호출이 종료된 후), 잘가라는 인사 메시지를 출력하는 클래스를 작성한다. AfterReturningAdvice 인터페이스를 구현하는 클래스가 되겠다.
package com.springinaction.chapter03.store;
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
public class ThankYouAdvice implements AfterReturningAdvice {
public void afterReturning(Object arg0, Method arg1, Object[] arg2,
Object arg3) throws Throwable {
System.out.println("Thank you. Come again!");
}
}
이 클래스를 사용해 보기 위해 좀 전에 작성했던 스프링 설정 파일을 아래와 같이 수정한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
<bean id="kwikEMartTarget"
class="com.springinaction.chapter03.store.ApuKwikEMart" />
<bean id="thankyouAdvice"
class="com.springinaction.chapter03.store.ThankYouAdvice" />
<bean id="kwikEMart"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>com.springinaction.chapter03.store.KwikEMart</value>
</property>
<property name="interceptorNames">
<list>
<value>thankyouAdvice</value>
</list>
</property>
<property name="target">
<ref bean="kwikEMartTarget" />
</property>
</bean>
</beans>
바뀐 부분은 ThankYouAdvice 빈을 추가하였고, interceptorNames 프로퍼티의 값을 thankyouAdvice로 바꾼 것 밖에 없다.
실행 결과는 아래와 같다.
Thank you. Come again!
3. 주변충고
주변충고는 MethodInterceptor를 통해 하나의 충고 객체로 사전충고와 사후충고를 모두 하는 것을 말한다.
MethodInterceptor 인터페이스는 아래와 같다.
public MethodInterceptor {
public Object invoke(MethodInvocation arg0) throws Throwable;
}
사전충고와 사후충고와는 달리 주변충고(MethodInterceptor) 사이에는 아래와 같은 두 가지 중요한 차이점이 존재한다.
- MethodInterceptor 구현 클래스는 대상 메소드의 실제 호출 여부를 제어할 수 있다.
- MethodInterceptor는 어떤 객체가 리턴될지에 대해 제어할 수 있도록 해준다.
MethodInterceptor를 구현하는 클래스 예제를 보면 아래와 같다.
package com.springinaction.chapter03.store;
import java.util.HashSet;
import java.util.Set;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class OnePerCustomerInterceptor implements MethodInterceptor {
// 이전 고객을 담는 Set 설정
private Set customers = new HashSet();
public Object invoke(MethodInvocation arg0) throws Throwable {
// 현재 고객을 얻어옴.
Customer customer = (Customer)arg0.getArguments()[0];
if (customers.contains(customer)) {
throw new KwikEMartException("One per customer.");
}
Object squishee = arg0.proceed(); // 대상 메소드 호출
customers.add(customer);
return squishee;
}
}
이 역시 동일한 방식으로 스프링 설정 파일에 빈을 추가해 주고, interceptorNames 프로퍼티의 값을 바꿔주면 실헹시킬 수 있다. 중요한 점은 MethodInterceptor는 코드에서 볼 수 있듯이, process() 메소드를 기준으로 대상 메소드가 호출되기 전과 호출된 후의 로직이 모두 존재한다는 것이다.
일부러 예외를 발생시키기 위하여 아래와 같이 메인 메소드가 포함된 클래스를 수정해 본다.
public class Main {
public static void main(String[] args) throws KwikEMartException {
BeanFactory factory = new XmlBeanFactory(new FileSystemResource("kwikemart.xml"));
KwikEMart mart = (KwikEMart)factory.getBean("kwikEMart");
Customer customer = new Customer();
mart.buySquishee(customer);
mart.buySquishee(customer); // 예외가것임…
}
}
실제로 실행을 시키면 아래 그림과 같이 예외가 발생한다.
4. 예외 충고
예외 충고는 ThrowsAdvice 인터페이스를 이용하여 예외가 발생했을 때의 행위를 정의한다. ThrowsAdvice는 이전의 충고 유형과는 달리 마커 인터페이스(marker interface)이므로 구현해야 할 메소드를 가지고 있지는 않지만, 이 인터페이스를 구현하는 클래스는 다음과 같은 두 개의 시그너처 중 적어도 하나의 메소드를 포함해야 한다.
void afterThrowing(Throwable e)
void afterThrowing(Method method, Object[] args, Object target, Throwable throwable)
이러한 ThrowsAdvice를 구현하는 예제 코드를 살펴보면 아래와 같다.
package com.springinaction.chapter03.store;
import org.springframework.aop.ThrowsAdvice;
public class KwikEMartExceptionAdvice implements ThrowsAdvice {
public void afterThrowing(NoMoreSquisheesException e) {
orderMoreSquishees();
}
public void afterThrowing(CustomerIsBrokeException e) {
showCustomerAtmMachine();
}
}
예외가 발생하면 던져진 예외의 타입에 따라 적절한 메소드가 호출될 것이다. 중요한 점은 두 메소드 모두 애플리케이션에 추가적인 행위를 더할 뿐, 예외를 잡아서 처리하지는 않는다는 것이다. 프록시 객체는 예외를 잡아 적절한 ThrowsAdvice의 메소드를 호출할 뿐이며, ThrowsAdvice가 실행된 후에도 원래의 메소드는 여전히 던져진다.