Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

네트워크 사용량 관리

이 과정에서는 네트워크 리소스의 사용량을 미세하게 제어하는 애플리케이션을 작성하는 방법에 대해 설명합니다. 애플리케이션이 많은 네트워크 작업을 수행할 경우 사용자가 앱의 데이터 습관을 제어할 수 있는 사용자 설정을 제공해야 합니다(예: 앱의 데이터 동기화 빈도, Wi-Fi에 연결되었을 때만 업로드/다운로드할지 여부, 로밍 중 데이터 사용). 사용자가 이러한 제어 기능을 사용할 수 있게 되면 한도에 가까워지고 있을 때 앱의 백그라운드 데이터 액세스를 비활성화할 가능성이 작아집니다. 그 대신 앱이 사용하는 데이터 양을 정밀하게 제어할 수 있기 때문입니다.

일정 기간의 네트워크 연결 횟수와 유형을 포함하여 앱의 네트워크 사용량에 대한 자세한 내용은 웹 앱네트워크 프로파일러로 네트워크 트래픽 검사를 참조하세요. 다운로드 및 네트워크 연결이 배터리 수명이 미치는 영향을 최소화하는 앱을 개발하는 방법에 대한 일반 가이드라인은 배터리 수명 최적화배터리 소모 없이 데이터 전송을 참조하세요.

네트워크 연결 샘플도 참조할 수 있습니다.

기기의 네트워크 연결 확인

기기는 다양한 유형의 네트워크 연결을 사용할 수 있습니다. 이 과정에서는 Wi-Fi 또는 모바일 네트워크 연결을 사용하는 방법을 중점으로 설명합니다. 이용 가능한 네트워크 유형의 전체 목록은 ConnectivityManager를 참조하세요.

일반적으로 Wi-Fi 속도가 더 빠릅니다. 또한 모바일 데이터는 사용량에 따라 요금을 부과하는 경우가 많으므로 비용이 많이 들어갈 수 있습니다. 앱에 가장 많이 사용하는 전략은 Wi-Fi 네트워크가 연결되었을 때만 용량이 큰 데이터를 가져오는 것입니다.

네트워크 작업을 수행하기 전에 네트워크 연결 상태를 확인하는 것이 좋습니다. 특히 이 방법은 앱이 잘못된 라디오를 실수로 사용하지 못하게 합니다. 네트워크 연결을 사용할 수 없다면 애플리케이션이 적절히 응답해야 합니다. 네트워크 연결을 확인할 때는 일반적으로 다음과 같은 클래스를 사용합니다.

  • ConnectivityManager: 네트워크 연결 상태에 대한 쿼리에 답합니다. 또한 네트워크 연결이 변경되면 애플리케이션에 알립니다.
  • NetworkInfo: 특정 유형(현재는 모바일 또는 Wi-Fi)의 네트워크 인터페이스 상태를 설명합니다.

이 코드 스니펫은 Wi-Fi 및 모바일의 네트워크 연결성을 테스트합니다. 이 네트워크 인터페이스가 이용 가능한지(즉, 네트워크 연결이 가능한지) 및/또는 연결되었는지(즉, 네트워크 연결이 존재하고 소켓을 설정하여 데이터를 전달할 수 있는지) 결정합니다.

Kotlin

private const val DEBUG_TAG = "NetworkStatusExample"
...
val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
var isWifiConn: Boolean = false
var isMobileConn: Boolean = false
connMgr.allNetworks.forEach { network ->
    connMgr.getNetworkInfo(network).apply {
        if (type == ConnectivityManager.TYPE_WIFI) {
            isWifiConn = isWifiConn or isConnected
        }
        if (type == ConnectivityManager.TYPE_MOBILE) {
            isMobileConn = isMobileConn or isConnected
        }
    }
}
Log.d(DEBUG_TAG, "Wifi connected: $isWifiConn")
Log.d(DEBUG_TAG, "Mobile connected: $isMobileConn")

