시스템아 미안해
Chapter 6: Class design - Item 38. 동작과 액션을 전달할 때는 함수 타입 또는 함수형 인터페이스를 사용하라 본문
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의 구현이 하나의 인터페이스에 묶일 필요가 없습니다. 각 함수를 독립적으로 변경할 수 있고, 람다 표현식과 같은 함수 리터럴로 바로 설정할 수도 있습니다.