Skip to content

Commit 408a85d

Browse files
edv-Shinclaude
andcommitted
refactor: FCM 토큰 등록 경로를 워커로 통합 #569
진입 경로와 워커가 같은 일을 두 갈래로 하던 것을 워커 하나로 모읍니다. 재시도와 백오프, 프로세스 종료 후 재개를 WorkManager가 맡습니다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 296a7f3 commit 408a85d

10 files changed

Lines changed: 127 additions & 131 deletions

File tree

app/src/main/java/com/project200/undabang/di/RepositoryModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.project200.undabang.di
22

33
import com.project200.common.utils.ChatRoomStateRepository
4+
import com.project200.domain.manager.FcmTokenSyncScheduler
45
import com.project200.undabang.fcm.ChatRoomStateRepositoryImpl
6+
import com.project200.undabang.fcm.FcmTokenSyncSchedulerImpl
57
import dagger.Binds
68
import dagger.Module
79
import dagger.hilt.InstallIn
@@ -14,4 +16,8 @@ abstract class RepositoryModule {
1416
@Binds
1517
@Singleton
1618
abstract fun bindChatRoomStateRepository(impl: ChatRoomStateRepositoryImpl): ChatRoomStateRepository
19+
20+
@Binds
21+
@Singleton
22+
abstract fun bindFcmTokenSyncScheduler(impl: FcmTokenSyncSchedulerImpl): FcmTokenSyncScheduler
1723
}

app/src/main/java/com/project200/undabang/fcm/FcmTokenSyncPolicy.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,17 @@ package com.project200.undabang.fcm
33
/** FcmTokenSyncWorker의 재시도 판단. 안드로이드 의존성이 없어 단위 테스트로 검증합니다 */
44
object FcmTokenSyncPolicy {
55
/** 전송 실패 시 총 시도 횟수 상한 */
6-
const val MAX_ATTEMPT_COUNT = 5
6+
const val MAX_ATTEMPT_COUNT = 10
77

88
/** runAttemptCount는 WorkManager가 주는 값으로 첫 실행이 0입니다 */
99
fun shouldRetry(runAttemptCount: Int): Boolean = runAttemptCount + 1 < MAX_ATTEMPT_COUNT
10+
11+
/**
12+
* 토큰 갱신 실패 시 재시도 여부입니다.
13+
* invalid_grant는 리프레시 토큰이 무효한 상태라 재시도해도 결과가 같습니다
14+
*/
15+
fun shouldRetryAfterRefreshFailure(
16+
invalidGrant: Boolean,
17+
runAttemptCount: Int,
18+
): Boolean = !invalidGrant && shouldRetry(runAttemptCount)
1019
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.project200.undabang.fcm
2+
3+
import android.content.Context
4+
import com.project200.domain.manager.FcmTokenSyncScheduler
5+
import dagger.hilt.android.qualifiers.ApplicationContext
6+
import javax.inject.Inject
7+
import javax.inject.Singleton
8+
9+
@Singleton
10+
class FcmTokenSyncSchedulerImpl
11+
@Inject
12+
constructor(
13+
@ApplicationContext private val context: Context,
14+
) : FcmTokenSyncScheduler {
15+
override fun schedule() = FcmTokenSyncWorker.enqueue(context)
16+
17+
override fun cancel() = FcmTokenSyncWorker.cancel(context)
18+
}

app/src/main/java/com/project200/undabang/fcm/FcmTokenSyncWorker.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,8 @@ class FcmTokenSyncWorker
4646
val invalidGrant =
4747
ex?.type == AuthorizationException.TYPE_OAUTH_TOKEN_ERROR &&
4848
ex.error == "invalid_grant"
49-
// invalid_grant는 재시도해도 같음. 세션 정리는 refreshAccessToken() 내부가 함
50-
return if (invalidGrant) {
51-
Result.failure()
52-
} else if (FcmTokenSyncPolicy.shouldRetry(runAttemptCount)) {
49+
// 세션 정리는 refreshAccessToken() 내부가 함
50+
return if (FcmTokenSyncPolicy.shouldRetryAfterRefreshFailure(invalidGrant, runAttemptCount)) {
5351
Result.retry()
5452
} else {
5553
Result.failure()
@@ -102,5 +100,10 @@ class FcmTokenSyncWorker
102100
WorkManager.getInstance(context)
103101
.enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.REPLACE, request)
104102
}
103+
104+
/** 예약된 등록을 취소합니다. 세션이 정리되면 보낼 회원ID가 없어 실행할 이유가 없습니다 */
105+
fun cancel(context: Context) {
106+
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
107+
}
105108
}
106109
}

