Skip to content

Instantly share code, notes, and snippets.

@benelog
Last active October 20, 2023 02:05
Show Gist options
  • Star 83 You must be signed in to star a gist
  • Fork 20 You must be signed in to fork a gist
  • Save benelog/5981448 to your computer and use it in GitHub Desktop.
Save benelog/5981448 to your computer and use it in GitHub Desktop.
Volley 설명

안드로이드 개발에서 많은 비중을 차지하는 UI패턴은 ListView에서 여러 이미지를 보여주는 Activity입니다. 전형적인 흐름을 정리하면 아래와 같습니다.

​1. 목록조회 API호출

​2. API를 파싱하고 ListView에 데이터를 보여 줌.

​3. 각 아이템마다의 이미지 주소로 다시 서버를 호출

​4. 이미지를 디코딩하고 ImageView에서 보여줌.

흔한 패턴이지만, 각종 예외 상황까지 감안해서 이 흐름을 처리하는데에는 많은 코드가 들어갑니다. API호출을 위한 쓰레드풀 관리나 API호출을 한 후에 화면 회전과 전환이 일어났을 때의 호출 취소, Activity의 Null pointer 체크, ImageView의 재활용과 이미지 Cache등 고려해야할 것이 많습니다. 이렇게 전형적인 UI에서도 다양한 버전에서, 괜찮은 성능으로, 모든 상황에서 크래쉬없이 돌아가도록 만드려면 챙겨야할 요소가 많습니다.

Google IO 2013 에서는 이런 패턴의 처리하기에 적합한 Volley라는 라이브러리가 공개되었습니다. Volley는 다른 안드로이드용 Http 클라이언트 라이브러리가 제공하는 기능을 대부분 제공하면서도 용량이 작습니다. (jar파일 기준으로 86,591 bytes) Spring RestTemplate + Robospice + Universal ImageLoader의 핵심기능을 다 제공하면서 경량입니다. Google의 playstore앱에서 사용된 라이브러리라서 상용에서 바로 쓸 수 있는 수준이라고 합니다. 국내에서도 개발자 커뮤니티를 통해서 이미 Volley를 쓰고 있다고 밝힌 분들이 나오고 있습니다.

Google I/O 2013의 발표와 이 후에 인터넷에 올라온 정보를 정리해보았습니다.

