본문 바로가기

Programing/JVM(Java, Kotlin)

[Java] 한 영역(scope)에서 변수를 두 번 선언할 수 없는 이유?

한 카페에 올라왔던 질문이다.

코드

아래의 코드가 왜 동작하지 않는지에 대한 문의이다.

public class DuplicationVarDeclare {
    public static void main(String[] args) {
        int i = 100;
        byte b = (byte) i;
        System.out.println(b);

        int i = 300;
        byte b = (byte) i;
        System.out.println(b);

        int k = -2;
        b = (byte) k;
        System.out.println(b);

        System.out.println(Integer.toBinaryString(k));
    }
}

질문내용:

왜 TYPE을 두번 선언하면 안되나요?
예제 2-13을 보면, I와 b는 처음에만 타입 선언이 되어있고 그 이후에는 그냥 i, b에 다른 수를 할당하는데,
두번째 나오는 i와 b를 다시 int i , byte b 로 하니까 오류가 발생하더라고요!

아래와 같은 댓글이 달렸다.

첫 번째 댓글, 자바에서 그렇게 할 수 없게 되어 있다는 이야기이다.

두 번째 댓글, 런타임 상의 이야기를 끌어다가 안되는 이유를 설명하고 있다.

 

최근 자바 컴파일러 공부를 하면서 많은 사람들이 자바 코드를 짤 때 이미 코드가 컴파일 되었다고 생각하고 런타임에서 코드를 머리로 돌리고 있다는 것을 알게 되었다. 마치 아이디 vuejs 의 댓글과 같은 관점이다.

자바 컴파일러 공부하면 바뀐점은

나도 처음에는 후자의 관점이었는데, 최근에는 좀 달라졌다.

컴파일러에 대한 것은 자바 스펙 명세(The Java Language Specification)에 맡기고,

런타임에 대한 것은 자바 가상 머신 명세(The Java Virtual Machine Specification)에 위임한다.

 

문제는 런타임에 대한 것 까지 컴파일러까지 끌어다가 설명하면 설명을 하지 못하는 문제들도 생긴다.

2019년 11월 21일에 올라왔던 질문에 왜 static 메서드 안에서 static 변수 선언 및 초기화가 안되는지 물어보는 질문이 올라왔다.

아래와 같은 형태이다.

public class WhyNot {
    public static void main(String[] args) {
        static int i = 10;
    }
}

질문자는 댓글로 왜 안되는지 메모리 구조적으로 알고 싶어했다. 부연해서 컴파일할 때 JVM이니 Method 영역이니 Heap 영역같은 이야기까지 나온다.

하지만 컴파일 하는 과정에서 JVM에 클래스파일이 로딩이 일어나는 일은 발생하지 않는다.

자바 컴파일러도 JVM 상에서 돌아가도록 되어 있지만 코드를 파싱하고 컴파일해서 바이트코드로 만들 때 이런 JVM의 구조를 이용하지는 않는다.

내가 만약에 댓글을 달았다면?

만약 질문자가 컴파일러를 만들어본 경험이 있다면 쉽게 대답해 줄 수 있을 것이다.

해당 영역의 심볼 테이블에 이미 심볼이 들어가 있으니까 컴파일러가 구분을 할 수 없다고..

안그러면 JSL 6.5.6.1 에 있는 Simple Expression Names 에 그렇게 되어있다고 던지고 끝낸다.

If an expression name consists of a single Identifier, then there must be exactly one declaration denoting either a local variable, formal parameter, or field in scope at the point at which the Identifier occurs. Otherwise, a compile-time error occurs.

또한 변수 이름이라는 것은 소스 코드상의 이름이고 실제 변수가 바이트 코드가 되면 이름은 숫자로 바뀌게 된다.

따라서 이름이라는 개념은 런타임에 큰 의미가 없다.

바이트 코드 탐험

아래와 같은 코드가 있다고 하자.

public class Foo {
    public static void main(String[] args) {
        int var = 0;
        int name = 1;
        int will = 2;
        int be = 3;
        int erased = 4;
        int sum = var + name + will + be + erased; // 10 = 0 + 1 + 2 + 3 + 4
        System.exit(sum); // can read by "echo $?" (Bash) or "echo %errorlevel%" (Windows)
    }
}

변수들을 더해서 프로그램의 종료 코드의 값으로 반환하는 간단한 코드이다.

컴파일을 해서 실행을 해보면 10이 반환되는 것을 알 수 있다.

zsh이라 echo $? 로 프로그램의 종료코드를 획득했다.

 

컴파일러마다 만드는 바이트 코드는 달라질 수 있지만 javac 1.8.0_211 기준에서 아래와 같은 class 파일을 만들었다.
(매직넘버인 CAFE BABE가 잘 보인다.)

전체 바이트 코드 중에, main 함수의 body 부분에 관심을 가져보면 아래 그림의 파랗게 선택된 부분이다.

해석을 하면 아래와 같다.

03 3c : iconst_0 istore_1 // 상수 0을 현재 프레임의 로컬 변수 배열 1번에 넣는다.
04 3d : iconst_1 istore_2 // 상수 1을 현재 프레임의 로컬 변수 배열 2번에 넣는다.
05 3e : iconst_2 istore_3 // 상수 2를 현재 프레임의 로컬 변수 배열 3번에 넣는다.
06 3604 : iconst_3 istore+index:04 // 상수 3을 현재 프레임의 로컬 변수 배열 4번에 넣는다.
07 3605 : iconst_4 istore+index:05 // 상수 4을 현재 프레임의 로컬 변수 배열 5번에 넣는다.

1b 1c 60 : iload_1 iload_2 iadd // 로컬 변수 배열 1번2번의 값을 꺼내서 operand stack에 넣고 더한다.
1d 60 : iload_3 iadd // 로컬 변수 배열 3번의 값을 꺼내서 operand stack에 넣고 더한다. (기존에 1 + 2의 결과가 있어서 합쳐진다.)
1504 60 // 로컬 변수 배열 4번의 값을 꺼내서 operand stack에 넣고 더한다. (기존에 1 + 2 + 3의 결과가 있어서 합쳐진다.)
1505 60 // 로컬 변수 배열 5번의 값을 꺼내서 operand stack에 넣고 더한다. (기존에 1 + 2 + 3 + 4의 결과가 있어서 합쳐진다.)
3606 : istore+index:06 // operand stack의 값을 로컬 변수 배열 6번에 넣는다.

1506 : iload+index: 06 // 위에서 합쳤던 값들을 꺼내서(즉, sum부분)
b80002 : invokestatic+indexbyte1:00+indexbyte2:02 (System.exit 정적 메서드를 호출한다)

b1 : return // void 타입으로 리턴한다.

바이트 코드가 되면서 변수의 이름은 등장하지 않는다. 그냥 로컬 변수에 인덱스 정도로 구별이 된다.


현실 세계에서도 이름은 식별자로 사용이 되고 있다. (코드) 하지만 DB와 같은 엄격한 세계에서는 별도의 대리키로 PK를 만드는 것과 유사하다고 볼 수 있다. (바이트코드)