Annotation based AOP

다음에서는 AOP 대표적인 툴 중 @AspectJ(Annotation)을 이용하여 Aspect를 정의하고 테스트하는 방법에 대해서 다루고자 한다. @AspectJ(Annotation)은 AspectJ 5 버전에 추가된 Annotation이며, Spring 2.0 에서부터 이러한 Annotation에 대한 처리가 가능하므로, Spring 기반일 경우 별도의 Compiler나 Weaver 없이 @AspectJ(Annotation) 기반의 AOP 적용이 가능하다. 또한 Annotation을 이용하여 Aspect를 정의할 경우 별도 XML 파일에 대한 정의가 불필요하므로, Aspect 적용이 보다 간결해짐을 알 수 있을 것이다. (단, Annotation은 JAVA 5 이상에서만 정의 가능함에 유의하도록 한다.)

Configuration

@AspectJ(Annotation)이 적용된 클래스들을 로딩하여 해당 클래스에 정의된 Pointcut, Advice를 실행하기 위해서는 Spring 속성 정의 XML 파일에 다음과 같이 추가해주어야 한다.
<aop:aspectj-autoproxy/>

@Aspect 정의

@Aspect를 이용하여 특정 클래스가 Aspect임을 나타낸다.
다음 PrintStringUsingAnnotation 에서는 @Aspect를 이용하여 해당 클래스가 Aspect임을 나타내고 있다.
@Aspect
public class PrintStringUsingAnnotation {
	// ...
}

@Pointcut 정의

@Pointcut을 이용하여 해당 Aspect를 적용할 부분을 정의한다. (Pointcut 정의시에는 Pointcut Designator와 Pattern Matching 활용 방법 을 참고한다.)
다음은 PrintStringUsingAnnotation 의 Pointcut 정의 부분이다. @Pointcut을 "execution(public * integration.anyframe.services..*Impl.get*(..))"와 같이 정의하고, 해당 Pointcut에 대해 식별자로써 getMethods라는 메소드명을 부여하였다.
이것은 integration.anyframe.services 패키지 내에 속한 모든 클래스 중 클래스명이 Impl로 끝나고, 이 클래스들의 public 메소드 중 메소드명이 get으로 시작하는 메소드의 실행 부분이 Aspect을 적용할 Pointcut임을 의미한다. 해당 Pointcut은 getMethods()라는 이름으로 이용 가능하다.
@Pointcut("execution(public * integration.anyframe.services..*Impl.get*(..))")
public void getMethods() {
}

@Advice 정의

다음에서는 Annotation을 이용하여 동작 시점별 Advice를 정의하는 방법에 대해 살펴보기로 한다.

Before Advice

@Before를 이용하여 Before Advice를 정의한다.
다음은 PrintStringUsingAnnotation 의 Before Advice 정의 부분이다. Before Advice인 beforeExecuteGetMethod()는 앞서 정의한 getMethods()라는 Pointcut 전에 "Before Advice of PrintStringUsingAnnotation"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.
@Before("getMethods()")
public void beforeExecuteGetMethod(JoinPoint thisJoinPoint) {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out.println("Before Advice of PrintStringUsingAnnotation");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
}
beforeExecuteGetMethod()는 1개의 입력 인자(JoinPoint)를 가지고 있는데 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있다. Target 정보가 불필요한 Advice인 경우에는 JoinPoint라는 입력 인자를 선언하지 않아도 된다.

AfterReturning Advice

@AfterReturning을 이용하여 AfterReturning Advice를 정의한다.
다음은 PrintStringUsingAnnotation 의 AfterReturning Advice 정의 부분으로 해당 Pointcut 실행 결과를 retVal이라는 변수에 담도록 정의하고 있다. AfterReturning Advice인 afterReturningExecuteGetMethod()는 앞서 정의한 Pointcut 후에 , "AfterReturning Advice of PrintStringUsingAnnotation"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.
@AfterReturning(pointcut = "getMethods()", returning = "retVal")
public void afterReturningExecuteGetMethod(JoinPoint thisJoinPoint,
		Object retVal) {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out.println("AfterReturning Advice of PrintStringUsingAnnotation");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
}
afterReturningExecuteGetMethod()는 2개의 입력 인자(JoinPoint, Object)를 가지고 있는데 첫번째 인자는 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있으며, 두번째 인자는 해당 Pointcut의 실행 결과이다. AfterReturning Advice에서 특정 Pointcut 실행 결과를 참조해야 한다면, Advice 정의시 returning의 값을 정의하고 해당하는 메소드의 입력 인자명을 동일하게 정의해주도록 한다. 각 입력 인자는 AfterReturning Advice 정의시 필요에 따라 선택 정의할 수 있다.

AfterThrowing Advice

@AfterThrowing을 이용하여 AfterThrowing Advice를 정의한다.
다음은 PrintStringUsingAnnotation 의 AfterThrowing Advice 정의 부분으로 해당 Pointcut 실행시 발생한 Exception 객체를 exception이라는 변수에 담도록 정의하고 있다. AfterThrowing Advice인 afterThrowingExecuteGetMethod()는 앞서 정의한 Pointcut에서 Exception이 발생한 후에 , "AfterThrowing Advice of PrintStringUsingAnnotation"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.
@AfterThrowing(pointcut = "getMethods()", throwing = "exception")
public void afterThrowingExecuteGetMethod(JoinPoint thisJoinPoint,
		Exception exception) {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out.println("AfterThrowing Advice of PrintStringUsingAnnotation");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
}
afterThrowingExecuteGetMethod()는 2개의 입력 인자(JoinPoint, Exception)를 가지고 있는데 첫번째 인자는 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있으며, 두번째 인자는 Pointcut 실행시 발생한 Exception 객체이다. AfterThrowing Advice에서 특정 Pointcut 실행시 발생한 Exception을 참조해야 한다면, Advice 정의시 throwing의 값을 정의하고 해당하는 메소드의 입력 인자명을 동일하게 정의해주도록 한다. 각 입력 인자는 AfterThrowing Advice 정의시 필요에 따라 선택 정의할 수 있다.

After(finally) Advice

@After를 이용하여 After(finally) Advice를 정의한다.
다음은 PrintStringUsingAnnotation 의 After(finally) Advice 정의 부분이다. After(finally) Advice인 afterExecuteGetMethod()는 앞서 정의한 getMethods()라는 Pointcut 후에 "After(finally) Advice of PrintStringUsingAnnotation"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.
@After("getMethods()")
public void afterExecuteGetMethod(JoinPoint thisJoinPoint) {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out
			.println("After(finally) Advice of PrintStringUsingAnnotation");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
}
afterExecuteGetMethod()는 1개의 입력 인자(JoinPoint)를 가지고 있는데 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있다. Target 정보가 불필요한 Advice인 경우에는 JoinPoint라는 입력 인자를 선언하지 않아도 된다.

Around Advice

@Around를 이용하여 Around Advice를 정의한다.
다음은 PrintStringAroundUsingAnnotation 의 Around Advice 정의 부분이다. Around Advice인 aroundExecuteUpdateMethod()는 updateMethods()라는 Pointcut 후에 "Around Advice of PrintStringUsingAnnotation"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.
@Around("updateMethods()")
public Object aroundExecuteUpdateMethod(ProceedingJoinPoint thisJoinPoint)
		throws Throwable {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out.println("Around Advice of PrintStringUsingAnnotation");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
	// before logic
	Object retVal = thisJoinPoint.proceed();
	// after logic
	return retVal;
}
aroundExecuteUpdateMethod()는 1개의 입력 인자(ProceedingJoinPoint)를 가지고 있는데 proceed()라는 메소드 호출을 통해 대상 Pointcut을 실행할 수 있으며, Target 클래스명, 메소드명 등과 같은 Target 정보도 포함하고 있다. 즉, Pointcut 전, 후 처리가 가능하며, Pointcut 실행 시점을 결정할 수 있다. 또한 다른 Advice와는 달리 입력값, target, return 값 등에 대해 변경이 가능하다. Target 정보가 불필요한 Advice인 경우에는 ProceedingJoinPoint라는 입력 인자를 선언하지 않아도 된다.

