JAVA의 가장 기본이 되는 내용

지역 클래스

cwchoiit 2024. 4. 21. 12:28
728x90
반응형
SMALL

참고자료:

 

김영한의 실전 자바 - 중급 1편 | 김영한 - 인프런

김영한 | 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을

www.inflearn.com

 

 

지역 클래스는 내부 클래스의 특별한 종류 중 하나이다. 그렇기에 내부 클래스의 특징을 그대로 가진다.

예를 들어, 지역 클래스도 내부 클래스이므로 바깥 클래스의 인스턴스 멤버에 접근할 수 있다.

 

지역 클래스는 지역 변수와 같이 코드 블럭 안에서 정의된다.

지역 클래스 예시

class Outer {
    
    public void process() { 
        int localVar = 0; //지역 변수
        
        class Local {...} //지역 클래스
        
        Local local = new Local();
    }
}

 

 

그래서 지역 클래스가 접근 가능한 것에는 다음과 같은 것들이 있다.

  • 지역 변수
  • 바깥 클래스의 인스턴스 변수
  • 바깥 클래스의 클래스 변수 (사실 이건 당연한 것. 클래스 변수는 어디서나 접근이 가능하기 때문에. 근데! private으로 선언해도 접근이 가능)

 

지역 클래스를 직접 만들어 보면 다음과 같다.

package nested.local;

public class OuterClass {

    private int outerInstanceVar = 10;
    private static int outerClassVar = 100;

    public void process(int paramVar) {
        int localVal = 2;

        class LocalClazz {
            private int localInstanceVal = 5;

            public void localClazzMethod() {
                System.out.println("localInstanceVal = " + localInstanceVal);
                System.out.println("localVal = " + localVal);
                System.out.println("paramVar = " + paramVar);
                System.out.println("outerInstanceVar = " + outerInstanceVar);
                System.out.println("outerClassVar = " + outerClassVar);
            }
        }

        LocalClazz localClazz = new LocalClazz();
        localClazz.localClazzMethod();
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        outerClass.process(1);
    }
}
  • 지역 클래스는 선언한 블록 안에서만 접근이 가능하다. 
    • 그렇기 때문에 process() 메서드 안에서 지역 클래스를 생성하고 지역 클래스의 인스턴스로부터 메서드를 호출할 수 있다.
  • 지역 클래스는 접근 제어자는 붙일 수 없다.
    • 지역 변수에 접근 제어자를 못 붙이는거랑 같은 맥락으로 생각하면 된다.

 

모든 중첩 클래스들은 일반 클래스처럼 인터페이스를 구현하거나 부모 클래스를 상속할 수 있다.

여기서 모든 중첩 클래스는(정적 중첩 클래스, 내부 클래스, 지역 클래스, 익명 클래스)를 말한다.

 

LocalClazzInterface

package nested.local;

public interface LocalClazzInterface {
    void print();
}

인터페이스를 상속받는 지역 클래스 예시

package nested.local;

public class OuterClass {

    ...

    public void process(int paramVar) {
        int localVal = 2;

        class LocalClazz implements LocalClazzInterface {
            private int localInstanceVal = 5;

            public void localClazzMethod() {
                System.out.println("localInstanceVal = " + localInstanceVal);
                System.out.println("localVal = " + localVal);
                System.out.println("paramVar = " + paramVar);
                System.out.println("outerInstanceVar = " + outerInstanceVar);
                System.out.println("outerClassVar = " + outerClassVar);
            }

            @Override
            public void print() {
                System.out.println("Hi");
            }
        }
		...
    }

    ...
}

 

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

지역 변수 캡처라는 내용을 이해하기 전 변수의 생명 주기에 대해서 먼저 짚고 넘어가보자.

변수는 크게 세가지가 있다.

  • 클래스 변수
  • 인스턴스 변수
  • 지역 변수

변수의 생명 주기

  • 클래스 변수: 생명 주기가 가장 길다. 왜냐하면 이 클래스 변수는 프로그램 종료까지 살아 있다. 메모리 구조 내 메서드 영역에 있는 변수이기 때문에 이 프로그램에 종료되기까지 살아 있게 된다. 
  • 인스턴스 변수: 생명 주기는 인스턴스가 살아 있는 동안까지이다. 인스턴스는 힙 영역에 자리하고 있다. 인스턴스 변수는 본인이 소속된 인스턴스가 GC되기 전까지 존재한다. 생존 주기가 긴 편이다.
  • 지역 변수: 메서드 호출이 끝나면 사라진다. 지역 변수는 스택 영역에서 스택 프레임 안에 존재한다. 따라서 메서드가 호출될 때 생성되고 메서드 호출이 종료되면 스택 프레임이 제거되면서 그 안에 있는 지역 변수도 모두 제거된다. 생존 주기가 제일 짧다. 매개변수도 지역변수다. 

