네트워크에 연결

애플리케이션에서 네트워크 작업을 수행하려면 매니페스트에 다음 권한이 포함되어야 합니다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

안전한 네트워크 통신 설계

앱에 네트워킹 기능을 추가하기 전에 앱의 데이터와 정보를 네트워크로 전송할 때 안전하게 유지되도록 해야 합니다. 이를 위해서는 다음과 같은 네트워킹 보안 모범 사례를 따르세요.

  • 네트워크에서 전송하는 민감하거나 개인적인 사용자 데이터를 최소화합니다.
  • 앱의 모든 네트워크 트래픽을 SSL로 전송합니다.
  • 네트워크 보안 구성을 생성하는 것을 고려하세요. 이렇게 하면 앱에서 맞춤 CA를 신뢰하거나 앱이 보안 통신에 대해 신뢰하는 시스템 CA 세트를 제한할 수 있습니다.

안전한 네트워킹 원칙 적용에 대한 자세한 내용은 Android의 네트워킹 보안 팁을 참조하세요. Android NetworkConnect 샘플을 참조할 수도 있습니다.

HTTP 클라이언트 선택

네트워크에 연결된 Android 앱 대부분은 HTTP를 사용하여 데이터를 송수신합니다. Android 플랫폼에는 HttpsURLConnection 클라이언트가 포함되는데, 이는 TLS, 스트리밍 업로드 및 다운로드, 구성 가능한 시간 초과, IPv6, 연결 풀링을 지원합니다.

별도의 스레드에 네트워크 작업 도입

반응하지 않는 UI가 생성되는 것을 방지하려면 UI 스레드에서 네트워크 작업을 수행해서는 안 됩니다. 기본적으로 Android 3.0(API 레벨 11) 이상에서는 기본 UI 스레드를 제외한 스레드에서 네트워크 작업을 수행해야 합니다. 그렇지 않으면 NetworkOnMainThreadException이 발생합니다.

다음 Activity 스니펫은 헤드가 없는 Fragment를 사용하여 비동기식 네트워크 작업을 캡슐화합니다. 나중에 Fragment 구현인 NetworkFragment가 이를 캡슐화하는 방법을 확인할 수 있습니다. Activity는 DownloadCallback 인터페이스도 구현해야 하며, 연결 상태가 필요하거나 UI로 업데이트를 다시 전송해야 할 경우에 대비하여 Fragment가 Activity로 콜백되도록 합니다.

Kotlin

class MainActivity : FragmentActivity(), DownloadCallback<String> {

    ...

    // Keep a reference to the NetworkFragment, which owns the AsyncTask object
    // that is used to execute network ops.
    private var networkFragment: NetworkFragment? = null

    // Boolean telling us whether a download is in progress, so we don't trigger overlapping
    // downloads with consecutive button clicks.
    private var downloading = false

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        networkFragment = NetworkFragment.getInstance(supportFragmentManager, "https://www.google.com")
    }

    private fun startDownload() {
        if (!downloading) {
            // Execute the async download.
            networkFragment?.apply {
                startDownload()
                downloading = true
            }
        }
    }
}

Java

public class MainActivity extends FragmentActivity implements DownloadCallback {

    ...

    // Keep a reference to the NetworkFragment, which owns the AsyncTask object
    // that is used to execute network ops.
    private NetworkFragment networkFragment;

    // Boolean telling us whether a download is in progress, so we don't trigger overlapping
    // downloads with consecutive button clicks.
    private boolean downloading = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        networkFragment = NetworkFragment.getInstance(getSupportFragmentManager(), "https://www.google.com");
    }

    private void startDownload() {
        if (!downloading && networkFragment != null) {
            // Execute the async download.
            networkFragment.startDownload();
            downloading = true;
        }
    }
}

DownloadCallback 인터페이스는 최소한 다음과 같이 구성할 수 있습니다.

Kotlin

const val ERROR = -1
const val CONNECT_SUCCESS = 0
const val GET_INPUT_STREAM_SUCCESS = 1
const val PROCESS_INPUT_STREAM_IN_PROGRESS = 2
const val PROCESS_INPUT_STREAM_SUCCESS = 3

interface DownloadCallback<T> {

