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


지난 글에서는 안드로이드 앱에서 중앙집중화된 방식으로 에러를 처리하기 위해 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 에코지오
,

꼭 안드로이드일 필요는 없지만, 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 에코지오
,

포인트컷 재사용


공통 포인트컷만 모아놓은 애스펙트 작성를 작성합니다. 예를들어 MyPointcuts.

다른 애스펙트에서는 MyPointcuts.somePointcut() 처럼 static하게 액세스하여 참조합니다.


@Aspect

public class MyPointcuts {

 @Pointcut("within(my..On*Listener+) && execution(!private !static * on*(..) throws !Exception)")

 public void eventListenerMethods() {}

}


@Aspect

public class MyAspect {

    @Around("my.MyPointcuts.eventListenerMethods()")

    public Object handleListenerException(final ProceedingJoinPoint pjp) {

        try {

            return pjp.proceed();

        } catch (final Throwable e) {

            exceptionHandler.handle(e);

            return null;

        }

    }

}



어드바이스 재사용

abstract 포인트컷을 만들고 그 포인트컷을 사용하는 어드바이스를 작성합니다. 애스펙트를 abstract로 선언해야 합니다(여전히 @Aspect 어노테이션 필요함).


@Aspect

public abstract class MyAspect {

 @Pointcut("")

 protected abstract void toOverridePointcut(); 


 @After("toOverridePointcut()")

 public void myAdvice() {

    ... ...

 }

 ...

}


하위 애스펙트에서 abstract 포인트컷을 구현합니다.


@Aspect

public class YourAspect extends MyAspect {

 @Pointcut("within(android.app.Activity+) && execution(void onCreate())")

 protected void toOverridePointcut() {}


 ... ...

}


Posted by 에코지오
,

RoboGuice 사용환경에서 AspectJ의 aspect 클래스에 모듈을 injection하는 방법입니다.



방법1. static 방식으로 injection


(1) 애스펙트 클래스에서 static 멤버변수로 선언

주입할 모듈을 static 멤버변수로 선언합니다.


 @Inject protected static ContextScopedProvider<T> tProvider; // T는 @ContextSingleton이어야함

 @Inject protected static ExceptionHandler exceptionHandler;//Singleton


(2) Guice 모듈 설정에서 static injection 처리

configure() 메소드에서 requestStaticInjection 메소드를 통해 주입합니다.


requestStaticInjection(ExceptionHandlingAspect.class);


그러나 이 방법은 권장하지 않습니다. Guice에서도 static injection은 deprecated될 것이라고 합니다.

http://code.google.com/p/google-guice/wiki/AvoidStaticState



방법 2. aspect 객체를 구하여 injection


aspect는 AspectJ에 의해 인스턴스화되며 우리가 직접 생성할 수 없습니다. 대부분의 경우 aspect는 싱글턴 객체이며, AspectJ는 싱글턴 aspect 객체를 참조할 수 있는 Aspects.aspectOf() 메소드를 제공합니다.


(1) 주입할 모듈을 애스펙트 클래스에 보통의 멤버변수로 선언


 @Inject protected ExceptionHandler exceptionHandler;


(2) Guice 모듈 설정에서 requestInjection을 통해 injection 처리


requestInjection(org.aspectj.lang.Aspects.aspectOf(ExceptionHandlingAspect.class));


참고:

Posted by 에코지오
,

AspectJ 문서에는 아래와 같이 @within()과 @annotation()에 대해 설명합니다.



즉, Anno라는 어노테이션에 대해서(제가 제대로 해석했다면...)


- @within(Anno) : Anno 어노테이션을 갖는 타입(클래스) 안에 정의된 코드와 관련된 조인포인트

- @annotation(Anno) : 조인포인트 대상이 Anno 어노테이션을 갖는 조인포인트


http://whiteship.tistory.com/379 에서는 이렇게 설명합니다.


- @within(Type) : 선언된 타입에 @Type 어노테이션이 붙어있을 때 그 객체의 모든 execution Join point를 나타냅니다.

- @annotation(Type) : 실행되는 메소드에 @Type 어노테이션이 붙어있을 때 그 메소드의 execution Join point를 나타냅니다.


http://www.egovframe.org/wiki/doku.php?id=egovframework:rte:fdl:aop:aspectj 에서는 이렇게 설명하네요.


- @within(Transactional) : 대상 객체의 선언 타입이 @Transactional 어노테이션을 갖는 모든 결합점

- @annotation(Transactional) : 실행 메소드가 @Transactional 어노테이션을 갖는 모든 결합점



그러니까 @within()은 어노테이션이 붙은 클래스에 적용되고, @annotation()은 어노테이션이 붙은 메소드에 적용된다고 심플하게 이해하고 사용하면 될듯합니다만, 정확히 하자면 @annotation()은 조인포인트가 클래스건 메소드건 따지지 않는 것으로 보입니다.


(1) @annotation(Anno) && within(my.aop.*)

=> my.aop 패키지의 클래스 중에서 Anno 어노테이션이 붙은 클래스


(2) @annotation(Anno) && within(my.aop.*) && execution(* test*(..))
=> my.aop 패키지의 클래스에 정의된 test* 메소드 중에서 Anno 어노테이션이 붙은 메소드

(3) @annotation(Anno) && execution(* test*(..)) 
=> test* 메소드 중에서 Anno 어노테이션이 붙은 메소드

(4) @annotation(Anno) && @within(Anno) && execution(* test*(..))
=> Anno 어노테이션이 붙은 클래스의 test* 메소드 중에서 Anno 어노테이션이 붙은 메소드


대충 감이 오네요.


참고로 @annotation(Anno) && execution(* test*(..)) 포인트컷은 이렇게도 표현할 수 있습니다.

  

execution(@Anno * test*(..))




Posted by 에코지오
,

안드로이드 앱 프로젝트가 AspectJ 클래스를 포함하고 있는 안드로이드 라이브러리 프로젝트를 참조하는 경우에, 라이브러리 프로젝트의 aspect는 예상과는 달리 앱 프로젝트에 적용되지 않습니다. 

그러니까 앱 프로젝트의 소스 컴파일시에 라이브러리 프로젝트의 aspect들과 함께 위빙되어 컴파일이 돼야하는데 그렇지 않다는 겁니다.

아래의 방법으로 해결할 수 있습니다.


1. Aspect Path 설정

앱 프로젝트 속성창 > AspectJ Build > Aspect Path 탭에서 "Add Project"를 통해 라이브러리 프로젝트를 추가해줍니다.

(아래 그림에서는 mobile-framework-android 프로젝트가 라이브러리 프로젝트입니다)




이렇게 하면 AspectJ 컴파일러가 앱 프로젝트 빌드시에 라이브러리 프로젝트에 포함된 aspect를 포함하여 함께 위빙시킵니다.

(라이브러리 프로젝트의 bin/xxx.jar 파일을 추가해도 되며, jar파일이 'Reference Libraries' 변수로 자동 추가됨)


2. Ant 빌드파일 설정

custom_rules.xml의 iajc 타스크에 아래 설정을 추가합니다.


      <aspectPath>

        <!-- 라이브러리 프로젝트에 속한 애스펙트도 적용 -->

        <path refid="project.libraries.jars" />

      </aspectPath>



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 에코지오
,

안드로이드에서 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 에코지오
,

안드로이드에 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 에코지오
,