(Java) 람다 캡처링과 final 제약조건

람다의 바디에서는 파라미터 말고 바디 외부에 있는 변수를 참조할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
public class LambdaCapturing {
private int a = 12;

public void test() {
int b = 123;

final Runnable r = () -> System.out.println(a);

final Runnable r2 = () -> System.out.println(b);
}
}

이렇게 람다 시그니처의 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 **자유 변수(Free Variable)**라고 부른다.
또한 람다 바디에서 자유 변수를 참조하는 행위를 유식한 말로 **람다 캡처링(Lambda Capturing)**이라고 부른다.

람다 캡처링의 제약 조건

지역 변수를 람다 캡처링 할 때 아래 두 가지 제약조건이 존재한다.

  1. 지역변수는 final로 선언돼있어야한다.
  2. final로 선언되지 않은 지역변수는 final처럼 동작해야한다.
    즉, 값의 재할당이 일어나면 안 된다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class LambdaCapturing {
    private int a = 12;

    public void test() {
    final int b = 123;
    int c = 123;
    int d = 123;

    final Runnable r = () -> {
    // 인스턴스 변수 a는 final로 선언돼있을 필요도, final처럼 재할당하면 안된다는 제약조건도 적용되지 않는다.
    a = 123;
    System.out.println(a);
    };

    // 지역변수 b는 final로 선언돼있기 때문에 OK
    final Runnable r2 = () -> System.out.println(b);

    // 지역변수 c는 final로 선언돼있지 않지만 final을 선언한 것과 같이 변수에 값을 재할당하지 않았으므로 OK
    final Runnable r3 = () -> System.out.println(c);

    // 지역변수 d는 final로 선언돼있지도 않고, 값의 재할당이 일어났으므로 final처럼 동작하지 않기 때문에 X
    d = 12;
    final Runnable r4 = () -> System.out.println(d);
    }
    }

왜??

왜 이런 제약조건이 생기게 돼었을까?
왜 인스턴스 변수에는 이런 제약조건이 없는 걸까?

우선 JVM의 메모리 구조를 알아야한다.
JVM에서 지역 변수는 스택이라는 영역에 생성된다.
그리고 실제 메모리와는 달리 JVM에서 스택 영역은 쓰레드마다 별도의 스택이 생성된다.
따라서 지역 변수는 쓰레드끼리 공유가 안 된다.
JVM에서 인스턴스 변수는 힙 영역에 생성된다.
인스턴스 변수는 쓰레드끼리 공유가 가능하다.

람다는 별도의 쓰레드에서 실행이 가능하다.
따라서 원래 지역 변수가 있는 쓰레드는 사라져서 해당 지역변수가 사라졌는데도 불구하고,
람다가 실행 중인 쓰레드는 살아있을 가능성이 있다.
하지만 이 람다에서 사라진 쓰레드의 지역변수를 참조하고 있으면 어떻게 될까?
당연히 오류가 날 것이다. 하지만 우리의 예상과는 달리 오류는 나지 않는다.
또한 별도의 쓰레드에서 실행된다면 별도의 스택 영역을 가질테고, 그럼 다른 쓰레드의 스택에 있는 지역변수는 참조조차 할 수 없다.
왜 오류는 나지 않고, 어떻게 다른 쓰레드의 스택 영역에 있는 지역 변수를 참조할 수 있는 걸까?
이는 람다에서 지역 변수(해당 쓰레드의 스택)에 직접적으로 접근하는 게 아니라 변수를 자신(쓰레드)의 스택에 복사하기 때문이다.
그렇기 때문에 별도의 쓰레드의 스택에 있는 지역 변수와 동일한 값을 참조할 수 있는 거고, 원래 쓰레드가 사라져도 본인의 쓰레드에서 자신의 할 일을 착실히 수행할 수 있는 것이다.
하지만 위와 같이 변수를 복사해서 쓰는데 그 변수의 값이 중구난방으로 변경된다고 하면 해당 복사본을 믿고 쓸 수 있을까?
따라서 지역 변수에는 final이어야하거나 final 같이 동작해야한다는 제약 조건이 생긴 것이다.

그렇다면 인스턴스 변수는 왜 이런 조건이 없는 걸까?
이는 인스턴스 변수는 힙에 존재하고, 쓰레드끼리 공유도 가능하기 때문에 별도로 복사할 필요도 없고, 직접 힙에 접근해서 사용하면 되기 때문이다.