Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 부록 B. 고전적인 Spring AOP 사용방법

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



부록 B. 고전적인 Spring AOP 사용방법

이번 부록에서는 저수준 스프링 AOP API와 스프링 1.2 애플리케이션에서 사용하는 AOP 지원을 설명한다. 새로운 애플리케이션에서는 AOP장에서 설명한 스프링 2.0 AOP를 사용하길 권장하지만, 기존 애플리케이션에서 작업하거나 책, 기사를 읽는 경우 스프링 1.2 방식의 예제를 볼 수도 있다. 스프링 2.0은 스프링 1.2와 하위 호환성을 완전히 유지하고 있고 이번 부록에서 설명하는 모든 내용은 스프링 2.0에서도 지원한다.

B.1 스프링의 포인트컷(Pointcut) API

스프링이 결정적인 포인트컷의 개념을 어떻게 다루는지 살펴보자.

B.1.1 개념

스프링의 포인트컷 모델은 어드바이스 타입과 독립적으로 포인트컷을 재사용할 수 있게 한다. 그래서 다른 어드바이스를 대상으로 같은 포인트컷을 사용할 수 있다.

org.springframework.aop.Pointcut 인터페이스가 특정 클래스와 메서드를 대상으로 하는 어드바이스에 사용하는 핵심 인터페이스이다. 아래 전체 인터페이스가 나와 있다.

public interface Pointcut {
  ClassFilter getClassFilter();
  MethodMatcher getMethodMatcher();
}

Pointcut 인터페이스를 두 부분으로 분리해서 클래스와 메서드에 걸맞는 부분을 재사용할 수 있고 세밀한 구성(composition) 작업을 할 수 있다.(다른 메서드 매처(matcher)와 함께 "union"을 수행하는 등)

포인트컷을 주어진 대상 클래스의 세트로 제한할 때 ClassFilter 인터페이스를 사용한다. matches() 메서드가 항상 true를 반환하면 모든 클래스에 매칭될 것이다.

public interface ClassFilter {
  boolean matches(Class clazz);
}

MethodMatcher가 보통은 더 중요하다. 전체 인터페이스가 아래 나와 있다.

public interface MethodMatcher {
  boolean matches(Method m, Class targetClass);
  boolean isRuntime();
  boolean matches(Method m, Class targetClass, Object[] args);
}

이 포잇트컷이 대상 클래스의 해당 메서드에 매칭될 것인지를 테스트할 때 matches(Method, Class) 메서드를 사용한다. 메서드 호출시마다 테스트가 필요한 경우를 피하려고 AOP 프락시를 생성할 때 이 평가를 수행할 수 있다. 2개의 인자를 가진 matches 메서드는 해당 메서드에 true를 반환하고 MethodMatcher의 isRuntime() 메서드가 true를 반환한다면 3개의 인자를 가진 matches 메서드는 메서드 호출시마다 호출될 것이다. 대상 어드바이스가 실행되기 전에 메서드 호출에 전달된 인자를 바로 보고 포인트컷을 활성화한다.

대부분의 MethodMatcher는 정적이라서 MethodMatcher의 isRuntime() 메서드는 false를 반환한다. 이 경우 3개의 인자를 가진 matches 메서드는 절대 호출되지 않는다.

Tip
가능하다면 포인트컷을 정적으로 만들어서 AOP 프레임워크가 AOP 프락시를 생성할 때 포인트컷 평가 결과를 캐시하도록 해라.


B.1.2 포인트컷에서의 작업(operation)

스프링은 포인트컷에서 작업(operation)을 지원한다. 특히 union과 intersection가 있다.

  • Union은 어느 한쪽의 포인트컷에 매칭되는 메서드를 의미한다.
  • Intersection는 두 포인트컷에 모두 매칭되는 메서드를 의미한다.
  • 보통은 Union이 더 유용하다.
  • org.springframework.aop.support.Pointcuts 클래스의 정적 메서드나 같은 패키지의 ComposablePointcut 클래스를 사용해서 포인트컷을 구성할 수 있다. 하지만 AspectJ를 사용한다면 포인트컷 표현식이 보통은 더 간단해질 것이다.

B.1.3 AspectJ 표현식 포인트컷

2.0부터는 스프링이 사용하는 포인트컷 중에 가장 중요한 타입은 org.springframework.aop.aspectj.AspectJExpressionPointcut이다. 이는 AspectJ 포인트컷 표현식 문자열을 파싱하려고 AspectJ가 제공하는 라이브러리를 사용하는 포인트컷이다.

지원하는 AspectJ 포인트컷에 대한 얘기는 이전 장을 참고해라.

B.1.4 편리한 포인트컷 구현체

스프링은 편리한 포인트컷 구현체를 여러 가지 제공한다. 일부는 독창적으로 사용할 수 있고 일부는 애플리케이션에 특화된 포인트컷의 하위클래스로 만들어졌다.

B.1.4.1 정적(Static) 포인트컷

정적 포인트컷은 메서드와 대상 클래스에 기반을 두고 있어서 메서드의 인자를 고려할 수 없다. 정적 포인트컷은 대부분의 경우에 만족스럽다.(가장 좋은 방법이기도 하다) 스프링이 메서드를 처음 호출할 때 정적 포인트컷을 딱 한 번만 평가하고 이후에는 메서드 호출하더라도 포인트컷을 다시 평가하지 않도록 할 수 있다.

스프링에 포함된 정적 포인트컷 구현체를 보자.

정규 표현식 포인트컷

정적 포인트컷을 지정하는 명확한 방법의 하나는 정규표현식을 사용하는 것이다. 스프링 외의 여러 AOP 프레임워크도 정규표현식을 지원한다. org.springframework.aop.support.Perl5RegexpMethodPointcut은 일반적인 정규표현식 포인트컷으로 Perl 5 정규표현식 문법을 사용한다. Perl5RegexpMethodPointcut 클래스는 정규표현식 매칭을 할 때 Jakarta ORO에 의존한다. 스프링은 JDK 1.4x에서 지원하는 정규표현식을 사용하는 JdkRegexpMethodPointcut 클래스도 제공한다.

Perl5RegexpMethodPointcut 클래스를 사용해서 패턴 문자열의 목록을 제공할 수 있다. 이 중에서 일치하는 패턴이 있다면 포인트컷은 true로 평가할 것이다.(그래서 결과는 이러한 포인트컷을 효과적으로 합친 결과이다.)

사용방법은 아래 나와 있다.

<bean id="settersAndAbsquatulatePointcut"
  class="org.springframework.aop.support.Perl5RegexpMethodPointcut">
  <property name="patterns">
    <list>
      <value>.*set.*</value>
      <value>.*absquatulate</value>
    </list>
  </property>
</bean>

스프링은 Advice도 참조할 수 있도록(Advice는 interceptor, before advice, throws advice가 될 수 있다는 점을 기억해라) RegexpMethodPointcutAdvisor라는 편리한 클래스를 제공한다. 내부에서 스프링은 JdkRegexpMethodPointcut를 사용할 것이다. RegexpMethodPointcutAdvisor로 아래 나와 있는 대로 포인트컷과 어브다이스를 모두 하나의 빈으로 캡슐화해서 연결을 간소화한다.

<bean id="settersAndAbsquatulateAdvisor"
  class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
  <property name="advice">
    <ref local="beanNameOfAopAllianceInterceptor"/>
  </property>
  <property name="patterns">
    <list>
      <value>.*set.*</value>
      <value>.*absquatulate</value>
    </list>
  </property>
</bean>

RegexpMethodPointcutAdvisor는 어떤 Advice 종류와도 사용할 수 있다.

Attribute 주도 포인트컷

정적 포인트컷의 중요한 종류는 메타데이터 주도(metadata-driven) 포인트컷이다. 이는 메타데이터 속성(attribute)의 값(보통은 소스 수준의 메타데이터)을 사용한다.

B.1.4.2 동적 포인트컷

동적 포인트컷은 정적 포인트컷보다 평가하는데 더 큰 비용이 들고 메서드의 인자를 정적인 정보처럼 고려할 수 있다. 그래서 메서드 호출시마다 평가해야 한다. 인자가 다양하므로 결과는 캐시할 수 없다.

대부분의 예제는 control flow 포인트컷이다.

흐름 제어(Control flow) 포인트컷

스프링 흐름 제어 포인트컷은 AspectJ의 cflow 포인트컷보다 덜 강력하지만, 개념적으로는 유사하다. (다른 포인트컷으로 매칭된 조인 포인트 하에서 포인트컷을 실행하도록 하는 방법은 현재 없다.) 흐름 제어 포인트컷은 현재 콜스택(call stack)에서 매칭한다. 예를 들어 com.mycompany.web 패키지나 SomeCaller 클래스에서 메서드가 조인포인트를 호출했다면 흐름 제어 포인트컷이 호출된다. 흐름 제어 포인트컷은 org.springframework.aop.support.ControlFlowPointcut 클래스로 지정한다.

Note
흐름 제어 포인트컷은 다른 동적 포인트컷보다도 런타임 실행 비용이 상당히 더 크다. Java 1.4에서 다른 동적 포인트컷보다 5배 정도의 비용이 더 든다.


B.1.5 포인트컷 슈퍼클래스

스프링은 자신만의 포인트컷을 구현할 수 있도록 유용한 포인트컷 슈퍼클래스를 제공한다.

