ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ Spring AOP ] pointcut 안걸릴때
    프로그래밍/서버 프로그래밍 2020. 5. 12. 11:48

    문제 발생

    최근 흥미가 있던 Kotlin으로 간단한 텔레그램 알림을 주는 봇을 만들면서, Telegram Bot Java Library를 사용하고 있었다.
    Telegram Bot Java Library에서는 TelegramLongPollingBotonUpdateReceived를 오버라이드하여서 사용자로 부터 메시지를 받았을 때 어떻게 동작할 지를 정의하게 되어있다.

    onUpdateReceived 메소드

    해당 메소드가 실행되기 전과 후에 사용자가 가장 마지막에 보낸 요청의 시간 및 마지막에 요청했던 커맨드를 저장하기 위해 Spring Aop를 이용해서 커스텀 어노테이션을 달고, 해당 메소드 실행 전후에 기록을 남기려 했었는데, 커스텀 어노테이션을 이용해보기도 하고, 메소드명을 직접 넣어보기도 했는데 계속해서 걸리지 않았다.

    해결 방안 탐색

    초기에는 PointCut잘못걸어놓고 메소드명으로 걸면 걸리는데, 어노테이션으로 걸면 안걸린다고, 오해를 하기도 하였는데 다시 확인해보니 분명 둘다 안걸리는 것이 맞았다.

    코틀린에 익숙치 않아서 코틀린 문제인 줄 알고, Spring Aop kotlin이란 키워드로 많은 검색을 했지만, 결국 해결에는 실패했다.
    혹시 코틀린 문제가 아닐 수도 있지 않을까 싶어서, 옆자리 똑똑한 직장동료의 Java 프로젝트로 잠시 확인을 해보니 자바에서도 안걸리는 걸 확인했다.
    이 후 Aspect 클래스에 @Aspect@Component가 둘다 잘 적혀있는 지 확인, 메소드명이나 패키지명이 틀리지 않았는 지 확인, @EnableAspectJAutoProxy를 넣었는 지 확인 ( 결과적으로 Spring boot라서 넣지 않아도 잘 작동) 등등 다양한 확인들을 했지만 실패했다.

    ?????????

    아니 그럼 코틀린 문제도 아니고, fianl 메소드나 클래스도 아닌데 도데체 왜안되는거야ㅑㅑㅑㅑㅑㅑㅑㅑ

    라고 생각하면서, 라이브러리 내부의 인터페이스를 살펴보았다.

    먼저 내가 구현한 커스텀 봇이 상속받고 있는 TelegramLongPollingBot의 코드는 아래와 같았다.

    /**
     * @author Ruben Bermudez
     * @version 1.0
     * Base abstract class for a bot that will get updates using
     * <a href="https://core.telegram.org/bots/api#getupdates">long-polling</a> method
     */
    public abstract class TelegramLongPollingBot extends DefaultAbsSender implements LongPollingBot {
    
              ⋮
              ⋮
              ⋮
              
    }
    

    그리고 내가 실제 포인트컷을 걸려고하는 onUpdateReceived 메소드는 LongPollingBot 인터페이스 내부에 존재했다.

    /**
     * @author Ruben Bermudez
     * @version 1.0
     * @brief Callback to handle updates.
     * @date 20 of June of 2015
     */
    public interface LongPollingBot {
        /**
         * This method is called when receiving updates via GetUpdates method
         * @param update Update received
         */
        void onUpdateReceived(Update update);
    
        /**
         * This method is called when receiving updates via GetUpdates method.
         * If not reimplemented - it just sends updates by one into {@link #onUpdateReceived(Update)}
         * @param updates list of Update received
         */
        default void onUpdatesReceived(List<Update> updates) {
            updates.forEach(this::onUpdateReceived);
        }
                ⋮
                ⋮
                ⋮            
    }
    

    코드를 보다보니 default void onUpdatesReceived(List<Update> updates) 에서 updates의 모든 요소에 대해 onUpdateReceived를 내부적으로 호출하도록 구현되어 있었다.
    이를 구글링해보니 StackOverFlow1, StackOverFlow2 에서 해답을 찾을 수 있었다.

    이전에 문제 원인을 찾으려고 CGLib방식의 프록시와 JDK interface-based 프록시들을 공부하면서 Spring AOP의 동작 방식을 이해했기 때문에 내부 호출에는 Pointcut을 걸 수 없는 이유도 금방 이해할 수 있었다.

    AOP가 작동할 때는 실제로는 실제 구현체 클래스가 아닌 프록시 객체를 만들어서 프록시 객체에서 원래 메소드를 invoke 하기 전 후에 사용자가 aspect에 정의한 메소드들을 적절히 실행시키는데, 포인트컷을 걸고 싶은 메소드의 실행이 외부호출이 아닌 내부호출로 실행되는 경우에는 프록시 객체가 해당 메소드를 호출할 수가 없기 때문에 내부호출 메소드에서는 aop를 사용할 수 없었던 것이다.

    Spring 공식 Doc Understanding AOP proxies를 읽어보면

    However, once the call has finally reached the target object (the SimplePojo, reference in this case), any method calls that it may make on itself, such as this.bar() or this.foo(), are going to be invoked against the this reference, and not the proxy. This has important implications. It means that self-invocation is not going to result in the advice associated with a method invocation getting a chance to execute.

    라는 부분이 나온다. 예제와 함께 이해하기 쉽게 설명이 되어있었다.....ㅎㅎ

    문제 해결

    해당 Doc에서는 이런 상황의 최선의 방안은 네 코드를 리팩토링해서 내부호출하지 않도록 바꿔보렴 이라고 말하고 있는데, 나같은 경우에는 라이브러리를 사용중이라 라이브러리 내부의 코드를 내 입맛대로 고칠 순 없으니 다른 방법을 선택해야했다.

    결국 UpdateHandler라는 클래스를 만들어서 onUpdateReceived에서 처리하던 모든 일을 위임하고, 위임한 메소드에 PointCut을 걸어서 해결하였다.

    배운 점

    • Spring AOP 의 작동방식에 대한 이해
      • AOP는 프록시를 기반으로 작동
      • 해당 프록시는 JDK dynamic(interface-based) 방식과 CGlib 방식이 존재한다.
      • JDK dynamic 방식은 Spring AOP가 선호하는 방식이고, 타겟 클래스가 인터페이스의 구현체 일 때 자동으로 JDK dynamic 방식을 선택하여 작동한다.
      • CGlib 는 타겟 클래스가 인터페이스의 구현체가 아닐 때 작동할 수 있다.
      • CGlib 방식으로 강제선택하도록 하려면 @EnableAspectJAutoProxy(proxyTargetClass = true)로 설정할 수 있다 ( default는 false)
      • AOP를 적용시킬 때는 해당 메소드가 내부호출 메소드가 아닌 지 혹은 final 메소드는 아닌 지 다시 한번 확인하자... :D

    AOP의 동작 방식은 이 글 을 읽고, 공식문서를 읽으니 더 잘 이해가 되는 것 같았다. 역시 한글 최고👍

    댓글

Designed by Tistory.