app/src/main/java/com/project200/undabang/main/MainViewModel.kt

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,16 @@ package com.project200.undabang.main
33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
55
import com.project200.common.utils.NetworkMonitor
6-
import com.project200.domain.model.FcmTokenSyncResult
6+
import com.project200.domain.manager.FcmTokenSyncScheduler
77
import com.project200.domain.model.UpdateCheckResult
88
import com.project200.domain.usecase.CheckForUpdateUseCase
99
import com.project200.domain.usecase.ClearSessionUseCase
1010
import com.project200.domain.usecase.GetMemberIdUseCase
11-
import com.project200.domain.usecase.SyncFcmTokenUseCase
1211
import com.project200.undabang.oauth.AuthManager
1312
import com.project200.undabang.oauth.AuthStateManager
1413
import com.project200.undabang.oauth.TokenRefreshResult
1514
import dagger.hilt.android.lifecycle.HiltViewModel
1615
import kotlinx.coroutines.CancellationException
17-
import kotlinx.coroutines.Job
1816
import kotlinx.coroutines.delay
1917
import kotlinx.coroutines.flow.MutableStateFlow
2018
import kotlinx.coroutines.flow.StateFlow
@@ -30,7 +28,7 @@ class MainViewModel
3028
@Inject
3129
constructor(
3230
private val checkForUpdateUseCase: CheckForUpdateUseCase,
33-
private val syncFcmTokenUseCase: SyncFcmTokenUseCase,
31+
private val fcmTokenSyncScheduler: FcmTokenSyncScheduler,
3432
private val networkMonitor: NetworkMonitor,
3533
private val authManager: AuthManager,
3634
private val authStateManager: AuthStateManager,
@@ -46,9 +44,6 @@ class MainViewModel
4644

4745
private var wasOffline = !networkMonitor.isCurrentlyConnected()
4846

49-
private var registrationJob: Job? = null
50-
private var registrationPending = false
51-
5247
init {
5348
observeNetworkReconnection()
5449
observeForceLogout()
@@ -119,7 +114,8 @@ class MainViewModel
119114
}
120115
}
121116
}
122-
ensureFcmRegistration()
117+
// 등록은 워커에 맡김. 재시도와 백오프, 프로세스 종료 후 재개를 WorkManager가 처리
118+
fcmTokenSyncScheduler.schedule()
123119
_entryState.compareAndSet(EntryState.Loading, EntryState.Content)
124120
}
125121

@@ -145,9 +141,6 @@ class MainViewModel
145141
viewModelScope.launch {
146142
networkMonitor.networkState.collect { isOnline ->
147143
if (isOnline && wasOffline && _entryState.value is EntryState.Content) {
148-
if (registrationPending) {
149-
ensureFcmRegistration() // 실패했던 FCM 등록 재시도
150-
}
151144
recheckForUpdateOnReconnect()
152145
}
153146
wasOffline = !isOnline
@@ -170,23 +163,6 @@ class MainViewModel
170163
.onFailure { Timber.w(it, "재연결 후 업데이트 확인 실패 - 무시") }
171164
}
172165

173-
// FCM 토큰 등록
174-
private fun ensureFcmRegistration() {
175-
// 재시도 중 재연결이 겹치면 등록 요청이 중복 발행되는 것을 방지
176-
if (registrationJob?.isActive == true) return
177-
registrationPending = true
178-
registrationJob =
179-
viewModelScope.launch {
180-
// SKIPPED는 보낼 토큰이 없는 상태 - 대기를 닫지 않고 재연결 때 재시도
181-
if (syncFcmTokenUseCase() == FcmTokenSyncResult.SUCCESS) {
182-
registrationPending = false
183-
Timber.d("FCM 토큰 등록 성공")
184-
} else {
185-
Timber.w("FCM 토큰 등록 실패 - 재연결 시 재시도 예정")
186-
}
187-
}
188-
}
189-
190166
private suspend fun transitionToLogin() {
191167
runCatching { clearSessionUseCase() }
192168
.onFailure { Timber.e(it, "세션 정리 실패 - 전이는 계속") }

app/src/test/java/com/project200/undabang/fcm/FcmTokenSyncPolicyTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,27 @@ class FcmTokenSyncPolicyTest {
2727
fun `상한을 넘긴 뒤에도 재시도하지 않는다`() {
2828
assertThat(FcmTokenSyncPolicy.shouldRetry(FcmTokenSyncPolicy.MAX_ATTEMPT_COUNT)).isFalse()
2929
}
30+
31+
@Test
32+
fun `토큰 갱신이 일시 실패하면 재시도한다`() {
33+
assertThat(
34+
FcmTokenSyncPolicy.shouldRetryAfterRefreshFailure(invalidGrant = false, runAttemptCount = 0),
35+
).isTrue()
36+
}
37+
38+
@Test
39+
fun `invalid_grant면 첫 시도라도 재시도하지 않는다`() {
40+
assertThat(
41+
FcmTokenSyncPolicy.shouldRetryAfterRefreshFailure(invalidGrant = true, runAttemptCount = 0),
42+
).isFalse()
43+
}
44+
45+
@Test
46+
fun `토큰 갱신 실패도 상한에 닿으면 재시도하지 않는다`() {
47+
val lastAttempt = FcmTokenSyncPolicy.MAX_ATTEMPT_COUNT - 1
48+
49+
assertThat(
50+
FcmTokenSyncPolicy.shouldRetryAfterRefreshFailure(invalidGrant = false, runAttemptCount = lastAttempt),
51+
).isFalse()
52+
}
3053
}

0 commit comments

Comments
 (0)