주말이 되어 오랜만에 어떤 글을 써볼까 하다가 이제 새로운 회사에 입사한지 한달이 살짝 넘은 터라 안드로이드 기능 개발 프로세스에 대해서 고민하는 시간을 갖도록 하겠습니다. (사실 저는 개발 방법론이나 개발 프로세스 자체는 관심이 없긴 한데, 아키텍쳐와 밀접하게 연관이 되어 있기 때문에 살펴봤네요ㅎㅎ;;;) 티몬에 재직할 때에는 사실 개발 프로세스에 대해서는 제가 신경쓰지 않아도 기획자 및 PM유닛에서 관리하기 때문에 고민할 필요가 없었지만, 지금 재직하는 회사는 벤처 회사이기도 하고 좀 더 기민하게 개발할 필요가 있다는 느낌적인 느낌이 들어 필히 개발 과정 동안 발생할 수 있는 상황에 대해 유연하게 대응할 수 있는 개발 방식에 대한 고민이 필요해 보였습니다.


기획서가 성숙단계에 접어들어 안드로이드 개발팀이 개발을 시작한다고 합시다. 기획서만 있고 디자인, 서버 API가 아직 나오지 않는 상태에서 모바일팀에서는 업무를 어떻게 진행해야 할지가 문제입니다. 디자인 시안이 안나와서 기획서대로 xml를 짜놓은 후 액티비티, 프래그먼트 구현을 했는데, 디자인이 기획서와 상당하게 차이가 나도록 변경되는 경우가 생긴다면 액티비티와 프래그먼트의 UI로직도 변경이 필요합니다. 서버에서 mock-up 데이터를 안내려 주거나 힘든 경우 모바일 클라이언트에서는 mock-up만 오기를 기다려야만 하는가? 이런 문제가 있는데요.


사실 서버에서 데이터를 내려주지 않고 디자인 시안이 나오지 않는 상황에서도 할 수 있는게 있습니다. MVVM 아키텍쳐를 채택해서 ViewModel을 미리 만들어서 기획서에 나온 UI를 가지고 xml을 만들어 바인딩하는 정도의 작업을 할 수 있습니다. ViewModel에 대한 테스트까지 해주면 땡큐죠. View에서 사용할 Presenter도 이 단계에서 정의해 줄 수 있습니다. 만약 디자인 시안이나 최종 디자인이 나왔다면 해당 디자인에 따라 xml을 개발하고 기존 ViewModel과 바인딩만 해주면 되고 서버 API 개발이 어느정도 진행되어 mock-up 데이터를 내려줄 수 있게되었다라고 한다면 json2pojo 서비스를 사용해서 5분만에 POJO클래스를 만들고 POJO를 Adapter 패턴을 사용하여 ViewModel에 사용될 수 있도록 적용해 주기만 하면 됩니다. 그리고 Presenter가 model를 사용하도록 하여 서버 연동을 해주면 작업이 완료가 되겠네요.


좀 더 정리해보면, 

1. 기획서를 바탕으로 ViewModel을 만들고, 테스트할 수 있는 기본 UI를 기획서에서 있는대로 최대한 단순한 xml을 구현한다.

2. ViewModel에 대한 테스트를 수행 및 테스트 코드를 작성한다.

3. Activity, Fragment, RecyclerView등의 View는 ViewModel만을 사용하도록 구현 한다. 절대 Model의 data(POJO)와는 디커플링 시킨다.

4. View layer에 대한 테스트 수행 및 테스트 코드를 작성한다.


윗 단계까지 작업하면 디자인, 서버 API의 mock-up이 준비되지 않아도 기획서에서 요구한 요구사항을 만족시키고 실행 가능한 앱이 됩니다. 


그럼 만약에,


- 기획서가 변경되었다면 어떻게 해야 할까요?

기획서가 변경되면... 사실 답없네요ㅎㅎㅎ 그냥 기존 코드 베이스를 싹다 수정해야 한다면 수정 해야겠죠. 허탈하게 웃을 뿐입니다.


- 디자인 작업이 완료 되었다면 어떻게 해야할까요?