정적 포인트컷이 가장 유용하므로 다음과 같이 StaticMethodMatcherPointcut의 하위 클래스를 만들 것이다. StaticMethodMatcherPointcut를 구현할 때는 딱 하나의 추상 메서드만 구현하면 된다.(동작을 수정하려고 다른 메서드를 오버라이드하는 것도 가능하지만)

class TestStaticPointcut extends StaticMethodMatcherPointcut {
  public boolean matches(Method m, Class targetClass) {
    // 커스텀 criteria가 일치한다면 true를 반환한다
  }
}

동적 포인트컷의 슈퍼클래스도 있다.

스프링 1.0 RC2 이상의 모든 어드바이스 타입을 사용해서 커스텀 포인트컷을 사용할 수 있다.

B.1.6 커스텀 포인트컷

스프링 AOP의 포인트컷이 언어 기능(AspectJ처럼)이 아니라 자바 클래스이므로 정적이든 동적이든 커스텀 포인트컷을 정의할 수 있다. 스프링의 커스텀 포인트컷은 상황에 따라 꽤 복잡해 질 수 있다. 하지만 가능하다면 AspectJ 포인트컷 표현식 언어를 사용하기를 권장한다.

Note
스프링의 차기 버전에서는 예를 들면 "대상 객체의 인스턴스 변수를 바꾸는 모든 메서드" 처럼 JAC가 제공하는 "세만틱 포인트컷(semantic pointcuts)"을 지원할 것이다.


B.2 스프링의 어드바이스(Advice) API

스프링 AOP가 어드바이스를 다루는 방법을 살펴보자.

B.2.1 어드바이스 생명주기

각 어드바이스는 스프링 빈(bean)이다. 어드바이스 인스턴스를 모든 어드바이스된 객체 사이에 공유할 수도 있고 각 어드바이스된 객체 내에서 유일할 수도 있다. 이를 각각 per-class와 per-instance 어드바이스라고 한다.

Per-class 어드바이스를 가장 많이 사용한다. 이는 트랜잭션 어드바이저같은 일반적인 어드바이스에 적절하다. 프락시 된 객체의 상태나 새로운 상태 추가에 따라 동작하지 않고 메서드와 인자에 따라 동작한다.

Per-instance 어드바이스는 믹스인을 지원하는 인트로덕션(introduction)에 적절하다. 이 경우에 어드바이스는 프락시 된 객체에 상태를 추가한다.

같은 AOP 프락시에서 공유된 어드바이스와 per-instance 어드바이스를 섞어서 사용할 수 있다.

B.2.2 스프링의 어드바이스 타입

스프링은 다양한 어드바이스 타입을 제공하고 임의의 어드바이스 타입을 지원하도록 확장도 가능하다. 표준 어드바이스 타입과 기본 개념을 살펴보자.

B.2.2.1 Interception around advice

스프링에서 가장 기본적인 어드바이스 타입은 interception around advice이다.

스프링은 메서드 인터셉션을 사용해서 around advice의 AOP Alliance interface와 호환성이 있다. around advice를 구현한 MethodInterceptor는 다음 인터페이스를 구현해야 한다.

public interface MethodInterceptor extends Interceptor {
  Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke() 메서드의 MethodInvocation 인자는 호출되는 메서드, 대상 조인 포인트, AOP 프록시, 메서드의 인자를 노출한다. invoke() 메서드는 호출의 결과(조인포인트(join point)의 반환값)를 반환해야 한다.

간단한 MethodInterceptor 구현체는 다음과 같다.

public class DebugInterceptor implements MethodInterceptor {
  public Object invoke(MethodInvocation invocation) throws Throwable {
    System.out.println("Before: invocation=[" + invocation + "]");
    Object rval = invocation.proceed();
    System.out.println("Invocation returned");
    return rval;
  }
}

MethodInvocation의 proceed() 메서드의 호출을 봐라. 이는 인터셉터 체인을 따라 조인포인트로 진행한다. 대부분의 인터셉터는 이 메서드를 호출하고 그 반환 값을 반환할 것이다. 하지만 다른 around advice처럼 MethodInterceptor는 다른 값을 반환하거나 proceed 메서드를 호출하지 않고 예외를 던질 수 있다. 그렇지만 그럴듯한 이유가 없다면 이렇게 할 이유가 없다!

Note
MethodInterceptor는 다른 AOP Alliance 호환 AOP 구현체와 상호운용성이 있다. 이번 장의 뒷부분에 나오는 어드바이스 타입들은 공통의 AOP 개념을 구현했지만, 스프링에 특화된 방법으로 구현했다. 가장 구체적인 어드바이스 타입을 사용하는 이점을 누리면서 다른 AOP 프레임워크의 관점을 사용하고자 한다면 MethodInterceptor around advice를 사용해라. 포인트컷은 현재 프레임워크간의 상호운용성이 없고 AOP Alliance는 포인트컷 인터페이스를 정의하지 않았다.


B.2.2.2 Before advice

더 간단한 어드바이스 타입은 before advice다. 이는 메서드에 진입하기 전에만 호출되므로 MethodInvocation 객체가 필요없다.

before advice의 주요 이점은 proceed() 메서드를 호출할 필요가 없으므로 인터셉터 체인을 따라가다가 실패할 가능성이 없다는 점이다.

MethodBeforeAdvice 인터페이스가 아래 나와 있다. (일반적인 객체가 필드 가로채기를 할 수 있고 스프링이 이를 구현할 것 같지는 않지만, 스프링 API 디자인은 어드바이스 이전에 필드를 허용한다.)

public interface MethodBeforeAdvice extends BeforeAdvice {
  void before(Method m, Object[] args, Object target) throws Throwable;
}

반환타입은 void이다. before advice는 조인포인트를 실행하기 전에 임의의 동작을 추가할 수 있지만 반환 값은 바꿀 수 없다. before advice가 예외를 던지면 인터셉터 체인을 추가로 실행하지 않는다. 예외는 인터셉터 체인을 거꾸로 따라 전파될 것이다. 예외가 unchecked 예외이면(아니면 호출된 메서드의 시그니처에서) 클라이언트에게 직접 전달될 것이다. 그렇지 않으면 unchecked 예외에서 AOP 프락시가 감쌀 것이다.

다음은 스프링에서 모든 메서드의 호출 횟수를 세는 before advice 예시이다.

public class CountingBeforeAdvice implements MethodBeforeAdvice {
  private int count;
  public void before(Method m, Object[] args, Object target) throws Throwable {
    ++count;
  }
  public int getCount() {
    return count;
  }
}
Tip
before advice를 어떤 포인트컷과도 사용할 수 있다.


B.2.2.3 Throws advice

Throws advice는 조인 포인트가 예외를 던지면 조인포인트의 반환 후에 호출된다. 스프링은 타입이 있는 throws advice를 제공한다. 이는 org.springframework.aop.ThrowsAdvice 인터페이스가 아무런 메서드도 가지지 않는 다는 것을 의미한다. 주어진 객체가 하나 이상의 타입이 있는 throws advice 메서드를 구현했는지 구별하는 태그 인터페이스이다. 이는 다음과 같은 형식이 되어야 한다.

afterThrowing([Method, args, target], subclassOfThrowable) 

마지막 인자만 필수값이다. 어드바이스 메서드가 메서드와 인자에 관심있냐에 따라 메서드 시그니처는 한 인자나 네 개의 인자를 가질 수 있다. 다음 클래스는 throws advice의 예시이다.

RemoteException가 던져질 때(하위 클래스를 포함해서) 아래 어드바이스를 호출한다.

public class RemoteThrowsAdvice implements ThrowsAdvice {
  public void afterThrowing(RemoteException ex) throws Throwable {
    // 원격 예외로 어떤 작업을 한다
  }
}

ServletException가 던져질 때 다음 어드바이스를 호출한다. 앞의 어드바이스와는 달리 4개의 인자를 선언했으므로 호출한 메서드, 메서드 인자, 대상 객체에 접근한다.

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
  public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
    // 모든 인자로 어떤 작업을 한다
  }
}

마지막 예제에서 RemoteException와 ServletException를 모두 처리하는 한 클래스에서 이 두 메서드를 어떻게 사용할 수 있는지 설명한다. 하나의 클래스에 다수의 throws advice 메서드를 합칠 수 있다.

public static class CombinedThrowsAdvice implements ThrowsAdvice {
  public void afterThrowing(RemoteException ex) throws Throwable {
    // 원격 예외로 어떤 작업을 한다
  }

  public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
    // 모든 인자로 어떤 작업을 한다
  }
}

Note: throws advice 메서드가 예외를 던진다면 원래의 예외를 오버라이드할 것이다.(예: 사용자에게 던지는 예외를 바꾼다.) 보통 오버라이딩된 예외는 RuntimeException가 될 것이고 이는 어떤 메서드 시그니처와도 호환성을 가진다. 하지만 throws advice 메서드가 체크드 예외를 던진다면 대상 메서드에서 선언된 예외와 일치해야 하므로 특정 대상 메서드 시그니처와 커플링이 생길 것이다. 대산 메서드 시그니처와 호환되지 않고 선언되지 않은 체크드 예외를 던지지 마라!

Tip
throws advice는 어떤 포인트컷과도 사용할 수 있다.


B.2.2.4 After Returning advice

스프링에서 after returning advice는 아래 나와 있는 것처럼 org.springframework.aop.AfterReturningAdvice 인터페이스를 구현해야 한다.

