본문 바로가기

spring

Spring AOP - @Aspect 애노테이션 (<aop:aspectj-autoproxy proxy-target-class="true" />)

AOP 개념

사용자 삽입 이미지
애니매이션 기능을 사용해서, 각각의 (횡단)로직들이 여러 클래스에 분산되어 들어가는 모습을 보여주면 더 멋질 것 같습니다.

사용자 삽입 이미지


Spring 2.0에서는 AspectJ에서 제공되는 애노테이션을 이용해 POJO로 Aspect를 작성 할 수 있습니다.
AspectJ를 전략적으로 이용하기 때문에 Spring2.0 에서의 AOP도 여전히 Proxy방식으로 작동 됩니다.

출처 : http://whiteship.tistory.com/1958


자금 이체를 하는 프로그램을 작성한다고 생각해보자. 

출금계좌와 입금계좌, 그리고 이체금액을 입력받아 SQL 문장 또는 함수 한 번 돌리는 것으로 끝나는가? 

절대 아니다. 먼저 해킹을 방지하기 위해 사용자가  적절한 보안 프로그램을 설치했는지 점검하는 코드도 있어야 하고, 

사용자가 인증 되었는지 점검하는 코드도 써야 하고, 상대방 은행에서 적절하게 처리되었는지 점검해야 하고,

혹시 사용자가 이체버튼을 두번 누른 것은 아닌지 체크해야 하고 로그도 남겨야 한다.

즉, 구현하려고 하는 기능 뿐 아니라 보안, 인증, 로그, 성능와 같은 다른 기능도 들어있어야 한다.

어쩌면 이체를 위한 코드보다 잡다한 측면의 문제를 다루는 코드가 더 길어질 수 있다.

이런 코드들은 입금이나 출금 같은 다른 곳에서 들어가야 한다. 

구현하려고 하는 비즈니스 기능들을 Primary(Code) Concern, 보안, 로그, 인증과 같이 

시스템 전반적으로 산재딘 기능들을 Cross-cutting concern 이라고 부른다.

Aop는 Cross-cutting concern 을 어떻게 다룰 것인가에 대한 새로운 패러다임이라고 할수 있다. 


Aspect는   Advice + Point-cut을 함께 지칭하는 단어이다.

Point-cut은 어떤 Advice를 code 어느 위치에 둘 것인가 하는 것이다.

예를 들면 로그 기능을 구현한 Advice는 code 속에 있는 모든 public method가 수행되고 나면

그 마지막에 실행되어라 라고 지정한 것이라 할 수 있다.


출처 : http://endlessendeavor.tistory.com/107

Aspect
@Aspect 애노테이션은 클래스 레벨에 위치하며 해당 클래스가 Aspect임을 명시합니다.

@Aspect
public class ExampleAspect {
    ...
}
이 클래스는 pointcut, advice, introduction 을 가질 수 있습니다.

@Aspect 애노테이션이 존재하는 클래스를 찾아서 Aspect로 인식하기 위해 후처리기를 등록 합니다.
다음 두 가지 방법은 동일 합니다.
aop 스키마
<aop:aspectj-autoproxy proxy-target-class="true" />

DTD 기반
<bean
 class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator">
    <property name="proxyTargetClass" value="true" />
</bean>
두 경우 모두 CGLIB 사용 여부를 설정 할 수 있습니다.
하지만 aop스키마를 이용할 경우 세부설정들(exposeProxy, 등등)은 설정 할 수가 없더군요.
혹시 있다면 알려주세요.

Pointcut
@Pointcut 애노테이션을 이용해 pointcut을 정의 합니다.
Spring AOP는 메소드 Joinpoint만 지원하기 때문에 메소드레벨에서만 사용 가능 합니다.
아래 간단한 예제는 모든 set이라는 이름을 포함한 메소드 Joinpoint를 나타냅니다.
@Pointcut("execution(* set*(..))")
private void examplePointcut() {}
중요한 것은 메소드 자체는 @Pointcut 애노테이션을 식별하기 위한 id로 사용되기 때문에 몸체가 없습니다.
Pointcut을 나타내는 메소드는 반드시 리턴타입이 void여야 하며 접근제어자는 일반적인 메소드와 
다를 것 없이 접근성을 나타냅니다. 자세한 Pointcut 표현식 문법은 Spring 래퍼런스 6장을 참고 하세요.