UI디자인에 맞게 xml을 수정하고 data binding을 활용해서 ViewModel과 연동하면 됩니다. 문제가 있네요. ViewModel의 수정이 필요하게되면 ViewModel에 대한 테스트 코드 또한 수정해야하고... 


- 그럼, 서버에서 mock-up 데이터를 내려줄 수 있고 통신 프로토콜 방식에 대한 방식이 확정되었으면 어떻게 해야할까요?

data layer가 부재되어 있다면 data layer를 구현해야 하고 앞서 구현한 Presenter에서 API 요청을 보낼 모델을 사용하도록 합니다. json이 응답으로 오는 경우 json2pojo로 POJO 클래스를 뚝딱 만들면 되겠고, POJO와 ViewModel간 Adapter pattern으로 적용하여 기존 UI 로직 수정이 없도록 하면 개발이 끝나겠습니다.


우선 이러한 방식으로 회사에 적용해 볼까?라는 생각이 듭니다. 아직 제가 검증한 방법은 아니고, 이렇게 하면 어떨까 수준에서 나온 아이디어 입니다. 써보니 순수한 mvvm이 아니군요. 사실 click event 처리를 ViewModel에서해야 하나 Presenter를 만들어서 해야하나 현재 아리까리한 상황입니다. 글 보다는 그림으로 설명하는게 좋을것 같아 다음 글에서는 위에 너무 성의없게 작성한 글을 한번 visualization 하고 코드도 작성해서 설명해야겠습니다.




Posted by 제이제이랩
,

2016/03/15 - [java] - benchmarking 코드를 작성해 보자 #1

2016/06/06 - [java] - Dynamic Proxy를 꼭 써야해? #1


이 글에서는 Dynamic Proxy를 꼭 써야하나? 라는 고민을 해봅시다. 


