Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Climeet-iOS/Climeet-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Climeet-iOS/Preview Content\"";
DEVELOPMENT_TEAM = 7MJ69FU8BU;
DEVELOPMENT_TEAM = L56LNTR7PZ;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
Expand Down Expand Up @@ -594,7 +594,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Climeet-iOS/Preview Content\"";
DEVELOPMENT_TEAM = 7MJ69FU8BU;
DEVELOPMENT_TEAM = L56LNTR7PZ;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
Expand Down
5 changes: 0 additions & 5 deletions Climeet-iOS/Climeet-iOS/App/Climeet_iOSApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ struct ClimeetiOSApp: App {
KeyChain.shared.refreshToken = Env.MASTER_TOKEN // 테스트 값 설정
// print(KeyChain.shared.refreshToken) // 값 읽어오기
// KeyChain.shared.deleteRefreshToken() // 리프레시 토큰 초기화(테스트메서드)
configureAPIClient()
applyGlobalNavigationTitleAttributes()
}

Expand All @@ -34,8 +33,4 @@ struct ClimeetiOSApp: App {
// Font.climeetFontTitle4 해당함
]
}

func configureAPIClient() {
APIClient.shared.configure(tokenRefresher: TokenRefresher())
}
}
2 changes: 1 addition & 1 deletion Climeet-iOS/Climeet-iOS/Data/Client/ClimberClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ extension ClimberClient: DependencyKey {
},
login: { param in
let endPoint = ClimberEndPoint.login(param)
return try await APIClient.shared.request(endPoint, decode: SignResponse.self)
return try await APIClient(session: .default, tokenRefresher: nil).request(endPoint, decode: SignResponse.self)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

login API는 token 없이 network해야하므로 기존 shared(공통)으로 빼놓은 경우랑 달리 해야합니다.

}
)
}
Expand Down
14 changes: 14 additions & 0 deletions Climeet-iOS/Climeet-iOS/Data/Common/APIClient+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// APIClient+Extension.swift
// Climeet-iOS
//
// Created by 송형욱 on 12/18/24.
//

import Foundation
import Alamofire
import NetworkKit

extension APIClient {
static let shared: APIClient = APIClient(session: .default, tokenRefresher: TokenRefresher())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shared 싱글톤 쓰는것보다 더 좋은 방법이 있을지 고민인데 같이 생각해보면 좋을것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 네트워크 객체 특성상 싱글턴 패턴이 적절해 보입니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

세션 객체는 하나만 유지하는 게 좋을 것 같아서요~

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래 추가한 내용처럼 session 설정을 달리할 필요가 있을때가 있어요!
예를 들어 이미지 혹은 동영상을 왕창 보내거나 받거나 할때 session의 timeout 설정을 다르게 할때도 있고요!
오늘 같은 경우엔 저는 login API 를 보내는데 실제 token이 필요하지않았고 그 token이 없을때 Interceptor를 괜히
설정할 필요가 없었고요!
이 외에도 더 있을 것 같은데... 지금은 생각이 안나네요ㅜ

세션 객체는 하나만 유지하는 게 좋을 것 같아서요~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇다면 싱글턴 API 클라이언트가 필요한 세션들을 가지고 있고, request 형태에 따라 적절한 세션을 통해 요청을 수행하는 방식은 어떤가요?
필요에 따라 다른 세션을 세션을 주입하는 의도는 이해했으나, API 카테고리마다 모두 다른 클라이언트를 가지고 있는 현재 구조에서는
필요한 API를 요청할 때 마다 서로 다른 세션이 중복 생성되는 상황이 발생할 수 있다고 생각합니다~~
혹시 더 좋은 방법이 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2024-12-20 at 5 24 14 PM

요청마다 세션을 생성하는 것이 안티패턴인 이유도 함께 첨부드립니다!
애플 개발자 포럼

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

외부에서 Session 주입할 때 유의하여 사용하고 APIClient는 보통 싱글톤을 사용하되 예외케이스 시 주입하여 사용하는 방식으로 수정하였습니다!

}
38 changes: 13 additions & 25 deletions NetworkKit/Sources/NetworkKit/Kit/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,21 @@ import Foundation
import Alamofire

public final class APIClient: APIProtocol, @unchecked Sendable {
private let session: Session

public static let shared = APIClient()
private var tokenRefresher: TokenRefreshable?
private var isConfigured = false

private init() { }

private lazy var session: Session = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

session memory 가 해제될 가능성이 있는건 인지하고 있었지만
lazy var로 사용시 생성되는 걸 이용하여 해결할 거라 생각하여..
안일하게 작업하였습니다. ㅜ

let configuration = URLSessionConfiguration.af.default
configuration.waitsForConnectivity = true
configuration.timeoutIntervalForRequest = 60 // seconds that a task will wait for data to arrive
configuration.timeoutIntervalForResource = 300 // seconds for whole resource request to complete ,.
return Session(
configuration: configuration,
interceptor: tokenRefresher.map { APIInterceptor(tokenRefresher: $0) },
eventMonitors: [APILogger()]
)
}()

