6월 3일의 ⁠#Android11: 베타 버전 출시 행사에 참여하세요.

동기화 어댑터 실행

참고: 대부분의 백그라운드 처리 사용 사례에 권장되는 솔루션인 WorkManager를 사용하는 것이 좋습니다. 어떤 솔루션이 가장 적합한지 알아보려면 백그라운드 처리 가이드를 참조하세요.

이 과정의 이전 강의에서는 데이터 전송 코드를 캡슐화하는 동기화 어댑터 구성요소를 만드는 방법과 동기화 어댑터를 시스템에 플러그인할 수 있는 구성요소를 추가하는 방법을 알아보았습니다. 현재 동기화 어댑터가 포함된 앱을 설치하는 데 필요한 것을 모두 갖고 있지만 지금까지 본 코드 중 어느 것도 실제로 동기화 어댑터를 실행하지 않습니다.

일정에 따라 또는 특정 이벤트의 간접 결과로 동기화 어댑터를 실행해 봐야 합니다. 예를 들어 특정 기간 이후 또는 하루 중 특정 시점에 규칙적인 일정으로 동기화 어댑터를 실행하고 싶을 수 있습니다. 기기에 저장된 데이터에 변경사항이 있을 때 동기화 어댑터를 실행해야 할 때도 있습니다. 사용자 작업의 직접적인 결과로 동기화 어댑터를 실행해서는 안 됩니다. 그럴 경우 동기화 어댑터 프레임워크의 일정 예약 기능을 최대한 활용할 수 없기 때문입니다. 예를 들어 사용자 인터페이스에 새로고침 버튼을 제공해서는 안 됩니다.

동기화 어댑터 실행 시 다음과 같은 옵션을 사용할 수 있습니다.

서버 데이터 변경 시
서버 기반 데이터가 변경된 것을 나타내는 서버의 메시지에 대한 응답으로 동기화 어댑터를 실행합니다. 이 옵션을 사용하면 서버를 폴링하여 성능을 저하시키거나 배터리 수명을 낭비하지 않고도 서버에서 기기로 데이터를 새로고침할 수 있습니다.
기기 데이터 변경 시
기기에서 데이터가 변경되면 동기화 어댑터를 실행합니다. 이 옵션을 통해 기기에서 서버로 수정된 데이터를 보낼 수 있습니다. 이 옵션은 서버의 기기 데이터를 항상 최신 상태로 유지해야 할 때 특히 유용하며 실제로 콘텐츠 제공업체에 데이터를 저장한다면 간단하게 구현할 수 있습니다. 스터브 콘텐츠 제공업체를 사용 중이라면 데이터 변경을 감지하는 것이 더 어려울 수 있습니다.
일정한 간격으로
선택한 간격이 만료된 후 동기화 어댑터를 실행하거나 매일 특정 시간에 실행합니다.
요청 시
사용자 작업에 응답하여 동기화 어댑터를 실행합니다. 그러나 최상의 사용자 환경을 제공하려면 더 자동화된 옵션 중 하나를 기본으로 사용해야 합니다. 자동화된 옵션을 사용하면 배터리 및 네트워크 리소스를 절약할 수 있습니다.

이 학습 과정의 나머지 부분에서는 각 옵션에 관해 자세히 설명합니다.

서버 데이터 변경 시 동기화 어댑터 실행

앱이 서버에서 데이터를 전송하고 서버 데이터가 자주 변경된다면 동기화 어댑터를 사용하여 데이터 변경에 대한 응답으로 다운로드를 수행할 수 있습니다. 동기화 어댑터를 실행하려면 서버를 통해 특별한 메시지를 앱의 BroadcastReceiver로 보내세요. 이 메시지에 대한 응답으로 ContentResolver.requestSync()를 호출하여 동기화 어댑터 프레임워크에 동기화 어댑터를 실행하라는 신호를 보내세요.