public interface AfterReturningAdvice extends Advice {
  void afterReturning(Object returnValue, Method m, Object[] args, Object target)
      throws Throwable;
}

after returning advice는 반환 값(수정할 수 없다), 호출된 메서드, 메서드 인자와 대상에 접근한다.

다음 after returning advice는 예외를 던지지 않고 성공적으로 이뤄진 모든 메서드 호출의 횟수를 센다.

public class CountingAfterReturningAdvice implements AfterReturningAdvice {
  private int count;
  public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
      throws Throwable {
    ++count;
  }

  public int getCount() {
    return count;
  }
}

이 어드바이스는 실행경로를 바꾸지 않는다. 이 어드바이스가 예외를 던진다면 값을 반환하는 대신 인터셉터 체인에 예외를 던질 것이다.

Tip
After returning advice는 어떤 포인트컷과도 사용할 수 있다.


B.2.2.5 인트로덕션(introduction) 어드바이스

스프링은 introduction advice를 특별한 종류의 인터셉트 어드바이스로 다룬다.

Introduction은 IntroductionAdvisor와 다음 인터페이스를 구현하는 IntroductionInterceptor를 필요로 한다.

public interface IntroductionInterceptor extends MethodInterceptor {
  boolean implementsInterface(Class intf);
}

AOP Alliance MethodInterceptor 인터페이스에서 상속받은 invoke() 메서드는 introduction을 반드시 구현해야 한다. 이 말은 호출된 메서드가 인트로덕션이 적용된 인터페이스에 있을 때 인트로덕션 인터셉터가 메서드 호출을 다루게 된다는 의미이다. 이는 proceed()를 호출할 수 없다.

인트로덕션 어드바이스는 어떤 포인트컷과도 사용할 없고 메서드가 아니라 클래스 수준에만 적용된다. 인트로덕션 어드바이스는 다음 메서드를 가지는 IntroductionAdvisor만 함께 사용할 수 있다.

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
  ClassFilter getClassFilter();
  void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {
  Class[] getInterfaces();
}

MethodMatcher가 없으므로 인트로덕션 어드바이스와 관련된 Pointcut도 없다. 클래스 필터링만 적합하다.

getInterfaces() 메서드는 이 어드바이저가 인트로덕션한 인터페이스를 반환한다.

구성한 IntroductionInterceptor가 구현할 수 있는 인트로덕션이 된 인터페이스인지 아닌지를 보기 위해 내부적으로 validateInterfaces() 메서드를 사용한다.

스프링 테스트 슈트의 간단한 예시를 보자. 다음 인터페이스를 하나 이상의 객체에 인트로덕션하려고 한다고 가정해보자.

public interface Lockable {
  void lock();
  void unlock();
  boolean locked();
}

이는 mixin을 설명한다. 어드바이즈가 적용된 객체를 Lockable(류의 타입)로 캐스팅해서 lock, unlock 메서드를 호출할 수 있기를 원한다. lock() 메서드를 호출하면 모든 setter 메서드가 LockedException를 던지기를 원한다. 그래서 객체에 대해서 전혀 모르더라도 객체를 불변상태로 만들 수 있는 관점을 추가할 수 있다. 이는 AOP의 좋은 예시이다.

우선, 무거운 작업을 하는 IntroductionInterceptor가 필요할 것이다. 이 경우 간편한 org.springframework.aop.support.DelegatingIntroductionInterceptor 클래스를 확장한다. IntroductionInterceptor를 직접 구현할 수도 있지만, 대부분의 경우 DelegatingIntroductionInterceptor를 사용하는 것이 최상의 선택이다.

DelegatingIntroductionInterceptor는 인트로덕션이 된 인터페이스의 실제 구현체에 인트로덕션을 위임하도록 설계되어서 인터셉션의 사용은 감추면서 생성자 인자로 어떤 객체에도 위임을 설정할 수 있다. 이것이 기본적인 위임(인자가 없는 생성자를 사용하는 경우)이다. 그래서 아래 예제에서 위임은 DelegatingIntroductionInterceptor의 하위클래스인 LockMixin이다. 해당 위임(기본 위임)인 DelegatingIntroductionInterceptor 인스턴스는 위임(IntroductionInterceptor)으로 구현된 모든 인터페이스를 찾고 이 인터페이스에 인트로덕션을 지원할 것이다. LockMixin같은 하위클래스가 노출되면 안되는 인터페이스를 감추도록 suppressInterface(Class intf) 메서드를 호출하게 하는 것이 가능하다. 하지만 IntroductionInterceptor가 얼마나 많은 인터페이스를 지원할 준비가 되었는 지와는 관계없이 사용한 IntroductionAdvisor가 실제로 노출되는 인터페이스를 제어할 것이다. 인트로덕션이 된 인터페이스는 같은 인터페이스의 모든 구현체를 대상한테서 감출 것이다.

LockMixin는 DelegatingIntroductionInterceptor를 서브클래스화 하고 Lockable를 구현한다. 슈퍼클래스는 인트로덕션으로 지원할 수 있는 Lockable을 자동으로 선택하므로 지정할 필요가 없다. 이 방법으로 다수의 인터페이스를 인트로듀스할 수 있다.

locked 인스턴스 변수의 사용을 잘 봐라. 이는 대상 객체에 보관되는 추가 상태를 효과적으로 추가하는 방법이다.

public class LockMixin extends DelegatingIntroductionInterceptor
  implements Lockable {

  private boolean locked;

  public void lock() {
    this.locked = true;
  }

  public void unlock() {
    this.locked = false;
  }

  public boolean locked() {
    return this.locked;
  }

  public Object invoke(MethodInvocation invocation) throws Throwable {
    if (locked() && invocation.getMethod().getName().indexOf("set") == 0)
      throw new LockedException();
    return super.invoke(invocation);
  }
}

때로는 invoke() 메서드를 오버라이드 할 필요가 없다. DelegatingIntroductionInterceptor 구현체(메서드가 인트로듀스되었다면 위임 메서드를 호출하고 인트로유스되지 않았다면 조인포인트로 진행한다.)로 보통은 충분하다. 이 경우에 locked 모드라면 호출할 수 있는 setter 메서드가 없다는 것을 추가로 확인해야 한다.

필요한 인트로덕션 어드바이저는 간단한데 별도의 LockMixin 인스턴스를 보관하고 인트로듀스된 인터페이스(여기서는 Lockable이다)를 지정하는 것이 해야하는 전부이다. 더 복잡한 예시에서는 인트로덕션 인터셉터(prototype으로 정의될 것이다.)를 참조해야 할 수도 있다. 여기서는 LockMixin과 관련된 설정이 없으므로 new로 생성한다.

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
  public LockMixinAdvisor() {
    super(new LockMixin(), Lockable.class);
  }
}

이 어드바이저를 아주 간단히 적용할 수 있는데 필요한 설정이 없다.(하지만 이는 필수적인데 IntroductionAdvisor없이 IntroductionInterceptor는 사용할 수 없다.) 일반적으로 인트로덕션을 사용할 때처럼 어드바이저는 상태를 가져야 하므로 인스터스마다 존재해야 한다. 다른 LockMixinAdvisor 인스턴스가 필요하므로 어드바이즈된 객체마다 LockMixin가 필요하다. 이 어드바이저가 어드바이즈된 객체의 상태를 구성한다.

다른 어드바이저처럼 Advised.addAdvisor()메서드나 XML 설정(권장하는 방법)을 사용해서 이 어드바이저를 프로그래밍으로 적용할 수 있다. 이후에 나오는 모든 프락시 생성방법("auto proxy creators"를 포함해서)은 인트로덕션과 상태를 가진 믹스인을 제대로 다룬다.

B.3 스프링의 어드바이저 API

스프링에서 Advisor는 포인트컷 표현식과 연관된 단일 어드바이스 객체를 가진 관점이다.

특수한 경우의 인트로덕션를 제외하고 모든 어드바이저는 어떤 어드바이스와 함께 사용할 수 있다. org.springframework.aop.support.DefaultPointcutAdvisor가 가장 일반적으로 사용하는 어드바이저 클래스다. 예를 들면 MethodInterceptor, BeforeAdvice, ThrowsAdvice와 함께 사용할 수 있다.

스프링에서 어드바이저와 어드바이스 타입을 같은 AOP 프록시에 믹스인할 수 있다. 예를 들어 하나의 프록시 설정에서 interception around advice, throws advice, before advice를 사용할 수 있다. 스프링이 자동으로 필요한 인터셉터 체인을 만들 것이다.

B.4 AOP 프록시를 생성할 때 ProxyFactoryBean 사용하기

비즈니스 객체에 Spring IoC 컨테이너(ApplicationContext나 BeanFactory)를 사용 중이라면(꼭 사용해야 한다!) 스프링의 AOP FactoryBean 중 하나를 사용하고자 할 것이다. (이는 간접계층(layer of indirection)을 도입해서 다른 타입의 객체들을 생성할 수 있도록 하는 팩토리 빈이라는 점을 유념해라.)

Note
스프링 2.0 AOP 지원도 내부에서 팩토리 빈을 사용한다.

스프링에서 AOP 프록시를 생성하는 기본적인 방법은 org.springframework.aop.framework.ProxyFactoryBean를 사용하는 것이다. 이를 통해 적용할 포인트컷과 어드바이스에 대한 완전한 제어권과 순서에 대한 제어권을 가질 수 있다. 하지만 이러한 제어권이 필요 없다면 더 선호되는 간단한 방법이 있다.

