[JAVA]지역 클래스(Local Class)
해당 게시글의 내용은'김영한의 실전 자바 - 중급편'을 기반으로 작성되었으며, 제가 학습한 내용을 토대로 다시 작성하게 되었습니다.
지역 클래스
- 내부 클래스의 종류 중 하나로, 내부 클래스의 특징을 그대로 갖습니다. 따라서, 바깥 클래스의 인스턴스 멤버에 접근할 수 있습니다.
class Outer {
public void process() {
//지역 변수
int localVar = 0;
//지역 클래스
class Local {...}
Local local = new Local();
}
}
- 위 코드 처럼 바깥 클래스의 메서드 안에 정의한 클래스를 지역 클래스라 합니다.
특징
- 지역 변수처럼 코드 블럭 안에 클래스를 선언합니다.
- 지역 클래스는 지역 변수에 접근할 수 있습니다.
package chapter08.local;
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("outInstanceVar = "+ 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
outInstanceVar = 3
*/
지역변수의 접근 범위
- 자신의 인스턴스 변수인 value에 접근 가능합니다.
- 자신이 속한 코드 블럭인 localVar에 접근 가능합니다.
- 자신이 속한 코드 블럭의 매개변수인 paramVar에도 접근이 가능합니다.
- 바깥 클래스의 인스턴스 멤버인 outInstanceVar에도 접근이 가능합니다.
구현 및 상속 가능
내부 클래스를 포함한 중첩 클래스들도 일반 클래스처럼 인터페이스를 구현하거나, 부모 클래스를 상속할 수 있습니다.
public interface Printer {
void print();
}
package chapter08.local;
public class LocalOuterV2 {
private int outInstanceVar = 3;
public void 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);
}
}
Printer printer = new LocalPrinter();
printer.print();
}
public static void main(String[] args) {
LocalOuterV2 localOuterV2 = new LocalOuterV2();
localOuterV2.process(2);
}
}
/*
[실행 결과]
value=0
localVar=1
paramVar=2
outInstanceVar=3
*/
지역 변수 캡처
변수의 생명주기
변수는 크게 클래스, 인스턴스, 지역 변수 3가지가 존재합니다.
- 클래스 변수: 클래스 변수(static 변수)는 메서드 영역에 존재하고, 자바가 클래스 정보를 읽어들이는 순간부터 프로그램 종료까지 존재합니다.(생명 주기가 가장 깁니다.)
- 인스턴스 변수: 인스턴스 변수는 본인이 소속된 인스턴스가 GC 되기 전까지 존재합니다.(생명 주기가 긴 편)
- 지역 변수: 메서드 호출 시 생성되고, 호출이 끝나면 스택 프레임에서 제거 되면서 그 안에 있는 지역 변수도 모두 제거됩니다.(매개변수도 지역 변수의 한 종류입니다.)
package chapter08.local;
import java.io.File;
import java.lang.reflect.Field;
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("outInstanceVar = "+outInstanceVar);
}
}
Printer printer = new LocalPrinter();
//printer.print();
return printer;
}
public static void main(String[] args) {
LocalOuterV3 localOuterV3 = new LocalOuterV3();
Printer printer = localOuterV3.process(2);
//Printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후
printer.print();
}
}
/*
[실행 결과]
value=0
localVar=1
paramVar=2
outInstanceVar=3
*/
- process()는 Printer 인스턴스를 반환하는데, 여기서 Printer 인터페이스를 구현한 LocalPrinter 인스턴스를 반환할 수 있습니다.
- LocalPrinter.print() 메서드를 process() 안에서 실행하는 것이 아니라 process() 메서드가 종료된 이후에 main() 메서드에서 실행됩니다.
- 지역 클래스 객체(LocalPinter의 객체) 역시, 인스턴스이기 때문에 힙 영역에 존재합니다. 따라서 GC 전까지 존재하게 됩니다.
- LocalPrinter 인스턴스는 process()에서 생성되고 반환합니다. printer 변수에 참조를 보관하게 됩니다. 따라서 LocalPrinter 인스턴스는 main()이 종료될 때 까지 생존합니다.
- paraVar, localVar는 지역 변수이기 때문에 process()가 종료되면 스택 영역에서 제거되며 사라집니다.
main()에서 LocalPrinter 인스턴스를 반환받아 printer.print()를 통해 힙 영역에 존재하는 바깥 인스턴스 변수인 outInstanceVar에 접근합니다. 이는 인스턴스의 필드를 참조하는 것이기 때문에 특별한 문제가 없습니다.
또한, printer.print()를 통해 지역 변수에도 접근하는 것으로 보입니다. 사실은 그렇지 않습니다.
사라진 지역 변수
지역 변수의 생명 주기는 메소드 호출 ~ 종료 시점까지 입니다.
따라서, 이론 상 process()가 종료되는 시점에 지역 변수인 localVar와 paramVar가 사라지게 됩니다.
따라서, process()가 종료되었기 때문에 해당 지역 변수들도 이미 제거된 상태입니다.
하지만, 실행 결과를 보면 지역 변수(paramVar, localVar)들의 값들이 모두 정상적으로 출력되고 있습니다.
지역변수 캡처
위와 같이 지역 변수들이 정상적으로 출력될 수 있는 이유는 지역 변수 캡처 때문입니다.
자바는 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사하여 생성한 인스턴스에 함께 넣어둡니다. 이를 변수 캡처라 합니다.
모든 지역 변수를 캡처하는 것은 아니고, 접근이 필요한 지역 변수만 캡처합니다.
[지역 변수 캡처 과정]
- LocalPrinter 인스턴스 생성 시도:지역 클래스 인스턴스를 생성 시 지역 클래스가 접근하는 지역 변수를 확인합니다.
- 사용하는 지역 변수 복사
- 지역 변수 복사 완료: 복사한 지역 변수를 인스턴스에 포함합니다.
- 인스턴스 생성 완료: 복사한 지역 변수를 포함하여 인스턴스 생성이 완료됩니다.
- printer.print()를 통해 지역 변수(localVar, paramVar)에 접근하는 것은 인스턴스에 있는 캡처한 변수에 접근하는 것입니다.
- 캡처한 지역 변수의 생명 주기는 LocalPrinter 인스턴스(=printer)의 생명 주기와 같습니다.
지역 변수는 절대로 값이 변해선 안됩니다. 따라서, final로 선언하거나 또는 사실상 final이어야 합니다.
사실상 final
사실상 final 지역 변수는 지역 변수에 final 키워드를 사용하진 않았지만, 값을 변경하지 않는 지역 변수를 뜻합니다.
final 키워드만 넣지 않았을 뿐이지, 실제로는 final 키워드를 넣은 것 처럼 중간에 값을 변경하지 않는 지역 변수입니다.
따라서, 사실상 final 지역 변수는 final 키워드를 넣어도 동일하게 작동해야 합니다.
public class LocalOuterV4 {
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);
}
}
Printer printer = new LocalPrinter();
// 만약 localVar의 값을 변경한다면? 다시 캡처해야 하나??
// localVar = 10; // 컴파일 오류
// paramVar = 20; // 컴파일 오류
return printer;
}
public static void main(String[] args) {
LocalOuterV4 localOuter = new LocalOuterV4();
Printer printer = localOuter.process(2);
printer.print();
}
}
/*
[실행 결과]
value=0
localVar=1
paramVar=2
outInstanceVar=3
*/
Printer printer = new LocalPrinter();
LocalPrinter를 생성하는 시점에 지역 변수인 localVar, paramVar를 캡처합니다.
그런데 이후에 캡처한 지역변수의 값을 다음과 같이 변경하면 어떻게 될까요?
Printer printer = new LocalPrinter();
// 만약 localVar의 값을 변경한다면? 다시 캡처해야 하나??
localVar = 10; // 컴파일 오류
paramVar = 20; // 컴파일 오류
이렇게 되면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 캡처 변수의 값이 서로 달라지는 문제가 발생합니다. 이를 동기화 문제라 합니다.
물론 자바 언어를 설계할 때 지역 변수의 값이 변경되면 인스턴스에 캡처한 변수의 값도 함께 변경하도록 설계하면 됩니다. 하지만 이로 인해 수 많은 문제들이 파생될 수 있습니다.
[캡처 변수의 값을 변경하지 못하는 이유]
- 지역 변수의 값을 변경하면 인스턴스에 캡처해놓은 변수의 값도 변경해야 합니다.
- 반대로 인스턴스에 있는 캡처 변수의 값을 변경하면 해당 지역 변수의 값도 다시 변경해야 합니다.
- 개발자 입장에서 보면 예상하지 못한 곳에서 값이 변경될 수 있습니다. 이는 디버깅을 어렵게 합니다.
- 지역 변수의 값과 인스턴스에 있는 캡처 변수의 값을 서로 동기화 해야 하는데, 멀티 쓰레드 상황에서 이런 동기화는 매우 어렵고, 성능에 나쁜 영향을 줄 수 있습니다.
이 모든 문제는 챕처한 지역 변수의 값이 변하기 때문에 발생합니다. 자바는 캡처한 지역 변수의 값을 변하지 못하게 막아서 이런 복잡한 문제들을 근본적으로 차단합니다.
관련 포스팅