프록시 팩토리 빈 방식의 장점/한계
장점
데코레이터 패턴과 같은 프록시를 도입하려고 했을 때 고민했던 문제점을 거의 완벽하게 해결해 준다.
- 다이내믹 프록시를 이용하여 타깃 인터페이스를 구현하는 클래스를 일일이 만들지 않아도 된다.
- 하나의 핸들러 메소드를 구현하여 부가기능 코드의 중복 문제를 해결한다.
- DI 설정만으로 다양한 타깃 오브젝트에 적용 가능하다.
한계
- 하나의 클래스 안에 여러개의 메소드 적용은 가능하지만 여러 개의 클래스에 공통적인 부가기능을 제공하는 일은 불가능하다.
- 하나의 타깃에 여러 개의 부가기능을 적용하려 할 때도 문제이다.
- 트랜잭션, 보안 기능, 기타 부가기능을 담은 프록시를 추가하려 해도 설정 코드는 그만큼 추가로 늘어나는 한계가 생긴다.
- TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 만들어진다.
스프링의 프록시 팩토리 빈
ProxyFactoryBean은 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈이다. TxProxyFacotryBean과 달리, 순수하게 프록시를 생성하는 작업만을 담당하며 프록시를 제공해줄 부가기능은 별도의 빈에 둘 수 있다. 부가기능의 경우 InvocationHandler의 invoke()와 달리, MethodInterceptor를 사용하여 타깃 오브젝트에 대한 정보를 함께 제공한다. 이를 통해 타깃 오브젝트에 상관없이 독립적으로 만들어 싱글톤 빈으로 등록 가능하다.
public class DynamicProxyTest { @Test public void simpleProxy() { Hello proxiedHello = (Hello) Proxy.newProxyInstance( getClass().getClassLoader(), new Class[] { Hello.class }, new UppercaseHandler(new HelloTarget())); ... } @Test public void proxyFactoryBean() { ProxyFactoryBean pfBean = new ProxyFactoryBean(); pfBean.setTarget(new HelloTarget()); //타깃 설정 pfBean.addAdvice(new UppercaseAdvice()); //부가기능 추가 Hello proxiedHello = (Hello) pfBean.getObject(); //FacotryBean이므로 생성된 프록시를 가져온다. assertThat(ProxiedHello.sayHello("Havi"), is("Hello Havi")); ... } static class UppercaseAdvice implements MethodInterceptor { public Object invoke(MethodInvocation invocation) throws Throwable { String ret = (String)invocation.proceed(); //타깃을 알고 있기에 타깃 오브젝트를 전달할 필요가 없다. return ret.toUpperCase(); //부가기능 적용 } } static interface Hello { String sayHello(String name); String sayHi(String name); String sayThankYor(String name); } static class HelloTarget implements Hello { public String sayHello(String name) { return "Hello" + name; } ... } }
어드바이스: 타깃이 필요 없는 순수한 부가기능
MethodInvocation은 일종의 콜백 오브젝트로, proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해주는 기능이 있다. ProxyFactoryBean은 작은 단위의 템플릿/콜백 구조를 응용하여 적용하였기에 템플릿 역할을 하는 MethodInvocation을 싱글톤으로 두고 공유할 수 있다.
어드바이스(advice)는 MethodInvocation처럼 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트이다. ProxyFactoryBean은 타깃 오브젝트가 구현하고 있는 모든 인터페이스를 동일하게 구현하는 프록시를 만들어 준다. 그래서 따로 인터페이스 타입을 제공받지 않아도 Hello 인터페이스를 구현한 프록시를 만들 수 있다.
포인트컷: 부가기능 적용 대상 메소드 선정 방법
MethodInterceptor는 타깃에 대한 정보를 들고 있지 않기에 싱글톤 빈으로 등록할 수 있었다. 따라서 확장성까지 고려하면 적용 대상 메소드를 선정하는 로직은 분리하는 것이 옳바르다.
- 부가기능(Advice), 메소드 선정 알고리즘(pointcut)을 활용하는 유연한 구조를 제공한다.
- 어드바이스와 포인트컷은 모두 프록시에 DI로 주입돼서 사용된다.
- 어드바이스
- 타깃 메소드를 직접 호출하는 것은 프록시가 메소드 호출에 따라 만드는 Invocation 콜백의 역할이다.
- 재사용 가능한 기능을 만들어 두고 바뀌는 부분(콜백 오브젝트, 메소드 호출정보)만 외부에서 주입하는 전형적인 템플릿/콜백 구조이다.
- 어드바이스가 일종의 템플릿이 되고 MethodInvocation 오브젝트가 콜백이 되는 것이다.
- 그러므로 어드바이스는 독립적인 싱글톤 빈으로 등록하고 DI를 주입해서 여러 프록시가 사용할 수 있다.
@Test public void pointcutAdvisor() { ProxyFactoryBean pfBean = new ProxyFactoryBean(); pfBean.setTarget(new HelloTarget()); NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); //메소드이름을 비교대상으로 선정하는 포인트컷 생성 pointcut.setMappedName("sayH*"); //이름 비교조건 설정(sayH로 시작하는 모든 메소드 선택) pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice())); //포인트컷과 어드바이스를 Advisor로 묶어서 한 번에 추가 Hello proxiedHello = (Hello) pfBean.getObject(); assertThat(ProxiedHello.sayHello("Havi"), is("Hello Havi")); assertThat(ProxiedHello.sayThankYou("Havi"), is("Thank You Havi")); //적용 안됨 }
- NameMatchMethodPointcut을 사용하여 간단하게 포인트컷을 생성할 수 있다.
- 포인트컷과 어드바이스를 묶어서 addAdvisor로 등록하는 이유는 여러 개를 등록해야 할 경우를 위해서 이다.
어드바이저 = 포인트컷(메소드 선정 알고리즘) + 어드바이스(부가기능)
ProxyFactoryBean 적용
이전에 만든 TransactionHandler에서 타깃과 메소드 선정 부분을 제거해 주면 된다.
public class TransactionAdvice implements MethodInterceptor { //스프링 어드바이스 인터페이스 구현 PlatformTransactionManager transactionManager; public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } public Object invoke(MethodInvocation invocation) throws Throwable { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { Object ret = invocation.proceed(); //타깃의 메소드 실행. 호출 전후로 부가기능을 추가할 수 있다. this.transactionManager.commit(status); return ret; } catch(RuntimeException e) { this.transactionManager.rollback(status); throw e; } } }
6.5 스프링 AOP
이제까지 반복되는 프록시의 메소드 구현을 코드 자동생성 기법을 이용해 해결했다면 반복적인 ProxyFactoryBean 설정 문제도 자동등록하는 기법이 없을까? 또는 실제 빈 오브젝트가 되는 것은 ProxyFactoryBean을 통해 생성되는 프록시 그 자체이므로 프록시가 자동으로 빈으로 생성할 수 없을까?
빈 후처리기를 이용한 자동 프록시 생성기
스프링은 컨테이너로서 제공되는 기능 중에서 변하지 않는 핵심적인 부분 외에는 대부분 확장가능 하도록 해준다. 그 중 중요한 확장 포인트는 BeanPostProcessor 인터페이스를 구현해서 만드는 빈 후처리기이다. 그 중 하나인 DefaultAdvisorAutoProxyCreator는 어드바이저를 이용한 자동 프록시 생성기이다. 빈 후처리기의 등록은 간단하게 빈으로 등록하기만 하면 된다.
- 빈 후처리기는 빈 오브젝트의 프로퍼티를 강제로 수정 가능하다.
- 별도의 초기화 작업도 가능하다.
- 심지어 만들어진 빈 오브젝트를 바꿔치기 할 수도 있다.
- 즉, 스프링이 설정을 참고해서 만든 오브젝트가 아닌 다른 오브젝트를 빈으로 등록시키는 것이 가능하다.
- DefaultAdvisorAutoProxyCreator는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 프록시 적용 대상인지 확인한다.
- 빈 후처리기는 프록시가 생성되면 원래 컨테이너가 전달해준 빈 오브젝트 대신 프록시 오브젝트를 반환한다.
확장된 포인트컷
이제까지의 포인트컷은 다깃 오브젝트의 메소드 중 선정된 메소드를 선별하는 역할을 해 왔다. 여기에 또 하나의 기능인 등록된 빈 중에서 어떤 빈에 프록시를 적용할지를 선택할 수 있다. 포인트컷은 ClassFilter, MethodMatcher를 돌려주는 메소드를 갖고 있다. 이제까지는 메소드 선정 기능만 필요하였지만 DefaultAdvisorAutoProxyCreator는 클래스와 메소드 선정 알고리즘 모두가 필요하다.(두 가지 모두 충족해야 어드바이스가 적용되는 것)
public interface Pointcut { ClassFilter getClassFilter(); //프록시를 적용할 클래스인지 확인 MethodMatcher getMethodMatcher(); //어드바이스를 적용할 메소드인지 확인 }
포인트컷 테스트 ~ 자동 프록시 생성 ~ 확인
책에서는 포인트컷과 어드바이저를 일일이 명시해서 사용한 뒤 이를 더 간편하게 작성할 수 있게끔 해주는 자동 프록시 생성 방법을 익힌다. 후에 다음의 구문으로 자동 생성된 프록시인지 확인한다.
@Test public void advisorAutoProxyCreator() { asserThat(testUserService, is(java.lang.reflect.Proxy.class)); //프록시로 변경된 오브젝트인지 확인 }
자세한 내용은 책을 통해서 확인!(처음의 원시적인 코드에서 자동생성까지의 흐름위주로 파악!)
포인트컷 표현식을 이용한 포인트컷
스프링은 포인트컷의 클래스와 메소드를 아주 효율적으로 선정할 수 있는 알고리즘을 제공한다. JSP나 정규식의 비슷한 일종의 표현식 언어를 사용하는데 이와 비슷하여 포인트컷 표현식이라 부른다. 이를 적용하려면 AspectJExpressionPointcut 클래스를 사용하면 된다. AspectJ라는 유명한 프레임워크에서 제공하는 문법을 확장해서 사용 가능하다.
execution([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴 (타입패턴 | "..", ...) [throws 예외 패턴])
- 접근제한자 패턴 - public, private 같은 접근제한자. 생략가능
- 타입패턴 - 리턴값의 타입 패턴
- 타입패턴(2번째) - 패키지와 클래스 이름에 대한 패턴. 생략가능
- 이름패턴 - 메소드 이름 패턴
- (타입패턴 | "..", ...) - 파라미터의 타입 패턴을 순서대로 넣을 수 있다. 와일드카드를 이용해 개수에 상관없는 패턴 생성도 가능
System.out.println(Target.class.getMehod("minus", int.class, int.class)); //public int springbook.learningtest.spring.pointcut.Target.minus(int, int) throws java.lang.RuntimeException
- int - 리턴 값의 타입을 나타내는 패턴이다. 필수항목이며
*
를 써서 모든 타입을 선택해도 된다. - springbook.learningtest.spring.pointcut.Target
- 패키지와 타입 이름을 포함한 클래스의 타입 패턴이다.
- 생략하면 도든 타입 허용
*
사용가능,'..'
를 사용하면 한 번에 여러 패키지 선택 가능
- minus - 메소드 이름 패턴. 메소드를 다 선택할 시
*
- (int,int) - 메소드 파라미터의 타입 패턴. 상관없이 모두 허용하고 싶으면
'..
@Test public void methodSignaturePointcut() throws SecurityException, NoSuchMethodException { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression("excution(pulic int" + "springbook.learningtest.spring.pointcut.Target.minus(int, int)" + "throws java.lang.RuntimeException)"); ... }
기타 적용되는 다양하게 적용되는 예제 케이스는 책 및 구글링 참고
포인트컷 표현식을 이용하는 포인트컷 적용
포인트컷 표현식은 execution() 이외에도 몇 가지 스타일이 더 있다. bean(*Service)라고 쓰면 아이디가 Service로 끝나는 모든 빈을 선택한다. 또 특정 어노테이션이 타입, 메소드, 파라미터에 적용되어 있는 것을 보고 메소드를 선정하게 하는 포인트컷도 만들 수 있다. 아래와 같이 쓰면 @Transactional이라는 어노테이션이 적용된 메소드를 선정하게 해준다.
@annotaion(opg.springfameework.transaction.annotation.Transactional)
이전에 Bean으로 생성하여 methodClassName, mappedName을 선정했던 부분들은
execution(* *..*ServiceImpl.upgrade*(..)**)
로 간단히 설정할 수 있다.
타입 패턴과 클래스 이름 패턴
이전에 TestUserServiceImpl와 UserSerivceImpl 클래스 두 개의 빈이 선정되고 테스트는 성공했다. 포인트컷 표현식으로 적용하고 다시 테스트 해 봐도 두 개 모두 성공한다. execution(* *..*ServiceImpl.upgrade*(..)**)
로 등록되어 있는데 어떻게 성공한 것인가? 이유는 포인트컷 표현식의 클래스 이름에 적용되는 패턴은 클래스 이름 패턴이 아니라 타입 패턴이기 때문이다. 이름은 TestUserServiceImpl이지만 타입은 UserServiceImpl 타입이기 때문이다.
'Back-End > 토비의 스프링' 카테고리의 다른 글
토비의 스프링 | 6장. AOP4 - AOP란 무엇인가? (0) | 2017.05.02 |
---|---|
토비의 스프링 | 6장. AOP2 - 다이내믹 프록시 (0) | 2017.04.07 |
토비의 스프링 | 6장. AOP (0) | 2017.04.05 |
토비의 스프링 | 3장. 템플릿2 - 응용 (0) | 2017.03.03 |
토비의 스프링 | 3장. 템플릿 (0) | 2017.01.27 |
댓글