https://surrealcode.tistory.com/62
내부 중첩 클래스 및 내부 클래스에 대한 설명은 위 링크를 참고하자.
이전에 배웠듯이 중첩 클래스의 종류는 크게 두가지로 분류하고 작게는 네가지가 있었다.
1. 정적 중첩 클래스
2. 내부 클래스 : 내부 클래스, 지역 클래스, 익명 클래스
그 중 지역 클래스(Local class)는 내부 클래스의 특별한 종류의 하나이다. 따라서 내부 클래스의 특징을 그대로 가진다. 예를 들어서 지역 클래스도 내부 클래스이므로 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.
지역 클래스는 지역 변수와 같이 코드 블럭 안에서 정의 된다.
public class LocalOuterV1 {
private int outInstanceVar = 3;
public void process(int paramVar){
int localVar = 1;
class LocalPrinter{
int value = 0;
public void printData(){
System.out.println("value = " + value);
System.out.println("localVar = " + localVar);
System.out.println("paramVar = " + paramVar);
System.out.println("LocalOuterV1.this.outInstanceVar = " + LocalOuterV1.this.outInstanceVar);
}
}
LocalPrinter localPrinter = new LocalPrinter();
localPrinter.printData();
}
public static void main(String[] args) {
LocalOuterV1 localOuterV1 = new LocalOuterV1();
localOuterV1.process(2);
}
}
value = 0
localVar = 1
paramVar = 2
LocalOuterV1.this.outInstanceVar = 3
지역 클래스의 접근 범위
1. 자신의 인스턴스 변수인 value에는 당연히 접근 가능하다.
2. 자신이 속한 코드 블럭의 지역 변수인 localVar에도 접근할 수 있다.
3. 자신이 속한 코드 블럭의 매개변수인 paramVar에 접근할 수 있다. 참고로 "매개변수도 지역 변수의 한 종류이다"
4. 바깥 클래스의 인스턴스 멤버인 outInstanceVar에 접근할 수 있다.(지역 클래스도 내부 클래스의 한 종류이다)
지역클래스는 지역 변수처럼 접근 제어자를 사용할 순 없다.
내부클래스를 포함한 중첩 클래스들도 일반 클래스처럼 인터페이스를 구현하거나 부모 클래스를 상속할 수 있다.
public interface Printer {
void print();
}
class LocalPrinter implements Printer
public void print() {
System.out.println("value = " + value);
System.out.println("localVar = " + localVar);
System.out.println("paramVar = " + paramVar);
System.out.println("LocalOuterV1.this.outInstanceVar = " + LocalOuterV2.this.outInstanceVar);
}
단순한 인터페이스를 구현해 보았다.
**지역클래스 - 지역 변수 캡처
지금부터의 설명은 깊이있게 이해하지 않아도 되지만, 지역 클래스가 접근하는 지역 변수의 값을 왜 변경하면 안되는지에 대한 이유에 관한 것이기에 알고 넘어가자.
우선 변수의 생명 주기를 다시 복습해보자.
클래스 변수 : 프로그램 종료까지로 가장 길다. static변수는 메서드 영역에 존대하고 자바가 클래스 정보를 읽어들이는 순간부터 프로그램 종료까지 존재한다.
인스턴스 변수 : 인스턴스의 생존 기간(힙 영역)으로 인스턴스 변수는 본인이 소속된 인스턴스가 GC되기 전까지 존재한다. 생존 주기가 긴 편이다.
지역 변수 : 메서드 호출이 끝나면 사라진다(스택 영역) 지역 변수는 스택 영역의 스택 프레임 안에 존재한다. 따라서 메서드가 호출되면 생성되고, 메서드 호출이 종료되면 스택 프레임이 제거되면서 그 안에 있는 지역 변수도 모두 제거된다.
public class LocalOuterV3 {
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("LocalOuterV1.this.outInstanceVar = " + LocalOuterV3.this.outInstanceVar);
}
}
LocalPrinter printer = new LocalPrinter();
//localPrinter.print(); 여기서 실행하지 않고 Printer 인스턴스만 반환
return printer;
}
public static void main(String[] args) {
LocalOuterV3 localOuter = new LocalOuterV3();
Printer printer = localOuter.process(2);
printer.print();
}
}
하지만 이 예제를 실행해보면 뭔가 이상하다.
localVar와 paramVar는 지역변수여서 메서드가 끝나면 사라지는 것이 당연할 터인데 printer.print()메서드로 호출이 되었다.
LocalPrinter 인스턴스는 print() 메서드를 통해 힙 영역에 존재하는 바깥 인스턴스의 변수인 outInstanceVar에 접근한다. 이 부분은 인스턴스의 필드를 참조하는 것이기 때문에 특별한 문제가 없다.
LocalPrinter 인스턴스는 print() 메서드를 통해 "스택 영역에 존재하는 지역 변수도 접근하는 것 처럼 보인다." 하지만 스택 영역에 존재하는 지역 변수를 힙 영역에 있는 인스턴스가 접근하는 것은 생각처럼 단순하지 않다.
지역 변수의 생명주기는 매우 짧다. 반면에 인스턴스의 생명주기는 GC 전까지 생존할 수 있다.
지역변수인 paramVar, localVar는 process() 메서드가 실행되는 동안에만 생존할 수 있다. process() 메서드가 종료되면 process()의 스택 프레임이 제거되면서 두 지역 변수도 함께 제거된다.
여기서 문제는 process()메서드가 종료되어도 LocalPrinter 인스턴스는 계속 생존할 수 있다는 점이다.
여기서는 process() 메서드가 종료된 이후 main()메서드 안에서 LocalPrinter.print()메서드를 호출한다.
LocalPrinter 인스턴스에 있는 print() 메서드는 지역 변수인 paramVar, localVar에 접근 해야한다. 하지만 process 메서드가 이미 종료되었으므로 해당 지역 변수 또한 제거된 상태이다.
근데 왜 localVar와 paramVar와 같은 지역 변수 값들이 정상적으로 출력이 되었을까?
지역 변수 캡쳐
자바는 이런 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둔다. 이런 과정을 변수 캡쳐(Capture)라고 한다.
인스턴스를 생성할 때 필요한 지역 변수를 복사해서 보관해 두는 것이다.
"필요한 지역 변수만 캡쳐한다"
1.LocalPrinter 인스턴스 생성 시도 : 지역 클래스의 인스턴스를 생성할 때 지역 클래스가 접근하는 지역 변수를 확인한다.(paramVar, localVar)
2. 사용하는 지역 변수 복사 : 지역 클래스가 사용하는 지역 변수를 복사한다.(매개변수도 지역 변수의 한 종류이다)
3. 지역 변수 복사 완료 : 복사한 지역 변수를 인스턴스에 포함한다.
4. 인스턴스 생성 완료 : 복사한 지역 변수를 포함해서 인스턴스 생성이 완료 된다. 복사한 지역 변수를 인스턴스를 통해 접근할 수 있다.
지역 클래스가 접근하는 지역 변수는 "절대로 중간에 값이 변하면 안된다."
따라서 final로 선언 하거나, 사실상 final이다. 이것은 자바 문법이고 규칙이다.
ex)paramVar, localVar
용어 - 사실상 final
영어로 effectively final이라 한다. 사실상 final 지역 변수는 지역 변수에 final 키워드를 사용하지는 않았지만, 값을 변경하지 않는 지역 변수를 뜻한다.
지역 클래스가 접근하는 지역 변수는 왜 final 또는 사실상 final이어야 할까? 왜 중간에 값이 변하면 안될까?
Printer printer = new LocalPrinter()
LocalPrinter를 생성하는 시점에 지역 변수인 localVar, paramVar를 캡처한다.
그런데 이후 캡처한 지역 변수의 값을 다음과 같이 변경하면 어떻게 될까?
Printer printer = new LocalPrinter()
localVar = 10;
paramVar = 20;
이렇게 되면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 변수의 값이 서로 달라지는 문제가 발생한다. 이것을 동기화 문제라 한다.
물론 자바 언어를 설계할 때 지역 변수의 값이 변경되면 인스턴스에 캡처한 변수의 값도 함께 변경하도록 설계하면 된다. 하지만 이로 인해 수많은 문제들이 파생될 수 있다.
1. 지역 변수의 값을 변경하면 인스턴스에 캡처한 변수의 값도 변경해야한다.
2. 예상치 못한 곳에서 값이 변경되면 사이드 이펙트가 발생할 수 있고 디버깅을 어렵게 한다.
3. 멀티 쓰레드 상황에서는 이런 동기화가 매우 어렵고, 성능에 나쁜 영향을 줄 수 있다.
이 모든 문제는 캡처한 지역 변수의 값이 변하기 때문에 발생하므로 캡처한 지역 변수의 값을 변하지 못하게 막아서 이런 복잡한 문제를 근본적으로 차단한다.
'공부 > Java' 카테고리의 다른 글
자바 예외처리 1 (0) | 2024.09.15 |
---|---|
자바 익명 클래스 (1) | 2024.09.14 |
자바 중첩 클래스, 내부 클래스 (1) | 2024.09.13 |
자바 날짜와 시간 (1) | 2024.09.12 |
자바 열거형-ENUM (3) | 2024.09.11 |