Outsider's Dev Story

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

[Spring 레퍼런스] 8장 스프링의 관점 지향 프로그래밍 #2

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



8.2.4.6 어드바이스 파라미터
스프링 2.0은 완전히 타입이 있는 어드바이스를 제공한다. 즉 어드바이스 시그니처에서 언제나 Object[] 배열을 사용하는 대신에 필요한 파라미터 (위의 예제에서 반환값과 예외에 대해서 선언한 것처럼)를 선언한다는 의미이다. 이제 어떻게 어드바이스 바디에서 아규먼트와 다른 컨텍스트상의 값들을 사용할 수 있도록 만드는 지를 볼 것이다. 우선 어드바이스가 현재 어드바이징한 메서드를 찾을 수 있는 제너릭 어드바이스를 어떻게 작성하는지 살펴보자.

현재 JoinPoint에 접근하기
모 든 어드바이스는 org.aspectj.lang.JoinPoint 타입의 파라미터를 어드바이스의 첫 파라미터로 선언할 수 있다. around advice는 JoinPoint의 하위클래스인 ProceedingJoinPoint 타입의 파라미터를 필수적으로 첫 파라미터로 선언해야 한다. JoinPoint 인터페이스는 getArgs() (메서드 아규먼트를 반환한다), getThis() (프록시 객체를 반환한다), getTarget() (대상 객체를 반환한다), getSignature() (어드바이즈되는 메서드의 설명(description)을 반환한다), toString() (어드바이즈되는 메서드의 유용한 설명을 출력한다)같은 다수의 유용한 메서드를 제공한다. 자세한 내용은 Javadoc을 참고해라.

어드바이스에 파라미터 전달하기
반 환값이나 예외값에 어떻게 바인딩하는 지를 이미 봤다.(after returning advice와 after throwing advice를 사용해서) 아규먼트 값을 어드바이스 바디에서 사용할 수 있도록 하려면 args 형식의 바인딩을 사용할 수 있다. args 표현식에서 타입이름 대신에 파라미터 이름을 사용했다면 어드바이스를 호출할 때 이름에 대응되는 아규먼트의 값이 파라미터 값으로 전달될 것이다. 예제를 보면 이를 더 명확하게 이해할 수 있다. 첫 파라미터로 Account 객체를 받는 dao 작업의 실행을 어드바이즈하고 어드바이스 바디에서 account에 접근해야 한다고 가정해 보자. 이를 다음과 같이 작성할 수 있다.

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + "args(account,..)")
public void validateAccount(Account account) {
  // ...
}

포 인트컷 표현식의 args(account,..) 부분은 두가지 목적을 제공한다. 첫째로 최소 하나이상의 파라미터를 받고 파라미터로 전달되는 아규먼트는 Account의 인스턴스인 메서드 실행만으로 매칭을 제한한다. 두번째로 account 파라미터로 실제 Account 객체가 어드바이스에서 사용할 수 있도록 만든다.

다른 방법으로 이것을 작성하려면 조인포인트가 매칭되었을 때 Account 객체를 "제공"하는 포인트컷을 선언하고 어드바이스에서 포인트컷 이름으로 참조하면 된다. 다음과 같이 작성한다.

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + "args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
  // ...
}

더 자세한 내용에 흥미가 있다면 AspectJ programming guide를 참고해라.

프 록시 객체 (this), 대상 객체 (target), 어노테이션(@within, @target, @annotation, @args)은 모두 비슷한 방법으로 바인딩할 수 있다. 다음 예제는 @Auditable 어노테이션이 붙은 메서드의 실행을 어떻게 매칭하고 audit 코드를 추출하는지 보여준다.

우선 @Auditable 어노테이션의 정의이다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
  AuditCode value();
}

다음은 @Auditable 메서드의 실행을 매칭하는 어드바이스이다.

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && " + "@annotation(auditable)")
public void audit(Auditable auditable) {
  AuditCode code = auditable.value();
  // ...
}

어드바이스 파라미터와 제너릭
스프링 AOP는 클래스 선언과 메서드 파라미터에서 사용한 제너릭을 다룰 수 있다. 다음과 같은 제너릭 타입이 있다고 생각해보자.

public interface Sample<T> {
  void sampleGenericMethod(T param);
  void sampleGenericCollectionMethod(Collection>T> param);
}

인터셉트하려는 메서드의 파라미터 타입에 어드바이스 파라미터의 타입을 지정해서 특정 파라미터 타입으로 인터셉트하는 메서드 타입을 제한할 수 있다.

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
  // 어드바이스 구현체
}

앞에서 이미 얘기했듯이 이 동작은 꽤 명확하다. 하지만 제너릭 컬렉션에는 동작하지 않는다는 사실은 중요하다. 그러므로 다음과 같은 포인트컷은 정의할 수 없다.

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
  // 어드바이스 구현체
}

이 예제가 동작하려면 컬렉션의 모든 요소를 검사해야 하는데 null 값을 보통 어떻게 처리해야 하는지 결정할 수 없으므로 모든 요소를 검사하는 것은 말이 안된다. 이와 비슷하게 하려면 파라미터를 Collection<?> 타입으로 지정하고 수동으로 요소의 타입을 확인해야 한다.