오직 메소드 Joinpoint만 지원 하는 이유로 AspectJ의 모든 Pointcut 지시자를 사용 할 수 없습니다.
만약 지원되지 않는 지시자를 사용할 경우 IllegalArgumentException이 던져 집니다.
Spring AOP에서 사용 가능한 Pointcut 지시자는 다음과 같습니다.
execution(메소드패턴) - 메소드패턴에 해당하는 Joinpoint를 나타냅니다.
within(타입패턴) - 타입패턴에 해당하는 클래스의 모든 Joinpoint를 나타냅니다.
this(타입) - 타입과 일치하는 객체의 모든 Joinpoint를 나타냅니다.
target(타입) - 타입과 일치하는 Target 객체의 모든 메소드 Joinpoint를 나타냅니다.
args(타입) - 타입과 메소드 인자의 타입이 일치하는 모든 Joinpoint를 나타냅니다.
@target(애노테이션) - 지정된 애노테이션을 가지고 있는 Target 객체의 모든 Joinpoint를 나타냅니다.
@args(애노테이션) - 지정된 애노테이션과 런타임시 메소드 인자로 전달된 객체의 애노테이션이 일치하는
                              Joinpoint를 나타냅니다.
@within(애노테이션) - 지정된 애노테이션을 가지고 있는 객체의 모든 Joinpoint를 나타냅니다.
Spring AOP는 런타임 Proxy방식이기 때문에 this와 target 지시자는 같은 객체를 참조 하며
@target과 @within 지시자 역시 같은 객체를 참조 합니다.

Pointcut 연산
Pointcut 표현은 '&&' , '||', '!' 를 이용하여 논리연산을 할 수 있고, id로써 연산도 가능 합니다.
아래 예제에서 examplePointcut3() 는 x.y.z.service 나 하위 패키지 모든 클래스의 set 이나 get으로 
시작하는 모든 public 메소드를 나타냅니다.
@Pointcut("execution(public * set*(..)) || execution(public * get*(..))")
private void examplePointcut() {}
    
@Pointcut("x.y.z.service..*")
public void examplePointcut2() {}
    
@Pointcut("examplePointcut() && examplePointcut2()")
public void examplePointcut3() {}

Pointcut의 참조
외부 클래스에 정의된 Pointcut을 참조하기 위해 다음과 같이 합니다.
@Before("x.y.z.ExamplePointcut.beforePointcut()")
public void beforeAdvice() {
   ...
}
해당 Pointcut이 있는 클래스를 인스턴스화할 필요는 없습니다. 단지 리플렉션을 통해 메소드에 선언된
애노테이션만 얻어오면 되기 때문입니다.

Advice
Before Advice
@Before 애노테이션을 이용해 정의하며 인자로 Pointcut의 id를 사용하거나 
직접 in-line형식으로 표현식을 기술할 수 있습니다. (in-line 방식은 다른 Advice에서도 사용 가능 합니다.)
@Before("examplePointcut()")
public void beforeAdvice() {
    System.out.println("호출 전 입니다.");
}

또한 메소드의 첫번째 인자로 JointPoint를 선언하면 Joinpoint에 대한 정보를 얻을 수 있습니다.
@Before("examplePointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
    System.out.println(joinPoint.getTarget());
        for (Object args : joinPoint.getArgs()) {
            System.out.println(args);
        }
}

마지막 예제 입니다.
@Before("examplePointcut() && args(msg)")
public void beforeAdvice(String msg) {
    System.out.println(msg);
}
@Before 애노테이션의 Pointcut에 '&&' 연산자를 이용해 args 지시자를 추가 했습니다.
그런데 이전에 알던 것과 다르게 args 지시자의 인자가 타입이 아니고 매개변수명으로 되어 있습니다.
Target 객체의 호출되는 메소드로 전달되는 인자를 Advice에서 받고 싶다면 위 예제의 문법을 사용해야 합니다.
또한 JoinPoint와 같이 받는것도 가능하며 이 때 JoinPoint는 args 지시자의 인자와 관계 없이 첫번째 매개변수로 하면 됩니다.

After-Returning Advice
@AfterReturning 애노테이션을 이용해 정의하며 Target 객체의 메소드가 정상적으로 종료되었을 때
호출 됩니다.
@AfterReturning("examplePointcut()")
public void beforeAdvice() {
    System.out.println("호출 후");
}

Target 객체의 호출되는 메소드의 리턴값을 받을 수 있습니다.
또한 Before Advice 처럼 첫번째 매개변수를 JoinPoint로 정의할 수 있습니다.
@AfterReturning(pointcut="examplePointcut()", returning="retVal")
public void afterReturningAdvice(JoinPoint joinPoint, Object retVal) {
    ...
}
returning 값은 매개변수명과 동일 합니다.

After-Throwing Advice
@AfterThrowing 애노테이션을 이용해 정의하며 지정된 타입의 예외가 발생했을 때만 적용하려면
throwing 속성을 사용 합니다. throwing 속성은 Advice에 정의된 예외타입의 매개변수명 입니다.
@AfterThrowing(pointcut="examplePointcut()", throwing="ex")
public void afterThrowingAdvice(DataAccessException ex) {
    ...
}
이것 역시 첫번째 매개변수로 JoinPoint를 사용 할 수 있습니다.