GCM(Google 클라우드 메시징)에서는 이 메시지 시스템이 작동하는 데 필요한 서버 및 기기 구성요소를 제공합니다. GCM을 사용하여 전송을 트리거하는 것이 상태 확인을 위해 서버를 폴링하는 것보다 더 안정적이고 효율적입니다. 폴링에는 항상 활성 상태인 Service가 필요하지만 GCM에서는 메시지가 도착하면 활성화되는 BroadcastReceiver를 사용합니다. 업데이트가 제공되지 않을 때도 일정한 간격으로 폴링하면 배터리 전원이 소모되는 반면, GCM에서는 필요할 때만 메시지를 보냅니다.

참고: 앱이 설치된 모든 기기에 GCM을 사용하여 브로드캐스트를 통해 동기화 어댑터를 트리거하면 기기에서는 거의 동시에 메시지를 수신합니다. 이로 인해 동기화 어댑터의 다중 인스턴스가 동시에 실행되어 서버 및 네트워크 과부하가 발생할 수 있습니다. 모든 기기에 브로드캐스트할 때 이러한 상황을 피하려면 기기마다 고유한 기간 동안 동기화 어댑터 시작을 연기하는 것을 고려해야 합니다.

다음 코드 스니펫에서는 수신 GCM 메시지에 응답하여 requestSync()를 실행하는 방법을 보여줍니다.

Kotlin

    ...
    // Constants
    // Content provider authority
    const val AUTHORITY = "com.example.android.datasync.provider"
    // Account type
    const val ACCOUNT_TYPE = "com.example.android.datasync"
    // Account
    const val ACCOUNT = "default_account"
    // Incoming Intent key for extended data
    const val KEY_SYNC_REQUEST = "com.example.android.datasync.KEY_SYNC_REQUEST"
    ...
    class GcmBroadcastReceiver : BroadcastReceiver() {
        ...
        override fun onReceive(context: Context, intent: Intent) {
            // Get a GCM object instance
            val gcm: GoogleCloudMessaging = GoogleCloudMessaging.getInstance(context)
            // Get the type of GCM message
            val messageType: String? = gcm.getMessageType(intent)
            /*
             * Test the message type and examine the message contents.
             * Since GCM is a general-purpose messaging system, you
             * may receive normal messages that don't require a sync
             * adapter run.
             * The following code tests for a a boolean flag indicating
             * that the message is requesting a transfer from the device.
             */
            if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE == messageType
                && intent.getBooleanExtra(KEY_SYNC_REQUEST, false)) {
                /*
                 * Signal the framework to run your sync adapter. Assume that
                 * app initialization has already created the account.
                 */
                ContentResolver.requestSync(mAccount, AUTHORITY, null)
                ...
            }
            ...
        }
        ...
    }
    

자바

    public class GcmBroadcastReceiver extends BroadcastReceiver {
        ...
        // Constants
        // Content provider authority
        public static final String AUTHORITY = "com.example.android.datasync.provider";
        // Account type
        public static final String ACCOUNT_TYPE = "com.example.android.datasync";
        // Account
        public static final String ACCOUNT = "default_account";
        // Incoming Intent key for extended data
        public static final String KEY_SYNC_REQUEST =
                "com.example.android.datasync.KEY_SYNC_REQUEST";
        ...
        @Override
        public void onReceive(Context context, Intent intent) {
            // Get a GCM object instance
            GoogleCloudMessaging gcm =
                    GoogleCloudMessaging.getInstance(context);
            // Get the type of GCM message
            String messageType = gcm.getMessageType(intent);
            /*
             * Test the message type and examine the message contents.
             * Since GCM is a general-purpose messaging system, you
             * may receive normal messages that don't require a sync
             * adapter run.
             * The following code tests for a a boolean flag indicating
             * that the message is requesting a transfer from the device.
             */
            if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)
                &&
                intent.getBooleanExtra(KEY_SYNC_REQUEST)) {
                /*
                 * Signal the framework to run your sync adapter. Assume that
                 * app initialization has already created the account.
                 */
                ContentResolver.requestSync(mAccount, AUTHORITY, null);
                ...
            }
            ...
        }
        ...
    }
    