아규먼트 이름 결정하기
어드 바이스 호출시에 파라미터 바인딩은 포인트컷 표현식에서 사용한 이름과 (어드바이스와 포인트컷) 메서드 시그니처에서 선언한 파라미터 이름이 일치하는지 여부에 달려있다. 파라미터 이름은 자바 리플렉션에서 사용할 수 없으므로 스프링 AOP는 파라미터 이름을 결정하기 위해 다음의 전략들을 사용한다.

  1. 사용자가 명시적으로 파라미터 이름을 지정했다면 지정된 파라미터 이름을 사용한다. 어드바이스와 포인트컷 어노테이션에는 둘다 어노테이션이 붙은 메서드의 아규먼트 이름을 지정할 수 있는 선택적인 "argNames" 속성이 있다. 이러한 아규먼트 이름은 런타임시에 사용할 수 있다. 예를 들어 다음과 같다.
    
    @Before(value="com.xyz.lib.Pointcuts.anyPublicMethod()
     && target(bean) && @annotation(auditable)", 
    argNames="bean,auditable")
    public void audit(Object bean, Auditable auditable) {
      AuditCode code = auditable.value();
      // ... code와 bean을 사용한다
    }
    

    첫 파라미터가 JoinPoint, ProceedingJoinPoint, JoinPoint.StaticPart의 타입이면 "argNames" 속성의 값에서 파라미터 이름을 무시할 것이다. 예를 들어 앞의 어드바이스를 조인포인트 객체를 받도록 수정해도 "argNames" 속성에 조인포인트 객체를 포함시킬 필요가 없다.
    
    @Before(value="com.xyz.lib.Pointcuts.anyPublicMethod()
     && target(bean) && @annotation(auditable)", 
    argNames="bean,auditable")
    public void audit(JoinPoint jp, Object bean, Auditable auditable) {
      AuditCode code = auditable.value();
      // ... code, bean, jp를 사용한다
    }
    

    JoinPoint, ProceedingJoinPoint, JoinPoint.StaticPart 타입의 첫 파라미터를 특별하게 처리하는 것은 다른 어떤 조인포인트 컨텍스트도 수집하지 않는 어드바이스에 특히 편리하다. 이러한 경우에는 그냥 "argNames" 속성을 생략한다. 예를 들어 다음의 어드바이스는 "argNames" 속성을 선언할 필요가 없다
    
    @Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
    public void audit(JoinPoint jp) {
      // ... jp를 사용한다
    }
    
  2. 'argNames' 속성을 사용하는 것은 별로 보기가 좋지 않으므로 'argNames' 속성을 지정하지 않으면 스프링 AOP가 해당 클래스의 디버그 정보를 검사하고 로컬변수 테이블에서 파라미터 이름을 결정하려고 시도할 것이다. 클래스가 디버그정보(최소한 '-g:vars')와 함께 컴파일되는 한 이 정보는 존재할 것이다. 이 플래그를 사용한 컴파일 결과는 (1) 코드를 이해하기가 다소 쉬워질 것이고(역엔지니어링), (2) 클래스 파일 크기가 미세하게 커질 것이고(보통 사소한 정도다), (3) 사용하지 않는 로컬 변수를 제거하는 최적화를 컴파일러가 적용하지 않을 것이다. 즉, 이 플래그를 사용하는데 아무런 어려움이 없다.

    @AspectJ 관점을 디버그 정보 없이 AspectJ 컴파일러 (ajc)로 컴파일하면 컴파일러가 필요한 정보를 유지할 것이므로 argNames 속성을 추가할 필요가 없다.
  3. 필 수적인 디버그 정보없이 코드를 컴파일했으면 스프링 AOP가 변수와 파라미터를 바인딩하는 연결(pairing)을 추론하려고 시도할 것이다. (예를 들어 포인트컷 표현식에서 딱 하나의 변수만 바인딩하고 어드바이스 메서드가 딱 하나의 파라미터만 받으면 연결(pairing)은 명확하다!) 주어진 정보로 변수의 바인딩이 모호하다면 AmbiguousBindingException가 던져질 것이다.
  4. 위의 전략이 모두 실패하면 IllegalArgumentException를 던질 것이다.

아규먼트를 가지고 속행하기(Proceeding with arguments)
스프링 AOP와 AspectJ에서 일관성있게 동작하면서 어떻게 아규먼트를 가진 호출을 속행하도록 작성하는지 설명한다. 어드바이스 시크니처를 메서드 파라미터 각각에 순서대로 바인딩하는 것이 해결책이다. 예를 들면 다음과 같다.

@Around("execution(List<Account> find*(..)) &&" +
        "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")        
public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern)
throws Throwable {
  String newPattern = preProcess(accountHolderNamePattern);
  return pjp.proceed(new Object[] {newPattern});
}

많은 경우에 이 바인딩을 사용할 것이다.(위의 예제처럼)


8.2.4.7 어드바이스 순서
여 러 어드바이스들이 모두 같은 조인포인트에서 실행하려고 하면 무슨 일이 발생하는가? 어드바이스 실행의 순서를 결정하는데 AspectJ와 같은 우선순위 규칙을 스프링 AOP도 따른다. "안으로 들어갈 때는" 가장 높은 우선순위를 가진 어드바이스가 먼저 실행된다. (그러므로 주어진 두 before advice 중 가장 높은 우선순위를 가진 어드바이스가 먼저 실행된다.) "밖으로 나올 때는" 조인포인트에서 가장 높은 우선순위를 가진 어드바이스가 나중에 실행된다.(그러므로 주어진 두 after advice 중 가장 우선순위가 높은 어드바이스가 두번째로 실행된다.)

