Skip to content

Commit 017f0e3

Browse files
committed
[BOOK-72] refactor: remove concurrency remnants
- Removed async/await-based request implementation - Replaced with Combine publishers and conditional retry logic - Introduced retryIf operator for controlled retry on specific error
1 parent 15fb5da commit 017f0e3

9 files changed

Lines changed: 82 additions & 45 deletions

File tree

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// Copyright © 2025 Booket. All rights reserved
22

3+
import Combine
4+
35
public protocol NetworkProvider {
46
@discardableResult
57
func request<T: Decodable>(
68
target: RequestTarget,
7-
type: T.Type,
8-
isRetry: Bool
9-
) async throws -> T
9+
type: T.Type
10+
) -> AnyPublisher<T, Error>
1011
}

src/Projects/BKData/Sources/Interface/Storage/TokenProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
public protocol TokenProvider {
44
var accessToken: String? { get }
5-
func refreshIfNeeded() async throws
5+
func refreshIfNeeded()
66
}

src/Projects/BKNetwork/Sources/Extension/Data+.swift

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,8 @@ import Foundation
44

55
extension Data {
66
func decode<T: Decodable>(
7-
to type: T.Type,
8-
with response: URLResponse
7+
to type: T.Type
98
) throws -> T {
10-
guard let httpResponse = response as? HTTPURLResponse else {
11-
throw NetworkError.invalidResponse
12-
}
13-
14-
switch httpResponse.statusCode {
15-
case 200...299:
16-
break
17-
case 401:
18-
throw NetworkError.unauthorized
19-
case 500...599:
20-
throw NetworkError.internalServerError
21-
default:
22-
throw NetworkError.invalidResponse
23-
}
24-
259
return try JSONDecoder().decode(T.self, from: self)
2610
}
2711
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright © 2025 Booket. All rights reserved
2+
3+
import Combine
4+
5+
extension Publisher {
6+
func retryIf(
7+
_ shouldRetry: @escaping (Failure) -> Bool,
8+
maxRetries: Int
9+
) -> AnyPublisher<Output, Failure> {
10+
self.catch { error -> AnyPublisher<Output, Failure> in
11+
guard maxRetries > 0, shouldRetry(error) else {
12+
return Fail(error: error).eraseToAnyPublisher()
13+
}
14+
return self
15+
.retryIf(shouldRetry, maxRetries: maxRetries - 1)
16+
.eraseToAnyPublisher()
17+
}
18+
.eraseToAnyPublisher()
19+
}
20+
}

src/Projects/BKNetwork/Sources/Helper/AuthInterceptor.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,23 @@ public struct AuthInterceptor {
1010
self.tokenProvider = tokenProvider
1111
}
1212

13-
func adapt(_ request: URLRequest) throws -> URLRequest {
13+
func adapt(_ request: URLRequest) -> URLRequest {
1414
guard let token = tokenProvider.accessToken else { return request }
1515

1616
var adapted = request
1717
adapted.addAuthorization(token)
1818
return adapted
1919
}
2020

21-
func retryIfNeeded(_ response: URLResponse?, _ data: Data) async throws -> Bool {
22-
guard let httpResponse = response as? HTTPURLResponse else { return false }
21+
func retryIfNeeded(
22+
_ response: URLResponse,
23+
_ data: Data
24+
) throws {
25+
let httpResponse = try response.asHTTP
26+
.orThrow(NetworkError.invalidResponse)
2327
if httpResponse.statusCode == 401 {
24-
try await tokenProvider.refreshIfNeeded()
25-
return true
28+
tokenProvider.refreshIfNeeded()
29+
throw RetryTrigger()
2630
}
27-
return false
2831
}
2932
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
// Copyright © 2025 Booket. All rights reserved
22

3+
import Combine
34
import Foundation
45

56
public protocol NetworkRequestable {
6-
func data(for request: URLRequest) async throws -> (Data, URLResponse)
7+
func data(for request: URLRequest) -> AnyPublisher<(Data, URLResponse), Error>
8+
}
9+
10+
extension URLSession: NetworkRequestable {
11+
public func data(for request: URLRequest) -> AnyPublisher<(Data, URLResponse), Error> {
12+
self.dataTaskPublisher(for: request)
13+
.map { ($0.data, $0.response) }
14+
.mapError { $0 as Error }
15+
.eraseToAnyPublisher()
16+
}
717
}
Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright © 2025 Booket. All rights reserved
22

33
import BKData
4+
import Combine
45
import Foundation
56

67
public struct DefaultNetworkProvider: NetworkProvider {
@@ -13,11 +14,21 @@ public struct DefaultNetworkProvider: NetworkProvider {
1314
@discardableResult
1415
public func request<T: Decodable>(
1516
target: RequestTarget,
16-
type: T.Type,
17-
isRetry: Bool = false
18-
) async throws -> T {
19-
let request = try target.makeURLRequest()
20-
let (data, response) = try await requestor.data(for: request)
21-
return try data.decode(to: type, with: response)
17+
type: T.Type
18+
) -> AnyPublisher<T, Error> {
19+
do {
20+
let request = try target.makeURLRequest()
21+
return requestor.data(for: request)
22+
.tryMap { data, response in
23+
try response.asHTTP
24+
.orThrow(NetworkError.invalidResponse)
25+
.validate()
26+
return try data.decode(to: type)
27+
}
28+
.eraseToAnyPublisher()
29+
} catch {
30+
return Fail(error: error)
31+
.eraseToAnyPublisher()
32+
}
2233
}
2334
}
Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright © 2025 Booket. All rights reserved
22

33
import BKData
4+
import Combine
45
import Foundation
56

67
public struct OAuthNetworkProvider: NetworkProvider {
@@ -15,16 +16,23 @@ public struct OAuthNetworkProvider: NetworkProvider {
1516
@discardableResult
1617
public func request<T: Decodable>(
1718
target: RequestTarget,
18-
type: T.Type,
19-
isRetry: Bool = false
20-
) async throws -> T {
21-
let adaptedRequest = try interceptor.adapt(target.makeURLRequest())
22-
let (data, response) = try await requestor.data(for: adaptedRequest)
23-
24-
if try await interceptor.retryIfNeeded(response, data), isRetry == false {
25-
return try await request(target: target, type: type, isRetry: true)
19+
type: T.Type
20+
) -> AnyPublisher<T, Error> {
21+
do {
22+
let adaptedRequest = try interceptor.adapt(target.makeURLRequest())
23+
return requestor.data(for: adaptedRequest)
24+
.tryMap { data, response in
25+
try response.asHTTP
26+
.orThrow(NetworkError.invalidResponse)
27+
.validate()
28+
try interceptor.retryIfNeeded(response, data)
29+
return try data.decode(to: type)
30+
}
31+
.retryIf({ $0 is RetryTrigger }, maxRetries: 1)
32+
.eraseToAnyPublisher()
33+
} catch {
34+
return Fail(error: error)
35+
.eraseToAnyPublisher()
2636
}
27-
28-
return try data.decode(to: type, with: response)
2937
}
3038
}

src/Projects/BKStorage/Sources/TokenStorage/KeychainTokenProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public struct KeychainTokenProvider: TokenProvider {
1616
try? storage.load(for: accessTokenKey)
1717
}
1818

19-
public func refreshIfNeeded() async throws {
19+
public func refreshIfNeeded() {
2020
// TODO: Refresh 기능 구현
2121
}
2222
}

0 commit comments

Comments
 (0)