[자바] Anonymouns class는 inner class일까 static class일까

프로그래밍/자바2021. 4. 25. 12:52

익명 클래스(Anonymouns class) 문법은 아주 흔하게 사용하게 되는 자바의 문법이 아닌가 싶습니다.

람다를 쓰면서도 이 익명 클래스가 사용되는 듯 한데, 호기심이 생긴 김에 간단하게 조사좀 해보려고 합니다.

 

1. Inner 클래스와 Static 클래스

자바에서 중첩(nested) 클래스는 2가지 종류가 유명합니다.

(전체는 총 4개)

Inner 클래스와 Static 클래스로 불리는데 자바를 좀 써본 사람들이라면 이 차이를 알 것입니다.

 

Inner 클래스는 Outer 클래스에 대한 참조를 가지고 있어 Outer 클래스의 멤버에 바로 접근할 수 있습니다.

그러나 Static 클래스는 기본적으로는 Outer 클래스의 멤버에 접근할 수 없고 생성자로 Outer 클래스의 객체를 받든지 해야합니다.

class OuterClass {

    int outerInt = 5;

    class InnerClass {

        public void testFunc() {
            System.out.println("outerInt = " + outerInt);
        }
    }

    static class StaticClass {

        /*
        //기본적으로 Outer 클래스의 멤버 접근 못함
        public void testFunc() {
            System.out.println("outerInt = " + OuterClass.this.outerInt);
        }
        */
    }
}

 

2. 익명 클래스는?

 

IntelliJ에서 몇 가지 테스트를 해봤는데 우리가 쓰는 익명 클래스는 또 다른 형태네요.

우리가 흔히 사용하는 함수 내부에서 사용하는 익명 클래스는.. Method–Local inner classes인 것 같습니다. 번역하면 메소드 내부 클래스 정도 될까요? 말 그대로 함수 내부에 정의된 클래스입니다.

    public static void main(String[] args) {
        System.out.println("Hello World");

        int a = 5;

        //[S] 익명 클래스
        new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println("a = " + a);
            }
        }).start();
        //[E] 익명 클래스
        
        //[S] 익명 클래스-람다 표현
        new Thread(() ->System.out.println("a = " + a)).start();
        //[E] 익명 클래스-람다 표현

        //a = 6; //초기화 이후 a가 수정되면 더 이상 effectively final이 아니게 되어 Method-Local class에서 참조 불가능
        
        //[S] Named class
        class RunnableTest implements Runnable {
            @Override
            public void run() {
                System.out.println("a = " + a);
            }
        }
        Runnable namedRunnable = new RunnableTest();
        new Thread(namedRunnable).start();
        //[E] Named class
    }

사실 익명이든 아니든.. main이라는 함수 내부의 변수에 접근이 가능해야 하므로 이 함수 내부에서 정의된 클래스여야 합니다. 그런데 이렇게 함수 내부의 정의된 클래스(method-local inner class)의 경우에는 outer method의 변수에 접근(읽기)하려면 이 변수가 effectively final이거나 final 이어야 합니다.

(값 변경은 불가능)

왜 그럴까요?

이렇게 설계가 됐다고 봐야할 것 같습니다. (익명 클래스를 호출하는 쓰레드와 같은 쓰레드에서 실행될 수도 있지만..) HTTP 콜백이 불리는 경우를 생각해보면 콜백으로 불리는 익명 클래스의 코드는, 그 익명 클래스를 정의한 외부 함수가 종료된 이후에 실행될 수 있습니다. 이렇게 라이프 사이클이 다르기 때문에 익명 클래스(메소드 내부 클래스)에서 외부 함수의 변수에 직접적으로 접근이 가능하다면, 외부 함수가 종료된 이후에 접근하는 문제가 생길 수 있습니다. 그래서 이렇게 직접적인 접근을 허락하기 보다는 capture라고 하여 익명 클래스가 생성되는 시점에 이 익명 클래스에서 사용하는 메소드 내부의 지역 변수들을 copy하여 가지고 있는 것 같습니다. 이런 구조에서 만약 메소드 내부의 지역 변수(위 예제 코드에선 변수 a)가 추후에 바뀌게 된다면 동시성 문제가 생길 수 있으니 에러를 출력하도록 만든 것 같습니다.

그런데 이런 설계/구조가 좋은 것일까요? 흠..

 

3. 그럼 코틀린에서는?

class Main
    fun main(args: Array<String>) {
        println("Hello World")
        var a = 5

        //[S] 익명 클래스
        Thread {
            println("a = $a")
        }.start()
        //[E] 익명 클래스

        a = 6
        println("main: a = $a")

그런데 신기하게도 코틀린에서는 함수 내부 클래스가 외부 함수의 지역변수가 effectively final이 아니더라도 접근(읽기/쓰기)이 가능합니다.

 

goyalnitesh.medium.com/kotlin-final-variables-in-anonymous-class-8ef9fe690c49

 

위의 글에 이에 대한 자세한 설명이 있습니다.

제가 간단하게 설명하면, 코틀린에서는 이런 경우에 단순 변수가 아닌 ObjectRef나 IntRef 타입의 변수가 사용된다고 합니다. 마치 익명 클래스 내부에서 포인터를 통해 외부 변수를 접근하는 것처럼 되어서 수정이 가능합니다.

그리고 한가지 걱정되던 외부 함수가 종료되면 어떻게 되나를 생각해봤는데 외부 함수가 사라져도 스택에 있던 포인터 변수가 사라지는 것이지, 힙에 생성된 객체가 사라지는 것 같지는 않습니다. 외부 함수가 종료되어도 스택에 있던 포인터 변수만 사라지고.. 힙에 생성된 객체가 있기에 문제가 되지 않고 GC에도 살아남을 것 같습니다.

 

참고

thdev.tech/kotlin/2020/11/17/kotlin_effective_11/

beginnersbook.com/2013/05/inner-class/

Lambda에서 사용 가능한 변수(Stack과 Heap)

goyalnitesh.medium.com/kotlin-final-variables-in-anonymous-class-8ef9fe690c49

 

작성자

Posted by 드리머즈

관련 글

댓글 영역