Skip to content

Commit 1fb4162

Browse files
committed
refactor: Firebase에 대해 Sendable 래퍼 구현 및 적용
1 parent 8a0e246 commit 1fb4162

6 files changed

Lines changed: 178 additions & 64 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// FirebaseDependency.swift
3+
// DevLogInfra
4+
//
5+
// Created by opfic on 6/5/26.
6+
//
7+
8+
import FirebaseAuth
9+
import FirebaseFirestore
10+
import FirebaseFunctions
11+
import FirebaseMessaging
12+
13+
struct FirebaseDependency<Value>: @unchecked Sendable {
14+
private let value: Value
15+
16+
init(value: Value) {
17+
self.value = value
18+
}
19+
}
20+
21+
extension FirebaseDependency where Value == Firestore {
22+
func document(_ path: String) -> DocumentReference {
23+
value.document(path)
24+
}
25+
}
26+
27+
extension FirebaseDependency where Value == Functions {
28+
func httpsCallable(_ name: some RawRepresentable<String>) -> HTTPSCallable {
29+
value.httpsCallable(name)
30+
}
31+
}
32+
33+
extension FirebaseDependency where Value == Messaging {
34+
func token() async throws -> String {
35+
try await value.token()
36+
}
37+
38+
func deleteToken() async throws {
39+
try await value.deleteToken()
40+
}
41+
}
42+
43+
extension FirebaseDependency where Value == AuthStateDidChangeListenerHandle {
44+
func removeAuthStateDidChangeListener() {
45+
Auth.auth().removeStateDidChangeListener(value)
46+
}
47+
}

Application/DevLogInfra/Sources/Service/AuthServiceImpl.swift

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@ import DevLogCore
1313
import DevLogData
1414

