Kotlin의 람다 및 멤버 참조

람다(Lambda expression)은 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다. 자바에서는 오랜 기간 람다를 지원하지 않다가 자바 8에 와서 람다를 지원하기 시작했다. 코틀린 표준 라이브러리는 람다를 많이 사용하기 때문에 람다를 더욱 유용하게 활용할 수 있다.

코드 블록을 함수 인자로 넘기기

“이벤트가 발생하면 이 핸들러를 실행하자”, “데이터 구조의 모든 원소에 이 연산을 적용하자” 같은 아이디어를 코드로 표현하려면 어떻게 할까? 자바에서는 무명 내부 클래스를 활용하여 이 목적을 달성했다.

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        //클릭 시 수행할 동작
    }
});

그러나 함수형 프로그래밍은 함수를 값처럼 다루는 접근 방법을 통해 이를 해결한다. -> 함수형 언어는 함수를 직접 다른 함수에 전달할 수 있다. 람다 식을 사용하면 함수를 선언할 필요가 없고 코드 블록을 직접 함수의 인자로 전달할 수 있다.

button.setOnClickListener { /*클릭 시 수행할 동작 */ }

코틀린에서 위 코드는 자바의 무명 내부 클래스와 같은 역할을 하지만 훨씬 간결하고 읽기 쉽다.

람다와 컬렉션

코틀린은 컬렉션에 다룰 때 수행하는 여러 일반적인 작업을 모아놓은 라이브러리가 있다. 이런 패턴은 람다가 없으면 제공하기 어렵기 때문에 자바 8 이전에는 자바에서 쓰기 편한 컬렉션 라이브러리가 적었으며 직접 구현해 사용하곤 했다.

data class Person(val name: String, val age: Int)

Person으로 만들어진 리스트가 있다고 가정하자. 여기서 가장 연장자를 찾고 싶을 땐 어떻게 해야 할까. 보통은 이렇게 할 것이다.

fun findTheOlder(people: List<Person>) {
    var maxAge = 0
    var theOldest: Person? = null

    for (person in people) {
        if(person.age > maxAge) {
            maxAge = person.age
            theOldest = person
        }
    }

    println(theOldest)
}

구현 난이도가 높지는 않지만 코드량이 많아 귀찮다. 코틀린에서는 이런 기능을 제공하는 라이브러리 함수를 사용하면 된다.

println(people.maxByOrNull { it.age })

모든 컬렉션에 대해 이 함수를 사용할 수 있다. 멤버 참조를 사용하면

println(people.maxByOrNull(Person::age))

다음과 같이 쓸 수도 있다. 이는 멤버 참조 절에서 자세히 다룰 예정이다.

코틀린의 버전 업데이트에 따라 글 작성 시점 기준 최신 버전 코틀린은 maxBy 함수가 Deprecated 되고 maxByOrNull 함수가 그 위치를 대신하고 있습니다.

람다 식의 문법

람다 식의 문법은 다음과 같다.

람다 식 문법

코틀린의 람다 식은 항상 중괄호로 둘러싸여 있다. 화살표( -> )가 파라미터와 본문을 구분한다.

람다 식을 변수에 저장할 수 있다.

val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2))

람다 식을 직접 호출할 수 있지만 굳이 쓸모는 없다. 코드의 일부분을 블록으로 둘러싸 실행하려면 run을 사용한다.

{ println(42) }()
run { println(42) }

람다 호출은 부가 비용이 들지 않으며 비슷한 성능을 낸다.

앞서 작성했던 컬렉션 함수를 다시 풀어서 쓰면 다음과 같다.

people.maxByOrNull({ person: Person -> person.age })

중괄호 안의 코드는 람다 식이고 이 람다 식을 maxByOrNull 함수에 넘기는 것으로 해석할 수 있다. 이제 이 코드를 간단하게 해보자. 먼저 코틀린은 함수 호출 시 맨 뒤 인자가 람다면 람다를 괄호 밖으로 빼는 관습이 있다. 람다가 유일한 인자일 경우 빈 괄호는 생략할 수 있다.

people.maxByOrNull { person: Person -> person.age }

컴파일러는 람다 파라미터 타입도 추론한다. 따라서 파라미터 타입을 명시할 필요가 없다. 컴파일러는 Person 타입 컬렉션에서 maxByOrNull 함수를 호출하는 것을 알고 있으므로 람다의 파라미터도 Person이라는 사실을 알 수 있다. 다만 타입 정보가 코드 해석에 도움이 된다면 남겨놔도 괜찮다.

people.maxByOrNull { person -> person.age }

파라미터의 디폴트 이름 it을 사용하면 파라미터 이름도 생략해 람다 식을 더 간결하게 표현할 수 있다. 람다의 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론할 수 있으면 it을 사용할 수 있다.

people.maxByOrNull { it.age }
it은 코드를 간단하게 만들어주지만 남용하면 안된다. 람다 안에 람다가 중첩되는 경우 각각의 it이 어느 람다에 속해있는지 알기 어렵다. 문맥상 파라미터의 의미나 타입을 쉽게 알 수 없는 경우에도 이를 명시하는 것이 도움이 된다.

파라미터의 타입을 추론할 수 없는 경우에는 타입을 명시해야 한다.

val getAge = { p: Person -> p.age }

여러 줄로 이뤄진 람다의 경우 맨 마지막의 식이 람다의 결과 값이 된다.

val sum = { x: Int, y: Int ->
    println("$x + $y")
    x + y 
}

변수에 접근

람다를 함수 안에서 정의한다면 함수의 파라미터 뿐 아니라 람다 정의의 앞에 선언된 로컬 변수도 람다에서 사용할 수 있다.

fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
    messages.forEach { //For-Each Lambda
        println("$prefix $it")
    }
}

자바와 비슷하지만 자바와 다른 점은 코틀린 람다 내에서 final이 아닌 변수에도 접근할 수 있다. 또한 람다 안에서 바깥 변수를 변경할 수 있다. 이처럼 람다가 사용할 수 있는 변수를 람다가 포획한(captured) 변수라고 한다.

멤버 참조

::를 사용하는 식을 멤버 참조(Member reference)라고 한다. 멤버 참조는 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어준다.

val getAge = Person::age

위 멤버 참조는 다음 람다 식을 간략하게 표현한 것이다.

val getAge = { person: Person -> person.age }

멤버 참조 뒤에는 괄호를 넣으면 안된다. 또한 멤버 참조는 그 멤버를 호출하는 람다와 같은 타입이다. 따라서 람다와 자유롭게 바꿔 쓸 수 있다.

최상위에 선언된 함수나 프로퍼티를 참조하려면 다음과 같이 한다.

fun salute() = println("Salute!")
>>> run(::salute)

생성자 참조(Constructor reference)를 사용하여 클래스 생성 작업을 연기하거나 저장할 수 있다.

val createPerson = ::Person
val p = createPerson("Alice", 29)

확장 함수도 멤버 함수와 같은 방식으로 사용할 수 있다.


© 2021. All rights reserved.

Powered by Hydejack v9.1.6