B.4.1 기초

다른 스프링 FactoryBean 구현체처럼 ProxyFactoryBean는 간접계층을 도입한다. foo라는 이름의 ProxyFactoryBean를 정의했다면 foo를 참조하는 객체가 ProxyFactoryBean 인스턴스가 아니고 getObject() 메서드의 ProxyFactoryBean의 구현체가 생성한 객체이다. getObject() 메서드는 대상 객체를 감싸는 AOP 프록시를 생성할 것이다.

AOP 프록시를 생성할 때 ProxyFactoryBean나 다른 IoC에 특화된 클래스를 사용할 때 가장 중요한 이점 중 하나는 IoC로 어드바이스나 포인트컷도 관리할 수 있다는 점이다. 이는 다른 AOP 프레임워크에서는 하기 힘든 접근방식을 가능하게 하는 강력한 기능이다. 예를 들어 의존성 주입으로 가능한 플러거블(pluggability) 기능의 모든 이점을 사용해서 어드바이스 자체가 애플리케이션 객체(다른 AOP 프레임워크에서 사용할 수 있어야 하는 대상객체를 제외하고)를 참조할 수 있다.

B.4.2 JavaBean 프로퍼티

스프링이 제공하는 FactoryBean 구현체와 마찬가지로 ProxyFactoryBean 클래스는 JavaBean이고 이 클래스의 프로퍼티들은 다음의 상황에 사용한다.

  • 프락시할 대상을 지정한다.
  • CGLIB을 사용할 지 여부를 지정한다.(아래의 내용과 Section 9.5.3, “JDK기반과 CGLIB기반의 프록시”를 참고해라.)

몇몇 핵심 프로퍼티는 org.springframework.aop.framework.ProxyConfig에서 상속받는다. (스프링에서 모든 AOP 프락시 팩토리의 슈퍼클래스다.) 이 핵심 프로퍼티들은 다음과 같다.

  • proxyTargetClass: 대상 클래스의 인터페이스가 아니라 대상 클래스가 프락시 된 경우 true이다. 이 프로퍼티 값을 true로 설정하면 CGLIB 프락시를 생성할 것이다. (Section 9.5.3, “JDK기반과 CGLIB기반의 프록시”도 참고해라.)
  • optimize: CGLIB로 생성한 프락시에 적극적인 최적화(aggressive optimizations)를 적용할 것인지를 제어한다. 관련 AOP 프락시가 어떻게 최적화를 하는지 완전히 이해하지 못했다면 이 설정은 조심스럽게 사용해야 한다. 이 설정은 현재 CGLIB 프락시에만 사용되므로 JDK 동적 프락시에는 영향을 주지 않는다.
  • frozen: 프락시 구성이 frozen이면 구성을 더는 변경할 수 없다. 이는 약간의 최적화를 하거나 콜러(caller)가 프락시를 다루지 (Advised 인터페이스로) 못하게 하고 싶은 경우에 유용하다. 이 프로퍼티의 기본 값이 false이므로 다른 어드바이스를 추가하는 등의 변경을 할 수 있다.
  • exposeProxy: 대상에서 프락시에 접근할 수 있도록 현재 프락시를 ThreadLocal에 노출할 것인지를 결정한다. 대상이 프락시를 획득해야 하고 exposeProxy 프로퍼티가 true이면 대상이 AopContext.currentProxy() 메서드를 사용할 수 있다.
  • aopProxyFactory: 사용할 AopProxyFactory 구현체로 동적 프락시나 CGLIB이나 다른 프락시 전략을 사용해야 하는지를 커스터마이징하는 방법을 제공한다. 기본 구현체는 동적 프락시나 CGLIB을 적절하게 선택할 것이다. 이 프로퍼티를 사용할 필요가 없어야 한다. 이는 스프링 1.1에서 새로운 프락시 타입을 추가할 수 있도록 만든 것이다.

ProxyFactoryBean에 한정된 프로퍼티에는 다음과 같은 것들이 있다.

  • proxyInterfaces: 문자열로 된 인터페이스 이름의 배열. 이를 제공하지 않으면 대상 클래스의 CGLIB 프락시를 사용할 것이다.(뒷부분의 Section 9.5.3, “JDK기반과 CGLIB기반의 프록시”도 참고해라.)
  • interceptorNames: 적용할 Advisor, 인터셉터, 다른 어드바이스 이름의 문자열 배열. 순서가 중요해서 앞에 작성한 것을 먼저 제공하게 된다. 이는 목록의 첫 인터셉터가 가장 먼저 호출을 가로챌 수 있다는 의미이다.

    현재 팩토리에서 이름은 bean 이름이다.(조상 팩토리의 bean 이름을 포함한다.) 여기서 빈(bean) 참조를 사용하면 ProxyFactoryBean이 어드바이스의 싱글톤 설정을 무시하게 되므로 여기서 빈(bean) 참조를 사용할 수 없다.

    별표(*)로 인터셉터 이름을 추가할 수 있다. 별표 앞부분으로 시작하는 이름의 모든 어드바이저 빈을 가리킬 것이다. 이 기능의 예제를 Section 9.5.6, “'전역' 어드바이저 사용하기”에서 볼 수 있다.

  • singleton: getObject() 메서드를 여러 번 호출하더라도 팩토리가 하나의 객체를 반환해야 하는지를 지정한다. 다수의 FactoryBean 구현체가 이러한 메서드를 제공한다. 기본값은 true이고 상태를 가진 어드바이스를 사용하고자 한다면 (예시로 상태를 가진 믹스인) singleton 값을 false로 설정하고 프로토타입 어드바이스를 사용해라.

B.4.3 JDK와 CGLIB에 기반을 둔 프록시

이번 장에서는 ProxyFactoryBean가 특정 대상 객체(프락시 할) JDK와 CGLIB에 기반을 둔 프락시 중 하나를 생성하는 방법을 자세히 설명한다.

Note
JDK나 CGLIB에 기반을 둔 프락시를 생성하는 ProxyFactoryBean의 동작이 스프링 1.2.x와 2.0에서 달라졌다. ProxyFactoryBean는 이제 TransactionProxyFactoryBean 클래스의 자동탐지(auto-detecting) 인터페이스와 비슷한 기능을 한다.

프락시 할 대상 객체의 클래스(이후에는 대상 클래스라고 지칭한다.)가 어떤 인터페이스도 구현하지 않았다면 CGLIB에 기반을 둔 프락시를 생성할 것이다. 이는 JDK 프락시가 인터페이스에 기반을 두므로 인터페이스가 없다는 것은 JDK 프락시가 불가능하다는 것을 의미하므로 가장 이해하기 쉬운 경우이다. 그냥 대상 빈에 연결하고 interceptorNames 프로퍼티로 인터셉터 목록을 지정해라. CGLIB에 기반을 둔 프락시는 ProxyFactoryBean의 proxyTargetClass 프로퍼티를 false로 설정하더라도 생성될 것이다.(물론 자연스럽지 않다. 좋아져 봤자 중복이고 그렇지 않으면 혼란스러우므로 빈 정의에서 제거하는 것이 제일 좋다.)

대상 클래스가 하나 이상의 인터페이스를 구현했다면 생성되는 프락시 타입은 ProxyFactoryBean 설정을 따른다.

ProxyFactoryBean의 proxyTargetClass프로퍼티를 true로 설정했다면 CGLIB에 기반을 둔 프락시가 생성될 것이다. 이는 합리적이고 최소 놀람의 법칙을 따른 것이다. ProxyFactoryBean의 proxyInterfaces 프로퍼티를 하나 이상의 정규화된 인터페이스 명으로 설정하더라도 proxyTargetClass 프로퍼티를 true로 설정하면 CGLIB에 기반을 둔 프락시에 영향을 끼칠 것이다.

ProxyFactoryBean의 proxyInterfaces 프로퍼티를 하나 이상의 정규화된 인터페이스 명으로 설정했다면 JDK에 기반을 둔 프락시가 생성될 것이다. 생성된 프락시는 proxyInterfaces 프로퍼티에서 지정한 모든 인터페이스를 구현할 것이다. 대상 클래스가 proxyInterfaces 프로퍼티에서 지정한 것보다 훨씬 많은 인터페이스를 구현하게 되더라도 동작도 잘하고 문제없지만 반환된 프락시는 여분의 인터페이스를 구현하지 않을 것이다.

ProxyFactoryBean의 proxyInterfaces 프로퍼티를 설정하지 않았고 대상 클래스가 하나 이상의 인터페이스를 구현했다면 ProxyFactoryBean는 대상 클래스가 인터페이스를 최소 하나는 구현했다는 사실을 자동으로 탐지하고 JDK에 기반을 둔 프락시를 생성할 것이다. 여기서 실제로 프락시 될 인터페이스는 대상 클래스가 구현한 인터페이스 전부이다. 이는 대상 클래스가 구현한 모든 인터페이스의 목록을 proxyInterfaces 프로퍼티에 작성한 것과 같지만, 훨씬 손쉽고 오타가 날 가능성도 적다.

B.4.4 프록시 인터페이스

동작하는 간단한 ProxyFactoryBean의 예시를 보자. 이 예제에는 다음이 포함되어 있다.

  • 프락시할 대상 빈. 이 빈은 아래 예제에서 "personTarget" 빈 정의이다.
  • 어드바이스를 제공하는 데 사용할 어드바이저와 인터셉터.
  • 프락시할 대상 객체(여기서는 personTarget 빈)과 인터페이스를 지정하는 AOP 프락시 빈 정의와 적용할 어드바이스
