본문으로 바로가기

[android] 개선된 http통신

category etc/유용해요 2015. 5. 7. 14:38
http://www.bsidesoft.com/?p=1021#runnable%ec%83%9d%ec%84%b1-%eb%b0%8f-%ec%8b%a4%ec%a0%9c-http%ed%86%b5%ec%8b%a0%ec%b2%98%eb%a6%ac
android


기존에 작성했던 http 처리기 를 보다 편리한 인터페이스와 버전에 따라 파기된 메서드를 교체하는 작업을 포함하여 대대적으로 개편했습니다.

특히 REST API 등의 이슈에서 흔히 발생하는 헤더처리와 통바디라 불리는 기능들을 메서드별로 처리할 수 있게 개선했습니다.

 
 
 

비동기처리를 위한 준비top

엄격한 메인쓰레드보호 정책에 따라 어지간한 경우에 동기적인 통신을 할 일은 없을 것입니다. 하지만 이미 서브쓰레드 상황이라면 동기적으로 사용하는게 편리할테니 동기 비동기를 전부 대응할 수 있게 계획했습니다.

커넥션 객체가 다양한 정보를 갖게 되므로 이를 소유하고 외부에 편리한 인터페이스를 제공할 수 있는 커넥션 래핑 클래스가 우선적으로 필요합니다.

public class bsConn{
 
  //연결객체
  public HttpURLConnection conn;
 
  //결과값
  public InputStream is;
 
  //내부에서만 생성하는걸로..
  private bsConn(){}
}

이제 두 번째로 준비할 재료는 비동기 통신을 위한 콜백 인터페이스입니다. 실은 이미 정의되어있는 Runnable 을 쓰면 이래저래 유용할텐데 인자를 못보내는 게 흠입니다. 간략히 정의합니다.

public interface bsCallback{public void run( bsConn $conn );}

걍 Runnable 흉내로 run 을 가져왔습니다 ^^;

 
 
 

인터페이스의 설계top

어떻게 사용하는게 편리할까 고민하다가 다음과 같은 결론에 도달했습니다.

  1. 우선 동기냐 비동기냐를 결정하기 위해 리스너가 왔냐 널이냐로 판단하고
  2. uri는 무조건 받아야하고
  3. 뒤의 선택적 인자로 파라메터를 보내는

인터페이스입니다. 실제 사용 예는 다음과 같습니다.

//비동기 Get
Get(
  //비동기 콜백
  new bsCallback(){
    public void run( bsConn $conn ){
      //로딩완료시 할일
    }
  },
 
  //로딩할 url
  "http://.../regist",
 
  //데이터 key, value
  "userid""hika",
  "name""kiwan",
 
  //커스텀헤더
  "@Connection""Keep-Alive"
);
 
//동기화된 Get
bsConn conn = Get(
  //콜백을 null로 보냄!
  null,
  //이하 상동
  "http://.../regist",
  "userid""hika",
  "name""kiwan",
  "@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;
  String t0 = "";
  ArrayList<String> t1 = new ArrayList<String>();
 
  //미리 첫번째 요소를 예약해둠
  t1.add("");
 
  try{
    while( i < j ){
 
      String k = arg[i++];
      String v = arg[i++];
 
      //@로 시작하면 헤더처리함
      if( k.charAt( 0 ) == '@' ){
        t1.add( k.substring( 1 ) );
        t1.add( v );
 
      //아니라면 데이터에 더해감
      }else{
        t0 += "&" + URLEncoder.encode( k, "UTF-8" ) + "=" + URLEncoder.encode( v, "UTF-8" );
      }
    }
 
    //첫번째 요소를 생성한 데이터 텍스트로 교체
    t1.set( 0, t0.substring( 1 ) );
 
  }catch( Exception e ){}
 
  return t1;
}

위 함수는 list를 반환하는데 0번 요소에는 데이터를 정리하여 합친 문자열이 들어가 있고 1번부터는 헤더를 키, 밸류로 차근차근 정리한 요소가 들어가게 됩니다. 따라서 다음과 같은 예를 볼 수 있습니다.

Get(
  callback,
  url,
  "userid""hika",
  "name""kiwan",
  "@Accept-Encoding""gzip"
  "@Connection""Keep-Alive"
);
 