이 내용을 머리속에 넣은 상태에서 다음 코드를 보자.

package nested.local;

public class OuterClass {

    private int outerInstanceVar = 10;
    private static int outerClassVar = 100;

    public LocalClazzInterface process(int paramVar) {
        int localVal = 2;

        class LocalClazz implements LocalClazzInterface {
            private int localInstanceVal = 5;

            @Override
            public void print() {
                System.out.println("localInstanceVal = " + localInstanceVal);
                System.out.println("localVal = " + localVal);
                System.out.println("paramVar = " + paramVar);
                System.out.println("outerInstanceVar = " + outerInstanceVar);
                System.out.println("outerClassVar = " + outerClassVar);
            }
        }

        return new LocalClazz();
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        LocalClazzInterface localClazz = outerClass.process(1);

        localClazz.print();
    }
}

 

이전에 사용한 코드를 조금 수정한 내용이다.

우선 process() 메서드는 LocalClazzInterface 타입을 반환한다. 그래서 LocalClazzInterface를 구현한 구현체인 LocalClazzprocess() 메서드가 반환한다. 

 

main() 메서드에서 반환받은 LocalClazzInterface 타입의 변수 localClazzprint() 메서드를 실행한다.

실행 결과는 다음과 같다.

localInstanceVal = 5
localVal = 2
paramVar = 1
outerInstanceVar = 10
outerClassVar = 100

 

결론은 잘 찍힌다. 근데 의아한 부분이 하나 있다. 우선 하나씩 생각해보자.

내부 클래스로 만든 인스턴스도 인스턴스다. 그렇기 때문에 힙 영역에 존재하게 된다. 그리고 내부 클래스를 생성하면 내부 클래스는 바깥 클래스를 참조하고 있다. 

 

그럼 main() 메서드에서 process() 메서드를 호출해서 받은 내부 클래스의 인스턴스는 main() 메서드가 종료될 때 까지 살아 있는게 당연하다. 어디선가(main()) 이 인스턴스를 참조하고 있기 떄문에 GC에 해당하지 않는다. 그 말은 내부 클래스가 살아있으면 바깥 클래스도 여전히 살아 있게 된다는 말이다(내부 클래스가 바깥 클래스의 참조값을 가지고 있으니까).

 

그럼 내부 클래스의 변수인 localInstanceVal, 바깥 클래스의 인스턴스 변수인 outerInstanceVar, 바깥 클래스의 클래스 변수인 outerClassVar는 저 시점에 접근이 가능한게 맞다. 그러니까 아직 저 변수 세 개는 살아 있는게 맞다.

 

근데? process() 메서드의 지역 변수(매개변수 포함)인 localVal, paramVar는? 얘네는 process()가 호출될 때 스택 영역에 스택 프레임에 쌓이는 순간 생성되고 process() 메서드가 종료될 때 스택 프레임에서 제거되므로 저 변수 두 개도 삭제가 된다.

 

변수 두 개가 삭제가 되는데 여전히 실행을 하면 저 두개의 값이 잘 찍히고 있다. 어떻게 이게 가능할까?

여기서 나오는 해결책이 바로 지역 변수 캡처이다.

 

사실, 저렇게 실행결과를 찍을 때 지역 변수를 찾아서 가져오는 게 아니다. 지역 클래스에서 필요한 지역 변수가 있을 때 그 값을 지역 클래스의 필드로 몰래 만들어 낸다.

참고로, 모든 지역 변수를 캡처하는 게 아니다. 접근이 필요한 지역 변수만 캡쳐한다.

 

그러니까 코드로 보면 다음과 같다.

package nested.local;

public class OuterClass {

	...
    
    public LocalClazzInterface process(int paramVar) {
        int localVal = 2;

        class LocalClazz implements LocalClazzInterface {
            private int localInstanceVal = 5;

            // 이렇게 클래스 내부에 필드로 선언을 몰래 해버린다.
            int localVal = 2;
            int paramVar = ...;
        }

        return new LocalClazz();
    }
	...
}

 

