최근 알고리즘 문제를 풀면서 스트림과 람다를 가끔 활용하곤 하는데, 위와 같은 경우를 몇 번 맞닥뜨리게 되었습니다.
읽어보면, "람다 표현식에서 사용하는 변수는 final이거나, effectively final이어야 한다..." 라고 하는데 이에 대해 알아보겠습니다.
순서는 그렇다면,
- effectively final은 무엇인지,
- 왜 람다 표현식에서 사용하는 변수는 final이거나, effectively final이어야 하는지
와 같겠습니다.
effectively final
effectively final은 람다 표현식이 등장한 자바 8에 함께 등장했습니다. 이는 final 키워드가 선언되지 않았지만, 값이 재할당되지 않아 final 변수와 유사하게 동작하는 변수를 의미합니다.
자바 언어 스펙을 살펴보면 아래와 같은 경우를 effectively final로 간주합니다.
- final 키워드가 사용되지 않았다.
- 초기화 후, 재할당되지 않았다.
- 전위 또는 후위 연산자가 사용되지 않았다.
객체의 경우라면, 해당 객체가 가리키는 참조를 변경하지 않으면 effectively final로 간주합니다.
그렇다면 왜 이러한 특징을 가지는 변수를 람다 표현식 내부에서 사용해야 하는지에 대해 알아보겠습니다.
람다 표현식에서 사용하는 변수
먼저 람다 표현식에 대해 짧게 알아봐야 합니다.
이는 자바 8에서 등장한 개념으로, 메소드를 하나의 식으로 표현하는 프로그래밍의 방법입니다.
(원래 자바는 익명 클래스를 이용해 익명 구현 객체를 사용할 수 있었습니다.)
람다 표현식은 하나의 메소드를 가지는 인터페이스와 같은 익명 클래스의 부담을 줄여주기 위해 등장했다고 할 수 있습니다.
(이에 대한 자세한 내용은 또 다른 포스트로 다뤄야 할 것 같습니다...)
따라서 람다 표현식은 마치 하나의 메소드를 선언하는 형태인데, 자바에서는 메소드를 단독으로 선언할 수 없기에, 람다 표현식은 이 메소드를 가진 객체를 생성합니다.
- 그리고 이 객체의 타입은 인터페이스입니다. 즉, 람다 표현식은 인터페이스의 익명 구현 객체를 생성한다는 의미입니다.
이러한 람다에서는 외부에 정의된 변수를 사용할 때 내부에서 사용할 수 있도록 복사본을 생성합니다. 이를 람다 캡처링(Lambda Capturing)이라고 합니다.
- 여기서 외부에 정의된 변수는 지역 변수와 인스턴스 변수, 클래스 변수를 포함합니다.
지역 변수
그렇다면 여기서, "복사본을 생성"하는 이유는 무엇일까요?
복사본을 생성하는 외부 변수 중, 지역 변수를 통해 이해해보겠습니다.
먼저 지역 변수는 블록 안에서 선언된 변수로, 메모리 영역 중 스택에 할당됩니다.
스택 영역의 특징으로는 다음과 같은데, 두번째 특징이 여기서 사용됩니다.
- 자바의 기본 자료형의 데이터값이 저장되는 공간
- 스레드마다 고유하게 가지는 영역
따라서 해당 메모리 영역은 스레드끼리 공유가 불가능하고, 스레드 종료 시 해당 메모리도 해제됩니다.
그렇다면, 다음과 같은 경우가 발생할 수도 있습니다.
- 만약 람다가 현재 스레드가 아닌 별도의 스레드에서 실행되고,
- 해당 람다에서 사용하려는 지역 변수 값을 복사하지 않고 그대로 사용한다면,
지역 변수가 존재하는 스레드와 람다가 실행되는 스레드의 소멸 시점에 따라 람다의 결과가 달라지게 될 수도 있습니다.
- 지역 변수가 존재하는 스레드가 먼저 소멸하게 된다면, 람다를 실행하는 스레드에서 해당 지역 변수에 접근할 수도 없게 됩니다.
따라서, 람다에서는 사용하려는 지역 변수의 값을 복사해서 사용하도록 합니다.
당연히, 해당 지역 변수에 대한 수정도 불가능합니다.
서로 다른 스레드에서, 특정 변수 값에 대한 수정이 발생하면, 해당 변수가 도대체 어떤 값을 택해야 할지 예측할 수 없는 동시성 문제가 발생하게 됩니다.
인스턴스 변수, 클래스 변수
이제 "지역 변수"는 할당되는 메모리 영역이 "스택 메모리"이기에 복사본을 생성한다라는 것을 알 수 있었습니다.
그렇다면 인스턴스 변수나 클래스 변수의 경우는 어떨까요?
똑같이, 해당 변수들의 할당 영역을 살펴보면, 다음과 같습니다.
- 인스턴스 변수 : 힙 영역
- 클래스 변수 : 메소드 영역
간단하게 두 영역의 특징을 살펴보면, 공통적으로 "다수의 스레드가 동시 접근이 가능하다"는 점이 있습니다.
즉, 스레드에 구애받지 않으므로 복사본을 생성할 필요가 없습니다.
결론
람다 표현식 내부에서 외부 지역 변수들은 final이거나, effectively final이어야 합니다.
이러한 이유는 지역 변수가 스택 영역에 할당되기 때문입니다.
그렇지 않은 클래스 변수나 인스턴스 변수의 경우는 final 또는 effectively final이지 않아도 람다 표현식에서 사용할 수 있습니다.
자바를 배우고 사용하면서, final 키워드에 대해 알고는 있었지만, effectively final과 람다 표현식에서 사용하는 변수는 어떤 특성을 가져야 한다는 점은 이번 글을 통해 배우게 된 것 같습니다.
앞으로도 사소한 것에 질문을 던지면서 생각해보고 글로 풀어 설명해보는 연습을 많이 해야겠습니다!
출처
- effectively final : https://madplay.github.io/post/effectively-final-in-java
- 람다 표현식 : https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
- 람다와 메모리 구조 : https://devkingdom.tistory.com/272
'Java지식' 카테고리의 다른 글
기본 자료형과 참조 자료형 (7) | 2022.11.30 |
---|---|
Overloading과 Overriding (1) | 2022.09.19 |
절차적 프로그래밍과 객체 지향 프로그래밍 (1) | 2022.08.22 |