공부/Java

자바 지역클래스 복습하기

Stair 2024. 9. 21. 15:45
반응형

지역클래스(Local class)는 내부 클래스의 특별한 종류의 하나이다. 따라서 내부 클래스의 특징을 그대로 가진다. 지역클래스도 내부 클래스이므로 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.

 

지역 클래스 특징

1. 지역 클래스는 지역 변수처럼 코드 블럭(메서드) 안에 클래스를 선언한다.

2. 지역 클래스는 지역 변수에 접근할 수 있다.

 

다음과 같은 코드를 보자

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 printer = new LocalPrinter();
        printer.printData();
    }

    public static void main(String[] args) {
        LocalOuterV1 localOuter = new LocalOuterV1();
        localOuter.process(2);
    }
}

 

지역 클래스도 내부 클래스이기에 바깥 클래스 인스턴스에 소속이 된다.

코드블럭 안에서만 쓸 수 있는 클래스이기 때문에 메서드가 끝나기 전 생성해서 사용해야한다.

 

접근 범위

1. 자신의 인스턴스 변수인 value에 접근 가능하다.

2. 자신이 속한 코드 블랙의 지역변수인  localVar에 접근할 수 있다.

3. 자신이 속한 코드 블럭의 매개변수인  paramVar에 접근할 수 있다. 참고로 매개변수도 지역 변수의 한 종류이다.

4. 바깥 클래스의 인스턴스 멤버인 outInstanceVar에 접근할 수 있다.(지역 클래스도 내부 클래스의 한 종류이다)

 

지역 클래스는 지역 변수처럼 접근 제어자를 사용할 수 없다.

 

 

 

 

지역 클래스 - 지역 변수 캡처

지역 변수 캡처를 알아보기 전 잠깐 변수의 생명주기를 확인해보자

 

클래스 변수 : 프로그램 종료까지, 가장 길다(메서드 영역) : 클래스 변수는 메서드 영역에 존재하고, 자바가 클래스 정보를 읽어들이는 순간부터 종료까지 존재한다.

인스턴스 변수 : 인스턴스의 생존 기간(힙영역) : 인스턴스 변수는 본인이 소속된 인스턴스가  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("outInstanceVar = " + outInstanceVar);
            }
        }

        LocalPrinter printer = new LocalPrinter();
//        printer.print();를 여기서 실행하지 않고 Printer 인스턴스만 반환한다.

        return printer;
    }

    public static void main(String[] args) {
        LocalOuterV3 localOuter = new LocalOuterV3();
        Printer printer = localOuter.process(2);
        //printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후 실행
        printer.print();
    }
}

process()는 Printer타입을 반환한다. 여기서는 LocalPrinter 인스턴스를 반환한다.

LocalPrinter.print() 메서드를 process()안에서 실행하는 것이 아니라  process()메서드가 종료된 이후 main()에서 실행한다.

LocalPrinter인스턴스는 print9)메서드를 통해 힙 영역에 존재하는 바깥 인스턴스의 변수인 outInstanceVar에 접근한다. 이 부분은 필드를 참조하는 것이기에 특별한 문제가 없다.

하지만 지역 변수인 paramVar와 localVar의 경우는 좀 다른데, 스택 프레임에 있는 이 변수들은 스택프레임에서 없어진 이 지역변수들은 어떻게 되는걸까?

 

 

이 때문에 지역변수 캡처가 존재한다.

앞서 본 것 처럼 지역 변수의 생명주기는 짧고, 지역 클래스를 통해 생성한 인스턴스의 생명주기는 길다.

지역 클래스를 통해 생성한 인스턴스가 지역 변수에 접근해야 하는데, 둘의 생명주기가 다르기 때문에 인스턴스는 살아있지만, 지역 변수는 이미 제거된 상태일 수 있다.

 

이런 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둔다. 이런 과정을 변수 캡처(Capture)라 한다.

모든 지역 변수를 캡처하는 것은 아니고 접근이 필요한 지역 변수만 캡처한다.

 

이 복사한 지역 변수를 인스턴스에 포함해서 집어넣고, 복사한 지역 변수를 포함해서 인스턴스 생성이 완료된다.

 

