Theme:

코틀린에서 함수는 일급 시민(first-class citizen)이다. 변수에 저장하고, 파라미터로 넘기고, 반환값으로 쓸 수 있다. 자바 8의 람다와 비슷해 보이지만, 코틀린의 람다는 바깥 변수를 수정할 수 있고, trailing lambda 문법으로 더 깔끔하게 쓸 수 있다. 면접에서 고차 함수와 람다는 필수 주제이니 제대로 정리해보자.

함수 타입 — (파라미터) -> 반환값

코틀린에서는 함수의 타입을 명시할 수 있다.

KOTLIN
// 함수 타입 선언
val sum: (Int, Int) -> Int = { a, b -> a + b }
val isPositive: (Int) -> Boolean = { it > 0 }
val greet: () -> String = { "안녕하세요" }
val printMsg: (String) -> Unit = { println(it) }

함수 타입의 다양한 형태

KOTLIN
// 수신 객체가 있는 함수 타입 (확장 함수 타입)
val toUpperCase: String.() -> String = { this.uppercase() }
"hello".toUpperCase()  // "HELLO"

// nullable 함수 타입
val callback: ((String) -> Unit)? = null
callback?.invoke("호출")  // null이면 실행 안 됨

// 함수 타입을 반환하는 함수
fun getOperation(type: String): (Int, Int) -> Int = when (type) {
    "add" -> { a, b -> a + b }
    "multiply" -> { a, b -> a * b }
    else -> { _, _ -> 0 }
}

고차 함수 — 함수를 파라미터로 받거나 반환하는 함수

KOTLIN
// 함수를 파라미터로 받는 고차 함수
fun operate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

// 다양한 호출 방법
operate(3, 4, { a, b -> a + b })   // 7
operate(3, 4) { a, b -> a * b }    // 12 — trailing lambda

실무에서 자주 쓰는 고차 함수 패턴

KOTLIN
// 1. 콜백 패턴
fun fetchData(url: String, onSuccess: (String) -> Unit, onError: (Exception) -> Unit) {
    try {
        val data = /* HTTP 요청 */ "데이터"
        onSuccess(data)
    } catch (e: Exception) {
        onError(e)
    }
}

// 2. 전략 패턴
fun <T> List<T>.customSort(comparator: (T, T) -> Int): List<T> {
    return this.sortedWith(Comparator { a, b -> comparator(a, b) })
}

// 3. 리소스 관리 패턴
fun <T> withConnection(block: (Connection) -> T): T {
    val conn = getConnection()
    return try {
        block(conn)
    } finally {
        conn.close()
    }
}

람다 문법 — 코틀린의 핵심

기본 람다 문법

KOTLIN
// 전체 형태
val sum = { a: Int, b: Int -> Int
    a + b
}

// 타입 추론 가능하면 파라미터 타입 생략
val numbers = listOf(1, 2, 3, 4, 5)
numbers.filter({ num: Int -> num > 3 })  // 전체
numbers.filter({ num -> num > 3 })        // 타입 생략
numbers.filter({ it > 3 })               // it 사용
numbers.filter { it > 3 }                // trailing lambda

trailing lambda — 마지막 파라미터가 함수일 때

KOTLIN
// 마지막 파라미터가 함수면 괄호 밖으로 뺄 수 있다
numbers.map { it * 2 }
numbers.filter { it > 3 }

// 람다가 유일한 인자면 괄호 자체를 생략
run { println("실행") }

// 여러 파라미터 중 마지막이 람다
numbers.fold(0) { acc, num -> acc + num }

it — 파라미터가 하나일 때

KOTLIN
// 파라미터가 하나면 이름을 생략하고 it으로 참조
numbers.filter { it > 3 }
numbers.map { it.toString() }

// 중첩 람다에서는 it이 헷갈리므로 이름을 명시하자
numbers.flatMap { num ->
    (1..num).map { multiplier ->  // it을 쓰면 어떤 it인지 혼란
        num * multiplier
    }
}

구조 분해(destructuring)와 밑줄(_)

KOTLIN
val map = mapOf("이름" to "코틀린", "버전" to "2.0")

// 구조 분해
map.forEach { (key, value) ->
    println("$key: $value")
}

// 사용하지 않는 파라미터는 밑줄로
map.forEach { (_, value) ->
    println(value)
}

람다의 마지막 줄이 반환값

KOTLIN
val transform: (String) -> String = { input ->
    val trimmed = input.trim()
    val upper = trimmed.uppercase()
    upper  // 마지막 줄이 반환값 (return 키워드 불필요)
}

클로저 — 바깥 변수 캡처

코틀린의 람다는 바깥 스코프의 변수를 캡처하고, 수정까지 할 수 있다. 자바의 람다는 effectively final 변수만 캡처할 수 있었던 것과 다른 점이다.

