본문 바로가기
Kotlin

[Kotlin in Action] 5.1 람다 식과 멤버 참조

by Nhahan 2025. 3. 18.

5.1.1 람다 소개: 코드 블록을 함수 인자로 넘기기

// 무명 내부 클래스로 리스너 구현하기
/* 자바 */
button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        /* 클릭 시 수행할 동작 */
    }
}

무명 내부 클래스를 선언하느라 코드가 번잡스러워졌다. 이와 비슷한 작업을 많이 수행해야하는 경우 그런 번잡함은 난잡함으로 변해 개발자를 괴롭힌다.

코틀린에서는 위 코드를 아래와 같이 간단하게 표현 가능하다.

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

 

5.1.2 람다와 컬렉션

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

사람들(위의 Person 데이터 클래스 이용)로 이뤄진 리스트가 있고, 그 중에 가장 연장자를 찾고 싶다. 람다를 사용해본 경험이 없는 개발자라면 루프를 써서 직접 검색을 구현할 것이다. 아마도 나이의 최댓값과 그 최댓값에 해당하는 나이를 먹은 첫 번째 인물을 저장하기 위해 변수를 두 개 만들고 리스트에 대해 이터레이션하면서 그 두 변수를 갱신할 것이다.

fun findTheOldest(people: List<Person>) {
    var maxAge = 0;
    var theOldest: Person? = null
    for (person in people) {
        if (person.age > maxAge) {
            maxAge = person.age
            theOldest = person
        }
    }
    println(oldest)
}

 

코틀린에서는 더 좋은 방법이 있다. 라이브러리 함수를 쓰면 된다.

// 람다를 사용해 컬렉션 검색하기
>>> val perple = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.maxBy { it.age }) // people.maxBy(Person::age) 도 가능
Person(name=Bob, age=31)

모든 컬렉션에 대해 maxBy 함수를 호출할 수 있다.

 

5.1.3 람다 식의 문법

람다는 값처럼 여기저기 전달할 수 있는 동작의 모음이다. 람다를 따로 선언해서 변수에 저장할 수도 있다. 하지만 함수에 인자로 넘기면서 바로 람다를 정의하는 경우가 대부분이다.

람다는 항상 중괄호 사이에 위치

코틀린 람다 식은 항상 중괄호로 둘러싸여 있다. 인자 목록 주변에 괄호가 없다는 사실을 꼭 기억해야한다. 화살표(->)가 인자 목록과 람다 본문을 구분해준다.
람다 식을 변수에 저장할 수 있다. 람다가 저장된 변수를 다른 일반 함수화 마찬가지로 다룰 수 있다.

>>> val sum = { x: Int, y: Int -> x + y } // 변수에 람다 저장
>>> println(sum(1, 2)) // 변수에 저장된 람다 호출
3

 

원한다면 람다 식을 직접 호출해도 된다.

>>> { println(42) }()
42

하지만 이와 같은 구문은 읽기 어렵고 그다지 쓸모도 없다. 이렇게 코드의 일부분을 굳이 블록으로 둘러싸 실행할 필요가 있다면 run을 사용한다. run은 인자로 받은 람다를 실행해주는 라이브러리 함수다.

>>> run { println(42) }
42

실행 시점에 코틀린 람다 호출에는 아무 부가 비용이 들지 않으며, 프로그램의 기본 구성 요소와 비슷한 성능을 낸다(8.2절에 추가 설명).


>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.maxBy { it.age })
Person(name=Bob, age=31)

위 코드에서 코틀린이 코드를 줄여 쓸 수 있게 제공했던 기능을 제거하고 정식으로 람다를 작성하면 다음과 같다.

people.maxBy({ p: Person -> p.age})

이 코드는 명확하기는 하지만 번잡하다. 구분자가 너무 많이 쓰여서 가독성이 떨어진다. 그리고 컴파일러가 문맥으로부터 유추할 수 있는 인자 타입을 굳이 적을 필요는 없다. 마지막으로 인자가 단 하나뿐인 경우 굳이 인자에 이름을 붙이지 않아도 된다.

먼저 중괄호부터 개선해보자. 코틀린에는 함수 호출 시 맨 뒤에 있는 인자가 람다 식이라면 그 람다를 괄호 밖으로 빼낼 수 있다는 문법 관습이 있다. 이 예제에서는 람다가 유일한 인자이므로 마지막 인자이기도 하다. 따라서 괄호 뒤에 람다를 둘 수 있다.