public func configure(tokenRefresher: TokenRefreshable) {
guard !isConfigured else {
print("APIClient는 이미 초기화 되었습니다.")
return
public init(session: Session, tokenRefresher: TokenRefreshable?) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

token 없이 network할 경우 retry method가 필요가 없습니다!
그래서 tokenRefresher 없이 APIClient를 사용할 경우를 위해 작업하였습니다.

if let tokenRefresher {
self.session = Session(
configuration: session.sessionConfiguration,
interceptor: APIInterceptor(tokenRefresher: tokenRefresher),
eventMonitors: [APILogger()]
)
} else {
self.session = Session(
configuration: session.sessionConfiguration,
eventMonitors: [APILogger()]
)
}

self.tokenRefresher = tokenRefresher
self.isConfigured = true
}

public func request<T: Decodable>(_ endpoint: Endpoint, decode: T.Type) async throws -> T {
Expand Down
12 changes: 10 additions & 2 deletions NetworkKit/Sources/NetworkKit/Kit/APIInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ final class APIInterceptor: RequestInterceptor {
let token = tokenRefresher.readToken()

guard !token.isEmpty else {
completion(.failure(APIError(errorCode: "401 Token Error", message: "Token Missing")))
completion(.failure(APIError(
statusCode: 401,
errorCode: "401 Token Error",
message: "Token Missing"
)))
return
}

Expand All @@ -34,7 +38,11 @@ final class APIInterceptor: RequestInterceptor {
}

func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping @Sendable (RetryResult) -> Void) {

guard let response = request.task?.response as? HTTPURLResponse,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트해보니 Token이 없으면 무조건 retry를 타고 무조건 retry API를 돌리는건 비효율적이라
판단하여 추가하였습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다~
불필요한 세션 발생 방지를 위해 APIClient는 싱글턴으로 유지하면 좋을 것 같은데요, request 자체에 토큰 관련 검증 로직을 넣는 방식은 어떨까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request 자체에 토큰 관련 검증 로직이 무엇일까요?
(현재 tokenRefresher, interceptor 그리고 urlRequest 에서 token이 있는지 없는지 체크하는 로직이 있는데..)
싱글턴으로 유지하면서 session 설정을 다르게 할 수 있는 방법 알려주시면 적용해보겠습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러네요 request 자체에서는 할 수 없겠군요...
위에서 말씀드린것처럼 서로 다른 세션을 싱글턴 내부에 두고
request 메소드를 다르게 해 재사용하는 방식은 어떨지 궁금합니다~!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

싱글톤 내부에서 세션을 여러개 사용하는 것이 아닌 싱글톤을 여러개 생성하는 방식으로 회의 때 결정하였습니다.

https://www.notion.so/climbers/24-12-21-iOS-27-e5ff159050534540b6575120e13d5d19?pvs=4

response.statusCode == 401 else {
completion(.doNotRetryWithError(error))
return
}
let retryLimit = 3
guard request.retryCount < retryLimit else {
completion(.doNotRetry)
Expand Down
22 changes: 15 additions & 7 deletions NetworkKit/Sources/NetworkKit/Kit/APILogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ struct APILogger: EventMonitor {

func requestDidFinish(_ request: Request) {
#if DEBUG
print("🚀 NETWORK Reqeust LOG")
print("🚀 NETWORK Request LOG")
print(request.description)

print(
Expand All @@ -22,12 +22,20 @@ struct APILogger: EventMonitor {
func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
#if DEBUG
print("✅ NETWORK Response LOG")
print(
"URL: " + (request.request?.url?.absoluteString ?? "nil") + "\n"
+ "Result: " + "\(response.result)" + "\n"
+ "StatusCode: " + "\(response.response?.statusCode ?? 0)" + "\n"
+ "Data: \(response.data?.prettyJson ?? "nil")"
)
switch response.result {
case let .success(data):
print(
"URL: " + (request.request?.url?.absoluteString ?? "nil") + "\n"
+ "Result: " + "\(data)" + "\n"
+ "StatusCode: " + "\(response.response?.statusCode ?? 0)"
)
case let .failure(error):
print(
"URL: " + (request.request?.url?.absoluteString ?? "nil") + "\n"
+ "Result: " + "\(error.localizedDescription)" + "\n"
+ "StatusCode: " + "\(response.response?.statusCode ?? 0)"
)
}
#endif
}
}
Expand Down
2 changes: 1 addition & 1 deletion NetworkKit/Sources/NetworkKit/Kit/EndPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public protocol Endpoint: URLRequestConvertible {

extension Endpoint {
private var defaultHeaders: HTTPHeaders {
var headers: HTTPHeaders = []
var headers: HTTPHeaders = [.contentType("application/json")]

if let token = token {
headers.add(.authorization(bearerToken: token))
Expand Down