1515
final class AuthServiceImpl: AuthService {
16-
private let store = Firestore.firestore()
17-
private let messaging = Messaging.messaging()
16+
private let store = FirebaseDependency(value: Firestore.firestore())
17+
private let messaging = FirebaseDependency(value: Messaging.messaging())
1818
private let logger = Logger(category: "AuthServiceImpl")
19-
private let subject = CurrentValueSubject<Bool, Never>(Auth.auth().currentUser != nil)
20-
private var handler: AuthStateDidChangeListenerHandle?
21-
private var isCompletingSignIn = false
19+
private let authStatePublisher: AuthStatePublisher
2220

2321
var uid: String? {
2422
Auth.auth().currentUser?.uid
@@ -37,45 +35,26 @@ final class AuthServiceImpl: AuthService {
3735
}
3836

3937
init() {
40-
handler = Auth.auth().addStateDidChangeListener { [weak self] _, user in
41-
guard let self else { return }
42-
let signedIn = user != nil
43-
self.logger.info("Firebase auth state changed. signedIn: \(signedIn)")
44-
45-
if signedIn && self.isCompletingSignIn {
46-
self.logger.info("Delaying signed-in publication until user bootstrap finishes")
47-
return
48-
}
49-
50-
self.subject.send(signedIn)
51-
}
52-
}
53-
54-
deinit {
55-
guard let handler else { return }
56-
Auth.auth().removeStateDidChangeListener(handler)
38+
authStatePublisher = AuthStatePublisher(logger: logger)
5739
}
5840

5941
func observeSignedIn() -> AnyPublisher<Bool, Never> {
60-
subject.eraseToAnyPublisher()
42+
authStatePublisher.observeSignedIn()
6143
}
6244

6345
func beginSignIn() {
6446
logger.info("Beginning sign-in bootstrap")
65-
isCompletingSignIn = true
66-
subject.send(false)
47+
authStatePublisher.beginSignIn()
6748
}
6849

6950
func completeSignIn() {
7051
logger.info("Completing sign-in bootstrap")
71-
isCompletingSignIn = false
72-
subject.send(Auth.auth().currentUser != nil)
52+
authStatePublisher.completeSignIn()
7353
}
7454

7555
func cancelSignIn() {
7656
logger.info("Cancelling sign-in bootstrap")
77-
isCompletingSignIn = false
78-
subject.send(Auth.auth().currentUser != nil)
57+
authStatePublisher.cancelSignIn()
7958
}
8059

8160
func getProviderID() async throws -> String? {
@@ -134,3 +113,68 @@ final class AuthServiceImpl: AuthService {
134113
}
135114

136115
}
116+
117+
private final class AuthStatePublisher: @unchecked Sendable {
118+
private let logger: Logger
119+
private let subject: CurrentValueSubject<Bool, Never>
120+
private let lock = NSLock()
121+
private var handler: FirebaseDependency<AuthStateDidChangeListenerHandle>?
122+
private var isCompletingSignIn = false
123+
124+
init(logger: Logger) {
125+
self.logger = logger
126+
self.subject = CurrentValueSubject<Bool, Never>(Auth.auth().currentUser != nil)
127+
self.handler = FirebaseDependency(
128+
value: Auth.auth().addStateDidChangeListener { [weak self] _, user in
129+
self?.handleAuthStateChange(user)
130+
}
131+
)
132+
}
133+
134+
deinit {
135+
guard let handler else { return }
136+
handler.removeAuthStateDidChangeListener()
137+
}
138+
139+
func observeSignedIn() -> AnyPublisher<Bool, Never> {
140+
lock.lock()
141+
defer { lock.unlock() }
142+
return subject.eraseToAnyPublisher()
143+
}
144+
145+
func beginSignIn() {
146+
lock.lock()
147+
isCompletingSignIn = true
148+
subject.send(false)
149+
lock.unlock()
150+
}
151+
152+
func completeSignIn() {
153+
lock.lock()
154+
isCompletingSignIn = false
155+
subject.send(Auth.auth().currentUser != nil)
156+
lock.unlock()
157+
}
158+
159+
func cancelSignIn() {
160+
lock.lock()
161+
isCompletingSignIn = false
162+
subject.send(Auth.auth().currentUser != nil)
163+
lock.unlock()
164+
}
165+
166+
private func handleAuthStateChange(_ user: User?) {
167+
lock.lock()
168+
defer { lock.unlock() }
169+
170+
let signedIn = user != nil
171+
logger.info("Firebase auth state changed. signedIn: \(signedIn)")
172+
173+
if signedIn && isCompletingSignIn {
174+
logger.info("Delaying signed-in publication until user bootstrap finishes")
175+
return
176+
}
177+
178+
subject.send(signedIn)
179+
}
180+
}

Application/DevLogInfra/Sources/Service/SocialLogin/AppleAuthenticationServiceImpl.swift

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,11 @@ final class AppleAuthenticationServiceImpl: AuthenticationService {
2323
case revokeAppleAccessToken
2424
}
2525

26-
private var appleSignInDelegate: AppleSignInDelegate?
27-
private var appleSignInContinuation: CheckedContinuation<ASAuthorization, Error>?
28-
private let store = Firestore.firestore()
29-
private let functions = Functions.functions(region: "asia-northeast3")
30-
private let messaging = Messaging.messaging()
26+
private let session = AppleSignInSession()
27+
private let store = FirebaseDependency(value: Firestore.firestore())
28+
private let functions = FirebaseDependency(value: Functions.functions(region: "asia-northeast3"))
29+
private let messaging = FirebaseDependency(value: Messaging.messaging())
3130
private var user: User? { Auth.auth().currentUser }
32-
private let providerID = AuthProviderID.apple
3331
private let logger = Logger(category: "AppleAuthService")
3432

3533
func signIn() async throws -> AuthDataResponse {
@@ -81,9 +79,9 @@ final class AppleAuthenticationServiceImpl: AuthenticationService {
8179
try await changeRequest.commitChanges()
8280

8381
// FirebaseAuth 계정에 Apple ID 연결
84-
if !result.user.providerData.contains(where: { $0.providerID == providerID.rawValue }) {
82+
if !result.user.providerData.contains(where: { $0.providerID == AuthProviderID.apple.rawValue }) {
8583
let appleCredential = OAuthProvider.credential(
86-
providerID: providerID,
84+
providerID: AuthProviderID.apple,
8785
idToken: idTokenString,
8886
rawNonce: nonce
8987
)
@@ -151,7 +149,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService {
151149
}
152150

153151
let appleCredential = OAuthProvider.credential(
154-
providerID: providerID,
152+
providerID: AuthProviderID.apple,
155153
idToken: idTokenString,
156154
rawNonce: nonce
157155
)
@@ -187,7 +185,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService {
187185
}
188186

189187
logger.info("Starting Firebase Apple provider unlink. uid: \(uid)")
190-
_ = try await user?.unlink(fromProvider: providerID.rawValue)
188+
_ = try await user?.unlink(fromProvider: AuthProviderID.apple.rawValue)
191189
} catch {
192190
logger.error("Failed to unlink Apple account", error: error)
193191
throw error
@@ -197,7 +195,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService {
197195
// Apple 인증 메서드
198196
@MainActor
199197
func authenticateWithAppleAsync() async throws -> AppleAuthResponse {
200-
guard appleSignInDelegate == nil, appleSignInContinuation == nil else {
198+
guard session.canStartSignIn else {
201199
throw SocialLoginError.authenticationAlreadyInProgress
202200
}
203201

@@ -213,13 +211,11 @@ final class AppleAuthenticationServiceImpl: AuthenticationService {
213211
let controller = ASAuthorizationController(authorizationRequests: [request])
214212

215213
let authorization = try await withCheckedThrowingContinuation { continuation in
216-
let delegate = AppleSignInDelegate { [weak self] result in
214+
let delegate = session.start(continuation: continuation) { [weak self] result in
217215
self?.completeAppleSignIn(with: result)
218216
}
219-
self.appleSignInDelegate = delegate
220-
self.appleSignInContinuation = continuation
221-
controller.delegate = self.appleSignInDelegate
222-
controller.presentationContextProvider = self.appleSignInDelegate
217+
controller.delegate = delegate
218+
controller.presentationContextProvider = delegate
223219
controller.performRequests()
224220
}
225221

@@ -241,17 +237,7 @@ final class AppleAuthenticationServiceImpl: AuthenticationService {
241237

242238
@MainActor
243239
private func completeAppleSignIn(with result: Result<ASAuthorization, Error>) {
244-
guard let continuation = appleSignInContinuation else { return }
245-
246-
appleSignInContinuation = nil
247-
appleSignInDelegate = nil
248-
249-
switch result {
250-
case .success(let authorization):
251-
continuation.resume(returning: authorization)
252-
case .failure(let error):
253-
continuation.resume(throwing: error)
254-
}
240+
session.complete(with: result)
255241
}
256242

257243
// Apple CustomToken 발급 메서드
@@ -313,3 +299,41 @@ final class AppleAuthenticationServiceImpl: AuthenticationService {
313299
_ = try await revokeFunction.call(["token": token])
314300
}
315301
}
302+
303+
private final class AppleSignInSession: @unchecked Sendable {
304+
@MainActor
305+
private var delegate: AppleSignInDelegate?
306+
@MainActor
307+
private var continuation: CheckedContinuation<ASAuthorization, Error>?
308+
309+
@MainActor
310+
var canStartSignIn: Bool {
311+
delegate == nil && continuation == nil
312+
}
313+
314+
@MainActor
315+
func start(
316+
continuation: CheckedContinuation<ASAuthorization, Error>,
317+
completion: @escaping @MainActor (Result<ASAuthorization, Error>) -> Void
318+
) -> AppleSignInDelegate {
319+
let delegate = AppleSignInDelegate(finish: completion)
320+
self.delegate = delegate
321+
self.continuation = continuation
322+
return delegate
323+
}
324+
325+
@MainActor
326+
func complete(with result: Result<ASAuthorization, Error>) {
327+
guard let continuation else { return }
328+
329+
self.continuation = nil
330+
self.delegate = nil
331+
332+
switch result {
333+
case .success(let authorization):
334+
continuation.resume(returning: authorization)
335+
case .failure(let error):
336+
continuation.resume(throwing: error)
337+
}
338+
}
339+
}

Application/DevLogInfra/Sources/Service/SocialLogin/GithubAuthenticationServiceImpl.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,10 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService {
2626
static let acceptHeader = "application/vnd.github.v3+json"
2727
}
2828

29-
private let store = Firestore.firestore()
30-
private let functions = Functions.functions(region: "asia-northeast3")
31-
private let messaging = Messaging.messaging()
29+
private let store = FirebaseDependency(value: Firestore.firestore())
30+
private let functions = FirebaseDependency(value: Functions.functions(region: "asia-northeast3"))
31+
private let messaging = FirebaseDependency(value: Messaging.messaging())
3232
private var user: User? { Auth.auth().currentUser }
33-
private let providerID = AuthProviderID.gitHub
3433
private let provider = TopViewControllerProvider()
3534
private let logger = Logger(category: "GithubAuthService")
3635
private let gitHubApiClient = NXAPIClient(
@@ -161,7 +160,7 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService {
161160
try await tokensRef.updateData(["githubAccessToken": FieldValue.delete()])
162161

163162
logger.info("Starting Firebase GitHub provider unlink. uid: \(uid)")
164-
_ = try await user?.unlink(fromProvider: providerID.rawValue)
163+
_ = try await user?.unlink(fromProvider: AuthProviderID.gitHub.rawValue)
165164
} catch {
166165
logger.error("Failed to unlink GitHub account", error: error)
167166
throw error

Application/DevLogInfra/Sources/Service/SocialLogin/GoogleAuthenticationServiceImpl.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import DevLogCore
1414
import DevLogData
1515

1616
final class GoogleAuthenticationServiceImpl: AuthenticationService {
17-
private let store = Firestore.firestore()
18-
private let messaging = Messaging.messaging()
17+
private let store = FirebaseDependency(value: Firestore.firestore())
18+
private let messaging = FirebaseDependency(value: Messaging.messaging())
1919
private var user: User? { Auth.auth().currentUser }
2020
private let provider = TopViewControllerProvider()
2121
private let logger = Logger(category: "GoogleAuthService")

Application/DevLogInfra/Sources/Service/UserServiceImpl.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import DevLogCore
1111
import DevLogData
1212

1313
final class UserServiceImpl: UserService {
14-
private let store = Firestore.firestore()
14+
private let store = FirebaseDependency(value: Firestore.firestore())
1515
private let logger = Logger(category: "UserServiceImpl")
1616

1717
// 유저를 Firestore에 저장 및 업데이트

0 commit comments

Comments
 (0)