콘텐츠 제공업체 데이터 변경 시 동기화 어댑터 실행

앱이 콘텐츠 제공업체에서 데이터를 수집하고 개발자는 제공업체를 업데이트할 때마다 서버를 업데이트하려 한다면 동기화 어댑터를 자동으로 실행하도록 앱을 설정하면 됩니다. 이를 위해서는 콘텐츠 제공업체에 관찰자를 등록해야 합니다. 콘텐츠 제공업체의 데이터가 변경되면 콘텐츠 제공업체 프레임워크에서 관찰자를 호출합니다. 관찰자에서 requestSync()를 호출하여 프레임워크에 동기화 어댑터를 실행하도록 지시합니다.

참고: 스터브 콘텐츠 제공업체를 사용 중이라면 콘텐츠 제공업체에 데이터가 없고 onChange()는 호출되지 않습니다. 이 경우 기기 데이터의 변경사항을 감지하는 자체 메커니즘을 제공해야 합니다. 이 메커니즘은 데이터 변경 시 requestSync() 호출을 담당해야 합니다.

콘텐츠 제공업체에 관찰자를 만들려면 ContentObserver 클래스를 확장하고 onChange() 메서드의 두 가지 형식을 구현하세요. onChange()에서 requestSync()를 호출하여 동기화 어댑터를 시작하세요.

관찰자를 등록하려면 registerContentObserver() 호출 시 관찰자를 인수로 전달하세요. 이 호출에서는 보려는 데이터의 콘텐츠 URI도 전달해야 합니다. 콘텐츠 제공업체 프레임워크에서는 이 보기 URI를 ContentResolver.insert()와 같이 제공업체를 수정하는 ContentResolver 메서드에 인수로 전달되는 콘텐츠 URI와 비교합니다. 일치되는 부분이 있으면 ContentObserver.onChange() 구현이 호출됩니다.

다음 코드 스니펫에서는 테이블 변경 시 requestSync()를 호출하는 ContentObserver를 정의하는 방법을 보여줍니다.

Kotlin

    // Constants
    // Content provider scheme
    const val SCHEME = "content://"
    // Content provider authority
    const val AUTHORITY = "com.example.android.datasync.provider"
    // Path for the content provider table
    const val TABLE_PATH = "data_table"
    ...
    class MainActivity : FragmentActivity() {
        ...
        // A content URI for the content provider's data table
        private lateinit var uri: Uri
        // A content resolver for accessing the provider
        private lateinit var mResolver: ContentResolver
        ...
        inner class TableObserver(...) : ContentObserver(...) {
            /*
             * Define a method that's called when data in the
             * observed content provider changes.
             * This method signature is provided for compatibility with
             * older platforms.
             */
            override fun onChange(selfChange: Boolean) {
                /*
                 * Invoke the method signature available as of
                 * Android platform version 4.1, with a null URI.
                 */
                onChange(selfChange, null)
            }

            /*
             * Define a method that's called when data in the
             * observed content provider changes.
             */
            override fun onChange(selfChange: Boolean, changeUri: Uri?) {
                /*
                 * Ask the framework to run your sync adapter.
                 * To maintain backward compatibility, assume that
                 * changeUri is null.
                 */
                ContentResolver.requestSync(account, AUTHORITY, null)
            }
            ...
        }
        ...
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            ...
            // Get the content resolver object for your app
            mResolver = contentResolver
            // Construct a URI that points to the content provider data table
            uri = Uri.Builder()
                    .scheme(SCHEME)
                    .authority(AUTHORITY)
                    .path(TABLE_PATH)
                    .build()
            /*
             * Create a content observer object.
             * Its code does not mutate the provider, so set
             * selfChange to "false"
             */
            val observer = TableObserver(false)
            /*
             * Register the observer for the data table. The table's path
             * and any of its subpaths trigger the observer.
             */
            mResolver.registerContentObserver(uri, true, observer)
            ...
        }
        ...
    }
    

