본문 바로가기

안드로이드 Android

안드로이드 개발 (41) Kotlin In Action 정리 - 5

(이전편 다시보기)

 

안드로이드 개발 (40) Kotlin In Action 정리 - 4

(이전편 다시보기) 안드로이드 개발 (38) Kotlin In Action 정리 - 2 (이전 편 다시보기) 안드로이드 개발 (37) Kotlin In Action 정리 - 1 현업에서 일을 하다보면, Back To The Basic 을 통하여 디버깅 추적 및 안전

gift123.tistory.com

이전편들을 총 요약해보면, 1장은 kotlin의 탄생 배경,철학, 특징을 알아보았습니다.  2장은 기본적인 문법(if, when,try,for 등등..)을 설명하였습니다. 3장은 함수 호출 관련, collection 의 설명, 문자열 치환 등등을 설명하였고 4장은 kotlin의 class,interface, object, companion object, 가시성 등등을 알아보았습니다. 

 

그리고 이어서 kotlin in action 5장에서 정리할만한 내용들을 설명합니다. 

참고로, kotlin in action을 읽으면서 잊혀지기 쉽거나 중요한 내용을 위주로 정리를 하였습니다.

전체 요약본은 아니므로 참고 바랍니다.

 

5장 - 람다로 프로그래밍

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

* Loner 생각: 인자로 람다를 넘길 수 있는 부분이 매우 중요하다 생각합니다. 상속보다 합성으로 코드 구조를 설계할 수 있으며, (대표적으로 compose) 표준 라이브러리를 사용해서 선언형 방식의 코드 작성을 돕게 합니다.* 

 

kotlin 표준 라이브러리는 람다를 아주 많이 사용하고 있습니다. 과거 자바 프로그래머들은 자바에 람다의 도입을 오랫동안 기다려왔고, 자바 8의 람다는 그 기다림의 끝이었습니다. 그만큼 람다는 중요하게 생각되는 기능 입니다. 

 

1) 람다 소개

- 아래는 람다를 사용하지 않고 이벤트를 넘길 때  (Java)

람다 사용전.. 이벤트를 정의할 때
public class LamdaTest {
    public static void main(String[] args) {
        Button button = new Button();
        button.setOnClickListener(new OnClickListener() {
            public void onClick() {
                //....//
            }
        });
    }
}

class Button{
    public void setOnClickListener(OnClickListener listener) {
        listener.onClick();
    }
}


interface OnClickListener{
    void onClick();
}

- 람다를 사용한 koltin의 이벤트처리 

fun main() {
    val button = Button()
    button.setOnClickListener{
        //..//
    }
}
class Button {
    fun setOnClickListener(onClick:()->Unit) {
        onClick()
    }
}

위 예제는 이벤트를 넘기는 상황을 가정하여 작성하였습니다. 두 예제를 비교해봐도 람다가 간결하며, 추가 인터페이스를 생성할 필요도 없으며, 언제든지 인자로 정의된 람다를 넘겨서 사용할 수 있어서 편리합니다.

 

- collection에서 표준 라이브러리 함수는 기본적으로 람다를 활용합니다. 람다를 통해  collection의 원소 찾기, copy 등등을 쉽게 할 수 있습니다.

  val test = listOf(1,2,3,4)
    println(test.filter { it% 2==0 }.maxBy { it })

 

- 멤버 참조를 통해 깔끔한 코드 구성도 가능하다.

fun main() {
    val people = listOf(
        Person("로너", 31),
        Person("누나", 34)
    )
    println(people.maxBy(Person::age))
}

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

* Loner 생각: 개인적으로 멤버 참조는 직관적인 가독성을 보여주기 때문에 매우 유용하기 쓰고 있습니다.* 

 

- 람다를 쓸때 가장 깔끔하게 쓰는건 대괄호만 남겨 쓰는 방식이 깔끔하다. (혹은 멤버 참조)

  // 아래 두개로도 사용할 수 있으나 깔끔한 문법은 아니다.
  people.maxBy(){ it.age }
  people.maxBy({ it.age })
    
    // 아래가 가장 깔끔하다.
    people.maxBy{ it.age }

* Loner 생각: kotlin으로 프로젝트를 진행하다보면 보통 함수 파러미터 정의 규약에 람다는 맨 마지막에 두는 경우가 많은데 위와 같이 깔끔하게 { }로 마지막을 꾸밀 수 있기 때문 입니다.* 

 

- 자바의 익명클래스 본문 안에서 외부 변수는 final 변수만 접근할 수 있습니다.

* Loner 생각: 요즘 kotlin으로만 개발하다보니, 잊고 있었던 지식이네요.."

 

- kotlin의 람다에서 캡쳐링한 변수가 final 변수가 아닐 때 Ref<T> 로 감싸서 래퍼에 대한 참조를 람다 코드와 함께 저장한다. java로 디컴파일한 코드는 아래와 같다.

 

