[Spring] XML 스키마 기반의 POJO 클래스를 이용한 AOP 구현

XML 스키마 기반의 POJO 클래스를 이용한 AOP 구현


1. XML 스키마 기반 AOP 


XML 스키마를 이용해서 AOP를 구현하는 과정은 다음과 같다.

1 관련 .jar파일을 클래스패스에 추가한다.

2 공통 기능을 제공하는 Advice 클래스를 구현한다.

3 XML 설정 파일에서 <aop:config>를 이용해서 Aspect를 설정한다. Advice를 이떤 Pointcut에 적용할지를 지정하게 된다.


(1) 공통 기능을 제공한 Advice 클래스를 작성

ProfilingAdvice.java

import org.aspectj.lang.ProceedingJoinPoint; 

public class ProfilingAdvice {

// ProfilingAdvice 클래스는 Around Advice를 구현한 클래스이다.

public Object trace(ProceedingJoinPoint joinPoint) throws Throwable {

// trace() 메서드는 ProceedingJoinPoint 타입의 joinpoint 파라미터를 전달받는데, 이 파라미터를 이용해서 Around Advice를 구현할 수 있게 된다.

String signatureString = joinPoint.getSignature().toShortString();

System.out.println(signatureString + " 시작");

long start = System.currentTimeMillis();

try { // Advice가 적용될 대상 객체를 호출 하기 전의 시간을 구해서 대상 객체를 메서드 호출 실행 시간을 출력

Object result = joinPoint.proceed();

return result;

} finally {

long finish = System.currentTimeMillis();

System.out.println(signatureString + " 종료");

// Advice가 적용될 대상 객체를 호출한 후에 시간을 구해서 대상 객체를 메서드 호출 실행 시간을 출력

System.out.println(signatureString + " 실행 시간 : " + (finish - start) + "ms");

}

}

}

ProfilingAdvice 클래스는 Around Advice를 구현한 클래스이며, trace() 메서드는 Advice가 적용될 대상 객체를 호출하기 전과 후에 시간을 구해서 대상 객체의 메서드 호출 실행 시간을 출력한다.


(2) XML 파일에 Advice를 빈으로 등록하고 Aspect를 설정

acQuickStart.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

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

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

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

--> XML 스키마를 이용해서 AOP를 구현하려면 aop 네임스페이스를 추가해주어야 한다.


<!-- Advice 클래스를 빈으로 등록 -->

<bean id="performanceTraceAdvice" class="madvirus.spring.chap05.aop.pojo.ProfilingAdvice" />

<!-- Aspect 설정: Advice를 어떤 Pointcut에 적용할 지 설정 -->

<aop:config>

<aop:aspect id="traceAspect" ref="performanceTraceAdvice">

<aop:pointcut id="publicMethodexpression="execution(public * madvirus.spring.chap05..*(..))" />

<!-- excution 명시자는 Advice를 적용할 패키지, 클래스 그리고 메서드를 표현할 때 사용된다. -->

<aop:around pointcut-ref="publicMethod" method="trace" />

</aop:aspect>

</aop:config>

<!-- <aop:config>, <aop:aspect>, <aop:pointcut>, <aop:around> 태그를 이용해서 AOP 설정을 할 수 있다.(aop 네임스페이스를 추가) 

madvirus.spring.chap05 패키지 및 그 하위 패키지에 있는 모든 public 메서드를 Pointcut으로 설정하고, 이들 Pointcut에 Around Advice로 performTraceAdvice 빈 객체의 trace() 메서드를 적용한다. 따라서 writeArticeService 빈의 public 메서드나 articleDao 의 public 메서드가 호출되면 ProfilingAdvice 클래스의 trace() 메서드가 Around Advice로 적용된다.--> 


<bean id="writeArticleService" class="madvirus.spring.chap05.board.service.WriteArticleServiceImpl">

<constructor-arg>

<ref bean="articleDao" />

</constructor-arg>

</bean>


<bean id="articleDao" class="madvirus.spring.chap05.board.dao.MySQLArticleDao" />


<bean id="memberService" class="madvirus.spring.chap05.member.service.MemberServiceImpl" />

</beans>

Advice 클래스를 적용하려면 일단 XML 설정 파일에 Advice 클래스를 빈으로 등록해주어야 한다.


(3) Advice가 Pointcut으로 지정한 메서드에 적용되는 지의 여부를 확인하기 위한 클래스

