ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] lambda capture
    Java & Kotlin 2022. 7. 23. 21:22
    반응형

    1) 서론

    자바 개발자라면 '람다 표현식' 이라고 불리는 익명 함수를 자주 사용할 것 같습니다. 꼭 자바 개발자가 아니더라도 '람다 대수'라는것을 배웠다면, 이는 컴퓨터 공학 어디에서든 사용되는 것을 알 수 있는데요.

    기본적으로 람다 대수는 함수의 이름을 익명으로 하고, 추상화된 방식으로 계산을 하는데요.

    f(x)=x+1λx.x+1로 표현할 수 있습니다.

    중요한것은 함수에 X를 받아, X+1 연산을 한다는 것입니다. 즉 함수의 이름은 익명화 시키고, x가 오면 x+1을 계산합니다.

    이러한 람다 표현식이라고 불리는 익명 함수가 자바에도 있는데요.

    이때 지역변수로 선언된 값을 람다 표현식에서 사용하다가 어려움을 만났던 점을 기록합니다.


    2) 람다 표현식으로 지역변수 사용하기

    • 지역변수를 Supplier 함수형 인터페이스로 return 합니다.
    • 단순 지역변수를 사용하는것에 문제없습니다.
    • 람다 표현식 내부에서 지역변수의 값을 +1 합니다.
    • 컴파일 오류 발생합니다.

    3) 람다 표현식과 지역변수

    위의 첫번째와 두 번째 코드의 차이점은 이렇습니다.

    • 둘 다 동일한 지역변수를 참조합니다.
    • 하지만 두번째 코드에서는 지역변수의 값을 변경하려고 합니다.


    람다 표현식에서 지역변수의 값을 변경하려고 하면, 왜 컴파일 오류가 발생하는 걸까요?

    결론을 먼저 말씀드리면, 람다 표현식에서 지역변수를 참조하려면 아래의 조건을 충족해야 합니다.

    • final keyword
    • effectively final (final 인 것처럼 변경되지 않는 값)


    그리고 이유는 람다에서는 변수를 캡처(capture) 하기 때문입니다.


    4) Lambda Capture

    그래서 람다 캡처(caputre)가 뭘까요?

    JVM 메모리 구조에서 하나의 함수는 stack 공간에 할당됩니다. 그리고 stack 공간은 한 개의 스레드가 독립적으로 가지게 되는데요.

    Stack 공간에 할당되는 것은 아래와 같습니다.

    • 함수 내 선언된 지역변수
    • Heap 공간에 선언된 Obejct 타입의 참조 값


    Stack에서 heap 공간에 할당된 인스턴스 변수를 사용하려면, 참조할 수 있는 주소를 가지고 와야 합니다. 값을 그대로 사용하는 것이 아닌, 인스턴스 변수를 가리키는 주소를 통해 값을 참조하는데요. 이를 call by reference라고 합니다.

    Stack 공간에서 지역변수를 가지고 올 때는 실제 값을 가지고 옵니다. 왜냐하면 같은 스레드의 stack 공간에 존재하기 때문인데요. 이를 call by value라고 합니다.

    그리고 Stack이라는 이름에서 유추할 수 있듯이, return과 함께 할당된 변수들은 차례대로 pop 되어 나가게 됩니다.

    이때 람다 표현식은 새로운 stack 공간을 만들게 됩니다. 그리고 사용할 지역변수를 바로 접근하여 사용하는 것이 아닌 capture 후 복사본을 만들어 사용하는데요.

    하지만 capture 한다는 것은 call by value로서 값을 그대로 가지고 옵니다. 즉 다른 stack에 존재하는 지역변수의 값을 변경시키는 것은 안정성을 보장할 수 없는데요

    왜냐하면 지역변수가 선언된 stack에서 값을 어떻게 사용할지 알 수 없기 때문입니다.

    그래서 컴파일러는 지역변수를 final 혹은 effectively final을 사용할 것을 강제하고 있습니다.

    조금 불편할 수는 있지만, 불변성과 안정성이라는 측면에서는 합리적이라고 생각합니다.

    4. 1) 예제

    단순히 지역변수를 참조할 때

    • 위에서 언급한 effectively final입니다.
    • 지역변수로 선언된 localVariable을 return 전까지 변경하지 않습니다.
    • 즉 final은 아니지만, final처럼 불변성을 가진다고 추측할 수 있습니다.

    지역변수의 값을 람다 표현식 안에서 변경시킬 때의 문제

    • 람다 표현식이 참조하는 지역변수는 불변성을 강제하고 있습니다.
    • 반드시 final 혹은 effectively final로 선언하라는 컴파일 에러 발생합니다.

    지역변수의 값을 람다 표현식 안에서 변경시킬 때의 문제 해결

    • final 키워드로 변경하더라도 여전히 컴파일 오류가 발생합니다.
    • final 지역변수는 당연하게도 불변성을 가지며, 변경할 수 없습니다.

    그래서 아래와 같이 해결합니다.

    • 지역변수를 배열로 변경합니다.
    • 이때 배열을 선언한 지역변수 자체를 변경하는 것이 없으므로 effectively final입니다.
    • 컴파일러는 배열을 객체로서 인식하며, heap 메모리에 생성합니다.
    • 즉 배열[0]은 heap 메모리에 할당된 객체의 0번째 값을 참조(reference) 합니다.

    5) 결론

    사실 가볍게는 람다 표현식을 많이 사용했습니다. '→' 하나면 간편하게 한 줄로 표현할 수 있었는데요.

    람다 표현식 내부에서 지역변수를 변경시키는 경험은 처음이었고, 위와 같은 오류를 겪으며 람다 캡처라는 것을 배울 수 있었습니다.

    항상 같은 것만을 사용하지 않고, 도전적으로 새로운 것을 사용하며 오류를 맞이해보는 것도(?) 나름 괜찮은 방법이라고 생각합니다.

    반응형

    댓글

Designed by Tistory.