What Happened to NSMethodSignature?

NSInvocation에 대해 찾아보다가 Swift 공식 블로그에서 찾은 글을 간단히 번역해본다.

https://developer.apple.com/swift/blog/?id=19

What Happened to NSMethodSignature?

코코아 프레임워크를 Swift로 옮기는 것은 우리 스스로가 우리의 API를 새로운 관점에서 볼 수 있는 좋은 기회가 되었습니다. 우리는 Swift의 목표에 맞지 않는다고 생각되는 클래스들을 찾았고, 우리의 우선순위는 주로 안전성에 맞췄습니다. 예를 들어 동적인 메소드 호출(dynamic method invocateion)과 관련된 클래스들은 Swift에 반입되지 않습니다. 이러한 클래스에는 NSInvocationNSMethodSignature가 있지요.

우리는 최근에 이 클래스들이 빠져있음을 발견한 한 개발자로부터 버그 리포팅을 받았습니다. 이 개발자는 Objctive-C에서 메소드 인자들의 타입을 검사하는데 NSMethodSignature를 사용하고 있었고, Swift로 마이그레이션하는 과정에서 이 클래스를 사용할 수 없다는 것을 알았습니다. 실제 그 코드는 인자 타입이 정해지지 않은 HTTP 핸들러릘 받도록 되어 있었습니다. 예를 들면 이런 것들이죠.

func handlerRequest(request: HTTPRequest, 
            queryStringArguments: [String: String]) { }
func handlerRequest(request: HTTPRequest, jsonBody: JSON) { }

Objective-C에서는 NSMethodSignature를 사용하여 첫번째 메소드는 [String: String]타입의 인자를 필요로하며, 두 번째 메소드는 JSON 타입의 인자를 필요로 한다는 것을 알아낼 수 있습니다. 하지만 Swift는 충분히 강력한 언어이며, 언어가 제공하는 기능만으로도 이러한 시나리오를 다룰 수 있습니다.

아래는 같은 문제를 해결하는 Swift 코드입니다.

struct HTTPRequest { /*...*/ }

protocol HTTPHandlerType {
    typealias Data
    func handle(request: HTTPRequest, data: data) -> Bool
}

먼저 우리는 HTTPRequest를 다룰 수 있는 핸들러를 위한 프로포토콜을 하나 정의합니다. 이 ㅍ로토콜은 매우 심플하고 하나의 메소드만을 필요로합니다. 왜 프로토콜일가요? 프로토콜은 실제 구현을 클라이언트(여기서는 해당 프로토콜을 따르는 타입)에 이관하여 큰 유연성을 확보할 수 있기 때문입니다. 만약 HTTPHandler라는 클래스를 만든다면, 클라이언트쪽에서는 클래스를 만들어도 되지만 구조체나 열거체를 써도 무방합니다.

class HTTPServer {
    func addHandler<T: HTTPHandlerType>(handler: T) {
        handlers.append { (request: HTTPRequest, args: Any) -> Bool in
            if let typedArgs = args as? T.Data {
                return hander.handle(request, data: typedArgs)
            }
            return false
        }
    }
    ...
}

또, HTTPServer 클래스는 HTTPHandlerType을 파라미터로 받는 제네릭 메소드를 갖고 있습니다. 핸들러의 연관타입을 이용하여 args 파라미터는 조건에 따라 특정한 타입으로 다운캐스팅할 수 있습니다. 여기에 프로토콜의 강점이 있습니다. HTTPServer는 핸들러가 요청에 대해 어떤식으로 반응하는지에 대해서는 몰라도 되며, 핸들러 그 자체의 어떤 특성에 대해서도 알 필요가 없습니다. 우리가 알 필요가 있는 것은 그 값이 요청을 핸들링할 수 있다는 것 뿐입니다.

위 예에서 addHandler는 두 번째 인자의 타입이 일치하는 경우에 핸들러 호출값을 리턴하는 클로저를 배열에 추가해줍니다.

class HTTPServer {
    //...
    private var handlers: [(HTTPRequest, Any) -> Bool)] = []
    func dispatch(req: HTTPRequest, args: Any) -> Bool {
        for handler in handlers {
            if hanlder(req, args) {
                return true
            }
        }
        return false
    }
}

서버 객체가 요청을 받으면 서버는 핸들러중에서 주어진 데이터 타입을 처리할 수 있는 핸들러가 있고, 그 중에서 true를 내는 핸들러가 하나라도 있다면 true를 그렇지 않다면 false를 리턴할 것입니다.

그러면 커스텀 핸들러를 만들어보겠습니다.

class MyHandler: HTTPHandlerType {
    func handle(request: HTTPRequest, data: Int) -> Bool {
        return data > 5
    }
}

let server = HTTPServer()
server.addHandler(MyHandler())
server.dispatch(HTTPRequest(...), args: "x") // --> false
server.dispatch(HTTPRequest(...), args: 5) // --> false
server.dispatch(HTTPRequest(...), args: "10") // --> false
server.dispatch(HTTPRequest(...), args: 10) // --> true

프로토콜과 제네릭을 결합하는 테크닉을 동원하여 다양한 타입의 HTTP 핸들러를 등록하는 서버를 생성하는 Swift 코드를 작성할 수 있습니다. 이러한 접근은 컴파일 시점에 타입 안정성을 보장받을 수 있으며, 실행 성능또한 매우 좋습니다.