Skip to content

Commit ac836eb

Browse files
authored
Merge pull request #52 from boostcampwm-2024/feat/#51-http-api
[FEAT/#51] :: HTTP API를 추상화합니다.
2 parents 125efd8 + 8298d54 commit ac836eb

File tree

3 files changed

+245
-0
lines changed

3 files changed

+245
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import Foundation
2+
3+
public enum APIError: Error {
4+
case unknown
5+
case custom(message: String = "알 수 없는 오류가 발생하였습니다", code: Int = 999)
6+
case timeout
7+
case decodingError(DecodingError)
8+
case badRequest // 400번대 오류
9+
case unauthorized // 401 Unauthorized
10+
case forbidden // 403 Forbidden
11+
case notFound // 404 Not Found
12+
case serverError // 500번대 오류
13+
}
14+
15+
extension APIError: LocalizedError {
16+
public var errorDescription: String? {
17+
switch self {
18+
case .unknown:
19+
return "알 수 없는 오류가 발생하였습니다."
20+
case let .custom(message, _):
21+
return message
22+
case .timeout:
23+
return "타임 아웃 에러입니다."
24+
case let .decodingError(error):
25+
return error.fullDescription
26+
case .badRequest:
27+
return "잘못된 요청입니다."
28+
case .unauthorized:
29+
return "인증 오류입니다."
30+
case .forbidden:
31+
return "권한 없음입니다."
32+
case .notFound:
33+
return "요청한 것을 찾을 수 없습니다."
34+
case .serverError:
35+
return "서버 오류입니다."
36+
}
37+
}
38+
}
39+
40+
extension Error {
41+
public var asAPIError: APIError {
42+
if let decodingError = self.asDecodingError {
43+
return APIError.decodingError(decodingError)
44+
} else if let urlError = self.asURLError, urlError.code == .timedOut {
45+
return APIError.timeout
46+
} else {
47+
return self as? APIError ?? .unknown
48+
}
49+
}
50+
51+
var asDecodingError: DecodingError? {
52+
return self as? DecodingError
53+
}
54+
55+
var asURLError: URLError? {
56+
return self as? URLError
57+
}
58+
}
59+
60+
extension DecodingError {
61+
var fullDescription: String {
62+
switch self {
63+
case let .typeMismatch(type, context):
64+
return """
65+
타입이 맞지 않습니다.\n
66+
\(type) 타입에서 오류 발생:\n
67+
codingPath: \(context.codingPath)\n
68+
debugDescription: \(context.debugDescription)\n
69+
underlyingError: \(context.underlyingError?.localizedDescription ?? "none")
70+
"""
71+
case let .valueNotFound(type, context):
72+
return """
73+
값을 찾을 수 없습니다.\n
74+
\(type) 타입에서 오류 발생:\n
75+
codingPath: \(context.codingPath)\n
76+
debugDescription: \(context.debugDescription)\n
77+
underlyingError: \(context.underlyingError?.localizedDescription ?? "none")
78+
"""
79+
case let .keyNotFound(key, context):
80+
return """
81+
\(key) 를 찾을 수 없습니다:\n
82+
codingPath: \(context.codingPath)\n
83+
debugDescription: \(context.debugDescription)\n
84+
underlyingError: \(context.underlyingError?.localizedDescription ?? "none")
85+
"""
86+
case let .dataCorrupted(context):
87+
return """
88+
데이터 손실 에러입니다.\n
89+
codingPath: \(context.codingPath)\n
90+
debugDescription: \(context.debugDescription)\n
91+
underlyingError: \(context.underlyingError?.localizedDescription ?? "none")
92+
"""
93+
default:
94+
return "알 수 없는 디코딩에 실패했습니다."
95+
}
96+
}
97+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Foundation
2+
3+
public enum HTTPMethod: String {
4+
case get = "GET"
5+
}
6+
7+
public protocol EndPoint {
8+
var baseURL: URL { get }
9+
var path: String { get }
10+
var method: HTTPMethod { get }
11+
var parameters: [String: Any]? { get }
12+
var headers: [String: String]? { get }
13+
var body: Encodable? { get }
14+
}
15+
16+
extension EndPoint {
17+
public func request() -> URLRequest {
18+
var url = baseURL.appendingPathComponent(path)
19+
var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)!
20+
21+
urlComponents.queryItems = parameters?.map { URLQueryItem(name: $0.key, value: "\($0.value)") }
22+
url = urlComponents.url!
23+
24+
var request = URLRequest(url: url)
25+
request.httpMethod = method.rawValue
26+
27+
headers?.forEach {
28+
request.setValue($0.value, forHTTPHeaderField: $0.key)
29+
}
30+
31+
if let body, let jsonData = try? JSONEncoder().encode(body) {
32+
request.httpBody = jsonData
33+
}
34+
35+
request.cachePolicy = .reloadIgnoringLocalCacheData
36+
37+
return request
38+
}
39+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import Foundation
2+
import Combine
3+
4+
public enum Request {
5+
public static func requestJSON<E: EndPoint, T: Decodable>(
6+
_ endPoint: E,
7+
decoder: JSONDecoder = .init(),
8+
queue: DispatchQueue = .main
9+
) -> AnyPublisher<T, any Error> {
10+
decoder.dateDecodingStrategy = .iso8601
11+
return URLSession.shared.dataTaskPublisher(for: endPoint.request())
12+
.timeout(.seconds(10), scheduler: RunLoop.main)
13+
.map { output in
14+
printNetworkLog(request: endPoint.request(), output: output)
15+
}
16+
.tryMap(responseToData)
17+
.decode(type: T.self, decoder: decoder)
18+
.mapError(\.asAPIError)
19+
.receive(on: queue)
20+
.eraseToAnyPublisher()
21+
}
22+
23+
public static func requestVoid<E: EndPoint>(
24+
_ endPoint: E,
25+
queue: DispatchQueue = .main
26+
) -> AnyPublisher<Void, any Error> {
27+
return URLSession.shared.dataTaskPublisher(for: endPoint.request())
28+
.timeout(.seconds(10), scheduler: RunLoop.main)
29+
.map { output in
30+
printNetworkLog(request: endPoint.request(), output: output)
31+
}
32+
.tryMap(responseToData)
33+
.map { _ in () }
34+
.mapError(\.asAPIError)
35+
.receive(on: queue)
36+
.eraseToAnyPublisher()
37+
}
38+
}
39+
40+
private extension Request {
41+
static func responseToData(_ output: URLSession.DataTaskPublisher.Output) throws -> Data {
42+
guard let httpResponse = output.response as? HTTPURLResponse else {
43+
throw APIError.custom(message: "응답이 없습니다.", code: 999)
44+
}
45+
46+
switch httpResponse.statusCode {
47+
case 200...299:
48+
return output.data
49+
case 400:
50+
throw APIError.badRequest
51+
case 401:
52+
throw APIError.unauthorized
53+
case 403:
54+
throw APIError.forbidden
55+
case 404:
56+
throw APIError.notFound
57+
case 500...599:
58+
throw APIError.serverError
59+
default:
60+
throw APIError.unknown
61+
}
62+
}
63+
64+
static func printNetworkLog(request: URLRequest, output: URLSession.DataTaskPublisher.Output) -> URLSession.DataTaskPublisher.Output {
65+
// Request 정보 출력
66+
let method = request.httpMethod ?? "unknown method"
67+
let url = request.url?.absoluteString ?? "Unknown URL"
68+
69+
print("====================\n\n[\(method)] \(url)\n\n====================\n")
70+
71+
print("====================[ Request Headers ]====================\n")
72+
73+
if let headers = request.allHTTPHeaderFields {
74+
print("\(headers)")
75+
} else {
76+
print("header를 찾을 수 없습니다.")
77+
}
78+
79+
print("\n====================[ End Request Headers ]====================\n")
80+
81+
// Request Body 출력
82+
print("\n====================[ Request Body ]====================\n")
83+
84+
if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) {
85+
print("\(bodyString)")
86+
} else {
87+
print("Unable to encode utf8")
88+
}
89+
90+
print("\n====================[ End Request Body ]====================\n")
91+
92+
// Response 정보 출력
93+
let httpResponse = output.response as? HTTPURLResponse
94+
let statusCode = httpResponse?.statusCode ?? -1
95+
96+
print("====================\n\n[\(statusCode)]\n[\(url)]\n\n====================\n")
97+
98+
// Response Body 출력
99+
print("\n====================[ Response Body ]====================\n")
100+
if let bodyString = String(data: output.data, encoding: .utf8) {
101+
print("\(bodyString)")
102+
} else {
103+
print("Unable to encode utf8")
104+
}
105+
print("\n====================[ End Body ]====================\n")
106+
107+
return output
108+
}
109+
}

0 commit comments

Comments
 (0)