Sonar를 통해 안드로이드 앱 소스코드의 품질을 분석하고자 하는 분을 위해 현재 제가 사용중인 Ant 빌드스크립트를 공유합니다.


처음에는 custom_rules.xml 파일에 소스분석 스크립트를 작성했었는데요, 별도의 스크립트(analysis.xml)로 분리시켰습니다. 앱 프로젝트 루트경로에 build.xml, local.properties, project.properties 파일 등과 함께 두시면 됩니다.


[analysis.xml]

<project name="my-android-app-analysis" default="sonar">

  <property file="local.properties" />

  <property file="ant.properties" />

  <loadproperties srcFile="project.properties" />

  <fail message="sdk.dir is missing... "  unless="sdk.dir" />

  <import file="${sdk.dir}/tools/ant/build.xml" />


  <taskdef uri="antlib:org.sonar.ant" resource="org/sonar/ant/antlib.xml">

    <classpath path="${ant.home}/lib/sonar-ant-task-2.0.jar" />

  </taskdef>


  <target name="sonar"

          depends="-set-release-mode, -release-obfuscation-check, -compile"

          description="Sonar를 이용한 소스분석">

    <xpath input="${manifest.abs.file}"

           expression="/manifest/@android:versionName"

           output="app.version.name"

           default="1.0" />


    <property name="sonar.host.url" value="http://127.0.0.1:8888/sonar" />

    <property name="sonar.jdbc.url"

                    value="jdbc:h2:tcp://127.0.0.1:9092/sonar" />

    <property name="sonar.jdbc.username" value="sonar" />

    <property name="sonar.jdbc.password" value="sonar" />

    <property name="sonar.projectKey" value="${project.app.package}:my-android" />

    <property name="sonar.projectName" value="my-android" />

    <property name="sonar.projectVersion" value="${app.version.name}" />

    <property name="sonar.java.source" value="${java.source}" />

    <property name="sonar.java.target" value="${java.target}" />

    <property name="sonar.sourceEncoding" value="UTF-8" />

    <property name="sonar.language" value="java" />

    <!-- property name="sonar.sources" value="${source.absolute.dir},${gen.absolute.dir}" / -->

    <property name="sonar.sources" value="${source.absolute.dir}" />

    <property name="sonar.binaries" value="${out.classes.absolute.dir}" />

    <property name="sonar.libraries" value="${project.target.android.jar},${jar.libs.dir}/*.jar" />

    <property name="sonar.scm.url" value="scm:svn:svn://vcs.xxx.com/trunk/my-android" />


    <!-- 더많은 소나 파라미터 : http://docs.codehaus.org/display/SONAR/Analysis+Parameters -->


    <sonar:sonar xmlns:sonar="antlib:org.sonar.ant" />


  </target>



굵은 글씨로 나와있듯이 -set-release-mode, -release-obfuscation-check, -compile 이렇게 3개의 타겟이 미리 실행되도록 해야 문제없이 소스분석이 이루어집니다. 


이렇게 만드시고 실제 수행은 Jenkins 같은 CI서버에서 수행하시면 지속적으로 소스코드 품질을 체크하실 수 있습니다. 저는 Jenkins 서버에 앱 소스분석용 Job을 만들고 아래와 같이 analysis.xml 빌드스크립트의 sonar 타겟을 실행하게 했습니다.(보통 며칠~몇주에 한번씩 수행하도록 설정)




그러면 아래와 같이 sonar 대시보드 페이지에서 내 앱 소스의 품질지표를 확인할 수 있어서 참 좋습니다.

(부끄럽게도 junit 등의 단위테스트 케이스는 없어서 테스트 커버리지는 0 퍼센트네요 -.-)



Posted by 에코지오
,


WebView의 기본 User-Agent 구하기


String userAgent = new WebView(context).getSettings().getUserAgentString();


WebView의 user-agent 변경하기


webview.getSettings().setUserAgentString("변경된 스트링");



URLConnection의 기본 User-Agent 구하기


String userAgent = System.getProperty("http.agent");