자바

    public class MainActivity extends FragmentActivity {
        ...
        // Constants
        // Content provider scheme
        public static final String SCHEME = "content://";
        // Content provider authority
        public static final String AUTHORITY = "com.example.android.datasync.provider";
        // Path for the content provider table
        public static final String TABLE_PATH = "data_table";
        // Account
        public static final String ACCOUNT = "default_account";
        // Global variables
        // A content URI for the content provider's data table
        Uri uri;
        // A content resolver for accessing the provider
        ContentResolver mResolver;
        ...
        public class TableObserver extends ContentObserver {
            /*
             * Define a method that's called when data in the
             * observed content provider changes.
             * This method signature is provided for compatibility with
             * older platforms.
             */
            @Override
            public void onChange(boolean selfChange) {
                /*
                 * Invoke the method signature available as of
                 * Android platform version 4.1, with a null URI.
                 */
                onChange(selfChange, null);
            }
            /*
             * Define a method that's called when data in the
             * observed content provider changes.
             */
            @Override
            public void onChange(boolean selfChange, Uri changeUri) {
                /*
                 * Ask the framework to run your sync adapter.
                 * To maintain backward compatibility, assume that
                 * changeUri is null.
                 */
                ContentResolver.requestSync(mAccount, AUTHORITY, null);
            }
            ...
        }
        ...
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            ...
            // Get the content resolver object for your app
            mResolver = getContentResolver();
            // Construct a URI that points to the content provider data table
            uri = new Uri.Builder()
                      .scheme(SCHEME)
                      .authority(AUTHORITY)
                      .path(TABLE_PATH)
                      .build();
            /*
             * Create a content observer object.
             * Its code does not mutate the provider, so set
             * selfChange to "false"
             */
            TableObserver observer = new TableObserver(false);
            /*
             * Register the observer for the data table. The table's path
             * and any of its subpaths trigger the observer.
             */
            mResolver.registerContentObserver(uri, true, observer);
            ...
        }
        ...
    }
    

동기화 어댑터를 주기적으로 실행

실행 사이에 대기 기간을 설정하거나 하루 중 특정 시점에 실행하거나 이 두 가지 방식을 모두 사용하여 동기화 어댑터를 주기적으로 실행할 수 있습니다. 동기화 어댑터를 주기적으로 실행하면 서버의 업데이트 간격과 거의 일치시킬 수 있습니다.

이와 마찬가지로 동기화 어댑터가 밤에 실행되도록 예약하여 서버가 비교적 유휴 상태일 때 기기에서 데이터를 업로드할 수 있습니다. 대부분의 사용자는 밤에 전원을 켠 상태로 플러그를 꽂아 두므로 대개 이 시간은 확보할 수 있습니다. 게다가 기기는 동기화 어댑터와 동시에 다른 작업을 실행하지 않습니다. 그러나 이 방식을 취할 때는 기기마다 약간 다른 시점에 데이터 전송을 트리거해야 합니다. 모든 기기가 동시에 동기화 어댑터를 실행하면 서버 및 셀 제공업체 데이터 네트워크에 과부하가 발생할 수 있습니다.

일반적으로 사용자에게 즉각적인 업데이트가 필요하지 않지만 정기적인 업데이트가 필요하다면 주기적인 실행이 적합합니다. 최신 데이터의 가용성과 기기 리소스를 과도하게 사용하지 않는 소규모 동기화 어댑터 실행의 효율성 간에 균형을 유지하려 할 때도 주기적인 실행이 적합합니다.

동기화 어댑터를 일정한 간격으로 실행하려면 addPeriodicSync()를 호출하세요. 이렇게 하면 일정 시간이 지나면 동기화 어댑터가 실행되도록 예약됩니다. 동기화 어댑터 프레임워크에서는 다른 동기화 어댑터 실행을 고려해야 하고 배터리 효율성을 극대화하려 하므로 경과된 시간은 몇 초 정도 달라질 수 있습니다. 또한 네트워크를 사용할 수 없다면 프레임워크에서는 동기화 어댑터를 실행하지 않습니다.