<bean id="personTarget" class="com.mycompany.PersonImpl">
  <property name="name"><value>Tony</value></property>
  <property name="age"><value>51</value></property>
</bean>

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
  <property name="someProperty"><value>Custom string property value</value></property>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>

<bean id="person"
  class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="proxyInterfaces"><value>com.mycompany.Person</value></property>

  <property name="target"><ref local="personTarget"/></property>
  <property name="interceptorNames">
    <list>
      <value>myAdvisor</value>
      <value>debugInterceptor</value>
    </list>
  </property>
</bean>

interceptorNames 프로퍼티는 현재 팩토리의 인터셉터와 어드바이저의 빈(bean) 이름의 문자열 목록을 담고 있다. 어드바이저, 인터셉터, before/after/returning/throws 어드바이스 객체를 사용할 수 있다. 어드바이저의 순서는 중요하다.

Note
목록에 왜 빈 참조를 사용하지 않는지 궁금할 수도 있다. 이는 ProxyFactoryBean의 singleton 프로퍼티가 false이면 독립적인 프락시 인스턴스를 반환할 수 있어야 하기 때문이다. 어드바이스 중 prototype인 것이 있다면 독립적인 인스턴스를 반환해야 하므로 팩토리에서 prototype 인스턴스를 얻어낼 수 있어야 한다. 그래서 참조를 담고 있으면 안 된다.

앞에서 "person" 빈 정의를 다음과 같이 Person 구현체 대신 사용할 수 있다.

Person person = (Person) factory.getBean("person");

같은 IoC 컨텍스트의 다른 빈은 평범한 자바 객체처럼 타입 의존성을 명시적으로 지정할 수 있다.

<bean id="personUser" class="com.mycompany.PersonUser">
  <property name="person"><ref local="person" /></property>
</bean>

이 예제에서 PersonUser 클래스는 Person 타입의 프로퍼티로 노출될 것이다. 예상대로 "실제" person 구현체 대신 AOP 프락시를 투명하게 사용할 수 있다. 하지만 그 클래스는 동적 프락시 클래스가 될 것이므로 Advised 인터페이스로 캐스팅할 수 있다.(뒤에서 다시 얘기한다.)

다음과 같이 익명 내부 빈(inner bean)을 사용해서 대상과 프락시간의 차이를 숨길 수도 있다. ProxyFactoryBean 정의만 다르다. 어드바이스는 통일성만을 위해서 사용한 것이다.

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
  <property name="someProperty"><value>Custom string property value</value></property>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>

<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="proxyInterfaces"><value>com.mycompany.Person</value></property>
  
  <property name="target">
    <bean class="com.mycompany.PersonImpl">
      <property name="name"><value>Tony</value></property>
      <property name="age"><value>51</value></property>
    </bean>
  </property>
  <property name="interceptorNames">
    <list>
      <value>myAdvisor</value>
      <value>debugInterceptor</value>
    </list>
  </property>
</bean>

이는 Person 타입의 객체가 딱 하나만 있다는 장점이 있어서 애플리케이션 컨텍스트의 사용자가 어드바이즈가 안된 객체에 대한 참조를 얻지 못하게 하고 싶을 때나 스프링 IoC의 애매한 자동 연결을 피하고 싶을 때 유용하다. 그리고 ProxyFactoryBean 정의가 자기 충족적(self-contained)일때의 장점이기도 하다. 하지만 테스트 시나리오 등 팩토리에서 어드바이즈 되지 않은 대상을 얻을 수 있는 경우가 유용한 경우도 실제로 있다.

B.4.5 프록시 클래스

하나 이상의 인터페이스 말고 클래스를 프락시 해야 할 때는 어떻게 하는가?

앞의 예제에서 Person 인터페이스가 없다고 해보자. 어떤 비즈니스 인터페이스도 구현하지 않은 Person 클래스를 어드바이즈 해야 한다. 이럴 때 스프링이 동적 프락시가 아니라 CGLIB 프락시를 사용하도록 설정할 수 있는데 예제에서 ProxyFactoryBean의 proxyTargetClass 프로퍼티를 true로 설정하면 된다. 클래스를 사용하는 대신 인터페이스를 작성하는 것이 최선이지만 인터페이스를 구현하지 않은 클래스를 어드바이스할 수 있다는 것은 레거시 코드를 사용하는 경우 유용할 수 있다. (보통 스프링은 규법적(prescriptive)이지 않다. 좋은 방법을 적용하기 쉽게 하면서도 특정 접근을 강제하지 않는다.)

원한다면 인터페이스가 있더라도 모든 경우에 CGLIB을 사용하도록 강제할 수 있다.

런타임시에 대상 클래스의 하위 클래스를 생성해서 CGLIB 프락시가 동작한다. 스프링은 이 생성한 하위 클래스에 원래 대상의 메서드 호출을 위힘하도록 설정한다. 이 하위 클래스는 어드바이스에서 위빙을 하는 Decorator 패턴을 구현하는데 사용한다.

보통 CGLIB 프락시는 사용하기에 투명해야 하지만 이때 고려해야 할 부분이 있다.

  • Final 메서드는 오버라이드 할 수 없으므로 어드바이스 할 수 없다.
  • 클래스 패스에 CGLIB 2 바이너리가 필요하다. 동적 프락시는 JDK에서 사용할 수 있다.

CGLIB 프락시와 동적 프락시 사이에는 약간의 성능 차이가 있다. 스프링 1.0부터는 동적 프락시가 약간 더 빠르지만 앞으로는 달라질 수도 있다. 이 경우 성능을 결정적인 고려사항이 되면 안 된다.

B.4.6 'global' 어드바이저의 사용

인터페이스 이름에 별표(asterisk)를 붙이면 별표 앞부분과 일치하는 빈(bean) 이름을 가진 모든 어드바이저가 어드바이저 체인에 추가될 것이다. 이는 'global' 어드바이저의 표준 셋을 추가할 필요가 있을 때 편리한 방법이다.

<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="target" ref="service"/>
  <property name="interceptorNames">
    <list>
      <value>global*</value>
    </list>
  </property>
</bean>

<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>

B.5 간결한 프록시 정의

특히 트랜잭션이 적용된 프락시를 정의하는 경우 비슷한 프락시 정의를 많이 작성하게 될 것이다. 내부 빈과 함께 부모 빈과 자식 빈의 정의를 사용해서 더 명확하고 간단한 프락시 정의를 작성할 수 있다.

먼저 프락시에 대한 부모(template) 빈 정의를 생성한다.

<bean id="txProxyTemplate" abstract="true"
    class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
  <property name="transactionManager" ref="transactionManager"/>
  <property name="transactionAttributes">
    <props>
      <prop key="*">PROPAGATION_REQUIRED</prop>
    </props>
  </property>
</bean>

이 자체로는 절대 인스턴스화 되지 않을 것이고 실제로 불완전한 상태이다. 그다음 생성해야 하는 각 프락시는 자식 빈(bean) 정의이고 이는 내부 빈 정의로 프락시의 대상은 감싸므로 대상은 어떻게든 절대 사용되지 않을 것이다.

<bean id="myService" parent="txProxyTemplate">
  <property name="target">
    <bean class="org.springframework.samples.MyServiceImpl">
    </bean>
  </property>
</bean>

당연히 부모 템플릿의 프로퍼티를 오버라이드 할 수 있다. 이 경우에는 트랜잭션 전파 설정을 오버라이드 한다.

<bean id="mySpecialService" parent="txProxyTemplate">
  <property name="target">
    <bean class="org.springframework.samples.MySpecialServiceImpl">
    </bean>
  </property>
  <property name="transactionAttributes">
    <props>
      <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
      <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
      <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
      <prop key="store*">PROPAGATION_REQUIRED</prop>
    </props>
  </property>
</bean>

위 예제에서 이전에 설명했던 대로 명시적으로 abstract 속성을 사용해서 부모 빈 정의를 abstract로 표시했으므로 부모 빈은 절대 인스턴스화되지 않을 것이다. 애플리케이션 컨텍스트(간단한 빈 팩토리는 아니다)은 기본적으로 모든 싱글톤을 미리 인스턴스화 할 것이다. 그래서 템플릿으로만 사용할 (부모) 빈 정의가 있고 이 정의가 클래스를 지정하고 있다면 반드시 abstract 속성을 true로 설정해야 한다는 점이 중요하다(최소한 싱글톤 빈에는). 그렇지 않으면 애플리케이션 컨텍스트가 이를 미리 인스턴스화 하려고 할 것이다.

B.6 ProxyFactory로 AOP 프록시를 프로그래밍적으로 생성하기

스프링에서 AOP 프락시를 프로그래밍 적으로 생성하는 것은 쉽다. 스프링 IoC에 의존성을 갖지 않고 스프링 AOP를 사용할 수 있다.