URLConnection의 user-agent 변경하기


HttpURLConnection conn = ...;

conn.setRequestProperty("User-Agent", "변경된 스트링"); 



* 참고

http://stackoverflow.com/questions/3626071/retrieve-user-agent-programatically

http://arabiannight.tistory.com/204

Posted by 에코지오
,

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


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

최근에 안드로이드에서 글로벌한 방법으로 에러를 처리하는 방법에는 어떤 것이 있는지 찾아보다가 우연히 java.lang.Thread.UncaughtExceptionHandler 인터페이스에 대해서 알게됐습니다. 


Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); 


public class MyUncaughtExceptionHandler implements UncaughtExceptionHandler {

        @Override

        public void uncaughtException(Thread thread, final Throwable ex) {

           // 여기서 에러를 처리

        }

}


위와 같은 코드를 통해서 캐치되지 않은 런타임 exception을 일관된 방식으로 한곳에서 처리할 수 있도록 JDK 5.0부터 추가된 것이더군요.


안드로이드에서도 위 방법을 사용하는 것이 가능한데요, UncaughtExceptionHandler 구현시 도움이 될만한 팁을 공유합니다.


1. uncaughtException() 메소드의 맨 마지막에는 반드시 System.exit()를 호출해야 한다. 

그렇지 않으면 앱은 죽은 것도 아니고 작동하는 것도 아닌 불확실한 상태가 됩니다(백그라운드 쓰레드에서 던져진 exception인 경우에는 예외???).


http://stackoverflow.com/questions/13416879/show-a-dialog-in-thread-setdefaultuncaughtexceptionhandler

http://stackoverflow.com/questions/11281182/setdefaultuncaughtexceptionhandler-makes-app-crash-silently


참고로, ACRA의 경우에는 아래와 같은 코드를 사용하고 있습니다.


android.os.Process.killProcess(android.os.Process.myPid());

System.exit(10);



2. Toast를 띄우려면 명시적으로 UI쓰레드에서 띄워야 한다. 

그리고 사용자가 토스트 메시지를 읽을 수 있게 잠깐 쓰레드를 쉬는 것이 좋습니다.


http://stackoverflow.com/questions/11609640/toast-not-showing-up-in-uncaughtexceptionhandler


new Thread() {

@Override

public void run() {

                // UI쓰레드에서 토스트 뿌림

Looper.prepare();

Toast.makeText(getApplicationContext(), "에러메시지", Toast.LENGTH_SHORT)

.show();

Looper.loop();

}

}.start();


// 쓰레드 잠깐 쉼

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

}


 android.os.Process.killProcess(android.os.Process.myPid());

 System.exit(10);



3. Dialog를 띄우려면 Dialog 테마를 가진 새로운 액티비티를 시작하면 된다.

(잘 안되는 경우 ACRA 소스를 참조. Intent.FLAG_ACTIVITY_NEW_TASK 플래그 추가하여 액티비티 시작). 


http://stackoverflow.com/questions/13416879/show-a-dialog-in-thread-setdefaultuncaughtexceptionhandler

http://lky1001.tistory.com/45


Intent crashedIntent = new Intent(getApplicationContext(), MyErrorMessageDialogActivity.class);

crashedIntent.putExtra("error.message", "에러메시지");

crashedIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

startActivity(crashedIntent);


<activity

android:name=".MyErrorMessageDialogActivity"

android:excludeFromRecents="true"

android:finishOnTaskLaunch="true"

android:launchMode="singleInstance"

android:theme="@android:style/Theme.Dialog" >

</activity>


그런데 Toast 띄우는 것과는 다르게 Dialog 띄우는 것은 exception이 UI 쓰레드에서 던져진 것인지 백그라운드 쓰레드에서 던져진 것인지에 따라 다르게 처리해주어야 할 것으로 보입니다. 

실제로 ACRA 라이브러리의 경우 ReportingInteractionMode.DIALOG 모드로 설정했더라도 백그라운드 쓰레드에서 발생한 에러에 대해서는 dialog가 띄워지지 않았습니다(System.exit() 때문?)