안드로이드 대표적인 HTTP 라이브러리를 꼽자면 Retrofit2, Volley, Okhttp3 입니다. Retrofit2는 안드로이드 개발자들이 HTTP 통신을 쉽게 사용하도록 Okhttp3 기반의 상위 레이어에 불과합니다. Volley는 구글에서 만들었는데, 2013년 어떤 분이 마이크로 벤치마킹 한 결과가 별로 좋지 않아서(참조 http://instructure.github.io/blog/2013/12/09/volley-vs-retrofit/) 외국에서는 거의 Retrofit을 주로 사용하는 것으로 알고 있습니다.(오픈소스로 된 샘플앱들이 HTTP 라이브러리를 Retrofit으로 된걸로 보면요.) 티몬, 쿠팡, 위메프, 11번가등 apk를 까보면 Volley를 쓰고 있습니다. Volley의 HurlStack을 Okhttp로 사용하게 하면 그나마 성능 이슈는 해결되긴 합니다만, 굳이 사용성 측면에서나 okhttp3나 Retrofit2가 성능이나 메모리 사용에서 더 효율적이기 때문에 새로운 앱을 만들때 Volley를 선택할 필요는 없을 것입니다.


결국엔 서비스앱을 만들 때 Retrofit2를 사용할지 Okhttp3를 사용할지가 문제입니다. 앱의 가독성 측면을 보면 Retrofit2가 더 좋아 보이는데, 한가지 걸리는 것이 있습니다. 그건 바로 Dynamic Proxy를 내부적으로 런타임시 사용하고 있다는 것입니다. 개발자가 선언한 Interface를 런타임에 읽어서 그 인터페이스에서 선언한 메소드 및 annotation 등을 Reflection을 사용해서 처리하여 Proxy class를 만들어 그 구현체를 개발자가 사용하는 방식입니다. 실제로 서버에 HTTP 요청을 보내고 응답을 받는 작업은 Okhttp3가 하고있는 상황입니다. 이 사실만으로도 Reflection의 오버헤드를 피해서 조그이나마 성능 이점을 얻기 위해서는 개발자가 Retrofit2를 피하고 Okhttp3를 직접 사용하는 것이 나을것이라는 막연한 생각이 들수 있습니다. 그러면 어느 정도 성능 이점이 있을지 살펴 보도록 하겠습니다.


JMH를 이용해서 Retrofit2, Okhttp3를 이용하여 각각 동일한 GET 요청을 보내는 코드를 작성하도록 합니다. 동일한 요청을 보내는데 각 라이브러리가 얼마나 시간이 걸리는지 비교하도록 합니다. Benchmarking 코드와 결과는 이 Github[https://github.com/jayjaylab-benchmark/retrofitvsokhttp] repository에 저장하였으니 확인하세요.


하기는 벤치마킹코드입니다. 혹시나 보시고 제가 실수한 사항이 있으면 지적해 주시면 감사하겠습니다. 서버에 GET 요청을 보낸 후 응답으로 오는 json 스트링을 POJO 객체로 unmarshalling하는 지점까지를 성능 측정하는 범위로 정하였습니다. 본 벤치마킹을 실행한 환경은 MAC OS X yosemite version 10.10.5(14F27), 2.7GHz Intel core i5, 8GB 1867MHz DD3이고 JDK 1.8.0_45, VM 25.45-b02 자바환경에서 JVM warm up없이 cold start인 상황에서 측정하였습니다. JMH에서 코드 warm up한 상태에서 벤치마킹할 각 메소드를 각 iteration당 1번만 실행하는 옵션은 없어서 부득히하게 cold start인 경우에만 측정하게 되었습니다. 안드로이드의 경우 client mode로 dalvik이나 ART가 실행될 것으로 추측이 되고 warm up 과정은 없으리라 추측이 되는데, AOSP에서 런타임 까봐야 알겠죠?ㅋ...

public class AndroidHttpLibraryBenchmark {
    @org.openjdk.jmh.annotations.State(Scope.Benchmark)
    public static class State {
        public Retrofit retrofit;
        public OkHttpClient okHttpClient;
        public ObjectMapper objectMapper;

        @Setup(Level.Trial)
        public void setUp() {
            System.out.println("########## setUp() ##########");
            retrofit = new Retrofit.Builder()
                    .baseUrl("https://api.github.com/")
                    .addConverterFactory(JacksonConverterFactory.create())
                    .build();
            okHttpClient = new OkHttpClient();
            objectMapper = new ObjectMapper();
        }

        @TearDown(Level.Trial)
        public void tearDown() {
            System.out.println("########## tearDown() ##########");
            retrofit = null;
            objectMapper = null;
            okHttpClient = null;
        }
    }

    @Benchmark
    @BenchmarkMode({Mode.SingleShotTime})
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void testRetrofit2(State state, Blackhole blackhole) {
        GitHubService service = state.retrofit.create(GitHubService.class);
        Call<List<Repo>> repos = service.listRepos("octocat");
        List<Repo> result = null;

        try {
            retrofit2.Response<List<Repo>> response = repos.execute();
            result = response.body();
        } catch(Exception e) {
            e.printStackTrace();
        }

        blackhole.consume(result);
    }

    @Benchmark
    @BenchmarkMode({Mode.SingleShotTime})
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void testOkhttp3(State state, Blackhole blackhole) {
        HttpUrl url = new HttpUrl.Builder()
                .scheme("https")
                .host("api.github.com")
                .addPathSegment("users")
                .addPathSegment("octocat")
                .addPathSegment("repos")
                .build();

        Request request = new Request.Builder()
                .url(url)
                .build();
        List<Repo> result = null;

        try {
            Response response = state.okHttpClient.newCall(request).execute();
            result = state.objectMapper.readValue(response.body().charStream(),
                    new TypeReference<List<Repo>>(){});
        } catch (Exception e) {
            e.printStackTrace();
        }

        blackhole.consume(result);
    }
}


우선 성능만 보자면... 어떤걸 택하시겠나요? Retrofit2, Okhttp3를 사용하는 메소드를 10번씩 돌린 결과입니다. 네트워크 상황이 벤치마킹 돌릴때마다 일정하고 안정된 상황이라는 가정하에 아래 결과를 보면 Okhttp3를 사용하는게 성능상 이점이 있다고 볼 수 있습니다. 특히나 앱 시작할 때 몇개의 Restful API를 사용하는 경우라면 XXXms의 이점이 있을것으로 보입니다. reddit androiddev subreddit에 이 결과를 올려서 공유를 했습니다.(참고 : https://www.reddit.com/r/androiddev/comments/4rpdlv/retrofit2_vs_okhttp3/).

MicroBenchmark Result #1(2016-07-07T22:41:40+09:00)

Benchmark                                  Mode  Cnt     Score     Error  Units
AndroidHttpLibraryBenchmark.testOkhttp3      ss   10  2389.107 ± 550.925  ms/op
AndroidHttpLibraryBenchmark.testRetrofit2    ss   10  2476.903 ± 327.647  ms/op
MicroBenchmark Result #2(2016-07-08T00:47:12+09:00)

Benchmark                                  Mode  Cnt     Score     Error  Units
AndroidHttpLibraryBenchmark.testOkhttp3      ss   10  2472.586 ± 245.455  ms/op
AndroidHttpLibraryBenchmark.testRetrofit2    ss   10  2920.377 ± 760.955  ms/op
MicroBenchmark Result #3(2016-07-08T12:39:23+09:00)

Benchmark                                  Mode  Cnt     Score      Error  Units
AndroidHttpLibraryBenchmark.testOkhttp3      ss   10  2427.628 ±  556.801  ms/op
AndroidHttpLibraryBenchmark.testRetrofit2    ss   10  3110.812 ± 1659.656  ms/op

Jake wharton이 proxy class instance를 State class에서 만들어 써야 한다는 댓글을 남겼습니다. 그러면 결과는 좀 달라지겠지요. 그렇다면 Okhttp3의 Request instance 또한 State class에서 만들어서 써야하는데 그렇게 되면 제 예상으로는 수치상 차이는 없어질 가능성이 많아 보입니다. 제가 지적하고자 하는 점은 dynamic proxy 사용이 염려되어 proxy class의 인스턴스를 얻는 코드를 벤치마킹에 포함하였고 그와 동일한 행위를 하는 Okhttp3 코드 또한 벤치마킹 코드에 포함하였습니다. Retrofit2는 한번 만들어진 인스턴스를 내부적으로 캐쉬하는 것으로 보이고 Okhttp3에서 사용하는 Request 인스턴스는 개발자가 직접 캐쉬하도록 코드를 작성해야 하는 번거러움이 있습니다. 저에게는 직접 캐쉬하는 코드 작성이 별로 어렵지 않은 터라 다른 경쟁사 앱들과 조금이나마 차이를 만들고자 Okhttp3 선택하고 싶네요. 


마치기에 앞서 이 벤치마킹 수치에 지대한 영향을 미치는 통신 overhead에 관해 언급을 해야겠습니다. Retrofit2의 내부에서 Okhttp3를 사용하기에 HTTP 통신 로직의 오버헤드는 둘 다 동일할 것이고 나머지 Retrofit2 자체의 오버헤드와 네트워크 상태(?)가 벤치마크 수치를 다르게 만들 수 있습니다. 네트워크 상태가 각 벤치마크를 돌릴때마다 다르다고 해도 위 수치는 OkHttp3가 좀더 좋은 결과를 나타내고 있다고 말할 수 있는 수준이라고 보입니다. 또한 Retrofit2, Okhttp3는 안드로이드용 이기 때문에 Hotspot JVM에서 벤치마킹을 하는 JMH에서의 결과를 신뢰할 수 있을지 의문이 들긴 합니다. 그런데 Dalvik이나 ART에서 동일한 벤치마킹을 수행한다고 할 때 결과 양상이 바뀔까? 라는 질문을 했을 때, 글쎄요 수치는 바뀔 수 있을지언정 위 JMH에서 나온 결과 패턴은 바뀔 것 같아 보이진 않아 보입니다. 아무튼 안드로이드상에서 벤치마킹을 수행할 수 있는 툴이 현재로선 제가 없는걸로 알고 있어서 위 결과로 유추할 수 밖에 없겠습니다.


요약 : Retrofit2의 오버헤드가 반드시 Dynamic proxy 사용 때문이라고는 결론을 내릴 순 없습니다. 위 결과로부터 okhttp3을 직접 사용하는 것이 retrofit2를 사용하는 것보다 좀더 나은 성능을 줄 수 있다라고 볼 수 있을것 같습니다. 

Posted by 제이제이랩
,

개인적으로 안드로이드 UI 개발할 때 ButterKnife를 애용합니다.


제가 ButterKnife를 쓰는 가장 중요한 이유는 각 View, Resource Look-up 타임을 없애주고 다수의 안드로이드 개발자들과 협업할때 View, Resource를 바인딩 하는 convention을 제공하기 때문입니다. 성능상 단점이나 장점은 없지만 코드의 가독성을 높여주는데 참 좋은것 같습니다.


http://jakewharton.github.io/butterknife/ 


위 사이트에 가면 제가 중요하게 생각하는 점이 다 설명되어 있는데요.  Activity에서 @Bind, @BindColor, @BindString등의  annotation을 onCreate() 메소드 위에서 명시하여 사용하는게 일반적입니다. 각 멤버 변수가 레이아웃의 어느 뷰와 연결되어 있는지 한눈에 알 수 있습니다. String, Color, Drawable같은 리소스도 아래와 같이 onCreate() 메소드 위에서 다 바인딩하여 사용하는게 일반적인데 이러한 컨벤션을 지키면 소스 코드가 정말 깔끔해 집니다. 그리고 각 리소스와 뷰가 어디에 바인딩 되어 있는지 1초도 안되어 알 수 있습니다.

class ExampleActivity extends Activity {
  @Bind(R.id.title) TextView title;
  @Bind(R.id.subtitle) TextView subtitle;
  @Bind(R.id.footer) TextView footer;

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
    // TODO Use fields...
  }
}

만일 Activity.findViewById() 메소드를 사용하면 어떤 일이 벌어질까요? 아래 코드를 보시죠.

class ExampleActivity extends Activity {
  TextView title;
  TextView subtitle;
  TextView footer;

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);

    title = findViewById(R.id.title);
    subtitle = findViewById(R.id.subtitle);
    footer = findViewById(R.id.footer);
    // TODO Use fields...
  }
}

