기존의 에러처리 방식은 열거형을 선언하고, 해당 열거형의 케이스를 사용하여 에러를 정의한 후, 함수에서 에러를 던져주고 이후에는 do-catch
문을 사용하여 에러를 처리하는 방식이다.
이러한 에러처리의 단점은 열거형을 사용하여 에러를 정의하고, 각 에러 케이스에 대한 처리를 별도의 catch 블록에 작성해야해서 코드의 가독성을 떨어뜨리고 여러 곳에서 같은 에러처리 코드를 반복해서 작성해야 할 수도 있다. 또한, 에러가 추가되거나 수정될 때 변경사항을 반영하기 위해서 코드를 수정해야해서 유지보수를 어렵게 만든다.
Swift에서는 이러한 단점들을 완화하고 개선하기 위한 여러 가지 기능이 추가되고 있다. 그 중 Result Type
을 알아보자!
Result 타입은 제네릭 열거형으로 선언되어 있고, 경우에 따른 연관값을 포함하여, 성공케이스와 실패케이스를 나타내는 값이다. 성공, 실패의 경우를 깔끔하게 처리할 수 있고 기존의 에러처리 패턴을 완전히 대체하려는 목적이 아니라 개발자에게 에러 처리에 대한 다양한 처리 방법에 대한 옵션을 제공한다.
@frozen enum Result<Success, Failure> where Failure : Error
function의 반환 타입으로 Result Type에 성공했을 경우와, 실패했을 경우의 값을 넣어 넣어준다.
enum CoffeeMachineError: Error {
case invalidInput
case insufficienCoffes
case insufficientFunds(requiredCoins: Int)
}
class CoffeeMachine {
let coffeePrice: Int = 100
var coffeeStock: Int = 5
var depositedCoins: Int = 200
// 동전 투입 메서드
func insertCoins(coins: Int) -> Result<Bool, CoffeeMachineError> {
// 투입된 동전이 0보다 크지않으면 invalidSelection 에러를 던진다.
guard coins > 0 else {
return .failure(.invalidInput)
}
// 에러가 없을 시 정상 처리한다.
self.depositedCoins += coins
print("\(coins)원 동전 투입되었습니다.")
return .success(true)
}
// 커피 판매 메서드
func vendCoffee(numberOfCoffees: Int) -> Result<String, CoffeeMachineError> {
// 구입하는 커피 개수가 커피재고보다 크면 insufficienCoffes에러를 던진다.
guard numberOfCoffees <= coffeeStock else {
return .failure(.insufficienCoffes)
}
// 커피의 총 금액이 디파짓된 코인보다 크면 insufficientFunds 에러를 던진다.
guard numberOfCoffees * coffeePrice <= depositedCoins else {
let requiredCoins = numberOfCoffees * coffeePrice - depositedCoins
return .failure(.insufficientFunds(requiredCoins: requiredCoins))
}
// 에러가 없을 시 정상 처리한다.
let totalPrice = numberOfCoffees * coffeePrice
self.depositedCoins -= totalPrice
self.coffeeStock -= numberOfCoffees
return .success("\(numberOfCoffees)잔의 커피 가격은 \(totalPrice)입니다.")
}
}
switch문을 통해 success, failure의 경우에 따라 처리를 해주면 된다.
let isOrderable = a.insertCoins(coins: 0)
switch isOrderable {
case .success(let bool):
print("bool: \(bool)")
case .failure(let error):
print("error: \(error)")
}
네트워킹 시 Result Type 사용 예시를 보자.
enum NetworkError: Error {
case someError
}
func performRequest(with urlString: String, completion: @escaping (Result<Data,NetworkError>) -> Void) {
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
// 실패 케이스 전달
completion(.failure(.someError))
return
}
guard let safeData = data else {
// 실패 케이스 전달
completion(.failure(.someError))
return
}
// 성공 케이스 전달
completion(.success(safeData))
}.resume()
}
// switch문을 통해 success, failure의 경우에 따라 처리
performRequest(with: "Url") { result in
switch result {
case .failure(let error):
print(error)
case .success(let data):
// 데이터 처리 관련 코드
break
}
}
Result 타입을 사용하면 경우에 따라 에러를 명확하게 구분할 수 있어서 코드의 가독성이 높아지고 로직을 명확하게 구현할 수 있다. 또한, 기존의 에러처리 방식에서는 비동기 처리가 복잡할 수 있으나 Result 타입을 사용하면 간편하게 처리할 수 있다.