반년전즈음 스터디모임에서 Swift 3.0의 Array 에 대해 다루다가 map 이라는 함수를 보고 의문점이 생겼다. 분명히 내가 기억하기로는 Swift 2.2에서 Array의 map 함수는

Array.map(transform: T -> U)

이와 같이 생겼던걸로 기억한다. 이랬던 map이 Swift 3.0에서 형태가 바뀌었다.

map(_ transform: (Element) throws -> T) rethrows -> [T]

Swift 2.0 에서 do-catch Block 을 통한 에러 처리가 생기면서 바뀐 것으로 짐작한다. throws는 do-catch 와 try 를 쓰면서 자주 봤기 때문에, throws 보다는 rethrows 에 의문이 생겼다.

누구냐 너..

먼저, throws 를 살펴보겠다. Apple 의 Swift 3.0 문서에 따르면, throws와 rethrows 는 둘 다 함수의 타입의 한 부분이라고 표현한다. 또한, non-throwing 함수는 throwing 함수의 하위호환 된다. 이것을 확인하기 위해 다음 코드를 보자.

func twoTimes(_ number: Int) -> Int {
    return number * 2
}

let result = [1,2,3,4].map(twoTimes)
print(result)
// [2,4,6,8]

twoTimes() 함수는 throws를 명시하지 않기 때문에, non-throwing 함수다. 그리고 분명 map의 선언부에서는 throwing 함수를 parameter 로 받는다고 명시가 되어있었다. 하지만, non-throwing 함수는 throwing 함수의 하위호환되므로 위 코드는 정상 동작 한다. 하지만 반대는 성립하지 않는다. 다음 코드를 보자.

enum MyError: Error {
    case cannotDivide
}

func divideNumber(first: Float, second: Float) throws -> Float {
    if second == 0 {
        throw MyError.cannotDivide
    }
    return first/second
}

func calculateFunction(function: (Float, Float) -> Float) {
    print(function(2, 3))
}

calculateFunction(function: divideNumber) // Compile Error

약간 코드가 이상하긴 하지만 이해가리라 믿는다. 결과부터 말하자면, 위 코드는 동작하지 않는다. non-throwing 함수를 parameter 로 받는 함수인 calculateFunction 에 throwing 함수인 divideNumber를 넘겨주는 부분에서 에러가 발생한다. 에러를 throws 하는 방법은 위 코드에서 보인대로, Error Protocol 을 만족하는 Enumeration 을 만들어서 case 를 정의하면 된다. 그리고 함수에 throws 한다고 표시를 해주고, 에러상황에 처했을 때, 적당한 case 를 throw 해주면 된다. 자세한건 애플 문서를 참고 하시라.

이제 rethrows 에 대해 살펴보자. 패러미터들 중에 함수 하나가 에러를 throws 하는 경우에만, 에러를 throws 할 수 있게 암시하도록 함수를 선언할 때, rethrows 키워드를 넣어야 한다.

위에서 에러난 코드를 적절히 수정해서 다시 보자.

enum MyError: Error {
    case cannotDivide
}

func divideNumber(first: Float, second: Float) throws -> Float {
    if second == 0 {
        throw MyError.cannotDivide
    }
    return first/second
}

func calculateFunction(function: (Float, Float) throws -> Float) rethrows {
    print(try function(2, 0))
}

do {
    try calculateFunction(function: divideNumber)
} catch let error as MyError {
    switch error {
    case .cannotDivide:
         print("0으로 나누었다.")
    }
}

calculateFunction 함수가 parameter 로 throwing 함수를 받도록 명시하고, 해당함수에서 받은 에러를 다시 throws 할수 있도록 명시를 해준다. 여기서 calculateFunction 함수에서 parameter 인 function 을 사용할 때를 눈여겨 보자. function 이 throwing 함수 이기 때문에, 일반적인 경우라면,

func calculateFunction(function: (Float, Float) throws -> Float) {
    do {
        print(try function(2, 0))
    } catch let error as MyError {
  	      // Error Handling..
    }
}

이렇게 에러를 처리해야 할것이다. 하지만 우리는 이 함수내에서 에러를 처리하지 않고, 한번더 throws 할 것이기 때문에 여기서는 단순히 try 만 해주면 된다.

catch 는 calculateFunction(function: divideNumber) 를 호출한 시점에 do-catch 블럭으로 에러 처리를 해주면 된다.

여기서

func calculateFunction(function: (Float, Float) throws -> Float) throws { // rethrows -> throws 로 변경
    print(try function(2, 0))
}

이렇게 코드를 변경해보았다. 여전히 코드는 문제없이 돌아간다. 이에 대해 애플의 Swift3 책에는 이렇게 언급하고 있다.

A throwing method can’t override a rethrowing method, and a throwing method can’t satisfy a protocol requirement for a rethrowing method.

throws 와 rethrows 역시 한방향 호환된다고 이해하면 될 것 같다. throwing 함수에서 에러를 받아서 다시 throwing(rethrowing) 하는 경우에 대해 꼭 rethrows 라고 명시할 필요는 없지만, 받지도 않은 에러를 rethrowing 한다고 명시하는 것은 불가능하다. 그러나, 에러를 함수로부터 받아서 다시 throwing(rethrowing) 하는지 아니면 그냥 자신이 에러를 throwing하는지 함수선언부로 추측하기 위해서는 꼭 구분해서 명시하는 편이 좋을 것 같다.