    /**
     * Indicates that the callback handler needs to update its appearance or information based on
     * the result of the task. Expected to be called from the main thread.
     */
    fun updateFromDownload(result: T?)

    /**
     * Get the device's active network status in the form of a NetworkInfo object.
     */
    fun getActiveNetworkInfo(): NetworkInfo

    /**
     * Indicate to callback handler any progress update.
     * @param progressCode must be one of the constants defined in DownloadCallback.Progress.
     * @param percentComplete must be 0-100.
     */
    fun onProgressUpdate(progressCode: Int, percentComplete: Int)

    /**
     * Indicates that the download operation has finished. This method is called even if the
     * download hasn't completed successfully.
     */
    fun finishDownloading()
}

Java

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;
    }

    /**
     * Indicates that the callback handler needs to update its appearance or information based on
     * the result of the task. Expected to be called from the main thread.
     */
    void updateFromDownload(T result);

    /**
     * Get the device's active network status in the form of a NetworkInfo object.
     */
    NetworkInfo getActiveNetworkInfo();

    /**
     * Indicate to callback handler any progress update.
     * @param progressCode must be one of the constants defined in DownloadCallback.Progress.
     * @param percentComplete must be 0-100.
     */
    void onProgressUpdate(int progressCode, int percentComplete);

    /**
     * Indicates that the download operation has finished. This method is called even if the
     * download hasn't completed successfully.
     */
    void finishDownloading();
}

이제 DownloadCallback 인터페이스 메서드의 다음과 같은 구현을 Activity에 추가합니다.

Kotlin

override fun updateFromDownload(result: String?) {
    // Update your UI here based on result of download.
}

override fun getActiveNetworkInfo(): NetworkInfo {
    val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    return connectivityManager.activeNetworkInfo
}

override fun onProgressUpdate(progressCode: Int, percentComplete: Int) {
    when (progressCode) {
    // You can add UI behavior for progress updates here.
        ERROR -> {
        }
        CONNECT_SUCCESS -> {
        }
        GET_INPUT_STREAM_SUCCESS -> {
        }
        PROCESS_INPUT_STREAM_IN_PROGRESS -> {
        }
        PROCESS_INPUT_STREAM_SUCCESS -> {
        }
    }
}

override fun finishDownloading() {
    downloading = false
    networkFragment?.cancelDownload()
}

Java

@Override
public void updateFromDownload(String result) {
    // Update your UI here based on result of download.
}