LocalPrinter 인스턴스에서 print()메서드를 통해 paramVar, localVar를 접근하는 것처럼 보이지만 사실은 LocalPrinter인스턴스에 있는 paramVar와 localVar에 접근하는 것이다.

 

public static void main(String[] args) {
    LocalOuterV3 localOuter = new LocalOuterV3();
    Printer printer = localOuter.process(2);
    //printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후 실행
    printer.print();


    //추가
    System.out.println("필드 확인");
    Field[] declaredFields = printer.getClass().getDeclaredFields();
    for (Field declaredField : declaredFields) {
        System.out.println("declaredField = " + declaredField);
    }
}

필드 확인
declaredField = int nested.local.LocalOuterV3$1LocalPrinter.value
declaredField = final int nested.local.LocalOuterV3$1LocalPrinter.val$localVar
declaredField = final int nested.local.LocalOuterV3$1LocalPrinter.val$paramVar
declaredField = final nested.local.LocalOuterV3 nested.local.LocalOuterV3$1LocalPrinter.this$0

 

필드를 확인해 보면 localVar와 paramVar이 있는걸 볼 수 있다.(캡처를 해서 만들어 넣었다)

 

 

declaredField = final nested.local.LocalOuterV3 nested.local.LocalOuterV3$1LocalPrinter.this$0

또한 바깥 클래스를 참조하기 위한 필드도 확인할 수 있다.(x001같은거)

 

 

정리

지역 클래스는 인스턴스를 생성할 때 필요한 지역 변수를 먼저 캡처해서 인스턴스에 보관한다. 그리고 지역 클래스의 인스턴스를 통해 지역 변수에 접근하면, 실제로는 지역 변수에 접근하는 것이 아니라 인스턴스에 있는 캡처한 캡처변수에 접근한다.

 

 

 

지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안된다.

따라서 final로 선언하거나 사실상 final 이다.

 

용어 - 사실상 final

영어로 effectively final이라 한다 사실상 final 키워드를 넣지 않았을 뿐이지, 실제로는  final 키워드를 넣은 것 처럼 중간에 값을 변경하지 않는 지역 변수이다. 지역 클래스가 접근하는 지역 변수는 왜 final 또는 사실상 final이어야 할까?

    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);
            }
        }

        LocalPrinter printer = new LocalPrinter();
//        printer.print();를 여기서 실행하지 않고 Printer 인스턴스만 반환한다.

        //만약 localVar, paramVar의 값을 변경한다면? 다시 캡처해야 하나?
        localVar = 10;
        paramVar = 20;

        return printer;
    }

위는 컴파일 오류가 난다.

 

이렇게 되면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 캡처 변수의 값이 서로 달라지는 문제가 발생한다. 이것을 동기화 문제라 한다.

 

자바 언어를 설계할 때 지역 변수의 값이 변경되면 인스턴스에 캡처한 변수의 값도 함께 변경하도록 설계할 수 있으니 이로 인해 수 많은 문제들이 파생될 수 있다.

 

1. 지역 변수의 값이 변경되면 인스턴스의 값도 변경되어야 한다.

2. 반대로 인스턴스의 값이 변경되면 지역 변수의 값도 다시 변경해야 한다.

3. 개발자 입장에서 보면 예상하지 못한 곳에서 값이 변경될 수 있다. 이는 디버깅을 어렵게 한다.

4. 지역 변수의 값 과 인스턴스에 있는 캡처 변수의 값을 서로 동기화 해야 하는데, 멀티쓰레드 상황에서 이런 동기화는 매우 어렵고, 성능에 나쁜 영향을 줄 수 있다.

 

그렇기 때문에 언어를 설계할 때 지역 변수의 값을 변하지 못하게 막아서 이런 복잡한 문제들을 근본적으로 차단한다.

 

 

 

다음은 내부 클래스(내부클래스, 지역클래스, 익명클래스)중 마지막인

익명 클래스이다.

 

익명클래스는 지역 클래스의 특별한 종류 중 하나이다.