다음 예제에서는 대상 객체에 한 인터셉터와 하나의 어드바이저를 가진 프락시를 생성하는 것을 보여준다. 대상 객체를 자동으로 프록시 함으로써 인터페이스를 구현한다.

ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addInterceptor(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();

첫 단계는 org.springframework.aop.framework.ProxyFactory 타입의 객체를 생성하는 것이다. 앞의 예시처럼 대상 객체로 이 타입의 객체를 생성하거나 대체 생성자에 프록시될 인터페이스를 지정할 수 있다.

인터셉터와 어드바이저를 추가할 수 있고 ProxyFactory의 생명주기 동안 이를 조작할 수 있다. IntroductionInterceptionAroundAdvisor를 추가한다면 프록시가 여분의 인터페이스를 구현하도록 할 수 있다.

ProxyFactory에는(AdvisedSupport에서 상속받은) before 어드바이스나 throws 어드바이스 같은 다른 어드바이스 타입을 추가할 수 있는 편의 메서드도 있다. AdvisedSupport는 ProxyFactory와 ProxyFactoryBean의 슈퍼 클래스다.

Tip
애플리케이션 대부분에서 AOP 프록시 생성을 IoC 프레임워크와 통합하는 것이 좋은 방법이다. 보통은 AOP로 자바 코드에서 설정을 빼내는 것(externalize)을 추천한다.


B.7 어드바이즈된 객체 조작하기

AOP 프락시를 생성하더라도 org.springframework.aop.framework.Advised 인터페이스로 이를 조작할 수 있다. 어떤 AOP 프락시라도 이 인터페이스로(또는 이를 구현한 다른 인터페이스) 캐스팅할 수 있다. 이 인터페이스는 다음 메서드를 가진다.

Advisor[] getAdvisors();

void addAdvice(Advice advice) throws AopConfigException;

void addAdvice(int pos, Advice advice)
        throws AopConfigException;

void addAdvisor(Advisor advisor) throws AopConfigException;

void addAdvisor(int pos, Advisor advisor) throws AopConfigException;

int indexOf(Advisor advisor);

boolean removeAdvisor(Advisor advisor) throws AopConfigException;

void removeAdvisor(int index) throws AopConfigException;

boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;

boolean isFrozen();

getAdvisors() 메서드는 해당 팩토리에 추가된 모든 어드바이저, 인터셉터, 그 외 어드바이스 타입마다 Advisor를 반환할 것이다. Advisor를 추가하면 이 인덱스에서 반환한 어드바이저는 추가한 객체가 될 것이고 인터셉터나 다른 어드바이스 타입을 추가하면 스프링이 항상 true를 반환하는 포인트컷을 가진 어드바이저로 이를 감쌀 것이다. 그래서 MethodInterceptor를 추가하면 이 인덱스가 반환한 어드바이저는 모든 클래스 메서드와 매칭되는 포인트컷과 MethodInterceptor를 반환하는 DefaultPointcutAdvisor가 될 것이다.

어떤 Advisor든 추가할 때 addAdvisor() 메서드를 사용할 수 있다. 보통 포인트컷과 어드바이서를 가진 어드바이저는 어떤 어드바이스나 포인트컷과도 사용할 수 있는(하지만 인트로덕션과는 사용할 수 없다.) 일반적인(generic) DefaultPointcutAdvisor가 될 것이다.

기본적으로 일단 프록시가 생성되고 나면 어드바이저나 인터셉터를 추가하거나 삭제할 수 있다. 해당 팩토리에 존재하는 프록시는 인터페이스 변경사항을 보여주지 않으므로 인트로덕션 어드바이저를 추가하거나 삭제할 수 없다는 점이 유일한 제약사항이다. (팩토리에서 새로운 프록시를 받아서 이 문제를 피할 수 있다.)

AOP 프록시를 Advised 인터페이스로 캐스팅해서 그 어드바이스를 검사하고 조작하는 간단한 예제다.

Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");

// 포인트컷없는 인터셉처 처럼 어드바이스를 추가한다
// 프락시 된 모든 메서드에 매칭될 것이다
// 인터셉터, before, after returning, throws 어드바이스를 사용할 수 있다
advised.addAdvice(new DebugInterceptor());

// 포인트컷으로 선택할 수 있는 어드바이스를 추가한다
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));

assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);
Note
합당한 사용 사례가 확실히 있음에도 프로덕션에서 비즈니스 객체의 어드바이스를 수정하려고 어드바이저할 수 있는지(advisable, 농담이 아니다)한지가 궁금할 수 있다. 하지만 이는 테스트 등 개발에서는 아주 유용할 수 있다. 테스트하고자 하는 메서드 호출 내에서 가져오는 인터셉터나 다른 어드바이스 형식으로 테스트 코드를 추가할 수 있다는 점이 아주 유용하다는 것을 종종 경험한다. (예를 들어 해당 메서드를 위해 생성한 트랜잭션 내부에서 트랜잭션을 롤백하기 전에 데이터베이스가 제대로 갱신되었는지 확인하는 SQL을 실행하려고 이 어드바이스를 얻을 수 있다.)


프록시를 어떻게 만들었느냐에 따라 frozen 플래그를 보통 설정할 수 있는데 Advised isFrozen() 메서드가 true를 반환하면 추가하거나 제거하는 등 어드바이스를 수정하는 어떤 시도라도 AopConfigException이 발생할 것이다. 어드바이즈된 객체의 상태를 변경할 수 없게 하는 기능은 보안 인터셉터를 제거하는 코드의 호출을 차단하는 등의 경우에 유용하다. 이는 스프링 1.1에서 런타임 어드바이스 수정이 필수사항이 아닌 경우 적극적인(aggressive) 최적화를 허용하는데도 사용할 수 있다.

B.8 "autoproxy" 기능 사용하기

지금까지는 ProxyFactoryBean나 비슷한 팩토리 빈을 사용해서 AOP 프록시를 명시적으로 생성했다.

스프링에서도 선택한 빈(bean) 정의를 자동으로 프락시 할 수 있는 "autoproxy" 빈 정의를 사용할 수 있다. 이는 스프링 "bean post processor" 인프라스트럭처에 기분을 두고 있고 컨테이너 로딩처럼 모든 빈 정의의 수정을 활성화 할 수 있다.

이 모델에서 자동 프락시 인프라스트럭처를 구성하려고 XML 빈 정의 파일에 전용 빈 정의를 구성할 수 있다. 이를 통해 자동 프락시가 가능한 대상을 선언할 수 있어서 ProxyFactoryBean를 사용할 필요가 없다.

이를 위한 두 가지 방법이 있다.

  • 현재 컨텍스트에서 특정 빈을 참조하는 autoproxy creator를 사용한다.
  • 따로 고려해 볼 만한 autoproxy 생성의 특수한 경우가 있다. autoproxy 생성은 소스 수준의 메타데이터 속성으로 발생한다.

B.8.1 autoproxy 빈 정의

org.springframework.aop.framework.autoproxy 패키지에서 다음의 표준 autoproxy creator를 제공한다.

B.8.1.1 BeanNameAutoProxyCreator

BeanNameAutoProxyCreator 클래스는 리터럴 값이나 와일드카드와 일치하는 이름의 빈에 대한 AOP 프락시를 자동으로 생성하는 BeanPostProcessor이다.

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
  <property name="beanNames"><value>jdk*,onlyJdk</value></property>
  <property name="interceptorNames">
    <list>
      <value>myInterceptor</value>
    </list>
  </property>
</bean>

ProxyFactoryBean가 그러하듯 프로토타입 어드바이저에 대한 올바른 동작을 하게 하는 인터셉터의 목록 대신 interceptorNames 프로퍼티가 존재한다. 이름을 가진 "interceptors"는 어드바이저나 어떤 어드바이스 타입도 될 수 있다.

보통 자동 프락시가 그러하듯 BeanNameAutoProxyCreator 사용하는 주요 이유는 최소한의 설정으로 여러 객체에 일관성 있게 같은 설정을 적용하는 것이다. 여러 객체에 선언적인 트랜잭션을 적용하는 것이 가장 많이 쓰는 경우이다.

앞의 예제에서 "jdkMyBean"와 "onlyJdk"처럼 매칭된 이름을 가진 빈 정의는 대상 클래스에 대한 평범한(plain old) 빈 정의이다. BeanNameAutoProxyCreator가 AOP 프락시를 자동으로 생성할 것이다. 매칭된 모든 빈에 같은 어드바이스를 적용할 것이다. (앞의 예제에서 인터셉터를 사용하는 대신) 어드바이저를 사용한다면 포인트컷은 다른 빈에 다르게 적용되리라는 것을 유념해라.

B.8.1.2 DefaultAdvisorAutoProxyCreator

더 일반적이면서 훨씬 강력한 auto proxy creator가 DefaultAdvisorAutoProxyCreator이다. 이는 autoproxy 어드바이저의 빈 정의에서 특정 빈 이름을 지정하지 않고도 현재 컨텍스트에서 가능한 어드바이저를 바로 적용한다. BeanNameAutoProxyCreator처럼 DefaultAdvisorAutoProxyCreator도 일관된 설정과 중복 제거라는 장점이 있다.

이 방법을 사용하면 다음의 과정을 포함한다.

  • DefaultAdvisorAutoProxyCreator 빈 정의를 지정한다.
  • 같은 컨텍스트나 관련 컨텍스트에서 원하는 만큼의 어드바이저 지정한다. 이는 반드시 Advisor여야 하고 인터셉터나 다른 어드바이스일 수는 없다. 이는 후보 빈 정의에 각 어드바이스의 자격을 검사하려면 평가할 포인트컷이 반드시 존재해야 하기 때문이다.

