[Java] Lambda(람다) 사용 시 지역변수 사용하기(With. effectively final)
Java 에서 Lambda 를 사용하면서 개발하던 도중 아래 에러 메시지를 보여주면서 빨간 줄이 그어져 있었다.
Variable is accessed from within inner class needs to be final or effectively final
대충 직역해보면 "내부클래스(Inner Class)안에 있는 변수에 접근할 때는 그 값이 final 이나 effectively final 형태여야 한다." 이다.
원하던 로직은 Stream 을 사용해서 어떠한 값이 있는 경우 카운트를 하고 싶었다.
이 과정에서 지역변수(cnt)를 람다안에서 사용하고자 하니 위 에러가 발생하였다.
int cnt = 0;
memberList.stream().forEach(test -> {
if(test.getTeam().getId().equals("1")){
cnt++;
}
});
이번 글에서는 해당 내용에 대해 알아보고 사용 방법을 정리해보고자 한다.
1. Lambda(람다)
- 이전 글에 간단하게 람다에 대해서는 정리한 것이 있다.
- Lambda 에서는 다음과 같은 특징이 있다.
- 람다식은 외부 block 에 있는 변수에 접근할 수 있다.
- 외부에 있는 변수가 지역변수 일 경우 final 혹은 effectively final 인 경우에만 접근이 가능하다.
- 복사된 지역 변수 값은 람다식 내부에서도 변경할 수 없다.
effectively final?
A non-final local variable or method parameter whose value is never changed after initialization is known as effectively final.
Java 8 에 추가된 syntactic sugar 일종으로, 초기화된 이후 값이 한 번도 변경되지 않았다면 effectively final 이라고 할 수 있다.
- 즉, 람다식에서 참조하는 외부 지역 변수는 final 또는 effectively final 이어야만 한다.
2. 위와 같은 특징이 생기는 이유
2-1. 람다식에서 참조하는 외부 지역 변수는 복사 값이다.
- 지역 변수는 Stack(스택) 영역에 생성된다.(지역 변수가 선언된 block 이 끝나면 스택에서 제거된다.)
=> 메소드 내 지역 변수를 참조하는 람다식을 리턴하는 메소드가 있다면, 해당 메소드 block 이 끝나면 지역 변수가 스택에서 제거되면서 추후에 람다식이 수행될 때 참조할 수 없다.
- 지역 변수를 관리하는 Thread(스레드)와 람다식이 실행되는 Thread 가 다를 수 있다.
=> 스택은 Thread 고유의 공간이고 각 Thread 끼리는 서로 공유하지 않는다.
2-2. 람다식에서 사용하는 변수가 외부 지역 변수인 경우
- 참조하고자 하는 지역 변수가 final 혹은 effectivel final 인 경우에만 접근할 수 있다.
=> 변경이 가능한 경우이다.
public void executelocalVariableInMultiThread() {
boolean shouldRun = true;
executor.execute(() -> {
while (shouldRun) {
// do operation
}
});
shouldRun = false;
}
- 람다식이 어떤 Thread 에서 수행될지 알 수 없다.(예를 들어, 지역 변수(shoudRun)를 제어하는 쓰레드A, 람다식(executor.exute())를 수행하는 쓰레드B 라 가정한다.)
=> 쓰레드B 의 shouldRun 값이 가장 최신 값으로 복사된 것인지 확신할 수 없다.
=> 왜냐하면 shouldRun 은 지역 변수로 언제든지 변경이 가능하고, 지역 변수를 쓰레드 간에 sync 해주는 것은 불가능하기 때문이다.
=> 지역 변수는 쓰레드A 의 스택 영역에 존재하기 때문에 다른 쓰레드에서 접근이 불가능하다.
2-3. 복사된 지역 변수 값은 람다식 내부에서도 변경할 수 없다.
- 즉, final 변수로 다뤄야만 한다는 말이다.
- 복사될 값의 변조를 막기 위해 final 이란 제약을 걸었는데 람다식 내부에서 변경이 가능하다면 final 로 선언한 의미가 없어진다.
- 추가로 컴파일된 람다식은 static 메소드 형태로 변경이 되는데, 이때 복사된 값이 파라미터로 전달되므로 마찬가지로 스택 영역에 존재하여 sync 를 해주는 것이 불가능하다.
2-4. 지역 변수는 안되고 필드(인스턴스 변수, 클래스 변수)는 가능한 이유
- 필드는 Heap 영역 혹은 Method 영역에 선언되어 Thread 별로 모두 접근이 가능하기 때문에, 람다식에서도 바로 접근이 되어도 최신 값을 보장할 수 있다.
- 값이 메모리에서 바로 회수되지 않기 때문에 람다 실행 시 값이 존재하는 것을 보장할 수 있다.
- 그러나 멀티 스레드 환경에서는 sync 를 맞춰주는 작업이 필요할 수 있다.
3. 람다식 내부에서 지역변수를 사용하기
- effectively final 을 보장하기 위해 초기화 한 이후 값을 변경시키지 않으려면 배열이나 객체를 사용하면 가능하다.
배열 내부나 객체 내부의 값이 변경된다고 해도 주소 값이 변경되는 것이 아니기 때문에 가능하다.
// 사용 가능
final int[] cnt = {0};
memberList.stream().forEach(test -> {
if(test.getTeam().getId().equals("1")){
cnt[0]++;
}
});
// 사용 불가능(Variable used in lambda expression should be final or effectively final)
int cnt = 0;
memberList.stream().forEach(test -> {
if(test.getTeam().getId().equals("1")){
cnt++;
}
});
4. 결론
- 람다식에서 외부 지역 변수를 참조하여 사용할 경우 final 혹은 effectively final 이어야 하는 이유는 지역 변수가 Stack(스택)에 저장되기 때문이다.
- 람다식에서는 값을 바로 참조하는 것에 제약이 있어 복사된 값을 이용하게 되는데, 이때 멀티쓰레드 환경에서 복사될/된 값이 변경 가능할 경우 이로 인한 동시성 이슈를 대응할 수 없다.
참고