본문 바로가기
Back-End/토비의 스프링

토비의 스프링 | 6장. AOP3 - 프록시 팩토리빈과 포인트컷

by Havi 2017. 4. 17.

프록시 팩토리 빈 방식의 장점/한계

장점

데코레이터 패턴과 같은 프록시를 도입하려고 했을 때 고민했던 문제점을 거의 완벽하게 해결해 준다.

  • 다이내믹 프록시를 이용하여 타깃 인터페이스를 구현하는 클래스를 일일이 만들지 않아도 된다.
  • 하나의 핸들러 메소드를 구현하여 부가기능 코드의 중복 문제를 해결한다.
  • 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 타입이기 때문이다.

댓글0