AOP Sample - Logging

다음에서는 AOP 대표적인 툴 중 AspectJ, @AspectJ(Annotation), SpringAOP를 이용하여 Aspect를 생성하고 테스트하는 방법에 대해서 다루고자 한다. Aspect 적용 대상은 UserService이며, 다음에서 다루게 될 각각의 Aspect는 com.sds.emp.user.services.impl 패키지 내에 속한 모든 클래스의 특정 메소드(add*, update*, get*) 실행 전에 해당 메소드를 실행하기 위해 입력된 인자들의 값을 로그로 남기는 역할을 수행하게 될 것이다.
  1. AspectJ 이용하기
  2. Annotation 이용하기
  3. XML 정의 이용하기
  4. Spring ProxyBeanFactory 이용하기

시작하기 전에

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

  2. Eclipse 플러그인 AJDT는 Aspect 파일을 생성하고 컴파일하기 위한 개발툴이다. 사용중인 Eclipse 내에 플러그인 AJDT(AspectJ Development Tool)가 설치되어 있지 않다면, AJDT(AspectJ Development Tool)를 다운로드하여 사용중인 Eclipse 내에 설치하는 것이 좋다. 만약, AJDT를 이용하지 않고 Aspect를 컴파일하고자 한다면, aspectjtools.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.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.jar" />
             <pathelement location="${lib.dir}/commons-logging-1.1.jar" />
             <pathelement location="${lib.dir}/jamon-2.4.jar" />
          </classpath>
       </iajc>
    </target>
  3. Convert to AspectJ Project

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

AspectJ 이용하기

  1. Aspect 생성

  2. 해당 Eclipse 프로젝트에서 메뉴 New > Other... > AspectJ > AspectJ를 선택한 후, AutoLogFromAspectJ라는 이름을 가진 Aspect 파일 AutoLogFromAspectJ를 생성하고 다음과 같이 pointcut과 advice를 정의한다. AutoLogFromAspectJ는 com.sds.emp.user.services.impl 패키지 내에 속한 모든 클래스의 메소드 중 메소드명이 get으로 시작하는 메소드 실행 전에 해당 메소드 정보와 입력 인자값을 로그로 남기는 역할을 수행한다.
    public aspect AutoLogFromAspectJ {
       // pointcut : com.sds.emp.user.services.impl 패키지 내에 속한 모든 클래스의 메소드 중
       // 메소드명이 add로 시작하는 메소드 실행 시점
       pointcut addMethods() : execution(public * com.sds.emp.user.services.impl..add*(..));
    
       // advice : JoinPoint 실행 전 입력값을 Log로 남긴다.
       before() : addMethods() {
          // 해당 joinpoint가 정의된 클래스를 추출한다.
          String className = thisJoinPoint.getSignature().getDeclaringTypeName();
    
          // 해당 joinpoint의 입력 인자들을 추출한다.
          Object[] args = thisJoinPoint.getArgs();
    
          StringBuffer argSb = new StringBuffer();
          argSb.append("\n ****** AutoLogFromAspectJ Start!!!! *****");
          argSb.append("\n * " + thisJoinPoint.getSignature().toString());
          for (int i = 0; i < args.length; i++) {
             argSb.append("\n * " + (i + 1) + " arg's value : " 
                                                    + args[i].toString());
          }
          argSb.append("\n ******************************************************");
          Log logger = LogFactory.getLog(className);
          logger.debug(argSb.toString());
       }
    }
  3. 테스트 클래스 실행

  4. UserService를 호출하여, 신규 User를 등록하고 등록된 User 정보를 조회 및 수정하는 로직으로 구성된 AutoLogAspectTest 클래스를 실행시키면 AutoLogFromAspectJ Aspect가 적용되어, 콘솔창을 통해 다음과 같은 결과를 확인할 수 있다.
    2007-12-27 13:34:40,531 DEBUG [com.sds.emp.user.services.impl.UserServiceImpl] 
     ****** AutoLogFromAspectJ Start!!!! *****
     * void com.sds.emp.user.services.impl.UserServiceImpl.addUser(UserVO)
     * 1 arg's value :  [userId] woos41 [userName] gang [password] gang [role] user [ssn] 1234567890
     *                  [slYn] Y [birthDay] 19750319 [age] null [cellPhone] 1234567890 [addr] kamala
     *                  road [email] ga@samsung.com [emailYn] y [imageFile] ga [regDate] null
     ******************************************************
    2007-12-27 13:34:40,531 DEBUG [com.sds.emp.user.services.impl.UserDAO] 
     ****** AutoLogFromAspectJ Start!!!! *****
     * void com.sds.emp.user.services.impl.UserDAO.addUser(UserVO)
     * 1 arg's value :  [userId] woos41 [userName] gang [password] gang [role] user [ssn] 1234567890
     *                  [slYn] Y [birthDay] 19750319 [age] null [cellPhone] 1234567890 [addr] kamala
     *                  road [email] ga@samsung.com [emailYn] y [imageFile] ga [regDate] null
     ******************************************************