@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) {
        // You can add UI behavior for progress updates here.
        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() {
    downloading = false;
    if (networkFragment != null) {
        networkFragment.cancelDownload();
    }
}

헤드가 없는 Fragment를 구현하여 네트워크 작업 캡슐화

기본적으로 NetworkFragment는 UI에서 실행되므로 AsyncTask를 사용하여 백그라운드 스레드에서 네트워크 작업을 실행합니다. 이 Fragment는 UI 요소를 참조하지 않으므로 헤드가 없는 것으로 간주합니다. 대신 로직과 핸들 수명 주기 이벤트를 캡슐화할 때만 사용하여 호스트 Activity가 UI를 업데이트하도록 합니다.

AsyncTask의 하위 클래스를 사용하여 네트워크 작업을 실행할 때는 AsyncTask가 백그라운드 작업을 완료하기 전에 AsyncTask가 참조한 Activity가 소멸된 경우 메모리 누수가 발생하지 않도록 주의해야 합니다. 이런 일이 발생하지 않도록 다음 스니펫은 Fragment의 onDetach() 메서드에서 Activity에 대한 참조를 제거합니다.

Kotlin

private const val TAG = "NetworkFragment"
private const val URL_KEY = "UrlKey"

class NetworkFragment : Fragment() {
    private var mCallback: DownloadCallback<String>? = null
    private var downloadTask: DownloadTask? = null
    private var urlString: String? = null

    companion object {
        /**
         * Static initializer for NetworkFragment that sets the URL of the host it will be
         * downloading from.
         */
        fun getInstance(fragmentManager: FragmentManager, url: String): NetworkFragment {
            val networkFragment = NetworkFragment()
            val args = Bundle()
            args.putString(URL_KEY, url)
            networkFragment.arguments = args
            fragmentManager.beginTransaction().add(networkFragment, TAG).commit()
            return networkFragment
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        urlString = arguments?.getString(URL_KEY)
        ...
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        // Host Activity will handle callbacks from task.
        mCallback = context as? DownloadCallback<String>
    }

    override fun onDetach() {
        super.onDetach()
        // Clear reference to host Activity to avoid memory leak.
        mCallback = null
    }

    override fun onDestroy() {
        // Cancel task when Fragment is destroyed.
        cancelDownload()
        super.onDestroy()
    }

    /**
     * Start non-blocking execution of DownloadTask.
     */
    fun startDownload() {
        cancelDownload()
        mCallback?.also { callback ->
            downloadTask = DownloadTask(callback).apply {
                execute(urlString)
            }
        }
    }

    /**
     * Cancel (and interrupt if necessary) any ongoing DownloadTask execution.
     */
    fun cancelDownload() {
        downloadTask?.cancel(true)
    }

    ...
}

Java

/**
 * Implementation of headless Fragment that runs an AsyncTask to fetch data from the network.
 */
public class NetworkFragment extends Fragment {
    public static final String TAG = "NetworkFragment";

    private static final String URL_KEY = "UrlKey";

    private DownloadCallback<String> mCallback;
    private DownloadTask downloadTask;
    private String urlString;

    /**
     * Static initializer for NetworkFragment that sets the URL of the host it will be downloading
     * from.
     */
    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);
        urlString = getArguments().getString(URL_KEY);
        ...
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        // Host Activity will handle callbacks from task.
        mCallback = (DownloadCallback<String>) context;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        // Clear reference to host Activity to avoid memory leak.
        mCallback = null;
    }

    @Override
    public void onDestroy() {
        // Cancel task when Fragment is destroyed.
        cancelDownload();
        super.onDestroy();
    }

    /**
     * Start non-blocking execution of DownloadTask.
     */
    public void startDownload() {
        cancelDownload();
        downloadTask = new DownloadTask(mCallback);
        downloadTask.execute(urlString);
    }

    /**
     * Cancel (and interrupt if necessary) any ongoing DownloadTask execution.
     */
    public void cancelDownload() {
        if (downloadTask != null) {
            downloadTask.cancel(true);
        }
    }

    ...
}

이제 AsyncTask의 하위 클래스를 Fragment 내의 비공개 내부 클래스로 구현해야 합니다.

Kotlin

/**
 * Implementation of AsyncTask designed to fetch data from the network.
 */
private class DownloadTask(callback: DownloadCallback<String>)
    : AsyncTask<String, Int, DownloadTask.Result>() {

    private var mCallback: DownloadCallback<String>? = null

    init {
        setCallback(callback)
    }

    internal fun setCallback(callback: DownloadCallback<String>) {
        mCallback = callback
    }

    /**
     * Wrapper class that serves as a union of a result value and an exception. When the download
     * task has completed, either the result value or exception can be a non-null value.
     * This allows you to pass exceptions to the UI thread that were thrown during doInBackground().
     */
    internal class Result {
        var mResultValue: String? = null
        var mException: Exception? = null

        constructor(resultValue: String) {
            mResultValue = resultValue
        }

        constructor(exception: Exception) {
            mException = exception
        }
    }

    /**
     * Cancel background network operation if we do not have network connectivity.
     */
    override fun onPreExecute() {
        if (mCallback != null) {
            val networkInfo = mCallback?.getActiveNetworkInfo()
            if (networkInfo?.isConnected == false
                    || networkInfo?.type != ConnectivityManager.TYPE_WIFI
                    && networkInfo?.type != ConnectivityManager.TYPE_MOBILE) {
                // If no connectivity, cancel task and update Callback with null data.
                mCallback?.updateFromDownload(null)
                cancel(true)
            }
        }
    }

    /**
     * Defines work to perform on the background thread.
     */
    override fun doInBackground(vararg urls: String): DownloadTask.Result? {
        var result: Result? = null
        if (!isCancelled && urls.isNotEmpty()) {
            val urlString = urls[0]
            result = try {
                val url = URL(urlString)
                val resultString = downloadUrl(url)
                if (resultString != null) {
                    Result(resultString)
                } else {
                    throw IOException("No response received.")
                }
            } catch (e: Exception) {
                Result(e)
            }

        }
        return result
    }

    /**
     * Updates the DownloadCallback with the result.
     */
    override fun onPostExecute(result: Result?) {
        mCallback?.apply {
            result?.mException?.also { exception ->
                updateFromDownload(exception.message)
                return
            }
            result?.mResultValue?.also { resultValue ->
                updateFromDownload(resultValue)
                return
            }
            finishDownloading()
        }
    }

    /**
     * Override to add special behavior for cancelled AsyncTask.
     */
    override fun onCancelled(result: Result) {}
}

