Skip to content

Commit 6f9b3bf

Browse files
authored
[Feat] NetworkService 플러그인 구현
* feat: 네트워크 플러그인 프로토콜 구현 * feat: 네트워크 플러그인 구현 * feat: networkService에 플러그인 적용 * refactor: 코드래빗 리뷰 반영 * refactor: 코드 리뷰 반영
1 parent 52339a4 commit 6f9b3bf

6 files changed

Lines changed: 197 additions & 7 deletions

File tree

Projects/DataSource/Sources/NetworkService/NetworkError.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
// Created by 최정인 on 6/23/25.
66
//
77

8-
enum NetworkError: Error, CustomStringConvertible {
8+
enum NetworkError: Error, CustomStringConvertible, Comparable {
99
case invalidURL
1010
case invalidResponse
1111
case invalidStatusCode(statusCode: Int)
1212
case emptyData
1313
case decodingError
14+
case needRetry
15+
case noRetry
16+
case unknown(description: String)
1417

1518
var description: String {
1619
switch self {
@@ -24,6 +27,12 @@ enum NetworkError: Error, CustomStringConvertible {
2427
return "emptyData"
2528
case .decodingError:
2629
return "decodingError"
30+
case .needRetry:
31+
return "request need retry"
32+
case .noRetry:
33+
return "request don't need retry"
34+
case .unknown(let description):
35+
return "unknown error: \(description)"
2736
}
2837
}
2938
}

Projects/DataSource/Sources/NetworkService/NetworkService.swift

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,70 @@ import Shared
1111
final class NetworkService {
1212
static let shared = NetworkService()
1313
private let decoder = JSONDecoder()
14+
private let plugins: [NetworkPlugin]
15+
private let maxRetryCount = 1
1416

15-
private init() { }
17+
private init() {
18+
plugins = [
19+
TokenInjectionPlugin(),
20+
RefreshTokenPlugin(),
21+
RetryPlugin()]
22+
}
23+
24+
func request<T: Decodable>(
25+
endpoint: Endpoint,
26+
type: T.Type,
27+
withPlugins: Bool = true
28+
) async throws -> T? {
29+
var retryCount = 0
1630

17-
func request<T: Decodable>(endpoint: Endpoint, type: T.Type) async throws -> T? {
31+
while true {
32+
do {
33+
return try await performRequest(
34+
endpoint: endpoint,
35+
type: type,
36+
withPlugins: withPlugins)
37+
} catch let error as NetworkError {
38+
guard
39+
error == .needRetry,
40+
retryCount < maxRetryCount
41+
else { throw error }
42+
43+
retryCount += 1
44+
continue
45+
}
46+
}
47+
}
48+
49+
private func performRequest<T: Decodable>(
50+
endpoint: Endpoint,
51+
type: T.Type,
52+
withPlugins: Bool = true
53+
) async throws -> T? {
1854
var request = try endpoint.makeURLRequest()
19-
if endpoint.isAuthorized {
20-
let accessToken = try TokenManager.shared.loadToken(tokenType: .accessToken)
21-
request.headers["Authorization"] = "Bearer \(accessToken)"
55+
56+
if withPlugins {
57+
for plugin in plugins {
58+
request = try await plugin.willSend(request: request, endpoint: endpoint)
59+
}
2260
}
61+
2362
let (data, response) = try await URLSession.shared.data(for: request)
2463

64+
// TODO: - 로깅 로직 수정
65+
if let httpResponse = response as? HTTPURLResponse {
66+
BitnagilLogger.log(logType: .info, message: "응답 코드: \(httpResponse.statusCode)")
67+
}
68+
69+
if withPlugins {
70+
for plugin in plugins {
71+
try await plugin.didReceive(
72+
response: response,
73+
data: data,
74+
endpoint: endpoint)
75+
}
76+
}
77+
2578
guard let httpResponse = response as? HTTPURLResponse
2679
else { throw NetworkError.invalidResponse }
2780

@@ -39,7 +92,7 @@ final class NetworkService {
3992

4093
guard let responseDTO = baseResponse.data
4194
else { return nil }
42-
95+
4396
return responseDTO
4497
} catch {
4598
throw NetworkError.decodingError
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// RefreshTokenPlugin.swift
3+
// DataSource
4+
//
5+
// Created by 이동현 on 7/29/25.
6+
//
7+
8+
import Foundation
9+
import Shared
10+
11+
struct RefreshTokenPlugin: NetworkPlugin {
12+
func willSend(request: URLRequest, endpoint: Endpoint) async throws -> URLRequest {
13+
return request
14+
}
15+
16+
func didReceive(response: URLResponse, data: Data?, endpoint: Endpoint) async throws {
17+
guard
18+
let httpResponse = response as? HTTPURLResponse,
19+
httpResponse.statusCode == 401
20+
else { return }
21+
22+
23+
try await refreshAccessToken()
24+
}
25+
26+
private func refreshAccessToken() async throws {
27+
do {
28+
let tokenManager = TokenManager.shared
29+
30+
let refreshToken = try tokenManager.loadToken(tokenType: .refreshToken)
31+
let endpoint = AuthEndpoint.reissue(refreshToken: refreshToken)
32+
33+
guard let tokenResponse = try await NetworkService.shared.request(
34+
endpoint: endpoint,
35+
type: TokenResponseDTO.self,
36+
withPlugins: false)
37+
else { throw NetworkError.unknown(description: "토큰 갱신 실패") }
38+
39+
try tokenManager.saveToken(token: tokenResponse.accessToken, tokenType: .accessToken)
40+
try tokenManager.saveToken(token: tokenResponse.refreshToken, tokenType: .refreshToken)
41+
42+
BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(tokenResponse.accessToken)")
43+
BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(tokenResponse.refreshToken)")
44+
} catch {
45+
BitnagilLogger.log(logType: .error, message: "\(error.localizedDescription)")
46+
throw error
47+
}
48+
}
49+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// RetryPlugin.swift
3+
// DataSource
4+
//
5+
// Created by 이동현 on 7/29/25.
6+
//
7+
8+
import Foundation
9+
10+
struct RetryPlugin: NetworkPlugin {
11+
func willSend(request: URLRequest, endpoint: Endpoint) async throws -> URLRequest {
12+
return request
13+
}
14+
15+
func didReceive(
16+
response: URLResponse,
17+
data: Data?,
18+
endpoint: Endpoint
19+
) async throws {
20+
guard let httpResponse = response as? HTTPURLResponse else { return }
21+
22+
switch httpResponse.statusCode {
23+
case 200..<300:
24+
return
25+
case 401: // TODO: - 재전송 정책 필요시 조정하기!!
26+
throw NetworkError.needRetry
27+
default:
28+
throw NetworkError.noRetry
29+
}
30+
}
31+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// TokenInjectionPlugin.swift
3+
// DataSource
4+
//
5+
// Created by 이동현 on 7/29/25.
6+
//
7+
8+
import Foundation
9+
10+
struct TokenInjectionPlugin: NetworkPlugin {
11+
func willSend(request: URLRequest, endpoint: any Endpoint) async throws -> URLRequest {
12+
guard endpoint.isAuthorized else { return request }
13+
14+
var newRequest = request
15+
let accessToken = try TokenManager.shared.loadToken(tokenType: .accessToken)
16+
newRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
17+
return newRequest
18+
}
19+
20+
func didReceive(response: URLResponse, data: Data?, endpoint: any Endpoint) async throws {}
21+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// NetworkPlugin.swift
3+
// DataSource
4+
//
5+
// Created by 이동현 on 7/29/25.
6+
//
7+
8+
import Foundation
9+
10+
protocol NetworkPlugin {
11+
/// 네트워크 요청 전처리를 진행합니다.
12+
/// - Parameters:
13+
/// - request: 요청할 request 객체
14+
/// - endpoint: 요청할 엔드포인트
15+
/// - Returns: URLRequest 객체
16+
func willSend(request: URLRequest, endpoint: Endpoint) async throws -> URLRequest
17+
18+
/// 네트워크 응답 후처리를 진행합니다.
19+
/// - Parameters:
20+
/// - response: URLResponse 객체
21+
/// - data: 통신 후 받은 데이터
22+
/// - endpoint: 요청한 endpoint
23+
func didReceive(
24+
response: URLResponse,
25+
data: Data?,
26+
endpoint: Endpoint) async throws
27+
}

0 commit comments

Comments
 (0)