기존에 작성했던 http 처리기 를 보다 편리한 인터페이스와 버전에 따라 파기된 메서드를 교체하는 작업을 포함하여 대대적으로 개편했습니다.
특히 REST API 등의 이슈에서 흔히 발생하는 헤더처리와 통바디라 불리는 기능들을 메서드별로 처리할 수 있게 개선했습니다.
비동기처리를 위한 준비top
엄격한 메인쓰레드보호 정책에 따라 어지간한 경우에 동기적인 통신을 할 일은 없을 것입니다. 하지만 이미 서브쓰레드 상황이라면 동기적으로 사용하는게 편리할테니 동기 비동기를 전부 대응할 수 있게 계획했습니다.
커넥션 객체가 다양한 정보를 갖게 되므로 이를 소유하고 외부에 편리한 인터페이스를 제공할 수 있는 커넥션 래핑 클래스가 우선적으로 필요합니다.
public HttpURLConnection conn; |
이제 두 번째로 준비할 재료는 비동기 통신을 위한 콜백 인터페이스입니다. 실은 이미 정의되어있는 Runnable 을 쓰면 이래저래 유용할텐데 인자를 못보내는 게 흠입니다. 간략히 정의합니다.
public interface bsCallback{ public void run( bsConn $conn );} |
걍 Runnable 흉내로 run 을 가져왔습니다 ^^;
인터페이스의 설계top
어떻게 사용하는게 편리할까 고민하다가 다음과 같은 결론에 도달했습니다.
- 우선 동기냐 비동기냐를 결정하기 위해 리스너가 왔냐 널이냐로 판단하고
- uri는 무조건 받아야하고
- 뒤의 선택적 인자로 파라메터를 보내는
인터페이스입니다. 실제 사용 예는 다음과 같습니다.
public void run( bsConn $conn ){ |
"@Connection" , "Keep-Alive" |
"@Connection" , "Keep-Alive" |
즉 일반적인 키밸류는 그대로 기술하면 되고 헤더에 쓸 내용은 특별한 약속을 두어 @ 로 시작하면 됩니다.
이제 이를 각 메서드별로 확장하면 다음과 같습니다.
public bsConn Get( bsCallback $listener, String $uri, String...arg ); |
public bsConn Post( bsCallback $listener, String $uri, String...arg ); |
파라메터의 처리top
위의 시그니처에서 String…arg 부분이 여러가지 파라메터를 받아들이는 부분인데 Get, Post 가 전부 사용하고 있으므로 이를 종합적으로 처리할 메서드가 필요합니다. 다음과 같이 httpParam 함수를 만듭니다.
ArrayList<String> httpParam( String[]arg ){ |
int i = 0 , j = arg.length; |
ArrayList<String> t1 = new ArrayList<String>(); |
if ( k.charAt( 0 ) == '@' ){ |
t1.add( k.substring( 1 ) ); |
t0 += "&" + URLEncoder.encode( k, "UTF-8" ) + "=" + URLEncoder.encode( v, "UTF-8" ); |
t1.set( 0 , t0.substring( 1 ) ); |
위 함수는 list를 반환하는데 0번 요소에는 데이터를 정리하여 합친 문자열이 들어가 있고 1번부터는 헤더를 키, 밸류로 차근차근 정리한 요소가 들어가게 됩니다. 따라서 다음과 같은 예를 볼 수 있습니다.
"@Accept-Encoding" , "gzip" |
"@Connection" , "Keep-Alive" |
[ "userid=hika&name=kiwan" , "Accept-Encoding" , "gzip" , "Connection" , "Keep-Alive" ] |
즉 httpParam함수를 통해 String…arg부분을 데이터만 묶어 0번요소에 넣고 나머지는 전부 헤더를 정리한 리스트로 받게 됩니다.
Get의 처리top
Get의 특성은 데이터가 요청몸체에 들어가지 않고 URL에 삽입되는 것이므로 다음과 같이 처리됩니다.
public bsConn Get( bsCallback $listener, String $uri, String...arg ){ |
ArrayList<String> param = httpParam( arg ); |
if ( !param.get( 0 ).equals( "" ) ) uri += "?" + t0.get( 0 ).equals( "" ); |
return httpRun( $end, "GET" , uri, param ); |
이에 비해 post방식은 uri자체에 대한 변화가 필요없으므로 그저 다음과 같이 위임하면 됩니다.
public bsConn Post( bsCallback $listener, String $uri, String...arg ){ |
return httpRun( $end, "POST" , $uri, httpParam( arg ) ); |
이제 진짜 주인공인 httpRun을 만나볼 차례입니다.
Runnable생성 및 실제 http통신처리top
우선 기본적으로 java의 동적 스코프 바인딩전략은 다양하지만 보통 메서드 내에서 인터페이스를 구상할 때는
- final 지역변수나
- 감싸는 인스턴스의 필드가
암묵적 클래스 선언에 자동으로 필드화되어 바인딩 됩니다.
이 방식은 사실 내부적으로는 매번 새 클래스를 만드는 레벨까지는 아니더라도 상당한 부하를 일으킵니다만, 가장 편리하게 사용하는 방법이기도 합니다.
보다 최적화된 구현방법으로는 클래스를 정식으로 선언하고 인자를 일일히 넣어주는게 정석입니다만 달빅컴파일 최적화를 믿고 가보죠 ^^
우선 시그니쳐는 다음과 같습니다.
final Callback $callback, |
final ArrayList<String> $data |
내부에서 즉시 Runnable을 구상할거라 그 안에서 인자를 참조하기 위해 모든 인자를 final 화했습니다.
이제 본문을 보겠습니다.
bsConn httpRun( final Callback $end, final String $method, |
final String $uri, final ArrayList<String> $data ){ |
final bsConn result = new bsConn(); |
Runnable run = new Runnable(){ |
위의 구조를 보면
- 지역변수를 final 로 지정하여 이를 run 내부에서 처리할 수 있게 돕고,
- 마지막 부분에선 bsCallback이 온 경우와 null인 경우로 나눠서
- null인 경우는 즉시 run을 실행한뒤 bsConn을 반환합니다.
- 반대로 bsCallback이 온 경우는 ExecutorService에게 넘겨주고 대신 반환값은 null이 되는
형태입니다.
이러한 방법으로 하나의 함수가 동기, 비동기로 작동하면서도 알고리즘을 중복하지 않고 구상하게 됩니다.
쓰레드풀링처리top
안드로이드 기기들은 성능이 낮고 AP가 뻔하기 때문에 쓰레드를 풀링하는 것이 좋습니다. 일일히 손으로 만들어봐야 버그만 생기니 자바가 제공하는 것을 씁니다.
private ExecutorService _workers = Executors.newFixedThreadPool( 4 ); |
void worker( Runnable $run ){ |
_workers.execute( $run ); |
이 정도 겠죠. 앞의 설명에서 worker( run ) 부분은 바로 이걸 염두해둔 코드였습니다.
실제 통신부의 구현top
이제 남은 건 httpRun 에 run 만 구상해주면 됩니다. 또한 이 부분이야 말로 http 통신 코어이기도 하죠.
Runnable run = new Runnable(){ |
HttpURLConnection conn = null ; |
conn = ( HttpURLConnection ) new URL($uri).openConnection(); |
conn.setRequestMethod($method); |
conn.setConnectTimeout( 10000 ); |
conn.setReadTimeout( 10000 ); |
conn.setRequestProperty( "Connection" , "Keep-Alive" ); |
conn.setRequestProperty( "Accept-Encoding" , "gzip" ); |
conn.setUseCaches( false ); |
if ( !$method.equals( "GET" ) ){ |
conn.setDoOutput( true ); |
conn.setRequestProperty( "Content-Type" , |
"application/x-www-form-urlencoded" ); |
OutputStreamWriter out = null ; |
out = new OutputStreamWriter( conn.getOutputStream() ); |
out.write( $data.get( 0 ) ); |
int i = 1 , j = $data.size(); |
while ( i < j ) conn.setRequestProperty( $data.get( i++ ), $data.get( i++ ) ); |
if ( conn.getResponseCode() == HttpURLConnection.HTTP_OK ){ |
InputStream is = conn.getInputStream(); |
String gzip = conn.getHeaderField( "gzip" ); |
if ( gzip != null && gzip.equalsIgnoreCase( "Accept-Encoding" ) ){ |
is = new GZIPInputStream(is); |
if ( $end != null ) $callback.run( result ); |
log.i( "http Error:" + $uri + ":" + e.toString() ); |
기존에 소개한 형태보다 파일업로드는 안되지만 훨씬 편리하고 무엇보다 헤더정보를 처리할 수 있게 개선되었습니다. 또한 몇몇 파기된 메서드도 제거하여 4.4등에서도 깨끗한 소스가 되었네요 ^^;
전체 소스는 다음과 같습니다.
static public class Conn{ |
public HttpURLConnection conn; |
static public interface Callback{ |
public void run( Conn $conn ); |
static public Conn Get( Callback $end, String $uri, String...arg ){ |
ArrayList<String> param = httpParam( arg ); |
if ( !param.get( 0 ).equals( "" ) ) uri += "?" + t0.get( 0 ).equals( "" ); |
return httpRun( $end, "GET" , uri, param ); |
static public Conn Post( Callback $end, String $uri, String...arg ){ |
return httpRun( $end, "POST" , $uri, httpParam( arg ) ); |
static private ArrayList<String> httpParam( String[]arg ){ |
int i = 0 , j = arg.length; |
ArrayList<String> t1 = new ArrayList<String>(); |
if ( k.charAt( 0 ) == '@' ){ |
t1.add( k.substring( 1 ) ); |
t0 += "&" + URLEncoder.encode( k, "UTF-8" ) + |
"=" + URLEncoder.encode( v, "UTF-8" ); |
t1.set( 0 , t0.substring( 1 ) ); |
static private ExecutorService _workers = Executors.newFixedThreadPool( 4 ); |
static private void worker( Runnable $run ){ |
_workers.execute( $run ); |
Conn httpRun( final Callback $end, final String $method, |
final String $uri, final ArrayList<String> $data ){ |
final Conn result = new Conn(); |
Runnable run = new Runnable(){ public void run(){ |
HttpURLConnection conn = null ; |
conn = ( HttpURLConnection ) new URL($uri).openConnection(); |
conn.setRequestMethod($method); |
conn.setConnectTimeout( 10000 ); |
conn.setReadTimeout( 10000 ); |
conn.setRequestProperty( "Connection" , "Keep-Alive" ); |
conn.setRequestProperty( "Accept-Encoding" , "gzip" ); |
conn.setUseCaches( false ); |
if ( !$method.equals( "GET" ) ){ |
conn.setDoOutput( true ); |
conn.setRequestProperty( "Content-Type" , "application/x-www-form-urlencoded" ); |
OutputStreamWriter out = null ; |
out = new OutputStreamWriter( conn.getOutputStream() ); |
out.write( $data.get( 0 ) ); |
int i = 1 , j = $data.size(); |
while ( i < j ) conn.setRequestProperty( $data.get( i++ ), $data.get( i++ ) ); |
if ( conn.getResponseCode() == HttpURLConnection.HTTP_OK ){ |
InputStream is = conn.getInputStream(); |
String gzip = conn.getHeaderField( "gzip" ); |
if ( gzip != null && gzip.equalsIgnoreCase( "Accept-Encoding" ) ){ |
is = new GZIPInputStream(is); |
if ( $end != null ) $callback.run( result ); |
log.i( "http Error:" + $uri + ":" + e.toString() ); |
실제 사용은 다음과 같은 형태가 되겠죠(어리둥절 할 정도 짧습니다 ^^)
new bsHttp.Callback(){ public void run( bsHttp.Conn $conn ){ |
InputStream is = $conn.is; |
Log.i( "response:" + steam2string( is ) ); |
bsHttp.Conn $conn = bsHttp.Post( null , "http://.../name" , "name" , "kiwan" ); |
Log.i( "response:" + stream2string( $conn.is ) ); |