Annotation 이용하기

  1. Aspect 생성

  2. 다음과 같이 Annotation과 함께 구성된 AutoLogFromAnnotatedAspect라는 Aspect 클래스를 생성한다. AutoLogFromAnnotatedAspect는 com.sds.emp.user.services.impl 패키지 내에 속한 모든 클래스의 메소드 중 메소드명이 get으로 시작하는 메소드 실행 전에 해당 메소드 정보와 입력 인자값을 로그로 남기는 역할을 수행한다.
    @Aspect
    public class AutoLogFromAnnotatedAspect {
    	// pointcut : com.sds.emp.user.services.impl 패키지 내에 속한 모든 클래스의
    	// 메소드 중 메소드명이 get으로 시작하는 메소드 실행 시점
    	@Pointcut("execution(public * com.sds.emp.user.services.impl..get*(..))")
    	public void getMethods() {
    	}
    
    	@Before("getMethods()")
    	// JoinPoint 정보를 추출하기 위해 Advice의 입력 인자로 JoinPoint를 정의함.
    	public void autoLog(JoinPoint thisJoinPoint) {
    		// 해당 joinpoint가 정의된 클래스를 추출한다.
    		String className = thisJoinPoint.getSignature().getDeclaringTypeName();
    
    		// 해당 joinpoint의 입력 인자들을 추출한다.
    		Object[] args = thisJoinPoint.getArgs();
    
    		StringBuffer argSb = new StringBuffer();
    		argSb.append("\n ****** AutoLogFromAnnotatedAspect Start!!!! *****");
    		argSb.append("\n * " + thisJoinPoint.getSignature().toString());
    		for (int i = 0; i < args.length; i++) {
    			argSb.append("\n * " + (i + 1) + " arg's value : "
    					+ args[i].toString());
    		}
    		argSb.append("\n ******************************************************");
    		Log logger = LogFactory.getLog(className);
    		logger.debug(argSb.toString());
    	}
    }
  3. 테스트 클래스 실행

  4. UserService를 호출하여, 신규 User를 등록하고 등록된 User 정보를 조회 및 수정하는 로직으로 구성된 AutoLogAspectTest 클래스를 실행시키면 AutoLogFromAnnotatedAspect Aspect가 적용되어, 콘솔창을 통해 다음과 같은 결과를 확인할 수 있다.
    2007-12-27 14:15:06,359 DEBUG [com.sds.emp.user.services.impl.UserServiceImpl] 
     ****** AutoLogFromAnnotatedAspect Start!!!! *****
     * UserVO com.sds.emp.user.services.impl.UserServiceImpl.getUser(String)
     * 1 arg's value : woos41
     ******************************************************
    2007-12-27 14:15:06,375 DEBUG [com.sds.emp.user.services.impl.UserDAO] 
     ****** AutoLogFromAnnotatedAspect Start!!!! *****
     * UserVO com.sds.emp.user.services.impl.UserDAO.getUser(String)
     * 1 arg's value : woos41
     ******************************************************