익명 클래스는 지역 클래스인데, 클래스의 이름이 없다는 특징이 있다.

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);
            }
        }

        LocalPrinter printer = new LocalPrinter();
        printer.print();
    }

    public static void main(String[] args) {
        LocalOuterV2 localOuter = new LocalOuterV2();
        localOuter.process(2);
    }
}

이전 코드를 보자.

1. 선언 : 지역 클래스를 LocalPrinter라는 이름으로 선언하고, 이때  Printer 인터페이스도 함계 구현한다.

2. 생성 : new LocalPrinter()를 사용해서 앞서 선언한 지역 클래스의 인스턴스를 생성한다.

 

 

하지만 익명클래스를 사용하면 클래스의 이름을 생략하고, 클래스의 선언과 생성을 한번에 처리할 수 있다.

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);
    }
}

LocalPrinter printer = new LocalPrinter();

클래스를 선언하고, 인터페이스를 함께 구현하였고, 지역클래스의 인스턴스를 생성하는 코드를

다음과 같이 인터페이스를 구현과 동시에 생성을 한번에 할 수 있게 하는 것이 익명클래스이다.

Printer printer = new 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);
    }
};

 

new Printer() { };를 잘 보자

익명 클래스는 클래스의 본문을 정의하면서 동시에 생성한다.

new 다음에 바로 상속 받으면서 구현 할 부모 타입을 입력하면 된다.

이 코드는 마치 인터페이스  Printer를 생성하는 것 처럼 보이지만 자바에서 인터페이스를 생성하는 것은 불가능하다. 이 코드는 인터페이스를 생성하는 것이 아니고  Printer라는 이름의 인터페이스를 구현한 익명 클래스를 생성하는 것이다.

 

 

1.익명 클래스는 이름 없는 지역 클래스를 선언하며 동시에 생성한다.

2. 익명 클래스는 부모 클래스를 상속 받거나, 인터페이스를 구현해야 한다.

3. 익명 클래스는 말 그대로 이름이 없다. 이름을 가지지 않으므로, 생성자를 가질 수 없다.

4. 익명 클래스는 자바 내부에서 바깥 클래스 이름 + $ + 숫자 로 정의된다.

 

 

익명클래스를 사용해야 하는 이유 : 익명 클래스를 사용하면 클래스를 별도로 정의하지 않고도 인터페이스나 추상 클래스를 즉석에서 구현할 수 있어 코드가 더 간결해진다. 하지만, 복잡하거나 재사용을 해야 할 때는 지역 클래스를 사용해야 한다.

 

익명클래스가 어떻게 활용되는지는 다음 코드를 통해 확인해보자

public class Ex1RefMainV2 {
    
    public static void hello(Process process){
        System.out.println("프로그램 시작");

        process.run();

        System.out.println("프로그램 종료");

    }


    public static void main(String[] args) {


        hello(new Process() {
            @Override
            public void run() {
                int randomValue = new Random().nextInt(6) + 1;
                System.out.println("randomValue = " + randomValue);
            }
        });
        
        System.out.println();
        
        hello(new Process(){
            @Override
            public void run() {
                for(int i = 0; i < 3; i++){
                    System.out.println("i = " + i);
                }
            }
        });


    }



}

 

지금처럼 코드 조각을 전달하기 위해 클래스를 정의하고 메서드를 만들고 또 인스턴스를 생성해서 전달해야할까? 생각해보면 클래스나 인스턴스와 관계 없이 메서드만 전달할 수 있다면 얼마나 좋을까.

public class Ex1RefMainV2 {
    
    public static void hello(Process process){
        System.out.println("프로그램 시작");

        process.run();

        System.out.println("프로그램 종료");

    }


    public static void main(String[] args) {


        hello(() -> {
            int randomValue = new Random().nextInt(6) + 1;
            System.out.println("randomValue = " + randomValue);
        });
        
        System.out.println();
        
        hello(() -> {
            for(int i = 0; i < 3; i++){
                System.out.println("i = " + i);
            }
        });


    }



}

다음과 같이 자바8 이후에서 자바는 lamda를 제공하고 있다.

코드를 보면 클래스나 인스턴스를 정의하지 않고, 메서드(더 정확히는 함수)의 코드 블럭을 직접 전달하는 것을 확인할 수 있다.

반응형