거의 모든 앱은 서버 의존도가 필수불가결적으로 높습니다. 앱 개발자로써 서버가 다운되거나, 점검 중이거나, 배포 전일 때에도 계속 개발을 지속할 수 있어야합니다. 그래서 서버의 응답이 완료 될 떄에만 앱이 실행되는 것은 굉장히 불리한 조건을 갖게 됩니다.
우리 편행 프로젝트에서도 이 기술을 활용하여 서버 의존도를 낮추고 효과적으로 개발에 임할 수 있었습니다.
이번 시간에는 URLProtocol을 이용해 서버 없이도 개발을 효과적으로 진행하는 방법을 소개해드리겠습니다.
URLProtocol
URLProtocol은 네트워크 요청을 실제 데이터로 받지 않고 Mock 데이터로 받을 수 있게 할 수 있는 객체입니다.
URLSession의 Configuration을 통해 주입이 되어야합니다.
일반적인 네트워크 통신URLProtocol을 이용한 네트워크 통신
URLSessionDataTask에서 실제 서버와 통신하는 것과 달리 URLSessionConfiguration에 URLProtocol이 있다면 완전히 다른 방식으로 네트워크 통신을 진행합니다.
let configuration: URLSessionConfiguration = .ephemeral
configuration.protocolClasses?.insert(MockURLProtocol.self, at: .zero)
우리가 만들어줄 Mock Data를 통해 네트워크 커넥션을 열어 Request 생성, Response를 받아 처리하는 과정을 실제 서버와 통신하는 것처럼 사용할 수 있습니다.
Custom URLProtocol 구현하기
이제 URLProtocol을 이용해 원하는 Mock 데이터로 서버 통신을 해볼 차례입니다.
public final class HomeURLProtocol: URLProtocol {}
먼저 원하는 클래스 객체에 URLProtocol을 상속 받아야합니다. 이름은 Protocol인데 Class인 것을 혼동하실 수 있습니다.
URLProtocol을 상속 받게 되면 필수적으로 구현해야할 4가지 메서드가 있습니다.
public final class HomeURLProtocol: URLProtocol {
override public class func canInit(with _: URLRequest) -> Bool {
true
}
override public class func canonicalRequest(for request: URLRequest) -> URLRequest {
request
}
override public func startLoading() {}
override public func stopLoading() {}
}
canInit은 URLRequest에 URLProtocol을 적용할 것인지에 대한 메서드입니다. 일반적으로 true를 반환합니다.
canonicalRequest는 URLRequest를 중간에 바꿔치기할 수 있는 메서드입니다. 이것도 일반적으로 인자로 들어온 request를 바로 반환합니다.
startLoading은 Task가 실행될 때의 로직을 적는 곳입니다. 해당 메서드에서 대부분의 로직 처리가 일어납니다.
stopLoading은 Task가 끝날 때의 로직을 적는 곳입니다. 일반적으로 빈 값으로 둡니다.
Mock Data 구현
Mock Data는 다양한 방식으로 구현할 수 있습니다. 우리는 JSON파일을 따로 만들어서 관리했습니다.
API 명세서가 나왔다면, Response 구조와 똑같이 구현하면 됩니다. 하지만 명세서가 나오기 전이라면 화면에 구성되는 데이터만을 사용하는 것도 하나의 방법입니다.
startLoading 로직 추가하기
이제 만들어둔 Mock Data를 가지고 startLoading에 로직을 추가하면 됩니다.
public final class HomeURLProtocol: URLProtocol {
// ...
override public func startLoading() {
defer { client?.urlProtocolDidFinishLoading(self) }
if let url = request.url,
let mockData = mockData[url.path(percentEncoded: true)],
let data = mockData,
let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) {
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
} else {
client?.urlProtocol(self, didFailWithError: NetworkError.urlError)
}
}
// ...
}
여기서 client는 URLProtocol을 상속받으면 있는 URLProtocolClient입니다.
client는 response, data와 error를 전달할 수 있습니다. 결과값과 에러를 커스텀할 수 있게되니 네트워크를 테스팅하기에 좋은 요건을 갖추고 있습니다.