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) {
  // 어드바이스 구현체
}
Java


이 예제가 동작하려면 컬렉션의 모든 요소를 검사해야 하는데 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>
XML


멱등작업만 재시도를 하는 것으로 관점을 개선하려면 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>
XML


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


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

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

<aop:config>

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

</aop:config>
XML


포 인트컷 표현식 자체는 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>
XML


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


@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>
XML


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

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


포 인트컷 하위 표현식을 결합할 때 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


이 방법으로 정의한 포인트컷은 해당 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>
XML


여기서 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>
XML


@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>
XML


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

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

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


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

public void doAccessCheck(Object retVal) {...
Java



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


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

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

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


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

public void doRecoveryActions(DataAccessException dataAccessEx) {...
Java



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



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


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

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



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"/>
XML


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);
  }
}
Java


다 음은 관점이다. 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());
    }
  }
}
Java


마지막으로 다음은 특정 조인포인트에서 위의 어드바이스가 실행되는데 필요한 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>
XML


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

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);
  }
}
Java

 

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



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


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

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


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

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



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


위의 예제에서 사용한 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;
  }
}
Java


관 점이 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>
XML


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

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


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

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

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) {
  // ...
}
Java


포 인트컷 표현식의 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) {
  // ...
}
Java


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

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

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

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


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

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


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

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


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

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


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

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


이 예제가 동작하려면 컬렉션의 모든 요소를 검사해야 하는데 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을 사용한다
    }
    
    Java

    첫 파라미터가 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를 사용한다
    }
    
    Java

    JoinPoint, ProceedingJoinPoint, JoinPoint.StaticPart 타입의 첫 파라미터를 특별하게 처리하는 것은 다른 어떤 조인포인트 컨텍스트도 수집하지 않는 어드바이스에 특히 편리하다. 이러한 경우에는 그냥 "argNames" 속성을 생략한다. 예를 들어 다음의 어드바이스는 "argNames" 속성을 선언할 필요가 없다
    @Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
    public void audit(JoinPoint jp) {
      // ... jp를 사용한다
    }
    
    Java
  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});
}
Java


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


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();
  }
}
Java


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

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



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() {
    // ...
  }    
}
Java


'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;
  }
}
Java


관 점이 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>
XML


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

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


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

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



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


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


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

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

<aop:config>

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

</aop:config>
XML


포 인트컷 표현식 자체는 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>
XML


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


@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>
XML


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

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


포 인트컷 하위 표현식을 결합할 때 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


이 방법으로 정의한 포인트컷은 해당 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>
XML


여기서 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>
XML


@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>
XML


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

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

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


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

public void doAccessCheck(Object retVal) {...
Java



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


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

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

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


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

public void doRecoveryActions(DataAccessException dataAccessEx) {...
Java



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



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


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

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



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"/>
XML


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);
  }
}
Java


다 음은 관점이다. 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());
    }
  }
}
Java


마지막으로 다음은 특정 조인포인트에서 위의 어드바이스가 실행되는데 필요한 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>
XML


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

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);
  }
}
Java

 

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



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


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

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


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

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



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


위의 예제에서 사용한 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;
  }
}
Java


관 점이 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>
XML


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

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


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

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

 

AWS에 접속 가능한 상태이고, Spring 프로젝트가 이미 하나 이상의 RequestMapping이 실행 가능한 상태라고 가정한 상태라고 가정합니다. Docker Image로 Spring 배포하는 법을 알아봅시다.

 

이 글은 도커에 대한 설명이나 Spring에 대한 설명을 포함하고 있지 않습니다. 오로지 배포하는 법만 알려드립니다.

 

Spring boot 프로젝트의 Docker 배포에 대한 공식 사이트 가이드는 링크는 여기를 참고해주시면 됩니다.

 


 

Docker 설치

아래의 링크의 설명대로 OS에 맞게 Docker를 설치해줍니다.

 

 

docker image 만들기

1. Spring boot project 만들고 jar file을 생성하기

start.spring.io 에서 Spring 프로젝트를 만들어 줍니다.

 

jar file을 생성해줍니다.

 

  • Maven