다른 관점에서 정의된 두 어드바이스를 모두 같은 조인포인트에서 실행해야 하는 경우 직접 지정하지 않으면 실행의 순서는 정의되어 있지 않다. 우선순위를 명시해서 실행 순서를 제어할 수 있다. 관점 클래스에서 org.springframework.core.Ordered 인터페이스를 구현하거나 Order 어노테이션을 사용해서 일방적인 스프링식으로 실행순서를 제어한다. 두 관점이 있을 때 Ordered.getValue()(또는 어노테이션 값)가 더 작은 값을 반환하는 관점이 더 높은 우선순위를 가진다.

같은 관점에서 정의된 두 어드바이스를 모두 같은 조인포인트에서 실행해야 하는 경우 순서는 정의되어 있지 않다.(javac로 컴파일된 클래스를 리플랙션해서 순서선언을 획득하는 방법은 없으므로) 이러한 어드바이스 메서드들을 각 관점클래스의 조인포인트마다 하나의 어드바이스 메서드로 구성하거나 어드바이스들을 분리된 관점 클래스로 리팩토링하는 것을 고려해 봐라. 이렇게 하면 관점 수준에서 정렬할 수 있다.


8.2.5 인트로덕션(Introduction)
인트로덕션(AspectJ에서는 inter-type 선언이라고 부른다)은 관점이 주어진 인터페이스를 구현한 어드바이즈된 객체를 선언하고 이러한 객체 대신에 주어진 인터페이스의 구현체를 제공하도록 할 수 있다.

인 트로억션은 @DeclareParents 어노테이션으로 만든다. 이 어노테이션은 새로운 부모를 가진 타입 매칭(따라서 그 이름으로)을 선언하는데 사용한다. 예를 들어 UsageTracked 인터페이스와 DefaultUsageTracked 인터페이스의 구현체가 주어졌을 때 다음의 관점은 서비스 인터페이스의 모든 구현체가 UsageTracked 인터페이스도 구현한다고 선언한다. (예를 들면 JMX를 통해서 통계를 노출하기 위해서)

@Aspect
public class UsageTracking {

  @DeclareParents(value="com.xzy.myapp.service.*+",
                  defaultImpl=DefaultUsageTracked.class)
  public static UsageTracked mixin;
  
  @Before("com.xyz.myapp.SystemArchitecture.businessService() &&" +
          "this(usageTracked)")
  public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
  }
}

구 현할 인터페이스는 어노테이션이 붙은 필드의 타입으로 결정한다. @DeclareParents 어노테이션의 value 속성은 AspectJ 타입패턴이다. (매칭된 타입의 모든 빈은 UsageTracked 인터페이스를 구현할 것이다.) 앞의 예제에서 before advice의 서비스 빈을 UsageTracked 인터페이스의 구현체로 직접 사용할 수 있다. 프로그래밍적으로 빈에 접근하려면 다음과 같이 작성한다.

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");


8.2.6 관점 인스턴스화 모델(Aspect instantiation models)
(이 주제는 고급주제이느로 AOP를 시작하는 단계라면 이번 섹션은 나중에 봐도 된다.)

기 본적으로 어플리케이션 컨텍스트내에는 각 관점마다 하나의 인스턴스가 있을 것이다. AspectJ는 이를 싱글톤 인스턴스화 모델(singleton instantiation model)이라고 부른다. 이 모델로 대리 생명주기로 관점을 정의하는 것이 가능하다. 스프링은 AspectJ의 perthis 인스턴스화 모델과 pertarget 인스턴스화 모델을 지원한다. (percflow, percflowbelow,와 pertypewithin는 현재 지원하지 않는다.)

@Aspect 어노테이션에서 perthis절을 지정해서 "perthis" 관점을 선언한다. 예제를 먼저 보고 어떻게 동작하는지 설명하겠다.

@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {

  private int someState;
    
  @Before(com.xyz.myapp.SystemArchitecture.businessService())
  public void recordServiceUsage() {
    // ...
  }    
}

'perthis' 절은 비즈니스 서비스를 실행하는 유일한 각각의 서비스 객에마다 하나의 관전 인스턴스를 생성할 것이다.(유일한 각각의 객체는 포인트컷 표현식으로 매칭된 조인포인트의 'this'로 바인딩된다.) 관점 인스턴스는 서비스 객체에서 메서드가 최초로 호출될 때 생성된다. 서비스객체가 범위를 벗어날 때 관점도 범위를 벗어난다. 관점 인스턴스가 생성되기 전에 관점 인스턴스 내에서 실행되는 어드바이스는 없다. 관점 인스턴스가 생성되자 마자 관점에서 선언된 어드바이스가 매칭된 조인포인트에서 실행되지만 이 관점과 연결된 서비스 객체에서만 실행된다. per-절에 대한 자세한 내용은 AspectJ programming guide를 봐라.

'pertarget' 인스턴스화 모델도 perthis와 완전히 같은 방법으로 동작하지만 매치된 조인포인트에서 유일한 각각의 대상객체마다 하나의 관점인스턴스를 생성한다.


8.2.7 예제
어떻게 모든 구성요소가 동작하는지 모았으므로 무언가 유용한 작용을 위에서 이를 섞어보자.