//kotlin의 예시가 아래와 같다면..
var counter = 0 
val inc = {counter++}
/*자바에서 
아래와 같이 InfRef() 를 생성해서 counter.element에 value를 대입하여 사용합니다. */
final IntRef counter = new IntRef();
      counter.element = 0;
      Function0 inc = (Function0)(new Function0() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() {
            return this.invoke();
         }

         public final int invoke() {
         // counter라는 IntRef() 참조 타입을 내부적으로 가지고 사용 합니다. 
            IntRef var10000 = counter;
            int var1;
            // counter++ 는 -> element + 1 을 counter 내부 element에 대입하는 것을 확인 할 수 있습니다.
            var10000.element = (var1 = var10000.element) + 1;
            return var1;
         }
      });

 

 

- kotlin의 멤버 참조, Person::age  -> Person은 클래스, ::은 구분  age는 멤버(프로퍼티나 메서드)입니다.

- Person::age는 {person:Person -> person.age} 를 간략하게 쓴것이다. 

* Loner 생각: 즉, 람다식 입니다. 그렇기 때문에 람다를 인자로 받는 함수에서 중괄호 없이 {} 소괄호 ()안에 간단히 사용 할 수 있는 것입니다. ex: collection<T>.map{ it.age } 을 collection<T>.map(Person::age) 로 사용 가능한 이유입니다."

 

- 함수 언어에서 에타 변환은 함수 f와 람다 {x -> f(x)}를 서로 바꿔쓰는 것을 뜻하는데,  멤버참조가 대표적인 예시다.

- kotlin의 멤버 참조에서 클래스명만 생략한 것은 최상위에 선언된 멤버를 호출하는 것을 뜻한다. (::age, ::name)

data class Person(
    val name:String,
    val age:Int
)
    init {
    //::멤버명은 최상위의 함수나 프로퍼티를 참조한다.
        println(run(::age))
    }
}

* Loner 생각: 위에서 run없이 println(::age) 를 사용하면 property age ~ 라는 문구만 출력될 뿐이다. ::age는 기본적으로 람다이기 때문에 println의 인자에서 바로 읽지는 못하는 것이다. 그래서 run(::age)을 통해 age를 return 받아 사용한다. "

 

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

val createPerson =::Person    
val p = createPerson("아기",1)

- 바운드 멤버 참조는 멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장한 다음 나중에 그 인스턴스에 대해 멤버를 호출해준다. 

 val p = Person("로너",31)
    val getAgeByPerson = Person::age
    println(getAgeByPerson(p))

// {p.age} 를 쓰지 않고 바운드 멤버 참조를 통해 바로 age를 return 해준다. 
    val boundExample = p::age
    println(boundExample())

* Loner 생각: 멤버 참조는 람다식을 에타변환을 하는 것이라서, Person::age 는 {Person -> it.age} 와 같고 마지막 age를 반환해서 int를 얻는다 그래서 getAgeByPerson()은 인자로 Person을 받고 age를 return 하는 것이다."

 

2) 컬렉션 함수형 API

- collection의 map을 사용할 때 참조 멤버를 쓰면 문법이 명확해보인다.

- collection의 filter() 혹은 map() 을 쓸때 반복 작업으로 최종값을 여러번 구하는 것을 조심 하자 

//아래 코드는 100번 최댓값 연산을 수행한다.
people.filter { it.age == people.maxBy(Person::age)!!.age }

//아래와 같이 개선 하면 한번만 연산을 수행한다.
    val maxAge = people.maxBy(Person::age)?.age?:throw NullPointerException("Is Null")
    people.filter { it.age == maxAge }

* Loner 생각:  사이드 이펙트를 최대한 줄인다 생각하고 코딩한다보면 위와 같은 실수를 한번쯤은 할 수 있겠네요. 배열의 반복 계산은 상당히 주의해야할 문제라 생각하고 때에 따라 객체를 따로 선언해서 쓰는것이 가독성면에서 더 plus가 되는 경우가 많은 경우가 있었습니다."

 

- collection의 any, all 을 사용할때 !any,!all을 반대로 쓰지 않는것을 권장한다. !는 가독성이 떨어지기 때문이다.

    println(people.all { it.age<=27 })
    println(people.any { it.age<=27 })
    
    // 아래 처럼 !를 쓰면 가독성이 떨어진다. !는 보기 놓치기 쉽다.
    println(!people.any { it.age<=27 })
    println(!people.all { it.age<=27 })

- collection에서 filter().size 보다, count를 사용하는 것이 효율적이다. 이유는 filter는 collection을 중간에 만들고 반면 count는 조건을 만족하는 원수의 개수만을 추적하기만 하고 collection을 만들지 않기 떄문에 효율적이다. 

 

- find는 firstOrNull 과 같다. null이 나온다는 사실을 더 명확하게 하고 싶다면 firstOrNull을 사용하는 것이 나을 수도 있다. 

 

- groupBy는 인자로 넘겨주는 값을 기준으로 key를 만들고 분류해서 map으로 변환 한다.

println(people.groupBy(Person::age))

/* 
31=[Person(name=로너, age=31),Person(name=로너쌍둥이, age=31)], 
26=[Person(name=로너동생, age=26)],
33=[Person(name=로너형, age=33)],
34=[Person(name=로너누나, age=34)]
*/