Java

/**
 * Implementation of AsyncTask designed to fetch data from the network.
 */
private class DownloadTask extends AsyncTask<String, Integer, DownloadTask.Result> {

    private DownloadCallback<String> mCallback;

    DownloadTask(DownloadCallback<String> callback) {
        setCallback(callback);
    }

    void setCallback(DownloadCallback<String> callback) {
        mCallback = callback;
    }

     /**
     * Wrapper class that serves as a union of a result value and an exception. When the download
     * task has completed, either the result value or exception can be a non-null value.
     * This allows you to pass exceptions to the UI thread that were thrown during doInBackground().
     */
    static class Result {
        public String mResultValue;
        public Exception mException;
        public Result(String resultValue) {
            mResultValue = resultValue;
        }
        public Result(Exception exception) {
            mException = exception;
        }
    }

    /**
     * Cancel background network operation if we do not have network connectivity.
     */
    @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)) {
                // If no connectivity, cancel task and update Callback with null data.
                mCallback.updateFromDownload(null);
                cancel(true);
            }
        }
    }

    /**
     * Defines work to perform on the background thread.
     */
    @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;
    }

    /**
     * Updates the DownloadCallback with the result.
     */
    @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 to add special behavior for cancelled AsyncTask.
     */
    @Override
    protected void onCancelled(Result result) {
    }
}

HttpsUrlConnection으로 데이터 가져오기

위의 스니펫에서 doInBackground() 메서드는 백그라운드 스레드에서 실행되고 도우미 메서드인 downloadUrl()을 호출합니다. downloadUrl() 메서드는 지정된 URL을 가져와 HTTP GET 요청을 수행하는 데 사용해야 합니다. 연결이 설정되었으면 getInputStream() 메서드를 사용하여 데이터를 InputStream으로 검색해야 합니다. 다음 스니펫은 HttpsURLConnection API로 다음과 같은 결과를 얻습니다.

Kotlin

/**
 * Given a URL, sets up a connection and gets the HTTP response body from the server.
 * If the network request is successful, it returns the response body in String form. Otherwise,
 * it will throw an IOException.
 */
@Throws(IOException::class)
private fun downloadUrl(url: URL): String? {
    var connection: HttpsURLConnection? = null
    return try {
        connection = (url.openConnection() as? HttpsURLConnection)
        connection?.run {
            // Timeout for reading InputStream arbitrarily set to 3000ms.
            readTimeout = 3000
            // Timeout for connection.connect() arbitrarily set to 3000ms.
            connectTimeout = 3000
            // For this use case, set HTTP method to GET.
            requestMethod = "GET"
            // Already true by default but setting just in case; needs to be true since this request
            // is carrying an input (response) body.
            doInput = true
            // Open communications link (network traffic occurs here).
            connect()
            publishProgress(CONNECT_SUCCESS)
            if (responseCode != HttpsURLConnection.HTTP_OK) {
                throw IOException("HTTP error code: $responseCode")
            }
            // Retrieve the response body as an InputStream.
            publishProgress(GET_INPUT_STREAM_SUCCESS, 0)
            inputStream?.let { stream ->
                // Converts Stream to String with max length of 500.
                readStream(stream, 500)
            }
        }
    } finally {
        // Close Stream and disconnect HTTPS connection.
        connection?.inputStream?.close()
        connection?.disconnect()
    }
}

Java

/**
 * Given a URL, sets up a connection and gets the HTTP response body from the server.
 * If the network request is successful, it returns the response body in String form. Otherwise,
 * it will throw an IOException.
 */