예를들어, UI쓰레드에서 던져진 exception인 경우에는 현재 액티비티를 바로 종료하고, 백그라운드 쓰레드에서 던져진 경우에는 현재 액티비티 화면을 그대로 살려두고 싶다면 아래와 같이 System.exit()를 선택적으로 호출할 수 있습니다.

if (Thread.currentThread() == Looper.getMainLooper().getThread() ) {
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);
}


4. 앱 종료된 후 앱을 자동으로 재시작하고 싶으면 AlarmManager를 이용한다.

http://www.kmshack.kr/277#.UZCNP4Ez3E0

http://stackoverflow.com/questions/8943288/how-to-implement-uncaughtexception-android




Posted by 에코지오
,

최근에 Sonar를 이용해서 회사에서 만든 안드로이드 앱 소스의 품질을 분석한 적이 있습니다.




나름 잘 만들었다고 생각하고 있었는데 규칙 위반사항이 꽤 많이 검출되어 놀랐었죠. 다행히 중복코드는 없었습니다. Major한 위반사항부터 고쳐나가다가 패키지간 의존성을 없애기 위해 어떻게 하면 좋을까 고민을 많이 했습니다. 단순히 다른 Activity를 명시적으로 클래스명으로 부르지 않고 인텐트필터를 이용해서 부른다고 패키지간 의존성이 명쾌히 해소되지는 않더군요. 

그러다가 EventBus라는 라이브러리를 발견했습니다. 


EventBus 홈페이지

https://github.com/greenrobot/EventBus 


EventBus 사용 예제 설명

http://awalkingcity.com/blog/2013/02/26/productive-android-eventbus/


EventBus 슬라이드 자료

http://www.slideshare.net/greenrobot/eventbus-for-android-15314813


Square에서 만든 Otto라는 원조(?) 라이브러리도 있지만 greenrobot의 EventBus가 왠지 더 땡기더군요. 사용법도 심플하고, 홈페이지에 나오는 Otto와의 비교자료가 마음을 굳히게 만들었죠. (바빠서 직접 비교 테스트는 패스~)


특히나 저희 앱에서는 로그인, 로그아웃 관련해서 단순히 세션을 조작하는데 그치지 않고 앱 내부적으로 다양한 일을 처리합니다. 그래서 로그인 모듈에서 로그인 완료후 다른 모듈들을 직접(!) 호출하는 방식으로 처리하고 있었는데, LoggedInEvent, LoggedOutEvent라는 걸 만들어서 이벤트버스 방식으로 변경했더니 모듈간 의존성도 없어지고 소스도 깔끔해지고 좋네요. 


물론 별별 상황에서 제대로 안정적으로 오류없이 작동하는지는 좀더 지켜봐야겠지만 현재까지는 만족하고 있습니다. 구글링해보면 사실 Otto가 더 많은 레퍼런스와 개발자의 지지를 받는 것으로 보입니다. Square 개발자의 기술력을 신뢰하기 때문으로 보이는데요, 여유가 생기면 Otto로도 재구현해서 테스트해볼 생각입니다. (EventBus의 컨벤션 방식과 비교해서 Otto의 어노테이션 방식도 나름 장점이 있기에...)


아직까지 EventBus 사용하면서 큰 이슈는 없었는데요 이런 점은 고려하셔야 합니다. 


(1) 이벤트를 수신하는 메소드가 컨벤션에 따라서 onEventMainThread(), onEventBackgroundThread() 와 같은 식이어서 만약 상속관계에 있는 클래스에서 동일한 메소드 시그너쳐를 쓴다면 부모 클래스의 메소드를 호출하는 것을 빼먹으면 안됩니다. 이런 경우는 Otto의 어노테이션 방식이 좀더 유연해보입니다.


(2) 이벤트를 어디서 던지고 어디에서 처리할 것인지 아키텍처적으로 정하는 일이 필요해보입니다. 그냥 아무데서나 이벤트 발생시키고 아무데서나 처리하는 것보다는 Presentation 레이어 던지고 처리한다거나, Biz 레이어에서 던지고 처리한다 처럼 어떤 규칙을 정해놓고 개발하는 것이 나을듯합니다.




