람다식과 맴버 참조

유우선·2026년 2월 2일

Kotlin Study📚

목록 보기
9/32

람다의 유용성과 생김세

람다 소개: 코드 블록을 값으로 다루기

코드에서 인련의 동작을 변수에 저장하거나 다른 함수에 저장해야 하는 경우가 더러 있음

  • 이벤트 발생 시 특정 핸들러 실행
  • 데이터 구조의 모든 원소에 특정 연산 적용

예전 자바에서는 위의 경우들에 익명 내부 클래스를 사용했음

  • 코드를 함수에 넘기거나 변수에 저장할 순 있었지만 번거로웠음

이 문제를 해결하는 접근 방법 : 함수를 값처럼 다루기

  • 클래스 인스턴스를 함수에 넘기는 대신, 함수 자체를 다른 함수에 전달

람다의 장점

  • 코드가 간결해짐
  • 함수를 선언할 필요가 없어짐

버튼 클릭에 따른 동작을 정의하는 예제 (자바 익명 내부 클래스)

  • Button 객체 → 클릭을 처리하는 onClickListener 인터페이스를 전달받고 싶어함
  • 인 인터페이스는 onClick 이라는 메서드만 들어있음
  • object 선언으로 이를 구현할 수 있음
button.setOnClickListener(object: onClickListner {
    override fun onClick(v: View) {
        println("I was clicked!")
    }
})

위 예제의 문제점

  • 코드가 번잡함
  • 여러 번 반복해서 선언해야 할 경우 귀찮음

람다 사용을 통한 코드 개선

button.onClickListener {
    println("I was clicked!")
}
  • 자바 익명 내부 클래스에 비해 훨씬 간결하고 읽기 쉬움

람다와 컬렉션

  • 코드에서 중복을 제거하는 것은 프로그래밍 스타일을 개선하는 중요한 방법 중 하나임
  • 컬렉션을 다룰 때 수행하는 대부분의 작업 → 몇 가지 일반적인 패턴이 있음
  • 람다는 코틀린에서 컬렉션을 다룰 때 편리한 표준 라이브러리를 제공함

사람의 이름과 나이를 저장하는 Person 클래스 예제

data class Person(val name: String, val age: Int)
  • 위 클래스를 저장하는 컬렉션에서 나이가 가장 많은 사람 찾기
  1. 직접 구현하기

    data class Person(val name: String, val age: Int)
    
    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(theOldest)
    }
    
    fun main() {
        val people = listOf(Person("Alice", 29), Person("Bob", 31))
        findTheOldest(people)
    }
    
    //Person(name=Bob, age=31)
  2. 표준 라이브러리 사용하기

    data class Person(val name: String, val age: Int)
    
    fun main() {
        val people = listOf(Person("Alice", 29), Person("Bob", 31))
        println(people.maxByOrNull { it.age })
    }
    
    //Person(name=Bob, age=31)
    • maxByOrNull 함수
      • 가장 큰 값을 찾기 위해 비교할 원소를 인자로 받음
      • 중괄호 ( { } ) 식 → 최대 값을 찾기 위한 선택자 로직 구현부
      • 람다는 인자를 하나만 받음
        • 그 인자에 it이라는 암시적 이름을 사용함

람다가 단순한 함수나 프로퍼티에 위임할 경우 맴버 참조를 사용할 수 있음

people.maxByOrNull (Person :: age)

람다식의 문법

  • 람다식은 한상 중괄호( {} )에 둘러싸여 있음
  • 인자 목록 주변에 괄호( () )가 없음
  • 화살표( -> )가 인자 목록과 람다 본문을 구분해줌

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

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

람다식을 직접 호출할 수도 있음

fun main(){
    { println(42) }()
}
// 42
  • 구문을 읽기 힘들고 그다지 쓸 일도 없음
  • 코드 일부분을 블록으로 감싸야 한다면 run을 사용하는게 효율적임
    • run은 인자로 받은 람다를 실행해주는 라이브러리 함수임
      val myFavoriteNumber = run {
              println("I'm thinking!")
              println("I'm doing some more work...")
              42
          }

people.maxByOrNull { it.age } 람다식의 여러 모습

  • 암시적 인자를 사용하지 않고 정식으로 람다를 작성한 모습

    people.maxByOrNull({ p: Person -> p.age })
    • 중괄호 안의 코드 → 람다식
    • 람다식을 maxByOrNull 함수에 넘김
    • 람다식은 Person 타입의 값을 인자로 받아 인자의 age를 반환
    • 구분자가 많이 가독성이 떨어짐
    • 커파일러가 자동으로 유추할 수 있는 인자 타입을 직접 적을 필요는 없음
    • 인자가 하나뿐일 경우 인자에 이름을 붙이지 않아도 됨
  • 중괄호 제거

    people.maxByOrNull() { p: Person -> p.age }
    • 함수 호출 시 맨 뒤에 있는 인자가 람다 식이라면 괄호 밖으로 뺄 수 있음
  • 괄호 제거

    people.maxByOrNull {p: Person -> p.age}
    • 람다가 유일한 인자이고 괄호 뒤에 람다를 썼다면 빈 괄호를 없애도 됨
  • 여러 인자 중 마지막 인자만 람다인 경우 람다를 밖으로 빼는것이 바람직함

  • 둘 이상의 람다를 인자로 받는 경우 람다를 괄호 밖으로 뺄 수 없음

이름 붙인 인자를 사용해 람다 넘기기

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

fun main() {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    val names = people.joinToString(
        separator = " ",
        transform = { p: Person -> p.name }
    )
    println(names)
}

