ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotlin in Action: 5.1 람다로 프로그래밍
    Book/Kotlin in Action 2022. 11. 15. 18:17

    -이 글은 책 Kotlin in Action의 5장을 정리한 글입니다.

    5.1 람다 식과 멤버 참조

     

    1. 람다란 무엇일까? : 코드 블록을 함수 인자로 넘기기

     

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

    예전 자바에서는 무명 내부 클래스를 통해 이 목적을 달성했다. 근데 이 방법은 매우 번거롭다.

     

    무명 내부 클래스(내부 무명 클래스)에 대한 내용은 다음과 같다.


    내부 클래스란, 하나의 클래스 안에 다른 클래스를 정의한 클래스이다.

     

    자바에서는 다중 상속이 되지 않기 때문에 2개 이상을 사용할 경우에는 한개를 내부클래스를 사용하게 되면 한개를 상속받아서 2개를 같이 쓰는 것처럼 동작시킬 수 있다. 클래스 내부의 객체에 대한 편한 접근성을 제공해준다.

    내부 클래스는 외부 클래스의 멤버나 함수에 자유롭게 접근 가능하며, 대부분 private으로 해당 외부 클래스 내에서만 사용하는 편이다. 때문에 내부 클래스의 객체는 외부클래스에서 만드어져 사용된다. 

     

     

    무명 클래스란 클래스 이름이 없는 클래스로 바디만 존재한다. 이는 클래스를 정의하는 동시에 객체를 생성하기 때문에 한 개의 객체만 만들 수 있다.

    new 이후 상속받고자하는 클래스나 인터페이스의 이름을 적어 사용한다.

    무명 클래스에서는 변수와 메소드를 정의할 수 있는데 이 때 변수는 final 변수만 가능하다.

     

    내부 무명 클래스란, 객체를 만들어서 내부 클래스에 바로 넣는 것을 나타낸다. 클래스 구조를 외부에 나타내지 않고 구조로만 클래스를 대입할 경우 사용한다.


     

    이런 내부 무명 클래스와는 달리 함수형 프로그래밍에서는 함수를 값처럼 다루는 접근 방법을 택함으로써 이 문제를 해결한다. 클래스를 선언하고 그 클래스의 인스턴스를 함수에 넘기는 대신 함수형 언어에서는 함수를 직접 다른 함수에 전달할 수 있다.

     

    예시를 통해 이해해보자. 

     

    버튼 클릭에 따른 동작을 정의하고 싶을 때, 클릭 이벤트를 처리하는 리스너를 추가한다. 리스너는 onClick이라는 메소드가 들어있는 OnClickListener를 구현해야 한다.

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

    위와 같은 자바 코드를 람다로 리스너를 구현하면 아래와 같이 간결해진다.

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

    람다식을 쉽게 말하면 이름이 없어도 함수 역할을 하는 익명 함수의 형태이다. 

     

    2. 람다와 컬렉션

    컬렉션을 다룰 때 수행되는 반복적인 패턴이 포함된 라이브러리가 존재해야 한다. 하지만 람다가 없다면 컬렉션을 편리하게 처리할 수 있는 좋은 라이브러리를 제공하기 힘들다. 자바에서는 필요한 컬렉션 기능을 직접 작성하곤 했는데, 코틀린에서는 이 습관을 버려야 한다.

     

    책에서는 사람들로 이루어진 리스트 중 가장 연장자를 찾는 코드로 예시를 든다.

    만일 람다를 사용해본 적 없는 개발자라면 루프를 써서 직접 검색을 구현할 것이다.

     

    근데 코틀린에서는 라이브러리 함수를 사용하면 된다.

     

    이런 예시 코드를 보자 

    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)
    }
    //람다를 사용해 컬렉션 검색하기
    
    val people= listOf((Person("Alice",29),Person("Bob",31))
    println(people.maxBy { it.age }) //나이 프로퍼티를 비교해서 값이 가장 큰 원소 찾기

    이처럼 코틀린에서는 라이브러리 함수가 있기때문에 모든 컬렉션에 대해 maxBy함수를 호출할 수 있따. maxBy는 가장 큰 원소를 찾기 위해 비교에 사용할 값을 돌려주는 함수를 인자로 받는다. 

    중괄호에 둘러싸인 코드 {it.age} => 이게 바로 람다! 는 바로 비교에 사용할 값을 돌려주는 함수다. 이 코드는 컬렉션의 원소를 인자로 받아서 비교에 사용할 값을 반환한다. 

     

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

    people.maxBy(Person::age)

     

    이렇게 더 짧고 쉽게 표현할 수 있는 람다 식의 문법을 더 자세히 보자.

     

    3. 람다 식의 문법

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

     

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

    코틀린 람다 식은 항상 중괄호로 둘러싸여 있다. 인자 목록 주변에 괄호가 없다! 화살표가 인자 목록과 람다 본문을 구분해준다.

     

    아까 살펴봤던 코드를 다시 살펴보면,

    people.maxBy{it.age}
    
    //이 예제에서 코틀린이 코드를 줄여 쓸 수 있게 제공했더 기능을 제거하고 정식으로 람다를 작성하면 다음과 같다.
    
    
    people.maxBy({ p: Person -> p.age})

    이렇게 다시 바꾸면, 람다 식 안에서 어떤 일이 벌어지는 지 더 명확히 알 수 있다. 중곽호 안에 있는 람다 식을 maxBy 함수에 넘기고, 람다식은 Person타입의 값을 인자로 받아서 인자의 age를 반환한다.

     

    이 코드를 더 리팩토링해볼 수 있는데 그 요소는 다음과 같다

    1. 너무 구분자(괄호)가 많이 쓰이고 있고, 컴파일러가 문맥을 읽어서 굳이 인자에 이름을 붙이지 않아도 된다.

    2.1. 코틀린에서는 함수 호출 시 맨 뒤에 있는 인자가 람다 식이라면 그 람다를 괄호 밖으로 빼낼 수 있다는 문법 관습이 있다. (이 예제에서는 람다가 유일한 인자이므로 마지막 인자이기도 하다.)

    2.2. 그리고 만약 이 코드처럼 람다가 어떤 함수의 유일한 인자라면 호출 시 빈 괄호를 없애도 된다.

     

    그래서 최종적으로, 괄호에서 람다 식을 꺼내고, 파라미터 타입을 제거하면 다음과 같은 코드가 나온다.

    people.maxBy{ p-> p.age}

    코틀린 컴파일러가 람다 파라미터의 타입을 추론하지 못하는 경우도 있지만, 일단 타입을 쓰지 않고 컴파일러가 타입을 모르겠다고 하지 않는 이상 그대로 가도 된다. 만일 모르겠다고 하면 명시해주면 된다.

     

    그리고 여기서 최종적으로 리팩토링 할 요소가 나오는데

    람다의 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론할 수 있는 경우에는 it을 바로 사용할 수 있다. it은 자동 생성된 파라미터 이름이다.  ( 근데 it은 남용하면 안된다고 한다.)

     

     

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

     

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

     

    예시를 보자면, 

    fun printMessagesWithPrefix(messages:Collection<String>, prefix: String){
    	message.forEach( //각 원소에 대해 수행할 작업을 람다로 받는다.
        	println("$prefix $it") //람다 안에서 함수의 "prefix" 파라미터를 사용한다.
        }
    }
    
    val errors = listOf("403 Forbidden", "404 Not Found")
    printMessgaesWithPrefix(errors, "Error: ")

     

    자바와 다른 점 중 중요한 한 가지는 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다는 점이다. 또한 람다 안에서 바깥의 변수를 변경해도 된다.

     

    앞선 예제와 같이 람다 안에서 사용하는 외부 변수를 '람다가 포획한 변수'라고 부른다. 자바에서는 파이널 변수만 포획할 수 있다. 

     

    ⚠️🚨 꼭 알아둬야 할 점

    람다를 이벤트 핸들러나 다른 비동기적으로 실행되는 코드로 활용하는 경우, 함수 호출이 끝난 다음에 로컬 변수가 변경될 수도 있다. 

    다음 코드는 버튼 클릭 횟수를 제대로 셀 수 없다.

    fun tryToCountButtonClicks(button: Button) : Int{
    	var clicks = 0
        button.onClick{clicks++}
        return clicks;
    }

    이 함수는 항상 0을 반환한다. onClick 핸들러는 호출될 때마다 clicks의 값을 증가시키지만 그 값의 변경을 관찰할 수는 없다. 핸들러는 함수가 clicks를 반환한 다음에 호출되기 때문이다(?!)

     

    이 함수를 제대로 구현하려면 클릭 횟수를 세는 카운터 변수를 함수의 내부가 아니라 클래스의 프로퍼티나 전역 프로퍼티 등의 위치로 빼내서 나중에 변수 변화를 살펴볼 수 있게 해야한다.

     

    5. 멤버 참조

     

    람다를 사용해 코드 블록을 다른 함수에게 인자로 넘기는 방법을 살펴봤다. 하지만 넘기려는 코드가 이미 함수로 선언된 경우는 어떻게 해야할까? 그 함수를 호출하는 람다를 만들면 되지만 이는 중복이다. 함수를 직접 넘기는 방법을 찾아봐야한다. 코틀린에서는 자바 8과 마찬가지로 함수를 값으로 바꿀 수 있다. 이때 이중콜론(::)을 사용한다.

     

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

    이는 다음과 같은 람다 식을 더 간략하게 표현한 것이다.

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

    참조 대상이 함수인지 프로퍼티인지와는 관계 없이 멤버 참조 뒤에는 괄호를 넣으면 안된다. 멤버 참조는 그 멤버를 호출하는 람다와 같은 타입이다.

     

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

    fun salute() = println("Salute!")
    
    run(::salute) //최상위 함수를 참조한다.

    이 코드는 클래스 이름을 생략하고 ::로 바로 참조를 시작한다. ::salute라는 멤버 참조를 run 라이브러리 함수에 넘긴다.

     

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

    val action = { person: Person, message: String -> sendEmail(person, message)}
    //위의 람다 식은 sendEmail 함수에게 작업을 위임한다.
    val nextAction = ::sendEmail //람다 대신 멤버 참조를 쓸 수 있다.

    생성자 차조를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있다. ::뒤에 클래스 이름을 넣으면 생성자 참조를 만들 수 있다.

     

    ❣️바운드 멤버 참조❣️

     

    코틀린 1.0에서는 클래스의 메소드나 프로퍼티에 대한 얻은 참조를 호출할 때 인스턴스 객체를 제공해야 했다. 코틀린 1.1 부터는 바운드 멤버 참조를 지원한다. 바운드 멤버 참조를 사용하면 멤버 참조를 사용할 때 클래스 인스턴스를 함께 저장한 다음 그 인스턴스에 대해 멤버를 호출해준다. 따라서 호출 시 수신 대상 객체를 별도로 지정해 줄 필요가 없다.

     

    val p = Person("Dmitry",34)
    //여기서부터 
    val personsAgeFunction = Person::age
    println(personsAgeFuncion(p))
    
    //위가 코틀린 1.0의 코드
    
    //여기는 
    val dmitrysAgeFunction = p::age 
    println(dmitrysAgeFunction())
    //코틀린 1.1

    여기서 dmitrysAgeFunciton은 인자가 없는 함수여서 코틀린 1.0에서는 p::age 대신에 {p.age}라고 직접 객체의 프로퍼티를 돌려주는 람다를 만들어야만 한다.

Designed by Tistory.