비 스니스 서비스의 실행은 종종 동시성 이슈때문에 실패할 수 있다.(예를 들면 데드락 실패) 작업이 재시도되었다면 다음번에는 성공할 가능성이 높아보인다. 이러한 경우처럼(사용자가 복잡한 해결책을 적용할 필요가 없는 멱등 작업) 재시도가 적절해 보이는 비스니스 서비스에서는 클라이언트가 PessimisticLockingFailureException를 보지 않도록 투명하게 작업을 재시도할 것이다. 이는 서비스계층에서 여러 서비스에 걸쳐서 명확하게 적용하는 것이 필수사항이므로 관점을 통해서 구현하는 것이 이상적이다.

작업을 재시도해야 하기 때문에 여러번 proceed를 호출할 수 있도록 around advice를 사용할 필요가 있다. 다음은 기본적인 관점 구현체의 예제이다.

@Aspect
public class ConcurrentOperationExecutor implements Ordered {
   
  private static final int DEFAULT_MAX_RETRIES = 2;

  private int maxRetries = DEFAULT_MAX_RETRIES;
  private int order = 1;

  public void setMaxRetries(int maxRetries) {
    this.maxRetries = maxRetries;
  }
   
  public int getOrder() {
    return this.order;
  }
   
  public void setOrder(int order) {
    this.order = order;
  }
   
  @Around("com.xyz.myapp.SystemArchitecture.businessService()")
  public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { 
    int numAttempts = 0;
    PessimisticLockingFailureException lockFailureException;
    do {
      numAttempts++;
      try { 
        return pjp.proceed();
      }
      catch(PessimisticLockingFailureException ex) {
        lockFailureException = ex;
      }
    }
    while(numAttempts <= this.maxRetries);
    throw lockFailureException;
  }
}

관 점이 Ordered 인터페이스를 구현하므로 트랜잭션 어드바이스보다 더 높은 우선순위를 관점에 설정할 수 있다.(시도할 때마다 새로운 트랜잭션이 필요하다) maxRetries와 order 프로퍼티는 둘다 스프링으로 설정한다. 핵심 동작은 doConcurrentOperation around advice에서 일어난다. 지금은 모든 businessService()s에 재시도 로직을 적용하고 있다. proceed를 시도하고 PessimisticLockingFailureException로 실패하면 모든 재시도 횟수를 소진하지 않고 그냥 다시 시도한다.

이에 상응하는 스프링 설정은 다음과 같다.

<aop:aspectj-autoproxy/>

<bean id="concurrentOperationExecutor"
  class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>  
</bean>

멱등작업만 재시도를 하는 것으로 관점을 개선하려면 Idempotent 어노테이션을 정의한다.

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
  // marker annotation
}

그리고 서비스 작업의 구현체에 어노테이션을 사용해라. 멱등 작업만 재시도하도록 관점을 변경하는 것은 @Idempotent 작업에만 매치하기 위해서 포인트컷을 개선하는 것을 포함한다.

