[Kotlin] 코틀린 람다에 대한 이해 - Understanding Kotlin Lambda

Nilto·2023년 5월 21일
0

람다는 코틀린에서 중요하다! 아니, 람다는 현대 프로그래밍 언어에서 중요한 역할을 한다! 그래서 정리를 간단히 한 번 해보고 싶었고, 글을 쓴 이후에도 관련된 내용들이 계속 추가될 수 있는 글이 될 것 같다.

람다는 무엇인가?

만약 당신이 코틀린 사용자라면 (사실 아니라 할지라도), 람다를 많이 많이 쓰고 있을 것이다. 람다는 여러가지로 정의해볼 수 있을 것이다. 명확한 정의가 따로 정해져있는지는 모르겠으나, 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다고 생각하면 될 것 같다. 함수를 값 처럼 넘기는 것이다.

람다는 클로저와도 연관이 있는데, 클로저는 자신을 둘러싸고 있는 외부의 코드에 있는 변수에 마음대로 접근할 수 있는 코드 조각이라고 볼 수 있다. 이 코드 조각에 외부 변수를 capture한 것이므로 캡쳐링이라고도 하는 것 같다. (개인적으로 이러한 용어들 그 자체로는 중요하지 않다고 생각한다.) 그렇게 보면, 클로저는 항상 람다이지만, 람다는 항상 클로저는 아니다. 이 개념은 생각보다 중요하다고 할 수 있다.

자바에서의 람다

※ 이 단락은 '폴리글랏 프로그래밍'이라는 책을 많이 참고했다.

우리는 람다를 잘 이해하기 위해서 코틀린 이전에 자바를 봐야한다. 자바에서는 원래 람다가 없었다. 자바의 나름 긴 역사를 생각해보면, 생각보다 얼마 되지 않았다고도 볼 수 있다.

2014년! 지금 글을 쓰는게 2023년이니 대략 10년 전이라고 할 수 있을 것이다.
우리가 잘 아는 자바에서의 익명클래스를 보도록 하자. 결국 람다는 이 익명클래스가 본질이다.

public void foo() {
	final int n = 1;
    button.setOnClickListener(new OnClickListener(){
    	@Override
        public void onClick(View view) {
       		println(n.toString())
        }
    });
}

이 코드 블럭에는 final이 존재한다. 자바에서는 final이 없으면 캡쳐링 할 수 없다. 그러나 평범한 클로저(다른 언어들이 클로저를 적용한 방식)에서 이 final이라는 제약이, 즉, 자유 변수(캡쳐링한 값)를 바꿀 수 없다는 제약이 존재할까? 그렇지는 않다고 한다. 그래서 익명클래스는 다른 언어에서 지원하는 클로저와 동일하다고는 할 수 없다.

그래서 이 익명 클래스가 클로저인지 아닌지에 대한 논쟁이 있었다고 한다. 조슈아 블로흐, 닐 게프터라는 두 자바 구루들이 이러한 논쟁을 벌였는데, 조슈아 블로흐는 클로저 관련은 익명 클래스만으로 충분하다의 입장이였고, 닐 게프터는 익명 클래스를 클로저로 보지 않았고, 함수 타입이 추가되기를 바랬다고 한다. 이런 이야기가 전해져 내려오고 있는 것처럼, 자바에서 람다 도입은 결코 쉬운 일은 아니었다고 한다. 최근 버전의 자바에서 자유 변수를 바꿀 수 있는지는 모르겠다. 하지만 JDk 8버전 까지만 해도 그런 일은 없었다. (아마? 지금도 안될 것이라 생각한다.)

그러면 왜 java에서 자유 변수가 final 이어야하는가?

기본적인 JVM 구조를 생각해보자. 람다에서 캡쳐한 내용은 스택에 있을 수 있다. 그러나 람다 그 자체는 힙에 존재할 것이다. 만약 람다에서 변수의 레퍼런스를 가지고 있다고 친다면, 그 변수가 스택에서 사라질 때, 힙에 있던 람다에서는 그 변수를 참조 할 수 없다. 그래서 이른바 '캡쳐링'을 하는 것이다. 그래서 final로 지정해서 바뀔 일이 없어야 하는 것이다.

코틀린 람다에서의 캡쳐링

그러면 돌고 돌아 코틀린에서의 람다를 생각해보자. 코틀린에서는 var, val 둘 다 캡쳐가 가능하다! 약간의 속임수를 통해 변경 가능한 변수를 포획할 수 있는데, 특별한 래핑을 한다. 그리고 특별한 래퍼에 대한 참조를 람다 코드와 함께 저장하는 것이다.
즉, 다음과 같은 변경이 일어난다고 보면 된다.

var counter = 0
val inc = { counter.value++ }

의 코드가

class Ref<T> (var value:T)
val counter = Ref(0)
val inc = { counter.value++ }

이렇게 변경이 일어난다고 보면 된다.

또한, 만약 캡쳐링이 없는 람다는 컴파일러 단에서 싱글톤처리를 해주지만, 캡쳐를 하면 그럴 수 없다는 점에 유의하자. 매번 저렇게 Ref 클래스를 가지도록 인스턴스 생성을 따로 해야하기 때문이다.
(이는 Jetpack Compose에서의 람다 최적화도 약간은 연관되어 있는데, 이후에 작성하겠다)

컴포즈의 예시들로 람다 이해도 높여보기

rememberUpdateState

다음 링크를 참고해보자.

@Composable
fun LandingScreen(onTimeout: () -> Unit) {

    // This will always refer to the latest onTimeout function that
    // LandingScreen was recomposed with
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // Create an effect that matches the lifecycle of LandingScreen.
    // If LandingScreen recomposes, the delay shouldn't start again.
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }

    /* Landing screen content */
}

만약 rememberUpdatedState 없이 바로 onTimeOutLaunchedEffect에 넣었다면 LaunchedEffect가 가진 onTimeOut은 바뀌지 않을 것이다. 캡쳐를 했기 때문이다. onTimeOut같이 람다가 아니라 그냥 일반 변수라도 마찬가지다.

끝으로

'폴리글랏 프로그래밍'에는 내가 좋아하는 프로그래밍의 원리에 대한 구절이 있다.
"모든 언어의 발전은 추상수준을 상승시켜서 프로그래머가 작성해야하는 행사코드(보일러 플레이트)의 분량을 줄이는 방향으로 움직인다."
람다 또한, 수많은 프로그래머들의 보일러 플레이트 코드를 없애줬음에 틀림없다.

Reference

코틀린 공식 문서
도서 - 코틀린 인 액션
도서 - 폴리글랏 프로그래밍
https://developer.android.com/jetpack/compose/side-effects

profile
안드로이드 개발자. 컴포즈, 코루틴, 코틀린, 유니티에 관심이 많습니다.

0개의 댓글