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 영역에 할당되는 메서드에게 넘겨주는 파라미터는 call by value입니다. 이는 변수의 값을 copy 하여 사용하는 것입니다. 즉 메서드로 파라미터를 넘겨주고, 넘어온 값을 변경시키더라도 원래의 변수에 할당된 값에는 영향이 없습니다.

    람다식은 함수를 람다 표현식으로 코드 레벨에서 작성하는것인데요. 실제로 컴파일 타임에 람다는 하나의 메서드로 취급됩니다. 그리고 람다식을 호출하고 있는 메서드의 local 변수도 사용할 수 있습니다.

     

    만약 Runnable 인터페이스를 구현한 람다식이라면 컴파일 타임에 새로운 클래스로 생성되고, 지역변수를 제공하는 메서드가 이미 GC에 의해서 제거된 후 시점까지 살아남을 수 있습니다. 즉 메서드와 함께 지역변수가 해제된다면, 람다식이 사용하는 지역변수가 사라져서 문제가 발생할 수 있습니다.

     

    그렇기 때문에 람다식은 지역변수를 capture(copy)하여 사용합니다. 해당 지역변수를 람다식 내/외부 모든 범위에서 변경된다면 일관성 문제가 생길 수 있기 때문에 컴파일러는 지역변수를 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.