안녕하세요 iOS 개발자 루크입니다
오늘은 예외처리의 1등 공신 Result 타입에 대해서 배워보도록 하겠습니다.
Result 타입이란?
Result 타입의 장점을 이해하기 위해서는 먼저 그 이전의 코드를 작성하는 방식을 이해하여야 합니다.
이전의 문제 상황을 해결하기위해 등장한 놈이기 때문이죠!
문제 상황
Result 타입이 등장하게된 배경은 아래와 같이 Completion Handler 를 작성하는 상황입니다.
let url = URL(string: "https://www.swiftbysundell.com")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in // 2개 이상의 optional 타입
if let error = error {
// 에러 핸들링
...
} else if let data = data {
// 데이터 존재시 데이터 핸들링
...
}
}
task.resume()
Swift
복사
위와 같이 이전에는 compltion handler 를 호출하는 경우에, 성공, 실패를 표현하기 위해 두개의 optional 한 값을 다루었다고 하네요.
여기에 두가지 문제가 있습니다.
첫번째는 바로, 성공, 실패를 모델링하기 위해 만들어졌지만, 2가지가 아닌 4가지 경우의 수가 존재한다는 것입니다.
•
데이터가 존재하며, 에러도 존재하는 경우
•
데이터가 존재하며, 에러가 존재하지 않는 경우
•
데이터가 존재하지 않으며, 에러가 존재하는 경우
•
데이터가 존재하지 않으며, 에러가 존재하지 않는 경우
성공, 실패를 모델링하고자 했는데 그 이상의 case 가 존재하게 됩니다.
두번째 문제점은 값을 사용할 때마다, optional 타입을 unwrap 해줄 필요가 있다는 것입니다.( 귀찮아요…)
따라서 Swift 의 Result 타입을 사용하면, 이러한 문제점들을 한번에 해결할 수 있습니다.
자 그럼 Result 타입을 만나러 가봅시다.
Result 정의부
Result 타입의 장점을 다시 이야기 하면서 가보죠
•
결과의 성공, 실패 여부와는 상관없이 하나로 결과를 표현할 수 있게끔 도와줌. → 가독성 증가
•
optional 타입을 사용하지 않기 때문에, unwrap 을 위한 코드를 작성하지 않아도 됨. → 코드 길이 단축
정의되어 있는 모습은 다음과 같습니다.
enum Result<Success, Failure> where Failure: Error {
case success(Success)
case failure(Failure)
}
Swift
복사
제네릭 타입으로 작성되어 있기 때문에, 어떠한 타입에든 사용이 가능합니다.
먼저 URLSession 을 Result<Data, Error> 를 passing 하는 새 API 를 작성하여 확장해보죠
extension URLSession {
typealias Handler = (Result<Data, Error>) -> Void
func dataTask(
with url: URL,
handler: @escaping Handler) -> URLSessionDataTask {
dataTask(with: url) { data, _, error in
if let error = error {
handler(.failure(error))
} else {
handler(.success(data ?? Data()))
}
}
}
}
Swift
복사
자 이제 URLSession 의 유려해진 코드를 다시 확인해보죠
let task = URLSession.shared.dataTask(with: url) { result in
switch result {
case .success(let data):
// Handle successful response data
...
case .failure(let error):
// Handle error
...
}
}
Swift
복사
훨씬 가독성이 좋아진 거 보이시나요?.
성공이면, 성공인 case 로
실패면, 실패인 case 로
누가봐도 분명해보이죠
또한 optional 값을 바인딩하는 코드도 보이지 않습니다.
이처럼 Result 타입을 사용하면, 코드가 더 읽기 쉬워진다는 장점이 있습니다.
또 한가지 장점 - 협업시 발생 가능한 에러타입 암시
협업을 하다보면은 Swift 의 에러 핸들링 방식인 throw 를 사용해서, 에러를 발생시킬 수 있음을 알린다고 해도 함수를 호출하는 사람은 에러가 발생한다는 사실만 알 수 있을 뿐, 어떠한 에러가 발생가능한지는 함수의 정의부를 확인해야 알 수 있다는 불편함이 있었습니다.
하지만 Result 타입을 사용하면, 유형화된 에러타입을 가지는 경우, 이 타입이 분명히 호출하는 사람이 확인할 수 있기 때문에 switch 문을 사용해서 모든 case 를 핸들링할 수 있게 돼요
예시와 함께 봅시다.
enum ImageLoadingError: Error {
case networkFailure(Error)
case invalidData
}
Swift
복사
위는 이미지를 불러오는 데 발생가능한 에러들의 유형을 명시한 직접 만든 에러타입니다.
•
하나는 네트워크 에러이고
•
하나는 다운로드한 데이터가 유효하지 않은 경우이죠
Result 타입을 위에서 만든 에러타입을 사용해 구체화 해주기만 하면 됩니다.
이렇게 하면 호출부에 발생할 수 있는 에러에 대한 더 많은 정보를 담아 보낼 수 있다.
struct ImageLoader {
typealias Handler = (Result<UIImage, ImageLoadingError>) -> Void
var session = URLSession.shared
func loadImage(at url: URL,
then handler: @escaping Handler) {
let task = session.dataTask(with: url) { result in
switch result {
case .success(let data):
if let image = UIImage(data: data) {
handler(.success(image))
} else {
handler(.failure(.invalidData))
}
case .failure(let error):
handler(.failure(.networkFailure(error)))
}
}
task.resume()
}
}
Swift
복사
위처럼 디자인된 API 는 잠재적인 각각의 에러에 대해서 빠짐없이 핸들링할 수 있게 됩니다.
let imageURL = URL(string: "https://www.swiftbysundell.com/images/logo.png")!
let imageLoader = ImageLoader()
imageLoader.loadImage(at: imageURL) { result in
switch result {
case .success(let image):
// Handle image
...
case .failure(.invalidData):
// Handle an invalid data failure
...
case .failure(.networkFailure(let error)):
// Handle any network error
...
}
}
Swift
복사
아무래도 에러타입이 명확하니까, 발생가능한 에러를 더 빠르게 확인하고 적절하게 action 을 취해줄 수 있겠죠?
오늘 포스팅은 여기서 마치겠습니다 ~,~
해당 포스팅은 아래 포스팅으로 이어집니다.