After-Finally Advice
@After 애노테이션을 이용해 정의하며 Target 객체의 메소드에서 예외가 발생하든 발생하지 않든 종료되면 
호출 됩니다. 이것은 자원을 해제 하기 위해 사용 됩니다.
@After("examplePointcut()")
public void afterAdvice(JoinPoint joinPoint) {
    System.out.println("자원 해제");
}

Around Advice
@Around 애노테이션을 이용해 정의하며 2.0이전의 방식과 사용법은 동일 합니다.
첫번째 매개변수로 반드시 ProceedingJoinPoint 타입의 변수를 선언해야 합니다. 
@Around("examplePointcut()")
public Object aroundAdvice(ProceedingJoinPoint pjp) {
    System.out.println("호출 전");
    Object retVal = null;
    try {
        retVal = pjp.proceed();
    } catch (Throwable e) {
        e.printStackTrace();
    }
    System.out.println("호출 후");
    return retVal;
}

Advice 적용순서
2.0 이전에서는 Advisor의 order 속성을 이용했으나 2.0이후의 애노테이션 방식에서는
클래스내 선언된 Advice의 순서대로 적용 됩니다.
@Before("examplePointcut()")
public void before1() {
    ...       
}
    
@Before("examplePointcut()")
public void before2() {
    ...
}

@Before("examplePointcut()")
public void before3() {
    ...
}
3개의 Advice 적용 순서는 선언된 순서대로 before1() -> before2() -> before3() 입니다.

Introduction Advice
2.0 이전 방식에서는 특정 클래스를 상속받아서 Introduction Advice를 구현 했습니다.
2.0 이후에서는 POJO로 구현이 가능한데 잠금기능이 추가된 예제를 구현해 보겠습니다.

일반 bean 입니다.
  1. public class Foo {  
  2.     public void setMessage(String msg) {  
  3.         System.out.println(msg);  
  4.     }  
  5. }  

잠금기능이 정의된 인터페이스를 구현한 클래스 입니다.
이 잠금 기능이 위에서 정의한 Foo 클래스에 추가됩니다.
  1. public class LockImpl implements Lockable {  
  2.     private boolean lock;  
  3.      
  4.     public void lock() {  
  5.         lock = true;  
  6.     }  
  7.   
  8.     public boolean locked() {  
  9.         return lock;  
  10.     }  
  11.   
  12.     public void unLock() {  
  13.         lock = false;  
  14.     }  
  15. }  

그리고 Aspect를 정의하겠습니다.
추가된 기능이 정의된 인터페이스를 public static 필드로 선언 하고 @DeclareParents 애노테이션을 이용해
Pointcut과 구현클래스를 지정합니다. 그러면 Pointcut에 일치하는 클래스를 찾아서 구현클래스로 지정된 
클래스를 Target 객체에 추가될 것입니다.
  1. @Aspect  
  2. public class ExampleAspect {     
  3.     @DeclareParents(value="aspectj.Foo", defaultImpl=LockImpl.class)  
  4.     public static Lockable lock;  
  5.      
  6.     @Pointcut("execution(* set*(..))")  
  7.     public void examplePointcut() {}  
  8.      
  9.     @Around("examplePointcut()")  
  10.     public Object lockCheckAdvice(ProceedingJoinPoint joinPoint) throws Throwable{  
  11.         Lockable lock = (Lockable) joinPoint.getThis();  
  12.         if(lock.locked()) {  
  13.             System.out.println("현재 잠금상태 이므로 호출 할 수 없음");  
  14.             return null;  
  15.         } else {  
  16.             return joinPoint.proceed();  
  17.         }  
  18.     }  
  19. }  

2.0이전에는 상속받은 DelegatingIntroductionInterceptor 클래스의 invoke 메소드를 재정의 하는방식으로
추가된 상태에 따라 메소드 호출을 제어 할 수 있었지만 2.0이후에서는 DelegatingIntroductionInterceptor 클래스를 상속받지 않기 때문에 별도의 Around Advice를 추가 했습니다.

테스트 클래스 (XML 설정은 별거 없기 때문에 생략 하겠습니다.)
  1. public class AspectJTest {  
  2.     public static void main(String[] args) {  
  3.         ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"aspectj/beans.xml"});  
  4.         Foo foo = (Foo) context.getBean("foo");  
  5.         foo.setMessage("안녕 Foo");  
  6.         ((Lockable)foo).lock();  
  7.         foo.setMessage("안녕 Foo");  
  8.     }  
  9. }  

결과는
안녕 Foo
현재 잠금상태 이므로 호출 할 수 없음





<beans ...

    xmlns:aop=http://www.springframework.org/schema/aop ... >

 

일단 xmlns에 aop를 추가해주고...

 

<aop:aspectj-autoproxy/>

 