people.maxBy() { p: Person -> p.age }

이 코드처럼 람다가 어떤 함수의 유일한 인자이고 괄호 뒤에 람다를 썼다면 호출 시 빈 괄호를 없애도 된다.

people.maxBy { p: Person -> p.age }

더 복잡한 경우를 보자.

// 이름 붙인 인자를 사용해 람다 넘기기
>>> val people = listOf(Person("이몽룡", 29), Person("성춘향", 31))
>>> val names = people.joinToString(separator = " ",
...                      transform = { p: Person -> p.name})
>>> println(names)
이몽룡 성춘향

이 함수 호출에서 함수를 괄호 밖으로 뺀 모습은 다음과 같다.

people.joinToString(" ") { p: Person -> p.name }

하지만, 이 코드는 joinToString에 익숙하지 않은 개발자에게는 더 이해하기 어려운 코드일 것이다.


이제 구문을 더 간단하게 다듬고 파라미터 타입을 없애보자.

people.maxBy { p -> p.age }

마지막으로 람다의 파라미터 이름을 디폴트 이름인 it으로 바꾸면 람다 식을 더 간단하게 만들 수 있다.

people.maxBy { it.age }

람다를 변수에 저장할 때는 파라미터의 타입을 추론할 문맥이 존재하지 않는다. 따라서 파라미터 타입을 명시해야 한다.

>>> val getAge = { p: Person -> p.age }
>>> people.maxBy(getAge)

 

지금까지는 한 문장(식 또는 명령)으로 이뤄진 작은 람다만을 예제로 살펴봤다. 하지만 꼭 한 줄로 이뤄진 작은 람다만 있지는 않다. 본문이 여러 줄로 이뤄진 경우 본문의 맨 마지막에 있는 식이 람다의 결과 값이 된다.

>>> val sum = { x: Int, y: Int ->
...     println("Computing the sum of $x and $y...")
...     x + y
... }
>>> println(sum(1, 2))
Computing the sum of 1 and 2...
3

 

5.1.4 현재 영역에 있는 변수에 접근

자바 메소드 안에서 무명 내부 클래스를 정의할 때 메소드의 로컬 변수를 무명 내부 클래스에서 사용할 수 있다. 람다 안에서도 같은 일을 할 수 있다. 람다를 함수 안에서 정의하면 함수의 파라미터뿐 아니라 람다 정의의 앞에 선언된 로컬 변수까지 람다에서 모두 사용할 수 있다.

이런 기능을 보여주기 위해 forEach 표준 함수를 사용해보자. forEach는 가장 기본적인 컬렉션 조작 함수 중 하나다. forEach는 컬렉션의 모든 원소에 대해 람다를 호출해준다.

// 함수 파라미터를 람다 안에서 사용하기
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
    messages.forEach {
        println("$prefix $it") // 람다 안에서 함수의 "prefix" 파라미터를 사용
    }
}

>>> val errors = listOf("403 Forbidden", "404 Not Found")
>>> printMessagesWithPrefix(errors, "Error:")
Error: 403 Forbidden
Error: 404 Not Found

자바와 다른 점 중 중요한 한 가지는 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다는 점이다. 또한 람다 안에서 바깥의 변수를 변경해도 된다. 다음 리스트는 전달받은 상태 코드 목록에 있는 클라이언트와 서버 오류의 횟수를 센다.

// 람다 안에서 바깥 함수의 로컬 변수 변경하기
fun printProblemCounts(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    responses.forEach {
        if (it.startsWith("4")) {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++
        }
    }
    println("$clientErrors client errors, $serverErrors server errors")
}

>>> val responses = listOf("200 OK", "418 I'm, a teapot", "500 Internal Server EWrror")
>>> printProblemCounts(responses)
1 client errors, 1 server errors

코틀린에서는 자바와 달리 람다에서 람다 밖 함수에 있는 파이널이 아닌 변수에 접근할 수 있고, 그 변수를 변경할 수도 있다.

 

5.1.5 멤버 참조

코틀린에서는 자바 8과 마찬가지로 함수를 값으로 바꿀 수 있다. 이 때 이중 콜론(::)을 사용한다.

val getAge = Person::age

::를 사용하는 식을 멤버 참조(member reference)라고 부른다. 멤버 참조는 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어준다. ::는 클래스 이름과 여러분이 참조하려는 멤버 이름 사이에 위치한다.

 

댓글