느린 것을 걱정하지 말고, 멈춰서는 것을 걱정하라
article thumbnail

스프링의 주요 기능은 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]

<java />
public interface MyService { public String action(String str); }

 

 

[MyServiceImpl.java]

<java />
public class MyServiceImpl implements MyService { @Override public String action(String str) { str = str + "!"; return str; } }

 

 

[NameMatchePointCut.java]

<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]

<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]

<html />
<?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인 폴더에 접근하도록 해준다.

 

 

<java />
@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

 

 

 

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

 

 

- 끝 -

profile

느린 것을 걱정하지 말고, 멈춰서는 것을 걱정하라

@주현태

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!