본문 바로가기

코틀린

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

코틀린의 람다는 자바8의 람다식과 개념이 매우 비슷합니다.

람다를 쉽게 설명하자면, 값처럼 여기저기 전달할 수 있는 동작의 모음(?)이라고 할 수 있습니다.

기본적으로 람다식은 자바8부터 사용이 가능하고, 안드로이드에서 제대로 된 Functional Interface를 사용하려면  N(Nougat 7.0) OS 이상이어야만 합니다.

 

하지만, 코틀린에서는 이런 제한과 상관없이 람다식을 사용할 수 있다는게 큰 장점이지 아닐까 싶습니다.

 

추가로, 람다식은 기본적으로 편리한 Lambda API들을 제공하기 때문에, 안드로이드 스튜디오에서 개발할 때 자동완성 기능을 이용해서 먼저 해당 API를 살펴본다면, 훨씬 더 간결하고 직관적인 코드를 짤 수 있을 것 같습니다.

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

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

 

/* 자바 */
private View.OnClickListener onClickListener = new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //TODO
    }
};

button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       //TODO
   }
});

 

자바에서는 이렇게 구현을 하였지만, 상당히 번거롭고 코드가 번잡스럽습니다...!

이런 비슷한 작업을 많이 수행해야 하는 경우에는 이런 번잡합이 -> 난잡함으로 변해서 우리 개발자들을 고통스럽게 만들겠죠...ㅠㅠ

 

이와 달리 함수형 프로그래밍에서는 함수를 값처럼 다루는 접근 방식을 택함으로써, 이런 문제를 해결해줍니다.

 

기존처럼 클래스를 선언하고 그 클래스의 인스턴스를 함수에 넘기는 대신, 함수형 프로그래밍에서는 함수를 직접 다른 함수에 전달할 수 있습니다.

그렇기 때문에, 람다식을 사용하면 함수를 선언할 필요가 없고, 코드 블록을 직접 함수의 인자로 전달하게 됩니다.

 

예제를 하나 살펴보도록 하겠습니다. 

버튼 클릭에 동작을 정의하는 경우, 보편적으로 클릭 이벤트 리스너를 구현하게 됩니다. 버튼 클릭 리스너는 onClick이라는 메소드를 포함하는 OnClickListener를 구현해야 합니다.

 

/* 자바에서의 일반적인 클릭 리스너 구현 */
button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       //TODO
   }
});

 

코드가 벌써 번잡스러워졌습니다...! 이런 불필요한 코드를 제거하기 위해 람다로 구현해보겠습니다.

 

/* 람다로 클릭 리스너 구현 */
button.setOnClickListener { 
    //TODO
}

 

훨씬 더 간결하고 가독성이 좋아졌습니다!

이 예제는 메소드가 하나뿐인 인터페이스 객체 대신 람다를 사용할 수 있다는 것을 보여주기도 합니다.

람다와 컬렉션

람다를 사용한 컬렉션을 처리하기 위해, 간단한 예제를 만들어 보도록 하겠습니다.

 

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

 

여러 개의 Person 리스트가 있고, 이 중에 가장 연장자를 찾고 싶다면 우리는 어떻게 구현을 하게 될까요?

람다를 사용해본 경험이 없는 개발자라면 아마도 루프를 써서 직접 검색을 구현하게 될 것입니다.

 

/* 컬렉션을 직접 검색하기 */
fun findOldest(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("안드로이드", 29), Person("코틀린", 30))
>>> findOldest(people)
Person(name=코틀린, age=30)

 

하지만 이 루프에는 상당히 많은 코드가 들어있기 때문에 쉽게 파악하기도 힘들며, 자칫 실수를 저지르기도 쉽습니다.

 

코틀린에서는 maxBy란 컬렉션 확장 함수를 제공합니다.

따라서, 위에 처럼 findOldest() 함수를 구현하지 않고도 간단하게 표현할 수 있습니다.

 

/* 람다를 사용해 컬렉션 검색하기 */
>>> val people = listOf(Person("안드로이드", 29), Person("코틀린", 30))
>>> println(people.maxBy { it.age }) //람다식 {it.age}
Person(name="코틀린", age=30)

 

maxBy는 가장 큰 원소를 찾기 위해 비교에 사용할 값을 인자로 받으며, 모든 컬렉션에 대해 maxBy 함수를 호출할 수 있습니다.

