'Retrofit'에 해당되는 글 2건

  1. 2016.07.08 Dynamic Proxy를 꼭 써야해? #2 1
  2. 2016.06.06 Dynamic Proxy를 꼭 써야해? #1

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 제이제이랩
,

이번 글에서는 java.lang.reflect.Proxy, java.lang.reflect.InvocationHandler에 대해 다루겠습니다. 안드로이드 개발에서 Retrofit을 사용하여 Restful API 사용 로직을 구현하려면 아래와 같이 코드를 작성합니다.


<출처 : http://square.github.io/retrofit/>

public interface GitHubService {
    @GET("users/{user}/repos")
    Call<List<Repo>> listRepos(@Path("user") String user);
}
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);
Call<List<Repo>> repos = service.listRepos("octocat");

위 코드를 보면 개발자는 HTTP 요청을 보내기 위해 Interface를 정의(여기서는 GitHubService)해서 Retrofit.create() 메소드에 전달합니다. 그러면 Retrofit.create()는 GitHubService 인터페이스를 구현한 Proxy class를 만들어 반환하고 개발자는 interface에 선언한 메소드 GitHubService.listRepos() 메소드를 호출하면 "https://api.github.com/users/{user}/repos" 리소스에 GET 요청을 보내고 응답을 List<Repo>로 얻습니다.


Retrofit 사용자는 서버 URL을 설정하고, 어떤 리소스에 무슨 요청을 보낼지(GET, PUT, POST, DELETE 등의 메소드 설정), 응답은 어떤 형식으로 받을지만 정의하면 됩니다. 사용자가 상기처럼 GitHubService와 같은 Interface를 정의하면 Retrofit은 내부적으로 Dynamic Proxy란 기술을 써서 인터페이스에 대한 구현 부분(Proxy class)을 런타임에 만들어줍니다. HTTP 통신을 어떻게 해야하고 에러 처리는 또 어떻게 해야할지 등에 대한 고민은 Proxy 클래스가 처리하고 있습니다.  우선 Dynamic Proxy를 사용하는 샘플을 보면서 Proxy, InvocationHandler가 어떻게 쓰이는지 이해를 해봅시다.

public class TestDynamicProxy1 {
    public static void main(String[] args) {
        TestInterface proxyInstance = (TestInterface)
                Proxy.newProxyInstance(TestInterface.class.getClassLoader(),
                new Class[]{TestInterface.class},
                new CustomInvocationHandler());

        System.out.println(proxyInstance.padSomeSymbolInTail("aaaaa"));
    }

    public interface TestInterface {
        String padSomeSymbolInTail(String text);
    }

    public static class CustomInvocationHandler implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            return args[0].toString() + " FaNtAsTiC TaIlS";
        }
    }
}
위의 코드를 실행 시키면 아래와 같이 표준 출력에 문자열이 출력됩니다.

aaaaa FaNtAsTiC TaIlS


Interface에 정의된 메소드를 호출하면 실제 코드 실행은 InvocationHandler.invoke()에서 수행하는 구조입니다. 사실 이러한 방식은 Spring Data 에서도 제공하는 것으로 DB에서 CRUD 연산을 간편하게 하기 위해 개발자가 아래와 같이 인터페이스를 선언하면 DBMS에 접근하여 커넥션을 맺고 CRUD 동작을 수행하여 원하는 데이터를 반환해 주는 일은 Proxy class가 합니다.


<출처 : Spring in Action 4th edition. p320>

public interface SpitterRepository extends JpaRepository<Spitter, Long> {
    Spitter findByUsername(String username);
}

위와 같이 Dynamic proxy 기술은 자바 라이브러리나 프레임워크에서 자주 사용되고 있습니다. 안드로이드 개발자로서 모바일 환경에서 Dynamic proxy를 사용하는 것에 대해 계속 괜찮은 걸까?라는 의문을 가지고 있어서 다음 포스트에서는  Dynamic proxy 사용으로 어떤 장점과 단점이 있는지 살펴보고 다른 대안이 있으면 그 대안을 구현해 보고 dynamic proxy와 비교해 보겠습니다.


<참고자료>

1. http://docs.oracle.com/javase/1.5.0/docs/guide/reflection/proxy.html

2. https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Proxy.html#getProxyClass(java.lang.ClassLoader,%20java.lang.Class...)

3. https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/InvocationHandler.html

4. Spring in Action 4th edition by Walls, Craig (2014) Paperback[ISBN-13 : 978-1617291203]


Posted by 제이제이랩
,