import madvirus.spring.chap05.board.Article;

import madvirus.spring.chap05.board.service.WriteArticleService;

import madvirus.spring.chap05.member.Member;

import madvirus.spring.chap05.member.service.MemberService;

import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;


public class MainQuickStart {


public static void main(String[] args) {

String[] configLocations = new String[] { "acQuickStart.xml" };

ApplicationContext context = new ClassPathXmlApplicationContext(configLocations);


WriteArticleService articleService = (WriteArticleService) context.getBean("writeArticleService");

articleService.write(new Article());


MemberService memberService = context.getBean("memberService", MemberService.class);

memberService.regist(new Member());

}

}

MainQuickStart 클래스는 acQuickStart.xml 파일을 이용해서 ApplicationContext를 생성한 뒤 writeArticleService 빈의 write() 메서드를 호출하고, memberService 빈의 regist() 메서드를 호출하고 있다.

앞서 acQuickStart.xml 설정 파일을 설명할 때 madvirus.spring.chap05 패키지 및 그 하위 패키지에 있는 빈 객체의 public 메서드를 호출하면 ProfilingAdvice 클래스의 trace() 메서드가 Around Advice로 사용된다고 했는데 실제로 실행해보면 다음과 같은 결과가 출력된다.


WriteArticleService.write() 시작

WriteArticleServiceImpl.write() 메서드 실행

ArticleDao.insert() 시작

MySQLArticleDao.insert() 실행

ArticleDao.insert() 종료

ArticleDao.insert(..) 실행시간:0ms

WriteArticleService.write(..): 종료

WriteArticleService.write(..): 실행 시간:0ms

MemberService.regist(..): 시작

MemberServiceImpl.regist(): 메서드 실행

MemberService.regist(..): 종료

MemberService.regist(..): 실행 시간:0ms


위 코드에서 굵게 표시한 부분이 ProfilingAdvice 클래스의 trace() 메서드에서 출력한 내용이다. 실행 결과를 보면 실제 빈 객체의 메서드가 호출되기 전/후로 trace() 메서드에서 실행한 내용이 출력되는 것을 확인할 수 있다. 위 실행결과를 통해서 눈 여겨 볼 부분은 WriteArticleServiceImpl,MySQLServiceImpl 등 클래스를 변경하지 않고(즉, 핵심 로직 코드의 변경 없이) 공통 기능을(즉, 메서드 실행 시간을 출력해주는 기능을) 추가했다는 점이다. 이는 설정 파일만 변경하면 기존 코드의 변경 없이 공통 기능을 추가하거나 변경할 수 있는 AOP의 장점을 잘 보여주고 있다.


2. XML 스키마를 이용한 AOP 설정


AOP를 설정하기 위한 aop 네임스페이스 및 aop 네임스페이스와 관련된 XML 스키마가 추가되었다. aop 네임스페이스와 관련된 XML 스키마는 다음과 같이 <beans> 태그에 명시할 수 있다.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

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

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

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

...

<beans>


aop 네임스페이스와 관련된 XML 스키마를 지정한 뒤에, 다음과 같이 <aop:config> 태그를 사용하여 AOP 관련 정보를 설정할 수 있다.

<bean id="performanceTraceAdvice" class="madvirus.spring.chap05.aop.pojo.ProfilingAdvice" />


<aop:config>

<aop:aspect id="traceAspect1" ref="performanceTraceAdvice">

<aop:pointcut id="publicMethod"

expression="execution(public * madvirus.spring.chap05.board..*(..))" />

<aop:around pointcut-ref="publicMethod" method="trace" />

</aop:aspect>


<bean id="writeArticleService" class="madvirus.spring.chap05.board.service.WriteArticleServiceImpl"/>

위 코드에서 <aop:*>에 해당하는 태그는 다음과 같은 정보를 설정한다.


<aop:config>: AOP 설정 정보임을 나타낸다.

<aop:aspect>: Aspect를 설정한다.

<aop:pointcut>: Pointcut을 설정한다.

<aop:around>: Around Advice를 설정한다. 이외에도 다양한 Advice를 설정할 수 있다.


<aop:aspect> 태그의 ref 속성은 Aspect로서 기능을 제공할 빈을 설정할 때 사용된다. 위 코드의 경우 "performanceTraceAdvice" 빈이 Aspect 기능을 제공한다고 명시하고 있다.