* Loner 생각:  groupBy는 인자를 2개를 받는다. 두번쨰 인자는 valueTransform이라는 인자인데 그룹화 된 map의 <T>를 새로운 형식으로 바꾸거나 값을 바꿀 수 있다. 예시는 아래와 같다."

public inline fun <T, K, V> Iterable<T>.groupBy(keySelector: (T) -> K,valueTransform: (T) -> V): Map<K, List<V>> {
    return groupByTo(LinkedHashMap<K, MutableList<V>>(), keySelector, valueTransform)
}
println(people.groupBy(Person::age,Person::name))
{31=[로너, 로너쌍둥이], 26=[로너동생], 33=[로너형], 34=[로너누나]}

println(people.groupBy(Person::age){it.age * 2 })
{31=[62, 62], 26=[52], 33=[66], 34=[68]}

- flatMap 은 map -> flatten 순으로 이루어지는 함수다. 

* Loner 생각: 개인적으로 배열<T>에서 T가 collection을 프로퍼티로 포함하고 있다면, 모든 원소의 이 collection을 전체로 묶어 하나의 리스트로 추출하기 위해 많이 사용하는 편 입니다. 아래 코드와 같은 상황에서 많이 씁니다."

    val rooms = listOf(
        MyRoom("로너", listOf("asd", "asd", "asd")),
        MyRoom("로너", listOf("asd", "asd", "asd"))
    )
    println(rooms.flatMap { it.books })
    //[asd, asd, asd, asd, asd, asd]

 

3) 지연 계산 컬렉션 연산

- 즉시 연산은 filter().map() 처럼 collection을 중간과정에 만들고 최종값을 얻는 방식이고, 지연 계산은 각 원소마다 filter의 계산식과 map의 계산식을 거쳐서 최종값을 얻는 방식이다. sequence를 이용해 활용할 수 있다.

 

//collection을 .asSequence() 를 통해 시퀀스로 변환 시킨다.
people.asSequence()
    //아래 filter와 map은 중간 연산
            .filter { it.name.endsWith("너") }
            .map(Person::name)
    // 아래는 최종 연산이다. 마지막에 sequence로 반환 되기 때문에 필요한 collection으로 변환 해야한다.
            .toList()

아래는 sequence의 내부 동작 설명용 코드다.

listOf(1, 2, 3, 4).asSequence().map {
        print("map($it)")
        it * it
    }.filter {
        print("filter($it)")
        it % 2 == 0
    }.toList()
    
    // 결과 : map(1)filter(1)map(2)filter(4)map(3)filter(9)map(4)filter(16)

 

- sequence는 최종 계산이 끝나면 더 이상 계산하지 않는다. 

    listOf(1, 2, 3, 4).asSequence().map {
        print("map($it)")
        it * it
    }.find {
        if (it > 3) {
            print("find($it)")
            true
        } else {
            false
        }
    }
    
    //결과 map(1)map(2)find(4)
    //시퀀스를 쓰지 않았다면 map(3),map(4) 까지도 진행 되었을 것이다.

- sequence 에서 filter와 map을 쓸때 filter를 먼저 쓰는것이 불필요한 연산을 덜하게 만든다. map을 먼저 사용하면 모든 원소를 변환하지만 filter를 먼저 하면 부적절한 원소를 먼저 제외하기 때문에 그런 원소는 변환되지 않기 때문이다.

  val people = listOf(
        Person("로너", 31),
        Person("로너동생", 26),
    )

    people.asSequence()
        .filter {
            it.name.endsWith("너")
        }
        .map {
            print("이 상황에서 map은 1번만 호출한다.")
            Person::name
        }
        .toList()

//결과: 이 상황에서 map은 1번만 호출한다.

 

- generateSequnce 함수로 시퀀스를 생성해서 사용할 수 있다. 

 val result = generateSequence { i ++ }.takeWhile { i <= 10 }.last()
    println(result)
    결과: 9

* Loner 생각: generateSequnce의 다양한 사용법은 아래 링크에서 확인이 가능하다."

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/generate-sequence.html

 

generateSequence - Kotlin Programming Language

 

kotlinlang.org

 

4) 자바 함수형 인터페이스 활용

 

- (SAM을 인자를 요구하는 메서드에 람다를 넘겨서 만든 객체일 경우) object: ~ 로 만든 무명객체와 람다는 차이가 있다. 객체를 명시적으로 선언하는 경우 메서드을 호출 할 때마다 새로운 객체가 생성된다. 람다는 주변 영역의 변수를 포획하지 않을 경우 무명 객체를 호출할 떄마다 반복 사용한다.

 

-  (SAM을 인자를 요구하는 메서드에 람다를 넘겨서 만든 객체일 경우) 주변 영역의 변수를 포획해서 사용하는 람다의 경우 새로운 인스턴스를 매번 만들어 반환한다.

 

-   리스너 등록 / 해제를 위해서라면 무명객체를 써서 this을 활용해서 도중 취소가 가능하게끔 하는 방식도 있다.

 

람다에 대해 다시 살펴볼 수 있는 시간이라서 좋았습니다.