invoke 관례: 함수처럼 호출할 수 있는 객체
invoke 관례는 괄호를 사용한다(operator 변경자가 붙은 invoke 메서드 정의가 들어있는 클래스를 호출할 수 있다)
클래스 안에서 invoke 메서드 정의
class Greeter(val greeting: String) {
operator fun invoke(name: String){ //Greeter 안에 invoke 메서드를 정의
println("$greeting, $name")
}
}
fun main() {
val bavarianGreeter = Greeter("Servus")
bavarianGreeter("Dmitry") //Greeter 인스턴스를 함수처럼 호출한다
}

- bavarianGreeter("Dmitry")는 내부적으로 bavarianGreeter.invoke("Dmitry")로 컴파일된다.
- invoke 또한 다른 관례랑 마찬가지로 미리 정해둔 이름을 사용한 메서드를 통해 짧고 간결하게 식을 쓸수 있게 해준다
- invoke 메서드의 시그니처에 대한 요구사항은 없으므로 원하는 대로 파라미터 갯수나 타입을 지정할 수 있다.
- invoke를 오버로딩 할 수도 있는데,오버로딩한 invoke가 있는 클래스의 인스턴스를 함수처럼 사용할 때는 오버로딩한 여러 시그니처를 모두 다 활용할 수 있다.
invoke 관례와 함수형 타입
인라인 하는 람다를 제외한 모든 람다는 함수형 인터페이스를 구현하는 클래스로 컴파일된다. 각 함수형 인터페이스 안에는 그 인터페이스 이름이 가리키는 개수만큼 파라미터를 받는 invoke 메서드가 들어있다.
람다를 함수처럼 호출하면 이 관례에 따라 invoke 메서드 호출로 변환되는데, 이 사실을 알면 람다를 여러 메서드로 분리하면서도 여전히 분리 전의 람다처럼 외부에서 호출할 수 있는 객체를 만들 수 있다.
함수 타입을 확장하면서 invoke를 오버라이딩
data class Issue(
val id: String, val project: String, val type: String,
val priority: String, val description: String
)
class ImportantIssuesPredicate(val project: String) : (Issue) -> Boolean { //함수 타입을 부모 클래스로 사용한다
override fun invoke(issue: Issue): Boolean { //invoke 메서드 구현
return issue.project == project && issue.isImportant() //invoke 메서드를 구현한다.
}
private fun Issue.isImportant(): Boolean {
return type == "Bug" &&
(priority == "Major" || priority == "Critical")
}
}
fun main() {
val i1 = Issue(
"IDEA-154446", "IDEA", "Bug", "Major",
"Save settings failed"
)
val i2 = Issue(
"KT-12183", "Kotlin", "Feature", "Normal",
"Intention: convert several calls on the same receiver to with/apply"
)
val predicate = ImportantIssuesPredicate("IDEA")
for (issue in listOf(i1, i2).filter(predicate)) { //술어를 filter에 넘긴다.
println(issue.id)
}
}
- 술어의 로직이 복잡해 여러 메서드로 나누고 싶어 위와 같이 분리하였다.
- 람다를 함수 타입 인터페이스를 구현하는 클래스로 변환하고, 클래스의 invoke 메서드를 오버라이딩 하면 위와 같은 리펙토링이 가능해 진다.
- 람다 본문에서 따로 분리해 낸 메서드가 영향을 끼치는 영역을 최소화 할 수 있다.(술어 내부애서만 람다를 분리해낸 메서드 확인 가능)
- 여러 관심사를 깔끔하게 분리해낼수 있는 것이 가장 큰 장점이다.
DSL의 invoke 관례: Gradle에서 의존관계 정의
Gradle 의존 관계 여러가지 경우 지원
dependencies.compile("junit:junit:4.11") // 1번 형식
dependencies{ //2번 형식
compile("junit:junit:4.11")
}
- 1, 2 번 형식 모두 지원하고 싶다( 설정 많으면 2번 DSL 형식, 설정 적으면 1번 함수 호출 형식으로 사용할 수 있다.)
- 1번 형식 : dependencies 변수에 대해 compile 메서드를 호출한다.
- 2번 형식: dependencies 안에 람다를 받는 invoke 메서드를 정의하면 된다.
유연한 DSL 문법을 제공하기 위해 invoke 사용하기
class DependencyHandler {
fun compile(coordinate: String) { //일반적인 명령형 API 정의
println("Added dependency on $coordinate")
}
operator fun invoke(
body: DependencyHandler.() -> Unit
) {// invoke를 정의해 DSL 스타일 API를 제공
body() //this가 함수의 수신객체가 되므로 this.body()와 같다
}
}
fun main() {
val dependencies = DependencyHandler()
dependencies.compile("org.jetbrains.kotlin:kotlin-stdlib:1.0.0")
dependencies{
compile("org.jetbrains.kotlin:kotlin-stdlib:1.0.0")
}
}
- dependencies 객체는 DependencyHandler 클래스의 인스턴스다.
- compile, invoke 메서드가 있다
- invoke 는 수신 객체 지정 람다를 파라미터로 받으며, 이 람다의 수신객체는 다시 DependencyHandler다
- DependencyHandler가 묵시적 수신 객체이므로, 람다안에서 compile과 같은 본인의 메서드를 직접 호출할 수 있다.
- dependencies{ ...} 이러한 호출은 dependencies.invoke({...})와 같이 변환되어 호출된다고 보면 된다.
- dependencies를 함수처럼 호출하면서 람다를 인자로 넘긴다.
- 람다는 확장 함수 타입이며, 지정한 수신객체 타입은 DependencyHandler이다.(invoke 메서드는 이 수신 객체 지정 람다를 호출)
- invoke가 DependencyHandler 메서드이므로 메서드 내부에서 this는 DependencyHandler 이다.
- 결론적으로 invoke안에서 DependencyHandler 타입의 객체를 따로 명시하지 않고, compile()을 호출할 수 있다.