👋 들어가기 전
우리의 두번째 도전은 프로젝트 전반적으로 사용될 범용적인 라이브러리를 만드는 도전이다.
우리는 크게 2가지 범용 라이브러리를 기획했다.
첫번 째는 네트워크 통신을 담당하는 라이브러리와 두번 째는 uikit 레이아웃을 도와주는 라이브러리가 있다.
실제 오픈소스로는 네트워크 쪽은 알라모파이어, 모야, 레이아웃은 스냅킷 등이 있다.
우린 위 오픈소스들을 컨셉을 참고하고 , 당장 사용하기 쉽게 만들기로 계획했다.
여기서 나는 네트워크 팀으로 합류하게 되었다.
위 사진은 프로토 타입으로 정리한 백로그다. 각 과정을 간단하게 살펴보자.
P.S 레이아웃 쪽은 EasyLayout이라는 명칭으로 만들어뒀으니 궁금하면 살펴보길 바란다.
(추후에 Package 형태로 교체할 예정)
네트워크쪽 이름은 아직 정하지 못햇으니 좋은 이름이 있으면 댓글로 추천 바란다.
✈️ 구성요소 살펴보기
첫번 째로 우리는 네트워크 통신할 때 어떤 요소들이 있는 지 먼저 살펴보자.
먼저 요청할 URL을 구성하는 정보를 분석해보자.
위 구성요소와 함께 HTTP Reuqst에 필요한 내용을 정리해보면 다음과 같다.
여기서 추상적인 개념이 ReuqstTask만 조금 더 자세히 살펴보자.
RequstTask는 다음과 같은 분기가 된다.
- body에 전혀 값이 없을 때
- body에 [Stirng: String] 형태로 전달하거나 query 파라미터가 있을 때
- body에 Encodable 객체로 전달하거나 query 파라미터가 있을 때
📊 더 편하게 분석하자
보내기 직전 Request와 응답받은 직후 Response를 확인할 수 있다면 조금 더 사용하기 편한
네트워크 라이브러리라고 생각하고 계획 없던 로깅기능을 구현했다.
1. Requst 훔치기
실질적인 네트워킹을 진행하는 reuqstNetworkTask 전에 interceptRequst를 통해 requst를 가로채서
별도의 작업을 진행한다.
대표적으로 로깅 기능에 해당하는데 나는 다음과 같이 DefaultLoggingInterceptor를 만들어
request와 response의 내용을 미리 로깅하여 디버깅을 매우 효율적으로 할수 있도록 제공했다.
public final class DefaultLoggingInterceptor: Interceptor {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "NETWORK")
public init() {}
public func willRequest(_ request: URLRequest, from endpoint: any Endpoint) throws {
guard let url = request.url else { throw NetworkError.invaildURL }
let method = endpoint.method
var log = "====================\n\n[\(method)] \(url)\n\n====================\n"
log.append("✅ Endpoint: \(endpoint)\n")
...
}
2. Response 훔치기
Response 역시 받아오자마자 interceptor가 가로챈 후 별도의 작업을 진행한다.
🧪 Test
자 이제 우리가 만든 네트워크 모듈을 검증할 시간이다.
그런데 현재 서버가 아직 구현되어있지 않다. iOS 개발자는 Request와 Response를 확인하기위해서
서버가 구현될 때까지 월급루팡 하면서 기다려야할까??
애플에서 우리가 월급루팡이되지 말라고 만들어 놓은 것이 있다.
바로 URL Protocol이다.
🛜 URLProtocol이란?
An abstract class that handles the loading of protocol-specific URL data.
WWD 영상을 살펴보면
URLSessionConfiguration을 통해 URLSession이 만들어지고 이후
URLSession의 URLSessionDataTask를 통해 Rereuqst와 Response를 받게되는데
DataTask 이후 숨겨진 과정이 있는데 바로 URLProtocol이 숨겨져잇다.
URLProtocol은 네트워크 연결 열기, requst 쓰기, Response 읽기 같은 작업을 진행하는데
여기서 Respones읽기를 잘 활용하면 URLProtocol의 subclass는 서버 역할을 대신할 수 있다.
여기서 URLProtocol을 사용하기 전 알아야할 사전 지식을 간단히 정리해보자.
🪛 URL Loading System이란?
Interact with URLs and communicate with servers using standard Internet protocols.
System Foundation안에 내장되어있는 기능으로 표준 인터넷 프로토콜을 이용하여
URL과 상호작용하여 서버의 자원에 접근하는 역할 을 한다.
이게 URLProtocol을 사용하는데 무슨 의미가 있냐면
URLProtocol이 바로 URL Loading System 기능을 확장하여 사용하는 역할을 하는 추상 클래스이기 때문이다.
URLPorotocol vs URL Loading System
네트워크 연결 열기, requst 쓰기, Response 읽기 = URL과 상호작용하여 서버의 자원에 접근하는 역할
둘이 크게 차이가 없다.. 다만 URL Loading System은 System Foundation안에 있기때문에 내가 커스텀할 수 없을 뿐
즉, URLProtocol을 subclass를 하면 충분히 만족스러운 작업을 할 수 있다는 뜻이다 ..
여기서 하나만 짚고 넘어가면 URLProtocol은 URLProtocolClient에게 작업을 넘준다는 것..
그렇다면 URLPtocolClient는 또 뭔데??
💡 URLProtocolClient 이란?
The interface used by URLProtocol subclasses to communicate with the URL Loading System.
URLProtocol을 subclass들이 URL Loading System들과 소통하기 위한 인터페이스다..
어디서 많이 본 구조다...
커널 기능을 사용하기위해 유저 어플리케이션에서 시스템 콜을 호출하는 느낌과 굉장히 비슷하다.
MockURLProtocol 만들기
final class MockURLProtocol: URLProtocol {
static var mockData: Data?
static var mockResponse: HTTPURLResponse?
private(set) static var mockRequest: URLRequest?
override static func canInit(with request: URLRequest) -> Bool { true }
override static func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
MockURLProtocol.mockRequest = request
if let response = MockURLProtocol.mockResponse {
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
}
if let data = MockURLProtocol.mockData {
client?.urlProtocol(self, didLoad: data)
}
client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {}
}
func canInit(with: request..)
- MockURLProcol이 특정 request에 대해 처리할 수 있는 지 여부
func canInit(with: request..)
- 전달된 request를 표준화 하는데 사용한다.
- 보통은 전달받은 그대로 호출하지만 내부적으로 일관적으로 설정해야될 부분을 여기서 설정해줄 수 있다.
func startLoading()
- 전달반은 request, response, data , error 등을 client에게 알려준다.
- 여기서 클리아인트는 URLProtocol안에 있는 변수로 위에서 설명한 URLProtocolClient 프로토콜 타입이다
func stopLoading()
- Requst가 끝났거나 중지되었을 때를 처리할 부분
테스트 코드 작성 시 작성한 URLProtocol subclass 등록
final class NetworkClientTest: XCTestCase {
private var interceptors: [any Interceptor]!
private var client: NetworkClient<MockEndpoint>!
override func setUp() {
super.setUp()
interceptors = [DefaultLoggingInterceptor()]
let configuration = URLSessionConfiguration.ephemeral // 일시적인
configuration.protocolClasses = [MockURLProtocol.self] // URLProtocol subclasses 등록
let session = URLSession(configuration: configuration)
client = NetworkClient(session: session, interceptors: interceptors)
}
...
}
이후 각 tesetcase에 원하는 request, error , resnpose를 넣어 테스트한다.
// MARK: - Success
func test_success_response() async throws {
MockURLProtocol.mockData = mockData
MockURLProtocol.mockResponse = mockSuccessResponse
let mockEndpoint = MockEndpoint.fetch
let response = try await client.request(mockEndpoint)
let request = MockURLProtocol.mockRequest
guard let httpResponse = response.response as? HTTPURLResponse else {
return XCTFail("HTTP 응답이 아닙니다.")
}
XCTAssertEqual(request?.url?.absoluteString, "https://www.example.com/fetch")
XCTAssertEqual(httpResponse.statusCode, 200)
XCTAssertEqual(response.data, MockURLProtocol.mockData)
}
결과는 다음과 같이 예상대로 잘 오는걸 확인할 수 있다.
🧹 최종 정리
URLProtocol을 통해 테스트도 잘되었다.. 서버가 열리면 실제로도 잘 작동할 확률이 많이 올라갔다.
자, 마무리로 우리가 만든 모듈의 전첸흐름을 한버 정리하고 끝내자.
1. Endpoint 프로토콜을 채택하여 통신에 필요한 각 요소를 미리 채워 넣는다.
2. 이후 Client의 request 함수에 endpoint를 전달받아 URLRequest를 만든다.
3. 이후 Interceptor가 존재한다면 reuqst를 보내기 전 가로채서 별도의 작업을 진행한다.
4. Requst를 진행한다.
🙏 소감
현준님과 함께 네트워크 팀에서 범용 네트워크 라이브러리를 만들어봤는데
만들면 만들수록 미리 만들어진 네트워크 라이브러리들이 참 잘 만들었다고 생각이 들었다.
정말 좋은 구조와 개념들을 코드에 너무 쉽게 표현했고 많은 참고자료가 되었다.
또한 위 작업을 혼자하면 퀄리티가 정말 많이 떨어졌을 것 같다.
현준님께서 조금 더 편하게 사용할 수 있도록 네비게이터 역할을 정말 잘해주셨다.
대표적으로 static 함수보다 계산 프로퍼티를 사용해서 조금이라도 더 편하게 입력하고
URLCompoent를 활용해서 URLReuqst를 만들 때 간편하게 만들 수 있게 조언을 많이 해주셨다.
현재 프로젝트에서 레이아웃 팀원들이 네트워크 작업할 때 우리 라이브러리를 정말 편하게
사용하고 있다고 칭찬을 받고 있고 내심 정말 많이 뿌듯했다. 😁
현준님 수고 하셨습니다.
참고
'iOS' 카테고리의 다른 글
[ 부스트 캠프 ] 채팅 기능을 위한 웹소켓 만들기 (1) | 2024.11.30 |
---|