Posted by 에코지오
,

작년 하반기에 제가 다니는 회사에서 쇼핑 관련된 앱을 하나 만들었습니다. 

사실 대부분의 기능이 웹으로 구현된 터라 실제 네이티브 앱으로 구현된 기능은 별로 없는(?) 편입니다.

그런데도 사용하는 라이브러리가 참 많다는 생각이 드네요.


라이브러리

용도

 대안

Roboguice

객체 의존성 주입, 모듈 생명주기 관리

 AndroidAnnotations, Dagger

AspectJ

에러 처리, 어노테이션 처리 , 아키텍처룰 체크

 

Spring Android (RestTemplate)

REST 통신

 retrofit, restlet

Jackson Json

JSON-Object 매핑

 Gson

Simple Xml

XML-Object 매핑

 

EventBus

모듈간 의존성 제거

 Otto

Spring Security

데이터 암복호화

 

Zbar

QR코드 리더

 zxing

GCMClient

GCM Push 수신

 


Posted by 에코지오
,

앱단에서 서버와 java.net.HttpURLConnection을 이용해서 POST 방식으로 통신하는 경우에 가끔가다 java.io.EOFException 에러가 발생하더군요. 


Caused by: java.io.EOFException

at libcore.io.Streams.readAsciiLine(Streams.java:203)

at libcore.net.http.HttpEngine.readResponseHeaders(HttpEngine.java:560)

at libcore.net.http.HttpEngine.readResponse(HttpEngine.java:813)

at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:274)

at libcore.net.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:486)

at org.springframework.http.client.SimpleClientHttpResponse.getRawStatusCode(SimpleClientHttpResponse.java:49)

at org.springframework.http.client.SimpleClientHttpResponse.getStatusCode(SimpleClientHttpResponse.java:55)

at org.springframework.web.client.DefaultResponseErrorHandler.hasError(DefaultResponseErrorHandler.java:46)

at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:476)


주로 삼성 갤럭시류의 폰에서 발생하는데, 이 문제 때문에 엄청 골치가 아팠습니다.


구글링해보면 gzip 압축문제 때문이라고 하는 사람도 있고 여러가지 해법이 있었지만 현재까지는 http.keepAlive를 끄는 것이 맞는듯합니다.


public class MyApplication extends Application {

    @Override

    public void onCreate() {

        super.onCreate();

... ...

      

patchEOFException();

        ... ...

    }


    private void patchEOFException() {

        System.setProperty("http.keepAlive", "false");

    }

}



* 참고 사이트

http://stackoverflow.com/questions/13182519/spring-rest-template-usage-causes-eofexception

https://jira.springsource.org/browse/ANDROID-102

http://stackoverflow.com/questions/12280629/nullpointerexception-when-using-spring-resttemplate-in-android

http://android-developers.blogspot.fr/2011/09/androids-http-clients.html

http://stackoverflow.com/questions/3352424/httpurlconnection-openconnection-fails-second-time

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

안드로이드에서 쓰레드 사용기법에 대해서 잘 정리된 자료를 발견했습니다.


안드로이드에는 비동기 작업을 위한 여러 API 들이 제공되는데요, 각각의 차이점과 어떤 경우에 사용하면 좋은지에 대해서 깔끔하게 정리가 되어있네요.



Loader는 이 자료를 보면서 처음 알게되네요.


자료 출처 : http://www.slideshare.net/andersgoransson/efficient-android-threading  


Posted by 에코지오
,

지난 번에는 AndroidAnnotations(AA) 오픈소스 라이브러리의 @Click 어노테이션을 흉내내봤습니다. 이번에는 @Background@UiThread 어노테이션을 흉내내보죠.


먼저 @Background와 @UiThread 어노테이션 클래스를 작성합니다.


(1) Background.java

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;


