Programming/Kotlin

코틀린(Kotlin) - 람다식과 멤버 참조

JunsuKim 2021. 10. 16.
728x90

람다식 또는 람다는 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다.

이를 사용하면 쉽게 공통 코드 구조를 라이브러리 함수로 뽑아낼 수 있다.

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

"이벤트가 발생하면 이 리스너를 실행하자" 또는 데이터 구조의 모든 원소에 이 연산을 적용하자"와 같은 생각을 코드로 표현하기 위해 일련의 동작을 변수에 저장하거나 다른 함수에 넘겨야 하는 경우가 종종 있다.

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

이처럼 자바에서는 무명 내부 클래스를 선언하여 코드가 번잡스러워진다.

이와 비슷한 작업을 많이 수행해야 할 경우 더욱 난잡해져 불편함을 느낄 것이다.

이를 해결하기 위한 함수형 프로그래밍에서는 함수를 값처럼 다루는 접근 방식을 택한다.

위의 코드를 람다식으로 보면 다음과 같다.

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

두 코드는 같은 역할을 하지만 람다식을 사용함으로써 훨씬 더 간결하고 가독성이 좋아졌다.

람다와 컬렉션

컬렉션을 다룰 때 수행하는 대부분의 작업은 일반적인 패턴에 속하며 이 패턴은 라이브러리 안에 있어야 한다.

람다가 없으면 컬렉션을 편리하게 처리할 수 있는 좋은 라이브러리를 제공하기 힘들다.

이 때문에 자바에서 쓰기 편한 컬렉션 라이브러리를 적었으며, 그에 따라 개발자들은 필요한 컬렉션 기능을 직접 작성하였으나, 코틀린에서는 그럴 필요가 없다.

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)
}

이처럼 루프 안에 상당히 많은 코드가 들어가 변잡스러움을 유발한다. 이를 코틀린에서는 라이브러리 함수를 사용하여 간결하게 만들 수 있다.

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

이처럼 모든 컬렉션에 대해 maxBy 함수를 호출할 수 있다. 이 함수는 가장 큰 원소를 찾기 위해 사용할 값을 돌려주는 함수를 인자로 받는다.

이런 식으로 단지 함수나 프로퍼티를 반환하는 역할을 수행하는 람다는 멤버 참조로 대치할 수 있다.

people.maxBy(Person::age)

멤버 참조에 관해서는 이따 멤버 참조에 대해 공부할 때(이 글의 뒷편) 더 자세히 보자.

람다 식의 문법

이는 람다 식을 선언하기 위한 문법이다.

람다 식의 특성은 다음과 같다.

  • 항상 중괄호로 둘러싸여 있다.
  • 인자 목록 주변에 괄호가 없다.
  • 화살표(->)가 인자 목록과 람다 본문을 구분한다.

람다식은 변수에 저장할 수 있으며, 람다가 저장된 변수를 다른 일반 함수처럼 다룰 수 있다.

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

3

원한다면 람다 식을 직접 호출할 수도 있다.

{println(42)} ()

42

하지만 이는 그다지 쓸모가 없다.

굳이 람다를 만들자마자 바로 호출하느리 람다 본문을 직접 실행하는 편이 낫다.

코드의 일부분을 블록으로 둘러싸 실행할 필요가 있다면 인자로 받은 람다를 실행해주는 라이브러리인 run을 사용하면 된다.

run {println(42)}

42

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

 

앞에서 만들었던 연장자를 찾는 예제를 다시 보자.

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

이를 람다를 통해 작성하면 다음과 같다.

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

하지만 이 코드는 번잡하고, 구분자가 너무 많아 가독성이 떨어진다.

컴파일러가 문맥으로부터 유추할 수 있는 인자 타입을 굳이 적을 필요가 없고, 마지막으로 인자가 단 하나뿐인 경우 굳이 인자에 이름을 붙이지 않아도 된다.

코틀린에서는 함수 호출 시 맨 뒤에 있는 인자가 람다 식이면, 그 람다를 괄호 밖으로 빼낼 수 있다.

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

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

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

로컬 변수처럼 컴파일러는 람다 파라미터의 타입도 추론 가능하다. maxBy 함수의 경우 파라미터의 타입은 항상 컬렉션 원소 타입과 같다.

컴파일러는 Person 타입의 객체가 들어있는 컬렉션에 대해 maxBy를 호출한다는 것을 알고 있으므로, 람다의 파라미터도 Person이라는 사실을 이해할 수 있다.

허나 컴파일러가 타입을 추론하지 못하거나 타입 정보가 코드를 읽을 때 도움이 된다면 타입을 표시해도 된다.

people.maxBy{p -> p.age}

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

람다의 파라미터가 하나뿐이고, 그 타입을 컴파일러가 추론할 수 있을 경우 사용 가능하다.

 

※ it을 사용하면 코드를 아주 간단하게 만들 수 있지만 이를 남용해서는 안된다. 파라미터를 명시하지 않으면, 각각의 it이 가리키는 파라미터가 어떤 람다에 속했는지 파악하기 어렵기 때문이다.

 

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

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

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

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

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

람다를 함수 안에서 정의하면 함수의 파라미터뿐만 아니라 람다 정의의 앞에 선언된 로컬 변수까지 람다에서 모두 사용할 수 있다.

fun printMessageWithPrefix(message: Collection<String>, prefix: String) {
    message.forEach {
        println("$prefix $it")
    }
}

자바와는 달리 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있고, 람다 안에서 바깥의 변수를 변경해도 된다.

람다 안에서 사용하는 외부 변수를 "람다가 포획한 변수"라고 한다.

멤버 참조

람다를 사용해 코드 블록을 다른 함수에 인자로 넘길 때 이 코드가 이미 함수로 선언돼 있다 하자.

이 함수를 호출하는 람다를 만들면 되지만 이는 중복이기 때문에 함수를 직접 넘길 있는 방법이 좋다.

코틀린에서는 함수를 값으로 바꿀 수 있는데, 이때 이중 콜론(::)을 사용하고, 이를 사용하는 식을 멤버 참조라고 한다.

멤버 참조는 프로퍼티나 메소드를 단 하나만 호출하는 값을 만들어준다.

val getAge = Person::age

참조 대상이 함수인지 프로퍼티인지 관계없이 멤버 참조 뒤에는 괄호를 넣으면 안된다.

또한 멤버 참조는 그 멤버를 호출하는 람다와 같은 타입이기 때문에 람다와 자유롭게 바꿔 쓸 수 있다.

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

 

최상위에 선언된 함수나 프로퍼티를 참조할 수도 있다.

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

람다가 인자가 여러 개인 다른 함수한테 작업을 위임하는 경우 람다를 정의하지 않고 직접 위임 함수에 대한 참조를 제공하면 편리하다.

val action = { person: Person, message: String ->
    sendEmail(person, message)
}

val nextAction = ::sendEmail

 

생성자 참조를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있다.

data class Person(val name: String, val age: Int)
val createPerson = ::Person
val p = createPerson("Alice", 29)

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

fun Person.isAdult() = age >= 21
val predicate = Person::isAdult
728x90

댓글