Java

private static final String DEBUG_TAG = "NetworkStatusExample";
...
ConnectivityManager connMgr =
        (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
boolean isWifiConn = false;
boolean isMobileConn = false;
for (Network network : connMgr.getAllNetworks()) {
    NetworkInfo networkInfo = connMgr.getNetworkInfo(network);
    if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
        isWifiConn |= networkInfo.isConnected();
    }
    if (networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) {
        isMobileConn |= networkInfo.isConnected();
    }
}
Log.d(DEBUG_TAG, "Wifi connected: " + isWifiConn);
Log.d(DEBUG_TAG, "Mobile connected: " + isMobileConn);

네트워크가 "이용 가능한지" 여부에 따라 결정을 내려서는 안 됩니다. 네트워크 작업을 수행하기 전에는 항상 isConnected()를 확인해야 합니다. isConnected()가 모바일 네트워크 끊김, 비행기 모드, 백그라운드 데이터 제한 등을 처리하기 때문입니다.

네트워크 인터페이스를 사용 가능한지 확인하는 더 간단한 방법은 다음과 같습니다. getActiveNetworkInfo() 메서드는 처음 연결된 네트워크 인터페이스를 찾아서 이를 나타내는 NetworkInfo 인스턴스를 반환하거나 아무 인터페이스도 연결되지 않았다면(즉, 인터넷 연결 불가) null을 반환합니다.

Kotlin

fun isOnline(): Boolean {
    val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val networkInfo: NetworkInfo? = connMgr.activeNetworkInfo
    return networkInfo?.isConnected == true
}

Java

public boolean isOnline() {
    ConnectivityManager connMgr = (ConnectivityManager)
            getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
    return (networkInfo != null && networkInfo.isConnected());
}

보다 자세한 상태를 쿼리하려면 NetworkInfo.DetailedState를 사용할 수 있으나 이런 경우는 흔하지 않습니다.

네트워크 사용량 관리

사용자에게 앱의 네트워크 리소스 사용량에 대한 명시적 제어 기능을 제공하는 기본 설정 Activity를 구현할 수 있습니다. 예:

  • 기기가 Wi-Fi 네트워크에 연결되었을 경우에만 사용자가 동영상을 업로드하도록 허용할 수 있습니다.
  • 네트워크 가용성, 시간 간격 등의 특정 기준에 따라 동기화할(또는 하지 않을) 수 있습니다.

네트워크 액세스를 지원하고 네트워크 사용량을 관리하는 앱을 개발하려면 매니페스트에 적절한 권한과 인텐트 필터가 있어야 합니다.

  • 아래에 발췌된 매니페스트에는 다음과 같은 권한이 포함됩니다.
  • ACTION_MANAGE_NETWORK_USAGE 작업에 대한 인텐트 필터를 선언하고(Android 4.0에서 도입) 애플리케이션이 데이터 사용량을 제어하는 옵션을 제공하는 Activity를 정의한다는 것을 나타냅니다. ACTION_MANAGE_NETWORK_USAGE는 특정 애플리케이션의 네트워크 데이터 사용량을 관리하는 설정을 보여줍니다. 앱에 사용자가 네트워크 사용량을 제어할 수 있는 설정 Activity가 있다면 해당 Activity에 대해 이 인텐트 필터를 선언해야 합니다. 샘플 애플리케이션에서 이 작업은 SettingsActivity 클래스가 처리하며, 이 클래스는 사용자가 피드를 다운로드하는 시점을 선택할 수 있는 기본 설정 UI를 표시합니다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.networkusage"
    ...>

    <uses-sdk android:minSdkVersion="4"
           android:targetSdkVersion="14" />

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

    <application
        ...>
        ...
        <activity android:label="SettingsActivity" android:name=".SettingsActivity">
             <intent-filter>
                <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
                <category android:name="android.intent.category.DEFAULT" />
          </intent-filter>
        </activity>
    </application>