위 코드를 보면 "traceAspect"가 MemberServiceImpl 클래스가 구현한 모든 인터페이스의 public 메서드에 Around Advice로 적용되며, 이때 Aspect의 구현 클래스인 ProfilingAdvice의 trace() 메서드가 

호출된다는 것을 쉽게 유추할 수 있다. XML 스키마 기반의 AOP 설정은 이렇게 설정 파일만 보더라도 어렵지 않게 어떤 코드에 어떤 Aspect가 어떤 타입의 Advice로 적용되는지를 파악할 수 있다.


cp.) Aspect 설정

Aspect 설정에서 <aop:aspect> 태그는 한 개의 Aspect를 설정한다. <aop:aspect> 태그의 ref: 속성에는 공통 기능을 구현하고 있는 빈을 전달한다.

<aop:config>

<aop:aspect id="traceAspect1" ref="performanceTraceAdvice">

<aop:pointcut id="publicMethod" expression="execution(public * madvirus.spring.chap05.board..*(..))" />

<aop:around pointcut-ref="publicMethod" method="trace" />

</aop:aspect>

<aop:aspect id="traceAspect2" ref="performanceTraceAdvice">

<aop:around pointcut="execution(public * madvirus.spring.chap05.member..*(..))" method="trace" />

</aop:aspect>

</aop:config>

Advice를 적용할 Pointcut은 <aop:pointcut> 태그를 이용하여 설정한다. <aop:pointcut> 태그의 id 속성은 Pointcut을 정의하는 AspectJ의 표현식을 입력 받는다.

Advice를 표현하는 태그에는 <aop:around>를 비롯하여 각 타입의 Advice를 정의하기 위해 다양한 태그를 제공하고 있다.


 태 그

 설 명

 <aop:before>

 메서드 실행 전에 적용되는 Advice를 정의한다.

 <aop:returning>

 메서드가 정상적으로 실행된 후에 적용되는 Advice를 정의한다.

 <aop:after-throwing>

 메서드가 예외를 발생시킬 때 적용되는 Advice를 정의한다.

 try-catch 블록에서 catch와 유사하다.

 <aop:after>

 메서드가 정상적으로 실행되는지 또는 예외를 발생시키는지 여부에 상관  없이 적용되는 Advice를 정의한다. try-catch-finally에서 finally 불록과 유사하다.

 <aop:around>

 메서드 호출 이전, 이후, 예외 발생 등 모든 시점에 적용 가능한 Advice를 정의한다.


각 태그는 pointcut 속성 또는 pointcut-ref 속성을 사용하여 Advice가 적용될 Pointcut을 지정한다. pointcut-ref 속성은 <aop:config> 태그를 이용하여 설정한 Pointcut을 참조할 때 사용되며, pointcut 속성은 직접 AspectJ 표현식을 이용하여 Pointcut을 지정할 때에 사용된다. 

Advice의 각 태그는 Pointcut에 포함되는 대상 객체의 메서드가 호출될 때, <aop:aspect> 태그의 ref 속성으로 지정한 빈 객체에서 어떤 메서드를 실행할 지를 지정한다. 예를 들어, 위 코드의 경우 madvirus.spring.chap05.member 패키지 및 그 하위 패키지의 public 메서드가 호출될 때 performTraceAdvice 빈의 trace 메서드가 호출되도록 설정하고 있다.


3. Advice 타입 별 클래스 작성


(1) Before Advice

Before Advice를 사용하려면 <aop:before> 태그를 이용하여 Before Advice를 설정하면 된다.

<aop:config>

...

<aop:aspect id ="loggingAspect" ref="logging">

<aop:before pointcut-ref="publicMethod" method="before" />

</aop:aspect>

</aop:config>


Aspect로 사용될 빈 클래스는 다음과 같이 <aop:before> 태그의 method 속성에 명시한 메서드를 구현해주면 된다.

public void before(){

//대상 객체의 메서드 실행 이전에 적용할 기능 구현

...

}


대상 객체 및 호출되는 메서드에 대한 정보나 전달되는 파라미터에 대한 정보가 필요한 경우 org.aspectj.lang.JoinPoint 타입의 파라미터를 메서드에 전달한다.

public void before(JoinPoint joinPoint){

// 대상 객체의 메서드 실행 이전에 적용할 기능 구현

...

}


Before Advice를 구현한 메서드는 일반적으로 리턴 타입이 void인데, 그 이유는 리턴 값을 갖더라도 실제 Advice의 적용 과정에 아무런 영향이 없기 때문이다.