@Around("com.xyz.myapp.SystemArchitecture.businessService() && " + 
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { 
  ...    
}


8.3 스키마 기반의 AOP 지원
자 바 5를 사용할 수 없거나 단순히 XML 기반의 형식을 선호한다면 스프링 2.0이 새로운 "aop" 네임스페이스 태그를 사용해서 관점을 정의하는 기능도 지원한다. @AspectJ 방식을 사용했을 때와 완전히 같은 포인트컷 표현식과 어드바이스들을 지원하기 때문에 이번 섹션에서는 새로운 문법에 집중하고 포인트컷 작성과 어드바이스 파라미터 바인딩에 대해서는 이전 섹션 (Section 8.2, “@AspectJ 지원”)을 참고하길 바란다.

이번 섹션에서 설명한 aop 네임스페이스를 사용하려면 Appendix C, XML Schema-based configuration에서 설명했듯이 spring-aop 스키마를 임포트해야 한다. aop 네임스페이스에서 태그를 임포트하는 방법은 Section C.2.7, “The aop schema”를 봐라.

스 프링 설정에서 모든 aspect과 advisor 요소는 <aop:config> 요소안에 있어야 한다. (어플리케이션 컨텍스트 설정에 하나 이상의 <aop:config> 요소를 둘 수 있다.) <aop:config> 요소에는 pointcut, advisor, aspect 요소가 있을 수 있다.(이 요소들은 반드시 저 순서대로 선언되어야 한다.)

Warning
<aop:config> 방식의 설정은 스프링의 auto-proxying 메카니즘을 많이 사용한다. 이미 BeanNameAutoProxyCreator 등을 사용해서 명시적인 auto-proxying을 사용하고 있다면 문제가 될 소지가 있다.(어드바이스가 위빙되지 않는 등) 추천하는 사용패턴은 <aop:config> 방식이나 AutoProxyCreator 방식을 사용하는 것이다.


8.3.1 관점 선언
스 키마 지원을 사용하면 관점은 스프링 어플리케이션 컨텍스트에 정의된 빈과 마찬가지로 단순히 보통의 자바 개체일 뿐이다. 상태(state)와 동작(behavior)는 객체의 필드와 메서드에 담겨있고 포인트컷과 어드바이스 정보는 XML에 담겨있다.

관점은 <aop:aspect> 요소를 사용해서 선언하고 지원하는 빈(backing bean)은 ref 속성을 사용해서 참조한다.

<aop:config>
  <aop:aspect id="myAspect" ref="aBean">
    ...
  </aop:aspect>
</aop:config>

<bean id="aBean" class="...">
  ...
</bean>

관점을 지원하는 빈(이 경우에는 "aBean")도 당연히 다른 스프링 빈처럼 설정하고 의존성을 주입에 사용할 수 있다.


8.3.2 포인트컷 선언
이름이 있는 포인트컷은 포인트컷 정의가 여러 관점과 어드바이저 사이에서 공유될 수 있도록 <aop:config> 요소내에서 선언할 수 있다.

서비스 계층의 어떤 비즈니스 서비스의 실행을 나타내는 포인트컷은 다음과 같이 정의할 수 있다.

<aop:config>

  <aop:pointcut id="businessService" 
      expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>

포 인트컷 표현식 자체는 Section 8.2, “@AspectJ 지원”에서 설명한 것과 같은 AspectJ 포인트컷 표현식 언어를 사용한다. 자바 5로 스키마 기반의 선언방식을 사용한다면 포인트컷 표현식내 타입(@Aspects)에 선언한 이름있는 포인트컷을 참조할 수 있지만 이 기능은 JDK 1.4나 그 이하의 버전에서는 사용할 수 없다.(자바 5의 AspectJ 리플렉션 API에 의존하고 있다.) 그러므로 JDK 1.5에서는 위의 포인트컷을 다음과 같이 정의할 수도 있다.

<aop:config>

  <aop:pointcut id="businessService" 
      expression="com.xyz.myapp.SystemArchitecture.businessService()"/>

</aop:config>

Section 8.2.3.3, “공통 포잇트컷 정의 공유하기”에서 설명한 SystemArchitecture 관점을 가지고 있다고 가정해 보자.

관점내부에서 선언한 포인트컷은 최상위수준의 포인트컷을 선언하는 것과 아주 유사하다.

<aop:config>

  <aop:aspect id="myAspect" ref="aBean">

    <aop:pointcut id="businessService" 
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>
          
    ...
    
  </aop:aspect>

</aop:config>

@AspectJ 관점과 거의 같은 방법으로 스키마 기반의 정의방식을 사용해서 선언한 포인트컷은 조인포인트 컨텍스트를 수집할(collect) 것이다. 예를 들어 다음 포인트컷은 조인포인트 컨텍스트로 'this' 객체를 수집해서 어드바이스에 전달한다.

<aop:config>

  <aop:aspect id="myAspect" ref="aBean">

    <aop:pointcut id="businessService" 
        expression="execution(* com.xyz.myapp.service.*.*(..)) &amp;&amp; this(service)"/>
    <aop:before pointcut-ref="businessService" method="monitor"/>
    ...
    
  </aop:aspect>

</aop:config>

어드바이스는 일치하는 이름의 파라미터를 포함시켜서 수집된 조인포인트 컨텍스트를 받도록 선언해야 한다.

public void monitor(Object service) {
  ...
}

포 인트컷 하위 표현식을 결합할 때 XML문서에서 '&&'는 다루기가 어려우므로 '&&', '||', '!'의 위치에 각각 'and', 'or', 'not' 키워드를 사용할 수 있다. 예를 들어 앞의 포인트컷은 다음과 같이 작성하는게 더 낫다.

<aop:config>

  <aop:aspect id="myAspect" ref="aBean">

    <aop:pointcut id="businessService" 
        expression="execution(* com.xyz.myapp.service.*.*(..)) and this(service)"/>
    <aop:before pointcut-ref="businessService" method="monitor"/>
    ...
    
  </aop:aspect>

</aop:config>

이 방법으로 정의한 포인트컷은 해당 XML id로 참조하고 혼합된 형태의 포인트컷의 이름이 붙은 포인트컷처럼 사용할 수는 없다. 그러므로 스키마 기반의 정의 방식에서 지원하는 이름이 붙은 포인트컷은 @AspectJ 방식이 제공하는 것보다 더 제한적이다.


8.3.3 어드바이스 선언
@AspectJ 방식과 같은 다섯 종류의 어드바이스를 지원하고 이 어드바이스들은 정확히 같은 의미를 가진다.


8.3.3.1 Before advice
Before advice는 매칭된 메서드 실행 이전에 실행된다. Before advice는 <aop:aspect>내에서 <aop:before>를 사용해서 선언한다.

<aop:aspect id="beforeExample" ref="aBean">

  <aop:before 
    pointcut-ref="dataAccessOperation" 
    method="doAccessCheck"/>
          
  ...
</aop:aspect>

여기서 dataAccessOperation는 최상위 (<aop:config>)에서 정의한 포인트컷의 id이다. 포인트컷을 인라인으로 정의하는 대신에 pointcut-ref 속성을 pointcut 속성으로 교체한다.

<aop:aspect id="beforeExample" ref="aBean">

  <aop:before 
    pointcut="execution(* com.xyz.myapp.dao.*.*(..))" 
    method="doAccessCheck"/>
          
  ...
    
</aop:aspect>

@AspectJ 방식에서 얘기했듯이 이름이 붙은 포인트컷을 사용하면 코드의 가독성을 약간 높힐 수 있다.

method 속성은 어드바이스의 바디를 제공하는 메서드 (doAccessCheck)를 식별한다. 이 메서드는 반드시 어드바이스를 담고 있는 aspect 요소가 참조하는 빈에 정의되어 있어야 한다. 데이터 접근 작업이 실행(포인트컷 표현식으로 매칭된 메서드 실행 조인포인트)되기 전에 관점 빈의 "doAccessCheck" 메서드가 호출될 것이다.


8.3.3.2 After returning advice
매 칭된 메서드 실행이 정상적으로 완료되었을 때 After returning advice가 실행된다. After returning advice는 before advice와 같은 방법으로 <aop:aspect>에서 선언한다.

<aop:aspect id="afterReturningExample" ref="aBean">

  <aop:after-returning 
    pointcut-ref="dataAccessOperation" 
    method="doAccessCheck"/>
          
  ...
</aop:aspect>

마치 @AspectJ 방식처럼 어드바이스 바디 내에서 반환값을 획득하는 것이 가능하다. 전달해야할 반환값의 파라미터 명을 지정하려면 returning 속성을 사용해라.

<aop:aspect id="afterReturningExample" ref="aBean">

  <aop:after-returning 
    pointcut-ref="dataAccessOperation"
    returning="retVal" 
    method="doAccessCheck"/>
          
  ...
</aop:aspect>

doAccessCheck 메서드는 retVal라는 이름의 파라미터를 선언해야 한다. 이 파라미터의 타입은 @AfterReturning에서 설명한 것과 같은 방법으로 매칭을 제약한다. 예를 들어 메서드 시그니처를 다음과 같이 선언한다.

public void doAccessCheck(Object retVal) {...


8.3.3.3 After throwing advice
매 칭된 메서드 실행이 예외를 던지고 종료되었을 때 after throwing advice가 실행된다. after throwing advice는 <aop:aspect>에서 after-throwing 요소를 사용해서 선언한다.

<aop:aspect id="afterThrowingExample" ref="aBean">

  <aop:after-throwing
    pointcut-ref="dataAccessOperation" 
    method="doRecoveryActions"/>
          
  ...
    
</aop:aspect>

마치 @AspectJ 방식처럼 던져진 예외를 어드바이스 바디내에서 획득하는 것이 가능하다. 예외를 전달할 파라미터 명을 지정하려면 throwing 속성을 사용해라.

<aop:aspect id="afterThrowingExample" ref="aBean">

  <aop:after-throwing 
    pointcut-ref="dataAccessOperation"
    throwing="dataAccessEx" 
    method="doRecoveryActions"/>
          
  ...
    
</aop:aspect>

doRecoveryActions 메서드는 dataAccessEx라는 이름의 파라미터를 선언해야 한다. 이 파라미터의 타입은 @AfterThrowing에서 설명한 것과 같은 방법으로 매칭을 제약한다. 예를 들어 메서드 시그니처를 다음과 같이 선언한다.

public void doRecoveryActions(DataAccessException dataAccessEx) {...


8.3.3.4 After (finally) advice
매칭된 메서드 실행이 종료되면 무조선 after (finally) advice가 실행된다. after (finally) advice는 after요소를 사용해서 선언한다.

<aop:aspect id="afterFinallyExample" ref="aBean">

  <aop:after
    pointcut-ref="dataAccessOperation" 
    method="doReleaseLock"/>
          
  ...
</aop:aspect>


8.3.3.5 Around advice
마 지막 어드바이드는 around advice다. around advice는 매칭된 메서드 실행 "주변에서(around)" 실행된다. around advice는 메서드 실행 이전과 이후에 모두 작업을 할 기회를 가지고 언제, 어떻게, 어떤 조건하에서 실행할지를 결정하기 위해 사실 메서드는 무조건 실행된다. 쓰레드 세이프한 방법으로 메서드 실행 이전과 이후에 상태를 공유해야 하는 경우 around advice를 종종 사용한다.(예를 들면 타이머를 시작하고 멈추는 작업) 항상 요구사항을 만족시키는 어드바이스 중에서 가장 덜 강력한 것을 사용해라. 간단히 어드바이스 이전에 어떤 작업을 하려고 around advice를 사용하지 마라.

around advice는 aop:around요소를 사용해서 선언한다. 어드바이스 메서드의 첫 파라미터는 반드시 ProceedingJoinPoint 타입이어야 한다. 어드바이스 바디내에서 ProceedingJoinPoint의 proceed()를 호출하면 의존하는 메서드가 실행된다. proceed 메서드는 Object[]를 전달하면서 실행할 수도 있다. - 배열의 값은 진행되면서 메서드실행의 아규먼트로 사용될 것이다. Object[]로 proceed를 호출하는 내용은 Section 8.2.4.5, “Around advice”를 봐라.

<aop:aspect id="aroundExample" ref="aBean">

  <aop:around
    pointcut-ref="businessService" 
    method="doBasicProfiling"/>
          
  ...
    
</aop:aspect>

doBasicProfiling 어드바이스의 구현체는 @AspectJ 예제와 완전히 똑같다.(물론 어노테이션은 빼고)

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
  // 스톱워치 시작
  Object retVal = pjp.proceed();
  // 스톱워치 멈춤
  return retVal;
}


8.3.3.6 어드바이스 파라미터

스 키마 기반의 선언 방식은 @AspectJ 지원에서 설명한 것과 같은 방법으로 완전한 타입의 어드바이스를 지원한다.(어드바이스 메서드 파라미터에 대해서 이름으로 포인트컷 파라미터를 매칭하는 방법) 자세한 내용은 Section 8.2.4.6, “어드바이스 파라미터”를 봐라. 어드바이스 메서드에 아규먼트의 이름을 명시적으로 지정하고 싶다면(앞에서 설명한 탐지 전력에 의존하지 않고) 어드바이스 요소의 arg-names 속성을 사용하면 된다. arg-names 속성은 the section called “아규먼트 이름 결정하기”에서 설명한 어드바이스 어노테이션의 "argNames" 요소와 같은 방법으로 다룬다. 다음은 그 예제다.

<aop:before
  pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
  method="audit"
  arg-names="auditable"/>

arg-names 속성은 콤마로 구분된 파라미터 이름의 리스트를 받는다.

약간 더 깊히 들어간 다음의 XSD 기반 접근의 예제는 다수의 강타입 파라미터의 결합에서 사용한 around advice를 보여준다.

package x.y.service;

public interface FooService {
  Foo getFoo(String fooName, int age);
}

public class DefaultFooService implements FooService {
  public Foo getFoo(String name, int age) {
    return new Foo(name, age);
  }
}

다 음은 관점이다. profile(..) 메서드는 다수의 강타입 파라미터를 받는데 첫 파라미터는 메서드 호출을 하는데 사용하는 조인포인트가 될 것이다. 이 메서드의 존재는 profile(..)가 around 어드바이스로 사용된다는 것을 의미한다.

package x.y;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;

public class SimpleProfiler {

  public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
    StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
    try {
      clock.start(call.toShortString());
      return call.proceed();
    } finally {
      clock.stop();
      System.out.println(clock.prettyPrint());
    }
  }
}

마지막으로 다음은 특정 조인포인트에서 위의 어드바이스가 실행되는데 필요한 XML 설정이다.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

  <!-- 이 객체는 스프링 AOP 기반이 프록시할 객체다 -->
  <bean id="fooService" class="x.y.service.DefaultFooService"/>

  <!-- 이는 그 자체로 실제 어드바이스다 -->
  <bean id="profiler" class="x.y.SimpleProfiler"/>

  <aop:config>
    <aop:aspect ref="profiler">

      <aop:pointcut id="theExecutionOfSomeFooServiceMethod"
          expression="execution(* x.y.service.FooService.getFoo(String,int))
          and args(name, age)"/>

      <aop:around pointcut-ref="theExecutionOfSomeFooServiceMethod"
          method="profile"/>

    </aop:aspect>
  </aop:config>

</beans>

다음의 드라이버 스크립터가 있다면 표준 출력에 다음과 같이 출력될 것이다.

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.FooService;

public final class Boot {

  public static void main(final String[] args) throws Exception {
    BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
    FooService foo = (FooService) ctx.getBean("fooService");
    foo.getFoo("Pengo", 12);
  }
}


StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)


