본문 바로가기

자바

[Java] 람다의 변수 범위

2021-03-19글

람다의 변수 범위

물론 위의 코드는 미션을 위해 일단 구현만을 목적으로 많은 리팩토링이 필요하니 참고 🥲 미션을 진행하다가 for 문 내에 stream을 쓰게 되었는데, i의 값을 람다식 내에서 사용하려니 컴파일 에러가 났다. "Variable used in lambda expression should be final or effectively final" 즉 람다식에서 사용되는 변수는 final이거나 effectively final이어야 한다.

effectively final은 무엇이며, 람다의 변수 범위를 정확히 짚고 넘어가자.

람다의 변수 범위

다음과 같은 Lambda 클래스가 있고, 각각의 메서드를 호출해 클래스 필드인 i가 어떻게 바뀌는지 살펴본다 🔎

public class Lambda {
private int i = 1;

public Integer example1() {
Supplier<Integer> function = () -> i * 5;
return function.get();
}

public Integer example2(int i) {
Supplier<Integer> function = () -> i * 10;
return function.get();
}

public Integer example3() {
int i = 1;
Supplier<Integer> function = () -> i * 15;
return function.get();
}
}

이에 대한 출력 결과는 다음과 같다.

위 예제에서 람다식에서는 자신을 감싼 메서드나 클래스에 속한 변수에 접근을 할 수 있다. 예제는 람다에 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 사용하고 있는데 이러한 변수를 자유 변수라고 부른다.

그렇다면 다시 맨 처음 오류를 가져와서 ...

이 부분에서 왜 자유 변수인 i를 final이나 effectvely final로 선언하라는 것일까 ?

먼저 지역 변수는 JVM에서 스택 영역에 저장이 된다. 그리고 실제 메모리와 다르게 JVM에서 이 스택 영역은 스레드 마다 별도의 스택이 생성된다. 따라서 지역 변수는 한 스레드 안에서만 사용이 되며, 스레드끼리 공유하지 못한다.

참고로 지역 변수와 다르게 인스턴스 변수는 힙 영역에 생성되어 서로 다른 스레드끼리도 공유할 수 있는 공유 변수이다.

람다는 각각 별도의 스레드에서 실행이 가능하다. 람다가 지역 변수에 접근하려 할 때에는 지역 변수가 존재하는 스택 영역에 직접 접근하지 않고 이 변수의 복사본을 만들어 동작한다. 이를 람다 캡처링이라고 한다. 때문에 이 복사본을 가지고 동작할 예정인데, 반환된 람다식은 여러 스레드에서 동작할 수 있기 때문에 동기화 문제가 일어날 수 있다. 때문에 이 복사본 값이 바뀌어 버리면 의도하지 않은 결과가 생길 수 있으므로 컴파일 단계에서 final 또는 effectively final로 선언해 변수를 신뢰할 수 있게 만드는 것이다. 이를 람다 캡처링이라고 한다.

위 문제에서 컴파일 에러가 뜨는 것은 i가 스코프 밖에 있어 값이 변할 수 있어 신뢰할 수 없다. 때문에 인텔리제이가 권해주는 방법을 쓰면 i를 스코프 안 변수에 새로 할당해서 사용하게 된다.

여기서 i는 final 로 선언되지는 않았지만, 자바 8에서 추가된 effectively final로 선언된 것이다. effectively final은 final로 선언되지 않아도 컴파일러가 해당 변수가 변경되지 않았다고 판단할 수 있다.

만약 effectively final인 i의 값을 바꾸려 한다면 이렇게 컴파일 오류가 발생한다.

파도 파도 끝없는 람다의 세계 🤸‍♀️

참고 자료