Before Advice를 위한 메서드 구현 시 주의해야 할 점은 메서드에서 예외를 발생시킬 경우 대상 객체의 메서드가 호출되지 않게 된다는 점이다.

cp.) Before Advice에서 예외를 발생시키면 대상 객체의 메서드가 호출되지 않기 때문에, 메서드를 실행 하기 전에 접근 권한을 검사해서 접근 권한이 없을 경우 예외를 발생시키는 기능을 구현하는 데 Before Advice가 적합하다.


(2) After Returning Advice
After Returning Advice는 대상 객체의 메서드가 정상적으로 실행된 후에 공통 기능을 적용하고 싶을 때 사용되는 Advice로서, 다음과 같이 <aop:after-returning> 태그를 이용하여 After Returning Advice를 설정한다.
<aop:config>
...
<aop:aspect id ="loggingAspect" ref="logging">
<aop:after-returning pointcut-ref="publicMethod" method="afterReturning" />
</aop:aspect>
</aop:config>

Aspect로 사용될 빈 클래스는 아래 코드처럼 <aop:after-returning> 태그에 명시한 메서드를 구현한다.
public void afterReturning(){
// 대상 객체의 메서드가 정상적으로 실행된 이후에 적용할 기능 구현
...
}

Advice를 구현한 메서드에서 리턴 값을 사용하고 싶다면 returning 속성을 사용하여 리턴 값을 전달 받을 파라미터의 이름을 명시해 주면 된다.
<aop:after-returning pointcut-ref="publicMethod" method="afterReturning" returning="ret"/>

Advice를 구현한 메서드는 returning 속성에 명시한 이름을 갖는 파라미터를 이용해서 리턴 값을 전달 받게 된다.
public void afterReturning(Object ret){
// 대상 객체의 메서드가 정상적으로 실행된 이후에 적용할 기능 구현
...
}

만약 리턴 된 객체가 특정 타입인 경우에 한해서 메서드를 실행하고 싶다면 다음과 같이 한정하고 싶은 타입의 파라미터를 사용하면 된다.
pulbic void afterReturning(Article ret){
// 대상 객체의 메서드가 정상적으로 실행된 이후에 적용할 기능 구현
...
}

대상 객체 및 호출되는 매서드에 대한 정보나 전달되는 파라미터에 대한 정보가 필요한 경우 다음과 같이 org.aspectj.lang.JoinPoint를 파라미터로 추가한다.
pulbic void afterReturning(Article ret, Object ret){
// 대상 객체의 메서드가 정상적으로 실행된 이후에 적용할 기능 구현
...
}

cp.) 스프링 AOP <aop:after-returning> 태그의 ret 속성에 명시한 파라미터 이름과 실제 Advice 구현 메서드의 파라미터 이름이 일치하는 지 확인하기 위해 클래스의 디버그 정보를 사용한다.
만약 디버그 모드로 컴파일하지 않았다면 Advice 구현 메서드의 파라미터 개수가 한 개인 경우 해당 파라미터 ret 속성에 명시한 파라미터라고 가정한다.

(3) After Throwing Advice

After Throwing Advice는 대상 객체의 메서드가 예외를 발생시킨 경우에 적용되는 Advice로서 다음과 같이 <aop:config-throwing> 태그를 이용하여 설정한다.

<aop:config>

...

<aop:aspect id ="loggingAspect" ref="logging">

<aop:after-throwing pointcut-ref="publicMethod" method="afterTrowing" />

</aop:aspect>

</aop:config>


Advice 구현 클래스는 다음과 같이 <aop:after-returning> 태그에 명시한 메서드를 구현한다.

public void afterThrowing(){

// 대상 객체의 메서드가 예외를 발생시킨 경우에 적용할 기능 구현

...

}


대상 객체의 메서드가 발생시킨 예외 객체가 필요한 경우 throwing 속성에 예외 객체를 전달받을 파라미터의 이름을 명시하면 된다.

<aop:after-throwing pointcut-ref="publicMethod" method="afterTrowing" throwing="ex"/>


Advice 구현 메서드에서 발생된 예외를 사용하려면 <aop:after-throwing> 태그의 throwing 속성에 명시한 이름을 갖는 파라미터를 추가하면 된다.