8.3.3.7 어드바이스 순서
여 러 어드바이스가 같은 조인포인트(메서드 실행)에서 실행되어야 할 때 우선순위 규칙은 Section 8.2.4.7, “어드바이스 순서”에서 설명했다. 관점들 사이의 우선순위는 관점을 지원하는 빈에 Order 어노테이션을 추가하거나 빈이 Ordered 인터페이스를 구현함으로써 결정된다.


8.3.4 인트로덕션
인트로덕션(AspectJ에서는 inter-type 선언이라고 부른다)은 관점이 주어진 인터페이스를 구현한 어드바이즈된 객체를 선언하고 이러한 객체 대신에 주어진 인터페이스의 구현체를 제공하도록 할 수 있다.

인 트로덕션은 aop:aspect내에서 aop:declare-parents 요소를 사용해서 만든다. 이 aop:declare-parents 요소는 새로운 부모(이름)를 가진 타입과의 매칭을 선언하는데 사용한다. 예를 들어 UsageTracked 인터페이스가 주어지고 이 인터페이스가 DefaultUsageTracked의 구현체일 때 서비스 인터페이스의 모든 구현체(implementor)를 선언하는 다음의 관점도 UsageTracked 인터페이스를 구현한다. (예를 들면 JMX로 통계를 노출하기 위해서)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">

  <aop:declare-parents
    types-matching="com.xzy.myapp.service.*+"
    implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
    default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
  
  <aop:before
    pointcut="com.xyz.myapp.SystemArchitecture.businessService() and this(usageTracked)"
    method="recordUsage"/>
  