KOTLIN
fun countMatches(items: List<String>, target: String): Int {
    var count = 0  // 바깥 변수

    items.forEach {
        if (it == target) {
            count++  // 바깥 변수 수정 가능!
        }
    }

    return count
}

내부 동작 원리

코틀린은 mutable 변수를 캡처할 때 Ref 객체로 감싼다.

KOTLIN
var counter = 0

// 컴파일러가 내부적으로 변환하는 형태 (개념적 설명)
// val counterRef = Ref(0)
// 람다 내부에서 counterRef.element++ 로 접근

val increment = { counter++ }
increment()
println(counter)  // 1

클로저 활용 예시

KOTLIN
// 카운터 팩토리
fun makeCounter(): () -> Int {
    var count = 0
    return { count++ }   // 호출할 때마다 count 증가
}

val counter = makeCounter()
println(counter())  // 0
println(counter())  // 1
println(counter())  // 2

함수 참조(::) — 이미 정의된 함수를 람다처럼 전달

KOTLIN
// 최상위 함수 참조
fun isEven(n: Int) = n % 2 == 0
val numbers = listOf(1, 2, 3, 4, 5)
numbers.filter(::isEven)  // [2, 4]

// 멤버 함수 참조
val strings = listOf("abc", "de", "fghi")
strings.sortedBy(String::length)  // ["de", "abc", "fghi"]

// 생성자 참조
data class User(val name: String)
val names = listOf("Alice", "Bob")
val users = names.map(::User)  // [User(name=Alice), User(name=Bob)]

바인딩된 참조

KOTLIN
val str = "Hello, World"

// 특정 인스턴스의 메서드 참조
val isHello = str::startsWith
println(isHello("Hello"))  // true

// 비교
val unboundRef: (String, String) -> Boolean = String::startsWith  // 언바인딩
val boundRef: (String) -> Boolean = str::startsWith               // 바인딩

SAM 변환 — 자바 인터페이스를 람다로 대체

SAM(Single Abstract Method) 변환은 추상 메서드가 하나인 자바 인터페이스를 람다로 대체하는 기능이다.

KOTLIN
// 자바의 Runnable — 추상 메서드 1개
// 원래 방식
val runnable1 = object : Runnable {
    override fun run() {
        println("실행")
    }
}

// SAM 변환 — 람다로 대체
val runnable2 = Runnable { println("실행") }

// 파라미터로 전달할 때
Thread { println("스레드 실행") }.start()  // SAM 변환 적용

코틀린의 fun interface

코틀린 인터페이스는 기본적으로 SAM 변환이 안 된다. fun interface로 선언하면 가능해진다.

KOTLIN
// 일반 인터페이스 — SAM 변환 불가
interface Processor {
    fun process(value: Int): Int
}
// val p: Processor = { it * 2 }  // 컴파일 에러!

// fun interface — SAM 변환 가능
fun interface Transformer {
    fun transform(value: Int): Int
}

val doubler: Transformer = Transformer { it * 2 }   // OK
val tripler: Transformer = { it * 3 }               // OK

SAM 변환 vs 함수 타입

KOTLIN
// 함수 타입 — 코틀린에서 더 자연스러운 방식
fun process(items: List<Int>, transform: (Int) -> Int): List<Int> {
    return items.map(transform)
}

// fun interface — 의미 있는 타입 이름이 필요할 때
fun interface Validator {
    fun validate(input: String): Boolean
}

fun register(validator: Validator) { /* ... */ }
register { it.isNotBlank() }  // SAM 변환

일반적으로 코틀린에서는 함수 타입을 쓰는 게 더 코틀린답다. fun interface는 의미 있는 타입 이름을 부여하고 싶거나, 자바 코드와의 호환이 필요할 때 사용한다.

고차 함수의 성능 — Function 객체 생성 비용

람다는 내부적으로 Function 인터페이스의 인스턴스로 변환된다. 매번 객체를 생성하면 성능에 영향을 줄 수 있다.

KOTLIN
// 이 코드는 매번 Function1 객체를 생성
fun filterPositive(numbers: List<Int>): List<Int> {
    return numbers.filter { it > 0 }  // 람다 → Function1 객체
}

이 문제를 해결하는 것이 inline 함수인데, 이 내용은 별도의 글에서 다루겠다.

정리 — 면접에서 기억할 포인트

  • 함수 타입(파라미터) -> 반환값으로 표현. 수신 객체 타입도 가능
  • trailing lambda — 마지막 파라미터가 함수면 괄호 밖으로. 유일한 인자면 괄호 생략
  • it — 파라미터 1개인 람다에서 자동 생성. 중첩 시에는 명시적 이름 권장
  • 클로저 — 바깥 변수를 캡처하고 수정 가능 (자바와 다른 점)
  • SAM 변환 — 자바 인터페이스 + 추상 메서드 1개. 코틀린은 fun interface
  • 함수 참조(::) — 이미 정의된 함수를 람다 대신 전달
댓글 로딩 중...