시스템아 미안해

Chapter 6: Class design - Item 38. 동작과 액션을 전달할 때는 함수 타입 또는 함수형 인터페이스를 사용하라 본문

책/Effective Kotlin

Chapter 6: Class design - Item 38. 동작과 액션을 전달할 때는 함수 타입 또는 함수형 인터페이스를 사용하라

if else 2026. 3. 5. 16:00

많은 언어에는 **함수 타입(function type)**이라는 개념이 없습니다.
대신 메서드가 하나만 있는 인터페이스를 사용합니다.

이러한 인터페이스를 **SAM(Single-Abstract Method)**이라고 합니다.

다음은 뷰(view)가 클릭되었을 때 어떤 동작을 수행해야 하는지를 전달하기 위해 사용되는 SAM의 예시입니다.

interface OnClick {
    fun onClick(view: View)
}

함수가 SAM을 인자로 받을 때는 해당 인터페이스를 구현한 객체를 직접 전달해야 합니다.

fun setOnClickListener(listener: OnClick) {
    //...
}

setOnClickListener(object : OnClick {
    override fun onClick(view: View) {
        // ...
    }
})

그러나 Kotlin은 이보다 더 자유로운 두 가지 메커니즘을 제공합니다.

  • 함수 타입(function type)
  • 함수형 인터페이스(functional interface)
// Function type usage
fun setOnClickListener(listener: (View) -> Unit) {
    //...
}

// Functional interface declaration
fun interface OnClick {
    fun onClick(view: View)
}

// Functional interface usage
fun setOnClickListener(listener: OnClick) {
    //...
}

이 두 방식을 사용하면 인자를 다음과 같이 다양한 형태로 전달할 수 있습니다.

람다 표현식 또는 익명 함수로 전달할 수 있습니다.

setOnClickListener { /*...*/ }
setOnClickListener(fun(view) { /*...*/ })

함수 레퍼런스 또는 바운드 함수 레퍼런스로 전달할 수 있습니다.

setOnClickListener(::println)
setOnClickListener(this::showUsers)

선언된 함수 타입이나 함수형 인터페이스를 구현하는 객체로도 전달할 수 있습니다.

class ClickListener : (View) -> Unit {
    override fun invoke(view: View) {
        // ...
    }
}

setOnClickListener(ClickListener())

함수 타입과 함수형 인터페이스 모두 위의 사용 방식을 지원합니다. 다만 일반적으로 함수 타입이 동작과 액션을 객체로 표현하는 표준 방법으로 간주됩니다.


타입 별칭과 함께 함수 타입 사용하기

앞서 설명했듯이, **함수 타입(function type)**은 연산이나 동작을 객체로 표현하는 표준적인 방법입니다.

만약 특정 함수 타입에 이름을 붙이고 싶다면 typealias를 사용할 수 있습니다.

 
typealias OnClick = (View) -> Unit

typealias는 어떤 타입에 다른 이름을 붙여 주는 기능입니다. 일종의 **별명(nickname)**과 같습니다.

사람을 부를 때 본명(full name)을 사용하든 별명을 사용하든 같은 사람을 의미하는 것과 동일한 개념입니다.
typealias 역시 마찬가지로 컴파일 과정에서 실제로는 해당 타입으로 치환됩니다.

또한 typealias는 제네릭(generic) 형태로도 정의할 수 있습니다.

 
typealias OnClick<T> = (T) -> Unit

함수 타입의 파라미터에는 이름을 붙일 수 있습니다.

파라미터에 이름을 붙이면 IDE가 해당 이름을 기본값으로 자동 제안해 주는 장점이 있습니다.

하지만 파라미터에 이름을 붙이기 시작하면 타입 선언이 길어지는 경향이 있습니다.
그래서 이 기능은 보통 typealias와 함께 사용하는 경우가 많습니다.

 
typealias OnClick = (view: View) -> Unit

fun setOnClickListener(listener: OnClick) { /*...*/ }

 


함수형 인터페이스를 사용해야 하는 경우

함수형 인터페이스는 함수 타입보다 무거운 방식입니다. 직접 선언해야 하는 비용이 있지만, 그 대신 다음과 같은 이점을 얻을 수 있습니다.

  • 새로운 명명된 타입(named type)을 정의합니다.
  • 핸들러 함수에 의미 있는 이름을 붙일 수 있습니다. 함수 타입에서는 핸들러 이름이 항상 invoke로 고정됩니다.
  • 다른 언어와의 상호운용성이 더 뛰어납니다.

