지역 클래스에 대해 공부하던 중 지역 변수 캡쳐와 사실상 final에 대해 알게 되었다. 엄청 중요한 내용은 아니지만 동작 방식이 흥미로워 한 번 정리해보았다.
1. 지역 클래스의 지역 변수 캡쳐
아래와 같은 Outer라는 클래스와 그 안에 있는 process()라는 메서드 그 안에 있는 인터페이스 Printer를 구현한 지역 클래스 LocalPrinter가 있다고 해보자. 아래에서 value는 지역 클래스의 변수이고, localVar는 클래스 내 메서드 내부에 있는 지역 변수, paramVar는 메서드에 넘겨진 매개 변수, outInstanceVar는 외부 클래스의 변수이다.
아래 코드를 실행해보면 정상적으로 잘 실행되는 것을 볼 수 있는데 잘 생각해보면 뭔가 이상하다는 것을 알 수 있을 것이다.
public class Outer {
private int outInstanceVar = 3;
public Printer process(int paramVar) {
int localVar = 1;
class LocalPrinter implements Printer {
int value = 0;
@Override
public void print() {
System.out.println("value = " + value);
System.out.println("localVar = " + localVar);
System.out.println("paramVar = " + paramVar);
System.out.println("outInstanceVar = " + outInstanceVar);
}
}
return new LocalPrinter();
}
public static void main(String[] args) {
Outer outer = new Outer();
Printer printer = outer.process(2);
printer.print();
}
interface Printer {
default void print() {}
}
}
실행 결과
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
하나하나 차근차근 살펴보자.
먼저 process() 메서드는 Printer 타입의 객체를 반환한다. 그리고 main 메서드를 보면 반환된 객체에서 print()를 호출하는 것을 알 수 있다.
얼핏 보면 문제가 없어보이지만 지역 변수 값인 localVar와 paramVar는 문제가 된다.
왜냐하면 process() 메서드가 실행이 끝나면 지역 변수 값인 localVar와 매개변수 값인 paramVar는 스택 프레임에서 제거되어 더 이상 남아있지 않을 것이다. 그럼에도 불구하고 print()를 호출하면 정상적으로 값이 출력된다.
이런 문제가 있기 때문에 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둔다. 이것을 변수 캡쳐라고 한다. 인스턴스를 생성할 때 필요한 지역 변수를 복사해서 보관해 두는 것이다.
이는 직접 코드로 확인해 볼 수 있다. 생성한 인스턴스의 모든 필드 값들을 출력해보면 된다.
public static void main(String[] args) {
Outer outer = new Outer();
Printer printer = outer.process(2);
printer.print();
Field[] fields = printer.getClass().getDeclaredFields();
for (Field field : fields) {
System.out.println("field = " + field);
}
}
실행 결과
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
field = int Outer$1LocalPrinter.value
field = final int Outer$1LocalPrinter.val$localVar
field = final int Outer$1LocalPrinter.val$paramVar
field = final Outer Outer$1LocalPrinter.this$0
위에서 localVar, paramVar가 실제로 필드값으로 가지고 있는 것을 확인할 수 있다.
2. 사실상 final
지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안된다. 따라서 final로 선언하거나 또는 사실상 final(effectively final) 이어야만 한다.
실제로 지역 변수에 final 키워드를 사용하지는 않았지만 값을 변경하지 않는 지역 변수를 뜻한다. 지역 클래스가 접근하는 지역 변수는 final 또는 사실상 final 이어야만하고 이는 문법, 규칙이기 때문에 어길 수 없다.
실제로 변경을 시도하면 아래와 같이 오류가 발생한다.
이렇게 사실상 final이어야만 하는 이유는 지역 변수의 값을 변경하면 인스턴스에 캡쳐한 변수의 값도 변경되어야하기 때문이다. 반대로 인스턴스에 있는 캡쳐 변수의 값을 변경하면 해당 지역 변수의 값도 다시 변경해야 한다. 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡쳐한 변수의 값이 서로 달라지는 문제가 발생할 수 있기 때문에 문법적으로 막아놓은 것이다.
위의 지역 클래스와 관련된 내용들은 흥미롭긴 하지만 사실 실제로 지역 클래스를 사용해서 프로그램을 작성해본 적은 없다. 그래도 프로그램이 실행되면서 어떤 과정을 거치는지 생각해 볼 수 있어서 흥미로웠다.
'Java' 카테고리의 다른 글
JAVA 타입 변환 정리 (0) | 2025.01.07 |
---|---|
JAVA 문자열 관련 메서드 정리 (0) | 2025.01.06 |
Java의 equals()와 hashCode() (0) | 2025.01.03 |
JAVA의 메모리 구조 (0) | 2025.01.02 |
JAVA 배열 (0) | 2024.12.31 |