Weekly Java: 인스턴스 변수는 기본값으로 초기화되지만, 왜 지역 변수는 초기화되지 않나요?
문
우리가 자바로 프로그래밍을 하면 변수 초기화에 대해 깊이 있게 고민하지 않는다. 우리는 변수를 선언하면 기본 값으로 초기화된다고 배운다.
boolean a; => false
로 초기화int b; => 0
으로 초기화String c; => null
로 초기화
다음 코드를 살펴보자.
public class Test {
boolean a;
void test() {
System.out.println(a);
boolean b;
System.out.println(b);
}
}
위 소스 코드를 컴파일 해보면 a를 출력하는 부분은 정상적으로 컴파일 된다. 하지만 b를 출력하는 부분에서 컴파일 에러가 발생한다. 우리가 알고 있기로 b 또한 false로 초기화가 되기 때문에 test() 메소드를 실행하면 false, false 값이 나올 것으로 예상했는데 그냥 컴파일 에러가 발행한다. 이유가 뭘까?
답
Each local variable (§14.4) and every blank
final
field (§4.12.4, §8.3.1.2) must have a definitely assigned value when any access of its value occurs. - Oracle Specification
일단 언어 스펙에서 그렇게 정의했다. 물론 스펙에서 그렇게 정의했으니 그런 것이다. 라고 생각할 수 있겠지만, 조금 더 들어가보자. 우리가 궁금한 점은, 왜 규칙이 멤버 변수와 인스턴스 변수 간에 상이하냐는 것이다.
먼저, 로컬 변수의 경우 하기와 같은 이유에서 컴파일 오류가 발생할 수밖에 없다고 생각한다.
- JVM이 로컬 변수로
boolean b = false;
와 같이 구현하게 되면, Stack Frame의 로컬 변수 영역에b
를 생성한다. - 쓰레드가
b
를 사용하려고 할 때, 로컬 변수 영역에 있는 로컬 변수b
를 Operand 영역으로 로드한 후 실행한다.
그런데, boolean b;
와 같이 초기화 없이 로컬 변수를 선언할 경우 Stack Frame의 로컬 변수 영역에 b
를 변수로 생성하지 않는다. 따라서 Stack Frame의 로컬 변수 영역에 b
가 없는 상태에서 b
를 사용하려고 할 경우, 로컬 변수 영역에 b
가 없으므 프로그램 실행 중 문제가 발생하게 된다. 따라서 이를 컴파일 에러로 해결하고 있다는 주장이 있다.
컴파일러마다 로컬 변수 참조에 대한 초기화가 다르게 적용된다. Objective-C LLVM ARC Option의 경우 nil
로 초기화를 해준다고 하며, GCC Compiler는 이와 달리 임의의 값으로 초기화될 수 있다고 한다.
인스턴스 (멤버) 변수의 초기화 여부는 컴파일 단계에서는 알 수 없기 때문에, Java에서는 일단 기본값으로 초기화하는 전략을 택한 것으로 보인다. 인스턴스 멤버 변수가 할당되는 Heap 메모리는 기본적으로 런타임 시간에 필요한 만큼 동적으로 메모리를 할당받아 사용할 수 있다. 또한, 필드의 경우 필드 값을 읽는 메소드(getter)가 있고 값을 할당하는 메소드(setter)가 있을 때, 사용자가 어느 메소드를 먼저 실행할 지 알 수 없다. 따라서 인스턴스 변수들은 컴파일 시간에 초기화가 되어 있는 지 검증할 수 없다. 이에 따라 인스턴스 변수는, 임의의 초기값을 설정하여 초기화하는 것으로 보인다.
이와 달리 멤버 변수가 할당되는 스택은 컴파일 시간에 미리 계산되어서 필요한 만큼만 확보가 된다. 또한, 로컬 스코프에서는 구문 하나하나가 실행되는 순서를 컴파일러가 알 수 있어 검증이 가능하다. 따라서 멤버 변수는 컴파일러가 로직을 검증할 수 있기 때문에 초기화하지 않았다는 오류를 반환할 수 있는 것으로 보인다.
Tip: Stack Frame(프레임) 에 대한 짧은 이야기 (사진 출처, 내용 참고)
JVM에서 쓰레드가 생성될 때 해당 쓰레드를 위한 스택이 별도로 생성된다. 스택에는 프레임이 있다.
프레임은 메소드가 호출될 때마다 만들어지고, 메소드의 상태 정보를 담게 된다.
프레임은 Local Variables, Operand Stack(스택 내 메소드 계산 공간), Constant Pool 참조를 갖는다.
Getting back to the spirit of Java — secure, deterministic, predictable behavior: If the computer and run time cannot guarantee the safety of local reference variables, how can Java ensure that programs are robust? The answer is simple: Design the compiler so that it requires the programmer to initialize local reference variables.
Bonus: Rust
그렇다면 메모리 안정성을 최우선하는 Rust는 어떨까? 다음 예시를 보자.
fn main() {
// Declare a variable binding
let a_binding;{
let x = 2;// Initialize the binding
a_binding = x * x;
}println!("a binding: {}", a_binding);let another_binding;// Error! Use of uninitialized binding
println!("another binding: {}", another_binding);
// FIXME ^ Comment out this lineanother_binding = 1;println!("another binding: {}", another_binding);
}
컴파일러는 다음의 오류를 내뿜게 된다. 초기화되지 않은 변수를 사용하지 마라고 한다.
Compiling playground v0.0.1 (/playground)
error[E0381]: borrow of possibly-uninitialized variable: `another_binding`
--> src/main.rs:17:37
|
17 | println!("another binding: {}", another_binding);
| ^^^^^^^^^^^^^^^ use of possibly-uninitialized `another_binding`
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)For more information about this error, try `rustc --explain E0381`.
error: could not compile `playground` due to previous error
다음 링크를 참조하라. Rust는 객체지향 언어가 아니기 때문에 로컬 변수와 인스턴스 변수의 차이가 없고, 따라서 변수가 초기화되지 않은 경우 컴파일러는 컴파일 단계에서 로직을 검증하고 에러를 반환한다. 역시 Rust가 짱인가?
Always set zero is cargo cult. Real thing is you must not read undefined values, because then your code behavior is undefined, no matter what language. Setting var to zero, then setting it to useful value is waste of your time and code readers’ attention, again, no matter what language. Rust is just strict about it.
Reference
- https://www.quora.com/Why-does-the-Java-compiler-require-reference-variables-to-be-initialized-even-though-the-Java-specification-requires-default-reference-values-to-be-null
- https://alvinalexander.com/scala/fp-book/recursion-jvm-stacks-stack-frames/
- https://velog.io/@josajang98/%EC%9E%90%EB%B0%94-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EB%B3%80%EC%88%98%EC%9D%98-%EC%B4%88%EA%B8%B0%EA%B0%92
- https://www.slipp.net/questions/162#answer-719
- https://johngrib.github.io/wiki/jvm-stack/
- https://stackoverflow.com/questions/268814/uninitialized-variables-and-members-in-java
- https://stackoverflow.com/questions/415687/why-are-local-variables-not-initialized-in-java?noredirect=1&lq=1
- https://doc.rust-lang.org/rust-by-example/variable_bindings/declare.html
- https://www.reddit.com/r/rust/comments/9skxf1/is_there_no_need_to_initialize_variables_in_rust/