private String downloadUrl(URL url) throws IOException {
    InputStream stream = null;
    HttpsURLConnection connection = null;
    String result = null;
    try {
        connection = (HttpsURLConnection) url.openConnection();
        // Timeout for reading InputStream arbitrarily set to 3000ms.
        connection.setReadTimeout(3000);
        // Timeout for connection.connect() arbitrarily set to 3000ms.
        connection.setConnectTimeout(3000);
        // For this use case, set HTTP method to GET.
        connection.setRequestMethod("GET");
        // Already true by default but setting just in case; needs to be true since this request
        // is carrying an input (response) body.
        connection.setDoInput(true);
        // Open communications link (network traffic occurs here).
        connection.connect();
        publishProgress(DownloadCallback.Progress.CONNECT_SUCCESS);
        int responseCode = connection.getResponseCode();
        if (responseCode != HttpsURLConnection.HTTP_OK) {
            throw new IOException("HTTP error code: " + responseCode);
        }
        // Retrieve the response body as an InputStream.
        stream = connection.getInputStream();
        publishProgress(DownloadCallback.Progress.GET_INPUT_STREAM_SUCCESS, 0);
        if (stream != null) {
            // Converts Stream to String with max length of 500.
            result = readStream(stream, 500);
        }
    } finally {
        // Close Stream and disconnect HTTPS connection.
        if (stream != null) {
            stream.close();
        }
        if (connection != null) {
            connection.disconnect();
        }
    }
    return result;
}

참고로 getResponseCode() 메서드는 연결의 상태 코드를 반환합니다. 이 방법은 연결에 대한 자세한 정보를 얻을 때 유용합니다. 200 상태 코드는 성공을 나타냅니다.

HttpsURLConnection을 더 자세히 살펴보고 싶다면 Android NetworkConnect 샘플을 참조하세요.

InputStream을 문자열로 변환

InputStream은 바이트의 판독 가능한 소스입니다. InputStream을 가져오고 나면 디코딩하거나 대상 데이터 유형으로 변환하는 것이 일반적입니다. 예를 들어 이미지 데이터를 다운로드하고 있다면 다음과 같이 디코딩하고 표시할 수 있습니다.

Kotlin

val inputStream: InputStream? = null
...
val bitmap: Bitmap = BitmapFactory.decodeStream(inputStream)
findViewById<ImageView>(R.id.image_view)?.apply {
    setImageBitmap(bitmap)
}

Java

InputStream inputStream = null;
...
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
ImageView imageView = (ImageView) findViewById(R.id.image_view);
imageView.setImageBitmap(bitmap);

위의 예시에서 InputStream은 응답 본문의 텍스트를 나타냅니다. 이런 방식으로 InputStream을 문자열로 변환해야 Activity가 UI에서 이를 표시할 수 있습니다.

Kotlin

/**
 * Converts the contents of an InputStream to a String.
 */
@Throws(IOException::class, UnsupportedEncodingException::class)
fun readStream(stream: InputStream, maxReadSize: Int): String? {
    val reader: Reader? = InputStreamReader(stream, "UTF-8")
    val rawBuffer = CharArray(maxReadSize)
    val buffer = StringBuffer()
    var readSize: Int = reader?.read(rawBuffer) ?: -1
    var maxReadBytes = maxReadSize
    while (readSize != -1 && maxReadBytes > 0) {
        if (readSize > maxReadBytes) {
            readSize = maxReadBytes
        }
        buffer.append(rawBuffer, 0, readSize)
        maxReadBytes -= readSize
        readSize = reader?.read(rawBuffer) ?: -1
    }
    return buffer.toString()
}

Java

/**
 * Converts the contents of an InputStream to a String.
 */
public String readStream(InputStream stream, int maxReadSize)
        throws IOException, UnsupportedEncodingException {
    Reader reader = null;
    reader = new InputStreamReader(stream, "UTF-8");
    char[] rawBuffer = new char[maxReadSize];
    int readSize;
    StringBuffer buffer = new StringBuffer();
    while (((readSize = reader.read(rawBuffer)) != -1) && maxReadSize > 0) {
        if (readSize > maxReadSize) {
            readSize = maxReadSize;
        }
        buffer.append(rawBuffer, 0, readSize);
        maxReadSize -= readSize;
    }
    return buffer.toString();
}