주의할 점은 addPeriodicSync()는 하루 중 특정 시점에 동기화 어댑터를 실행하지는 않는다는 것입니다. 동기화 어댑터를 매일 거의 같은 시간에 실행하려면 반복 알람을 트리거로 사용하세요. 반복 알람은 AlarmManager에 관한 참조 문서에 자세히 설명되어 있습니다. setInexactRepeating() 메서드를 사용하여 약간의 차이가 있는 하루 중 특정 시점 트리거를 설정할 때는 시작 시간을 무작위로 지정하여 여러 기기에서 동기화 어댑터가 시차를 두고 실행되게 해야 합니다.

addPeriodicSync() 메서드에서는 setSyncAutomatically()를 사용하지 않으므로 비교적 짧은 시간 내에 동기화를 여러 번 실행할 수 있습니다. 또한 addPeriodicSync() 호출 시 몇 개의 동기화 어댑터 제어 플래그만 허용됩니다. 허용되지 않는 플래그는 addPeriodicSync()의 참조 문서에 설명되어 있습니다.

다음 코드 스니펫에서는 주기적 동기화 어댑터의 실행 시점을 예약하는 방법을 보여줍니다.

Kotlin

    // Content provider authority
    const val AUTHORITY = "com.example.android.datasync.provider"
    // Account
    const val ACCOUNT = "default_account"
    // Sync interval constants
    const val SECONDS_PER_MINUTE = 60L
    const val SYNC_INTERVAL_IN_MINUTES = 60L
    const val SYNC_INTERVAL = SYNC_INTERVAL_IN_MINUTES * SECONDS_PER_MINUTE
    ...
    class MainActivity : FragmentActivity() {
        ...
        // A content resolver for accessing the provider
        private lateinit var mResolver: ContentResolver

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            ...
            // Get the content resolver for your app
            mResolver = contentResolver
            /*
             * Turn on periodic syncing
             */
            ContentResolver.addPeriodicSync(
                    mAccount,
                    AUTHORITY,
                    Bundle.EMPTY,
                    SYNC_INTERVAL)
            ...
        }
        ...
    }
    

자바

    public class MainActivity extends FragmentActivity {
        ...
        // Constants
        // Content provider authority
        public static final String AUTHORITY = "com.example.android.datasync.provider";
        // Account
        public static final String ACCOUNT = "default_account";
        // Sync interval constants
        public static final long SECONDS_PER_MINUTE = 60L;
        public static final long SYNC_INTERVAL_IN_MINUTES = 60L;
        public static final long SYNC_INTERVAL =
                SYNC_INTERVAL_IN_MINUTES *
                SECONDS_PER_MINUTE;
        // Global variables
        // A content resolver for accessing the provider
        ContentResolver mResolver;
        ...
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            ...
            // Get the content resolver for your app
            mResolver = getContentResolver();
            /*
             * Turn on periodic syncing
             */
            ContentResolver.addPeriodicSync(
                    mAccount,
                    AUTHORITY,
                    Bundle.EMPTY,
                    SYNC_INTERVAL);
            ...
        }
        ...
    }
    

요청 시 동기화 어댑터 실행

사용자 요청에 응답하여 동기화 어댑터를 실행하는 것은 가장 바람직하지 않은 동기화 어댑터 실행 전략입니다. 이 프레임워크는 일정에 따라 동기화 어댑터를 실행할 때 배터리 전원을 절약하도록 특별히 설계되었습니다. 데이터 변경에 응답하여 동기화를 실행하는 옵션에서는 전력이 새 데이터를 제공하는 데 사용되므로 배터리 전원을 효과적으로 사용합니다.