각 비즈니스 객체(여기서는 "businessObject1"와 "businessObject2")에 적용해야 하는 어드바이스가 어떤 것인지 보기 위해(존재한다면) DefaultAdvisorAutoProxyCreator는 각 어드바이저에 있는 포인트컷을 자동으로 평가할 것이다.

이는 각 비즈니스 객체에 원하는 만큼의 어드바이저를 자동으로 적용할 수 있다는 의미이다. 비즈니스 객체의 메서드와 매칭되는 어드바이스의 포인트컷이 없다면 해당 비즈니스 객체는 프락시 되지 않을 것이다. 새로운 비즈니스 객체에 빈 정의가 추가되었으므로 필요하다면 자동으로 프락시 될 것이다.

보통 오토 프락시는 호출자(caller)나 의존성에서 어드바이즈되지 않은 객체를 얻을 수 없게 하는 장점이 있다. 이 ApplicationContext에서 getBean("businessObject1")을 호출하면 대상 비즈니스 객체가 아니라 AOP 프락시를 반환한다.("내부 빈(inner bean)"도 이 장점을 제공한다는 것을 앞에서 보여줬다.)

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
  <property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>

<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>

<bean id="businessObject1" class="com.mycompany.BusinessObject1">
  
</bean>

<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>

DefaultAdvisorAutoProxyCreator는 다수 비즈니스 객체에 같은 어드바이스를 일관적으로 적용하고자 할 때 유용하다. 인프라 정의를 하고 나면 특정 프락시 구성을 포함하지 않고도 새로운 비즈니스 객체를 간단히 추가할 수 있고 구성을 아주 약간만 바꿔서 아주 쉽게 추가적인 관점(추적(tracing) 관점이나 성능 모니터링 관점 등)에 둘 수도 있다.

DefaultAdvisorAutoProxyCreator는 필터링(같은 팩토리에서 다르게 구성한 다수의 AdvisorAutoProxyCreator를 사용해서 특정 어드바이저만 평가하도록 작명관례를 사용해서)와 오더링(ordering)을 제공한다. 어드바이저는 순서가 중요하다면 올바른 순서를 보장하기 위해 org.springframework.core.Ordered를 구현할 수 있다. 앞의 예제에서 사용한 TransactionAttributeSourceAdvisor는 구성 가능한 순서(order) 값을 가진다. 기본 설정은 순서 없음(unordered)이다.

B.8.1.3 AbstractAdvisorAutoProxyCreator

이 클래스는 DefaultAdvisorAutoProxyCreator의 슈퍼클래스다. 그럴 일은 없지만 어드바이저 정의로 DefaultAdvisorAutoProxyCreator 프레임워크의 동작을 커스터마이징하는 것으로 불충분할 때 이 클래스의 하위클래스를 만들어서 자신만의 자동 프락시 생성자를 만들 수 있다.

B.8.2 메타데이터 주도 자동 프락시 사용하기

메타데이터가 주도하는 자동 프락시는 특히 중요하다. 이 방식은 .NET ServicedComponents와 유사한 프로그래밍 모델을 제공한다. EJB처럼 XML 배포 디스크립터를 사용하는 대신 트랜잭션 관리와 다른 엔터프라이즈 서비스에 대한 구성을 소스 수준의 속성에 담는다.

이 경우 메타데이터 속성을 이해하는 Advisor와 조합해서 DefaultAdvisorAutoProxyCreator를 사용한다. 관련 메타데이터는 자동프락시 생성 클래스가 아니라 후보 어드바이저의 포인트컷 부분에 작성한다.

사실 이 방식은 DefaultAdvisorAutoProxyCreator의 특수한 경우이지만 별도로 다룰 가치가 있다.(메타데이터를 인식하는 코드는 AOP 프레임워크가 아니라 어드바이저의 포인트컷이 담고 있다.)

JPetStore 예제 애플리케이션의 /attributes 디렉터리에 속성(attribute) 주도 자동프락시가 나와 있다. 이 경우에는 TransactionProxyFactoryBean를 사용할 필요가 없고 메타데이터를 인식하는 포인트컷을 사용하므로 비즈니스 객체에 transactional 속성을 정의하는 것으로 충분하다. /WEB-INF/declarativeServices.xml에 있는(이는 일반적이다) 빈 정의에 다음의 코드가 있고 JPetStore 외부에서 사용할 수 있다.

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
  <property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>

<bean id="transactionInterceptor"
    class="org.springframework.transaction.interceptor.TransactionInterceptor">
  <property name="transactionManager" ref="transactionManager"/>
  <property name="transactionAttributeSource">
    <bean class="org.springframework.transaction.interceptor.AttributesTransactionAttributeSource">
      <property name="attributes" ref="attributes"/>
    </bean>
  </property>
</bean>

<bean id="attributes" class="org.springframework.metadata.commons.CommonsAttributes"/>

DefaultAdvisorAutoProxyCreator 빈 정의(이름은 중요하지 않으므로 생략 가능하다)는 현재 애플리케이션 컨텍스트에서 적합한 포인트컷을 모두 선택할 것이다. 이 경우 TransactionAttributeSourceAdvisor 타입의 "transactionAdvisor" 빈 정의는 transaction 속성을 가진 클래스나 메서드에 적용될 것이다. TransactionAttributeSourceAdvisor는 생성자 주입으로 TransactionInterceptor에 의존하는데 예제는 자동연결(autowiring)으로 의존성을 해결한다. AttributesTransactionAttributeSource는 org.springframework.metadata.Attributes 인터페이스의 구현체에 의존한다. 이 예제에서는 "attributes" 빈이 이를 만족하게 하는데 속성 정보를 가져오는데 Jakarta Commons Attributes API를 사용한다. (애플리케이션 코드는 반드시 Commons Attributes 컴파일 테스크로 컴파일해야 한다.)

JPetStore 예제 애플리케이션의 /annotation 디렉터리에는 JDK 1.5+ 어노테이션을 사용한 자동 프락시의 비슷한 예제가 있다. 다음 설정은 스프링 Transactional 어노테이션의 자동 탐지를 활성화해서 어노테이션이 붙은 빈에 프락시를 만들어 준다.

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
  <property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>

<bean id="transactionInterceptor"
    class="org.springframework.transaction.interceptor.TransactionInterceptor">
  <property name="transactionManager" ref="transactionManager"/>
  <property name="transactionAttributeSource">
    <bean class="org.springframework.transaction.annotation.AnnotationTransactionAttributeSource"/>
  </property>
</bean>

여기서 정의한 TransactionInterceptor는 PlatformTransactionManager 정의에 의존하지만 애플리케이션의 트랜잭션 요구사항에 따라 다르므로(이 예제처럼 보통은 JTA이고 Hibernate, JDO, JDBC 등이 될 수 있다.) 이 일반적인 파일에는 포함하지 않았다.(넣을 수는 있지만)

<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
Tip
선언적인 트랜잭션 관리만 필요한 경우 이러한 일반적인 XML 정의를 사용하면 스프링이 자동으로 transaction 속성을 가진 모든 클래스와 메서드를 프락시 할 것이다. AOP를 직접 사용할 필요가 없게 되고 프로그래밍 모델이 .NET ServicedComponents의 프로그래밍 모델과 유사하다.


이 메커니즘을 확장 가능해서 커스텀 속성으로 자동프락시를 할 수 있다. 이를 하려면 다음의 작업이 필요하다.

  • 커스텀 속성을 정의한다.
  • 클래스나 메서드에서 커스텀 속성이 있으면 발생하는 포인트컷을 포함해서 필요한 어드바이스를 가진 Advisor를 지정한다. 단순히 커스텀 속성을 선택하는 정적 포인트컷을 구현해서 존재하는 어드바이스를 사용할 수도 있다.

이러한 어드바이저를 어드바이즈된 각 클래스에 유일하도록 할 수 있다.(예를 들면 믹스인) 이를 위해서는 빈 정의를 싱글톤이 아니라 프로토타입으로 정의하기만 하면 된다. 예를 들어 앞에 나왔던 스프링 테스트 슈트의 LockMixin 인트로덕션 인터셉터를 다음에 나온 것처럼 대상 믹스인에 속성주도 포인트컷과 함께 사용할 수 있다. JavaBean 프로퍼티로 구성한 일반적인 DefaultPointcutAdvisor를 사용한다.

<bean id="lockMixin" class="org.springframework.aop.LockMixin"
    scope="prototype"/>

<bean id="lockableAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
    scope="prototype">
  <property name="pointcut" ref="myAttributeAwarePointcut"/>
  <property name="advice" ref="lockMixin"/>
</bean>

<bean id="anyBean" class="anyclass" ...

속성을 이해하는 포인트컷이 anyBean이나 다른 빈 정의의 메서드와 매칭된다면 믹스인이 적용될 것이다. lockMixin와 lockableAdvisor 정의 모두 프로토타입임을 봐라. myAttributeAwarePointcut 포인트컷은 어드바이즈된 개별 객체의 상태를 보관하지 않으므로 싱글톤으로 정의할 수 있다.

B.9 TargetSource 사용하기

스프링은 org.springframework.aop.TargetSource 인터페이스에 나온 TargetSource의 개념을 제공한다. 이 인터페이스는 조인 포인트를 구현한 "대상 객체"의 반환을 담당한다. AOP 프락시가 메서드 호출을 다룰 때마다 대상 인스턴스에 대해 TargetSource 구현체에 질의한다.

