본문 바로가기

Kotlin

공식 문서로 배우는 코틀린 - 17. Functional (SAM) interfaces

열일곱 번째, 함수형 인터페이스입니다.

 

단지 하나의 추상 메소드만을 갖는 인터페이스를 함수형 인터페이스(functional interface), 또는 단일 추상 메소드(Single Abstract Method, SAM)라고 부릅니다. 함수형 인터페이스는 다수의 추상적이지 않은 멤버를 가질 수 있지만, 추상 멤버는 단 하나입니다.

 

Kotlin에서 함수형 인터페이스를 선언할 때는 fun 수정자를 사용합니다.

fun interface KRunnable {
   fun invoke()
}

 

SAM 변환(conversion)

함수형 인터페이스를 위해 SAM 변환을 사용할 수 있습니다. SAM 변환은 람다 표현식을 사용하여 코드를 보다 간결하고 가독성 있게 만들어 줍니다.

 

함수형 인터페이스를 구현하는 클래스를 직접 만드는 대신에 람다 표현식을 사용할 수 있습니다. 이 람다식이 자동으로 함수형 인터페이스의 구현으로 변하는 것을 SAM 변환이라고 합니다. SAM 변환을 통해 Kotlin에서는 인터페이스가 가진 단일 메소드의 시그니처와 일치하는 어떤 람다 표현식도 (함수형 인터페이스를 구현하는) 코드로 변환할 수 있습니다. 이 코드는 인터페이스의 구현을 동적으로 인스턴스화 합니다.

 

예를 들어 다음과 같은 함수형 인터페이스를 생각해 보겠습니다.

fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

 

만약 SAM 변환을 사용하지 않는 다면, 다음과 같은 코드를 작성해야 할 겁니다.

// 클래스의 인스턴스 생성
val isEven = object : IntPredicate {
   override fun accept(i: Int): Boolean {
       return i % 2 == 0
   }
}

 

대신에, Kotlin의 SAM 변환을 활용하여 다음과 같은 동일한 의미를 갖는 코드를 작성할 수 있습니다.

// 람다를 사용하여 인스턴스 생성
val isEven = IntPredicate { it % 2 == 0 }

 

짧은 람다 표현식이 불필요한 코드를 대체합니다.

fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

val isEven = IntPredicate { it % 2 == 0 }

fun main() {
   println("Is 7 even? - ${isEven.accept(7)}")
}

/* 결과
Is 7 even? - false
*/

 

자바 인터페이스를 위해서도 SAM 변환을 사용할 수 있습니다.

 

생성자 함수가 있는 인터페이스에서 함수형 인터페이스로 마이그레이션

버전 1.6.20부터 Kotlin은 함수형 인터페이스 생성자에 호출 가능 참조(callable reference)를 지원했습니다(호출 가능 참조가 생소한 분은 반드시 링크의 호출 가능 참조를 읽은 후에 나머지 내용을 보시기 바랍니다). 이 것은 생성자 함수가 있는 인터페이스에서 함수형 인터페이스로 이주할 수 있는 소스 수준의 호환 방법을 추가해 줍니다. 다음과 같은 코드를 살펴보겠습니다.

interface Printer {
    fun print()
}

fun Printer(block: () -> Unit): Printer = object : Printer { override fun print() = block() }

 

함수형 인터페이스 생성자에 호출 가능 참조가 가능함을로써, 이 코드는 단지 (다음과 같이) 함수형 인터페이스 선언으로 교체할 수 있습니다.

fun interface Printer {
    fun print()
}

 

생성자는 암묵적으로 만들어지고, ::Printer식으로 함수 참조를 사용하는 어떤 코드든 컴파일 될 것입니다. 예를 들면 다음과 같습니다.

documentsStorage.addPrinter(::Printer)

 

좀 더 풀어서 이렇게 애기할 수 있습니다. 함수형 인터페이스의 생성자에 대해 호출 가능 함수 참조(::)가 만들어지지 않던 때에는 맨 위의 코드처럼 Printer 인터페이스를 정의하고 fun Printer()라는 Printer 객체를 생성하는 함수(그래서 생성자 함수라고 부름)를 만들고, 이 함수에 대한 호출 가능 함수 참조를 사용했습니다. 하지만, 함수형 인터페이스에 암묵적으로 생성자가 만들어지고, 이에 대한 호출 가능 함수 참조를 ::인터페이스이름으로 사용할 수 있게 됨으로써 위의 긴 코드는 아래의 하나의 함수형 인터페이스로 대체될 수 있게 됐습니다.

 

바이너리 호환성을 유지하기 위해 DeprecationLevel.HIDDEN으로 설정한 @Deprecated 어노테이션을 Printer에 추가하여 레거시 함수로 표시합니다.

@Deprecated(message = "Your message about the deprecation", level = DeprecationLevel.HIDDEN)
fun Printer(...) {...}

 

함수형 인터페이스 vs 타입 별칭(type aliases)

위의 SAM 변환 부분에 나오는 코드는 간단하게 함수형 타입을 위한 타입 별칭으로 재작성 할 수 있습니다.

typealias IntPredicate = (i: Int) -> Boolean

val isEven: IntPredicate = { it % 2 == 0 }

fun main() {
   println("Is 7 even? - ${isEven(7)}")
}

 

하지만, 함수형 인터페이스와 타입 별칭은 서로 다른 용도로 사용됩니다. 타입 별칭은 단지 기존에 있는 타입의 별칭입니다. 그러므로, 새로운 타입을 만들지 않습니다. 반면에, 함수형 인터페이스는 새로운 타입을 만듭니다. 일반 함수나 해당 함수의 타입 별칭에는 확장(extensions)을 제공할 수 없지만, 특정 함수형 인터페이스에는 제공할 수 있습니다.

 

타입 별칭은 오로지 하나의 멤버만 가질 수 있는 반면에, 함수형 인터페이스는 다수의 추상적이지 않은 멤버와 하나의 추상 멤버를 가질 수 있습니다. 함수형 인터페이스는 또한 구현할 수 있고 다른 인테페이스를 확장(상속)할 수 있습니다.

 

함수형 인터페이스는 타입 별칭에 비해 보다 유연하며, 보다 많은 기능을 제공합니다. 하지만, 함수형 인터페이스는 특정 인터페이스로 변환이 필요할 수 있기 때문에 구문적으로나 실행시간에 보다 많은 비용이 들 수 있습니다. 어떤 것을 사용할 지 선택할 때는 요구사항을 고려해야 합니다.

  • API가 특정 타입의 매개변수와 반한 타입을 가진 어떠한 함수든 받아들여야 하는 경우에는 단순한 함수형 타입이나 해당하는 함수형 타입에 짧은 이름을 부여하는 타입 별칭을 사용합니다.
  • API가 함수보다 복잡한 개체(entity) - 예를 들어, 함수형 타입 시그니처로 표현할 수 없는 작업이나 규약 - 를 받아들여야 하는 때는 별도의 함수형 인터페이스를 선언합니다.