Swift3(PromiseKit)으로 Async 사용해보기 (scala, js 와 비교)
Updated:Categories: iOS
Tags: #Swift3 #Promise #Scala #JavaScript
OverviewPermalink
Swift3 의 Alamofire 라이브러리를 사용하여 callback 함수로 API request를 구현.
하지만, callback 함수 부분이 가독성이 떨어지는 것 같아 코드를 리팩토링해보기로 결정.
AngularJS
에서 defer 객체를 통한 Promise와 Scala
의 Future, Promise 등과 같이 익숙한 형태를 찾아봄.
Swift에서 Promise 패턴을 사용할 수 있는 라이브러리인 PromiseKit과
Alamofire을 확장한 PromiseKit+Alamofire을 발견하고 사용.
비교를 위한 AngularJS 내 Promise, Scala의 FuturePermalink
AngularJS + deferPermalink
this.getList = function() { | |
var url = "http://localhost:8080/{request path}" | |
var defer = $q.defer(); | |
$http.get(url, { | |
dataType: 'json', | |
headers: { | |
"Content-Type": "application/json", | |
"Accept": "application/json" | |
} | |
}).then(function(res) { | |
// response의 validation 체크 후, defer 객체의 resolve와 reject를 이용하여 success, fail을 넘겨 다음 로직 실행. | |
// ex. response 안에 status 값으로 성공/실패 여부 판단 | |
var data = res.data; | |
var status = data.status; | |
if(status === "success") { | |
defer.resolve(data); | |
} else { | |
defer.reject(data.message); | |
} | |
}).catch(function(e) { | |
defer.reject(e.message); // ex. Handling http status code error 404 not found | |
}); | |
return defer.promise; | |
}; |
Scala + FuturePermalink
def getList(productId: String): Future[Either[ErrorMessage, SomeResponseModel]]
에서는
ErrorMessage
라는 타입을 통해 실패에 대한 정보를 반환함.
아래의 예에서는 ErrorMessage
을 String
으로 선언하였지만, Enum 이나 case class 등 다르게 선언하여 사용할 수도 있음.
/* In API Caller class */ | |
// By using Apache http client. 아래 Apache http client의 요청은 async가 아니지만, Future로 감싸 async로 만들었음. | |
type ErrorMessage = String | |
def getList(productId: String): Future[Either[ErrorMessage, SomeResponseModel]] = Future { | |
val req = new HttpGet | |
val client = HttpClientBuilder.create().build() | |
req.setURI(new URI(s"http://localhost:8080/{request path}")) | |
req.setHeader(new BasicHeader("Content-Type", "application/json")) | |
val res = client.execute(req) | |
parseResponse[SomeResponseModel](res.getEntity) | |
} | |
private def parseResponse[T <: CommonResponse](resEntity: HttpEntity)(implicit m: Manifest[T]):Either[ErrorMessage, T] = { | |
val resBody = EntityUtils.toString(resEntity) | |
val jsonObj = parse(resBody) | |
jsonObj.extractOpt[T] match { | |
case Some(r) if r.status == "success" => Right(r) | |
case Some(r) => Left(r.message) | |
case _ => Left("UnknownError") | |
} | |
} |
Swift3 with PromiseKitPermalink
/* In ResponseParser class */ | |
func product(json: [String:Any]) -> SomeResponseModel? | |
/* In API Caller class */ | |
// API Error에 대해서 optional인 nil로 처리. Error를 Return 되는 값 형태로 처리. | |
func getProduct(productId: String) -> Promise<SomeResponseModel?> { | |
let url = "http://localhost:8080/{request path}" | |
let params = [ | |
"productId": productId | |
] | |
let headers = [ | |
"Accept": "application/json", | |
"Content-Type": "application/json" | |
] | |
return SessionManager.request(url, parameters: params, headers: headers) | |
.validate(statusCode: 200..<300).responseJSON().then { data -> SomeResponseModel? in | |
guard let json = data as? [String:Any] else { | |
return nil | |
} | |
return ResponseParser.product(json: json) | |
} | |
} | |
// js+angularJS+defer 와 비슷한 형태 | |
enum MyErrors: Int, Error { | |
case Unknown = 404 | |
case NoInternet = 401 | |
} | |
// 이 경우에는 에러가 나면 catch 문으로 잡히기 때문에 return 값에 optional이 들어가지 않아도 된다. | |
// 이 함수가 pure functional하지는 않지만 response handling하기는 이 코드가 더 쉬운듯. | |
// optional로 return하는 위의 함수처럼 작성해도 response handling하는 부분에서 어차피 error를 발생시켜야만 handling을 할 수 있었음. | |
// (다른 좋은 방법이 있는지는 확인하지 못하였음.) | |
func getProduct(productId: String) -> Promise<SomeResponseModel> { | |
//.. Here is same as upper | |
return SessionManager.request(url, parameters: params, headers: headers) | |
.validate(statusCode: 200..<300).responseJSON().then { data in | |
return Promise { fulfill, reject in | |
guard let json = data as? [String:Any], let product = ResponseParser.product(json: json) else { | |
reject(MyErrors.Unknown) | |
return | |
} | |
fulfill(product) | |
} | |
}.catch { error in // 해당 함수를 처리하는 쪽에서 handling해도 됨 | |
print("# error \(error.localizedDescription)") | |
} | |
} | |
위와 같이 PromiseKit, PromiseKit+Alamofire을 사용한 경우,
실패에 대한 처리가 앞에서 나온 AngularJS와 비슷함.
Promise 객체의 reject 함수(Promise의 callback 함수의 두번째 인자를 reject 으로 표현)를 사용하여, 실패에 대한 처리를 함.
reject 함수는 Error 객체를 통해 실패에 대한 자세한 정보(ex. 코드 내 MyErrors.Unknown
과 같은 커스텀된 실패 정보)를 전달 가능.
reject 함수가 호출되면,
- Promise의 반환값을 처리하는 곳에서는 catch문이 실행.
- catch 문 내에서 실패 정보에 따라 그에 맞는 행동을 정의.
Promise<SomeResponseModel?>
를 반환하는 첫번째 getProduct
함수와 같이 해당 함수의 실행 실패를 반환하는 방법도 있음.
하지만 실패에 대한 모든 경우가 nil
로 표현되어, 실패했다는 사실을 알릴 수는 있어도 실패 이유와 같은 자세한 정보를 알릴 수 없음.
실패에 대한 정보를 포함하고자 하기 위해서는 scala의 Either와 같은 객체를 만들어 전달할 수는 있음.
실패에 대한 이유를 같이 전달하는 코드의 장점Permalink
- 유지보수나 협업을 할 때, 별도의 문서가 아닌 코드 상에서 함수 내 실패 가능성을 명시적으로 표현 할 수 있었음.
- 좀 더 Pure Functional 형태로 Promise 객체 내 성공, 실패에 대한 정보를 반환할 수 있는 형태면 좋을 것 같음. 이 경우 함수의 명세(이름, Input parameters, Return 타입 등)만 보고서 어떤 실패 가능성이 있는 함수인지 알 수 있음. 그렇지 않은 경우, 그 함수를 쓰고 있는 catch 문을 봐야 어떤 실패 가능성이 있는 함수인지 알 수 있음.
위와 같은 이유로 스칼라에서는 발생할 수 있는 오류에 대해서 try catch 보다는 Option, Either 객체를 사용한 값으로 매핑하는 것을 권장하고 있음.
Comments