</manifest>

기본 설정 Activity 구현

위의 발췌된 매니페스트에서 확인할 수 있듯이 샘플 앱의 Activity SettingsActivityACTION_MANAGE_NETWORK_USAGE 작업에 대한 인텐트 필터가 있습니다. SettingsActivityPreferenceActivity의 하위 클래스입니다. 이는 사용자가 다음을 지정할 수 있는 기본 설정 화면(그림 1 참조)을 표시합니다.

  • 각 XML 피드 항목에 대한 요약을 표시할지, 각 항목의 링크만 표시할지 여부.
  • 네트워크가 연결되었을 때 XML 피드를 다운로드할지, Wi-Fi가 연결되었을 때만 XML 피드를 다운로드할지 여부.
기본 설정 패널 네트워크 기본 설정

그림 1. 기본 설정 Activity.

다음은 SettingsActivity입니다. 이는 OnSharedPreferenceChangeListener를 구현합니다. 사용자가 기본 설정을 변경하면 onSharedPreferenceChanged()를 실행하고, refreshDisplay가 true로 설정됩니다. 이렇게 하면 사용자가 기본 Activity로 돌아왔을 때 디스플레이가 새로고침 됩니다.

Kotlin

class SettingsActivity : PreferenceActivity(), OnSharedPreferenceChangeListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Loads the XML preferences file
        addPreferencesFromResource(R.xml.preferences)
    }

    override fun onResume() {
        super.onResume()

        // Registers a listener whenever a key changes
        preferenceScreen?.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
    }

    override fun onPause() {
        super.onPause()

        // Unregisters the listener set in onResume().
        // It's best practice to unregister listeners when your app isn't using them to cut down on
        // unnecessary system overhead. You do this in onPause().
        preferenceScreen?.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
    }

    // When the user changes the preferences selection,
    // onSharedPreferenceChanged() restarts the main activity as a new
    // task. Sets the refreshDisplay flag to "true" to indicate that
    // the main activity should update its display.
    // The main activity queries the PreferenceManager to get the latest settings.

    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
        // Sets refreshDisplay to true so that when the user returns to the main
        // activity, the display refreshes to reflect the new settings.
        NetworkActivity.refreshDisplay = true
    }
}

Java

public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Loads the XML preferences file
        addPreferencesFromResource(R.xml.preferences);
    }

    @Override
    protected void onResume() {
        super.onResume();

        // Registers a listener whenever a key changes
        getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onPause() {
        super.onPause();

       // Unregisters the listener set in onResume().
       // It's best practice to unregister listeners when your app isn't using them to cut down on
       // unnecessary system overhead. You do this in onPause().
       getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
    }

    // When the user changes the preferences selection,
    // onSharedPreferenceChanged() restarts the main activity as a new
    // task. Sets the refreshDisplay flag to "true" to indicate that
    // the main activity should update its display.
    // The main activity queries the PreferenceManager to get the latest settings.

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        // Sets refreshDisplay to true so that when the user returns to the main
        // activity, the display refreshes to reflect the new settings.
        NetworkActivity.refreshDisplay = true;
    }
}

기본 설정 변경사항에 대한 응답

사용자가 설정 화면에서 기본 설정을 변경한 경우 일반적으로는 앱 동작에 변화가 생깁니다. 이 스니펫에서 앱은 onStart()에서 기본 설정을 확인합니다. 설정과 기기의 네트워크 연결 사이에 일치하는 것이 있으면(예: 설정이 "Wi-Fi"이고 기기에 Wi-Fi가 연결된 경우) 앱이 피드를 다운로드하고 디스플레이를 새로고침합니다.

Kotlin

class NetworkActivity : Activity() {