$ mvn package
  • Gradle
$ ./gradlew build

 

2. 도커 이미지 만들어 주기

프로젝트 최상단 디렉토리에 Dockerfile를 만들어줍니다. (build.gradle이나 pom.xml과 같은 디렉토리 내)

docker 명령어의 디폴트 옵션 중 Dockerfile를 읽어 명령을 실행하기 때문에 이름은 꼭 이름을 "Dockerfile"로 만들어주세요.

 

Dockerfile

FROM openjdk:11
ARG JAR_FILE=build/libs/**-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

 

배포할 Docker Image를 관리하기 위해서는 Docker hub에 아이디를 생성 해주셔야합니다. (생성 링크: create one)

 

command나 bash를 실행해 Dockerfile 가 있는 디렉토리로 가서 아래 명령어를 실행해줍니다.

편의상 image 이름을 demo로 사용하였습니다. Docker build의 공식 설명은 여기를 확인해주세요.

 

$ docker build -t [당신의 도커 아이디]/demo .

docker image를 배포해줍니다.

$ docker push [당신의 도커 아이디]/demo

 

Server Instance에서 도커 컨테이너 배포

let’s connect again to our ec2 instance: -> 자신의 서버 인스턴스로 접속해주세요.

$ ssh -i /path/my-key-pair.pem ec2-user@<EC2 ip address>

1. Install docker on Server Instance

해당 서버에 Docker를 설치해주세요. 아래 링크 참고

https://hub.docker.com/search?offering=community&operating_system=linux&q=&type=edition 

 

Docker Hub

 

hub.docker.com

EC2 docker installing example

# 1. update yum

[ec2-user ~]$ sudo yum update -y

# 2. Install Docker

[ec2-user ~]$ sudo yum install docker -y

# 3. Start the Docker Service

[ec2-user ~]$ sudo service docker start

# 4. Add the ec2-user to the docker group so you can execute Docker commands without using sudo.

[ec2-user ~]$ sudo usermod -a -G docker ec2-user

 

어플리케이션 테스트 하기

Now you can run your application like this:

[ec2-user ~]$ docker run -p 80:8080 <Your Docker Hub account>/demo
이렇게 성공해보길 바란다

 

더 나아가보자!

 

Build tool을 사용하여 도커 이미지 만들기

Build a Docker Image with Gradle

You can build a tagged docker image with Gradle in one command:

./gradlew bootBuildImage --imageName=springio/gs-spring-boot-docker

 

Build a Docker Image with Maven

To get started quickly, you can run the Spring Boot image generator without even changing your pom.xml (remember that the Dockerfile, if it is still, there is ignored):

./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=springio/gs-spring-boot-docker

 

Docker 기본 명령어

docker ps
CONTAINER ID        IMAGE                                   COMMAND                  CREATED             STATUS              PORTS                    NAMES
81c723d22865        springio/gs-spring-boot-docker:latest   "java -Djava.secur..."   34 seconds ago      Up 33 seconds       0.0.0.0:8080->8080/tcp   goofy_brown
docker stop goofy_brown
docker rm goofy_brown

 

Using Spring Profiles

프로필 기능 사용하기

 

"prod"

docker run -e "SPRING_PROFILES_ACTIVE=prod" -p 80:8080 -t lsmman/demo

 

"dev"

docker run -e "SPRING_PROFILES_ACTIVE=dev" -p 80:8080 -t lsmman/demo

 

Reference

 

 

Spring Wiki 읽으면서 모르는 거 다 찾아보는 글입니다.

모르는 내용이 나오면 바로 검색 들어가기 때문에, 맥락 없이 새로운 내용으로 넘어갈 수 있습니다.

 

 

Spring Framework - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Application framework for Java platform The Spring Framework is an application framework and inversion of control container for the Java platform. The framework's core features can be

en.wikipedia.org

내가 읽을 Spring Framework Wikipedia 링크

 


 

아래의 글을 내 멋대로 해석하며 읽어보겠다.

The Spring Framework is an application framework and inversion of control container for the Java platform. The framework's core features can be used by any Java application, but there are extensions for building web applications on top of the Java EE (Enterprise Edition) platform. Although the framework does not impose any specific programming model, it has become popular in the Java community as an addition to the Enterprise JavaBeans (EJB) model. The Spring Framework is open source.

 

 

SpringFrame는 어플리케이션 프레임워크이다. 즉, 사람이 사용할 프로그램을 만드는데 필요한 그 뼈대와 프로그램의 복잡한 문제를 해결할 방식 등을 정의해둔 거대한 라이브러리다. 

 

그리고 Java platform 상의 IoC 컨테이너이다. Spring은 IoC 컨테이너의 구현을 위해 JavaBeans (EJB) 모델을 채택하고 있다. IoC 컨테이너는 개발자 입장에서 객체 관리에 대한 의존성을 주입하는 것 (Dendency Injection)이라고도 볼 수 있기 때문에, IoC 컨테이너와 DI는 거의 같은 용어이다. 

 

공식 사이트의 IoC 컨테이너 설명을 보면 객체의 컴포넌트 의존관계 설정, 프레임워크 내부의 설정 및 객체 생명주기을 관리하기 위한 디자인 패턴이나 구조체라고 나와있다. IoC는 컨트롤의 역전, 반전 정도로 해석 가능한데, 나는 개발자가 신경 쓸 필요 없도록 객체의 의존관계나 설정, 생명주기 관리의 책임을 넘기는 것이라고 본다. 그러니까 개발하는 입장에서는 아주 편리한 기능인 거다. (직접 관리하기 힘드니까 관리해줘라.) 

 

이 프레임워크의 핵심 기능은 모든 자바 어플리케이션에서 사용 가능하지만, 특히 Java EE 위에서 웹 사이트를 만들 때 유용한 확장 기능들이 몇 가지 있다. 이 프레임워크는 특정한 프로그래밍 모델을 도입하지는 않았지만, EJB model의 추가 버전으로서 Java 커뮤니티에서 인기를 얻고 있다. (EJB model은 Enterprises JavaBeans 모델로 프레임워크에 비즈니스 로직에 대한 액세스 권한을 부여하는 모델이다.)

 

스프링 프레임워크는 오픈소스이다.

 


 

참고한 링크들

- SAP의 EJB model 설명

https://help.sap.com/saphelp_nw73/helpdata/en/4b/f41f2bc0c33de4e10000000a42189e/content.htm?no_cache=true
- IoC 내용 추가 참조 블로그
https://dev-coco.tistory.com/80

 

오픈소스라면 언젠가 나도 기여하리라.

천리 길도 한 걸음부터

현재 동작하는 서버의 성능을 알아보고, 늘려보자!

서버 성능 기본 지표: 응답 시간과 처리량

클라이언트 (브라우저) <-> 서버 <-> DB

클라이언트, 서버, DB로 이루어진 기본 서버의 모습이다.

가장 주요한 기본 지표는 응답 시간과 처리량이다.

처리량 (Transaction Per Second)

  • 처리량은 초당 몇 개의 요청을 처리할 수 있는 지에 관한 지표이다. 약어로 TPS라고도 부른다.

응답 시간 (Response Time)

  • 응답 시간은 클라이언트가 어떤 동작을 수행했을 때 서버와 DB를 거쳐 동작이 수행 될 때까지의 총 응답 시간을 말한다.
  • 응답 시간은 대기 시간 (Latency Time)과 처리 시간 (Processing Time)으로 나눌 수 있다.
    • 대기 시간은 클라이언트의 요청이 서버에 도달할 때까지의 시간이다.
    • 처리 시간은 서버가 DB를 읽고 쓰는 처리에 필요한 총 시간을 말한다.

처리량 (TPS)를 높이려면

더 좋은 스펙의 컴퓨팅 서버 쓰기 (Scale Up)

  • CPU와 Memory 성능을 높인다. 더 빠르게 처리 가능하고 더 많은 양의 데이터를 메모리에 올려 빠르게 처리 가능하다.
  • DB의 경우 처리량 높은 DB 머신 쓰기

서버 늘리기 (Scale Out)

  • 서버 1대가 10 TPS라면 2대면 20 TPS
  • DB의 경우 쓰레드 풀 만들기 (쓰레드 풀 5 -> TPS 5, 쓰레드 풀 10 -> TPS 10)

Reference

  • 최범균님 서버 성능 올리기 영상 :Link

'개발 관심사' 카테고리의 다른 글

소프트웨어 신뢰성 선언문  (0) 2024.03.04
MSA 관련 자료집  (0) 2022.04.13
Gradle과 Maven  (0) 2022.02.16
Apache Log4j 원격코드 실행 취약점 (cve-2021-44228)  (0) 2021.12.14

Gradle과 Maven에 대해 알아보고 비교하는 글입니다.

 

이 글에서는 Gradle과 Maven의 고급 문법을 다루지 않습니다.


Gradle vs Maven

  • Gradle에 비해 Maven이 점유율이 높은 상황 (2022-02 기준, 점차 Gradle의 점유율은 상승 중)
  • 처리 성능 : Gradle > Maven
  • 대규모 프로젝트에서의 성능 : Gradle > Maven
  • 빌드 관리 파일
    • Maven : pom.xml
    • Gradle : build.gradle
  • Gradle은 Gradle Wrapper의 도움으로 설치 없이 사용할 수 있다.

비교하고 보니 Gradle 써야겠다


그리고 더 알아보자!

Gradle과 Maven은 빌드 관리 도구입니다.

빌드 관리 도구란

  • 프로젝트에서 필요한 xml, properties, jar, yaml 파일들을 자동으로 인식하여 빌드해주는 도구
  • 소스 코드를 컴파일, 테스트, 정적분석 등을 하여 실행가능한 앱으로 필드해줌
  • 프로젝트 정보 관리, 테스트 빌드, 배포 등의 작업을 진행
  • 외부 라이브러리를 참조하여 자동으로 다운로드 및 업데이트의 관리 제공
  • 개발자들이 편하게 설정들과 dependency들을 관리하고 다른 개발자들과 협업하기 편하게 하기 위해 사용함

Java의 대표적인 빌드 관리 도구

  • Ant
  • Maven
  • Gradle

Maven

개발된 이유

  • Ant는 빌드 관리 기능을 제공하지만 개발자가 소스의 위치와 작업의 순서(라이프싸이클)를 모두 정해줘야 했다.
  • 대규모 프로젝트에서 복잡해지는 경향이 있었다.
  • Maven은 Ant를 대체하기 위해 개발되었다.
  • Maven은 Pom.xml으로 편하게 의존성 관리를 할 수 있고, 필수적인 소스 위치와 작업의 순서가 정해져 있다.

pom.xml의 역할

  • 프로젝트 정보 관리
  • 해당 프로젝트에서 사용하는 외부 라이브러리 관리
  • 해당 프로젝트의 빌드 관련 설정

주로 사용하는 대표 태그 설명

Maven의 주요 태그다

Gradle

  • Groovy 스크립트를 활용한 빌드 관리 도구
  • 안드로이드 프로젝트의 표준 빌드 시스템으로 채택
  • 멀티 프로젝트 (Multi-Project)의 빌드에 최적화 하여 설계됨
  • Maven에 비해 더 빠른 처리속도를 가지고 있음
  • Maven에 비해 더 간결한 구성이 가능함

주로 사용하는 대표 태그 설명

  • repositories : 라이브러리가 저장된 위치 등 설정
  • mavenCentral : 기본 Maven Repository
  • dependencies : 라이브러리 사용을 위한 의존성 설정

Reference

이 글의 대부분의 내용은 우아한 형제들 Gif-flow 글에서 참고했음을 밝힙니다.

 

우린 Git-flow를 사용하고 있어요 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요. 우아한형제들 배민프론트개발팀에서 안드로이드 앱 개발을 하고 있는 나동호입니다. 오늘은 저희 안드로이드 파트에서 사용하고 있는 Git 브랜치 전략을 소개하려고 합

techblog.woowahan.com


Git flow

 

Git-Flow는 각각 feature가 독립적이면서 연속적으로 개발 될 수 있도록 하는 전략 중에 하나입니다.

 

Git flow를 이보다 잘 설명할 수 있을까

Git-flow에는 5가지 종류의 브랜치가 존재합니다. 항상 유지되는 메인 브랜치들(master, develop)과 일정 기간 동안만 유지되는 보조 브랜치들(feature, release, hotfix)이 있습니다.

  • master : 제품으로 출시될 수 있는 브랜치
  • develop : 다음 출시 버전을 개발하는 브랜치
  • feature : 기능을 개발하는 브랜치
  • release : 이번 출시 버전을 준비하는 브랜치
  • hotfix : 출시 버전에서 발생한 버그를 수정 하는 브랜치

git에서는 git flow 명령어를 제공하기도 합니다. git flow 전략을 그대로 명령어로 만든 형태입니다.

 

git-flow cheatsheet

 

danielkummer.github.io

 

action은 init, start, finish, publish, pull이 있고 아래는 주로 사용하게 되는 feature branch 관리 코드 예시입니다.

# 브랜치 추가
# feature/FeatureName 브랜치가 develop를 base로 생성됩니다.
$ git flow feature start [FeatureName]

# 브랜치 종료
# feature/FeatureName 브랜치가 develop로 merge되고 그 후 삭제됩니다.
$ git flow feature finish [FeatureName]

# 그 외 publish와 pull도 있습니다.

 

git flow를 위한 상황별 git 명령어들

 

하지만, 우리는 정해진 기능만 사용할 수가 없습니다. 기능 구현을 위해 일부 기능이 필요하거나, 개발하다가 코드가 충돌하거나 등등의 다양한 상황이 생기기 때문이죠.

 

몇 가지 상황별로 명령어를 모아 준비해봤습니다.

 

1. develop 브랜치에서 feature 브랜치를 생성

$ git switch develop
$ git pull origin develop
$ git checkout -b feature/user –track develop

생성 후에 소스코드 수정을 해서 변경사항을 commit 해줍니다.

 

 

2. 불필요하게 나눠진 커밋 합치기

 

아래와 같이 3개의 커밋(first commit, second commit, third commit)을 했다.

 

 

3개의 커밋을 1개의 커밋으로 합치고 싶은데 어떻게 해야할까?

이 때 git rebase를 사용하면 되는데, 명령어는 아래와 같습니다.

 

git rebase -i HEAD~3

 

 

그러고 나서, second commit, third commit 앞에 pick이라고 되어 있는 부분을 squash라고 수정한 뒤,

 

 

저장하고 빠져 나오면 아래와 같은 화면이 나옵니다.

 

 

제일 윗 줄에 새로 추가할 커밋 메시지를 작성해줍니다.

주의) 그 아래에 있는 first commit, second commit, third commit을 그대로 놔두면 커밋 메시지에 같이 찍히게 됩니다.

 

 

그리고 git log를 통해 합쳐진 내용을 확인하면 커밋이 잘 합쳐진 것을 확인할 수 있습니다.

git log


이미지 출처: DevNote

 

 

3. develop 변경사항을 feature로 가져와서 push 하기

 

작업을 할 때 브랜치의 수명은 되도록 짧게 가져가는 게 좋지만, feature 브랜치에서 기능을 완료하는데 해야 할 작업들이 많아서 오래 걸리는 경우 들이 있습니다. 그러다 보면 develop에 추가된 기능들이 필요한 경우가 종종 생기게 됩니다. 그럴 때는 feature 브랜치에 develop의 변경사항들을 가져와야 합니다.

# feature/user 브랜치에 develop 브랜치를 merge 합니다.
$ git switch feature/user
$ git merge –no-ff develop

# develop의 변경사항이 merge된 feature/user를 origin에 push 합니다.
$ git push origin feature/user

 

4. 앱 출시

 

발생하는 버그들을 모두 수정했다면 이젠 출시를 준비할 때입니다. release 브랜치를 master 브랜치와 develop 브랜치에 merge하고 마지막으로 master 브랜치에서 버전 태그를 달아줍니다.

  1. release 브랜치를 최신 상태로 갱신합니다.
    (release-1.0.0)]$ git pull upstream release-1.0.0​
  2. release 브랜치를 develop 브랜치에 merge 합니다.
    (release-1.0.0)]$ git checkout develop
    (develop)]$ git pull upstream develop
    (develop)]$ git merge –no-ff release-1.0.0​
  3. develop 브랜치를 upstream에 push 합니다.
    (develop)]$ git push upstream develop​
  4. release 브랜치를 master 브랜치에 merge 합니다.
    (develop)]$ git checkout master
    (master)]$ git pull upstream master
    (master)]$ git merge –no-ff release-1.0.0​
  5. 1.0.0 태그를 추가합니다.
    (master)]$ git tag 1.0.0​
  6. master 브랜치와 1.0.0 태그를 upstream에 push 합니다.
    (master)]$ git push upstream master 1.0.0​

 

EC2 프리티어의 메모리, 1GB는 터무니 없이 작다.

Jenkins 하나만 돌려도 메모리 1GB의 대부분을 사용한다. 적은 메모리로 프로그램을 돌리다보면 인스턴스 자체가 죽어버리는 일이 자주 볼 수 있다.

 

그 중 하나의 해결책으로 하드디스크를 가상메모리로 사용하여 메모리 양을 늘려주는 방법에 대해 소개하겠다.

 

본 글은 아래 2개의 포스팅을 참고하여 작성하였다.

 

 

 

스왑 파일을 사용하여 Amazon EC2 인스턴스의 스왑 공간으로 메모리 할당

1.    dd 명령을 사용하여 루트 파일 시스템에 스왑 파일을 생성합니다. 명령에서 bs는 블록 크기이고 count는 블록 수입니다. 스왑 파일의 크기는 dd 명령의 블록 크기 옵션에 블록 수 옵션을 곱

aws.amazon.com

 

 

OKKY | AWS EC2 프리티어 쓰시는분들 참고하세요!

일기장에 기록해둔건데 프리티어 쓰시는 분들에겐 좋은 정보일것 같아 남깁니다. 제 일기장에서 긁어온거라 폼이 많이 깨지긴 했는데 감안하고 봐주세요!   AWS 프리티어 EC2를 사용 중 겪은 문

okky.kr


하드디스크를 가상 메모리로 사용하기 

 

AWS에서는 메모리의 양에 따라 스왑 메모리의 크기를 아래와 같이 권장하고 있다.

물리적 RAM의 양 권장 스왑 공간
RAM 2GB 이하 RAM 용량의 2배(최소 32MB)
RAM 2GB 초과, 32GB 미만 4GB + (RAM – 2GB)
RAM 32GB 이상 RAM 용량의 1배

참고: 스왑 공간은 절대로 32GB 미만이 되지 않아야 한다.

 

스왑 파일 생성

$ sudo dd if=/dev/zero of=/swapfile bs=128M count=16

1. dd 명령을 사용하여 루트 파일 시스템에 스왑 파일을 생성한다.

명령에서 bs는 블록 크기이고 count는 블록 수이다.

지정한 블록 크기는 인스턴스에서 사용 가능한 메모리보다 작아야 한다.

그렇지 않으면 memory exhausted 오류가 발생한다.

 

프리티어의 메모리는 1GB이니, 권장사항대로라면 2GB를 증설시켜야 한다.

이 예제 dd 명령에서 스왑 파일은 2GB(128MB x 16 = 2,048MB)이다.

4GB를 늘리고 싶다면 4GB(128MB * 32)에 해당되도록 블록 count 값을 32로 설정해주면 된다.

 

$ sudo chmod 600 /swapfile

2. 스왑 파일에 대한 읽기 및 쓰기 권한을 업데이트

 

$ sudo mkswap /swapfile

3. Linux 스왑 영역을 설정

 

$ sudo swapon /swapfile

4. 스왑 공간에 스왑 파일을 추가하여 스왑 파일을 즉시 사용할 수 있도록 만든다

 

$ sudo swapon -s

5. 절차가 성공했는지 확인

 

$ sudo vi /etc/fstab

6. /etc/fstab 파일을 편집하여 부팅 시 스왑 파일을 활성화

 

/swapfile swap swap defaults 0 0

편집기에서 파일을 연 후 파일 끝에 다음 줄을 새로 추가하고 파일을 저장한 다음 종료한다.

 

7. free를 다시 입력하여 메모리를 확인해본다. 아래처럼 Swap 영역이 2GB로 되어있으면 성공.

$ free
              total        used        free      shared  buff/cache   available
Mem:         989140      151748      198108         444      639284      695068
Swap:       2097148           0     2097148

 

'AWS' 카테고리의 다른 글

서버리스  (0) 2023.09.05
클라우드 컴퓨팅, IaaS, PaaS, SaaS  (0) 2021.07.28

Java 8 버전은 지원 종료가 머지 않았으므로 Java 11 버전 기준으로 설치하였다.

각설하고 바로 설명하겠다.


1. AWS EC2 인스턴스를 만든다. (글 링크)

2. EC2에 접속한다. (글 링크)

    - Windows 라면 Putty를 사용해 접속하면 된다. (링크)

 

3. 아래 script로 Java를 설치한다.

sudo yum update -y

# 자바 11 설치
# 설치 가능한 Java 버전 확인
# yum list java*jdk-devel

## Example)
## Available Packages
## java-1.7.0-openjdk-devel.x86_64  1:1.7.0.261-2.6.22.2.amzn2.0.2 amzn2-core
## java-1.8.0-openjdk-devel.x86_64  1:1.8.0.312.b07-1.amzn2.0.2    amzn2-core

# Amazon Linux에 Java 11 설치
sudo amazon-linux-extras install java-openjdk11

# yum 패키지의 Java 8 설치
sudo yum install -y java-1.8.0-openjdk
# 자바 버전 11로 설정
# 아래 처럼 나오면 Java 11인 1번을 선택
sudo alternatives --config java

# $ sudo alternatives --config java

# There are 2 programs which provide 'java'.

#   Selection    Command
# -----------------------------------------------
#  + 1           java-11-openjdk.x86_64 (/usr/lib/jvm/java-11-openjdk-11.0.13.0.8-1.amzn2.0.3.x86_64/bin/java)
# *  2           java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.312.b07-1.amzn2.0.2.x86_64/jre/bin/java)

# Enter to keep the current selection[+], or type selection number: 1

+ Java Version 확인

# 자바 버전 확인
java -version

# openjdk version "11.0.13" 2021-10-19 LTS
# OpenJDK Runtime Environment 18.9 (build 11.0.13+8-LTS)
# OpenJDK 64-Bit Server VM 18.9 (build 11.0.13+8-LTS, mixed mode, sharing)

 

4. Jenkins, Git, Docker 설치

# jenkins 패키지 추가
sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat/jenkins.io.key
# install docker, git
sudo yum install -y git docker

# 자바 11버전에서 젠킨스 설치
sudo yum install -y epel-release # 혹시 안되면  sudo amazon-linux-extras install epel 로 먼저 설치
sudo yum install -y java-11-openjdk-devel
sudo yum install -y jenkins

# 젠킨스 실행
service jenkins start

 

5. Jenkins로 접속

   - 해당 AWS EC2의 "public IP 주소:8080" 으로 접속하면 다음처럼 화면이 나온다.

   - 아애 접속이 안된다면 해당 AWS EC2의 보안그룹의 inbound rule를 확인해보길 바란다. (링크)

 

그 후 아래 명령어로 비밀번호를 확인하고 입력하여 Continue를 누른다.

# 젠킨스 비밀번호 확인
sudo cat /var/lib/jenkins/secrets/initialAdminPassword

 

다음과 같은 화면이 나오면 lock을 풀고 성공적으로 접속한 것이다.

 

6. Plugins 설치

제시된 플러그인 외에 특정한 플러그인이 필요하지 않아 왼쪽의 "Install suggested plugins"으로 설치하였다

 

7. Admin 계정 만들기

 

8. You're ready to use Jenkins

  •  계정을 만들어주고 젠킨스 주소 확인을 해주면 젠킨스를 사용할 준비를 마쳤다.

+ Recent posts