보통 스프링 AOP를 사용하는 개발자가 TargetSource를 직접 사용할 필요는 없지만 이는 풀링(pooling)이 되고 핫스왑도 가능하면서 정교한 대상을 강력하게 지원한다. 예를 들어 풀링이 되는 TargetSource는 인스턴스를 관리하는 풀을 이용해서 호출할 때마다 다른 대상 인스턴스를 반환할 수 있다.

TargetSource를 지정하지 않으면 로컬 객체를 감싸는 기본 구현체를 사용한다. 이때는 (예상한 대로) 호출할 때마다 같은 대상을 반환한다.

스프링에서 제공하는 표준 대상 소스(target source)를 보고 어떻게 사용할 수 있는지 살펴보자.

Tip
커스텀 대상 소스를 사용하는 경우에는 보통 대상을 싱글톤 빈 정의가 아니라 프로토타입 빈 정의로 정의해야 할 것이다. 이를 통해 필요할 때 스프링이 새로운 대상 인스턴스를 만들게 할 수 있다.


B.9.1 핫스왑이 가능한 대상 소스

org.springframework.aop.target.HotSwappableTargetSource 호출자(caller)가 AOP 프락시에 대한 참조를 유지하게 하면서 AOP 프락시의 대상이 변경되도록 존재한다.

대상소스의 대상을 변경하면 즉시 영향을 끼친다. HotSwappableTargetSource는 스레드 세이프 하다.

다음과 같이 HotSwappableTargetSource의 swap() 메서드로 대상을 변경할 수 있다.

HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);

다음과 같은 XML 정의가 필요하다.

<bean id="initialTarget" class="mycompany.OldTarget"/>

<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
  <constructor-arg ref="initialTarget"/>
</bean>

<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="targetSource" ref="swapper"/>
</bean>

앞의 swap() 호출은 교환 가능한 빈의 대상을 변경한다. 이 빈에 대한 참조를 가진 클라이언트는 이 변경을 알지 못하지만 새로운 타켓을 바로 사용하기 시작할 것이다.

이 예제에서는 어드바이스를 전혀 추가하지 않았지만 (TargetSource를 사용할 때 어드바이스를 추가할 필요는 없다.) 당연히 임의의 어드바이스와 함께 어떤 TargetSource도 사용할 수 있다.

B.9.2 대상 소스 풀링

대상 소스 풀링을 사용해서 상태가 없는 세션 EJB와 유사한 프로그래밍 모델을 제공한다. 이는 같은 인스턴스의 풀을 관리해서 메서드 호출이 풀에서 놀고 있는 객체를 사용하도록 한다.

스프링 플링은 어떤 POJO에도 적용할 수 있다는 점이 스프링의 풀링과 SLSB 풀링간에 중요한 차이점이다. 보통 스프링에서 이 서비스는 비침투적인 방법으로 적용할 수 있다.

스프링은 꽤 효율적인 풀링 구현체를 제공하는 Jakarta Commons Pool 1.3을 지원한다. 이 기능을 사용하려면 애플리케이션의 클래스패스에 commons-pool Jar가 필요하다. 다른 풀링 API를 지원하기 위해 org.springframework.aop.target.AbstractPoolingTargetSource의 하위클래스를 만들 수도 있다.

예시 구성이 다음에 나와 있다.

<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
    scope="prototype">
  ... properties omitted
</bean>

<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPoolTargetSource">
  <property name="targetBeanName" value="businessObjectTarget"/>
  <property name="maxSize" value="25"/>
</bean>

<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="targetSource" ref="poolTargetSource"/>
  <property name="interceptorNames" value="myInterceptor"/>
</bean>

대상 객체(이 예제에서는 "businessObjectTarget")는 반드시 프로토타입이어야 한다. 대상의 새로운 인스턴스를 생성하려고 PoolingTargetSource 구현체가 풀(pool)의 크기를 필요한 만큼 증가시킬 수 있다. 이 프로퍼티에 대해서 알고 싶다면 AbstractPoolingTargetSource의 javadoc과 하위 구현체를 봐라. "maxSize"느 가장 기본적이고 항상 제공됨을 보장한다.

이 경우 "myInterceptor"가 같은 IoC 컨텍스트에서 정의되어야 하는 인터프리터의 이름이다. 하지만 풀링을 사용할 인터셉터를 지정할 필요는 없다. 풀링만 원하고 다른 어드바이스는 필요 없다면 interceptorNames 프로퍼티를 아예 설정하지 마라.

스프링이 풀링된 어떤 객체라도 인트로덕션으로 풀의 현재 크기와 구성에 대한 정보를 노출하는 org.springframework.aop.target.PoolingConfig 인터페이스로 캐스팅할 수 있도록 구성하는 것이 가능하다. 이와 같은 어드바이저를 정의할 필요가 있다.

<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
  <property name="targetObject" ref="poolTargetSource"/>
  <property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>

AbstractPoolingTargetSource에서 편의 메서드를 호출해서 이 어드바이저를 가져오므로 MethodInvokingFactoryBean을 사용한다. 이 어드바이저의 이름(여기서는 "poolConfigAdvisor")은 풀링된 객체를 노출하는 ProxyFactoryBean에서 interceptor 이름의 목록에 반드시 포함되어 있어야 한다.

다음처럼 캐스팅될 것이다.

PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());
Note
상태가 없는 서비스 객체의 풀링은 보통 필요치 않다. 대부분의 상태가 없는 객체는 자연히 스레드 세이프 하므로 이 풀링인 기본적으로 사용해야 하는 부분이라고 생각지 않고 리소스를 캐시 한다면 인스턴스 풀링은 문제의 소지가 많다.


자동프락시를 사용해서 더 간단한 풀링을 사용할 수 있다. 어떤 자동프락시 생성자(autoproxy creator)라도 자동 프락시 생성자가 사용하는 TargetSource를 설정할 수 있다.

B.9.3 프로토타입 대상 소스

"prototype" 대상 소스의 설정은 TargetSource 풀링과 비슷하다. 이 경우 메서드 호출할 때마다 대상의 새로운 인스턴스를 생성할 것이다. 최신 JVM에서 새로운 객체를 생성하는 비용이 크지는 않지만 새로운 객체를 연결하는 비용(객체의 IoC 의존성을 만족하게 하면서)은 더 클 수도 있다. 그러므로 아주 좋은 이유가 없이는 이 접근을 사용하지 않아야 한다.

이를 위해 앞의 poolTargetSource 정의를 다음과 같이 수정할 수 있다.(명확하게 이름도 변경했다.)

<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
  <property name="targetBeanName" ref="businessObjectTarget"/>
</bean>

대상 빈의 이름을 위한 딱 하나의 프로퍼티만 있다. 일관된 작명을 보장하려고 TargetSource 구현체에서 상속을 사용한다. 대상 소스의 풀링을 사용하려면 대상 빈은 반드시 프로토타입 빈 정의여야 한다.

B.9.4 ThreadLocal 대상 소스

유입되는 요청마다(스레드당) 객체를 생성해야 할 때 ThreadLocal 대상 소스가 유용하다. ThreadLocal의 개념은 한 스레드와 함께 리소스를 투명하게 저장하는 JDK의 기능을 제공한다. ThreadLocalTargetSource의 설정은 다른 종류의 대상 소스에서 설명한 것과 거의 같다.

<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
  <property name="targetBeanName" value="businessObjectTarget"/>
</bean>
Note
멀티스레드와 멀티 클래스로더 환경에서 ThreadLocal을 잘못 사용하면 심각한 문제가 발생한다.(메모리 누수가 발생할 수 있다.) 항상 다른 클래스로 스레드로컬을 감싸야 하고 ThreadLocal를 직접 사용하지 않아야 한다.(물론 래퍼 클래스는 제외다.) 스레드에 한정된 리소스를 올바르게 설정하고 해지하는 것을(해지는 ThreadLocal.set(null)를 호출하는 것이다.) 잊지 말아야 한다. 해지가 되지 않으면 동작이 잘못될 수 있으므로 해지는 어떤 경우에서 완료되어야 한다. 스프링은 다른 관리 코드 없이도 ThreadLocal을 항상 사용을 고려할 수 있도록 ThreadLocal 지원을 지원한다.


B.10 새로운 Advice 타입 정의하기

스프링 AOP는 확장 가능하도록 설계되었다. 인터셉션(interception) 구현 전략은 현재 내부적으로 사용되지만, interception around 어드바이스, before 어드바이스, throws 어드바이스, after returning 어드바이스 에 추가적으로 임의의 어드바이스 타입을 지원하도록 할 수 있다.

org.springframework.aop.framework.adapter 패키지는 핵심 프레임워크를 바꾸지 않고도 새로운 커스텀 어드바이스 타입을 추가할 수 있는 SPI 패키지이다. org.aopalliance.aop.Advice 태그 인터페이스를 구현해야 한다는 것이 커스텀 Advice 타입의 유일한 제약사항이다.

더 자세한 내용은 org.springframework.aop.framework.adapter 패키지의 Javadoc을 참고해라.

B.11 추가 자료

스프링 AOP의 추가적인 예제는 스프링의 예시 애플리케이션을 참고해라.

  • JPetStore의 기본 구성은 선언적인 트랜잭션 관리를 위한 TransactionProxyFactoryBean의 사용방법을 보여준다.
  • JPetStore의 /attributes 디렉터리는 속성주도의 선언적인 트랜잭션 관리의 사용방법을 보여준다.
2015/03/11 02:08 2015/03/11 02:08