public void afterThrowing(Trowable ex){

// 대상 객체의 메서드가 예외를 발생시킨 경우에 적용할 기능 구현

...

}


만약 특정 타입의 예외에 대해서만 처리하고 싶다면, Throwable이나 Exception이 아니라 처리하고 싶은 예외 타입을 파라미터로 지정하면 된다. 예를 들어, 아래 코드는 발생된 예외가 ArticleNotFoundException인 경우에만 호출된다.

public void afterThrowing(AtricleNotFoundException ex){

// 대상 객체의 메서드가 예외를 발생시킨 경우에 적용할 기능 구현

...

}


대상 객체 및 호출되는 메서드에 대한 정보나 전달되는 파라미터에 대한 정보가 필요한 경우 다음과 같이 org.aspectj.lang.JoinPoint를 파라미터로 추가한다.

public void afterThrowing(JoinPoint joinPoint, Exception ex){

// 대상 객체의 메서드가 예외를 발생시킨 경우에 적용할 기능 구현

...

}


(4) After Advice

After Advice는 대상 객체의 메서드가 정상적으로 실행되었는지 아니면 예외를 발생시켰는지의 여부에 상관없이 메서드 실행이 종료된 이후에 적용되는 Advice로서 try-catch-fanally

블록에서 finally 블록에서 finally 블록과 비슷한 기능을 수행한다. 다음과 같이 <aop:after> 태그를 이용하여 After Advice를 설정한다.


<aop:config>

...

<aop:aspect id ="loggingAspect" ref="logging">

<aop:after pointcut-ref="publicMethod" method="afterFinally" />

</aop:aspect>

</aop:config>


Aspect로 사용될 빈 클래스는 다음과 같이 <aop:after> 태그에 명시한 메서드를 구현해주면 된다. 이 때 메서드는 파라미터를 갖지 않는다.

public void afterFinally(){

...

}


대상 객체 및 호출되는 메서드에 대한 정보나 전달되는 파라미터에 대한 정보가 필요한 경우 다음과 같이  org.aspectj.lang.JoinPoint를 파라미터로 명시하면 된다.

public void afterFinally(JoinPoint joinPoint){

...

}


(5) Around Advice

Around Advice는 앞서 살펴 본 Before, After Returning, After Throwing, After Advice를 모두 구현할 수있는 Advice로서, 다음과 같이 <aop:around> 태그를 이용하여 Around Advice를 설정한다.

<bean id ="cache" class="madvirus.spring.chap05.aop.pojo.ArticleCacheAdvice"/>

<aop:config>

...

<aop:aspect id ="cacheAdpect" ref="cache">

<aop:around method="cache" pointcut="execution(public * *..ReadArticleService.*(..))" />

</aop:aspect>

</aop:config>


Around Advice를 구현한 메서드는 org.aspectj.lang.ProceedingJoinPoint를 반드시 첫 번째 파라미터로 지정해야 한다. 그렇지 않을 경우 스프링은 예외를 발생시킨다.

다음 코드는 Around Advice의 구현 예를 보여주고 있다.

package madvirus.spring.chap05.aop.pojo;

import java.util.HashMap;

import java.util.Map;

import madvirus.spring.chap05.board.Article;

import org.aspectj.lang.ProceedingJoinPoint;


public class ArticleCacheAdvice {

private Map<Integer, Article> cache = new HashMap<Integer, Article>();

// 첫 번째 파라미터로 ProceedingJoinPoint를 전달받고 있다. ProceedingJoinPoint의 procced() 메서드를 호출하면 프록시 대상 객체의 실제 메서드를 호출하게 된다.

// 따라서 ProceedingJoinPoint.proceed() 메서드를 호출하기 전과 후에 필요한 작업을 수행할 수 있다.

public Article cache(ProceedingJoinPoint joinPoint) throws Throwable {

Integer id = (Integer) joinPoint.getArgs()[0];

Article article = cache.get(id);

if (article != null) {

System.out.println("[ACA] 캐시에서 Article[" + id + "] 구함");

return article;

}

Article ret = (Article) joinPoint.proceed();

if (ret != null) {

cache.put(id, ret);

System.out.println("[ACA] 캐시에 Article[" + id + "] 추가함");

}

return ret;

}

}

위 코드는 대상 객체의 메서드 호출 전후에 캐시 기능을 삽입하였다. proceed() 메서드를 호출하기 전에 Map에 ID에 해당하는 Article 객체가 존재하는지 검사한다.