Skip to content

Commit c79472b

Browse files
authored
[#686] FCM token 동기화 요청을 사용자와 token 기준으로 제한한다 (#688)
fix: FCM token 동기화 중복 저장 제한
1 parent 247faad commit c79472b

3 files changed

Lines changed: 110 additions & 9 deletions

File tree

Application/App/Sources/App/Delegate/AppDelegate.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
4444
name: UIApplication.willEnterForegroundNotification,
4545
object: nil
4646
)
47+
NotificationCenter.default.addObserver(
48+
self,
49+
selector: #selector(requestAPNsRegistration),
50+
name: UIApplication.willEnterForegroundNotification,
51+
object: nil
52+
)
4753

4854
// 알림 권한 요청
4955
UNUserNotificationCenter.current().delegate = self
@@ -79,7 +85,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
7985
}
8086
}
8187

82-
func applicationWillEnterForeground(_ application: UIApplication) {
88+
@objc private func requestAPNsRegistration() {
8389
NotificationCenter.default.post(name: .didRequestAPNsRegistration, object: nil)
8490
}
8591

Application/App/Sources/App/Handler/FCMTokenSyncHandler.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ import Data
1111
import Foundation
1212

1313
final class FCMTokenSyncHandler {
14+
private struct SyncKey: Equatable {
15+
let uid: String
16+
let fcmToken: String
17+
}
18+
1419
private let authService: AuthService
1520
private let messagingService: PushMessagingService
1621
private let userService: UserService
1722
private let notificationCenter: NotificationCenter
1823
private let logger = Logger(category: "FCMTokenSyncHandler")
1924
private var cancellables = Set<AnyCancellable>()
25+
private var lastSyncedKey: SyncKey?
2026

2127
init(
2228
authService: AuthService,
@@ -68,7 +74,10 @@ final class FCMTokenSyncHandler {
6874

6975
private extension FCMTokenSyncHandler {
7076
func handleSessionUpdate(isSignedIn: Bool) {
71-
guard isSignedIn else { return }
77+
guard isSignedIn else {
78+
lastSyncedKey = nil
79+
return
80+
}
7281

7382
requestFCMTokenSync()
7483
}
@@ -114,11 +123,18 @@ private extension FCMTokenSyncHandler {
114123
}
115124

116125
func syncFCMTokenIfNeeded(_ fcmToken: String) async throws {
117-
guard authService.uid != nil else {
126+
guard let uid = authService.uid else {
118127
logger.info("Skipping FCM token update because no authenticated user exists")
119128
return
120129
}
121130

131+
let key = SyncKey(uid: uid, fcmToken: fcmToken)
132+
guard lastSyncedKey != key else {
133+
logger.info("Skipping FCM token update because the token was already synced")
134+
return
135+
}
136+
122137
try await userService.updateFCMToken(fcmToken)
138+
lastSyncedKey = key
123139
}
124140
}

Application/App/Tests/PushNotification/FCMTokenSyncHandlerTests.swift

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ struct FCMTokenSyncHandlerTests {
137137
_ = handler
138138
}
139139

140-
@Test("같은 사용자와 같은 FCM token도 매번 저장한다")
141-
func 같은_사용자와_같은_FCM_token도_매번_저장한다() async throws {
140+
@Test("같은 사용자와 같은 FCM token은 한 번만 저장한다")
141+
func 같은_사용자와_같은_FCM_token은_한_번만_저장한다() async throws {
142142
let notificationCenter = NotificationCenter()
143143
let messagingService = PushMessagingServiceSpy(currentFCMToken: "current-token")
144144
let userService = UserServiceSpy()
@@ -156,9 +156,8 @@ struct FCMTokenSyncHandlerTests {
156156
}
157157

158158
notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
159-
try await waitUntil {
160-
await userService.updatedFCMTokens == ["current-token", "current-token"]
161-
}
159+
try await Task.sleep(for: .milliseconds(100))
160+
#expect(await userService.updatedFCMTokens == ["current-token"])
162161

163162
_ = handler
164163
}
@@ -189,6 +188,72 @@ struct FCMTokenSyncHandlerTests {
189188
_ = handler
190189
}
191190

191+
@Test("FCM token이 바뀌면 같은 사용자도 다시 저장한다")
192+
func FCM_token이_바뀌면_같은_사용자도_다시_저장한다() async throws {
193+
let notificationCenter = NotificationCenter()
194+
let messagingService = PushMessagingServiceSpy(currentFCMToken: "first-token")
195+
let userService = UserServiceSpy()
196+
let authService = AuthServiceSpy()
197+
let handler = FCMTokenSyncHandler(
198+
authService: authService,
199+
messagingService: messagingService,
200+
userService: userService,
201+
notificationCenter: notificationCenter
202+
)
203+
204+
notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
205+
try await waitUntil { await userService.updatedFCMTokens == ["first-token"] }
206+
207+
messagingService.currentFCMToken = "second-token"
208+
notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
209+
try await waitUntil { await userService.updatedFCMTokens == ["first-token", "second-token"] }
210+
_ = handler
211+
}
212+
213+
@Test("로그아웃 후 같은 사용자로 다시 로그인하면 같은 FCM token도 다시 저장한다")
214+
func 로그아웃_후_같은_사용자로_다시_로그인하면_같은_FCM_token도_다시_저장한다() async throws {
215+
let notificationCenter = NotificationCenter()
216+
let messagingService = PushMessagingServiceSpy(currentFCMToken: "current-token")
217+
let userService = UserServiceSpy()
218+
let authService = AuthServiceSpy(uid: "user-id")
219+
let handler = FCMTokenSyncHandler(
220+
authService: authService,
221+
messagingService: messagingService,
222+
userService: userService,
223+
notificationCenter: notificationCenter
224+
)
225+
226+
notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
227+
try await waitUntil { await userService.updatedFCMTokens == ["current-token"] }
228+
229+
authService.updateSession(uid: nil)
230+
authService.updateSession(uid: "user-id")
231+
try await waitUntil { await userService.updatedFCMTokens == ["current-token", "current-token"] }
232+
_ = handler
233+
}
234+
235+
@Test("FCM token 저장에 실패하면 같은 요청을 다시 저장한다")
236+
func FCM_token_저장에_실패하면_같은_요청을_다시_저장한다() async throws {
237+
let notificationCenter = NotificationCenter()
238+
let messagingService = PushMessagingServiceSpy(currentFCMToken: "current-token")
239+
let userService = UserServiceSpy(updateError: FCMTokenSyncTestError())
240+
let authService = AuthServiceSpy()
241+
let handler = FCMTokenSyncHandler(
242+
authService: authService,
243+
messagingService: messagingService,
244+
userService: userService,
245+
notificationCenter: notificationCenter
246+
)
247+
248+
notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
249+
try await waitUntil { await userService.updatedFCMTokens == ["current-token"] }
250+
251+
await userService.setUpdateError(nil)
252+
notificationCenter.post(name: .didRequestFCMTokenSync, object: nil)
253+
try await waitUntil { await userService.updatedFCMTokens == ["current-token", "current-token"] }
254+
_ = handler
255+
}
256+
192257
@Test("로그인 세션 전이 시 현재 FCM token을 저장한다")
193258
func 로그인_세션_전이_시_현재_FCM_token을_저장한다() async throws {
194259
let notificationCenter = NotificationCenter()
@@ -214,16 +279,28 @@ struct FCMTokenSyncHandlerTests {
214279

215280
private actor UserServiceSpy: UserService {
216281
private(set) var updatedFCMTokens = [String]()
282+
private var updateError: Error?
283+
284+
init(updateError: Error? = nil) {
285+
self.updateError = updateError
286+
}
217287

218288
func upsertUser(_ response: AuthDataResponse) async throws { }
219289
func fetchUserProfile() async throws -> UserProfileResponse { fatalError() }
220290
func upsertStatusMessage(_ message: String) async throws { }
221291

222292
func updateFCMToken(_ fcmToken: String) async throws {
223293
updatedFCMTokens.append(fcmToken)
294+
if let updateError {
295+
throw updateError
296+
}
224297
}
225298

226299
func updateUserTimeZone() async throws { }
300+
301+
func setUpdateError(_ error: Error?) {
302+
updateError = error
303+
}
227304
}
228305

229306
private final class AuthServiceSpy: AuthService {
@@ -255,7 +332,7 @@ private final class AuthServiceSpy: AuthService {
255332
}
256333

257334
private final class PushMessagingServiceSpy: PushMessagingService {
258-
private let currentFCMToken: String?
335+
var currentFCMToken: String?
259336
private(set) var apnsTokens = [Data]()
260337

261338
init(currentFCMToken: String?) {
@@ -273,6 +350,8 @@ private final class PushMessagingServiceSpy: PushMessagingService {
273350
}
274351
}
275352

353+
private struct FCMTokenSyncTestError: Error { }
354+
276355
private final class NotificationObserver {
277356
private(set) var didReceiveNotification = false
278357
private var token: NSObjectProtocol?

0 commit comments

Comments
 (0)