Spring AOP
이번에 프로젝트 지원을 나오면서 JCO 이슈나 , 동적인 연동 관련된 이슈들이 나와....
고민을 조금 해본 결과 AOP를 하면 어떨까 싶어 스타트했다.
진심 제대로 정리된 글을 찾기도 어렵고, 삽질도 많이 해보고, 되게 간단하게 정리 가능한 것 같으면서도 어렵게 되어있어서 작업하면서 삽질했던 방향으로 일단 정리를 해보겠다. ( 추후 언제가 될지 모르지만 추가적으로 Append할 예정 )
1. AOP란
AOP는 간단하게 말해서 쉽게 이해하려면 제 3자의 입장에서 메소드들을 총괄 하는 매니저 역할을 한다고 이해하고 있다.
무슨뜻이냐. A Method / B Method / C Method 가 각각 있다고 예를 들고
[A Class]
public class A {
public void a() {
//a method를 지칭한다.
}
}
[B Class]
public class B {
public void b() {
//b method를 지칭한다.
}
}
[C Class]
public class C {
public void c() {
//c method를 지칭한다.
}
}
위와 같이 있을때 각각 연관성 없는 A/B/C Class의 a/b/c Method를 호출 전 / 호출 후 / 성공 여부 등등 시점에 맞춰 특별한 Method를 호출 할 수 있게 해줄수 있는 기능이다.
가장많이 사용하는 예시로
a Method / b Method / c Method에 대하여 각각 Log를 찍기 번거로울때 logging 처리를 위하여 사용할 경우도 aop를 사용한다.
간단한 예시는 위와 같고, 결론으로 들어와 삽질 부터 설명한다.
2. dispatchar-context.xml
우리 회사 사내는 dispatchar-context.xml / application-context.xml / transaction-context.xml 등 여러개의 설정들을 각각 분리해서 사용하고 있는데 aop 설정이 같이 뭉쳐져 있어 생각도 안하고 넘어간것도 있고...
애초에 aop 설정관련된 내역을 설명하는 모든 블로그에서 context-component-scan에 존재하는 annotation이 같은 xml내에 존재해야지만 반영된다는 내역이 단 1도 없었따.... 그냥 설정만 한 블로그가 태반이였다고ㅗㅗㅗㅗ ( 나만 모르는걸수도 있다.. 이거 발견하고 개발자 때려쳐야하나 진심 깊은 고민했다. )
<aop:aspectj-autoproxy />
<bean class="a.com.test.ExecutionLoggingAdvisor" />
<context:component-scan base-package="a.*"
use-default-filters="false">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
</context:component-scan>
무튼 개발자 분들은 저와 같이 실수하지말고, aop 설정시에 사용하려는 aspectj bean을 등록하시면, listener에서 읽는 동일한 spring xml 설정에서 context component-scan 을 따라가니 실수하지마세여.... ㅠ_ㅠ...
3. PointCut
Pointcut선택된 Joinpoints
execution(public * *(..)) | public 메소드 실행 |
execution(* set*(..)) | 이름이 set으로 시작하는 모든 메소드명 실행 |
execution(* set*(..)) | 이름이 set으로 시작하는 모든 메소드명 실행 |
execution(* com.xyz.service.AccountService.*(..)) | AccountService 인터페이스의 모든 메소드 실행 |
execution(* com.xyz.service.*.*(..)) | service 패키지의 모든 메소드 실행 |
execution(* com.xyz.service..*.*(..)) | service 패키지와 하위 패키지의 모든 메소드 실행 |
within(com.xyz.service.*) | service 패키지 내의 모든 결합점 |
within(com.xyz.service..*) | service 패키지 및 하위 패키지의 모든 결합점 |
this(com.xyz.service.AccountService) | AccountService 인터페이스를 구현하는 프록시 개체의 모든 결합점 |
target(com.xyz.service.AccountService) | AccountService 인터페이스를 구현하는 대상 객체의 모든 결합점 |
args(java.io.Serializable) | 하나의 파라미터를 갖고 전달된 인자가 Serializable인 모든 결합점 |
@target(org.springframework.transaction.annotation.Transactional) | 대상 객체가 @Transactional 어노테이션을 갖는 모든 결합점 |
@within(org.springframework.transaction.annotation.Transactional) | 대상 객체의 선언 타입이 @Transactional 어노테이션을 갖는 모든 결합점 |
@annotation(org.springframework.transaction.annotation.Transactional) | 실행 메소드가 @Transactional 어노테이션을 갖는 모든 결합점 |
@args(com.xyz.security.Classified) | 단일 파라미터를 받고, 전달된 인자 타입이 @Classified 어노테이션을 갖는 모든 결합점 |
bean(accountRepository) | “accountRepository” 빈 |
!bean(accountRepository) | “accountRepository” 빈을 제외한 모든 빈 |
bean(*) | 모든 빈 |
bean(account*) | 이름이 'account'로 시작되는 모든 빈 |
bean(*Repository) | 이름이 “Repository”로 끝나는 모든 빈 |
bean(accounting/*) | 이름이 “accounting/“로 시작하는 모든 빈 |
bean(*dataSource) || bean(*DataSource) | 이름이 “dataSource” 나 “DataSource” 으로 끝나는 모든 빈 |
PointCut은 위와같이 설정하면 되는데, 저는 쓰는 IDE가 Intellij라서 자동으로 현재 execution이 매칭이 되어있는지 확인이 가능한데.. 이클립스는 가능한지 모르겠습니다.
4. @Around / @Before / @After / @AfterThrowing / @AfterReturning
이제 본격적으로 삽질한 소스인데.
Controller만 들어왔을때 부터 2일 삽질하곤 나머지 나올때부턴 팍팍 진도가 나갓다.
1) @Around : 모든 시점에 해당 어노테이션을 가지고 있는 메소드가 호출된다.
2) @Before : PointCut에서 잡아놓은 method 타겟을 호출하기 이전에 호출한다. ( log로 예를들면 이제 들어간다~~~ 라고 쓰고 method 타겟 실행되고 @After 호출이 되는 것. )
3) @After : PointCut에서 잡아놓은 method 타겟을 호출하기 이후에 호출한다
4) @AfterThrowing / @AfterReturning : 여기서 부터 중요.
내가 제일 하고싶었던 작업이
@AfterThrowing / @AfterReturning 이놈들로 이루어져야하는데 하다보니깐
우리회사 jco / 동적연동은 구현체로 이루어져야하기 때문에 return type이 void로 구성되어있더라....
그래서 returning은 정확히 활용은 못하고.. 다른 방식으로 활용하기로 하였고..
( 메소드... 파라미터 명이랑 매칭되는 value 처리해서 각각 db 동적으로 삽입하고싶었는데... 그건 jdk 1.8 부터 제공되는 Method method = signature.getMethod(); 으로~~ method.getParmeters로 해결가능 할 것 같은데.. 사내는 jdk 1.7을 써서 불가능 할 것 같다. ( 바로 포기 )
@Aspect
@Component
public class JcoAopExecutionAdvisor {
private static Logger logger = LoggerFactory.getLogger("jcoAopExecution");
public JcoAopExecutionAdvisor(){
}
@Around("approvalPointMethod()")
private Object loggingApprovalAround(ProceedingJoinPoint joinPoint)
throws Throwable {
Object result = null;
try{
Object[] args = joinPoint.getArgs() == null? null : joinPoint.getArgs();
StringBuffer log = new StringBuffer();
log.append("\r\n*************************************************************************");
log.append("\r\n******** APPROVAL LOGGING AROUND JOIN POINT LOGGING ********");
log.append("\r\n controller-class = " + joinPoint.getTarget().getClass().getName());
log.append("\r\n method-name = " + joinPoint.getSignature().getName());
if(null != args) {
for(int i=0; i < args.length; i++){
log.append("\r\n APPROVAL PARAMETER =" + args[i].toString());
}
}
log.append("\r\n*************************************************************************\r\n");
this.writeLog(log.toString());
result = joinPoint.proceed();
}catch (Exception e){
e.printStackTrace();
}
return result;
}
@AfterReturning("approvalPointMethod()")
private void loggingApprovalBeforeSuc(JoinPoint joinPoint) throws Throwable{
try{
Object[] args = joinPoint.getArgs() == null? null : joinPoint.getArgs();
StringBuffer log = new StringBuffer();
log.append("\r\n*************************************************************************");
log.append("\r\n******** APPROVAL LOGGING BEFORE JOIN POINT LOGGING ********");
log.append("\r\n controller-class = " + joinPoint.getTarget().getClass().getName());
log.append("\r\n method-name = " + joinPoint.getSignature().getName());
/* JDK 1.8 에서는 아래와 같은 코드로 사용이 가능하다 하니, 필요하신 분들은 사용하세요.
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
for (int i = 0; i < method.getParameters().length; i++) {
parameterName = method.getParameters()[i].getName();
if (parameterName.equals("id"))
id = (Long) parameterValues[i];
if (parameterName.equals("token"))
token = (String) parameterValues[i];
}
System.out.println(id + " : " + token);*/
if(null != args) {
for(int i=0; i < args.length; i++){
log.append("\r\n APPROVAL PARAMETER =" + args[i].toString());
}
}
log.append("\r\n*************************************************************************\r\n");
this.writeLog(log.toString());
}catch (Exception e){
e.printStackTrace();
}
}
@AfterThrowing(value = "approvalPointMethod()" , throwing = "exception")
private void loggingApprovalBeforeSuc(JoinPoint joinPoint,Exception exception) throws Throwable{
try{
Object[] args = joinPoint.getArgs() == null? null : joinPoint.getArgs();
StringBuffer log = new StringBuffer();
log.append("\r\n*************************************************************************");
log.append("\r\n******** APPROVAL LOGGING BEFORE JOIN POINT LOGGING ********");
log.append("\r\n controller-class = " + joinPoint.getTarget().getClass().getName());
log.append("\r\n method-name = " + joinPoint.getSignature().getName());
if(null != args) {
for(int i=0; i < args.length; i++){
log.append("\r\n APPROVAL PARAMETER =" + args[i].toString());
}
}
log.append("\r\n APPROVAL ERROR EXCEPTION LOG =" + exception.getMessage());
log.append("\r\n*************************************************************************\r\n");
this.writeLog(log.toString());
}catch (Exception e){
e.printStackTrace();
}
}
@Pointcut("execution(* a.com.test.*(..))")
public void approvalPointMethod() {
}
private void writeLog(String log) {
switch(this.logLevel) {
case TRACE:
logger.trace(log);
break;
case DEBUG:
logger.debug(log);
break;
case INFO:
logger.info(log);
break;
case WARN:
logger.warn(log);
break;
case ERROR:
logger.error(log);
}
}
}
정리하자면 위와 같이 일단 사용하였고, 자세한 내역은 제거하였으나, 의도한 내역으로는 위와 같은 상황을 사용하여 return value를 받아와 동적으로 parameter를 name / value 매칭 시켜, 각각 db화 시킨 후 batch 나, 화면단으로 확인 가능하도록 자동화 처리를 할 생각이였다.
도움이 되었길 바랍니당...