1. 클로저 소개
클로저는 자신의 매개변수뿐 아니라 생성된 주변 컨텍스트의 변수를 ‘기억’하여 사용하는 함수(또는 함수 객체)입니다. 쉽게 말해, 메서드 내부의 람다식이나 익명 클래스가 그 메서드의 변수를 사용하면 — 그것이 바로 클로저입니다.
직관적인 예
public class ClosureDemo {
public static void main(String[] args) {
String greeting = "안녕, ";
Runnable sayHello = () -> System.out.println(greeting + "세계!");
sayHello.run(); // 출력: 안녕, 세계!
}
}
여기서 람다는 외부 메서드의 변수 greeting을 캡처하여 내부에서 사용합니다. 이것이 바로 클로저입니다!
2. 사실상 final 변수란? 왜 필요한가?
Java에서 람다식(및 익명 클래스)은 외부 메서드의 변수 중 final로 선언되었거나 초기화 이후 변경되지 않는 변수만 사용할 수 있습니다. 이런 변수를 사실상 final이라고 부릅니다.
왜 그럴까?
람다식은 생성된 메서드가 끝난 뒤에도 호출될 수 있기 때문입니다. 만약 변수를 변경할 수 있다면, 어느 시점의 값을 사용해야 하는지 혼란이 생깁니다. 이러한 놀라움을 방지하기 위해 Java는 변수가 불변(또는 최소한 불변처럼 보이는)이어야 한다고 요구합니다.
예: 올바른 사용
public static void main(String[] args) {
int number = 42; // number는 사실상 final
Runnable r = () -> System.out.println(number);
r.run(); // 42
}
예: 사용 후 변수를 변경하려는 시도
public static void main(String[] args) {
int number = 42;
Runnable r = () -> System.out.println(number);
number++; // 오류: 변수 number는 final 또는 사실상 final이어야 합니다
r.run();
}
컴파일러는 다음 오류를 보고합니다: Variable used in lambda expression should be final or effectively final.
사실상 final이란... 한 번만 대입되고 이후로 변경되지 않는 변수를 말합니다. 굳이 final을 붙이지 않아도 컴파일러가 판단합니다.
3. 람다식은 변수를 어떻게 캡처하나?
람다가 외부 변수를 사용하면 Java는 그 변수를 람다와 함께 ‘포장’합니다. 람다가 만들어진 메서드가 이미 종료되었더라도 변수는 사라지지 않고 — 클로저 내부에 살아 있습니다.
예시: 람다가 변수를 ‘기억’한다
public static Runnable createGreeter(String name) {
// name은 메서드 매개변수로, 람다에 의해 캡처됩니다
return () -> System.out.println("안녕, " + name + "!");
}
public static void main(String[] args) {
Runnable greeter = createGreeter("Vasya");
greeter.run(); // 안녕, Vasya!
}
여기서 변수 name은 더 이상 main 메서드의 스택에 없지만, greeter는 여전히 그 값을 ‘기억’하고 있습니다.
내부적으로 어떻게 구현되나?
Java 컴파일러는 캡처된 변수를 저장하는 특수 보조 객체(‘capture/display class’라고도 함)를 생성합니다. 람다식은 이 변수 ‘컨테이너’를 참조하는 객체가 됩니다.
4. 클로저 예: 변수를 사용하는 함수를 반환하기
자신의 컨텍스트의 변수를 사용하는 람다를 반환하는 함수를 작성해 봅시다:
import java.util.function.IntSupplier;
public class ClosureFactory {
public static IntSupplier makeAdder(int x) {
// x는 람다에 의해 캡처됨
return () -> x + 10;
}
public static void main(String[] args) {
IntSupplier adder = makeAdder(5);
System.out.println(adder.getAsInt()); // 15
}
}
여기서 변수 x는 이미 메서드 스택에서 ‘사라졌지만’, 람다는 여전히 그것을 사용할 수 있습니다.
5. 왜 캡처된 변수를 변경할 수 없을까?
public static void main(String[] args) {
int base = 100;
Runnable printer = () -> System.out.println(base);
base = 200; // 오류!
printer.run();
}
컴파일러가 이를 허용하지 않습니다. 만약 base를 변경할 수 있다면 람다에서 어떤 버전의 값을 사용해야 하는지 불명확해집니다. 따라서 Java는 람다가 캡처한 지역 변수를 변경하는 것을 금지합니다.
람다에서 무엇을 사용할 수 있나?
- 초기화 이후 변경되지 않는 지역 변수(사실상 final).
- 클래스 필드(static이든 비정적이든) — 변경할 수 있지만, 이는 지역 변수 캡처가 아니라 객체 상태에 대한 접근이라는 다른 메커니즘입니다.
6. 익명 클래스와의 비교
람다식이 등장하기 전에는 Java에서 익명 클래스를 사용해 클로저와 유사한 것을 만들 수 있었습니다:
public static void main(String[] args) {
String word = "Java";
Runnable r = new Runnable() {
public void run() {
System.out.println(word);
}
};
r.run(); // Java
}
규칙은 동일합니다: 변수 word는 final 또는 사실상 final이어야 합니다.
차이점: this의 스코프
- 익명 클래스에서 this는 익명 클래스의 인스턴스를 가리킵니다.
- 람다식에서 this는 외부 객체(예: 현재 클래스의 인스턴스)를 가리킵니다.
7. 클로저와 클래스 필드
람다가 클래스 필드를 사용한다면, 이는 엄밀히 말해 ‘지역 변수 캡처’가 아닙니다 — 필드는 항상 접근 가능하며, 변경할 수도 있습니다.
public class Counter {
private int count = 0;
public Runnable makeCounter() {
return () -> {
count++;
System.out.println("카운터: " + count);
};
}
public static void main(String[] args) {
Counter c = new Counter();
Runnable r = c.makeCounter();
r.run(); // 카운터: 1
r.run(); // 카운터: 2
}
}
8. Java에서 클로저의 흔한 실수와 특징
실수 №1: 람다가 캡처한 변수를 변경하려는 경우. 가장 흔한 실수는 람다에서 사용된 지역 변수를 나중에 변경하려는 것입니다. 컴파일러는 다음과 같이 알립니다: Variable used in lambda expression should be final or effectively final.
실수 №2: 변수가 ‘고정(frozen)’되어 있다고 기대하는 경우. Java에서 캡처된 변수가 클래스 필드라면 값의 복사가 아니라 원본에 대한 참조입니다. 클래스 필드가 바뀌면 람다도 새로운 값을 봅니다. 하지만 람다에서 사용하는 지역 변수는 반드시 사실상 final이어야 합니다.
실수 №3: 람다가 this에 대한 새로운 스코프를 만든다고 기대하는 경우. 람다에서 this는 외부 객체(컨테이너 클래스)이고, 익명 클래스에서 this는 익명 클래스 자신입니다.
실수 №4: 가변 객체를 사용하는 경우. 가변 객체(예: 리스트)에 대한 참조를 캡처하면, 변수 자체가 사실상 final이더라도 람다 내부에서 그 내용은 변경할 수 있습니다:
public static void main(String[] args) {
java.util.List<String> list = new java.util.ArrayList<>();
Runnable r = () -> list.add("Hello");
r.run();
System.out.println(list); // [Hello]
}
여기서 변수 list는 바뀌지 않습니다(list = ...를 하지 않음). 하지만 그 안의 객체 상태는 변경됩니다.
GO TO FULL VERSION