Java Local Class - 지역 변수 캡처
지역클래스로 접근하는 지역 변수의 값은 변경되어선 안된다.
캡처를 이야기 하기 전 메모리의 변수 생명 주기로부터 알아보자.
메모리 변수 생명 주기
메소드 영역
Method Area
+------------------------------+
| +--------------------------+ |
| | static Area | |
| | static Variable Creation | |
| +--------------------------+ |
+------------------------------+
스택 영역
Stack Area
+-----------------------+
| +-------------------+ |
| | method1 () frame | |
| | data1 = 0x000001 | |
| +-------------------+ |
| +-------------------+ |
| | main() frame Area | |
| | data1 = 0x000001 | |
| +-------------------+ |
+-----------------------+
힙 영역
Heap Area
+-----------------------+
| 0x000001 |
| +-------------------+ |
| | value = 10 | |
| +-------------------+ |
| Data Instance |
+-----------------------+
각각의 메소드, 스택, 힙 영역 각각 기존의 생명 주기를 살펴보자.
- 클래스 변수
- 프로그램 종료 까지 가장 길게 생명을 유지한다. (메소드 영역)
- 클래스 변수(static 변수)는 메소드 영역에 존재한다.
- 클래스 정보를 읽어 들이는 순간부터 프로그램 종료까지 존재하게 된다.
- 인스턴스 변수
- 인스턴스의 생존 기간만큼 유지한다. (힙 영역)
- 인스턴스 변수는 본인이 속한 인스턴스가 GC 되기 전까지 존재한다. (생명주기는 긴편)
- 지역 변수
- 메소드 호출이 끝나면 사라짐 (스택 영역)
- 지역 변수는 스택 영역의 프레임 안에 존재하여 메소드 호출되면 생성하고, 호출이 종료되면 스택 프레임이 제거되어 그 안의 지역 변수가 모두 삭제된다.
- 지역 변수의 생존 주기는 매우 짦다.
- 매개 변수 또한 지역 변수의 한 종류이다.
지역 클래스 변수 다루기 1
public class LocalOuter {
private int OutInstanceVar = 3;
public Printer process(int paramVar) {
// localVar 지역변수는 스택 프레임이 종료되면 함께 삭제된다.
int localVar = 1;
class LocalPrinter {
int value = 0;
public void printData() {
System.out.println("value is " + value);
// localVar, paramVar 스택프레임안에 지역 변수 호출
System.out.println("localVar is " + localVar);
System.out.println("paramVar is " + paramVar);
System.out.println("OutInstanceVar is " + OutInstanceVar);
}
}
LocalPrinter printer = new LocalPrinter();
return printer;
}
public static void main(String[] args) {
LocalOuter localOuter = new LocalOuter();
Printer printer = localOuter.process(2);
// printer.print() 실행하고
// process() 스택 프레임이 리턴으로 종료되어 스택프레임이 사라져 있다.
printer.print()
// 스택프레임 안에 있는 localVar, paramVar 지역변수가 사라졌는데, 호출되었다.
}
}
value is 0
localVar is 1
paramVar is 2
OutInstanceVar is 3
- process() 에는 Printer 타입을 먼저 반환한다.
- Printer 안에는 LocalPrinter 인스턴스를 반환한 것이다.
- LocalPrinter.print() 메소드는 process() 안에서 실행되는 것이 아닌 process() 메소드 종료 이후 main() 메소드에서 사라진 스택프레임 지역변수를 호출한 것을 볼 수 있다.
문제는 사라진 스택프레임에서 지역변수를 어떻게 호출되었는가? 라는 의문점이 생긴다.
다음 그림에서 지역 변수가 사라지는 과정을 보자.
지역 변수
- localVar = 1
- ParamVar = 2
코드에서 localOuter.process(2); 호출되어 리턴되어 스택프레임이 사라진다.
지역 변수가 어떻게 되는지 그림에서 살펴보자
Remove process() frame
+-----------------+
| process() frame |
| ParamVar=2 |
| localVar=1 |
| printer={Ref2} |
+--------+--------+
↑
| |
| process() frame | <- process(2) 호출되어 아래 프레임이 스택에서 제거됨
| ... |
| +--------------+ |
| | main() frame | |
| | args[] | |
| +--------------+ |
+------------------+
Stack Area
+--------------------------------+
| {Ref1} |
| +--------------------+ |
| | outInstanceVar = 3 | |
| +--------------------+ |
| LocalOuter Instance |
| |
| {Ref2} |
| +----------------------------+ |
| | localVar = 1 /* Capture */ | | <- capture process() Frame Variable
| | paramVar = 2 /* Capture */ | | <- capture process() Frame Variable
| | value = 0 | |
| | Nested... = {Ref1} | |
| | print() {..} | |
| +----------------------------+ |
| LocalPrinter Instance |
+--------------------------------+
Heap Area
- 스택에 있던 process() 프레임이 사라진다.
- process() 프레임에 있던 ParamVar, localVar 변수가 힙영역의 LocalPrinter 인스턴스로 캡쳐하여 복제된 모습을 볼 수 있다.
지역 클래스 변수 다루기 2 - 캡처
지역 변수의 생명주기는 매우 짦고, 인스턴스의 생명 주기는 길다는 것을 앞서 설명하였다.
내부 클래스에서 지역 클래스 프레임이 사라졌을 때 지역 변수는 어떻게 되는가.
프레임에 있던 지역 변수가 인스턴스로 캡쳐한다.
지역 변수 캡처
자바는 앞서 설명한 지역 변수가 삭제되는 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에서 필요한 지역 변수를 먼저 복사한다. 즉, 지역 변수를 복사해서 생성한 인스턴스와 함께 존재하게 되는 것이다. 이 기능이 캡처(Capture)다.
참고로 지역 클래스에 선언된 모든 지역변수가 아닌 접근이 필요한 지역 변수만 캡쳐한다.
지역 클래스 변수 다루기 3 - 값 변경
지역 변수 중간에 값을 변경할 수 없도록 막아야 한다.
즉, final 키워드로 선언이 필요하다. 자바는 알아서 지역변수를 감지하고 final 선언을 자동으로 해준다.
자바 문법에서는 effectively final 지정하라는 규칙이 있다.
LocalOuter.class
public class LocalOuter {
...
public void process(int paramVar) {
int localVar = 1;
localVar = 3; // 지역 변수 값 변경 시도
class LocalPrinter implements Printer{
...
@Override
public void print() {
System.out.println("value is " + value);
System.out.println("localVar is " + localVar); // effectively final
System.out.println("paramVar is " + paramVar);
System.out.println("OutInstanceVar is " + OutInstanceVar);
}
}
...
}
}
java: local variables referenced from an inner class must be final or effectively final
localVar 변경 시도하였더니 final, effectively final 선언하라고 오류로 표시해준다.
값을 못바꾸는 이유는?
스택 프레임이 사라질 때 지역 변수가 인스턴스로 캡쳐한다고 앞서 설명하였다.
만약 지역 변수가 변경되면 인스턴스로 캡쳐된 값도 함께 싱크를 맞춰야한다.
싱크와 관련된 모든 문제가 파생되고 새로운 설계가 필요하게 된다.
동기화에 필요한 설계
- 지역 변수의 값을 변경하면 인스턴스 캡처 변수도 변경 필요.
- 인스턴스 캡쳐 변수 변경하면 지역 변수 값도 변경 필요.
- 오류 발생시 예상이 어려움. 디버깅 추적 불가
- 지역 변수 값과 인스턴스 값 동기화 설계에 따른 단점이 따라옴
- 멀티 쓰레드의 동기화 어려움
- 성능 영향이 큼
동기화에 필요한 설계보다 차라리 값을 변경 못하게 하는 것이 복잡한 문제 발생하는 것을 차단할 수 있다.