안드로이드 앱 개발이 점점 성숙해지면서, 우리가 과거에 웹시스템을 개발하면서 했던 것처럼, 앱의 여러 위치에서 발생하는 에러를 한 곳에서 일관되게 처리할 수 있는 중앙집중식 에러처리 기능에 대한 인식과 요구가 늘어나고 있습니다.


지난 글에서는 안드로이드 앱에서 중앙집중화된 방식으로 에러를 처리하기 위해 Thread.UncaughtExceptionHandler 를 활용하는 방법을 살펴보았습니다. 유명한 오픈소스 에러리포트 라이브러리인 ACRA (https://github.com/ACRA/acra)도 내부적으로는 UncaughtExceptionHandler 를 이용하고 있는 것으로 알고있습니다.


그러나!!! UncaughtExceptionHandler를 이용하는 방법만 있는 것은 아닙니다. 


AOP(Aspect Oriented Programming) 기법을 통해서도 글로벌한 에러처리가 가능합니다. 

AOP를 통해서 다양한 곳에서 발생하는(throw) 에러를 자동으로 잡아내고(catch), 잡아낸 에러를 한 곳에서 처리(handle)할 수 있습니다. 실제로 저는 현재 서비스중인 앱에 이 방법을 적용하였습니다.(앱 개발 당시에 ACRA를 몰랐습니다... ㅠㅠ)



1. 던져진 에러를 잡아내는 애스펙트


저의 경우 아래의 위치에서 던져진 에러를 자동으로 잡아내는 애스펙트 구현체를 만들었습니다.


① Activity의 주요 라이프사이클 메소드

② 각종 이벤트 리스너 메소드

③ java.lang.Runnable 구현체의 run() 메소드

④ @ApplyExceptionHandler 어노테이션이 선언된 메소드


다음은 ExceptionCatcherAspect 구현 예제 코드입니다.(AspectJ 라이브러리 이용)


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
... ...

/**
 * 다양한 곳에서 발생하는 에러를 잡아내기 위한 애스펙트.
 */
@Aspect
public class ExceptionCatcherAspect {

    @Inject
    protected ExceptionHandler exceptionHandler;

    /** @IgnoreExceptionHandler 어노테이션이 붙은 메소드 및 ExceptionHandler 클래스에는 적용하지 않는다. */
    @Pointcut(" @annotation(my.IgnoreExceptionHandler) ||  within(my.ExceptionHandler+) ")
    protected void ignore() {}

     /**
      * Activity 하위 클래스의 주요 라이프사이클 메소드들.
      *  정확히 일치하지는 않을 수 있으며, onCreate(..), onResume(..) 등 주요 메소드만 포함됨 
      */
    @Pointcut("within(android.app.Activity+) && (execution(void onCreate(..)) || execution(void onStart())
                       || execution(void onResume()) || execution(void onPause()) || execution(void onStop())
                       || execution(void onRestart()) || execution(void onDestroy()) ")
    protected void activityLifecycleMethods() {}

    //TODO protected void fragmentLifecycleMethods() {}

    /**
      * 이벤트 리스너 메소드들.
      * On*Listener 형태의 리스너를 구현한 클래스의 on* 형태의 메소드.
      * 정확히 일치하지는 않을 수 있음. 
      */
    @Pointcut(" within(android..On*Listener+) && execution(!private !static * on*(..) throws !Exception) 
                     && !activityLifecycleMethods() ")
    protected void eventListenerMethods() {}

    /**
     * Runnable 또는 Thread 및 하위 클래스의 run 메소드. 
     */
    @Pointcut(" execution(public void java.lang.Runnable.run()) ")
    protected void runnableMethod() {}

    /**
     * 액티비티 생명주기 메소드에서 발생한 에러처리.
     */
    @Around(" this(activity) && !ignore() && activityLifecycleMethods() ")
    public Object catchActivityException(final Activity activity, final ProceedingJoinPoint pjp) {
        try {
            return pjp.proceed();
        } catch (final Throwable e) {           
            exceptionHandler.handleActivityException(activity, e);
            // 정책에 따라 에러처리 후 액티비티를 종료하거나 말거나. 
            // activity.finish();
            return null;
        }
    }

    /**
     * 이벤트 리스너 메소드에서 발생한 에러처리.
     */
    @Around(" !ignore() && eventListenerMethods() ")
    public Object catchListenerException(final ProceedingJoinPoint pjp) {
        try {
            return pjp.proceed();
        } catch (final Throwable e) {
            exceptionHandler.handleListenerException(e);
            return null;
        }
    }

    /**
     * Runnable에서 발생한 에러처리.
     */
    @Around(" !ignore() && runnableMethod() ")
    public void catchRunnableException(final ProceedingJoinPoint pjp) {
        try {
            pjp.proceed();
        } catch (final Throwable e) {
            exceptionHandler.handleBackgroundException(e);
        }
    }

    /**
     * @ApplyExceptionHandler 어노테이션이 붙은 메소드에서 발생한 에러처리.
     */
    @Around(" !ignore() && @annotation(my.ApplyExceptionHandler) ")
    public void catchApplyExceptionHandler(final ProceedingJoinPoint pjp) {
        try {
            pjp.proceed();
        } catch (final Throwable e) {
            exceptionHandler.handle(e);
        }
    }

}

에러가 발생한 위치(포인트컷)에 따라 다른 작업을 수행하는 것을 볼 수 있습니다. 액티비티 생명주기 메소드에서 던져진 에러인 경우에는 ExceptionHandler의 handleActivityException()를 호출하고, 이벤트리스너에서 던져진 에러의 경우에는 handleListenerException() 메소드를 호출하는 식입니다. 물론 에러처리 정책에 따라서 동일하게 작업을 수행하는 것도 가능하겠죠.

참고로, 위 예제에서는 모두 Around 어드바이스를 사용하고 있는데요, AfterThrowing 어드바이스를 이용할 수도 있습니다. AfterThrowing 어드바이스를 이용한다면 에러를 처리하고나서도, caller에 exception이 그대로 다시 전달됩니다. 캐치되지 않은 exception이라면 결국 Thread.UncaughtExceptionHandler에 의해 처리 되겠죠.

이러한 에러처리 애스펙트를 적용하면, 앱 개발시 메소드마다 일일이 try~catch 문으로 에러를 잡아낼 필요가 없습니다. 컴파일시에 ExceptionCatcherAspect가 자동으로 try~catch를 삽입해주기 때문이죠.


상황에 따라서 예외케이스가 있기 때문에 애스펙트가 적용되지 않기를 원하는 메소드에는, 예를들어 @IgnoreExceptionHandler 어노테이션을 붙일 수 있다는 것도 참고하시기 바랍니다.(IgnoreExceptionHandler와 ApplyExceptionHandler는 단순 어노테이션이므로 소스코드는 생략합니다)



2. 잡아낸 에러를 처리하는 ExceptionHandler 


에러를 잡아내는 것은 AOP를 이용하고 있지만, 실제로 무슨 작업을 할 것인지의 내용은 별도의 ExceptionHandler 클래스 등에서 구현하는 것이 좋습니다. ExceptionHandler에서는 아래와 같은 작업을 수행할 수 있을 것입니다. 

  • 에러 상세정보 로깅(로그캣 또는 파일)
  • 에러 상세정보를 서버로 전송
  • 적절한 메시지를 사용자에게 보여줌
  • 특정 액티비티로 이동
  • 액티비티 또는 앱 종료

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;

import com.google.inject.Inject;
import com.google.inject.Singleton;

/**
 * 에러처리 핸들러.(인터페이스와 구현체로 분리해도 됩니다)
 */
@Singleton
public class ExceptionHandler {

    @Inject
    protected Context applicationContext;

    protected void handle(Context context, Throwable t) {
        // 에러타입 t 에 해당하는 사용자 메시지 선택(ErrorMessageFinder 같은 별도 클래스 만드는 것 권장)
        String message = "..."; 

        // exception 타입에 따라 아래 작업도 선택적으로 수행 가능.
        doLogging(context, t, message);
        doMessage(context, t, message);
        doReport(context, t, message);

        // 그외 에러처리 작업 수행.
        // 예를들어 exception 타입이 AuthenticationException 같은 것이라면,
        // 여기서 자동으로 로그인 액티비티로 이동하게 할 수 있음.
    }

    public void handle(Throwable t) {
        handle(applicationContext, t);
    }

    / ** 액티비티 생명주기 메소드에서 던져진 에러 처리. */
    public void handleActivityException(Activity activity, Throwable t) {
        handle(activity, t);
        // 여기서 액티비티를 종료하는 것도 가능. 
    }

    / ** 이벤트 리스너 메소드에서 던져진 에러 처리. */
    public void handleListenerException(Throwable t) {        
        handle(t);
        // 기타 작업
    }

    / ** 백그라운드 작업 메소드에서 던져진 에러 처리. */
    public void handleBackgroundException(Throwable t) {        
        handle(t);
        // 기타 작업
    }

    /**
     * 에러정보를 로그에 기록한다.
     */
    protected void doLogging(Context context, Throwable t, String message) {
        // 에러정보를 로깅
        // exception 타입에 따라 에러정보를 간략히 또는 상세히 로깅하거나 다른 레벨에 로깅
    }

    /**
     * 사용자에게 적절한 에러메시지를 보여준다.
     * exception.getMessage() 같은 테크니컬한 내용을 보여주지 말 것.
     */
    @UiThread
    protected void doMessage(Context context, Throwable t, String message) {
        // UI쓰레드에서 Toast 또는 Dialog 형태로 메시지를 출력.
        // context가 액티비티 컨텍스트인 경우 Dialog를 띄울 수 있음
    }

    /**
     * 에러정보를 서버로 전송한다.
     */
    protected void doReport(Context context, Throwable t, String message) {
        // 에러 정보 및 발생 환경(디바이스 정보 등)을 서버로 전송
    }

}

위 ExceptionHandler 예제코드는 특별히 어려운 내용이 없으므로 상세한 설명은 하지 않겠습니다.

제 생각에 AOP를 이용한 에러처리 방법이 Thread.UncaughtExceptionHandler를 이용한 방법보다 나은 점은 다음과 같습니다.
  • 에러가 던져지는 위치(포인트컷)에 따라 다른 방식의 에러처리를 유연하게 적용할 수 있다.
  • 액티비티(UI쓰레드)에서 던져진 에러인 경우에도 굳이 해당 액티비티를 종료하지 않아도 된다.
  • 잡아낸 에러를 부모(caller)에 다시 던질 수도 있고, 던지지 않을 수도 있다.
  • 심지어 에러를 변환해서 다시 던지는 것도 가능하다.
  • Runtime Exception뿐 아니라 checked exception도 잡아서 처리할 수 있다.
반면, 단점도 있겠죠. 
  • AOP(AspectJ)에 대한 학습부담이 있고, 추가적인 개발환경 세팅이 필요하다.
  • 포인트컷을 꼼꼼하게 작성하지 않은 경우 캐치하지 못한 에러가 있을 수 있다.
  • 이미 컴파일된 클래스/라이브러리에는 적용할 수 없다.
  • 적용되는 메소드에 대해서 컴파일시 try/catch 코드가 추가되므로 클래스 용량이 살짝 증가한다.

신고
Posted by 에코지오

댓글을 달아 주세요

  1. 임성묵 2014.03.25 15:05 신고  댓글주소  수정/삭제  댓글쓰기

    신선하네요 ㅎㅎ 좋은 정보 공유 감사합니다 ^^

꼭 안드로이드일 필요는 없지만, AspectJ를 통해서 아키텍처 규칙 위반사항을 체크하는 예제를 공유합니다.

(제가 다니는 회사에서 적용 중인 애스팩트를 살짝 수정한 것입니다)


import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Pointcut;

import android.app.Activity;


/**

 * 아키텍처 규칙 위반을 체크하는 애스펙트.

 */

@Aspect

public class ArchitectureRuleCheckAspect {

    private static final String PREFIX = "[아키텍처 규칙위반]";


    //공통 액티비티(하위 액티비티)가 아닌 곳에 선언된 메소드 실행시

    @Pointcut("!within(com.mycompany.xxx.BaseActivity+) && execution(* *(..))")

    private void notInBaseActivity() {

    }


    @DeclareError("@annotation(com.mycompany.xxx.Click) && notInBaseActivity()")

    static final String useClickAnnotationOnlyInBaseActivity = "@Click 어노테이션은 BaseActivity 하위클래스의 메소드에만 적용할 수 있습니다.";


    @DeclareError("call((@com.google.inject.Singleton *).new(..))")

    static final String dontCreateSingletonDirectly = "@Singleton 클래스 인스턴스를 직접 생성하면 안됩니다. @Inject를 사용하세요.";


    @DeclareError("within(com.mycompany.xxx.Controller+) && call(com.mycompany.xxx.Controller+.new(..)) ")

    static final String dontUseAnotherControllerInControllers = PREFIX + "콘트롤러에서 다른 콘트롤러를 사용할 수 없습니다.";


    @DeclareError("call(* com.mycompany.xxx.Controller+.*(..)) && (within(*..*Action) || within(*..*Connector)) ")

    static final String useControllerOnlyInPresentationLayer = PREFIX + "콘트롤러는 프리젠테이션 레이어에서 사용해야합니다.";


    @DeclareError("within(*..*Connector) && call(* *.*Action.*(..)) ")

    static final String dontCallActionInConnectors = PREFIX + "커넥터 레이어에서는 비즈니스 레이어(액션)를 호출할 수 없습니다.";


    @DeclareError("call(* android.app.Activity+.*(..)) && (within(*..*Action) || within(*..*Connector)) ")

    static final String useActivityOnlyInPresentationLayer = PREFIX + "액티비티는 프리젠테이션 레이어에서 사용해야합니다.";


    @DeclareError("@annotation(com.mycompany.xxx.Rest) && !within(*..*Connector) ")

    static final String useRestAnnotationOnlyInConnectors = PREFIX + "@Rest 어노테이션은 커넥터 레이어에서만 사용할 수 있습니다.";


}


저의 경우 앱 개발시에 레이어 구조와 MVC 패턴을 적용한 아키텍처 표준을 정해놓고 개발합니다.

레이어 구조는 Presentation(Activity) -> Business(Action) -> Connector(Connector)가 되고, 

MVC패턴은 Presentation 레이어에 적용됩니다.


위 애스펙트 예제는 이러한  아키텍처 상에서 레이어간 역참조 및 MVC 담당 모듈을 엉뚱한 레이어에서 사용하는 것을 방지하고 있습니다.


아키텍처 표준뿐 아니라 자체 제작한 유틸리티 어노테이션을 올바른 곳에서 사용하고 있는지 체크하는 규칙을 포함하며, 

Injection에 의해 주입받아야할 컴포넌트를 직접 instance화하는지도 검사합니다.




신고
Posted by 에코지오

댓글을 달아 주세요

추상(abstract) 애스펙트 클래스를 상속받아 포인트컷 메소드나 어드바이스 메소드를 오버라이딩하는 경우에 무엇이 적용되는가에 대해서 정리합니다. (참고로 애스펙트는 abstract 애스펙트만 상속받을 수 있습니다)


포인트컷 메소드 오버라이딩


추상 애스펙트에 정의된 (non-abstract) 포인트컷 메소드를 하위 애스펙트에서 오버라이딩하는 경우


(1) 포인트컷을 재정의하지 않은 경우

=> 추상 애스펙트의 포인트컷이 적용됨


(2) 포인트컷을 재정의한 경우 

=> 하위 애스펙트의 포인트컷이 적용됨


결론 : 자식 애스펙트에서 부모 애스펙트에 정의된 포인트컷을 재정의할 수 있다.



어드바이스 메소드 오버라이딩


추상 애스펙트에 정의된 어드바이스 메소드를 하위 애스펙트에서 오버라이딩하는 경우


(1) 어드바이스를 재정의하지 않은 경우

=> 추상 애스펙트의 어드바이스가 적용되지만 동작(메소드)는 하위 애스펙트의 것이 적용됨


(2) 어드바이스도 재정의한 경우

=> 추상 애스펙트의 어드바이스도 적용되고 하위 애스펙트의 어드바이스도 적용됨. 동작(메소드)은 하위 애스펙트의 것이 적용됨


결론 : 자식 애스펙트에서는 부모 애스펙트에 정의된 어드바이스의 동작(메소드 내용)은 재정의할 수 있지만,

언제(before, after 등) 어드바이스를 적용할지는 수정할 수 없다(무조건 새로운 적용 위치가 추가됨).


신고
Posted by 에코지오

댓글을 달아 주세요

1. 우리가 작성한 코드에 대해서만 위빙이 가능

 AOP에서 위빙하는 방식은 시점에 따라 컴파일타임 위빙과 런타임 위빙으로 나눌 수 있습니다. 

안드로이드 Dalvik VM(.class 를 변형한 .dex 포맷을 사용)은 아직까지 런타임 바이트코드 생성을 지원하지 않습니다. 따라서 안드로이드에서는 런타임 위빙을 사용할 수 없고, 컴파일타임 위빙만 가능합니다(AspectJ는 컴파일 타임 위빙을 지원합니다).

우리가 작성한 소스코드에 대해서만 인터셉트할 수 있으며, 안드로이드 프레임워크나 그외 라이브러리에 대한 어드바이스는 작동하지 않습니다.


예들들어 Activity 클래스의 onCreate() 메소드에 대한 execution() 포인트컷은 작동하지만, 반면에 call() 포인트컷은 작동하지 않습니다. 왜냐면 onCreate() 메소드를 호출하는 클래스는 수정불가한 안드로이드 프레임워크에 속하기 때문입니다.


그리고 before나 after 같은 어드바이스를 통해 우리가 작성한 Activity 클래스의 예들들어 onResume() 라이프사이클 메소드에 로직을 추가하기  위해서는 반드시 해당 액티비티 클래스에 onResumE() 메소드 "코드"가 존재해야합니다. 비록 액티비티가 상속받은 android.app.Activity 클래스에 이미 onResume()  메소드가 존재하지만, 런타임이 아닌 컴파일 타임에 위빙이 수행되므로 어드바이스가 적용되기 바란다면 onResume() 메소드를 오버라이드 해주어야 합니다.



2. thisJointPoint 를 사용하기 위한 이클립스 설정

어드바이스 안에 thisJointPoint 키워드를 사용하는 코드가 존재하면 컴파일은 되지만 실제로 런타임에 아래와 같이 AspectJ 클래스를 못찾는다는 에러가 발생합니다.


Caused by: java.lang.NoClassDefFoundError: org.aspectj.runtime.reflect.Factory


이는 AJDT 플러그인에 의해 빌드패스에 추가된 'AspectJ Runtime Library' 라이브러리(aspectjrt.jar) 파일이 최종 dex 파일에 포함되지 않아 발생하는 문제입니다. 프로젝트 빌드패스 설정창의 "Order and Export" 탭에서 "AspectJ Runtime Library" 항목을 체크해주면 해결됩니다.


그러나 여전히 Ant 빌드(build.xml)를 통해 빌드를 시도하면 NoClassDefFoundError 에러가 발생합니다. build.xml을 수정해줄 수도 있지만 그냥 aspectjrt.jar를 libs 폴더에 넣어두면 간단히 해결됩니다. libs에 존재하는 jar 파일은 "Android Dependencies " 형태로 빌드패스에 자동으로 추가되므로 AJDT에 의해 추가된 'AspectJ Runtime Library'는 더이상 필요없습니다.


결론적으로  안드로이드 라이브러리('Android 2.3.x' 같은) 변수와 'Android Dependencies' 라이브러리 변수를 제외한 모든 외부 라이브러리 변수는 제거하고 필요한 모든 외부 jar 파일은 libs 폴더에 넣어두는 것이 좋습니다.


신고
Posted by 에코지오

댓글을 달아 주세요

  1. 2012.04.18 20:44  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

    • BlogIcon 에코지오 2012.04.16 16:35 신고  댓글주소  수정/삭제

      안녕하세요. 제 개발환경 버전을 말씀드리자면 jdk1.6, eclipse indigo, android sdk 17, adt 17, aspectj 1.7입니다. 그외 안드로이드 관련 사이트는 검색해보면 많이 나오는데요... 제가 운영하는 사이트인 javarss.pe.kr 에 보시면 안드로이드 관련 사이트가 몇개 나옵니다. 안드로이드 펍같은 커뮤니티도 있고 개인 블로그도 있구요. AspectJ는 검색해서 보시거나 문서로 공부하시는게 나을거 같네요. 사실 aspectj는 저도 잘은 모릅니다. try & error 방식으로 공부 중입니다.

안드로이드에서 AspectJ를 사용하기 위해 이클립스 및 Ant빌드 환경을 구성하는 방법을 설명합니다.


1. AspectJ 설치

AspectJ 홈페이지(http://www.eclipse.org/aspectj/)에서 AspectJ 컴파일러를 다운로드 받아 설치합니다.

저는 개발버전인 AspectJ 1.7을 받아서 D:/Compiler/aspectj1.7 경로에 설치했습니다.

설치는 다운받은 jar 파일을 실행하면 됩니다. 


> java -jar aspectj-1.7.0.M1.jar


2. 이클립스 AJDT 플러그인 설치

이클립스 AspectJ 개발 플러그인인 AJDT를 설치합니다. http://www.eclipse.org/ajdt/downloads/ 

(이클립스 메뉴에서 Help > Install New Software... 를 선택한 후 설치 주소에 업데이트 주소 입력)

저는 개발버전인 AJDT 2.2.x를 설치했습니다. http://download.eclipse.org/tools/ajdt/37/dev/update


3. 클래스패스에 aspectrt.jar 파일 추가

aspectrt-1.7.jar 파일을 안드로이드 프로젝트의 libs 폴더에 넣어줍니다. ADT(저의 경우는 r17을 쓰고 있습니다)가 libs 폴더의 모든 jar 파일들을 자동으로 'Android Dependencies' 라이브러리에 추가해줍니다.


4. 프로젝트에 AspectJ 특성 추가

프로젝트 오른 클릭 >  Configures > Convert to AspectJ Project 를 선택하여 AspectJ 특성을 추가합니다. 그러면 프로젝트 빌더에 AspectJ Builder가 추가되고 Java Builder는 제거되며, 빌드패스에 'AspectJ Runtime Library'가 자동으로 추가됩니다.

그러나 Java Builder가 제거되면 이클립스 Problems 뷰 및 Tasks 뷰에 기존에 보였던 컴파일 에러라든가 TODO 목록이 안보이게 되므로 .project 파일에   아래와 같이 다시 Java Builder를 강제로 추가해줍니다.


  <buildSpec>

    <buildCommand>

      <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>

      <arguments>

      </arguments>

    </buildCommand>

    <buildCommand>

      <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>

      <arguments>

      </arguments>

    </buildCommand>

    <buildCommand>

      <name>org.eclipse.wst.common.project.facet.core.builder</name>

      <arguments>

      </arguments>

    </buildCommand>

    <buildCommand>

      <name>org.eclipse.jdt.core.javabuilder</name>

      <arguments>

      </arguments>

    </buildCommand>

    <buildCommand>

      <name>org.eclipse.ajdt.core.ajbuilder</name>

      <arguments>

      </arguments>

    </buildCommand>

    <buildCommand>

      <name>com.android.ide.eclipse.adt.ApkBuilder</name>

      <arguments>

      </arguments>

    </buildCommand>

  </buildSpec>


그리고  'AspectJ Runtime Library' 라이브러리 변수는 제거해줍니다.


5. aspect 작성

예를들어 src/my/app/aop/MyFirstAspect.aj 파일을 만들고 포인트컷과 어드바이스 등을 코딩해줍니다.


AspectJ 코딩 방법은 다음사이트를 참고합니다.


http://dev.anyframejava.org/anyframe/doc/core/3.2.1/corefw/guide/aop-based-aspectj.html

http://blog.daum.net/oraclejava/15858189



개발시점에서 aspect 소스 컴파일은 AJDT로 충분하지만 릴리스용 빌드생성 또는 자동빌드를 위해서는 Ant 빌드(build.xml)을 이용하는 것이 편리합니다. (이하 Android SDK r17 버전 기준 설명입니다)


AspectJ 사용을 위한 Ant 빌드 설정

(1) local.properties 파일에 아래내용을 추가합니다.


# AspectJ 컴파일러 홈

aspectj.home=D:\\Compiler\\aspectj1.7


(2) project.properties 파일에 아래내용을 추가합니다.


# AspectJ 소스 리스트 파일 경로

aspectj.src.list=aspect-list.txt


(3) custom_rules.xml 파일을 아래의 내용으로 새로 작성합니다.(기존에 파일이 존재한다면 내용을 추가)


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

<project name="custom_rules" default="-post-compile">


  <!-- AspectJ 컴파일 추가 -->

  <taskdef resource="org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties">

   <classpath>

     <pathelement location="${aspectj.home}/lib/aspectjtools.jar" />

   </classpath>

  </taskdef>

  <!-- iajc 타스크 설명 : http://www.eclipse.org/aspectj/doc/released/devguide/antTasks.html -->

  <!-- sourceRoots, inpath, aspectpath에 대한 설명 : http://www.jroller.com/tmjee/entry/iajc_usage -->

  <target name="-post-compile">

    <echo message="Weaving aspects to .class files before dex converts .class files to .dex file" />

   <!-- aspectpathref : 라이브러리에 속한 애스펙트도 적용할 경우 설정 -->

    <iajc destDir="${out.classes.absolute.dir}"

          bootclasspathref="android.target.classpath"

          classpathref="project.libraries.jars"

          classpath="${aspectj.home}/lib/aspectjrt.jar"

          sourceroots="${source.absolute.dir}"

          inpath="${out.classes.absolute.dir}"

          aspectpathref="project.libraries.jars"

          Xlintwarnings="true"

          showWeaveInfo="true"

          encoding="${java.encoding}"

          source="${java.source}"

          target="${java.target}">

    </iajc>

  </target>


</project>


만약 프로젝트 유형이 안드로이드 '라이브러리' 프로젝트라면 아래와 같이 -post-compile 타겟을 정의합니다.


    <echo message="Weaving aspects to .class..." />

    <iajc destDir="${out.classes.absolute.dir}"

          bootclasspathref="android.target.classpath"

          classpathref="project.libraries.jars"

          classpath="${aspectj.home}/lib/aspectjrt.jar"

          Xlintwarnings="true"

          showWeaveInfo="true"

          encoding="${java.encoding}"

          source="${java.source}"

          target="${java.target}">


      <sourceroots>

        <pathelement location="${source.absolute.dir}" />

        <pathelement location="${gen.absolute.dir}" />

      </sourceroots>


      <!--

       inpath="${out.classes.absolute.dir}"

       => 이 옵션을 적용하면 앱에서 라이브러리의 애스펙트도 적용하도록 

            설정한 경우 bad WeaverState.Kind: -115 에러 발생함

      -->

    </iajc>


    <!-- 라이브러리 프로젝트인 경우 -compile 타겟에서 생성된 classes.jar를 위빙된 클래스로 교체 -->

    <if condition="${project.is.library}">

      <then>

        <echo>Overwrite library output jar file with weaved classes...</echo>

        <jar destfile="${out.library.jar.file}" update="false">

          <fileset dir="${out.classes.absolute.dir}"

                   includes="**/*.class"

                   excludes="${manifest.package.path}/R.class ${manifest.package.path}/R$*.class ${manifest.package.path}/Manifest.class ${manifest.package.path}/Manifest$*.class ${manifest.package.path}/BuildConfig.class" />

          <fileset dir="${source.absolute.dir}"

                   excludes="**/*.java ${android.package.excludes}" />

        </jar>

      </then>

    </if>




(4) proguard-project.txt에 다음 내용을 추가합니다.


# aspect 클래스 및 aspect가 적용되는 클래스에서 AspectJ 라이브러리를 참조할 수 없다는 경고 제거

# (can't find referenced class)

-dontwarn org.aspectj.**


# 패키지 변경 금지(주석해제시 런타임에 java.lang.NoSuchMethodError 에러 발생)

#-repackageclasses ''

#-allowaccessmodification


# AspectJ 클래스 보존

-keep class org.aspectj.**


# Aspect 클래스 및 멤버 보존

-keep @org.aspectj.lang.annotation.Aspect class * { *; }

-keepclasseswithmembers class * {

    public static *** aspectOf();

}


# around 어드바이스가 적용되는 target 클래스에서 around 어드바이스 메소드를

# 참조할 수 없다는 경고 제거(can't find referenced method) : aspect 클래스를 지정

-dontwarn my.app.aop.**

(또는 -dontwarn my.app.**.*Aspect 식으로 설정)


(5) aspect 소스 리스트 파일 작성

aspectj.src.list 속성으로 정의한 aspect-list.txt 파일을 생성하고, aspect 소스 리스트를 한줄에 하나씩 나열합니다.

경로는 프로젝트 루트에 대한 상대경로입니다.


src/my/app/aop/MyFirstAspect.aj

src/my/app/aop/MySecondAspect.aj

...


신고
Posted by 에코지오

댓글을 달아 주세요

  1. 2012.04.30 02:18  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

    • BlogIcon 에코지오 2012.04.30 11:51 신고  댓글주소  수정/삭제

      말씀하신 내용만으로는 무엇이 원인인지 파악하기 힘듭니다. 구글링을 하거나 직접 수많은 시도를 해보시는 수밖에 없을거 같습니다.

  2. 안드로이드 학생 2012.05.01 14:30 신고  댓글주소  수정/삭제  댓글쓰기

    일단 aspectj에 로그캣에서 적용된다는 걸 확인하였습니다.
    여기서 가장 궁금한건 aspectj를 이용하여 충고(Advice)한 것을 안드로이드에뮬레이터에 출력을 시킬수 있는지 궁금합니다.

  3. 이민수 2013.07.11 13:52 신고  댓글주소  수정/삭제  댓글쓰기

    정말 유용한 정보 감사합니다.

  4. 한훈 2013.08.27 09:51 신고  댓글주소  수정/삭제  댓글쓰기

    이클립스가 아닌 안드로이드 스튜디오에서는 환경설정을 어떻게 해야 할까요?
    AOP를 사용하고 싶은데 개발환경이 달라 사용을 못하고 있습니다. ㅜㅜ

  5. 김효석 2014.01.09 09:31 신고  댓글주소  수정/삭제  댓글쓰기

    질문이 있어서 글 남깁니다.

    custom_rules.xml 로 들어가긴 하는데, 실제 target이 실행되지 않는데 올려주신 소스는 실제로 실행이 되는건지 궁금합니다.
    굉장히 요긴할 것 같아서 계속 도전해 보고 있는데, ANT와 연동하기가 쉽지 않네요.

    ANT를 빼고는 AspectJ를 적용하는 건 성공했는데, Jenkins에 물릴 생각이라서 ANT와 꼭 연동하고 싶은데 잘 되지 않아서 글 남깁니다.
    그럼 의견 부탁드려요.~~~~^^

안드로이드에 AOP를 적용할 수 있는지 구글 형님께 알아보니 가능하다는 답변이 나옵니다.

=> http://blog.punegtug.org/2010/11/adding-aspect-to-android.html


1. 그러나 안드로이드는 런타임 바이트코드 생성을 지원하지 않기 때문에 컴파일타임 위빙만 가능합니다.

=> http://stackoverflow.com/questions/3759232/aspect-oriented-programming-in-android

2. 구글 Guice는 메소드 인터셉터 같은 일부 AOP 기능을 제공하는데

  이는 런타임 바이트코드 생성 방법을 사용하므로 안드로이드에서는 사용할 수 없습니다.

=> http://code.google.com/p/google-guice/wiki/OptionalAOP

3. 어쨋든 안드로이드에서도 컴파일 타임 위빙을 통한 AOP는 가능하기 때문에

  AspectJ 라이브러리를 이용해 AOP 프로그래밍을 할 수 있습니다.

=> http://deansserver.co.uk/~dean/2011/07/18/aspect-oriented-android-development-tool-integration/
=> http://code.google.com/p/android-aspectj/


4. 그럼 안드로이드에서 AOP를 어디에 활용할수 있을까요?

(1) 공통 에러 처리

- 발생된 Exception을 잡아서 에러로그를 서버로 보내 앱 유지보수에 참고할 수 있습니다.

  (물론 에러정보를 서버로 전송할지 사용자에게 물어야겠죠).

- Exception 발생시 안드로이드는 안드로이드 고유의 에러 창과 함께 앱프로세스가 죽어버리는데,

  이렇게 하기보다는 좀더 우아한 메시지와 함께 에러창을 꾸밀 수 있습니다.

- 또한 앱프로세스가 종료되지 않게 하거나 에러가 발생한 Activity만 종료하게 할 수 있을 것입니다.

  (앱을 종료할지 말지는 에러정책에 따라 달라질 것입니다)


(2) 개발을 위한 로깅

- 개발시 편의를 위해 디버그용 로그를 모든 Activity의 라이프사이클 메소드에 추가할 수 있습니다.

- Activity의 UI 렌더링 속도를 측정하기 위해 onCreate() 실행 전과 onResume() 실행 후의 경과시간을 계산할 수 있습니다.


(3) 아키텍처 위반 체크

- 예를 들어 레이어링 아키텍처를 갖도록 설계된 앱에서 아키텍처를 깨는 호출이 존재하는 경우 아예 컴파일이 안되게 처리할 수 있습니다.


(4) Activity 라이프사이클 메소드에 기능 추가

- 예들들어(이건 정말 그저 예입니다) 로그인이 포함된 앱에서 앱 자체의 세션 타임아웃을 체크하여 세션이 만료된 경우 로그인 Activity로 이동하게 할 수 있습니다. AOP를 이용하여 로그인 Activity를 제외한 다른 모든 Activity의 onCreate()나 onResume() 메소드에 세션타임아웃 체크 로직을 추가하면 됩니다.  단 이 경우 반드시 해당 메소드 "코드"가 액티비티에 존재해야합니다(왜냐면 안드로이드에서는 컴파일타임 위빙만 가능하기 때문)

- 또다른 예로 카톡같은 화면잠금을 생각할 수 있습니다. 모든 Activity의 onResume()과 onPause()에 화면잠금 로직을 쉽게 추가할 수 있습니다.


(5) ... ...



신고
Posted by 에코지오

댓글을 달아 주세요

아래는 안드로이드 개발시 AspectJ 라이브러리를 사용하는 경우 기본적인 ProGuard 설정에 추가해주어야 할 내용입니다.

# aspect 클래스 및 aspect가 적용되는 클래스에서 AspectJ 라이브러리를 참조할 수 없다는 경고 제거
# (can't find referenced class)
-dontwarn org.aspectj.**

# AspectJ 클래스 변경금지
-keep class org.aspectj.**

# Aspect 클래스 보존
-keep @org.aspectj.lang.annotation.Aspect class * { *; }
-keepclasseswithmembers class * {
  public static *** aspectOf();
}

# 패키지 변경 금지(주석해제시 런타임에 java.lang.NoSuchMethodError 에러 발생)
#-repackageclasses ''
#-allowaccessmodification

# around 어드바이스가 적용되는 target 클래스에서 around 어드바이스 메소드를
# 참조할 수 없다는 경고 제거(can't find referenced method) : aspect 클래스를 지정
-dontwarn my.app.aop.**
(또는 -dontwarn my.app.**.*Aspect 식으로 설정)


신고
Posted by 에코지오

댓글을 달아 주세요



티스토리 툴바