</aop:aspect>

usageTracking 빈을 지원하는 클래스는 다음 메서드를 가진다.

public void recordUsage(UsageTracked usageTracked) {
  usageTracked.incrementUseCount();
}

구 현되어야 할 인터페이스는 implement-interface 속성으로 결정한다. types-matching 속성의 값은 AspectJ 타입패턴이다. 매칭되는 타입의 모든 빈은 UsageTracked 인터페이스를 구현할 것이다. 위 예제의 before advice에서 서비스 빈을 UsageTracked의 구현체로 직접 사용할 수 있다. 프로그래밍적으로 빈에 접근하려면 다음과 같이 작성한다.

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");


8.3.5 관점 인스턴스화 모델
스키마로 정의한 관점에서 지원하는 인스턴스화 모델은 싱글톤 모델뿐이다. 다은 인스턴스화 모델은 차기 버전에서 지원할 것이다.


8.3.6 어드바이저(Advisors)
" 어드바이저"의 개념은 스프링 1.2에서 정의한 AOP 지원에서 가져온 것으로 AspectJ에는 완전히 같은 것이 없다. 어드바이저는 하나의 어드바이스를 가진 작고 독립적인 관점과 같다. 어드바이스 자체는 빈으로 표현하고 Section 9.3.2, “Advice types in Spring”에서 설명한 어드바이스 인터페이스 중의 하나를 구현해야 한다. 하지만 어드바이저는 AspectJ 포인트컷 표현식의 이점을 취할 수 있다.

