람다 캡처링

최창효·2023년 9월 8일
0
post-thumbnail

가끔씩 보던 문제

스트림을 사용하기 이전에 'List의 요소 중 가장 큰 값'을 구하기 위해 다음과 같은 코드를 구현했었습니다.

    int maxValue = Integer.MIN_VALUE;
    List<Integer> numbers = List.of(1,5,3,7);
    for (Integer number : numbers) {
        if(number > maxValue) {
            maxValue = number;
        }
    }
    System.out.println("maxValue = " + maxValue); // maxValue = 7

위 코드를 Stream으로 변경시키려 다음과 같은 코드를 짜면

    int maxValue = Integer.MIN_VALUE;
    List<Integer> numbers = List.of(1,5,3,7);
    numbers.stream()
            .forEach(number -> {if(number>maxValue){
                maxValue = number;
            }});
    System.out.println("maxValue = " + maxValue);

maxValue에 대해 Variable used in lambda expression should be final or effectively final라는 에러가 발생합니다.

물론 단순히 Integer형태의 stream에서 최댓값을 얻는 방법은 간단합니다.

    int maxValue = Integer.MIN_VALUE;
    List<Integer> numbers = List.of(1,5,3,7);
    Integer result = numbers.stream()
        .reduce(maxValue,Integer::max);
    System.out.println("max = " + result); // max = 7

여기서는 최댓값 찾기에 집중하기보다 위에서 작성한 코드가 왜 동작하지 않는지를 중심으로 살펴보겠습니다.

원인을 찾아서

해당 코드에서 발생한 Variable used in lambda expression should be final or effectively final에러의 의미는 maxValue라는 변수가 명시적으로 final로 선언되어 있거나, 실질적으로 final이 선언된 변수와 같아야 한다는 의미입니다.

  • 여기서 실질적으로 final변수여야 한다는 의미는 한번 할당된 후 값이 바뀌지 않아야 한다는 의미입니다.

에러가 발생한 이유는 1. maxValue라는 변수가 지역변수여서, 2. maxValue라는 변수의 값을 변경하려고 해서입니다.

1.

maxValue가 지역변수여서 해당 에러가 발생했다는 말은, maxValue가 지역변수가 아니면 이와 같은 문제가 발생하지 않는다는 의미입니다.

public class Temp {
	static int staticVariable = Integer.MIN_VALUE;
	int instanceVariable = Integer.MIN_VALUE;

	void func1(){
    	List<Integer> numbers = List.of(1,5,3,7);
        numbers.stream()
                .forEach(number -> {if(number>staticVariable){
                    staticVariable = number;
                }});        
    }
    
	void func2(){
    	List<Integer> numbers = List.of(1,5,3,7);
        numbers.stream()
                .forEach(number -> {if(number>instanceVariable){
                    instanceVariable = number;
                }});        
    }
    
    // 에러
    void func3(){
    	int localVariable = Integer.MIN_VALUE;
    	List<Integer> numbers = List.of(1,5,3,7);
        numbers.stream()
                .forEach(number -> {if(number>localVariable){
                    localVariable = number;
                }});
    }
}
  • static변수와 인스턴스 변수에 대해서는 아무런 에러가 발생하지 않습니다.

2.

지역변수 maxValue의 값을 변경하려고만 하지 않으면 에러가 발생하지 않습니다.

    int maxValue = Integer.MIN_VALUE;
    List<Integer> numbers = List.of(1,5,3,7);
    numbers.stream()
            .forEach(number -> {if(number>maxValue){
                System.out.println(maxValue);
            }});
    System.out.println("maxValue = " + maxValue);
  • 위 코드는 지역변수인 maxValue의 값을 변경하지 않으므로(실질적으로 final이 선언된 변수와 같으므로) 에러가 발생하지 않습니다.

람다 캡처링

  • 람다 표현식은 익명 함수처럼 자유 변수를 활용할 수 있습니다. 이처럼 파라미터로 넘겨받은 데이터가 아닌 람다식 외부에서 정의된 변수를 람다식 내부에서 활용하는 걸 람다 캡처링이라고 합니다.
    • 자유 변수: 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수

람다 캡처링은 왜 필요할까요? 왜 우리는 람다식 외부의 지역변수를 사용하려면 final또는 final과 같이 사용해야만 할까요?

그 이유는 지역변수가 스택에 저장되기 때문입니다. 지역변수를 할당한 쓰레드가 사라져 변수 할당이 해제되더라도, 람다를 실행한 다른 쓰레드에서는 여전히 그 변수에 접근할 수 있어야 합니다. (이는 람다에서 쓰레드가 다른 지역 변수에 바로 접근할 수 있음을 가정합니다)

즉, 해당 변수를 사용해야 하는데 이미 값이 사라져버리는 상황을 막기 위해 자바에서는 원래 변수에 접근을 허용하지 않고 그 복사본을 전달합니다.

복사본이기 때문에 쓰레드와 함께 지역변수의 할당이 해제되더라도 여전히 람다식을 사용할 수 있고, 복사본이기 때문에 값이 바뀌지 말아야 합니다.
그래서 지역변수를 람다식 안에서 사용하려면 final로 선언하거나, 그 값을 변경하지 않아야하는 것이였습니다.

이러한 지역변수의 제약은 외부 변수의 값을 변환시키는 일반적인 '명령형 프로그래밍 패턴'을 제약하는 효과를 가져다주기도 합니다. 아마 저같이 코드를 짜는 사람들을 막는 효과를 가져다준 거겠죠.

람다 캡처링을 알게 되었으니 앞으로는 아래와 같은 엉뚱한 코드를 짜지 않도록 주의해야 겠습니다.

    int maxValue = Integer.MIN_VALUE; // 람다식에 파라미터로 넘겨지지 않음 (자유 변수)
    List<Integer> numbers = List.of(1,5,3,7);
    // 람다식 안에서 자유 변수를 사용하려면 해당 변수가 final로 선언되어 있거나,
    // 람다식 안에서 자유 변수의 값을 변경하지 않아야 한다!
    numbers.stream()
            .forEach(number -> {if(number>maxValue){
                maxValue = number;
            }});
    System.out.println("maxValue = " + maxValue);

References

https://bugoverdose.github.io/development/lambda-capturing-and-free-variable/
https://thalals.tistory.com/353

profile
기록하고 정리하는 걸 좋아하는 개발자.

0개의 댓글