지금까지 코드 내에서 일어나는 이벤트 순서는 다음과 같습니다.

  1. Activity는 NetworkFragment를 시작하고 지정된 URL에 전달합니다.
  2. 사용자가 Activity의 downloadData() 메서드를 트리거하면 NetworkFragmentDownloadTask를 실행합니다.
  3. AsyncTask 메서드 onPreExecute()가 (UI 스레드에서) 먼저 실행되고 기기가 인터넷에 연결되어 있지 않으면 작업을 취소합니다.
  4. 그런 다음, AsyncTask 메서드 doInBackground()가 백그라운드 스레드에서 실행되고 downloadUrl() 메서드를 호출합니다.
  5. downloadUrl() 메서드는 URL 문자열을 매개변수로 받고 HttpsURLConnection 객체를 사용하여 웹 콘텐츠를 InputStream으로 가져옵니다.
  6. InputStreamreadStream() 메서드로 전달되고, 이 메서드가 스트림을 문자열로 변환합니다.
  7. 마지막으로 백그라운드 작업이 완료되면 AsyncTaskonPostExecute() 메서드가 UI 스레드에서 실행되고 DownloadCallback을 사용하여 결과를 문자열 형태로 UI에 다시 전송합니다.

구성 변경사항 존속

이제 네트워크 작업을 수행하는 Activity가 성공적으로 구현되었습니다. 그러나 doInBackground()가 백그라운드 스레드에서 실행되는 동안 기기 구성(예: 화면을 90도 회전)을 변경하기로 할 경우 Activity가 스스로 소멸되고 다시 생성됩니다. 그러면 onCreate()가 다시 실행되고 새로운 NetworkFragment를 참조합니다(런타임 변경사항 가이드를 참조하세요). 그러므로 원본 NetworkFragment에 남은 AsyncTask에는 UI를 더 이상 업데이트할 수 없는 원본 Activity를 참조하는 DownloadCallback이 있을 것입니다. 따라서 백그라운드 스레드에서 수행된 네트워크 작업이 낭비됩니다.

이러한 구성 변경사항이 있어도 이를 유지하려면 원본 Fragment를 그대로 두고 다시 구성된 Activity가 이를 참조하도록 해야 합니다. 이를 위해서는 코드를 다음과 같이 수정해야 합니다.

먼저 NetworkFragmentonCreate() 메서드에서 setRetainInstance(true)를 호출해야 합니다.

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Retain this Fragment across configuration changes in the host Activity.
    retainInstance = true
}

Java

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    // Retain this Fragment across configuration changes in the host Activity.
    setRetainInstance(true);
}

그런 다음, 정적 getInstance() 메서드에서 NetworkFragment를 초기화하는 방법을 변경합니다.

Kotlin

companion object {
    fun getInstance(fragmentManager: FragmentManager, url: String): NetworkFragment {
        // Recover NetworkFragment in case we are re-creating the Activity due to a config change.
        // This is necessary because NetworkFragment might have a task that began running before
        // the config change occurred and has not finished yet.
        // The NetworkFragment is recoverable because it calls setRetainInstance(true).
        var networkFragment = fragmentManager.findFragmentByTag(TAG) as? NetworkFragment
        if (networkFragment == null) {
            networkFragment = NetworkFragment()
            networkFragment.arguments = Bundle().apply {
                putString(URL_KEY, url)
            }
            fragmentManager.beginTransaction()
                    .add(networkFragment, TAG)
                    .commit()
        }
        return networkFragment
    }
}

Java

public static NetworkFragment getInstance(FragmentManager fragmentManager, String url) {
    // Recover NetworkFragment in case we are re-creating the Activity due to a config change.
    // This is necessary because NetworkFragment might have a task that began running before
    // the config change occurred and has not finished yet.
    // The NetworkFragment is recoverable because it calls 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;
}

이제 앱이 인터넷에서 데이터를 가져올 수 있습니다!

이와 똑같은 목표를 달성하는 데 도움을 줄 수 있는 다른 백그라운드 스레드 관리 도구가 여러 가지 있습니다. 앱이 더 복잡해지면 이런 다른 도구가 앱에 더 적합하게 될 수도 있습니다. AsyncTask 대신 고려해볼 만한 옵션은 IntentServiceAsyncTaskLoader입니다.

이 주제에 대한 자세한 내용은 다음 관련 가이드를 참조하세요.