본문 바로가기

Swift

[Swift] Result Type으로 명확한 결과값 만들기

Swift 5에 새롭게 추가된 에러 처리 방식으로 Result Type에 대해 알아보도록 하겠습니다.
먼저 그 동안 에러 처리를 어떻게 해오고 있었는지 살펴보겠습니다.

Swift 1 시절

Objective C와 동일한 방식으로 에러를 처리하였습니다.

 

var error: NSError?
let str: NSString
let url: URL
let success = str.writeToURL(url, atomically: true, encoding: NSUTF8StringEncoding, error: &error)

if !success {
   println("Error: \(error!)")
}

 

포인터 형식으로 NSError를 사용하였기에 포인터 사용을 지양하는 스위프트에는 어울리지 않았습니다.

Swift 2 시절

새로운 에러처리 모델이 도입되었고 현재까지 사용되고 있습니다.

 

enum MyError: ErrorType {
   case someError
   case criticalError
}

func doSomething() throws {
   throw MyError.someError
}

do {
   try doSomething()
} catch let myError as MyError {
   switch myError {
   case .someError:
      print("someError")
   case .criticalError:
      print("criticalError")
   }
} catch {
   print(error.localizedDescription)
}

 

에러가 발생할 수 있는 코드를 throwing function으로 선언하고 do-catch문에서 try 표현식을 통해 함수를 호출하고 발생한 에러를 처리하였습니다. 에러 형식은 특별한 프로토콜을 채용한 형식으로 선언합니다.

 

이 방식의 한계는 다음과 같습니다.

  • throws 코드 블록에서 에러를 던질 수 있다는 걸 나타내지만 에러의 형식은 특정할 수 없음
  • catch로 올 때 실제 에러가 아닌 에러 프로토콜 형식으로 전달되는데, 이 때 모호함이 발생함
  • 에러를 처리하기 위해서는 어떤 형식의 에러를 던지는지 파악한 후 해당 형식으로 타입캐스팅을 해야함
  • 새로운 에러 형식이 추가되어도 컴파일러는 인지할 수 없음 (새로운 에러에 대한 처리가 없으면 경우에 따라 런타임 에러의 위험성이 생김)

Result Type

이제 새롭게 도입된 Result에 대해 알아보도록 하겠습니다.
Result는 Generic Enumeration으로 선언되어 있습니다.

 

public enum Result<Success, Failure> where Failure : Error

 

제네릭으로 선언되었다는 것은 형식이 명확하다는 것을 의미합니다. 형식에 관한 모호함이 사라지게 되는 것이죠. Result는 성공과 실패 2가지가 존재합니다. Success에는 작업의 결과가 저장되고 Failure에는 에러가 저장됩니다.


예를 들어, 서버에서 JSON 데이터를 받아온다고 하면 성공할 경우 Success에 해당 데이터를 담아주게 됩니다.

Result Type은 완전히 새로운 개념은 아닙니다. Alamofire와 같은 라이브러리에서는 이미 직접 구현한 Result Type을 활용하고 있습니다. 이제는 Swift에서 직접 제공하기 때문에 라이브러리 의존성을 줄일 수 있게 되었습니다.

 

이제 예시를 통해 어떻게 사용하는지 살펴보도록 하겠습니다.

 

enum NumberError: Error {
   case negativeNumber
   case evenNumber
}

func isOddNumber(number: Int) throws -> Int {
   guard number >= 0 else {
      throw NumberError.negativeNumber
   }

   guard !number.isMultiple(of: 2) else {
      throw NumberError.evenNumber
   }

   return number * 2
}

let result = Result {
   try isOddNumber(number: 1)
}

switch result {
case .success(let data):
   print(data)
case .failure(let error):
   print(error.localizedDescription)    
}

 

성공과 실패가 명확하게 구분되었습니다. Result Type으로 처리하는 방식에는 위의 방법말고도 다양한 방법이 있습니다. 이번에는 throwing function을 사용하지 않고 Result를 바로 반환하도록 해보겠습니다.

 

func isOddNumber(number: Int) -> Result<Int, NumberError> {
   guard number >= 0 else {
      return Result.failure(NumberError.negativeNumber)
   }

   guard !number.isMultiple(of: 2) else {
      return .failure(.evenNumber) // 형식 추론이 가능하므로 간단하게 작성 가능
   }

   return .success(oddNumber * 2)
}

let result = isOddNumber(number: 1)

switch result {
case .success(let data):
    print(data)
case .failure(let error):
    print(error.localizedDescription)
}

 

작업이 성공하면 Int가 리턴되고 실패하면 NumberError가 리턴되는 것을 명확히 알 수 있도록 함수의 리턴 형식이 변경되었습니다. 에러 형식을 직접 선언하기 때문에 형식 안정성이 보장되었습니다. 이제는 잘못된 형식으로 인해 발생하는 문제는 컴파일 단계에서 확인되어집니다.

에러를 처리하는 시점이 함수를 호출하는 시점에서 작업 결과를 사용하는 시점으로 이동한 것을 알 수 있습니다. 이것을 Delayed Error Handling 이라고 부릅니다.

 

작업을 성공한 경우만 고려한다면, get을 활용하여 아래와 같이 더욱 간단하게 사용 가능합니다.

 

if let successResult = try? result.get() {
    print(successResult)
}

정리

  • throwing function은 정확히 어떤 에러 형식을 던지는지 파악하기 어려습니다.
  • 컴파일 타임에 에러 형식을 정확히 인식할 수 있다는 것은 형식 안정성이 보장된다는 뜻입니다.

Result Type을 활용하면,

  • 에러 형식이 명시적으로 선언됩니다.
  • 타입 캐스팅 없이 에러 처리가 가능해졌습니다.
  • 형식 추론을 통해 에러 처리 코드가 단순해졌습니다.
  • 작업의 결과를 성공과 실패로 명확히 구분 가능합니다.
  • get 메소드로 에러 처리 코드를 더욱 단순하게 구현 가능합니다.
  • 기존 에러 처리 패턴을 완전히 대체하는 것이 아니라 에러를 처리하는 방식이 다양해진 것입니다.

태그