접근이 필요한 지역 변수 localVal, paramVar를 클래스 내부에 필드로 몰래 선언을 한다. 이렇게 해놓으니까 process() 메서드가 종료되면서 날라가는 스택 프레임에 같이 있던 지역 변수가 삭제되더라도 지역 클래스에서 저 변수를 출력해낼 수 있는 것.

 

실제로 그런지 코드로도 확인해보자. 어떻게 확인할 수 있냐면 getClass() 메서드를 통해 가지고 있는 필드를 조회해보자.

public static void main(String[] args) {
    OuterClass outerClass = new OuterClass();
    LocalClazzInterface localClazz = outerClass.process(1);

    localClazz.print();

    Field[] declaredFields = localClazz.getClass().getDeclaredFields();
    for (Field declaredField : declaredFields) {
        System.out.println("declaredField = " + declaredField);
    }
}

실행 결과:

localInstanceVal = 5
localVal = 2
paramVar = 1
outerInstanceVar = 10
outerClassVar = 100
declaredField = private int nested.local.OuterClass$1LocalClazz.localInstanceVal
declaredField = final int nested.local.OuterClass$1LocalClazz.val$localVal
declaredField = final int nested.local.OuterClass$1LocalClazz.val$paramVar
declaredField = final nested.local.OuterClass nested.local.OuterClass$1LocalClazz.this$0

 

지역 클래스가 가지고 있는 localInstanceVal 외에 localVal, paramVar가 보인다. 정말로 우리 몰래 필드를 클래스에 넣어버렸다.

그리고 또 하나, 마지막에 출력된 이거.

declaredField = final nested.local.OuterClass nested.local.OuterClass$1LocalClazz.this$0

이게 바로 바깥 클래스의 참조값이다. 이렇게 지역 클래스는 바깥 클래스의 참조값도 보관하고 있음을 알 수 있다. 

 

결론은,

이러한 지역 변수 캡쳐를 통해서 스택 영역에 쌓인 스택 프레임이 삭제되면서 지역 변수가 같이 삭제되더라도 힙 영역에 존재하는 지역 클래스는 문제 없이 지역 변수에 접근이 가능하다. 사실 지역 변수에 접근이 가능하단 말도 정확한게 아니고 본인의 캡쳐된 변수에 접근한다.

 

근데.. 결론일까? 그럼 여기서 의문이 하나 더 생긴다.

그럼 지역 변수를 캡처해서 가지고 있는것 까지 알았는데 이 지역 변수의 값을 바꾸면 어떻게 되는거지?!

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

이건 자바 규칙이고 문법이다. 그래서 final로 선언하거나, 사실상 final이어야 한다.

사실상 finaleffectively final을 번역한 것이고 final로 선언하진 않았지만 선언 이후 값을 변경하지 않는 지역 변수를 의미한다.

 

그럼 왜 final 또는 사실상 final이어야 하고? 왜 중간에 값이 변하면 안될까?

지역 클래스가 접근하는 지역 변수는 지역 변수 캡쳐를 하기 때문이다.

 

중간에 변경하려고 하면 다음과 같이 컴파일 에러가 발생한다.

 

지역 클래스가 사용하는 지역 변수는 지역 변수 캡처가 일어나는데 중간에 지역 변수 값을 바꿔버리면 동기화 문제가 발생한다. 그런 이유로 인해 자바에서 아예 지역 변수를 변경하는 것 자체를 막아둔 것.

 

그래서 진짜 결론은 다음과 같다.

 

결론

지역 클래스는 접근하는 지역 변수를 지역 변수 캡처를 통해 가지고 있게 된다. 그래서 스택 영역에 쌓인 스택 프레임이 삭제되면서 같이 삭제되는 지역 변수에 접근하지 않아도 지역 변수 캡처값을 가지고 접근이 가능해진다. 그리고 이 지역 변수 캡처를 하기 때문에 지역 클래스로부터 접근이 되는 지역 변수는 중간에 값이 변경될 수 없다. 

 

728x90
반응형
LIST

'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글

try-with-resources  (0) 2024.04.23
익명 클래스  (0) 2024.04.21
중첩 클래스(정적 중첩 클래스, 내부 클래스)  (0) 2024.04.14
Stream API  (2) 2024.04.07
날짜와 시간  (0) 2024.04.04