중괄호로 둘러싸인 { it.age } 는 비교에 사용할 값을 돌려주는 함수입니다.

이런 식으로 단순히 함수나 프로퍼티를 반환하는 역할을 수행하는 람다는 멤버 참조로 대치할 수 있습니다. (아래 '멤버 참조' 섹션에서 자세히 설명드리겠습니다.) 

 

/* 멤버 참조를 사용해 컬렉션 검색하기 */
people.maxBy(Person::age)

람다 식의 문법

람다 식 문법

  1. 코틀린 람다 식은 항상 중괄호로 감싼다. {...}
  2. 인자는 () 로 감싸지 않는다.
  3. 인자와 본문은 -> 로 구분한다.
  4. 인자는 타입 추론이 가능하므로 타입을 생략할 수 있다.
  5. 변수에 람다식을 저장하는 경우에는 인자의 타입을 생략할 수 없다.

 

/* 람다 식을 변수에 저장하는 예제 */
>>> val sum = { x: Int, y: Int -> x + y }
>>> println(sum(1, 2))
3

 

람다 식을 변수에 저장하면, 저장된 변수를 일반 함수처럼 변수명 뒤에 괄호를 치고 그 안에 필요한 인자를 넣어서 람다를 호출할 수 있습니다.

 

여기서 중요한 것은, 실행 시점에 있어서 코틀린 람다 호출은 어떠한 부가 비용이 들지 않으며, 프로그램의 기본 구성 요소와 동일한 성능을 냅니다!

 

다시 그럼 위에서 만들었던 연장자를 찾는 예제로 돌아가서 다시 살펴보겠습니다.

 

people.maxBy { it.age }

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

 

이제 이 코드에서 어떤 일이 벌어졌었는지 더 명확하게 알 수 있게 되었습니다.

 

중괄호 안에 있는 코드는 람다 식이고, 그 람다 식을 maxBy함수에 넘기면, 람다 식은 Person 타입의 값을 인자로 받아서 인자의 age를 반환하는 기능이었네요!

 

하지만, 이 코드는 괄호 구분자가 너무 많이 쓰여서 가독성이 떨어지는 것 같습니다...!

그래서 코틀린은 코드의 간결성을 위해, 아래와 같은 규칙이 존재합니다.

 

people.maxBy ({p: Person -> p.age})
people.maxBy () {p: Person -> p.age} //함수의 맨 마지막 인자가 람다라면 () 밖에 람다를 표현할 수 있다.
people.maxBy {p: Person -> p.age} //함수의 인자가 하나면서, 람다라면 ()를 생략할 수 있다.
people.maxBy {p -> p.age} //타입추론으로 타입을 제거할 수 있다.
people.maxBy {it.age} //파라미터명을 디폴트인 it로 받을 수 있다.
people.maxBy (Person::age) //멤버 참조

 

  1. 함수의 맨 마지막 인자가 람다라면 () 밖에 람다를 표현할 수 있다.
  2. 함수의 인자가 하나면서, 그 인자가 람다라면 ()를 생략할 수 있다.
  3. 타입 추론으로 파라미터의 타입을 제거할 수 있다.
  4. 파라미터명을 따로 지정하지 않는다면, 디폴트인 it로 받을 수 있다.
  5. 멤버 참조

멤버 참조

람다를 넘길 때 프로퍼티메소드를 단 하나만 호출하는 함수 값을 갖고 있다면, 간단하게 이중 콜론(::) 으로 사용할 수 있습니다.

 

val getAge = Person::age

 

::를 사용하는 식을 멤버 참조라고 부릅니다.

 

::를 사용하여 표현하는 여러 가지 방법에 대해 알아보도록 하겠습니다.

 

 

클래스의 멤버 표현

Person::age //{person: Person -> person.age}와 동일

표현식) 클래스::멤버

 

 

최상위 함수의 표현

fun showName() = println("코틀린")

>>> run(::showName)

표현식) ::최상위 함수

 

 

함수

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

표현식) ::함수

 

 

생성자

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

>>> val createPerson = ::Person
>>> val p = createPerson("코틀린", 30)
>>> println(p)
Person(name=코틀린, age=30)

표현식) ::클래스

 

 

확장 함수

fun Person.isAdult() = age >= 20
val predicate = Person::isAdult

표현식) 클래스::확장 함수

 

 

이렇게, 람다 식과 멤버 참조를 활용한 기본적인 함수형 프로그래밍에 대해서 살펴보았습니다.^^