람다식 내에 자유변수가 사용되는 것을 클로저(Closure)라고 합니다. 클로저를 알기 위해서는 자유변수가 무엇이고, 어떻게 동작하는지 알아야 합니다.
자유변수(free variable)는 람다식 내에서 정의되지 않고, 또 파라미터로도 전달되지 않은 외부 Local variable를 참조하는 변수를 말합니다.
public static void repeatMessage(String text, int delay)//enclosing method
{
ActionListener listener = event ->
{
System.out.println(text); //여기에서 text는 free variable이 됩니다.
Toolkit.getDefaultToolkit().beep();
};
Timer t = new Timer(delay,listener);
t.start();
이 코드에서 자유변수는 ActionLiastener를 구현하는 람다식의 text입니다.
repeatMessage("Hello", 1000);
이렇게 실행하면 1초마다 Hello를 출력하게 됩니다.
만약 repeatMessage가 return되고, 파라미터 변수인 text가 메모리 상에서 사라진 후 즉, repeatMessage가 종료되고 나서 에 listener를 실행하게 되면 어떻게 될까요?
바로, text에 "Hello"가 그대로 있는 듯 동작하게 됩니다.
결국, 람다식이 포함되었던 메소드의 종료여부와 관계없이 자유롭게 데이터가 그대로 유지되기 때문에 자유변수라고 하는 것입니다.
이렇게 람다식에서 사용한 데이터를 자유변수에 저장하는 것을 Variable capture라고 합니다. 그리고 람다식에서 자유변수를 참조하는 행위를 람다캡쳐링(Lambda Capturing)이라고 합니다.
그리고 변수가 캡쳐되기 위해서는 다음 두 가지 중 하나의 조건이 만족해야합니다.
즉, 외부 local 변수가 값이 바뀌거나 다시 할당되면 안된다는 것입니다.
public static void countDown(int start, int delay)
{
ActionListener listener = event ->
{
start--;
System.out.println(start);
};
new Timer(delay, listener).start();
이 코드에서 start의 값이 바뀌고 있으므로 final처럼 동작하지 않습니다. 컴파일하게 되면 에러가 발생합니다.
Local 변수 중에서 값이 변하지 않는다고 컴파일러가 판단되면 final로 해석하는 것을 말합니다.
둘의 가장 큰 차이는 외부 의존성에 있습니다. 자유변수가 외부 의존성을 만들므로 자유변수 여부를 확인하면 됩니다.
일반적인 람다식은 자유변수가 없으므로, 외부 의존성이 없습니다.
클로저는 자유변수가 있으므로, 외부 의존성이 있습니다.
위 예시로 사용한 코드를 다음과 같이 바꾸면 외부 의존성이 사라진 람다식이 됩니다.
public static void repeatMessage(int delay)//enclosing method
{
ActionListener listener = event ->
{
System.out.println("Hello");
Toolkit.getDefaultToolkit().beep();
};
Timer t = new Timer(delay,listener);
t.start();
이를 알기 위해서는 람다식의 구현방식과 JVM 구조에 대해서 살펴봐야 합니다. 먼저, JVM의 메모리 구조에서 힙 영역과 스택 영역이 있습니다. 힙 영역은 인스턴스를 저장하고, 스택 영역은 Local 변수, 파라미터, 리턴 값 등등의 임시 값들이 저장되고, 메소드 호출시마다 생성됩니다.
그리고 앞서 정리한 람다식은 무엇인가?에서 알 수 있듯 람다식은 인스턴스로 구현이 됩니다.
public static void repeatMessage(String text, int delay)//enclosing method
{
ActionListener listener = event ->
{
System.out.println(text); //여기에서 text는 free variable이 됩니다.
Toolkit.getDefaultToolkit().beep();
};
Timer t = new Timer(delay,listener);
t.start();
이 코드에서 repeatMessage 메소드를 실행하면 스택 영역 A가 생성되고 그 안의 listener가 참조하는 인스턴스는 힙 영역에 생성됩니다. 그리고 인스턴스 생성과 동시에 람다식에 의한 메소드 실행으로 스택 영역 B가 생성됩니다. text는 스택 A와 B에 각각 있게 됩니다.
만약 스택 B에 있는 text가 스택 A에 있는 text를 참조하는 형식이라면 어떻게 될까요?
repeatMessage 메소드가 종료되어, 스택 A가 사라지게 되어도, 람다식은 힙 영역에 인스턴스가 존재하고, 따로 스택 B가 있기 때문에 계속 동작할 수 있습니다. 참조하는 형식으로 구현되면 스택 A가 사라져 스택 B에서 참조할 text가 사려지는 것이므로 에러가 나게 됩니다.
이러한 에러를 방지하기 위해 참조하는 방식이 아닌 캡처하는 방식으로 구현되는 것입니다. 그리고 캡처하게 되면 변하는 값을 반영할 수 없기 때문에 variable capture에는 조건이 따르는 것입니다.
또한, 객체 변수는 힙 영역에 저장되므로 스택 영역에 저장되는 Local 변수를 자유변수로서 사용될 때만 variable capture를 하는 것입니다.
capture조건/ 구현방식
람다와 클로저
유사 파이널
자유변수
JVM메모리 구조
피피티 두 페이지 밖에 안되서 정리 금방 될 줄 알았는데, 교수님 너무 하시네요.. 정리하다가 뇌절된 부분이 있을 수 있습니다. 피드백 달게 받겠습니다!!