고차 함수와 람다 — 함수를 파라미터로 넘기는 코틀린의 방법
코틀린에서 함수는 일급 시민(first-class citizen)이다. 변수에 저장하고, 파라미터로 넘기고, 반환값으로 쓸 수 있다. 자바 8의 람다와 비슷해 보이지만, 코틀린의 람다는 바깥 변수를 수정할 수 있고, trailing lambda 문법으로 더 깔끔하게 쓸 수 있다. 면접에서 고차 함수와 람다는 필수 주제이니 제대로 정리해보자.
함수 타입 — (파라미터) -> 반환값
코틀린에서는 함수의 타입을 명시할 수 있다.
// 함수 타입 선언
val sum: (Int, Int) -> Int = { a, b -> a + b }
val isPositive: (Int) -> Boolean = { it > 0 }
val greet: () -> String = { "안녕하세요" }
val printMsg: (String) -> Unit = { println(it) }
함수 타입의 다양한 형태
// 수신 객체가 있는 함수 타입 (확장 함수 타입)
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 }
}
고차 함수 — 함수를 파라미터로 받거나 반환하는 함수
// 함수를 파라미터로 받는 고차 함수
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
실무에서 자주 쓰는 고차 함수 패턴
// 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()
}
}
람다 문법 — 코틀린의 핵심
기본 람다 문법
// 전체 형태
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 — 마지막 파라미터가 함수일 때
// 마지막 파라미터가 함수면 괄호 밖으로 뺄 수 있다
numbers.map { it * 2 }
numbers.filter { it > 3 }
// 람다가 유일한 인자면 괄호 자체를 생략
run { println("실행") }
// 여러 파라미터 중 마지막이 람다
numbers.fold(0) { acc, num -> acc + num }
it — 파라미터가 하나일 때
// 파라미터가 하나면 이름을 생략하고 it으로 참조
numbers.filter { it > 3 }
numbers.map { it.toString() }
// 중첩 람다에서는 it이 헷갈리므로 이름을 명시하자
numbers.flatMap { num ->
(1..num).map { multiplier -> // it을 쓰면 어떤 it인지 혼란
num * multiplier
}
}
구조 분해(destructuring)와 밑줄(_)
val map = mapOf("이름" to "코틀린", "버전" to "2.0")
// 구조 분해
map.forEach { (key, value) ->
println("$key: $value")
}
// 사용하지 않는 파라미터는 밑줄로
map.forEach { (_, value) ->
println(value)
}
람다의 마지막 줄이 반환값
val transform: (String) -> String = { input ->
val trimmed = input.trim()
val upper = trimmed.uppercase()
upper // 마지막 줄이 반환값 (return 키워드 불필요)
}
클로저 — 바깥 변수 캡처
코틀린의 람다는 바깥 스코프의 변수를 캡처하고, 수정까지 할 수 있다. 자바의 람다는 effectively final 변수만 캡처할 수 있었던 것과 다른 점이다.
fun countMatches(items: List<String>, target: String): Int {
var count = 0 // 바깥 변수
items.forEach {
if (it == target) {
count++ // 바깥 변수 수정 가능!
}
}
return count
}
내부 동작 원리
코틀린은 mutable 변수를 캡처할 때 Ref 객체로 감싼다.
var counter = 0
// 컴파일러가 내부적으로 변환하는 형태 (개념적 설명)
// val counterRef = Ref(0)
// 람다 내부에서 counterRef.element++ 로 접근
val increment = { counter++ }
increment()
println(counter) // 1
클로저 활용 예시
// 카운터 팩토리
fun makeCounter(): () -> Int {
var count = 0
return { count++ } // 호출할 때마다 count 증가
}
val counter = makeCounter()
println(counter()) // 0
println(counter()) // 1
println(counter()) // 2
함수 참조(::) — 이미 정의된 함수를 람다처럼 전달
// 최상위 함수 참조
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)]
바인딩된 참조
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) 변환은 추상 메서드가 하나인 자바 인터페이스를 람다로 대체하는 기능이다.
// 자바의 Runnable — 추상 메서드 1개
// 원래 방식
val runnable1 = object : Runnable {
override fun run() {
println("실행")
}
}
// SAM 변환 — 람다로 대체
val runnable2 = Runnable { println("실행") }
// 파라미터로 전달할 때
Thread { println("스레드 실행") }.start() // SAM 변환 적용
코틀린의 fun interface
코틀린 인터페이스는 기본적으로 SAM 변환이 안 된다. fun interface로 선언하면 가능해진다.
// 일반 인터페이스 — 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 함수 타입
// 함수 타입 — 코틀린에서 더 자연스러운 방식
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 인터페이스의 인스턴스로 변환된다. 매번 객체를 생성하면 성능에 영향을 줄 수 있다.
// 이 코드는 매번 Function1 객체를 생성
fun filterPositive(numbers: List<Int>): List<Int> {
return numbers.filter { it > 0 } // 람다 → Function1 객체
}
이 문제를 해결하는 것이 inline 함수인데, 이 내용은 별도의 글에서 다루겠다.
정리 — 면접에서 기억할 포인트
- 함수 타입 —
(파라미터) -> 반환값으로 표현. 수신 객체 타입도 가능 - trailing lambda — 마지막 파라미터가 함수면 괄호 밖으로. 유일한 인자면 괄호 생략
- it — 파라미터 1개인 람다에서 자동 생성. 중첩 시에는 명시적 이름 권장
- 클로저 — 바깥 변수를 캡처하고 수정 가능 (자바와 다른 점)
- SAM 변환 — 자바 인터페이스 + 추상 메서드 1개. 코틀린은
fun interface로 - 함수 참조(::) — 이미 정의된 함수를 람다 대신 전달
댓글 로딩 중...