위 태그를 사용하면 @Aspect Annotation이 사용된 클래스의 Advice 및 Pointcut을 알맞은 빈 객체에 적용한다...

실제 @Aspect Annotation을 사용한 클래스를 살펴보자..

 

package test.aspect.annotation

 

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.annotation.After;

import org.aspectj.lang.annotation.AfterReturning;

import org.aspectj.lang.annotation.AfterThrowing;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

 

@Aspect

public class TestAspect

{

    @Before("execution(public * test.aspect.*.*(..))")

    public void Before(JoinPoint joinPoint)

    {

        System.out.println("Before");

    }

 

    @AfterReturning(pointcut = "execution(public * test.aspect.*.*(..))" returning = "ret")

    public void AfterReturning(JoinPoint joinPoint, Object ret)

    {

        System.out.println("AfterReturning");

    }

 

    @AfterThrowing(pointcut = "execution(public * test.aspect.*.*(..))" throwing = "ex")

    public void AfterThrowing(JoinPoint joinPoint, Throwable ex)

    {

        System.out.println("AfterThrowing");

    }

 

    @After("execution(public * test.aspect.*.*(..))")

    public void After(JoinPoint joinPoint)

    {

        System.out.println("After");

    }

 

    @Around("execution(public * test.aspect.*.*(..))")

    public void before(ProceedingJoinPoint joinPoint joinPoint)

    {

        System.out.println("Around");

        Object ret = joinPoint.proceed();

    }

}

 

앞서 살펴보았던 POJO를 이용한 설정과 매우 유사하다..

단지 소스 코드에 직접 입력하느냐 설정 파일에 설정하느냐의 차이만 있을 뿐이다...

위의 TestAspect를 생성해주면 test.aspect 패키지의 모든 클래스의 모든 메소드에 AOP가 적용이 된다..

 

<bean class="test.aspect.annotation.TestAspect"/>

<bean id="test" class="test.aspect.TestService"/>

 

위의 예제는 test.aspect.TestService의 모든 메소드에 TestAspect가 적용되는 설정이다...

그렇다면 도대체 AspectJ의 Pointcut 표현식이 어떤 뜻인지 궁금해진다..

 

Spring에서 제공하는 AspectJ의 Pointcut 명시자는 execution과 within 그리고 bean 명시자이다..

먼저 Advice를 적용할 메소드를 명시할 때 사용하는 execution부터 살펴보도록 하자..

 

execution([접근명시자패턴] 리턴타입패턴 이름패턴(파라미터패턴))

 

접근명시자패턴은 public, protected, private 등을 사용할 수가 있고 생략 가능하다..

리턴타입패턴은 가능한 모든 리턴 타입을 사용할 수 있고..

이름패턴에는 패키지를 포함한 클래스 이름과 메소드 이름을 사용하고...

파라미터패턴에는 매칭될 파라미터에 대해 명시해준다...

또한 모든 패턴에는 * 를 이용하여 모든 값을 명시할 수 있고 ..을 이용하여 0개 이상이라는 의미를 표현할 수 있다..

예제를 살펴보자....

 

execution(public void set*(..)) : 리턴 타입이 void이고 set으로 시작하는 파라미터 0개 이상인 public 메소드

execution(* test.aspect.*.*()) : test.aspect 패키지의 파라미터가 없는 모든 메소드

execution(* test.aspect..*.*(..)) : test.aspect 패키지 및 하위 패키지에 파라미터가 0개 이상인 메소드

execution(void test.aspect.TestService.test()) : 리턴 타입이 void인 TestService의 test 메소드 호출

execution(* test*(*)) : test로 시작하는 파라미터 1개를 갖는 메소드

execution(* test*(*, *)) : test로 시작하는 파라미터 2개를 갖는 메소드

execution(* test*(Integer, ..)) : test로 시작하고 첫번째 파라미터는 Integer이고 1개 이상의 파라미터를 갖는 메소드

 

자세한 내용은 첨부로 올리는 AspectJ Syntax를 참고하도록 하자..

 

within 명시자는 특정 메소드가 아닌 특정 타입에 속하는 메소드를 명시할 때 사용한다...

 

within(test.aspect.TestService) : TestService의 모든 메소드

within(test.aspect.*) : test.aspect 패키지의 모든 메소드

within(test.aspect..*) : test.aspect 패키지 및 하위 패키지의 모든 메소드

 

bean 명시자는 빈의 이름을 이용하여 Pointcut을 정의한다.

 

bean(test) : 이름이 test인 빈의 메소드

bean(*test) : 이름이 test로 끝나는 빈의 메소드

 

이상 Spring에서 제공하는 AspectJ 표현식을 알아보았다..

위의 각각의 표현식은 다음과 같이 &&와 ||를 이용하여 연결할 수 있다..

 

@After("execution(public * get*()) ||execution(public * set*(..))")