Spring AOP의 토대가 되는 빈 후처리기를 이용한 프록시 객체 생성
스프링의 주요 기능은 DI, AOP, POJO, 서비스 추상화이다. 이 중 AOP 기능은 자주 사용하지만 AOP의 기반이 되는 기술을 잘 모르는 경우가 많다. 그래서 AOP의 기반기술이 되는 스프링의 빈 후처리기 및 프록시 객체 생성에 대하여 적어보고자 한다.
우리는 보통 소스코드를 작성할 때 목적에 맞게 클래스를 나누고 각 클래스 안에는 특수 목적을 가진 소스코드와 코드와 성능측정, 로그, 트랜잭션등 여러 클래스에 산재해서 많이 쓰이는 부가기능 코드를 작성한다. 그런데 이러한 부가기능이 각 코드에 산재해서 존재할 경우 소스코드의 응집도가 떨어질 수 있는 문제점이 있다. 그래서 이러한 핵심코드및 부가기능의 분리를 위해 프록시 패턴, 데코레이터 패턴등 많은 디자인 패턴이 생겨나게 되었다.
스프링에서는 이러한 핵심코드 및 부가기능 코드의 분리를 위해 AOP라는 기술을 사용하는데 AOP는 Aspect Oriented Programming 의 약자로 관심사의 분리, 즉 부가기능을 따로 모듈화 하여 응집도를 높이는 것을 목표로 하는 개념이다.
Spring에서는 빈 후처리기를 통해 프록시 객체를 생성함으로써 부가기능을 핵심코드에 삽입하는데 여기서 말하는 프록시 객체란 부가기능 및 접근제어 코드가 들어있는 객체를 의미한다. 빈 후처리기로 인해 프록시 객체가 삽입되는 과정은 다음 그림과 같다.
프록시 객체 처리과정
-
빈 설정파일, 어노테이션을 참고하여 빈 오브젝트 생성
-
빈 후처리기가 어드바이저(어드바이스 + 포인트컷)을 참조하여 프록시 객체 생성유무 판단
-
포인트컷에 포함되어 있는 객체일 경우 프록시 생성기를 통해 프록시 객체 생성
-
이용(사용자는 프록시객체인지 일반객체인지 모르고 그냥 사용하면 된다.)
ref )
어드바이스 : 부가기능 객체
포인트컷 : 프록시객체가 적용될 메서드 구분에 사용되는 객체
그리고 위와 같이 빈 후처리기에 의해 만들어진 프록시 객체는 다음과 같이 사용이 되게 된다.
자, 그러면 소스코드를 통해 자동으로 프록시 객체가 생성되는지 확인해 보고자 한다.
생성하고자 하는 빈은 다음과 같다.
MyService : 핵심코드가 구현 될 Interface
MyServiceImpl : 핵심코드가 담긴 클래스
NamedMatchPointCut : 프록시객체를 만들지 여부를 결정하는 클래스
TransactionAdvice : 부가기능이 담긴 클래스
org.springframework.aop.support.DefaultPointcutAdvisor : NamedMatchPointCut + TransactionAdvice
org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator: 스프링에서 제공하는 빈 후처리기
[MyService.java]
public interface MyService {
public String action(String str);
}
[MyServiceImpl.java]
public class MyServiceImpl implements MyService {
@Override
public String action(String str) {
str = str + "!";
return str;
}
}
[NameMatchePointCut.java]
public class NamedMatchPointCut extends NameMatchMethodPointcut {
public void setMappedClassName(String mappedClassName) {
this.setClassFilter(new SimpleClassFilter(mappedClassName));
}
static class SimpleClassFilter implements ClassFilter {
String mappedName;
private SimpleClassFilter(String mappedName) {
this.mappedName = mappedName;
}
@Override
public boolean matches(Class<?> clazz) {
return PatternMatchUtils.simpleMatch(mappedName, clazz.getSimpleName());
}
}
}
[TransactionAdvice.java]
public class TransactionAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
System.out.println("부가기능 1");
//핵심 코드
Object ret = invocation.proceed();
System.out.println("부가기능 2");
return ret;
} catch (Exception e) {
throw e;
}
}
}
[context-beans.xml]
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="myService"
class="com.copocalypse.web.service.MyServiceImpl"></bean>
<!-- 포인트컷 -->
<bean id="transactionPointcut"
class="com.copocalypse.web.tx.NamedMatchPointCut">
<property name="mappedClassName" value="*ServiceImpl" />
<property name="mappedName" value="action" />
</bean>
<!-- 어드바이스 -->
<bean id="transactionAdvice" class="com.copocalypse.web.tx.TransactionAdvice">
</bean>
<!-- 어드바이저 = 포인트컷 + 어드바이스 -->
<bean id="transactionAdvisor"
class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="transactionAdvice" />
<property name="pointcut" ref="transactionPointcut" />
</bean>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
</beans>
위와 같이 빈을 등록하고 테스트 코드를 만들어 보았다. GenericXmlApplicationContext를 이용하여 xml 파일로 부터 bean을 가져오려고 한다. context-beans.xml 파일은 src/main/resources로 부터 가져오려고 하는데 "classpath:" 접두사를 써서 해당 파일에 접근하려고 한다. 프로젝트명 우클릭 > Properties > Deployment Assembly를 보면 Deploy path가 어떤지 볼 수 있는데 classpath: 접두어의 경우 Deploy path가 WEB-INF/classes인 폴더에 접근하도록 해준다.
@RunWith(JUnit4ClassRunner.class)
@ContextConfiguration
public class TestMyService {
@Autowired
ApplicationContext context;
@Before
public void setUp() {
context=new GenericXmlApplicationContext("classpath:config/context-beans.xml");
}
@Test
public void actionTest() {
MyService myService=(MyService) context.getBean("myService");
String result=myService.action("현태야");
assertThat(result, is("현태야!"));
assertTrue(java.lang.reflect.Proxy.isProxyClass(myService.getClass()));
}
}
TestMyService.java
결과 - console
위와같이 테스트코드를 작성하고 실행해보면 테스트가 정상적으로 수행되고 부가 기능들 또한 제대로 동작하고 있는 것을 확인함으로써 빈 후처리기로 인해 프록시 객체가 자동으로 생성되는 것을 확인할 수 있다. 하지만 위의 예제에서 처럼 Pointcut을 작성하면 각 클래스마다 Pointcut을 만들어야 하므로 불편함 점이 많다. 그래서 그에대한 해결책으로 AspectJ 표현식을 사용하는 방법이 있는데 아래의 링크를 참고한다.
https://honeyinfo7.tistory.com/105
어렴풋이 알고 있었지만 이렇게 정리하고 테스팅 까지 해봄으로써 또 하나 깨닳음을 얻었다는 생각이 든다.
- 끝 -