본문 바로가기

Programing/JVM(Java, Kotlin)

[Java] Inner clsss의 접근 범위

네이버 카페에 다음과 같은 질문이 올라왔다.

https://cafe.naver.com/javachobostudy/160868

코드는 아래와 같다. 아래 처럼 밖의 클래스(Outer)안의 메서드에 정의된 내부 클래스를 지역 내부 클래스(Local inner class)라고 부른다.

public class Outer {
    private int a = 1;
    private static int b = 2;

    public void method1() {
        int c = 3;
        class Inner {
            public void method2() {
                System.out.println("sum: " + (a + b + c));
            }
        }
        Inner i = new Inner();
        i.method2();
    }

    public static void main(String[] args) {
        Outer O = new Outer();
        O.method1();
    }
}

우선 고백을 하자면, 이너클래스는 가상 머신의 명세를 완성할 무렵 역사적인 실수로 인해 자바 언어 스펙에 "내부 클래스"라는 항목이 추가되었다고 한다. 이 이야기의 출처는 자바 챔피언 Cay S. Horstmann의 말이다.

Core Java for the Impatient

어떻게 하면 Inner class의 메소드에서 각각 a,b,c에 접근 가능한 이유를 원리적으로 설명할 수 있을까?

바이트 코드 관점

우선 위의 내부 클래스 예를 컴파일하면 하나가 아닌 두 개의 클래스가 생겨난다.

Outer 클래스와 Inner 클래스에 대한 것 두 개이다.

다만 Inner 클래스는 Outer에 종속되기에 Inner.class 가 아닌 Outer$1Inner.class 라는 이름으로 생긴다.

컴파일 된 클래스

JDK를 설치하면 java, javac 외에도 javap 라는 명령어 도구가 생겨나는데, decompile을 할 수 있다.

-private 혹은 -p 옵션을 주면 모든 클래스와 멤버들을 볼 수 있다.

$ javap -p com/example/springboot/sandbox/naver/goharrm/Outer\$1Inner

위의 뼈대를 일반 Java 코드로 옮겨보면 다음과 같다.

public class Inner {
    final int c;
    final Outer outer;
    public Inner() {

    }
    public void method2() {

    }
}

하지만 위의 코드는 컴파일이 되지 않을 것이다. 왜냐하면 final로 선언된 멤버의 경우 생성자에서 초기화가 되어야 하기 때문이다.

 

javap 명령에서 -c 옵션을 주면 디스어셈블된 바이트 코드를 볼 수 있다.

javap -c com/example/springboot/sandbox/naver/goharrm/Outer\$1Inner

위쪽의 com.example.springboot.sandbox.naver.goharrm.Outer$1Inner(); 라는 라벨 아래의 코드가 바로 생성자 부분이다.

따라서 생성자의 코드는 아래와 같다고 볼 수 있다.

public class Inner {
    final int c;
    final Outer outer;
    public Inner(Outer outer, int c) {
        this.outer = outer;
        this.c = c;

    }
    public void method2() {

    }
}

결국 Inner 클래스의 경우는 아래와 같은 형태라고 볼 수 있다.

public class Inner {
    final int c;
    final Outer outer;
    public Inner(Outer outer, int c) {
        this.outer = outer;
        this.c = c;

    }
    public void method2() {
        System.out.println("sum: " + (outer.a + Outer.b + c));
    }
}

Outer와 Inner 클래스를 동시에 나타낸다면 아래와 같은 코드가 될 것이다.

public class Outer {
    private int a = 1;
    private static int b = 2;

    public void method1() {
        int c = 3;
        Inner i = new Inner(this, c);
        i.method2();
    }

    class Inner {
        final int c;
        final Outer outer;

        public Inner(Outer outer, int c) {
            this.outer = outer;
            this.c = c;

        }

        public void method2() {
            System.out.println("sum: " + (outer.a + Outer.b + c));
        }
    }

    public static void main(String[] args) {
        Outer O = new Outer();
        O.method1();
    }
}

결국 Outer 클래스 내부의 인스턴스 변수 a는 외부 클래스를 지칭하는 this$0 에 의해(위의 코드에서는 이해하기 쉽도록 Outer로 되어 있다.) 참조가 가능하고, Outer 클래스의 정적 멤버인 b는 Outer.b 로 바로 참조가 가능하다. 메서드 변수 c는 생성자에 의해 내부 final 변수 val$c에 저장되어 참조가 되는 것이다.

 

실제 업무에서 이렇게 복잡한 코드를 만들어 본 적이 없다. 하지만 궁금증을 가지고 질문하는 것은 좋은 태도라고 생각하여 기록해둔다.