동기화 어댑터 실행

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

이 클래스의 이전 과정에서는 데이터 전송 코드를 캡슐화하는 동기화 어댑터 구성요소를 만드는 방법과 동기화 어댑터를 시스템에 연결할 수 있는 추가 구성요소를 추가하는 방법을 알아봤습니다. 이제 동기화 어댑터를 포함하는 앱을 설치하는 데 필요한 모든 것을 갖추었지만 지금까지 본 코드 중 어느 것도 실제로 동기화 어댑터를 실행하지 않았습니다.

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

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

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

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

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

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

Google 클라우드 메시징 (GCM)은 이 메시징 시스템이 작동하는 데 필요한 서버 및 기기 구성요소를 모두 제공합니다. 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)
            ...
        }
        ...
    }
    ...
}

Java

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)
        ...
    }
    ...
}

Java

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)
        ...
    }
    ...
}

Java

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 placeholder 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)
    }

Java

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