고차 함수란 무엇인가?
Kotlin에서 고차 함수(higher-order function)는 함수를 인자로 받거나, 함수를 반환하는 함수를 의미합니다. 이러한 특성은 람다 표현식과 긴밀하게 연결되어 있으며, 코드의 재사용성과 가독성을 크게 향상시킵니다.
일반적인 함수는 기본 타입이나 객체를 매개변수로 전달받지만, 고차 함수는 함수 자체를 매개변수로 사용할 수 있습니다. 예를 들어, 아래는 CharSequence에 정의된 일반적인 메서드입니다:
fun CharSequence.substring(range: IntRange): String {
return subSequence(range.start, range.endInclusive + 1).toString()
}
반면, 다음은 고차 함수의 예입니다. filter 함수는 각 문자에 대해 평가되는 람다식을 인자로 받습니다:
inline fun String.filter(predicate: (Char) -> Boolean): String {
return filterTo(StringBuilder(), predicate).toString()
}
이처럼, 고차 함수는 동작을 유연하게 주입할 수 있게 해주며, 호출 시점에 원하는 로직을 전달할 수 있는 장점이 있습니다.
내장 함수와 인라인 최적화
고차 함수는 편리하지만, 내부적으로는 함수 객체 생성 및 클로저 캡처로 인해 성능 오버헤드가 발생할 수 있습니다. 이를 해결하기 위해 Kotlin은 inline 키워드를 제공합니다.
inline으로 선언된 함수는 컴파일 시 호출 위치에 함수 본문이 직접 삽입되므로, 런타임 중 객체 할당과 가상 메서드 호출 비용이 제거됩니다. 이는 특히 반복적으로 호출되는 유틸리티 함수에서 유리합니다.
실제 활용 사례
날짜 기준 최대 타임스탬프 항목 추출
다음은 컬렉션 내 요소들 중 하루 단위로 가장 최근 타임스탬프를 가진 항목만 추출하는 확장 함수입니다. 제네릭과 람다를 조합하여 재사용성을 극대화했습니다.
inline fun <T> Iterable<T>.extractDailyMaxTimeItem(
crossinline timestampSelector: (T) -> Long
): List<T> {
val sortedItems = this.sortedByDescending(timestampSelector).toMutableList()
val result = mutableListOf<T>()
var lastTimestamp = 0L
for (item in sortedItems) {
val currentTimestamp = timestampSelector(item)
if (lastTimestamp == 0L) {
result.add(item)
lastTimestamp = currentTimestamp
continue
}
val lastDate = Calendar.getInstance().apply { timeInMillis = lastTimestamp }
val currentDate = Calendar.getInstance().apply { timeInMillis = currentTimestamp }
val isSameDay = lastDate.get(Calendar.YEAR) == currentDate.get(Calendar.YEAR) &&
lastDate.get(Calendar.DAY_OF_YEAR) == currentDate.get(Calendar.DAY_OF_YEAR)
if (!isSameDay) {
result.add(item)
lastTimestamp = currentTimestamp
}
}
return result
}
사용 예시:
val latestEntries: List<FamilyBean> = dataList.extractDailyMaxTimeItem { it.createTime }
여러 객체의 null 체크를 단순화하기
Kotlin에서는 안전한 호출과 let을 통해 null 처리를 하지만, 여러 변수를 동시에 다뤄야 할 경우 중첩이 깊어질 수 있습니다. 이를 방지하기 위해 다중 null 확인 함수를 작성할 수 있습니다.
fun <A, B> safeRunIfNotNull(a: A?, b: B?, block: (A, B) -> Unit) {
if (a != null && b != null) {
block(a, b)
}
}
fun <A, B, C> safeRunIfNotNull(a: A?, b: B?, c: C?, block: (A, B, C) -> Unit) {
if (a != null && b != null && c != null) {
block(a, b, c)
}
}
이 함수를 사용하면 다음과 같이 깔끔하게 null 안전한 블록을 실행할 수 있습니다:
safeRunIfNotNull(
deviceInfo?.features,
deviceState,
familyDevice?.serialNumber
) { features, state, serial ->
userViewModel.submitPassword(serial, state, features)
showProgress()
}
이 방식은 복잡한 조건 로직을 간결하게 유지하면서도 안정성을 보장합니다.