본문 바로가기
개발/Java, Kotlin

[Effective Kotlin] Item 38: Use function types or functional interfaces to pass operations and actions

by 달사쿠 2021. 10. 7.
반응형

Summary

  • 행동을 나타내기 위해, 표준 인터페이스나 추상 클래스 대신 함수 타입이나 함수형 인터페이스를 선호합시다.
  • 함수 타입이 좀 더 자주 사용 됩니다. 여러 번 사용되거나 타입이 길어지면 type alias를 사용합시다.
  • 함수형 인터페이스는 주로 Java 상호 운용성을 위해 선호되며, 표현하려는 것이 임의의 함수 이상으로 복잡한 경우에 선호됩니다.

(요약만 봐서는 사실 잘 이해가 안된다 살펴보자!)


Intro

많은 언어에는 fuction type이라는 개념이 없습니다. 대신 하나의 메소드를 갖는 interface를 사용합니다. 

이런 인터페이스를 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) {
        //...
    }
})

 

하지만 코틀린에서는 보다 자유를 주는 2가지 메커니즘을 제공합니다.

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

// Functional interface 선언
fun interface OnClick {
    fun onClick(view: View)
}
// Functional interface 사용
fun setOnClickListener(listener: OnClick) {
    //..
}

 

위 두가지 중 하나를 사용할 때, argument는 다음과 같이 정의할 수 있습니다.

  • lamabda 식 또는 익명 함수
setOnClickListener { /*...*/ }
setOnClickListener(fun(view) { /*...*/ })

 

  • 함수 참조 또는 제한된 함수 참조
setOnClickListener(::println)
setOnClickListener(this::showUsers)

 

  • 선언된 함수 type 나 함수형 인터페이스를 구현하는 객체
class ClickListener: (View)->Unit {
    override fun invoke(view: View) {
        //...
    }
}
setOnClickListener(ClickListener())

 


type aliases와 함께 function types 사용하기 

함수 타입이 복잡해지면, type aliase(타입 별칭)을 사용하여 이름을 정할 수 있습니다.

typealias OnClick = (View) -> Unit

 

type alias는 "별명"처럼 타입에 대한 다른 이름을 제공합니다.

컴파일 시간 동안 type alias는 그들이 나타내는 유형으로 대체됩니다. 이 type alias는 제네릭이 될 수도 있습니다.

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

 

함수 타입 매개변수는 이름을 지을 수 있습니다.

매개 변수 이름을 지정하기 시작하면 타입이 길어지는 경향이 있는데,

type alias와 함께 사용하면 가독성을 높일 수 있고, IDE에서 함수를 사용할 때 이름을 제안해주기도 합니다.

typealias OnClick = (view: View) -> Unit
fun setOnClickListener(listener: OnClick) { /*...*/ }

 


functional interfaces (함수형 인터페이스)를 사용하는 이유

함수형 인터페이스는 더 무거운 방법(?!)입니다. 

인터페이스를 정의해야 하지만, 그 대신

  • 함수형 인터페이스는 새로운 이름의 타입을 정의합니다. 
  • 핸들러 함수는 다르게 이름을 정할 수 있습니다. (함수 유형에서는 항상 호출)
  • 다른 언어와의 상호 운용성이 더 좋습니다.
interface SwipeListener {
    fun onSwipe()
}

fun interface FlingListener {
    fun onFling()
}

fun setOnClickListener(listener: SwipeListener) {
    listener.onSwipe()
}

fun main() {
    val onSwipe = SwipeListener { println("Swiped") }
    setOnClickListener(onSwipe) // Swiped
    
    val onFling = FlingListener { println("Touched") }
    setOnClickListener(onFling) // Error: Type mismatch
}

 

함수형 인터페이스는 non-abstract 함수를 추가할 수 있고, 다른 인터페이스를 구현할 수도 있습니다.

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

fun interface OnClick: ElementListener<View> {
    fun onClick(view: View)	//Non-abstract function
    
    fun invoke(element: View) {  //다른 인터페이스 구현
        onClick(element)
    }
}

 

Kotlin이 아닌 다른 언어에서 사용할 클래스를 설계할 때, 인터페이스가 더 깨끗하고 더 잘 지원됩니다.

다른 언어는 type alias나 이름 제안을 볼 수 없습니다.

일부 언어(특히 Java)에서 사용할 때는, Kotlin 함수 타입을 사용하기 위해 함수에서 Unit을 명시적으로 반환해야 합니다. 

 

//Kotlin
class CalenderView() {
    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 -> {});

 

전반적으로 함수형 인터페이스가 더 선호되는 이유는 다음과 같습니다.

  • Java 상호 운용성
  • 기본형 타입(primitive type)에 대한 최적화
  • 단순히 함수를 표현하는 것이 아닐 때, 구체적인 계약을 표현해야 함

함수형 인터페이스는 Kotlin 1.4에서 도입되었으며, 현재 함수 타입에 대한 IntelliJ의 지원은 함수형 인터페이스보다 낫습니다.

 

 


여러 추상 메서드가 있는 인터페이스를 사용해 action을 표현하지 마세요

 

Java에서 Kotlin으로 전환한 개발자들 사이에서 관찰되는 또 다른 특징은

여러 추상 메서드가 있는 인터페이스를 사용하여 action을 표현한다는 것입니다.

class CalendarView {
    var listener: Listener? = null
    
    interface Listener {
        fun onDateClicked(date: Date)
        fun onPageChanged(date: Date)
    }
}

 

이런 패턴은 함수형 인터페이스가 지원되지 않았을 때 Java에서 널리 사용되었습니다.

API를 사용하는 관점에서는 함수 타입이나 함수형 인터페이스를 보유하는 별도의 속성으로 설정하는 것이 좋습니다.

 

//함수형 인터페이스 사용
class CalendarView {
    var onDateClicked: OnDateClicked? = null
    var onPageChanged: OnPageClicked? = null
}

//함수 타입 사용
class CalendarView {
    var onDateClicked: ((date: Date) -> Unit)? = null
    var onPageChanged: ((date: Date) -> Unit)? = null
}

 

이렇게 하면 onDateClicked 및 onPageChanged 구현을 인터페이스에서 함께 묶을 필요가 없습니다.

이런 기능을 독립적으로 변경할 수 있으며, lambda식과 기타 모든 옵션을 사용해 설정할 수 있습니다.

반응형

댓글