Advanced Kotlin – Invoke operator

Invoke operator 란?

Invoke (작동시키다 또는 불러오다) operator (연산자) 로, 한마디로 +(plus), *(times), a..b(rangeTo) 등과 같이 기호로 쓸 수 있는 것을 의미한다.

문제

class Request(val method: String, val query: String, val contentType: String)

class Status(var code: Int, var descriprtion: String)

class Response(var contents: String, var status: Status) {
    fun status(status: Status.() -> Unit) {

    }
}

class RouteHandler(val request: Request, val response: Response) {
    var executeNext = false
    fun next() {
        executeNext = true
    }
}

fun routeHandler(path: String, f: RouteHandler.() -> Unit): RouteHandler.() -> Unit = f

fun response(response: Response.() -> Unit) {}

fun main(args: Array<String>) {
    routeHandler("/index.html") {
        if (request.query != "") {
            // process
        }

        response {
            status {
                code = 404
                descriprtion = "not found"
            }
        }
    }
}

RouteHandler 라는 클래스가 있는데, 이 클래스는 Request 클래스와 Response 클래스를 각각 속성으로 가지고 있고, Response는 또 Status라는 클래스를 속성으로 가지고 있다.

그리고 이 클래스를 메소드로 wrap 한 것이 밑의 routeHandler, response 등이다.

자 그러면 28번째 줄 부터 봐보자. response 라는 메소드는 Response 클래스를 인수로 가지는 람다 파라미터를 가지고 있다. Response는 Status라는 클래스를 속성으로 가지고 있다고 했는데, status를 설정하려면 저런 식으로 status { }  로 감싸야 한다.

간단한 클래스면 문제가 없겠지만 언제나와 그렇듯이 짧은 코드만 존재할 수는 없다. 저런게 몇개나 더 들어간다고 하면 훌륭한 웨이브가 생기지 않을까.

그래서, 우리는 저 status { } 를 없애고 response { } 단독으로만 쓰고 싶다. 그럴 때 등장하는 것이 Invoke operator 이다.

일단 response 메소드를 제거해보자.

Invoke Operator : Expression 'response' of type 'Respose' cannot be invoked as a function. The function 'invoke()' is not found

이런 오류가 뜬다. Quick Fix를 해보자.

이제 Response 클래스에 invoke 메소드가 생긴다. 우리가 활용하고 싶은건 Status 속성이므로 invoke가 Status를 인수로 가지는 람다 파라미터로 만들어보자. 겸사겸사 위에 safe delete 가 가능하다고 나오는 status 메소드도 같이.

class Response(var contents: String, var status: Status) {
    operator fun  invoke(function: Status.() -> Unit) { // modifier operator

    }
}

이런 모양새가 되었으면, 이제 밑의 main 메소드에 있는 status { } 를 제거해보자.

response {
            code = 404
            descriprtion = "not found"
}

자, 이제 우리가 원하는 방식으로 되었다.

왜 이것이 가능할까?

원리

Manager란 클래스를 하나 만들어보고, main에 그에 해당하는 참조 변수를 만들어보자.

fun main(args: Array<String>) {
    val manager = Manager()
}

class Manager {

}

그리고 만들어진 manager 라는 변수에 인수로 “Do something for me!” 를 넘겨보자. manager("Do something for me!") 이런 식으로 하고, Quick Fix를 해 줄 경우 Manager 클래스에 invoke 메소드가 생긴다. 인수로는 String를 받을거니 String로 선언하고, println 하는 식으로 구현을 해보면..

fun main(args: Array<String>) {
    val manager = Manager()
    manager("Do something for me!")
}

class Manager {
    operator fun  invoke(value: String) {
        println(value)
    }
}

이런 식이 될 것이다.

operator 는 메소드에 붙는 Modifier(수식어)의 일종으로, +, *, .. 와 같은 기호의 기능을 확장하는 기능을 수행한다.

예를 들면, 이런 식이다.

  • a + b 는 a.plus(b)
  • a * b 는 a.times(b)
  • a..b는 a.rangeTo(b)
  • a in b 는 a.contains(b)

그리고 우리의 invoke는.

manager.invoke("Do something for me!") 를 manager("Do something for me!") 식 으로 작동한다.

즉, 실제로 위의 response 에서도 response.invoke(Status.() -> Unit) 가 되어야 할 것을, response { }  로 줄인 셈이다. 당연히 Response 클래스는 Status 를 속성으로 가지고 있고, invoke 메소드는 Status 를 람다 파라미터로 가지고 있으니 람다 익스텐션 (Lambda Extension) 으로 활용이 가능, Status의 속성에 접근이 가능한 것이다.

이러한 Operator들을 확장하는 것을 Operator overloading (연산자 오버로드) 라고 부르며, 코틀린에서는 이러한 연산자 오버로드 기능을 적극적으로 활용할 수 있도록 다양한 operator (+, *와 같은 고정된 기호 표현과 코틀린에서 제공하는 우선 기호 등) 를 제공한다.

결론

이 Invoke operator를 가지고 DSL(Domain-specific Language) 구현에도 쉽게 사용할 수도 있고, 사용할 수 있는 곳이 매우 많다. 진작에 알았으면 하는 아쉬움은 있다. 이제라도 알았으니 남은건 라이브러리에 직접 활용하는 것이다.

사용 가능한 operator는 invoke 외에도 매우 많다. 이 것을 적절히 활용해서 짧고 효율적인 코드를 짜는 데에 큰 도움이 될 수 있도록 노력해야겠다.

참조 링크: https://kotlinlang.org/docs/reference/operator-overloading.html