IT/SpringFramework

Spring AOP의 토대가 되는 빈 후처리기를 이용한 프록시 객체 생성

주현태 2019. 11. 12. 14:15

스프링의 주요 기능은 DI, AOP, POJO, 서비스 추상화이다. 이 중 AOP 기능은 자주 사용하지만 AOP의 기반이 되는 기술을 잘 모르는 경우가 많다. 그래서 AOP의 기반기술이 되는 스프링의 빈 후처리기 및 프록시 객체 생성에 대하여 적어보고자 한다.

 

 우리는 보통 소스코드를 작성할 때 목적에 맞게 클래스를 나누고 각 클래스 안에는 특수 목적을 가진 소스코드와 코드와 성능측정, 로그, 트랜잭션등 여러 클래스에 산재해서 많이 쓰이는 부가기능 코드를 작성한다. 그런데 이러한 부가기능이 각 코드에 산재해서 존재할 경우 소스코드의 응집도가 떨어질 수 있는 문제점이 있다. 그래서 이러한 핵심코드및 부가기능의 분리를 위해 프록시 패턴, 데코레이터 패턴등 많은 디자인 패턴이 생겨나게 되었다. 

 

 스프링에서는 이러한 핵심코드 및 부가기능 코드의 분리를 위해 AOP라는 기술을 사용하는데 AOP는 Aspect Oriented Programming 의 약자로 관심사의 분리, 즉 부가기능을 따로 모듈화 하여 응집도를 높이는 것을 목표로 하는 개념이다.

 

Spring에서는 빈 후처리기를 통해 프록시 객체를 생성함으로써 부가기능을 핵심코드에 삽입하는데 여기서 말하는 프록시 객체란 부가기능 및 접근제어 코드가 들어있는 객체를 의미한다. 빈 후처리기로 인해 프록시 객체가 삽입되는 과정은 다음 그림과 같다.

 

 

프록시 객체 처리과정

  1. 빈 설정파일, 어노테이션을 참고하여 빈 오브젝트 생성

  2. 빈 후처리기가 어드바이저(어드바이스 + 포인트컷)을 참조하여 프록시 객체 생성유무 판단

  3. 포인트컷에 포함되어 있는 객체일 경우 프록시 생성기를 통해 프록시 객체 생성

  4. 이용(사용자는 프록시객체인지 일반객체인지 모르고 그냥 사용하면 된다.)

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

 

AspectJ 표현식을 이용한 포인트컷 예제

스프링에서 PointCut을 적용할 때 String의 equal로 여러개의 클래스나 메서드를 포괄하기에는 쉽지 않은 일인데 이러한 것을 해결해 주는 것이 AspectJ 표현식을 이용하는 것이다. [AspectJ 표현식을 사용하지 않..

honeyinfo7.tistory.com

 

 

 

어렴풋이 알고 있었지만 이렇게 정리하고 테스팅 까지 해봄으로써 또 하나 깨닳음을 얻었다는 생각이 든다.

 

 

- 끝 -