ButterKnife 없는 코드는 아래와 같이 Activity.findViewById()를 사용하여 바인딩을 해줘야 하는데요. 일명 boilerplate 코드이지요. 또 중요한 건 여러 개발자들과 같이 작업하다보면 Activity.findViewById() 메소드를 이곳 저곳 그곳에서 호출하여 어떤 멤버 변수가 어떤 View, Resource와 바인딩 되어 있는지 찾기가 쉽지 않습니다. ButterKnife로는 1초만에 걸리던게 적게는 수초 많게는 수십초이상이 걸리는데요. UI 개발하면서 개발자가 각 멤버 변수가 어떤 뷰, 리소스에 바인딩 되어있는지 항상 기억하지 못하기 때문에 이를 알기 위해 look-up 을 자주 합니다. 이렇게 자주하는 행위를 ButterKnife를 이용해 해쉬 처럼 빠르게 스트레스 안받고 작업하는 것과 바인딩 포인트를 찾을 때까지 탐색하는 것은 분명 차이가 있습니다. 게으른 코딩을 하기 위해 ButterKnife를 사용해 보시는건 어떠시나요? 완벽히 익히는데 1시간도 걸리지 않습니다. 그리곤 게을러 질 수 있어요. 아래 사이트 참고하셔서 굿 코딩하세요.


http://jakewharton.github.io/butterknife/


추가로 ButterKnife는  클릭 이벤트 핸들러를 annotation으로 명시하고 처리할 수 있습니다. 이 또한 기존의 클릭 이벤트 처리하면서 발생한 boilerplate 코드를 제거한겁니다.


요 아래는 안드로이드 스튜이오 플러그인입니다. 자동으로 layout xml의 뷰에 해당하는 멤버변수를 만들어 주네요~! 엄청 편합니다.

https://github.com/avast/android-butterknife-zelezny

Posted by 제이제이랩
,