/**

 * <pre>

 * 메소드를 백그라운드 쓰레드에서 실행해주는 어노테이션.

 *

 * ※ 메소드 제약사항

 * - 메소드 리턴타입은 void만 가능

 * - Exception을 던지는 것이 가능하지만 에러정책에 따라 메소드 caller한테는 throw되지 않을 수 있음

 *

 * ※ NOTE

 * - 이 어노테이션은 작업진행상태 스핀다이얼로그 표시, 작업취소 처리, 작업 콜백메소드 등의 기능을 지원하지 않는다.

 * </pre>

 */

@Documented

@Retention(RetentionPolicy.CLASS)

@Target(ElementType.METHOD)

public @interface Background {

}


(2) UiThread.java

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;


/**

 * <pre>

 * 메소드를 UI 쓰레드에서 실행해주는 어노테이션.

 *

 * ※ delay

 * - delay 파라미터 설정시 해당 밀리초 이후에 실행된다. 기본값은 0이다.

 *

 * ※ 메소드 제약사항

 * - 메소드 리턴타입은 void만 가능

 * - Exception을 던지는 것이 가능하지만 에러정책에 따라 메소드 caller한테는 throw되지 않을 수 있음

 * </pre>

 */

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface UiThread {

    long delay() default 0;

}


자 이제 위 어노테이션이 붙은 메소드를 백그라운드 쓰레드나 UI쓰레드에서 실행시키기 위해서 AOP(AsepctJ)의 힘을 빌려봅니다.


import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;


/**

 * <pre>

 * {@link Background}, {@link UiThread} 어노테이션이 붙은 메소드가

 * 백그라운드 및 UI쓰레드에서 실행되도록 애스펙트로 처리한다.

 * </pre>

 */

@Aspect

public class BackgroundAnnotationAspect {


    private Handler uiHandler = new Handler(Looper.getMainLooper());


    private Executor threadExecutor = Executors.newCachedThreadPool();


    @Around("execution(@com.mycompany.async.Background * *.*(..))")

    public void proceedInBackground(final ProceedingJoinPoint pjp) {

        threadExecutor.execute(new Runnable() {

            @Override

            public void run() {

                try {

                    pjp.proceed();

                } catch (final Throwable e) {

                    // 에러정책에 따라 적절히 에러 처리

                }

            }

        });

    }


    @Around("@annotation(uithread) && execution(@com.mycompany.async.UiThread * *.*(..))")

    public void proceedInUiThread(final ProceedingJoinPoint pjp, UiThread uithread) {

        runInUiThread(new Runnable() {

            @Override

            public void run() {

                try {

                    pjp.proceed();

                } catch (Throwable e) {

                    // 에러정책에 따라 적절히 에러 처리

                }

            }

        }, uithread.delay());

    }



    /**

     * {@link Runnable}을 UI쓰레드에서 실행한다.

     *

     * @param run

     * @param delayMillis 지연시간. 밀리초

     */

    private void runInUiThread(Runnable run, long delayMillis) {

        if (delayMillis <= 0) {

            // 현재 쓰레드가 UI 쓰레드인 경우 그냥 run 실행.

            if (Looper.getMainLooper().getThread() == Thread.currentThread()) {

                run.run();

                return;

            }


            uiHandler.post(run);

            return;

        }


        uiHandler.postDelayed(run, delayMillis);

    }

}


다 됐습니다. 아래처럼 쓰면 됩니다. (꼭 액티비티 내의 메소드에 국한되지 않고 아무 클래스에서든 사용가능합니다)


public class TestAsyncActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ... ...

        Button test = ....;
        test.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                doBackground();
            }
        });
    }

    @Background

    private void doBackground() {

        URL url = new URL("http://mycompany.com/xxx");

        URLConnection conn = url.openConnection();

        byte[] contents = FileCopyUtils.copyToByteArray(conn.getInputStream());

        doUiThread(new String(contents));

    }


    @UiThread

    private void doUiThread(String contents) {

        Toast.makeText(getApplicationContext(), contents, Toast.LENGTH_LONG).show();

    }


유용하게 쓰시길 바랍니다.



Posted by 에코지오
,