네트워크에 연결하기
네트워크 작업을 애플리케이션에서 수행하기 위해서는 아래의 권한들을 매니페스트에 등록해야 합니다.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
안전한 네트워크 통신 디자인하기
애플리케이션에 네트워크 기능을 추가하기 전에, 네트워크를 통해 데이터를 전송 할때, 데이터와 애플리케이션 내의 정보의 안전을 확신 해야 합니다. 그렇기 위해서는, 아래의 네트워크 보안을 위한 최고의 방법들을 따르십시오.
- 네트워크를 통해 전달하는 민감하고 개인적인 사용자 데이터를 최소화 하십시오.
- 모든 네트워크 트래픽을 SSL을 통해서 전달 하십시오.
- 네트워크 보안 구성을 만드는 것을 고려하십시오. 이것은 당신의 애플리케이션이 안전한 통신을 위해 커스텀된 CAs를 신용하거나, 시스템 CAs를 제한하는 것을 할 수 있도록 해줍니다.
HTTP 클라이언트 선택하기
대부분의 네트워크 통신 안드로이드 애플리케이션은 데이터를 송수신하기 위해서 HTTP를 사용합니다. 안드로이드 플랫폼은 HttpsURLConnection 클라이언트를 포함합니다. 이것은 TLS, 업로드 다운로드 스트리밍, 구성가능한 타임아웃, IPv6, 그리고 네트워크 풀링 (Network Pooling)들을 지원합니다.
분할 쓰레드 네트워크 작업 소개
사용자 인터페이스의 반응 없음을 만들어 내지 않기 위해, 네트워크 작업을 UI쓰레드에서 수행하지 마십시오. 기본적으로, 안드로이드 3.0( API 11)과 이상의 버전은 메인 UI 쓰레드가 아닌 별도의 쓰레드에서 네트워크 작업을 수행할 것을 요구합니다. 만약에 그렇지 않다면, NetworkOnMainThreadException이 발생하게 됩니다.
아래의 액티비티는 비동기 네트워크 작업을 캡슐화한 Fragment를 사용하는 부분을 보여주고 있습니다. 후반에 어떻게 이 과정을 NetworkFragment를 통해 완수하는 지를 볼 수 있을 겁니다. 당신의 액티비티는 DownloadCallback 인터페이스를 구현해야만합니다. 이는 네트워크 연결 상태가 필요하거나 UI에 업데이트 사항을 전달해야할 때 fragment가 액티비티로 콜백되기 위한 것입니다.
//networkFragment는 별도의 UI와 연개되어 있지 않은 Headless 프래그먼트입니다.
public class MainActivity extends FragmentActivity implements DownloadCallback {
...
//NetworkFragment로의 참조를 유지합니다. 이것은 네트워크 작업 수행에 사용될 비동기 객체를 가지고 있습니다.
private NetworkFragment mNetworkFragment;
// Boolean 값은 다운로드가 진행중인지 아닌지를 알려줍니다. 이를통해 연속적인 버튼 클릭으로 다운로드가 중복되는 것을 막습니다.
private boolean mDownloading = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
mNetworkFragment = NetworkFragment.getInstance(getSupportFragmentManager(), "https://www.google.com");
}
private void startDownload() {
if (!mDownloading && mNetworkFragment != null) {
// Execute the async download.
mNetworkFragment.startDownload();
mDownloading = true;
}
}
}
최소한, DownloadCallbak 인터페이스는 아래 정도의 내용으로 구성될 수 있습니다.
public interface DownloadCallback<T> {
interface Progress {
int ERROR = -1;
int CONNECT_SUCCESS = 0;
int GET_INPUT_STREAM_SUCCESS = 1;
int PROCESS_INPUT_STREAM_IN_PROGRESS = 2;
int PROCESS_INPUT_STREAM_SUCCESS = 3;
}
/**
* 콜백 자체ㅇ를 업데이트하거나, 작업 결과에 근간한 정보를 전달합니다. 메인 쓰레드에서 불려질 것으로 예상됩니다.
*/
void updateFromDownload(T result);
/**
* NetworkInfo 형태로 활성화된 네트워크의 상태를 얻을 수 있습니다.
*/
NetworkInfo getActiveNetworkInfo();
/**
* 진행 과정을 콜백 핸들러에게 알려줍니다.
* @param progressCode는 반드시 DownloadCallback에서 정의된 상수들 중 하나입니다.
* @param percentComplete는 반드시 0-100의 수입니다.
*/
void onProgressUpdate(int progressCode, int percentComplete);
/**
* 다운로드 작업이 완료됨을 알립니다.
* 이 메소드는 비록 다운로드가 성공적으로 끝나지 않았더라도 호출됩니다.
*/
void finishDownloading();
}
구현된 DownLoadCallback 인터페이스 메소드를 액티비티에 추가합니다.
@Override
public void updateFromDownload(String result) {
// 다운로드 결과에 맞춰 UI를 여기서 업데이트합니다.
}
@Override
public NetworkInfo getActiveNetworkInfo() {
ConnectivityManager connectivityManager =
(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo;
}
@Override
public void onProgressUpdate(int progressCode, int percentComplete) {
switch(progressCode) {
// 진행상태에 맞춘 UI 행위 여기서 추가할 수 있습니다.
case Progress.ERROR:
...
break;
case Progress.CONNECT_SUCCESS:
...
break;
case Progress.GET_INPUT_STREAM_SUCCESS:
...
break;
case Progress.PROCESS_INPUT_STREAM_IN_PROGRESS:
...
break;
case Progress.PROCESS_INPUT_STREAM_SUCCESS:
...
break;
}
}
@Override
public void finishDownloading() {
mDownloading = false;
if (mNetworkFragment != null) {
mNetworkFragment.cancelDownload();
}
}
네트워크 작업 캡슐화를 위한 UI가 없는(Headless) 프래그먼트 구현하기
NetworkFragment는 기본적으로 UI 쓰레드에서 동작하기에, 비동기 작업을 통해 백그라운드 쓰레드에서 네트워크 작업을 진행합니다. 해당 프래그먼트는 '머리 없는' 프래그먼트로 얘기되는데 이유는 어떠한 UI 요소도 참조하지 않기 때문입니다. 대신에 액티비티가 UI를 업데이트 하게 하며, 오로지 로직을 캡슐화하거나 생명주기 이벤트를 다루는 용도로 사용됩니다.
네트워크 작업을 수행하기 위해, 비동기 작업의 하위 클래스를 사용할 때, 비동기 작업이 참조하는 액티비티가 비동기 작업이 완료되기 전에 파괴됨으로써 생기는 메모리 누수를 만들지 않도록 주의해야합니다. 이러한 일이 발생하는 것을 막기 위해서는, 액티비티로의 모든 참조들을 프래그먼트가 OnDetach()가 선언될 때 정리하는 과정이 필요합니다. 아래의 예제를 확인하십시오.
/**
* 네트워크로 데이터로 받아오는 비동기 작업을 수행하는 헤드리스 프래그먼트를 구현합니다.
*/
public class NetworkFragment extends Fragment {
public static final String TAG = "NetworkFragment";
private static final String URL_KEY = "UrlKey";
private DownloadCallback mCallback;
private DownloadTask mDownloadTask;
private String mUrlString;
/**
* 다운로드 해올 자료의 출저인 호스트의 URL을 설정한 NetworkFragment를 초기화하는 스태틱 생성자입니다.
*/
public static NetworkFragment getInstance(FragmentManager fragmentManager, String url) {
NetworkFragment networkFragment = new NetworkFragment();
Bundle args = new Bundle();
args.putString(URL_KEY, url);
networkFragment.setArguments(args);
fragmentManager.beginTransaction().add(networkFragment, TAG).commit();
return networkFragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mUrlString = getArguments().getString(URL_KEY);
...
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
// 호스트 액티비티가 콜백을 처리합니다.
mCallback = (DownloadCallback) context;
}
@Override
public void onDetach() {
super.onDetach();
// 메모리 누수를 방지를 위해서 참조를 해제합니다.
mCallback = null;
}
@Override
public void onDestroy() {
// 프래그먼트가 파괴될 때 동작을 해제합니다.
cancelDownload();
super.onDestroy();
}
/**
* 논 블로킹(UI를 막지 않는) 다운로드 작업을 시작합니다.
*/
public void startDownload() {
cancelDownload();
mDownloadTask = new DownloadTask();
mDownloadTask.execute(mUrlString);
}
/**
* 진행 중인 다운로드를 취소합니다.
*/
public void cancelDownload() {
if (mDownloadTask != null) {
mDownloadTask.cancel(true);
}
}
...
}
비동기 작업의 상속 클래스를 프래그먼트 내에 프라이빗 클래스로 구현합니다.
/**
* 네트워크에서 데이터를 가져올 비동기 작업을 구현합니다.
*/
private class DownloadTask extends AsyncTask<String, Void, DownloadTask.Result> {
private DownloadCallback<String> mCallback;
DownloadTask(DownloadCallback<String> callback) {
setCallback(callback);
}
void setCallback(DownloadCallback<String> callback) {
mCallback = callback;
}
/**
* 작업 결과와 예외의 집합체인 래퍼 클래스 (wrapper class)입니다. 다운로드 작업이 완료되면, 결과 값 또는 예외사항이 null이
* 아닌 값을 가지게 됩니다. 이를 통해 doInBackground()실행 중에 생겨나는 예외사항을 UI쓰레드에 넘길 수 있습니다.
*/
static class Result {
public String mResultValue;
public Exception mException;
public Result(String resultValue) {
mResultValue = resultValue;
}
public Result(Exception exception) {
mException = exception;
}
}
/**
* 네트워크 연결이 없을 경우에 백그라운드 네트워크 작업을 취소합니다.
*/
@Override
protected void onPreExecute() {
if (mCallback != null) {
NetworkInfo networkInfo = mCallback.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnected() ||
(networkInfo.getType() != ConnectivityManager.TYPE_WIFI
&& networkInfo.getType() != ConnectivityManager.TYPE_MOBILE)) {
// 연결이 되어있지 않을 경우, 작업을 멈추고 콜백으로 null 데이터를 업데이트 합니다.
mCallback.updateFromDownload(null);
cancel(true);
}
}
}
/**
* 백그라운드 쓰레드에서 처리할 작업을 정의합니다.
*/
@Override
protected DownloadTask.Result doInBackground(String... urls) {
Result result = null;
if (!isCancelled() && urls != null && urls.length > 0) {
String urlString = urls[0];
try {
URL url = new URL(urlString);
String resultString = downloadUrl(url);
if (resultString != null) {
result = new Result(resultString);
} else {
throw new IOException("No response received.");
}
} catch(Exception e) {
result = new Result(e);
}
}
return result;
}
/**
* 결과값을 DownloadCallback으로 업데이트합니다.
*/
@Override
protected void onPostExecute(Result result) {
if (result != null && mCallback != null) {
if (result.mException != null) {
mCallback.updateFromDownload(result.mException.getMessage());
} else if (result.mResultValue != null) {
mCallback.updateFromDownload(result.mResultValue);
}
mCallback.finishDownloading();
}
}
/**
* 비동기 작업이 취소될 때 할 작업을 오버라이드하여 추가합니다.
*/
@Override
protected void onCancelled(Result result) {
}
...
}
// 비동기 작업 문서 정리
쓰레드 또는 핸들러를 처리하는 것 없이, 백그라운드 쓰레드에서 작업을 하며 결과 값을 UI 쓰레드에 전달할 수 있도록 해주는 클래스입니다.
비동기 작업은 쓰레드와 핸들러의 헬퍼 클래스로 디자인되어 있으며, 어떠한 일반적인 쓰레딩 프레임 워크를 구성하지는 않습니다. 비동기적인 작업은 백그라운드 쓰레드에서 처리되는 계산으로서 정의되어 있으며, 해당 결과는 UI 쓰레드로 전달됩니다. 비동기적 작업은 3개의 제네릭 타입들과 (params, progress, result) 4개의 과정 (onPreExecute, doInBackground, onProgressUpdate, onPostExecute)으로 구현됩니다.
제네릭 타입
- Params : 작업 실행히 전달되는 매개변수의 타입
- Progress 백그라운드 연산작업 동안 표현되는 진행 단위
- Result 백그라운드 연산의 결과 타입
4 과정
비동기 작업이 실행될 때, 4개의 과정을 거칩니다.
- onPreExecute()), 작업이 실행되기 전에 UI 쓰레드에서 발생하는 메소드입니다. 이 과정은 일반적으로 작업을 설정합니다. 예를 들면 사용자 인터페이스에 프로그레스 바를 보여주는 것이 있습니다.
- doInBackground(Params...)) onPreExecute())가 실행된 직후 백그라운드 쓰레드에서 실행됩니다. 이 과정은 긴 시간이 필요한 백그라운드 연산작업을 수행하기 위해 사용됩니다. 계산의 결과값은 이 과정을 통해 반횐되며, 결과 값은 마지막 과정으로 전달됩니다. 이 과정은 또한 publishProgress(Progress...))를 하나 또는 그 이상의 진행 단위를 알리기위해 사용합니다. 알려진 값들은 UI쓰레드에 onProgressUpdate(Progress...))에서 전달됩니다.
- onProgressUpdate(Progress...)) publishProgress(Progress...))다음에 UI 쓰레드에서 호출됩니다. 언제 실행되는 지는 정해져 있지않습니다. 이 메소드는 어떠한 형태의 진행과정이 백그라운드 연산이 진행되는 동안 사용자 인터페이스에 표시되기 위해 사용됩니다. 예를 들면, 이것으로 프로그래스 바의 애니메이션을 사용하거나, 텍스트 영역에 로그를 보여줄 수 있습니다.
- onPostExecute(Result)) 백그라운드 연산작업이 완료되면 UI 쓰레드에서 호출됩니다. 이 백그라운드 연산작업의 결과값이 매개변수로 이 메소드에 전달됩니다.
//
HttpUrlConnection 사용해 데이터 가져오기
위의 예제에서 나타나듯, doInBackground()) 메소드는 백그라운드 쓰레드에서 실행됩니다. 그리고 핼퍼 메소드인 downloadUrl()을 호출합니다. 이 downloadUrl()메소드는 URL을 받아와 HTTP의 GET 요청을 실행하는데 사용합니다. 한 번 연결이 이루어지면, getInputStream())메소드를 InputStream으로 데이터를 가져오기 위해 사용해야합니다. HttpsURLConnection API 를 사용한 예제입니다.
/**
* 주어진 URL을 사용해 연결을 설정합니다. 그리고 HTTP 응답 바디를 서버로부터 받아옵니다.
* 만약 네트워크 요청이 성공적이라면, 문자열 형태의 응답 바디를 반환할 것입니다. 그렇지 않다면 IOException이 발생합니다.
*/
private String downloadUrl(URL url) throws IOException {
InputStream stream = null;
HttpsURLConnection connection = null;
String result = null;
try {
connection = (HttpsURLConnection) url.openConnection();
// 임의로 3000ms의 인풋 스트림의 한계시간을 설정했습니다.
connection.setReadTimeout(3000);
// connection.connect()의 한계시간 역시 임의로 3000ms를 설정했습니다.
connection.setConnectTimeout(3000);
// 이 경우 HTTP 메소드의 요청은 GET으로 설정합니다.
connection.setRequestMethod("GET");
// 이미 기본 값으로 참의 값이 설정되어있으나 확실히하기 위해, 참으로 설정을 해놓습니다.
// 이는 인풋 계열의 바디를 요청이 수반하고 있음을 의미합니다.
connection.setDoInput(true);
// 통신 링크를 엽니다(여기서 네트워크 트래픽이 발생합니다.)
connection.connect();
publishProgress(DownloadCallback.Progress.CONNECT_SUCCESS);
int responseCode = connection.getResponseCode();
if (responseCode != HttpsURLConnection.HTTP_OK) {
throw new IOException("HTTP error code: " + responseCode);
}
// 응답 바디를 인풋스트림으로 받아옵니다.
stream = connection.getInputStream();
publishProgress(DownloadCallback.Progress.GET_INPUT_STREAM_SUCCESS, 0);
if (stream != null) {
// 스트림을 최대 500자의 문자열로 변환합니다.
result = readStream(stream, 500);
}
} finally {
// HTTPS 연결을 끊고 스트림을 닫습니다.
if (stream != null) {
stream.close();
}
if (connection != null) {
connection.disconnect();
}
}
return result;
}
getResponseCode()메소드가 연결 상태 코드(status code)를 반환합니다. 이는 네트워크 연결과 관련된 추가적인 정보를 얻을 쓸만한 방법입니다. 200의 상태코드는 성공을 의미합니다.
문자열로 인풋스트림 변환하기
인풋스트림은 이미 사용가능한 바이트로 구성된 데이터입니다. 한 번 인풋스트림을 받아오면, 이것을 원하는 데이터 형식으로 변환하거나 디코드 하는 것이 일반적입니다. 예를 들면, 이미지 데이터를 받아왔다면, 아래와 같은 방식으로 디코딩하여 화면에 표시합니다.
InputStream is = null;
...
Bitmap bitmap = BitmapFactory.decodeStream(is);
ImageView imageView = (ImageView) findViewById(R.id.image_view);
imageView.setImageBitmap(bitmap);
위의 예제에서 보여지 듯, 인풋스트림은 응답 바디의 택스트를 의미합니다. 아래의 예는 어떻게 인풋스트림을 문자열로 변경하고 액티비티의 UI에 표시하는지 보여줍니다.
/**
* 이눗스트림 스트링으로 변환하기
*/
private String readStream(InputStream stream, int maxLength) throws IOException {
String result = null;
// UTF-8 문자 세트로 인풋스트림을 읽습니다.
InputStreamReader reader = new InputStreamReader(stream, "UTF-8");
// 스트림 데이터를 담아둘 임시 버퍼를 형성합니다. 임의의 최대길이를 함게 정해둡니다.
char[] buffer = new char[maxLength];
// 임시 버퍼를 스트림 데이터로 채웁니다.
int numChars = 0;
int readSize = 0;
while (numChars < maxLength && readSize != -1) {
numChars += readSize;
int pct = (100 * numChars) / maxLength;
publishProgress(DownloadCallback.Progress.PROCESS_INPUT_STREAM_IN_PROGRESS, pct);
readSize = reader.read(buffer, numChars, buffer.length - numChars);
}
if (numChars != -1) {
// 버퍼의 최대길이보다 응답 바디의 길이가 짧다면 해당 길이를 이용해 문자열을 만듭니다.
numChars = Math.min(numChars, maxLength);
result = new String(buffer, 0, numChars);
}
return result;
}
현재까지의 과정은 다음과 같은 순서로 이루어져 있습니다.
- 액티비티가 NetworkFragment를 실행하며 특정 URL을 전달합니다.
- 사용자가 액티비티의 downloaddata()를 실행하면, NetworkFragment가 DownloadTask를 실행합니다.
- 비동기 작업의 onPreExectue()메소드가 실행됩니다.
- 비동기 작업의 doInBackground()메소드가 다음으로 백그라운드 쓰레드에서 실행됩니다. 그리고 downloadUrl()을 호출합니다.
- downloadUrl()메소드가 URL 문자열을 매개변수로 받습니다. 그리고 해당 URL을 HttpsURLConnetion 객체에서 웹 콘텐츠를 인풋스트림으로 받아오기 위해 사용합니다.
- 인풋스트림은 readStream()메소드로 전달되며 여기서 스트림은 스트링으로 변환됩니다.
- 마지막으로 백그라운드 작업이 오나료되면, 비동기작업의 onPostExecute()메소드가 UI쓰레드에서 실행되며 DownloadCallbck을 사용합니다. 문자열로 결과값이 UI에 전달됩니다.
구성 변경에서 살아남기
지금까지, 성공적으로 액티비티에 네트워크 작업을 구현했습니다. 그러나 만약에 사용자가 doinBackground()가 백그라운드 쓰레드에서 실행되는 동안 기기의 구성(예를 들면 화면을 90도 회전하는)을 바꾸기로한다면, 그로인해 액티비티가 파괴되고 스스로 재 생성한다면, onCreate()가 재호출되며 그에따라 새로운 NetworkFragment을 참조하게 됩니다. 자세한 사항은 런타임 변경처리를 확인해 주십시오.)그러면 기존의 NetworkFragment에 존재하는 비동기 작업은 DownloadCallback을 기존의 파괴된 액티비티의 참조에 따라 호출하며 이는 UI 쓰레드에 더이상 반영되지 않습니다. 그리고 해당 작업은 쓸모없어지게 됩니다.
이러한 구성 변경에 대항하기 위해서, 기존의 프래그먼트를 유지하고 새로 생성된 액티비티로 참조를 재구성해야합니다. 이를 위해 아래의 수정사항을 코드에 추가해야합니다.
먼저 NetworkFragment는 setRetainInstance(true)) onCreate()에서 호출해야합니다.
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
...
// 호스트 액티비티에서의 구성변화에도 프래그먼트가 유지되도록 합니다.
setRetainInstance(true);
}
그리고 스태틱 getInstance()메소드에서 NetworkFragment에서 인스턴스를 초기화 하는 방법을 수정해야합니다.
public static NetworkFragment getInstance(FragmentManager fragmentManager, String url) {
// 액티비티가 구성변경으로 인해 재생성 될 때 NetworkFragment가 복구되도록합니다.
// 이러한 과정은 구성변경이 이뤄지기 전에 실행되어 아직 끝나지 않은 작업을 가지고 있는 NetworkFragment가 있을 수 있음으로 필수적입니다.
// 해당 NetworkFragment는 setRetainInstance(true)메소드 덕분에 복구 가능합니다.
NetworkFragment networkFragment = (NetworkFragment) fragmentManager
.findFragmentByTag(NetworkFragment.TAG);
if (networkFragment == null) {
networkFragment = new NetworkFragment();
Bundle args = new Bundle();
args.putString(URL_KEY, url);
networkFragment.setArguments(args);
fragmentManager.beginTransaction().add(networkFragment, TAG).commit();
}
return networkFragment;
}
이제 성공적으로 인터넷에서 데이터를 받아올 수 있습니다
몇가지 다른 백그라운드 쓰레드 관리 도구들이 있음을 알고 계십시오 그것들은 동일한 목적을 달성하는데 도움을 줄 수 있습니다. 애플리케이션이 더욱 복잡해짐에 따라 다른 도구들이 더욱 적합하게 느껴질 수 있습니다. 비동기 작업 대신에 IntentService나 AsynchTaskLoader를 조사해볼만 합니다.