XML 정의 이용하기

  1. Aspect 생성

  2. 다음과 같이 AutoLogFromSpringAop라는 Aspect 클래스를 생성한다. AutoLogFromSpringAop는 com.sds.emp.user.services.impl 패키지 내에 속한 모든 클래스의 메소드 중 메소드명이 update로 시작하는 메소드 실행 전에 해당 메소드 정보와 입력 인자값을 로그로 남기는 역할을 수행한다.
    public class AutoLogFromSpringAop {
        // JoinPoint 정보를 추출하기 위해 Advice의 입력 인자로 JoinPoint를 정의함.
    	public void autoLog(JoinPoint thisJoinPoint) {
    		// 해당 joinpoint가 정의된 클래스를 추출한다.
    		String className = thisJoinPoint.getTarget().getClass().getName();
    
    		// 해당 joinpoint의 입력 인자들을 추출한다.
    		Object[] args = thisJoinPoint.getArgs();
    
    		StringBuffer argSb = new StringBuffer();
    		argSb.append("\n ****** AutoLogFromSpringAop Start!!!! *****");
    		argSb.append("\n * " + thisJoinPoint.getTarget().getClass().getName()
    				+ "." + thisJoinPoint.getSignature().getName() + "()");
    		for (int i = 0; i < args.length; i++) {
    			argSb.append("\n * " + (i + 1) + " arg's value : "
    					+ args[i].toString());
    		}
    		argSb.append("\n ******************************************************");
    		Log logger = LogFactory.getLog(className);
    		logger.debug(argSb.toString());
    	}
    }
  3. Aspect 정의

  4. Spring 속성 정의 XML(applicationContext-aspect.xml) 파일 내에 Aspect 클래스를 Bean으로 정의한 후, 해당 Aspect에 대한 pointcut과 advice를 정의한다.
    <bean id="autoLogFromSpringAop"
       class="integration.anyframe.services.aop.aspect.AutoLogFromSpringAop"/>
    <aop:config>
       <aop:aspect ref="autoLogFromSpringAop">
          <!-- pointcut : com.sds.emp.user.services.impl 패키지 내에 속한 모든 클래스의 메소드 중
                         메소드명이 update로 시작하는 메소드 실행 시점을 pointcut으로 정의 -->
          <aop:pointcut id="updateMethods" 
               expression="execution(public * com.sds.emp.user.services.impl..update*(..))"/>
          <!-- advice : 해당되는 메소드 실행 전에 , AutoLogFromSpringAop의 autoLog 메소드 실행 -->	
          <aop:before method="autoLog" pointcut-ref="updateMethods"/>
       </aop:aspect>
    </aop:config>
  5. 테스트 클래스 실행
  6. UserService를 호출하여, 신규 User를 등록하고 등록된 User 정보를 조회 및 수정하는 로직으로 구성된 AutoLogAspectTest 클래스를 실행시키면 AutoLogFromSpringAop Aspect가 적용되어, 콘솔창을 통해 다음과 같은 결과를 확인할 수 있다.
    2007-12-27 14:15:06,390 DEBUG [com.sds.emp.user.services.impl.UserDAO] 
     ****** AutoLogFromSpringAop Start!!!! *****
     * com.sds.emp.user.services.impl.UserDAO.updateUser()
     * 1 arg's value :  [userId] woos41 [userName] updgang [password] gang [role] null 
     *                  [ssn] 1234567890 [slYn] Y [birthDay] 19750319 [age] null 
     *                  [cellPhone] 9876543210 [addr] kamala road -- 
     *                  [email] ga@samsung.com [emailYn] y [imageFile] ga
     *                  [regDate] 2007-12-27 00:00:00.0
     ******************************************************
    2007-12-27 14:15:06,390 DEBUG [com.sds.emp.user.services.impl.UserServiceImpl] 
     ****** AutoLogFromSpringAop Start!!!! *****
     * com.sds.emp.user.services.impl.UserServiceImpl.updateUser()
     * 1 arg's value :  [userId] woos41 [userName] updgang [password] gang [role] null 
     *                  [ssn] 1234567890 [slYn] Y [birthDay] 19750319 [age] null 
     *                  [cellPhone] 9876543210 [addr] kamala road -- 
     *                  [email] ga@samsung.com [emailYn] y [imageFile] ga 
     *                  [regDate] 2007-12-27 00:00:00.0
     ******************************************************

Spring ProxyBeanFactory 이용하기