//위와 같이 호출한 경우 httpParam의 예상 결과
["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 ){
 
  //우선 arg를 정리하고
  ArrayList<String> param = httpParam( arg );
 
  //0번에 빈값이라면 원래 url을 보내면 되지만,
  String uri = $uri;
 
  //데이터가 있다면 이를 url뒤에 붙여준다
  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의 동적 스코프 바인딩전략은 다양하지만 보통 메서드 내에서 인터페이스를 구상할 때는

  1. final 지역변수나
  2. 감싸는 인스턴스의 필드가

암묵적 클래스 선언에 자동으로 필드화되어 바인딩 됩니다.

이 방식은 사실 내부적으로는 매번 새 클래스를 만드는 레벨까지는 아니더라도 상당한 부하를 일으킵니다만, 가장 편리하게 사용하는 방법이기도 합니다.

보다 최적화된 구현방법으로는 클래스를 정식으로 선언하고 인자를 일일히 넣어주는게 정석입니다만 달빅컴파일 최적화를 믿고 가보죠 ^^

우선 시그니쳐는 다음과 같습니다.

String httpRun(
  final Callback $callback,
  final String $method,
  final String $uri,
  final ArrayList<String> $data
);

내부에서 즉시 Runnable을 구상할거라 그 안에서 인자를 참조하기 위해 모든 인자를 final 화했습니다.

이제 본문을 보겠습니다.

bsConn httpRun( final Callback $end, final String $method,
    final String $uri, final ArrayList<String> $data ){
 
  //결과를 수신할 객체를 final로 선언
  final bsConn result = new bsConn();
 
  Runnable run = new Runnable(){
    public void run(){
      /* 여기에서 http통신처리 */
    }
  };
 
  //동기처리시
  if( $callback == null ){
    //즉시 실행하고 반환한다.
    run.run();
    return bsConn;
 
  //비동기처리기
  }else{
    //쓰레드풀관리자에게 위임하고 null반환
    worker( run );
    return null;
  }
}

위의 구조를 보면

  1. 지역변수를 final 로 지정하여 이를 run 내부에서 처리할 수 있게 돕고,
  2. 마지막 부분에선 bsCallback이 온 경우와 null인 경우로 나눠서
  3. null인 경우는 즉시 run을 실행한뒤 bsConn을 반환합니다.
  4. 반대로 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(){
  @Override
  public void run(){
    try{
 
    //conn생성 및 기본셋팅
    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 );
 
    //GET이 아닌 경우는 몸체에 정리한 파라메터를 기록해야함
    if( !$method.equals( "GET" ) ){
      conn.setDoInput( true );
      conn.setDoOutput( true );
 
      //컨텐츠 타입을 xform으로 바꾸고
      conn.setRequestProperty( "Content-Type",
        "application/x-www-form-urlencoded" );
      OutputStreamWriter out = null;
 
      //파라메터의 0번에 있는 내용을 out쪽에 써준다.
      out = new OutputStreamWriter( conn.getOutputStream() );
      out.write( $data.get( 0 ) );
      out.flush();
      out.close();
    }
 
    //남은 파라메터로 헤더에 넣어준다!
    int i = 1, j = $data.size();
    while( i < j ) conn.setRequestProperty( $data.get( i++ ), $data.get( i++ ) );
 
    //준비가 끝났으니 연결
    conn.connect();
 
    //잘 통신했다면..
    if( conn.getResponseCode() == HttpURLConnection.HTTP_OK ){
      InputStream is = conn.getInputStream();
 
      //만약 gzip으로 압축되어있다면 풀어준다.
      String gzip = conn.getHeaderField( "gzip" );
      if( gzip != null && gzip.equalsIgnoreCase("Accept-Encoding") ){
        is = new GZIPInputStream(is);
      }
 
      //결과객체에 넣어준다
      result.conn = conn;
      result.is = is;
    }
 
    //비동기상황이라면 이 시점에 콜백을 호출
    if( $end != null ) $callback.run( result );
 
    }catch( Exception e ){
      log.i( "http Error:" + $uri + ":" + e.toString() );
    }
  }
};

 
 
 

결론top

기존에 소개한 형태보다 파일업로드는 안되지만 훨씬 편리하고 무엇보다 헤더정보를 처리할 수 있게 개선되었습니다. 또한 몇몇 파기된 메서드도 제거하여 4.4등에서도 깨끗한 소스가 되었네요 ^^;

전체 소스는 다음과 같습니다.

public class bsHttp{
 
static public class Conn{
  public HttpURLConnection conn;
  public InputStream is;
  private 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 );
  String uri = $uri;
  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;
  String t0 = "";
  ArrayList<String> t1 = new ArrayList<String>();
  t1.add("");
  try{
  while( i < j ){
    String k = arg[i++];
    String v = arg[i++];
    if( k.charAt( 0 ) == '@' ){
      t1.add( k.substring( 1 ) );
      t1.add( v );
    }else{
      t0 += "&" + URLEncoder.encode( k, "UTF-8" ) +
        "=" + URLEncoder.encode( v, "UTF-8" );
    }
  }
  t1.set( 0, t0.substring( 1 ) );
  }catch( Exception e ){}
  return t1;
}
 
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(){
    try{
    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.setDoInput( true );
      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 ) );
      out.flush();
      out.close();
    }
    int i = 1, j = $data.size();
    while( i < j ) conn.setRequestProperty( $data.get( i++ ), $data.get( i++ ) );
    conn.connect();
    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);
      }
      result.conn = conn;
      result.is = is;
    }
    if( $end != null ) $callback.run( result );
    }catch( Exception e ){
      log.i( "http Error:" + $uri + ":" + e.toString() );
    }
  }};
  if( $callback == null ){
    run.run();
    return bsConn;
  }else{
    worker( run );
    return null;
  }
}
 
}

실제 사용은 다음과 같은 형태가 되겠죠(어리둥절 할 정도 짧습니다 ^^)

//비동기통신
bsHttp.Get(
  new bsHttp.Callback(){public void run( bsHttp.Conn $conn ){
    InputStream is = $conn.is;
    Log.i( "response:" + steam2string( is ) );
  }},
  "http://.../member",
  "userid""hika"
);
 
//동기통신
bsHttp.Conn $conn = bsHttp.Post( null"http://.../name""name""kiwan" );
Log.i( "response:" + stream2string( $conn.is ) );