Volley의 기능

  • 요청작업큐 + Thread pool을 이용한 동시 요청 지원.
  • 요청별 우선 순위 : 목록조회와 이미지 다운로드를 할때 목록조회가 우선순위가 높게 설정. 다음 페이지의 목록조회를 요청하면 이전페이지의 이미지로딩이 끝나지 않아도 기다리지 않고 수행.
  • Transparent cache : 요청하는 쪽에서 Cache 적용여부를 의식하지 않아도 됨
  • 이미지 로딩툴, NetworkImageVIew라는 View 제공
  • 요청 취소 : 특정 요청, 해당 Context의 요청 모두 취소할 수 있고, 취소할 규칙도 지정 가능
  • 진저브레드 이전의 HttpURLConnection의 버그 문제 해결 : 2013년 5월 The platform에 기고한 Android의 HTTP 클라이언트 라이브러리 기사에서 다룬 여러 오픈소스가 그랬던 것처럼 진저브레드 이상에서만 java.net.HttpURLConnection을 쓰는 로직이 들어가 있습니다.
  • retry, backoff policy 지원, 확장 기능
  • SPDY 지원 : 발표 당시에는 Volley의 tranport stack으로 OkHttp( http://square.github.io/okhttp/ ) 를 넣는 작업이 누군가가 진행하고 있다고 했는데, 기사를 작성한 이후에 따끈따끈하게 공개 되었습니다.
  • JSON, XML, Image, protobuffer 등 다양한 Response형식을 처리하도록 확장 가능

특히 Google IO의 발표에서 성능에 자신감이 대단했습니다. Google + 팀이 다양한 Network 라이브러리들과 Benchmark를 했을 때 Volley가 모든 것을 다 이겼고, 매번 빨랐고, 어떤 경우에는 10배까지 빨랐다고 합니다. 구글에서 앱을 개발하면서 어렵게 얻은 교훈의 이득을 쉽게 얻을 수 있을것이고, 스스로 개발하는것보다 훨씬 빠를 것이라고 자신 있게 말합니다.

Volley는 프로요부터 최신버전까지 사용할 수 있고, SDK manager를 통한 다운로드 등은 아직 계획된 것은 없다고 합니다.

현재까지는 메모리 Cache에 대한 기본 구현체가 없다는 점이 아쉽습니다. Cache interface의 구현객체가 DiskBaseCache와 NoCache 클래스 밖에 없습니다. 앱의 특성에 맞게 알아서 구현하라는 의미인듯한데, Cache 용량한계만 지정하면 바로 쓸 수 있는 구현체가 기본 제공된다면 더욱 좋을듯합니다.

라이브러리 다운로드

아직 별도의 release 프로세스가 없기 때문에 아래와 같이 git 저장소를 복사해야 합니다.

git clone https://android.googlesource.com/platform/frameworks/volley

Ant 빌드를 실행시켜서 jar파일을 생성할 수 있습니다.

cd volley
ant jar

만약 build.xml이 개발장비에 설치된 Android의 SDK의 버전과 잘 맞지 않는다면 'android update project -p .'를 실행시켜서 다시 생성합니다.

Maven 설정

원래는 Maven 프로젝트가 아니기 때문에 pom.xml을 수동으로 추가하고, mvn install로 설치해야 합니다.

pom.xml 예제

Volley 확장 예제

com.android.volley.toolbox.ImageLoader.ImageCache 구현

  • Ficusk의 LruBitmapCache : android.support.v4.util.LruCache를 상속해서 구현. Google I/O 2013 발표에서 공유.
  • howrobotswork의 DiskBitmapCache : Volley의 DiskBasedCache를 상속해서 구현
  • 전형일님의 WeakImageCache : java.lang.WeakReference를 활용.

Google IO 2013에서 Volley 세션의 발표자인 Ficus Kirkpatrick는 Google쪽에서는 soft reference나 weak reference를 쓰지 않는다고 언급을 하기는 합니다.( And so we don't use soft references or weak references. They don't work well. We use hard references and set a strong budget. we are conservative in setting that budget by scaling down with the screen and really the number of pixels ) 그래도 단순한 구현이라는 점에서 WeakImageCache 참고할만합니다.

com.android.volley.Request 상속

  • Ficusk의 GsonRequest : Google Gson 라이브러리로 json을 파싱하는 구현. Google I/O 2013 발표에서 공유.
  • njzk2의 DelegatingRequest : Request에 addHeader()메소드로 헤더 설정 가능. HTTP 인증 때문에 만든 클래스.

com.android.volley.toolbox.HttpStack 구현\

com.android.volley.toolbox.NetworkImageView 상속

com.android.volley.toolbox.ImageLoader 상속

  • njzkw의 ExtendedImageLoader : 기존 ImageLoader가 내부적으로 ImageRequest를 사용하는데, 이 클래스는 Request 구현체도 사용 가능.

종합

애플리케이션 예제와 설명

Google IO 2013의 Volley 세션 요약

요점은 위에서 정리했으나, 보다 자세한 내용을 원하시는 분들을 위해서 Google IO 2013에서 Ficus Kirkpatrick가 했던 Volley에 대한 발표 내용 전체를 글로 풀어봤습니다.

Volley란 무엇인가?

Volley는 테니스나 발리볼의 발리를 연상하게 하고, 클라이언트가 넷 넘어로 서버에 요청을 쏘면 그것이 돌아온다는 은유가 되기도 합니다.( a client sends a request a server over the net. It comes back.) 그러나 제가 이름을 지을 때는 '일제 사격'( A burst or emission of many things or a large amount at once.) 의 광경을 생각했습니다.

Screen+Shot+2013-05-23+at+3.32.46+PM.png

( 이미지 출처 : http://www.kpbird.com/2013/05/volley-easy-fast-networking-for-android.html )

앱의 UI가 발전하고 아름다워지면서 더 많은 메타데이터와 이미지가 쓰이고, 더 많은 네트워크호출이 필요하게 되었습니다. 그런 요청을 병렬로 날려서 앱의 성능을 더 향상시킬만한 여지가 더 커졌습니다. Volley는 스레딩이나 동기화에 대해서 스스로 생각할 필요없이 병행요청을 쉽게 해주는 라이브러리입니다.

이 라이브러리는 필요한 기능을 당장 바로 제공합니다. 예를 들면 다음과 같습니다.

  • JSON API 접근, 이미지 로딩, Raw Text 처리
  • 메모리와 디스크 캐쉬
  • 강력한 커스터마이징 능력 : 재시도와 백오프 알고리즘, 요청 순위 등에 대해서 스스로 정의 가능.
  • 디버깅과 추적 도구 : 버그를 잡고 프로파일링해서 최적화에 도움이 되는 정보를 제공

그런데 왜?

이미 안드로이드는 Apache Http Client와 HttpURLConnection으로 HTTP클라이언트 라이브러리를 제공하고, 그런 라이브러리로 사람들은 이미 많은 앱을 만들었습니다. 그런데 Foursquare, Twitter, Youtube, New and Weather 같은 앱들은 공통적으로 하고 있는 일이 많습니다. 페이징되는 목록을 보여주고, 썸네일 이미지를 ListView안에 넣습니다. 그러한 앱들은 응답 cache, 재시도, 병렬 요청 등 근본적으로 같은 일을 하는 바퀴를 재발명하고 있습니다.

Volley는 이런것들에 대한 공통 인터페이스를 제공하고, 개발자가 실행로직에 대해서 더 신경쓸 수 있도록 해주고, 구글에서 몇년동안 상용 수준의 앱을 만들면서 얻은 교훈을 포장해 놓았습니다.

Volley는 UI요소에 메타 데이터와 이미지를 넣는 상대적으로 적은 용량의 RPC스타일의 네트워크 동작에 가장 잘 맞습니다. UI와 네트워크가 교차하는 지점에서 탁월합니다. 비슷한 특성을 지닌 백그라운드 RPC작업에도 괜찮습니다. 그러나 응답의 전체가 메모리에 올라가기 때문에 큰 payload에는 적합하지 않습니다. 즉, Video나 MP3 다운로드 같은 Streaming 동작에는 적합하지 않습니다. 구글에서도 RPC스타일에는 Volley를 쓰고 오래걸리는 큰 파일의 다운로드 동작에는 DownloadManager 같은것을 쓰는 몇개의 앱이 있습니다. 모든 작업에 Volley를 쓸 필요는 없습니다.

간단한 앱 예제

지금까지 말한 공통적인 패턴인, 페이지별 조회가 가능한 목록이 있고, 그 목록안의 각각의 항목에는 제목, 설명, 썸네일이 들어가는 앱을 예제로 보겠습니다. 서버에서는 title, description, image_url 이 들어간 객체를 json 배열로 반환하는 단순한 프로토콜을 제공합니다. 클라이언트 쪽은 Activity, Adaptor, ListView로 이루어지는 전형적인 구조입니다. Activity는 Adaptor를 가지고 있고, Adaptor는 API서버로부터 데이터를 불러오고 View를 만들어냅니다.

아래코드는 Adaptor의 전형적인 getView(..) 메소드의 구현입니다.

@Override
public View getView(int position, View view, ViewGroup parent) {
    // Load more if we're close to the end.
    if(closeToEnd(position) && mLoading) {
	loadMoreData()
    }
}

여기에 적용될만한 보편적인 기법은, 이미지를 다운로드해야할 때 getView에서 넘어온 position 파라미터를 신호로 사용하는 방법입니다. 예를 들면 한 페이지에 10개의 항목이 있을때, 8번째 position으로 요청이 들어오면 다음 페이지의 로딩을 시작하는 식입니다.

loadMoreData()는 보통 AsyncTask를 활용합니다. 아래에 doInBackGround(..) 메소드 구현이 있습니다.

protected JSONObject doInBackground(URL ... params) {
	HttpURLConnection conn = (HttpURLConnection)
	params[0].openConnection();
	InputStream input = conn.getInputStream();
	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	copy(input, baos );
	JSONObject jsonRoot = new JSONObject(baos.toString());
	return jsonRoot;
}

구현은 평이하지만 여기에는 반복적인 코드(boilerplate code)가 많이 들어갑니다. 위의 코드에서는 슬라이드에 맞추기 위해 생략했지만 통신실패 Exception처리, 파싱실패 Exception처리와 Stream닫기 같은 코드를 포함하면 코드가 2배는 늘어납니다.

다음에는 onPostExecute(..) 메소드가 메인스레드에서 실행되고, JSON을 파싱해서 각각의 아이템을 뽑아내고, Adaptor와 연결되는 List에 넣은 후에 ListView에 보여질수 있도록 알립니다.

protected void onPostExecute(JSONObject jsonRoot){
      List<Items> items = parseJson(jsonRoot);
      appendItemsToList(item);
      notifyDataSetChanged();
}

데이터와 함께 getView로 돌아와 보겠습니다. 이제 title과 description의 두 텍스트 속성은 보여줄 수 있습니다. 이미지 URL도 있기 때문에 로딩을 시작해야 합니다. 여기에 다시 AsyncTask를 활용합니다.

@Override
public View getView(int position, View view, ViewGroup parent) {
	// Load more if needed, make ViewHolder, etc.

	mTitleView.setText(item.title);
	mDescriptionView.setText(item.description);
	new LoadImageTask(holder.imageView).execute(new URL(BASE\_URL +  item.imageURL));
}

아래에 순진한 방식으로 이미지를 로딩하는 AsyncTask구현을 봅시다.

// private class LoadImageTask extends AsyncTask<URL, Void, BitMap> 
public LoadImageTask(ImageView imageView){
	mImageVIew = imageView;
}

protected BitMap doInBackground(URL ... params) {
	HttpURLConnection conn = (HttpURLCOnnection)
	params[0].openConnection();
	InputStream input = conn.getInputStream();
	return BitmapFactory.decodeStream(input);
}

protected void onPostExecute(Bitmap result) {
	mImageView.setImageBitMap(result);

}

생성자를 통해 ImageView를 저장해 놓고, 나중에 주어진 URL의 이미지를 그 위에 올립니다. 백그라운드에서는 HttpURLConnection을 열어서 응답 스트링을 얻고, 그것을 BitmapFactory에 넘겨서 디코딩합니다. 여기서도 try,catch는 다 생략했습니다. 마찬가지로 반복되는 코드가 많습니다. onPostExecute를 통해 다시 메인스레드에서 생성자를 통해 저장했던 ImageView에 image를 지정합니다.

문제와 해결책

앞의 예제에는 명백한 버그부터 그대로 두고 출시하기에는 너무 비효율적인 요소 등 여러가지 문제점이 있습니다. 그런 문제를 살펴보고, 전형적인 접근방법과 Volley의 접근방식을 비교해 보겠습니다.

문제1 : 모든 네트워크 요청이 순차적으로 일어난다.

앞의 코드에서는 AsyscTask.execute()를 이용했기 때문에 모든 네트워크 요청이 순차적으로 실행됩니다. 그 코드에서는 AsyncTask.executeOnExecutor()를 쓰지 않았고 AsyncTask에 THREAD_POOL_EXECUTOR를 넘기지도 않았습니다. 따라서 요청은 엄격하게 FIFO(First In First Out)로 처리되어서, 스크롤을 내려서 다음페이지를 읽으려고 해도 전페이지의 이미지 로딩이 끝날때까지 기다려야 합니다.

Volley는 스레드풀을 활용해서 자동으로 네트워크 요청을 스케쥴링합니다. 풀의 size를 지정하거나 디폴트 값을 쓸 수도 있습니다. playstore에서 테스트했을 때, thread가 4개일때 우리는 최상의 결과를 얻습니다. Volley는 요청 우선순위도 제공합니다. 그래서 메인메타데이터에 우선순위를 높이고, 이미지로딩에 낮은 우선순위를 줄 수 있습니다. 단 하나의 스레드에서 실행된다고 해도 전 페이지의 이미지 로딩을 기다린다고 다음 페이지를 못 읽는 일은 발생하지 않습니다.

문제2 : 화면을 회전시키면 네트워크에서 다시 모든 데이터를 읽는다.\

화면을 회전시키면 Activity가 destroy된 후에 재생성되고, 처음부터 모든 작업을 시작해야 합니다. 네트워크로부터 모든 데이터를 다시 로딩해야한다는 의미입니다. 이것은 큰 손해입니다. 이미지를 LRU Cache나 HashMap에 넣고 Cache를 이용해서 해결할 수도 있지만, 그런 지루한 작업을 스스로해야 합니다. 그리고 Cache가 process의 lifetime동안만 살아남기 때문에 다음에 앱이 시작될때는 도움이 안 됩니다.

Volley는 응답에 대해서 메모리와 Disk를 활용하는 투명한 Cache를 기본제공합니다.투명한 Cache는 호출하는쪽에서 Cache의 존재를 신경쓰지 않아도 된다는 의미입니다. 즉 Cache가 되면 엄청나게 빠른 네트워크 응답처럼 동작합니다. Volley에서는 스스로 만든 Cache 구현을 써도 되고 기본 제공하는것을 써도 됩니다. 정말 빠르고 10~50k의 광고이미지의 응답시간이 보통 수 millisecond가 걸렸습니다. Volley는 Imageloading에 더 나아간 도구를 제공하는데, 뒤에서 살펴보도록 하겠습니다.

문제3 : AsyncTask는 View 재활용을 방해한다.

비슷한 앱을 구현해본 사람은 앞의 예제에서 버그를 바로 발견하셨을 것입니다. 이미지 로딩 요청을 날리고 그 사이에 사용자가 스크롤을 내렸을 때, View를 ListView에서 재활용하려고 해도 전 페이지에서 요청한 이미지가 뒤늦게 나타날 가능성이 있습니다. 이미지를 로딩하는 AsyncTask가 실제로 끝나기 전에 새 페이지의 View를 보여주는 postExecute(..) 메소드가 실행된다면, 더이상 그 전의 AsyncTask에서 건드려서는 안 되는 ImageVIew에 bitmap을 지정하게 됩니다. 여기서 ViewHolder를 만들고, URL을 저장해서 ImageView에 지정하기 전에 이미지 URL을 확인해볼수도 있습니다. 이 방식은 더 이상 쓸모가 없는 Request가 그대로 끝나게 내버려두고, 그 결과를 그냥 버리기 때문에 느리고 낭비가 심합니다.. AsyncTask를 추적해서, View를 재사용해야 할 때는 요청 취소를 할 수도 있지만, 그렇게 하려면 개발자가 할 일이 많습니다.

Volley는 요청 취소 기능을 제공합니다. 단 하나의 Request나, 특정 범위를 지정해서 취소할 수 있습니다. Volley의 toolbox에서 제공하는 NetworkImageView를 쓰면 요청취소에 대한 모든 것을 알아서 다 처리해줍니다.

문제4 : 프로요에서 호환성 문제

진저브레드 이전까지의 HttpURLConnection은 버그가 많고 신뢰하기 어렵습니다. 여기에 있는 샘플코드도 프로요와 그 이전에서는 문제가 생길 수도 있습니다. Apache HTTP Client를 사용할수도 있지만, 역시나 버그가 많고 더 이상 지원되지 않습니다.

Volley는 네트워크 전송을 추상화했습니다. 기본적으로 프로요 이전버전에는 Apache Http Client, 그 이후 버전에서는 Apache HTTP Client를 씁니다. 이러한 접근법은 문제를 해결할 뿐만 아니라 다른 기회를 제공하는데, FourSquare의 OkHttp를 사용하고 싶다면 전체 코드를 고칠 필요없이 앱의 한 부분만 대체하면 됩니다.

Volley로 구현

Volley에서 주로 사용자가 접하는 2개의 클래스는 RequestQueue와 Request입니다. RequestQueue는 request를 네트워크에 던지는 인터페이스입니다. RequestQueue는 필요에 따라 만들수도 있지만, 보통 singleton으로 씁니다. mRequestQueue 와 ImageLoader는 아래와 같이 생성합니다.

mRequestQueue = Volley.newRequestQueue(context);
mImageLoader = new ImageLoader(mRequestQueue, new BitmapLruCache());

다음 코드는 loadMoreData()의 Volley 버전입니다.

mRequestQueue.add(new JsonObjectRequest(Method.GET, url, null,
	new Listener<JSONObject>() {
		public void onResponse(JSONObject jsonRoot) {
			mNextPageToken = jsonGet(jsonRoot, "next", null);
			List<Items> items = parseJson(jsonRoot);
			appendItemsToList(item);
			notifyDataSetChanged();
		}
	}
);

Volley toolbox에서 제공되는 JsonObjectRequest를 사용해서 호출객체를 만들고 요청큐에 추가합니다. Request객체의 생성자에 파라미터로 남기는 Listener는 결과를 받는 표준 콜백 인터페이스입니다. Listener는 AysncTask의 postExecute(..)와 유사합니다. 여기서 결과를 파싱하고 View에 보여줄 아이템 목록을 추가하고, 데이터셋이 변경되었다는 것을 통지합니다. Boilerplate 코드가 없어졌고, AsyncTask와 비슷한 구조 덕분에 기존의 앱을 포팅하기도 쉽습니다.

이미지를 가지고 오는 작업은 더 편해졌습니다.

// retreving images with ImageLoader
if (holder.imageRequest != null) {
	holder.imageRequest.cancel();
}
holder.imageRequest = mImageLoader.get(BASE_URL + item.image_url,
                                          holder.imageView, R.drawable.loading. R.drawable.error);

이미지 요청을 view holder에 저장해놓고, View를 재활용해야할 때는 요청을 취소합니다. Volley toolbox에 있는 ImageLoader가 나머지를 처리합니다. 메모리 캐쉬를 제공하고, 로딩 중일때, 에러일때 표시될 drawable 자원 이미지를 지정할 수도 있습니다.

더 간편하게는 ImageView의 subclass인 NetworkImageView 클래스를 활용하면 됩니다. Layout 설정 파일에 ImageVIew 선언대신 NetworkImageView를 지정하고 ImageView 객체에 주소를 지정하기만 하면 됩니다.

- \<ImageView
+ \<com.android.volley.NetworkImageView\>
mImageView.setImageUrl(BASE\_URL + item.image\_url, mImageLoader);

NetworkImageView는 View 계층 구조에서 detach될 때 기존의 요청을 알아서 취소해줍니다.

요청일괄처리(response-batching)는 Volley의 또다른 멋진 기능입니다. ImageLoader나 NetworkImageVIew로 이미지 로딩할때 응답을 받아서 잠시 가지고 있다가 UI thread로 한꺼번에 보냅니다. 이 덕분에 여러 이미지가 UI에서 동시에 나타나는 효과를 낼 수 있습니다.

Volley는 새로운 형식의 요청을 만들기에도 쉽습니다. GSON은 리플렉션을 이용해서 JSON형태의 데이터를 Java Model로 넣어주는 라이브러리입니다. 아래에 Volley에서 GSON을 이용한 요청의 예제 코드가 있습니다.

@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
	try {
		String json = new String(response.data,
		HttpHeaderParser.parseCharset(response.headers));
		return Response.success(gson.fromJson(json, clazz),
		HttpHeaderParser.parseCacheHeaders(response));
	} catch (UnsupportedEncodingException e) {
		return Response.error(new ParseError(e));
	} catch (JsonSyntaxException e) {
		return Response.error(new ParseError(e));
	}
}

불과 60줄 정도의 코드이고 위에서는 그 일부만 나왔지만 https://gist.github.com/ficusk/5474673에 전체 코드가 올라가 있습니다.

JSON등답을 문자열 형식으로 만들고, Gson을 이용해서 원하는 객체로 변환시킵니다. HTTP 응답헤더에 있는 charset이나 cache관련 값들을 해석하기 위해 Volley에서 제공하는 HttpHeaderParser를 활용합니다.

이렇게 만든 새로운 Request형식으로 데이터를 로딩하는 코드는 아래와 같습니다.

mRequestQueue.add(
     new GsonRequest<ListResponse> (url, ListResponse.class, null ,
	 new Listener<ListResponse>(){
	     public void onResponse(ListResponse response) {
	         appendItemToList(response.items);
	         notifyDataSetChanged();
	     }
	 }
);

앞에 나온것처럼 단순한 코드입니다. 이 요청 방식에서는 파싱이 메인스레드가 아닌 백그라운드 스레드에서 일어나서 병행성을 더 활용한다는 점도 멋집니다.

내부 구조(Architecture와 semantics)

Volley Architecture

(이미지 출처 : http://readme.skplanet.com/?p=5072 )

Volley의 내부 구조와 구현이 어떻게 되어있는지 살펴보겠습니다. Volley는 스레드풀을 제공하는데, 'Cache dispatcher'에 1개의 스레드, 'Network dispatcher'에 1개 이상의 스레드를 할당합니다.

'Cache Disatcher'는 요청을 분류하는 일을 합니다. 요청을 Cache에서 빼야할지, Network로 호출해야할지를 결정합니다. cache miss나 cache hit나 expired 되었을 때 분기처리를 합니다. Expired cache일때는 cache 응답을 'Network dispatcher'에 넘겨줄 책임도 있는데, ETag match도 활용해서 서버가 304 response code를 줄 수도 있게하기 위해서입니다. Network dispatcher는 HTTP요청을 하고, 결과를 파싱한후에 Main 스레드에 응답을 전달합니다.

라이브러리 사용자라면 이런 내부구조를 크게 신경 쓸 필요는 없고, 메인 스레드에서 요청 큐에 추가하고, 빼기만 하면 됩니다. 그러나 조금 더 진행상황을 깊이 들여다보고 싶다면 디버깅 정보을 활용하면 됩니다. 시스템 속성으로 디버깅 정보를 보겠다고 설정을 하면, 실행 우선순위, 실행 스레드, 단계마다 걸린 시간 등 Dispatch pipline의 상세한 정보를 확인할 수 있습니다. 이런 정보를 통해 어플리케이션의 개선 지점을 찾습니다. PostResponse와 done사이에 많은 시간이 지체되고 있다면 메인 스레드에서 경쟁이 일어나고 있고 거기서 너무 많은 일을 하고 있다는 신호이고, 그 부분을 더 깊이 팔 수도 있습니다.

메인스레드

만약 하나의 스레드에서 모든 작업을 하고, 다른 Worker스레드와 아무것도 공유하지 않는다면 Locking에 대해서 신경쓸 필요없습니다. Locking은 어렵기 때문에 아무도 거기에 대해 고민하기를 원하지 않습니다.

하나의 예를 들어 보겠습니다.

public void onPostExecute(Result r) {
      if (getActivity() == null ) {
	      return;
      }

}

많은 사람들은 위와 같은 코드를 씁니다. AsyncTask가 끝났을 때는 Activity에서 어떤 일이 일어났는지 파악하기에는 너무 늦었기 때문에, NPE 체크 코드를 넣습니다. 그렇지 않으면 postExecute에서 AsyncTask가 끝났을때 UI가 죽을 수도 있습니다.그러나 이런 처리는 무시할 request를 허용할것이기 때문에 CPU 싸이클, 노력과 network bandwidth, 배터리 소모 면에서 낭비입니다. 그리고 전체 코드에 보기 싫은 흔적을 남깁니다. Volley는 강력한 취소 API를 제공합니다. Volley에서는 모든 요청의 결과를 메인스레드로 전달되는데, 만약 메인스레드에서 요청을 취소한다면 Volley는 그 응답이 전달되지 않도록 보장합니다.

아래와 같이 진행 중인 모든 요청건을 하나씩 취소하면 Listener 등에서 Null check를 할 필요가 없습니다.\

@Override
public void onStop(){
    for(Request<?> req : mInflightReqest) {
        req.cancel();
    }
}

그러나 여전히 AsyncTask를 쓸 수도 있습니다. 그럴 경우를 대비해서 Volley에서는 취소할 요청의 범위를 지정할 수 있습니다. 특정 Activity에 속한 모든 요청은 아래와 같이 취소합니다.

public void onStop() {
	mRequestQueue.cancelAll(this);
}

반드시 범위를 Activity로 지정할 필요는 없습니다. 하나의 뷰에 있는 썸네일그룹 같은 더 작은 범위로 정의해도 됩니다.

아래와 같이 RequestFilter를 구현해서 취소할 요청을 선택할 수 있는 커스텀 필터를 만들 수도 있습니다.

public void onStop() {
    mRequestQueue.cancelAll (
		new RequestFilter() {
     	   ...
		} 
   );
}

예를 들면 백그라운드에서 이루어지는 POST 요청은 그대로 두고, 썸네임요청은 취소하는 등의 규칙을 구현할 수 있습니다.

정리

Volley로 앱을 만들고, 있는 앱을 포팅하는 것은 쉽습니다. Google IO 2013앱을 Volley로 포팅하는데 반일 밖에 안 걸렸습니다.스스로 개발하는 것보다 더 작은 코드로 더 많은 기능을 제공합니다.

Volley를 통해 구글에서 앱을 개발하면서 어렵게 얻은 교훈의 이득을 쉽게 얻을 수 있고, 스스로 개발하는것보다 훨씬 빠를 것입니다. Google Plus 팀이 다양한 Network 라이브러리들과 Benchmark를 했을 때 Volley가 모든 것을 다 이겼고, 매번 빨랐고, 어떤 경우에는 10배까지 빨랐습니다. ( It's easier than doing it yourself, and you get the benefit of all of those lessons we've learned the hard way in developing our apps at Google. But it's also faster than doing it in yourself. Google + Team did a bench mark last year, a bunch of networking libraries and Volley won every single one.)

질문과 답변

1. 메모리 캐쉬와 디스크 캐쉬의 크기는? 캐쉬 hit시에 디코딩은 어느 스레드에서 일어나는가?

캐쉬의 사이즈는 라이브러리 사용자가 전적으로 설정할 수 있습니다. 디스크 캐쉬와 in memory 캐쉬 크기를 따로 지정할 수 있습니다.

모든 블록킹 IO는 백그라운드 스레드에서 실행합니다. 그러나 HttpStack에 도달하기 전에 메인 Thread에서 메모리 캐쉬를 쓰는 것은 중요합니다. BitMap을 있을 때 바로 인식해서 보여줄수 있다면,지체를 하지 않는 편이 더 좋은 화면움직임(you get a little flicker.)을 보이기 때문입니다.

2. 디스크 스레드와 네트워크 스레드의 관계

디스크 캐쉬는 캐쉬 스레드에서 읽습니다. 캐쉬 스레드는 순차성이 있습니다. 따라서 어느 하나의 캐쉬를 읽고 있으면 다른 캐쉬 읽기는 묶일 수도 있습니다. 우리는 Cache worker를 쪼개거나 다른 곳으로 그 역할을 넘길지(defer the work somewhere else) 고민도 했습니다. 그러나 이미 충분히 빨랐기 때문에 그럴 필요가 없었습니다.

3. Dynamic Cache나 Static Cache, Cache의 생존기간 등 다양한 캐쉬 연결이 Volley에서 지원되느냐?

Cache TTL에 대한 질문으로 이해했습니다. Volley의 디스크 캐쉬는 Http의 Cache shutter를 존중합니다. 메모리 캐쉬에는 TTL이 없습니다. 경험상 메모리 캐쉬는 저장된 값을 빨리 버리지만, 원하는 동작을 스스로 구현할 수도 있습니다.

4. Volley와 비슷하게 해결해보려고 했는데, 작은 device에서 큰 이미지의 OOM(Out Of Memory) 예외 발생이 공통적인 문제였다. 다른 솔루션과 비교해서 Volley에서 상황이 나타나는 빈도가 어떤지나 이런 문제를 다루는 팁을 알려달라.

우리도 OOM 에러과 많이 싸웠고, 어려운 문제입니다. 메모리가 많다면 성능을 빠르게 할 수도 있습니다.

우리가 도달했고, 잘 되었던 해결책은 몇가지인데, 하나는 CPU 코어의 숫자보다 더 많이 동시에 디코딩을 하지 않은점입니다. heap pressure가 즉각적으로 줄어드는 긍정적인 효과가 나타났습니다.

또 하나 했던 일은, 캐쉬 메모리사이즈를 스크린사이즈에 따라서 지정한 것입니다. 보통은 3개의 스크린만큼의 데이터( three screees' worth of data) 를 캐쉬하기를 원하기 때문에 잘 맞아 떨어집니다. 기기들은 보통 그 기기가 가진 스크린을 채울만한 메모리를 지원합니다. 이것은 Volley에 대한 것만이 아니고, 어떻게 cache size를 선택할것인가에 대한 문제입니다.

그리고 우리는 SoftReference나 WeakRefernce를 사용하지 않습니다. 그 방법은 잘 먹히지 않았습니다. 우리는 Hard Reference를 사용했고, 과감하게 자원 할당을 했습니다. 우리는 자원량을 산정하는데 보수적인 기준을 적용했고, 스크린사이즈와 실제 픽셀의 수를 기준으로 축소해서 잡았습니다. ( And so we don't use soft references or weak references. They don't work well. We use hard references and set a strong budget. We are conservative in setting that budget by scaling down with the screen and really the number of pixels. )

5. SDK manager 안으로 언제 들어가는가? SPDY지원은?

SDK manager에 언제들어갈지는 모릅니다. clone 해서 쓰기에 충분히 쉽습니다.

SPDY에 관심이 있고, 그 지원을 염두에 두고 설계되었습니다. SPDY는 OkHttp를 통해서 사용할 수 있습니다. Volley의 tranport stack으로 OkHttp를 넣는 작업을 누군가가 진행하고 있습니다.

6. 다음 Activity로 Transition 하기 전에 API를 호출하고 싶다. Activity로 Boundary를 넘어서 데이터를 전달할 수 있는가?

좋은 대답은 아닌것 같지만 static에 저장하든가 하세요.

7. 예제가 json request와 response를 사용했다. XML같은 다른 프로토콜도 사용가능한가? 요청이 막힐 경우 3번 재시도 같은 동작을 하는가?

playstore에서는 protobuff를 주로 사용합니다. XML 도 사용할 수 있고, plugin하기도 쉽습니다. 재시도 처리가 들어가있고, custom retry policy, backoff alogorithm 정의 등 모든 것을 할 수 있습니다.

@umid-podo
Copy link

감사합니다~~ 한번 적용해 봐야겠네요~

@smaspe
Copy link

smaspe commented Mar 4, 2016

Just to let you know that I renamed and moved my blog. https://njzk2.wordpress.com/2013/06/17/an-extension-to-volley/ is now found at https://smaspe.github.io/2013/06/17/volley-ext.html (the github is also renamed, but the link is still valid)

@brewagebear
Copy link

감사합니다!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment