AspectJ based AOP

다음에서는 AOP 대표적인 툴 중 AspectJ를 이용하여 Aspect를 정의하고 테스트하는 방법에 대해서 다루고자 한다.

시작하기 전에

AspectJ를 이용하기 위해서는 다음과 같은 사항에 대해 확인이 필요하다.
  1. AJDT(AspectJ Development Tool) 설치

  2. Eclipse 플러그인 AJDT는 Aspect 파일을 생성하고 컴파일하기 위한 개발툴이다. 사용중인 Eclipse 내에 플러그인 AJDT(AspectJ Development Tool)가 설치되어 있지 않다면, AJDT(AspectJ Development Tool)를 다운로드 하여 사용중인 Eclipse 내에 설치하는 것이 좋다. 만약, AJDT를 이용하지 않고 Aspect를 컴파일하고자 한다면, aspectjtools-1.5.4.jar 내에 정의된 Ant task "iajc"을 이용하도록 한다. 다음은 샘플 build.xml 파일의 compile target의 내용이다.
    <target name="compile" depends="init">
       <taskdef resource="org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties">
          <classpath>
            <pathelement location="${lib.dir}/aspectjtools-1.5.4.jar" />
          </classpath>
       </taskdef>
    
       <iajc verbose="true" destdir="${output.dir}" debug="on" source="1.5"
                 showweaveinfo="true" xnoinline="true">
          <sourceroots>
             <pathelement location="src/main/java" />
          </sourceroots>
          <classpath>
             <pathelement location="${lib.dir}/aspectjrt-1.5.4.jar" />
             <pathelement location="${lib.dir}/commons-logging-1.0.4.jar" />
          </classpath>
       </iajc>
    </target>
  3. Convert to AspectJ Project

  4. 특정 프로젝트가 확장자 aj를 가진 Aspect 파일을 인식할 수 있도록 하기 위해서는 해당 프로젝트에 대한 context menu AspectJ Tools > Convert to AspectJ Project를 선택하여 해당 프로젝트에 대해 AspectJ 프로젝트의 성격을 부여해 주어야 한다.

Aspect 정의

확장자가 aj인 파일을 생성하고, aspect을 이용하여 Aspect 클래스를 정의한다.
다음 PrintStringUsingAspctJ 에서는 aspect를 이용하여 해당 클래스가 Aspect임을 나타내고 있다.
public aspect PrintStringUsingAspctJ {
	// ...
}

Pointcut 정의

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

Advice 정의

다음에서는 AspectJ 기반에서 동작 시점별 Advice를 정의하는 방법에 대해 살펴보기로 한다.

Before Advice

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

	System.out.println("Before Advice of PrintStringUsingAspctJ");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");		
}
위에서 제시한 Before Advice는 내부에 정의된 JoinPoint 유형의 thisJoinPoint라는 객체를 이용하여, Target 클래스명, 메소드명 등과 같은 Target 정보를 추출하고 있다.

AfterReturning Advice

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

	System.out
			.println("AfterReturning Advice of PrintStringUsingAspctJ");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
}
위에서 제시한 AfterReturning Advice는 내부 정의된 JoinPoint 유형의 thisJoinPoint라는 객체를 이용하여, Target 클래스명, 메소드명 등과 같은 Target 정보를 추출하고 있다. 또한, 1개의 입력 인자(UserVO)를 가지고 있는데 이는 해당 Pointcut의 실행 결과이다. AfterReturning Advice에서 특정 Pointcut 실행 결과를 참조해야 한다면, Advice 정의시 returning에 해당하는 객체를 정의하고 메소드 로직 내에서 이를 활용하면 된다. 입력 인자는 AfterReturning Advice 정의시 필요에 따라 선택 정의할 수 있다.

AfterThrowing Advice

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

	System.out
			.println("AfterThrowing Advice of PrintStringUsingAspctJ");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
}
위에서 제시한 AfterThrowing Advice는 내부 정의된 JoinPoint 유형의 thisJoinPoint라는 객체를 이용하여, Target 클래스명, 메소드명 등과 같은 Target 정보를 추출하고 있다. 또한, 1개의 입력 인자(Exception)를 가지고 있는데 이것은 Pointcut 실행시 발생한 Exception 객체이다. AfterThrowing Advice에서 특정 Pointcut 실행시 발생한 Exception을 참조해야 한다면, Advice 정의시 throwing에 해당하는 객체를 메소드 로직 내에서 이를 활용하면 된다. 입력 인자는 AfterThrowing Advice 정의시 필요에 따라 선택 정의할 수 있다.

After(finally) Advice

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

	System.out
			.println("After(finally) Advice of PrintStringUsingAspctJ");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");		
}
위에서 제시한 After(finally) Advice는 내부에 정의된 JoinPoint 유형의 thisJoinPoint라는 객체를 이용하여, Target 클래스명, 메소드명 등과 같은 Target 정보를 추출하고 있다.

Around Advice

around()를 이용하여 Around Advice를 정의한다.
다음은 PrintStringAroundUsingAspctJ 의 Around Advice 정의 부분으로 다른 Advice와 다르게 Return Type 정의가 추가되어 있음을 알 수 있다. Around Advice는 updateMethods()라는 Pointcut 후에 "Around Advice of PrintStringUsingAnnotation"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.
Object around() : updateMethods() {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out.println("Around Advice of PrintStringUsingAspctJ");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
	// before logic
	Object retVal = proceed();
	// after logic
	return retVal;		
}
위에서 제시한 Around Advice는 내부에 정의된 JoinPoint 유형의 thisJoinPoint라는 객체를 이용하여, Target 클래스명, 메소드명 등과 같은 Target 정보를 추출하고 있다. 또한 Around Advice 내에서 proceed()라는 메소드 호출을 통해 대상 Pointcut을 실행할 수 있어, Pointcut 전, 후 처리가 가능하며, Pointcut 실행 시점을 결정할 수 있게 된다. 또한 다른 Advice와는 달리 입력값, target, return 값 등에 대해 변경이 가능하다.

Aspect 실행

이제 테스트코드 PrintStringAspectUsingAspctJTest 를 이용하여 앞서 언급한 Aspect들이 정상적으로 동작하는지 확인해 보도록 하자.
다음은 테스트코드 PrintStringAspectUsingAspctJTest 의 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 PrintStringUsingAspctJ
***class integration.anyframe.services.aop.service.UserServiceImpl.getUser()***
AfterReturning Advice of PrintStringUsingAspctJ
***class integration.anyframe.services.aop.service.UserServiceImpl.getUser()***
After(finally) Advice of PrintStringUsingAspctJ
***class integration.anyframe.services.aop.service.UserServiceImpl.getUser()***
또한 두번째 로직 userService.getUserWithException(); 실행시 테스트를 위해 getUserWithException() 메소드 내에서 EmpException을 throw 하고 있으므로, Before, AfterThrowing, After Advice가 적용되어 콘솔창에 다음과 같은 실행 결과를 포함하게 된다.
Before Advice of PrintStringUsingAspctJ
***class integration.anyframe.services.aop.service.UserServiceImpl.getUserWithException()***
...중략...
AfterThrowing Advice of PrintStringUsingAspctJ
***class integration.anyframe.services.aop.service.UserServiceImpl.getUserWithException()***
After(finally) Advice of PrintStringUsingAspctJ
***class integration.anyframe.services.aop.service.UserServiceImpl.getUserWithException()***
다음은 테스트코드 PrintStringAspectUsingAspctJTest 의 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 PrintStringUsingAspctJ
***class integration.anyframe.services.aop.service.UserServiceImpl.updateUser()***

Resources