NHN Business Platform 웹플랫폼개발랩 김원준, 정상혁
Android 애플리케이션에서는 HTTP 통신을 다루는 부분의 비중이 크다. 데이터를 조회하거나 저장하기 위해 서버와 통신하는 모듈은 대부분 HTTP API를 사용하고 있기 때문이다. 사용자가 보는 화면 개발을 제외한다면 HTTP 클라이언트가 애플리케이션 개발의 중심이라고도 할 만하다.
Android 환경에서 HTTP 클라이언트를 개발하는 방식은 다양하다. Android SDK에서 제공하는 기능을 직접 사용하기도 하고, 이를 좀 더 편하게 사용하도록 도와주는 유틸리티 클래스를 프로젝트마다 개발하기도 한다. 오픈 소스 라이브러리를 활용한 애플리케이션도 있다.
이렇듯 Android에서 HTTP 클라이언트와 관련된 개발은 빈도가 높고 선택의 길도 다양하지만, 어떤 클래스를 어떻게 사용하는 것이 최선일지 확신할 수 없는 때가 많다. 이 글에서는 이에 대해서 자세히 살펴보겠다.
SDK에서 제공하는 HTTP 클라이언트의 허점과 변경 사항
Android는 표준 JRE의 클래스와 동일한 형태인 HTTPURLConnection 클래스를 제공한다. 그리고 오픈 소스로 유명한 Apache HttpComponents의 HttpClient도 SDK에 포함되어 있다. 일반 JVM 환경과 동일한 방법으로 개발할 수 있고, JRE에 원래 포함되지 않았지만 많이 사용되었던 오픈 소스까지 바로 쓸 수 있어서 얼핏 보기에는 HTTP 통신을 개발하기에 좋은 환경 같다. 그러나 이러한 모듈들에는 불편한 점이 많다.
특히, 기본 SDK에서 제공하는 클래스들에는 버그가 많았다. 이미 널리 알려진 내용이 많지만 정리하면 아래와 같다.
표 1 Android의 HTTP 관련 버그 사례
대상 클래스 | 버그 설명 | 해결 버전 |
HttpURLConnection | 커넥션 풀을 사용할 때 이전 커넥션에서 남아있던 데이터가 읽힌다.[1] | Froyo(Android 2.2) |
HttpURLConnection | HTTP 헤더의 이름이 소문자로만 되어서 일부 웹 서버에서 잘 인식되지 않는다.[2] | Gingerbread(Android 2.3) |
HttpsURLConnection, DefaultHttpClient | SSL 인증 시 SNI(Server Name Identification)를 지원하지 않아 인증 실패 에러(javax.net.ssl.SSLException: Not trusted server certificate)가 발생한다.[3] | Gingerbread(Android 2.3) |
URLConnection | DIGEST 인증에 실패한다. | Gingerbread(Android 2.3) |
AndroidHttpClient | HTTPS로 첫 번째 POST 요청에서 응답이 느리다.[4] | Ice Cream Sandwich(Android 4.0) |
HttpURLConnection | Gzip 압축과 HEAD 메서드를 동시에 사용하면 에러(java.io.IOException: unexpected end of stream)가 발생한다. | Jelly Bean(Android 4.1) |
그리고, Android SDK는 하위 호환성 때문에 Apache HttpClient의 최신 버전을 따라가지 않고 있다. 이 라이브러리는 지나치게 방대하고 기능이 많아서 하위 호환성을 깨뜨리지 않고 기능을 개선하기가 쉽지 않기 때문이다. Android 개발팀은 새로운 애플리케이션에서는 HttpURLConnection을 쓸 것을 권고한다.[5] 이에 따르면 Gingerbread 미만에서는 HttpURLConnection의 버그를 피하기 위해서 Apache HttpClient를 사용할 것을 권고한다.
버그는 아니지만 버전에 따라 변경된 사항도 고려해야 한다. Android는 Gingerbread부터 HttpURLConnection을 쓰면 디폴트로 헤더에 “Accept-Encoding: gzip”을 추가해서 Gzip 압축을 최대한 활용하도록 변경했다. Honeycomb(Android 3.0)부터는 UI 스레드에서 HTTP 호출을 처리하는 것을 금지하고 HTTP 통신을 하는 모듈에서는 필수적으로 멀티스레드를 쓰도록 강제한다. 이 둘은 바람직한 변경 사항일 수 있지만, 같은 코드가 버전에 따라서 다르게 동작하니 애플리케이션 개발자 입장에서는 달갑지 않을 수도 있다. 그리고 Ice Cream Sandwich부터는 HTTP 응답을 파일로 캐싱하는 기능이 SDK에 기본으로 포함되었다.
Android와 Apache HttpClient의 사이
Android에서 포함된 Apache HttpClient 모듈은 버전이 불명확하고 더 이상 버전이 업데이트되지 않는다. Maven 중앙 저장소에 올라온 정보에 따르면 Android는 Apache HttpComponents-HttpClient 모듈의 4.0.1 버전에 의존하고 있다.[6] 그러나 정확히는 원래의 HttpClient 어느 버전과도 일치하지 않는다. 파일별로 비교하면 4.0-beta1, 4.0-beta2와 가장 비슷하다.[7]
Apache HttpClient는 Android와는 독립적인 오픈 소스이므로 나름대로의 발전을 이어가고 있다. 예를 들면 Apache HttpClient 4.1 미만에서는 ThreadSafeClientConnManager라는 클래스를 생성할 때 HTTP와 HTTPS에 대한 디폴트 포트와 SocketFactory 클래스를 따로 등록해야 했는데, 4.1부터는 매개변수가 없는 디폴트 생성자를 추가로 지원해서 이 클래스를 쓰기가 훨씬 편해졌다. 그리고 Apache HttpClient 4.2부터 ThreadSafeClientConnManager는 더 이상 사용되지 않고, 대신 같은 용도로 PoolingClientConnectionManager라는 클래스가 새로 제공된다.
이렇게 Apache HttpClient는 활발히 변경되는 라이브러리이다. 다양하고 세부적인 사용법까지 관여하는 고수준의 라이브러리이기에 그렇다. SDK의 일부라면 기능을 추가하거나 클래스를 더 이상 사용하지 않게 변경하기가 상당히 부담스럽지만, 외부 라이브러리라면 개선을 위해서 더 과감하게 변경할 수도 있다. Apache HttpClient는 여러 환경에서 사용되기 때문에 특별히 Android의 SDK를 의식하지는 않는 듯하다.
서버 모듈에서 라이브러리 업데이트는 서버 배포 한 번이면 끝난다. 그러나 모바일 클라이언트 모듈에서는 훨씬 어렵다. 단독 애플리케이션이라도 라이브러리를 교체하면 기기별, 운영체제 버전별로 고려해야 할 사항도 많고, 애플리케이션 자체의 업데이트 여부는 사용자에게 달려 있어서 언제 완료될지 보장하기가 어렵다. 그리고 SDK 업데이트는 이와는 비교할 수 없을 만큼 큰 작업이고 자주 실행할 수 없다. Android 1.0 미만에서는 Apache HttpClient 3.x가 제공되다가[8] 지금은 4.x으로 버전이 올라가긴 했지만, 이와 같은 버전 업데이트는 아주 초창기였기에 가능했을 것이다.
결국 Android SDK의 Apache HttpClient 도입은 성공하지 못한 듯하다. 처음에는 일반 Java 개발자에게 익숙해 보였지만, 최신 버전과 멀어질수록 개발자가 겪는 혼동은 더 커져 간다. SDK는 성격상 Apache HttpClient 업데이트를 따라갈 수 없고, Apache HttpClient 최신 버전을 사용하면 SDK와 충돌하는 문제가 발생하여 오히려 방해가 된다. 이 때문에 Apache HttpClient를 Android에서 쓰기 위해 리패키징한 배포판(https://code.google.com/p/httpclientandroidlib/)까지 등장했다.
Apache HttpClient는 많이 쓰이기는 하지만 워낙 다양한 기능과 사용법을 제공하기 때문에 제대로 쓰기 쉽지 않은 라이브러리이다. 예를 들면 Apache HttpClient 3.x에서는 SimpleHttpConnectionManager를 쓰면 releaseConnection() 메서드를 호출해도 소켓(socket)이 바로 닫히지 않고 CLOSE_WAIT 상태에서 대기하는 현상이 있었다.[9] 연결을 즉시 닫기 위해서는 shutdown() 메서드를 호출해야 하는데, 이를 제대로 알고 쓰는 사람은 별로 없다. 그리고 Apache HttpClient 4.x에서도 커넥션 풀을 사용할 때 요청 경로마다 동시 커넥션 숫자를 설정하는 ThreadSafeClientConManager의 setDefaultMaxPerRoute() 메서드를 적절히 지정하지 않아서 성능에 문제가 발생하는 경우도 많았다. 이 설정은 RFC 2616의 8.1.4[10]에 따라 2가 기본값인데, 멀티스레드에서 공유하여 사용할 때는 기본값을 그대로 쓰지 말고 동시 사용자의 숫자에 따라 조절해야 한다.
정리하자면, Android에서 Apache HttpClient는 더 이상 최신 버전을 반영하지 못하므로 기존의 Java 사용자에게 친숙한 환경을 제공한다는 의미는 갈수록 퇴색하게 된다. 앞으로 Android SDK에 포함된 버전과 최신 버전의 차이가 벌어질수록 그 혼동은 더 커질 것이다. 그리고 기능과 옵션이 다양한 라이브러리의 특성 때문에 제대로 쓰는 것도 쉽지 않다. 따라서 지금 Android에서 Apache HttpClient를 쓰는 것은 좋은 선택이 아니다. 그렇다고 해서 HttpURLConnection만 쓰려고 하면 하위 버전에서의 버그도 신경 써야 하고 InputStream 등을 직접 다뤄야 하는 등 사용하기에 불편한 점이 있다. 다음 절에서는 Android에서 HTTP 통신을 도와주는 주요 오픈 소스 라이브러리에서 이런 어려움을 어떻게 극복했는지 살펴보겠다.
오픈 소스 라이브러리
알려진 Android HTTP 클라이언트 오픈 소스 라이브러리 중 다음 6종을 비교, 분석해 보았다.
- Spring-Android RestClient: http://static.springsource.org/spring-android/docs/1.0.x/reference/html/rest-template.html
- google-http-java-client Android: https://code.google.com/p/google-http-java-client/wiki/Android
- Retrofit: https://github.com/square/retrofit
- Loopj Async-HttpClient: http://loopj.com/android-async-http/
- HttpRequest: https://github.com/kevinsawicki/http-request
- Droid-parts Rest Client: http://droidparts.org/
항목별 비교
오픈 소스 라이브러리 6종을 항목별로 비교한 표는 다음과 같다.
표 2 주요 Android HTTP 클라이언트 라이브러리의 기능 비교 1
라이브러리 | Android 특화 구현 | HttpURLConnection 사용 | Apache HttpClient 사용 | 비동기 처리 자체 지원 | Backoff policy | Interceptor |
Spring-Android RestTemplate | O | O | O | X | X | O |
google-http-java-client Android | O | O | O | O | O | O |
Retrofit | O | O | O | O | X | O |
Loopj Async-HttpClient | O | X | O | O | O | O |
HttpRequest | X | O | X | X | X | X |
Droid-parts | O | O | O | X | X | X |
표 3 주요 Android HTTP 클라이언트 라이브러리의 기능 비교 2
라이브러리 | Converter 확장 가능 | 기본 JSON Converter | 기본 XML Converter | Multipart 지원 |
Spring-Android RestTemplate | O | O JacksonJson, Gson | O SimpleXML 사용 | O |
google-http-java-client Android | O | O 자체 Converter | O | O |
Retrofit | O | O Gson 사용 | X | O |
Loopj Async-HttpClient | O | X JSONArray, JsonObject만 반환 | X | O |
HttpRequest | X | X | X | O |
Droid-parts | X | X JSONArray, JsonObject만 반환 | X | O |
각 라이브러리가 Android에서 차지하는 용량은 다음과 같다.
그림 1 라이브러리 필수 JAR 파일 합계 크기
위 비교 항목 중 중요하다고 생각되는 부분을 살펴보겠다.
Android에 특화된 구현
HttpRequest를 제외한 나머지 5종의 라이브러리에는 Android를 위한 구현 클래스가 존재한다. Spring-Android RestTemplate, Loopj Async-HttpClient, Droid-parts는 아예 Android만을 위해 구현되었고, google-http-java-client Android, Retrofit는 일반 Java에서도 쓸 수 있지만 Android 환경에서 특별한 처리를 위한 클래스가 추가되어 있다. HttpRequest는 일반 Java와 Android 모두에서 쓸 수 있고 특별히 Android 환경을 의식한 처리는 없다.
HttpURLConection vs. Apache HttpClient
HttpRequest는 HttpURLConection만을 사용하고, Loopj Async-HttpClient는 Apache HttpClient만을 사용한다. 그 외의 라이브러리는 양쪽 모두를 사용하고 Android 버전에 따라서 어떤 구현 클래스를 쓸 것인지 결정하는 코드가 유사하게 포함되어 있다.
예제 1 Spring-Android의 HttpAccesor.java에서 HTTP 연결 클래스 결정
protected HttpAccessor() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { this.requestFactory = new SimpleClientHttpRequestFactory(); } else { this.requestFactory = new HttpComponentsClientHttpRequestFactory(); } }
예제 2 google-http-java-client Android의 HttpAccesor.java에서 HTTP 연결 클래스 결정
public class AndroidHttp { private static final int GINGERBREAD = 9; public static HttpTransport newCompatibleTransport() { return isGingerbreadOrHigher() ? new NetHttpTransport() : new ApacheHttpTransport(); } public static boolean isGingerbreadOrHigher() { return Build.VERSION.SDK_INT >= GINGERBREAD; } }
예제 3 Restrofit의 Android.java에서 HTTP 연결 클래스 결정
private static class Android extends Platform { @Override Client.Provider defaultClient() { final Client client; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { client = new AndroidApacheClient(); } else { client = new UrlConnectionClient(); } …. } }
예제 4 Dropidparts의 RESTClient.java에서 HTTP 연결 방식 결정
private boolean useHttpURLConnection() { // http://android-developers.blogspot.com/2011/09/androids-http-clients.html boolean recentAndroid = Build.VERSION.SDK_INT >= 10; return recentAndroid && !forceApacheHttpClient; }
객체 Converter지원
여기서 ‘객체 Converter 지원’ 항목은 HTTP 클라이언트 모듈 안에서 최종적으로 파싱하려는 객체로의 변환을 지원하는지 여부를 나타낸다. HTTP 클라이언트 모듈의 호출 결과로 InputStream을받아서 직접 원하는 파서를 사용한다면 어떤 라이브러리를 쓰든 파싱은 가능하지만, 아무래도 한 번의 호출로 원하는 객체를 바로 받을 수 있다면 훨씬 편리하게 사용할 수 있다. 예를 들면 Spring-Android에서는 적절한 Converter만 등록되어 있다면 RestTemplate은 Person이라는 객체를 호출 결과로 바로 받을 수 있다.
예제 5 Spring RestTemplate으로 최종 파싱할 객체를 바로 받기
PersonInfo person = restTemplate.getForObject(URI_TEST_REST, Person.class, "kimwonjun");
Spring-Android RestTemplate와 google-http-java-client Android는 기본 구현체로 JSON과 XML 파서를 모두 지원하고, 이를 확장할 수 있는 지점도 제공한다. Retrofit은 기본 JSON 파서는 제공하지만 XML 파서는 확장 지점을 이용해서 만들어야 한다. Loopj Async-HttpClient와 Droid-parts는 JSON도 최종 객체가 아닌 JSONArray, JSONObject 객체를 반환하기 때문에 그 이후의 파싱은 라이브러리 사용자가 해결해야 한다.
라이브러리별 평가
Spring-Android RestTemplate
서버 프레임워크로 유명한 Spring의 RestClient 모듈을 Android용으로 변경해서 유사한 사용성을 제공하려는 의도의 프로젝트이다. Android의 각 버전별로 잘 대응하고 있고, Gson, Simple XML 등 Android 환경에 적합한 파서를 활용한 기본 Converter 클래스가 제공된다. 원래의 Spring의 RestTemplate에 익숙하다면 사용하기 부담 없다.
AndroidAnnotations에서 @Rest, @Get과 같은 애노테이션을 사용하면 더 짧은 코드로 RestTemplate을 쓸 수 있다. 비동기 처리를 기본으로 지원하지는 않지만, Android의 기본 멀티스레드 지원 클래스인 AsyncTask 등과 함께 사용하면 크게 아쉬운 점은 아니다. 비동기 처리를 도와주는 Robospice(https://github.com/octo-online/robospice)라는 라이브러리에서도 RestTemplate을 지원하고, AndroidAnnotations에서 @Background 같은 애노테이션을 쓸 수 있기 때문에, 비동기 처리를 편하게 할 수 있는 선택의 폭은 넓다. Spring 4.0에서는 RestTemplate에서도 비동기 처리를 지원하는 방안이 논의 중인데[11], 이미 코드가 갈라진 Spring-Android에 이 기능이 반영될지는 알 수 없다.
단점은 우선 비교적 용량을 많이 차지하고, 기능이 많다는 점이다. 필수 JAR 파일 2개가 299,403바이트로, 다른 경량 라이브러리에 비해서 크다. spring-android-rest-template 모듈이 의존하고 있는 spring-android-core 모듈은 순전히 기존 Spring과 동일한 Exception이나 인터페이스 구조를 유지하기 위한 것인데, 기존의 Spring RestTemplate을 똑같이 쓰고 싶지 않은 사람에게는 이러한 의존성 때문에 생기는 용량도 아까울 수 있다. 그리고 단순한 애플리케이션에서 JSON만을 파싱하려는 용도라면 다양한 Converter를 확장할 수 있는 기능도 필요 없다고 느낄 수도 있다.
타임아웃을 설정하기 다소 번거롭다는 것도 단점이다. Android 버전에 따라서 SimpleClientHttpRequestFactory 혹은 HttpComponentsClientHttpRequestFactory를 연결에 사용하는데, 두 클래스 모두 setConnectTimeout(int) 메서드와 setReadTimeout(int) 메서드를 가지고 있지만 인터페이스인 ClientHttpRequestFactory 인터페이스에는 정의되어 있지 않기 때문에 캐스팅을 해야 한다.
Spring의 RestTemplate은 거의 동일한 클래스 형태로 Java와 Android용을 따로 제공하고 있기 때문에, 그 둘의 차이점을 살펴보면 Android용 라이브러리가 갖춰야 할 특성을 파악하는 데 도움이 된다.
Spring-Android의 RestTemplate은 위에서 언급한 버전별 사용 클래스 분기 외에도 다음과 같은 버전별 대응 로직을 구현하고 있다.
- Jelly Bean에서 Authentication Error 메시지 처리
- Froyo 미만 버전
- 커넥션 재사용 시의 버그에 대처해서 keep-alive를 false로 설정
- 요청할 Content Length가 이미 정해져 있더라도 FixedLengthStreamingMode를 사용하지 않고 ChunkedStreamingMode 사용
그 외에도 Spring-Web쪽의 RestTemplate에서는 Default Chunk size가 4096인데 Spring-Android에서는 Default Chunk size가 0으로 선언되어 Android 시스템의 Default Chunk Length인 1024를 사용하는 등의 미묘한 차이가 있다.
Spring-Android에서는 Spring-Web의 모듈에서 꼭 필요하지 않은 패키지는 빼거나 부피를 줄이고, 일부 클래스에서는 복잡한 상속 단계들을 줄이고 있다. http, web 패키지에 있는 클래스들의 필요한 부분만 남기고 상속 단계를 줄이면서 클래스를 바로 사용할 수 있도록 Abstract Class를 final로 바꾼 부분도 있다. 자세한 내용은 다음 표에 정리되어 있다.
표 4 Spring-Web과 Spring-Android의 RestTemplate 차이점 비교
기능 | Spring-Web | Spring-Android |
PATCH 메서드 지원 | O | X |
MappingJackson2HttpMessageConverter, MappingJacksonHttpMessageConverter JSON pretty print 기능 지원 | O | X |
AbstractClientHttpRequest Request에서 GZIP Compressed Body 지원 | X | O |
AbstractClientHttpResponse Response에서 GZIP Compressed Body 지원 | X | O |
org.springframework.http.HttpEntity에서 두 HTTP entity의 헤더와 바디가 같다면 같은 entity로 판단 | O | X |
ParameterizedTypeReference<T> 지원 (T를 java.lang.reflect.Type으로 반환하는 클래스) | O | X |
UriComponents (URI보다 더 강력한 옵션과 URI 템플릿 변수 제공) | abstract Class | final class |
MediaType에서 copyQualityValue(…), removeQualityValue(…) 지원(HTTP 헤더의 accept-params Quality Value에 대한 유틸리티 메서드) | O | X |
ResourceHttpMessageConverter에서 Java Activation Framework 지원 | O | X |
org.springframework.http.converter.json.GsonHttpMessageConverter에서 Gson 기반의 JSON Conveter 지원 | X | O |
org.spring.framework.XmlAwareFormHttpMessageConverter 사용 가능 여부 | X(@Deprecated) AllEncopassingFormHttpMessageConverter를 대신 사용 | O |
org.springframework.web.util.UriUtils에서 encodeHttpUrl(String httpUrl, String encoding) 메서드, encodeUriComponents(String scheme, …) 사용 가능 여부 | X(@Deprecated) encodeUriComponents() 메서드의 파라미터가 너무 많아 대신 UriComponentsBuilder를 사용하는 것을 권장 | O Builder를 사용하는 것보다 가벼움 |
사용법이 비슷하기 때문에 편리한 점도 있지만, 내부 구현을 깊이 들여다보면 미세하게 다른 부분이 있어서 혼동될 여지도 있다.
google-http-java-client Android
google-http-java-client에 Android용 클래스(AndroidUtils, AndroidHttp)만을 추가한 라이브러리이다.
JSON, XML, ATOM 등 다양한 파서를 지원하고, JSON Converter는 라이브러리 의존 없이 Android Json Util을 이용하여 자체 구현되어 있다. GZIP Compression 지원, 비동기 메서드 실행, retry 정책 등이 기본 제공된다.
파싱할 대상 객체에 애노테이션으로 정보를 지정할 수 있다.
예제 6 google-http-java-client에서 파싱할 객체의 정보를 애노테이션으로 지정
public class Person { @Key private String id; @Key private String name; // getter/setter 생략 }
일반 Java에서도 실행되는 모듈에 Android 클래스를 추가한 라이브러리인 만큼 용량이 크다는 단점이 있어서 ProGuard 사용을 권장한다. ProGuard를 사용하면 애플리케이션 크기를 최대 88% 줄일 수 있다.
Loopj Asynchronous Http Client
크기가 25KB로 비교적 작으며, 비동기를 기본으로 지원하는 라이브러리이다. Instagram, Pinterest 등 유명한 애플리케이션에 사용되었다. 용량을 적게 차지하면서도 Request Cancellation. back-off Policy 지원, Binary File Converter, SharedPreferences를 이용한 Persistent Cookie Store 등 유용한 기능을 많이 제공한다.
단점은 AsyncHttpClient.javaApache HttpClient에 대해 강한 의존성이 있다는 것이다. 요청 전후의 작업을 하는 Interceptor나 backoff policy 등에서 Apache HttpClient의 기능을 직접 사용하고 있다.
Retrofit
Annotations 기반으로 주소와 연결 방식을 정의할 수 있어서 사용성이 돋보이는 라이브러리로, Square사에서 사용하다가 공개했다.
애노테이션을 이용하여 정보를 지정하는 방법은 Spring-Android RestTemplate과 AndroidAnnotations를 같이 사용했을 때와 비슷하다.
예제 7 Retrofit의 애노테이션 활용
public interface Me2day { @GET("get_person/{id}.json") Person getPerson(@Name("id") String id); @GET("get_posts/{id}.json") List getPosts(@Name("id") String id ); @GET("get_comments/{id}.json?post_id={post_id}") Comments getComments(@Name("id") String id, @Name("post_id") String postId ); }
Android와 일반 JVM에서 모두 사용할 수 있고, 실행 환경을 조사해서 Android 특성에 맞게 자동으로 설정한다.
단점은 런타임에 수행하는 작업이 많아서 성능 저하가 우려된다는 점이다. 내부적으로 Dynamic Proxy를 사용하고 런타임에 애노테이션을 파싱한다. 두 번째 호출부터는 애노테이션에 있는 정보가 캐시되지만 첫 번째 호출은 길어질 수 있다. 테스트 중에는 두 번째부터는 호출에 100ms 미만이 걸리는 상황도 첫 번째 호출에서는 실행 시간이 1초나 걸리는 상황도 만들어 낼 수 있었다.
HttpRequest
HttpURLConnection의 사용성을 개선한다는 명확하고도 단순한 목표를 가진 라이브러리이다. 30KB의 작은 크기와 클래스 하나 밖에 없는 단순한 구조, 간결한 사용법이 장점이다.
예를 들면 System.out으로 호출 결과를 찍는 코드는 다음과 같다.
예제 8 HttpRequest를 이용하여 호출 결과 출력
HttpRequest.get("http://google.com").receive(System.out);
단점은 HttpURLConnection만 사용하기 떄문에 Gingerbread 이하 버전에서는 HttpURLConnection의 버그 때문에 사용하기 어려울 수 있다는 점이다. 그리고 Android 전용은 아니라서 버전별 대응 등 특화된 처리가 부족하다.
Droid-parts RestClient
종합 프레임워크인 Droid-parts에 포함되어 있는 모듈로, 문서화나 기능 면에서 다른 라이브러리보다 부족한 편이다. 단 아이스크림샌드위치 이상에서 캐시를 알아서 활성화해 준다는 장점이 있다.
우리가 꿈꾸는 모범적인 HTTP 클라이언트는?
여러 개의 라이브러리를 살펴봤지만 모든 상황을 만족시키는 라이브러리를 찾기는 어렵다. 만약 아래 조건을 갖춘 라이브러리가 있다면 꿈의 HTTP 클라이언트라고 불러도 될지 모르겠다.
- HttpRequest나 Retrofit처럼 적은 용량
- google-http-java-client Android나 Retrofit 등처럼 backoff policy 지원
- Spring RestTemplate처럼 버전별 에러 대응 처리와 다양한 Converter 지원 및 확장
- Droid-parts처럼 버전에 따라서 캐시 기본 활성화
- HttpRequest처럼 단순한 사용법
쓰고 나서 보니 일부 상충되는 요소도 있는 듯하다. 필요한 기능이 늘어나면 사용법은 단순해지기 어렵고 용량은 커지기 마련이기 때문이다.
당장 특정 라이브러리를 도입해서 모든 문제를 해결하는 것보다는 이들 라이브러리가 취한 방식을 참고로 해서 당장 쓰고 있는 모듈을 개선해보는 것이 서비스 개발팀 입장에서는 도움이 될지도 모르겠다.
지금까지 살펴본 라이브러리들을 바탕으로 Android 구현체가 갖추어야 할 요건을 정리해보면 다음과 같다.
첫째, 알려진 버전별 에러에 미리 대처한다. Android 구버전에서는 커넥션 풀이나 인증과 관련된 버그가 특히 많았다. 지금까지는 에러가 발생하지 않았더라도 인증 방식이나 서버의 HTTP 옵션 변경으로 에러가 발생할 여지가 있는 부분은 파악해두어야 하겠다.
둘째, 버전에 따라 추가된 기능을 의식하고 대처한다. 캐싱이나 Gzip 압축등이 이에 해당한다.
셋째, 파서에 독립적으로 통신 모듈을 구성한다. Spring RestTemplate의 Converter 모듈 등이 이에 해당한다. 급하게 당장 편하게 구현할 수 있는 파서를 쓰더라도, 향후 애플리케이션의 성능 개선이나 경량화 과정을 거치면서 라이브러리를 변경하거나 추가할 수 있다. 모든 서비스 모듈마다 파서가 침범적으로 쓰였다면 그런 변경이 쉽지 않다. 애플리케이션이 작더라도 수정할 곳이 많으면 실수가 발생할 수 있다.
넷째, 다양한 스레드 방식을 사용할 수 있게 하거나 아예 스레드 처리와는 독립적으로 만든다. 통신 모듈 안에서 직접 new Thread()를 호출한다면 나중에 스레드의 개수를 제한하거나 스레드 풀을 이용하는 통제가 어렵다. AsyncTask 클래스와도 지나치게 강결합되지 않는 것이 좋다고 생각한다. 특별히 라이브러리를 추가하지 않아도 JDK의 Executors 클래스와 인터페이스만 잘 활용하면 유연하게 스레드를 관리할 수 있다.
다섯째, Android 개발팀의 권고 사안에 따라 Apache HttpClient에 대한 직접 의존을 줄인다. 하위 호환성을 위해 당장 해당 패키지를 없애지는 않겠지만, 갈수록 이 모듈의 장점이 퇴색할 것으로 예상된다.
추가로, HTTP의 모든 헤더가 다 중요하지만 content-size 정보는 주의해서 사용해야 한다. 이를 검사해서 통신의 정합성을 확인하는 로직은 경우에 따라 의도대로 동작하지 않는다. Gzip 압축을 썼을 때 그 크기가 달라질 수도 있다. 그리고 망 사업자가 제공하는 이미지 최적화 솔루션 등에서 그런 헤더를 변경하는 사례도 있었다.
마치며
요약하면, Android SDK에서 제공하는 클래스들에는 주의해야 할 점이 있고, 여러 오픈 소스 라이브러리들이 이를 해결한 방식을 참고해서 모범적인 구현체를 만들어나가야 한다는 것이다.
‘클라이언트’는 고객이다. HTTP 클라이언트는 서버에게 요청을 한다는 점에서, 그리고 Android 플랫폼의 SDK를 쓴다는 점에서 여러 의미로 소비자이자 고객이다. 그러나 이 고객은 마음 편하게 받아먹기만 하는 고객이 되어서는 안 된다. 제공자의 불완전함과 변경 가능성을 모두 이해하고 다 받아줄 수 있는 ‘착한 고객’이 되어야 한다.
더불어 모바일 환경에 대처해야 하는 개발의 어려움이 얼마나 큰지를 HTTP 클라이언트를 통해서도 엿볼 수 있다. 버전별 특성에 따라서 애플리케이션 개발자가 해야 하는 일이 많은데, 이는 서버 프로그래밍에서는 자주 일어나지 않는 일이다. 서버 모듈이라면 웬만한 API의 확정과 배포는 몇 년을 끄는 것이 보통이다. Java에 람다 표현식이 들어가는 것도 JDK7에 예정되어 있다가 JDK8로 연기되었고, JDK8의 일정도 2014년으로 계속 연기되고 있다. 언어 차원의 변경이라서 다소 큰 변화임을 감안하더라도 이렇게 몇 년을 끌 수 있는 것을 보면 그런 신중함이 부럽기만 하다.
반면 iOS의 Object C는 그보다는 훨씬 빨리 변해가는 것처럼 느껴지고, 라이브러리 차원이지만 Android의 SDK도 변화를 거듭하고 있다. 빠르게 변해가는 시장 환경 속에서 SDK는 충분한 검증 없이 배포되기도 하고, 일관성을 해치면서 발전하기도 할 것이다. 모바일 애플리케이션 개발자는 역시나 바쁜 일정의 압력에 시달리면서도 플랫폼의 빈틈을 메꿔야 하기도 한다. 이 글에서는 HTTP 클라이언트를 다루었지만, 앞으로도 비슷한 숙제들은 계속 생겨날 것만 같다.
참고 자료
[1] https://code.google.com/p/android/issues/detail?id=2939
[2] https://code.google.com/p/android/issues/detail?id=6684
[3] http://stackoverflow.com/questions/2012497/accepting-a-certificate-for-https-on-android/3998257#3998257, http://stackoverflow.com/questions/9574870/no-peer-certificate-error-in-android-2-3-but-not-in-4
[4] https://code.google.com/p/android/issues/detail?id=13117
[5] http://android-developers.blogspot.kr/2011/09/androids-http-clients.html의 내용 중 “New applications should use HttpURLConnection”
[8] 박헌재, “안드로이드를 지배하는 통신 프로그래밍”, 프리렉, 2011, pp331.
[10] http://www.w3.org/Protocols/rfc2616/rfc2616.txt
- NBP 웹플랫폼개발랩 김원준
- 개발자라 불리어도 부끄럽지 않을 사람이 되기 위해 노력하는 자칭 개발 꿈나무입니다. 카페에서 독서와 모바일게임을 즐기는 소박한 된장남이기도 합니다. 요새는 모바일, 분산이라는 키워드에 관심이 많으며, 그 외에 현대음악, 재즈, 인식론 등에 관심이 많다고 거짓말하고 다닙니다.
- NBP 웹플랫폼개발랩 정상혁
- Java와 Linux 를 주로 쓰는 흔한 주류 개발자이다. 서버,클라이언트, UI 등 다양한 분야를 소화하는 프로그래머가 되려고 노력 중이다. NHN계열사의 여러 조직에서 신규 프로젝트 개발, 기술지원, 교육 업무를 수행해 왔다.