    // The BroadcastReceiver that tracks network connectivity changes.
    private lateinit var receiver: NetworkReceiver

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Registers BroadcastReceiver to track network connection changes.
        val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
        receiver = NetworkReceiver()
        this.registerReceiver(receiver, filter)
    }

    public override fun onDestroy() {
        super.onDestroy()
        // Unregisters BroadcastReceiver when app is destroyed.
        this.unregisterReceiver(receiver)
    }

    // Refreshes the display if the network connection and the
    // pref settings allow it.

    public override fun onStart() {
        super.onStart()

        // Gets the user's network preference settings
        val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)

        // Retrieves a string value for the preferences. The second parameter
        // is the default value to use if a preference value is not found.
        sPref = sharedPrefs.getString("listPref", "Wi-Fi")

        updateConnectedFlags()

        if (refreshDisplay) {
            loadPage()
        }
    }

    // Checks the network connection and sets the wifiConnected and mobileConnected
    // variables accordingly.
    fun updateConnectedFlags() {
        val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

        val activeInfo: NetworkInfo? = connMgr.activeNetworkInfo
        if (activeInfo?.isConnected == true) {
            wifiConnected = activeInfo.type == ConnectivityManager.TYPE_WIFI
            mobileConnected = activeInfo.type == ConnectivityManager.TYPE_MOBILE
        } else {
            wifiConnected = false
            mobileConnected = false
        }
    }

    // Uses AsyncTask subclass to download the XML feed from stackoverflow.com.
    fun loadPage() {
        if (sPref == ANY && (wifiConnected || mobileConnected) || sPref == WIFI && wifiConnected) {
            // AsyncTask subclass
            DownloadXmlTask().execute(URL)
        } else {
            showErrorPage()
        }
    }

    companion object {

        const val WIFI = "Wi-Fi"
        const val ANY = "Any"
        const val SO_URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort;=newest"

        // Whether there is a Wi-Fi connection.
        private var wifiConnected = false
        // Whether there is a mobile connection.
        private var mobileConnected = false
        // Whether the display should be refreshed.
        var refreshDisplay = true
    
        // The user's current network preference setting.
        var sPref: String? = null
    }
...

}

Java

public class NetworkActivity extends Activity {
    public static final String WIFI = "Wi-Fi";
    public static final String ANY = "Any";
    private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort;=newest";

    // Whether there is a Wi-Fi connection.
    private static boolean wifiConnected = false;
    // Whether there is a mobile connection.
    private static boolean mobileConnected = false;
    // Whether the display should be refreshed.
    public static boolean refreshDisplay = true;

    // The user's current network preference setting.
    public static String sPref = null;

    // The BroadcastReceiver that tracks network connectivity changes.
    private NetworkReceiver receiver = new NetworkReceiver();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Registers BroadcastReceiver to track network connection changes.
        IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
        receiver = new NetworkReceiver();
        this.registerReceiver(receiver, filter);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // Unregisters BroadcastReceiver when app is destroyed.
        if (receiver != null) {
            this.unregisterReceiver(receiver);
        }
    }

    // Refreshes the display if the network connection and the
    // pref settings allow it.

    @Override
    public void onStart () {
        super.onStart();

        // Gets the user's network preference settings
        SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);

        // Retrieves a string value for the preferences. The second parameter
        // is the default value to use if a preference value is not found.
        sPref = sharedPrefs.getString("listPref", "Wi-Fi");

        updateConnectedFlags();

        if(refreshDisplay){
            loadPage();
        }
    }

    // Checks the network connection and sets the wifiConnected and mobileConnected
    // variables accordingly.
    public void updateConnectedFlags() {
        ConnectivityManager connMgr = (ConnectivityManager)
                getSystemService(Context.CONNECTIVITY_SERVICE);

        NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
        if (activeInfo != null && activeInfo.isConnected()) {
            wifiConnected = activeInfo.getType() == ConnectivityManager.TYPE_WIFI;
            mobileConnected = activeInfo.getType() == ConnectivityManager.TYPE_MOBILE;
        } else {
            wifiConnected = false;
            mobileConnected = false;
        }
    }

    // Uses AsyncTask subclass to download the XML feed from stackoverflow.com.
    public void loadPage() {
        if (((sPref.equals(ANY)) && (wifiConnected || mobileConnected))
                || ((sPref.equals(WIFI)) && (wifiConnected))) {
            // AsyncTask subclass
            new DownloadXmlTask().execute(URL);
        } else {
            showErrorPage();
        }
    }