특히 타입 안전성 측면에서 함수형 인터페이스가 유리한 경우가 있습니다.

interface SwipeListener {
    fun onSwipe()
}

fun interface FlingListener {
    fun onFling()
}

fun setOnClickListener(listener: SwipeListener) {
    // when swipe happens
    listener.onSwipe()
}

fun main() {
    val onSwipe = SwipeListener { println("Swiped") }
    setOnClickListener(onSwipe) // Swiped

    val onFling = FlingListener { println("Touched") }
    setOnClickListener(onFling) // Error: Type mismatch
}

함수형 인터페이스(functional interface)는 추상 메서드 외에도 일반(non-abstract) 메서드를 추가할 수 있습니다.

또한 다른 인터페이스를 함께 구현(implement)하는 것도 가능합니다.

interface ElementListener<T> {
    fun invoke(element: T)
}

fun interface OnClick : ElementListener<View> {
    fun onClick(view: View)

    override fun invoke(element: View) {
        onClick(element)
    }
}

Kotlin이 아닌 다른 언어(특히 Java)에서 사용될 클래스를 설계할 때는 인터페이스가 더 깔끔하고 지원도 잘 됩니다. 다른 언어에서는 타입 별칭을 인식하지 못하고, 파라미터 이름 제안도 동작하지 않습니다. 또한 Kotlin 함수 타입은 Java에서 사용할 때 반드시 무언가를 반환해야 하고, Unit도 명시적으로 반환해야 합니다.

// Kotlin
class CalendarView() {
    var onDateClicked: ((date: Date) -> Unit)? = null
    var onPageChanged: OnDateClicked? = null
}

fun interface OnDateClicked {
    fun onClick(date: Date)
}
// Java
CalendarView c = new CalendarView();
c.setOnDateClicked(date -> Unit.INSTANCE);
c.setOnPageChanged(date -> {});

또 다른 중요한 이점은 함수형 인터페이스는 래핑 타입이 아니라는 점입니다. 함수 타입은 내부적으로 제네릭 타입이기 때문에 기본 타입(primitive)을 사용할 수 없습니다. 예를 들어 Java에서 Int 파라미터가 int가 아닌 Integer로 해석되어 불필요한 객체 생성이 발생할 수 있습니다. 함수형 인터페이스는 이 문제가 없습니다.

정리하면, 함수형 인터페이스를 선호해야 하는 주요 이유는 다음과 같습니다.

  • Java 상호운용성이 필요한 경우
  • 기본 타입 최적화가 필요한 경우
  • 단순한 함수가 아닌 계약(contract)을 가진 인터페이스로 표현해야 하는 경우

이 중 어느 것도 해당하지 않는다면 함수형 인터페이스 대신 함수 타입을 사용하세요.


추상 메서드가 여러 개인 인터페이스로 액션을 표현하는 것을 피하라

Java에서 Kotlin으로 넘어온 개발자들 사이에서 종종 볼 수 있는 패턴이 있습니다. 여러 추상 메서드를 가진 인터페이스로 액션을 표현하는 것입니다.

class CalendarView {
    var listener: Listener? = null

    interface Listener {
        fun onDateClicked(date: Date)
        fun onPageChanged(date: Date)
    }
}

이 패턴은 함수형 인터페이스가 지원되지 않던 Java 시절에 유행했습니다. 하지만 이는 대부분 편의를 위한 선택일 뿐이며, API 사용자 입장에서는 각 동작을 독립적인 프로퍼티로 분리하는 것이 훨씬 낫습니다.

// Using functional interfaces
class CalendarView {
    var onDateClicked: OnDateClicked? = null
    var onPageChanged: OnPageClicked? = null
}

// Using function types
class CalendarView {
    var onDateClicked: ((date: Date) -> Unit)? = null
    var onPageChanged: ((date: Date) -> Unit)? = null
}

이렇게 하면 onDateClicked와 onPageChanged의 구현이 하나의 인터페이스에 묶일 필요가 없습니다. 각 함수를 독립적으로 변경할 수 있고, 람다 표현식과 같은 함수 리터럴로 바로 설정할 수도 있습니다.