이에 비해 사용자가 요청 시 동기화를 실행하도록 허용하면 동기화가 자체 실행되므로 네트워크 및 전원 리소스가 비효율적으로 사용됩니다. 또한 요청 시 동기화를 제공하면 데이터가 변경되었다는 증거가 없더라도 사용자가 동기화를 요청하도록 하고, 데이터를 새로고침하지 않는 동기화를 실행하면 배터리 전원을 비효율적으로 사용하게 됩니다. 일반적으로 앱에서는 다른 신호를 사용하여 동기화를 트리거하거나 사용자 입력 없이 일정한 간격으로 신호를 예약해야 합니다.

그럼에도 요청 시 동기화 어댑터를 실행하고 싶다면 수동 동기화 어댑터 실행에 동기화 어댑터 플래그를 설정한 후 ContentResolver.requestSync()를 호출하세요.

다음 플래그를 사용하여 요청 시 전송을 실행하세요.

SYNC_EXTRAS_MANUAL
수동 동기화를 강제합니다. 동기화 어댑터 프레임워크는 setSyncAutomatically()에서 설정한 플래그와 같은 기존 설정을 무시합니다.
SYNC_EXTRAS_EXPEDITED
동기화가 즉시 시작되도록 강제합니다. 이렇게 설정하지 않으면 시스템은 단기간에 많은 요청을 예약하여 배터리 사용을 최적화하려 하므로 몇 초 동안 기다려야 동기화 요청을 실행할 수 있습니다.

다음 코드 스니펫에서는 버튼 클릭에 응답하여 requestSync()를 호출하는 방법을 보여줍니다.

Kotlin

    // Constants
    // Content provider authority
    val AUTHORITY = "com.example.android.datasync.provider"
    // Account type
    val ACCOUNT_TYPE = "com.example.android.datasync"
    // Account
    val ACCOUNT = "default_account"
    ...
    class MainActivity : FragmentActivity() {
        ...
        // Instance fields
        private lateinit var mAccount: Account
        ...
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            ...
            /*
             * Create the dummy account. The code for CreateSyncAccount
             * is listed in the lesson Creating a Sync Adapter
             */

            mAccount = createSyncAccount()
            ...
        }

        /**
         * Respond to a button click by calling requestSync(). This is an
         * asynchronous operation.
         *
         * This method is attached to the refresh button in the layout
         * XML file
         *
         * @param v The View associated with the method call,
         * in this case a Button
         */
        fun onRefreshButtonClick(v: View) {
            // Pass the settings flags by inserting them in a bundle
            val settingsBundle = Bundle().apply {
                putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true)
                putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true)
            }
            /*
             * Request the sync for the default account, authority, and
             * manual sync settings
             */
            ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle)
        }
    

자바

    public class MainActivity extends FragmentActivity {
        ...
        // Constants
        // Content provider authority
        public static final String AUTHORITY =
                "com.example.android.datasync.provider";
        // Account type
        public static final String ACCOUNT_TYPE = "com.example.android.datasync";
        // Account
        public static final String ACCOUNT = "default_account";
        // Instance fields
        Account mAccount;
        ...
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            ...
            /*
             * Create the dummy account. The code for CreateSyncAccount
             * is listed in the lesson Creating a Sync Adapter
             */

            mAccount = CreateSyncAccount(this);
            ...
        }
        /**
         * Respond to a button click by calling requestSync(). This is an
         * asynchronous operation.
         *
         * This method is attached to the refresh button in the layout
         * XML file
         *
         * @param v The View associated with the method call,
         * in this case a Button
         */
        public void onRefreshButtonClick(View v) {
            // Pass the settings flags by inserting them in a bundle
            Bundle settingsBundle = new Bundle();
            settingsBundle.putBoolean(
                    ContentResolver.SYNC_EXTRAS_MANUAL, true);
            settingsBundle.putBoolean(
                    ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
            /*
             * Request the sync for the default account, authority, and
             * manual sync settings
             */
            ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle);
        }