...

}

연결 변경사항 탐지

마지막으로 필요한 부분은 BroadcastReceiver 하위 클래스, NetworkReceiver입니다. 기기의 네트워크 연결이 변경되면 NetworkReceiverCONNECTIVITY_ACTION 작업을 가로채고 네트워크 연결 상태가 무엇인지 확인한 다음, wifiConnectedmobileConnected 플래그를 적절히 true/false로 설정합니다. 그러면 사용자가 다음에 앱으로 돌아왔을 때 앱이 최신 피드를 다운로드하고 NetworkActivity.refreshDisplaytrue로 설정되어 있으면 디스플레이를 업데이트합니다.

불필요하게 호출되는 BroadcastReceiver를 설정하면 시스템 리소스가 낭비될 수 있습니다. 샘플 애플리케이션은 onCreate()에서 BroadcastReceiver NetworkReceiver를 등록하고 onDestroy()에서 등록을 해제합니다. 이 방법은 매니페스트에서 <receiver>를 선언하는 것보다 가볍습니다. 매니페스트에서 <receiver>를 선언하면 몇 주간 앱을 실행하지 않은 앱도 깨울 수 있습니다. 기본 Activity에서 NetworkReceiver를 등록하거나 해제하는 방법으로 사용자가 앱을 떠난 후 앱을 깨우지 않도록 할 수 있습니다. 매니페스트에서 <receiver>를 선언하고 이것이 어디에 필요한지 정확히 안다면 setComponentEnabledSetting()를 사용하여 이를 적절히 활성화/비활성화할 수 있습니다.

다음은 NetworkReceiver입니다.

Kotlin

class NetworkReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val conn = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val networkInfo: NetworkInfo? = conn.activeNetworkInfo

        // Checks the user prefs and the network connection. Based on the result, decides whether
        // to refresh the display or keep the current display.
        // If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection.
        if (WIFI == sPref && networkInfo?.type == ConnectivityManager.TYPE_WIFI) {
            // If device has its Wi-Fi connection, sets refreshDisplay
            // to true. This causes the display to be refreshed when the user
            // returns to the app.
            refreshDisplay = true
            Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show()

            // If the setting is ANY network and there is a network connection
            // (which by process of elimination would be mobile), sets refreshDisplay to true.
        } else if (ANY == sPref && networkInfo != null) {
            refreshDisplay = true

            // Otherwise, the app can't download content--either because there is no network
            // connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there
            // is no Wi-Fi connection.
            // Sets refreshDisplay to false.
        } else {
            refreshDisplay = false
            Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show()
        }
    }
}

Java

public class NetworkReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        ConnectivityManager conn =  (ConnectivityManager)
            context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = conn.getActiveNetworkInfo();

        // Checks the user prefs and the network connection. Based on the result, decides whether
        // to refresh the display or keep the current display.
        // If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection.
        if (WIFI.equals(sPref) && networkInfo != null
            && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
            // If device has its Wi-Fi connection, sets refreshDisplay
            // to true. This causes the display to be refreshed when the user
            // returns to the app.
            refreshDisplay = true;
            Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show();

        // If the setting is ANY network and there is a network connection
        // (which by process of elimination would be mobile), sets refreshDisplay to true.
        } else if (ANY.equals(sPref) && networkInfo != null) {
            refreshDisplay = true;

        // Otherwise, the app can't download content--either because there is no network
        // connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there
        // is no Wi-Fi connection.
        // Sets refreshDisplay to false.
        } else {
            refreshDisplay = false;
            Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show();
        }
    }
}