ProxyBeanFactory는 특정 클래스에 대한 Proxy를 생성하고 이 Proxy를 통해 해당 클래스의 메소드가 호출되도록 한다. 또한 ProxyBeanFactory를 통해 호출되는 Interceptor는 Around 유형으로, 해당 메소드 호출 전, 후에 수행할 수 있는 로직을 포함할 수 있다.
  1. Interceptor 생성

  2. 다음과 같이 AutoLogInterceptor라는 클래스를 생성한다. AutoLogInterceptor는 UserService의 모든 메소드 실행 전에 해당 메소드 정보와 입력 인자값을 로그로 남기는 역할을 수행한다. 모든 Interceptor는 org.aopalliance.intercept.MethodInterceptor를 implements 해야 하며, invoke 메소드를 포함하고 있어야 한다. invoke 메소드 내에서는 입력 인자 method의 proceed()를 호출함으로써 Proxy를 통해 대상 클래스의 메소드를 호출한다. 따라서, 필요시 proceed() 호출 전 후에 해당 Interceptor의 역할을 로직으로 구성해야 한다.
    public class AutoLogInterceptor implements MethodInterceptor {
    
    	public Object invoke(MethodInvocation method) throws Throwable {
    		// {before} 메소드 실행 전에 해당 메소드 정보와 입력 인자값을 로그로 남기는 로직 추가
    		final Class clazz = method.getMethod().getDeclaringClass();
    		final String methodName = method.getMethod().getName();
    
    		Log logger = LogFactory.getLog(clazz);
    
    		StringBuffer argSb = new StringBuffer();
    		argSb.append("\n ****** AutoLogFromProxyFactory Start!!!! *****");
    		argSb.append("\n * " + clazz.getName() + "." + methodName + "()");
    
    		Object[] arguments = method.getArguments();
    		if (arguments.length > 0) {
    			for (int i = 0; i < arguments.length; i++) {
    				argSb.append("\n * " + (i + 1) + " arg's value : "
    						+ arguments[i].toString());
    			}
    		} else
    			argSb.append("\nNo arguments\n");
    
    		argSb.append("\n ******************************************************");
    		logger.debug(argSb.toString());	
    		// 대상 클래스의 메소드 실행	
    		return method.proceed();
    	}
    }
  3. Interceptor 정의

  4. Spring 속성 정의 XML(applicationContext-aspect-proxyfactory.xml) 파일 내에 Interceptor와 대상 클래스를 정의한 후, ProxyFactoryBean을 통해 대상 클래스 실행시 Interceptor를 실행시킬 수 있도록 정의한다. 따라서, 클라이언트에서 대상 클래스에 대한 호출이 ProxyFactoryBean을 통해 생성된 Proxy를 통해 이루어지도록 하기 위해서는 UserServiceImpl이 아닌, UserServiceWithProxyFactory를 호출해야 한다.
    <bean id="UserServiceImpl" class="com.sds.emp.user.services.impl.UserServiceImpl">
    	<property name="userDAO">
    		<bean class="com.sds.emp.user.services.impl.UserDAO">
    			<property name="queryService" ref="queryService" />
    			<property name="propertiesService"
    				ref="propertiesService" />
    		</bean>
    	</property>
    </bean>
    	
    <bean id="autoLogInterceptor" class="integration.anyframe.services.aop.aspect.AutoLogInterceptor" />
    
    <bean id="UserServiceWithProxyFactory" class="org.springframework.aop.framework.ProxyFactoryBean">
    	<property name="proxyInterfaces">
    		<value>com.sds.emp.user.services.UserService</value>
    	</property>
    	<property name="interceptorNames">
    		<list>
    			<value>autoLogInterceptor</value>
    		</list>
    	</property>
    	<property name="target" ref="UserServiceImpl"/>
    </bean>
    
  5. 테스트 클래스 실행

  6. UserServiceWithProxyFactory를 호출하여, 신규 User를 등록하고 등록된 User 정보를 조회 및 수정하는 로직으로 구성된 AutoLogAspectTest 클래스를 실행시키면 AutoLogInterceptor가 적용되어, 콘솔창을 통해 다음과 같은 결과를 확인할 수 있다.
    2008-01-06 15:14:07,343 DEBUG [com.sds.emp.user.services.UserService] 
     ****** AutoLogInterceptor Start!!!! *****
     * com.sds.emp.user.services.UserService.updateUser()
     * 1 arg's value :  [userId] woos [userName] updgang [password] gang [role] null [ssn] 1234567890 [slYn] Y [birthDay] 19750319 
     [age] null [cellPhone] 9876543210 [addr] kamala road -- [email] ga@samsung.com [emailYn] y [imageFile] ga 
     [regDate] 2008-01-06 00:00:00.0
     ******************************************************

UserService

다음은 Aspect 적용 대상이 되는 UserService가 제공하는 상세 기능 목록이다.
Method Name Return Type Parameters
getUserList com.sds.emp.common.Page com.sds.emp.user.services.SearchVO
getUser com.sds.emp.user.services.UserVO java.lang.String
addUser void com.sds.emp.user.services.UserVO
updateUser void com.sds.emp.user.services.UserVO
checkDuplication boolean java.lang.String
updateUserList void com.sds.emp.user.services.UserVO, com.sds.emp.user.services.UserVO

Resources