스프링 2.0은 어드바이저의 개념을 <aop:advisor> 요소로 지원한다. 스프링 2.0이 지원하는 전용 네임스페이스도 가진 트랜잭션이 가능한 어드바이스와 어드바이저를 결합해서 사용하는 것을 가장 많이 볼 것이다. 다음과 같이 정의한다.

<aop:config>

  <aop:pointcut id="businessService"
      expression="execution(* com.xyz.myapp.service.*.*(..))"/>

  <aop:advisor 
      pointcut-ref="businessService"
      advice-ref="tx-advice"/>
      
</aop:config>

<tx:advice id="tx-advice">
  <tx:attributes>
    <tx:method name="*" propagation="REQUIRED"/>
  </tx:attributes>
</tx:advice>

위의 예제에서 사용한 pointcut-ref 속성처럼 인라인 포인트컷 표현식을 정의하는데 pointcut 속성을 사용할 수도 있다.

어드바이스가 순서대로 참여할 수 있도록 어드바이저의 우선순위를 정의하려면 어드바이저의 Ordered 값을 정의하는 order 속성을 사용해라.


8.3.7 예제
스키마 지원을 사용해서 재작성했을 때 Section 8.2.7, “예제”의 동시성 락(locking)으로 인한 재시도 예제가 어떻게 되는지 보자.

비 즈니스 서비스의 실행은 종종 동시성 이슈때문에 실패할 수 있다.(예를 들면 데드락 실패) 작업을 재시도한다면 이번에는 성공할 가능성이 높아보인다. 재시도를 하는 것이 절적한 이러한 상황에서 클라이언트가 PessimisticLockingFailureException를 보지 않도록 비즈니스 서비스에 대해서 작업 재시도를 투명하게 처리할 것이다. 이는 서비스계층에서 여러 서비스에 걸쳐진 명확한 요구사항이므로 관점으로 구현하는 것이 이상적이다.

작 업을 재시도하기를 원하므로 여러번 proceed를 호출할 수 있도록 around advice를 사용해야 한다. 다음 예제에서 기본적인 관점 구현체가 어떻게 생겼는지 보여주고 있다.(그냥 스키마 지원을 사용하는 보통의 자바클래스다.)

public class ConcurrentOperationExecutor implements Ordered {
   
  private static final int DEFAULT_MAX_RETRIES = 2;

  private int maxRetries = DEFAULT_MAX_RETRIES;
  private int order = 1;

  public void setMaxRetries(int maxRetries) {
    this.maxRetries = maxRetries;
  }
   
  public int getOrder() {
    return this.order;
  }
   
  public void setOrder(int order) {
    this.order = order;
  }
   
  public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { 
    int numAttempts = 0;
    PessimisticLockingFailureException lockFailureException;
    do {
      numAttempts++;
      try { 
        return pjp.proceed();
      }
      catch(PessimisticLockingFailureException ex) {
        lockFailureException = ex;
      }
    }
    while(numAttempts <= this.maxRetries);
    throw lockFailureException;
  }
}

관 점이 Ordered 인터페이스를 구현하고 있으므로 트랜잭션 어드바이스보다 관점의 우선순위를 높게 설정할 수 있따.(재시도 할 때마다 새로운 트랜잭션을 사용하길 원한다.) maxRetries와 order 프로퍼티는 둘다 스프링이 설정할 것이다. 핵심 동작은 doConcurrentOperation around advice 메서드에서 이뤄진다. proceed를 시도하고 PessimisticLockingFailureException로 실패했을 때 재시도 횟수를 모두 소진하지 않고 그냥 다시 시도한다.

이 클래스는 @AspectJ 예제에서 사용했던 것과 동일하지만 어노테이션은 제거했다.

동일한 스프링 설정은 다음과 같다.

<aop:config>

  <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

    <aop:pointcut id="idempotentOperation"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>
       
    <aop:around
       pointcut-ref="idempotentOperation"
       method="doConcurrentOperation"/>
  
  </aop:aspect>

</aop:config>

<bean id="concurrentOperationExecutor"
  class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>  
</bean>

당장은 모든 비즈니스 서비스가 멱등이라고 가정한다. 순수한 멱등 작업만 재시도 하기 위해 관점을 개선할 수 있는 상황이 아니라면 Idempotent 어노테이션을 사용하고

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
  // marker annotation
}

서비스 작업의 구현체에 어노테이션을 사용한다. 멱등작업만 재시도하도록 관점을 변경하는 것은 @Idempotent 작업만 매칭되도록 포인트컷 표현식을 개선하는 것을 포함한다

<aop:pointcut id="idempotentOperation"
    expression="execution(* com.xyz.myapp.service.*.*(..)) and
        @annotation(com.xyz.myapp.service.Idempotent)"/>

2012/09/30 04:43 2012/09/30 04:43