Aspect 실행

이제 테스트코드 PrintStringAspectUsingAnnotationTest 를 이용하여 앞서 언급한 Aspect들이 정상적으로 동작하는지 확인해 보도록 하자.
다음은 테스트코드 PrintStringAspectUsingAnnotationTest 의 testGetUser() 메소드로 integration.anyframe.services 패키지에 속한 UserServiceImpl 클래스의 getXXX() 메소드를 호출하는 로직으로 구성되어 있다.
public void testGetUser() throws Exception {
	UserService userService = (UserService) context.getBean(UserService.ROLE);
	String userID = "woos41";

	// 1. get user
	userService.getUser(userID);

	// 2. get user with exception
	try {
		userService.getUserWithException();
	} catch (EmpException e) {
		// ignore
	}
}
첫번째 로직 userService.getUser(userID); 실행시 Before, AfterReturning, After Advice가 적용되며, 콘솔창에 다음과 같은 실행 결과를 포함하게 된다.
Before Advice of PrintStringUsingAnnotation
***class integration.anyframe.services.aop.service.UserServiceImpl.getUser()***
AfterReturning Advice of PrintStringUsingAnnotation
***class integration.anyframe.services.aop.service.UserServiceImpl.getUser()***
After(finally) Advice of PrintStringUsingAnnotation
***class integration.anyframe.services.aop.service.UserServiceImpl.getUser()***
또한 두번째 로직 userService.getUserWithException(); 실행시 테스트를 위해 getUserWithException() 메소드 내에서 EmpException을 throw 하고 있으므로, Before, AfterThrowing, After Advice가 적용되어 콘솔창에 다음과 같은 실행 결과를 포함하게 된다.
Before Advice of PrintStringUsingAnnotation
***class integration.anyframe.services.aop.service.UserServiceImpl.getUserWithException()***
...중략...
AfterThrowing Advice of PrintStringUsingAnnotation
***class integration.anyframe.services.aop.service.UserServiceImpl.getUserWithException()***
After(finally) Advice of PrintStringUsingAnnotation
***class integration.anyframe.services.aop.service.UserServiceImpl.getUserWithException()***
다음은 테스트코드 PrintStringAspectUsingAnnotationTest 의 testUpdateUser() 메소드로 integration.anyframe.services 패키지에 속한 UserServiceImpl 클래스의 updateXXX() 메소드를 호출하는 로직으로 구성되어 있다.
			 
public void testUpdateUser() throws EmpException {
	UserService userService = (UserService) context.getBean(UserService.ROLE);

	UserVO userVO = new UserVO();
	userVO.setUserId("woos41");
	userVO.setUserName("updgang");
	userVO.setCellPhone("9876543210");
	userVO.setAddr("kamala road --");
	
	// 1. update user
	userService.updateUser(userVO);
}	
userService.updateUser(userID); 실행시 Around Advice가 적용되며, 콘솔창에 다음과 같은 실행 결과를 포함하게 된다.
Around Advice of PrintStringUsingAnnotation
***class integration.anyframe.services.aop.service.UserServiceImpl.updateUser()***

Resources

  • 다운로드
  • 샘플 테스트 코드를 포함하고 있는 anyframe-aoptest-src.zip 파일을 다운받은 후 위에서 제시한 예제 코드를 실행해 볼 수 있다.
    Name
    Download
    anyframe-aoptest-src.zip
    Download