Spring

Spring AOP

Developer Garam.Choi 2019. 5. 10. 18:01

 

 

이번에 프로젝트 지원을 나오면서 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 나, 화면단으로 확인 가능하도록 자동화 처리를 할 생각이였다.

 

도움이 되었길 바랍니당...