// Alice Bob

이 함수에서 람다를 괄호 밖으로 뺀 모습

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

파라미터 타입 제거하기

people.maxByOrNull {p: Person -> p.age} //파라미터 타입을 명시
people.maxByOrNull {p -> p.age} //파라미터 타입을 컴파일러가 추론
  • maxByOrNull 함수의 파라미터 타입은 항상 컬렉션 원소 타입과 같음
  • 컴파일러는 Person 타입 객체가 들어있는 컬렉션에 대해 함수를 호출했다는 것을 알고 있어 파라미터 타입을 유추할 수 있음

디폴트 파라미터 이름 it

people.maxByOrNull { it.age }
  • 람다 파라미터 이름을 따로 지정하지 않은 경우에만 it 이라는 이름이 자동으로 설정된다.

람다를 변수에 저장할 때

  • 파라미터 타입을 추론할 문맥이 존재하지 않음
  • 파라미터 타입을 명시해야 함
    val getAge = { p: Person -> p.age }
    people.maxByOrNull(getAge)

여러 줄로 이뤄진 람다

  • 본문의 맨 마지막에 있는 식이 람다의 결괏괎이 됨
  • 명시적인 return이 필요하지 않음
fun main() {
    val sum = {x: Int, y: Int ->
        println("Computing the sum of $x and $y...")
        x + y
    }
    println(sum(1, 2))
}

현재 영역에 있는 변수 접근

  • 람다가 선언된 함수 내부의 파라미터와 로컬 변수를 참조할 수 있음
fun printProblemCounts(response: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    response.forEach {
        if(it.startsWith("4"))
            clientErrors++
        else if (it.startsWith("5"))
            serverErrors++
    }

    println("$clientErrors client errors, $serverErrors server errors")
}

fun main() {
    val response = listOf("200 OK", "418 I'm teapot", "500 Internet Server Error")

    printProblemCounts(response)
}
//1 client errors, 1 server errors
  • 이렇게 람다 외부에 선언된 변수에 접근해 그 변수를 사용하는 것을 ‘람다가 캡처한 변수’라고 함
  • 어떤 함수가 자신의 로컬 변수를 캡처한 람다를 반환하거나 다른 변수에 저장한다면 로컬 변수의 생명 주기와 함수의 생명주기가 달라질 수 있음
    • 파이널 변수 캡처시 람다 코드를 변수 값과 함께 저장
    • 파이널이 아닌 변수 캡처시 특별한 래퍼로 감싸서 보관함
      • 나중에 변경하거나 읽을 수 있음
      • 래퍼에 대한 참조를 람다와 함께 저장함

람다를 이벤트 핸들러나 비동기적으로 실행되는 코드로 활용하는 경우 로컬 변수 변경은 람다가 실행되는 동안에만 이뤄짐

fun tryToCountButtonClicks(button : Button): Int {
    var clicks = 0
    button.onClick { clicks++ }
    return clicks
}
  • 이 함수는 항상 0을 반환함
  • 핸들러는 tryToCountButtonClicksrk clicks를 반환한 다음에 호출됨
  • 이 함수를 고치려면 클릭 수를 세는 카운터 변수를 함수 밖으로 빼야함

멤버 참조

넘기려는 코드가 이미 함수로 선언된 경우?

  • 그 함수를 호출하는 람다를 만들면 됨
  • 하지만 이는 중복임
  • 함수를 직접 넘기는 방법은?
val getAge = Person :: age

// val getAge = { p: Person -> p.age }와 기능이 같음

:: 를 사용하는 식을 맴버 참조라고 부름

  • 정확히 한 메서드를 호출하거나 한 프로퍼티에 접근하는 함수 값을 만들어줌
  • 참조 대상 뒤에 괄호를 붙이면 안됨
    • 참조하려는 것이지 호출하는 것이 아니기 때문

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

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

fun main() {
    run(::salute)
}

//Salute!
  • 클래스 이름을 생략하고 ::으로 참조를 바로 시작

람다가 인자가 여럿인 다른 함수에게 작업을 위임

  • 파라미터 이름과 함수를 반복하지 않아도 되서 편함
val action = {person: Person, message: String -> // sendEmail에 작업 위임
    sendEmail(person, message)
}

val nextAction = :: sendEmail // 람다 대신 맴버 참조를 쓸 수 있음

생성자 참조

  • 클래스 생성 작업을 연기하거나 저장해둘 수 있음
fun main() {
    val createPerson = ::Person
    val p = createPerson("Alice", 29)
    
    println(p)
}

확장 함수 참조

  • 맴버 참조와 똑같은 방법으로 참조할 수 있음
fun Person.isAdult() =  age >= 21
val predicate = Person::isAdult

값과 엮인 호출 가능 참조

  • 지금까지의 맴버 참조 → 항상 클래스의 맴버를 가리킴
  • 값과 엮인 호출 가능 참조 사용 → 특정 객체 인스턴스에 대한 메서드 호출 참조를 만들 수 있음
fun main() {
    val seb = Person("Sebastian", 26)
    val personAgeFunction = Person::age //Person이 주어지면 나이를 돌려주는 맴버 참조
    println(personAgeFunction(seb)) //사람을 인자로 받음
    //26
    val sebAgeFunction = seb::age // 특정 사람의 나이를 돌려주는, 값과 엮인 호출 가능 참조
    println(sebAgeFunction()) // 특정 값과 엮여있기 때문에 아무 파라밑를 지정하지 않아도 됨
    //26
}

0개의 댓글