3.6 현실 세계 언어 파헤치기

홍재민

2020년 10월 18일 17시 35분


아래 내용은 인사이트 출판사의 제안으로 작성 중인 책의 초고입니다. 실제 출판 시에는 내용이 달라질 수 있습니다. 많은 의견 부탁드립니다.

이제 파이썬, 자바스크립트, C의 변수에 대해 알아보자. 사실 대부분의 언어에는 지역 변수(local variable)만 존재하는 것이 아니고, 전역 변수(global variable)도 존재한다. 지역 변수가 앞서 본 것처럼 제한된 영역을 가지는 것과 달리 전역 변수의 영역은 프로그램 전체이다. 영역이 다른 것을 제외하면 지역 변수와 전역 변수에는 큰 차이가 없다. 지역 변수와 마찬가지로, 전역 변수 역시 정의하는 부분이 존재하고 그 변수를 사용하는 부분이 존재한다. 정의할 때는 변수의 값을 식을 써서 정하고 변수를 사용할 때는 이름을 통해 사용할 수 있다. 이처럼, 변수의 핵심이라고 할 수 있는 변수 정의와 변수 사용이 똑같기 때문에, 전역 변수에 대해서는 따로 다루지 않겠다. 이 절의 코드 예시에는 지역 변수만 등장한다.

파이썬

파이썬에서는 지역 변수를 정의하려면 함수를 만들어야 한다. 아직 이 책에서 함수를 다루지 않았는데 코드 예시에 함수를 사용하는 것이 약간 걸리기는 하지만, 함수가 예시에서 중요한 역할을 하는 것은 아니므로 그냥 함수를 사용하도록 하겠다.

먼저 변수를 정의하고 사용하는 프로그램부터 보자.

코드

def main():
    x = 1
    print(x)

main()

결과

1

이 프로그램은 main 함수를 정의하고 호출한다. 그 결과 x = 1print(x)가 실행된다. 함수에 대해 이야기하고자 하는 것은 아니므로, x = 1print(x)에만 집중하자. 먼저, x = 1은 변수 x를 정의하고 그 값을 1로 한다. 그 다음 줄의 print(x)x를 사용하는 식이다. x = 1 다음에 print(x)가 있으므로 print(x)x의 영역에 포함된다. 따라서 print(x)1을 출력한다.

위의 예시만 보면 파이썬과 산술x의 변수의 의미가 차이가 없어 보인다. 그러나 산술x의 변수는 변수를 정의하는 식의 부분식만을 영역으로 한 것과 달리, 파이썬의 변수는 그 변수를 정의하는 함수 전체를 영역으로 한다. 다음은 파이썬 3.9 명세의 일부를 발췌한 것이다. (발췌하면서 문장의 일부를 지웠다. 앞으로도 필요에 따라 발췌한 문구가 너무 길어지지 않도록 원래의 의미가 변하지 않는 선에서 문장을 수정할 수 있다. 정확한 내용이 궁금하다면 직접 명세를 확인해 보기를 바란다. 명세 원문은 영어로 되어 있으나 저자가 한국어로 번역하였다. 파이썬 등 일부 언어의 명세는 한국어 번역본이 존재하지만 그러한 경우에도 저자가 원문을 번역한 내용을 싣는다. 번역본의 질이 항상 높은 것은 아니며 용어를 이 책과 다르게 번역하는 경우가 많기 때문이다.)

지역 변수가 함수 블록 안에 정의되어 있다면, 그 변수의 영역은 그 함수 블록 안에 포함된 모든 블록으로 확장된다.1

함수 안에 정의되어 있는 지역 변수의 영역은 그 함수 전체라고 쓰여 있다. 따라서 파이썬에서는 다음과 같은 프로그램이 오류 없이 실행된다.

코드

def main():
    if True:
        x = 1
        print(x)
    print(x)

main()

출력

1
1

if True는 새로운 블록을 만들기 위해 넣은 것이므로 신경 쓰지 않아도 된다. (여기서 블록이란 들여쓰기가 같은 크기만큼 된 연속한 줄들을 의미한다.) 첫 print(x)는 확실히 x의 영역에 포함되어 있지만 두 번째 print(x)x의 영역에 포함되지 않은 것처럼 보이기도 한다. 특히, C나 자바 등의 언어에 익숙한 사람에게는 그렇게 보일 것이다. 그러나, 위의 언어 명세에서 본 것처럼 파이썬에서 변수의 영역은 함수 전체이다. 따라서 두 번째 print(x) 역시 x의 영역에 포함되어 있고, 프로그램은 1을 두 번 출력한 뒤 오류 없이 종료된다.

이처럼 변수의 영역이 함수 전체라는 점으로 인해 파이썬에서는 한 함수 안에서 가리기가 일어나지 않는다. 다음의 파이썬 프로그램을 생각해 보자.

코드

def main():
    x = 1
    if True:
        x = 2
        print(x)
    print(x)

main()

출력

2
2

아까와 마찬가지로, C나 Java에 익숙하거나 산술x의 의미를 생각 중인 사람에게는 프로그램이 21을 출력해야 하는 것처럼 보인다. 그러나 앞서 보았듯 파이썬의 변수는 함수 전체를 영역으로 한다. 그러므로 x = 2xx = 1x와 구분되지 않는다. x = 2x라는 새로운 변수를 정의하기보다는 이미 정의된 x의 값을 수정하는 것이다.2 수정된 x의 값은 함수가 끝날 때까지 유효하므로 두 번째 print(x) 역시 2를 출력한다. 결국 프로그램은 2만 두 번 출력한다.

이런 파이썬의 의미는 때때로 실수를 유발하기도 한다.

코드

def main():
    x = 1
    for x in range(10):
        pass
    print(x)

main()

출력

1

이 코드를 작성한 사람은 높은 확률로 1이 출력되기를 기대했을 것이다. 그러나 실제로는 for 반복문의 매 반복마다 x = 1에서 정의한 x의 값이 수정되기 때문에 print(x)가 실행될 때 x의 값은 1이 아니라 9이다. 따라서 파이썬에서는 C나 자바 등에서와 달리 반복문이나 조건문 안에서 잠시 사용할 변수의 이름을 정할 때도 주의를 기울여야 한다.

변수의 영역이 함수 전체라는 점 때문에 재미있는 일도 일어난다. 다음의 프로그램을 생각해 보자.

코드

def main():
    print(x)
    x = 1

main()

출력

UnboundLocalError: local variable 'x' referenced before assignment

print(x)x = 1 앞에 온다. 얼핏 보기에는 print(x)x가 자유 변수 같다. 그러나 x는 자유 변수가 아니라 묶인 등장이다. x = 1에서 정의한 x의 영역이 함수 전체이므로 print(x)x 역시 영역에 포함된다. 비록 x의 값이 정해지기 전에 x가 사용되었기에 결과적으로는 오류가 발생했지만 x는 분명히 묶인 등장이다. 이는 파이썬 명세에도 잘 설명되어 있다.

어떤 이름이 사용된 시점에 그 이름이 참조하는 지역 변수에 값이 아직 묶이지 않았다면, UnboundLocalError라는 예외가 발생한다.3

지역 변수의 값이 정해지기 전에 사용되면 UnboundLocalError 오류가 발생한다고 나와 있다. 이 오류는 자유 변수 때문에 발생하는 오류와는 다르기 때문에, 위의 x가 자유 변수가 아니라 묶인 등장이라는 확실한 증거이다.

코드

def main():
    print(x)

main()

출력

NameError: name 'x' is not defined

위의 프로그램에는 x를 정의하는 부분이 아예 없다. 따라서 print(x)x는 자유 변수가 맞고, UnboundLocalError 대신 NameError가 발생한다. 이 예시로부터 파이썬과 산술x의 변수의 의미에는 영역 말고도 차이가 더 있음을 알 수 있다. 산술x에서는 사용 가능한 모든 변수는 값이 있다. 그러나 파이썬에서는 변수를 정의하는 코드가 함수의 어디에 위치하든 상관없이 함수 전체가 그 변수의 영역이다. 따라서 사용할 수 있는 변수임에도 그 값은 아직 정해지지 않은 경우가 있을 수 있다. 그리고, 값이 정해지지 않은 변수를 사용하면 오류가 발생한다.

변수의 영역이 함수 전체라는 점으로 인해 변수의 재귀적인 정의도 자연스럽게 가능해진다. 이 역시 산술x와의 차이점이다. 파이썬과 달리 산술x는 변수의 재귀적인 정의를, 즉 변수를 정의하는 식이 그 변수를 사용하는 것을 허용하지 않는다.

코드

def main():
    x = x

main()

출력

UnboundLocalError: local variable 'x' referenced before assignment

앞서 본 프로그램과 마찬가지로, x = x에서 두 번째 x는 자유 변수가 아니라 첫 번째 x에 묶인 등장이다. x의 값이 정해지기 전에 x를 사용했으므로 UnboundLocalError가 발생하는 것도 동일하다. 오류만 발생시키는 이 프로그램만 봐서는 재귀적인 정의가 흥미롭게 느껴지지 않는다. 그러나 익명 함수와 함께 사용하면 def없이도 재귀 함수를 만들 수 있게 된다.

코드

def main():
    sum = lambda x: x + sum(x - 1) if x else 0
    print(sum(10))

main()

출력

55

위에서 sum은 인자로 자연수가 주어졌을 때 재귀적으로 0부터 주어진 인자까지의 합을 구하는 함수이다. 흥미로운 예시이지만, 익명 함수는 5장, 재귀는 6장에서 다룰 개념이므로 이 이상은 설명하지 않고 넘어가겠다.

자바스크립트

파이썬과 마찬가지로, 이번에도 지역 변수에 집중하기 위해 함수 안에 변수를 정의하겠다. 함수는 관심 대상이 아니므로 변수의 정의와 사용만 보면 된다.

ECMA스크립트 2015 전까지 자바스크립트에서 변수를 정의하는 방법은 var 하나였다. 다음은 var에 대한 설명을 ECMA스크립트 2020 명세에서 발췌한 것이다.

var 문은 현재 실행 중인 실행 문맥의 “변수환경”을 영역으로 하는 변수를 정의한다. 한 “변수환경”의 영역 안에서 같은 이름의 “묶기식별자”가 한 개 이상의 “변수선언”에 등장할 수 있으나, 그 선언은 모두 다 합쳐서 오직 한 변수만을 정의한다.4

여기서 “변수환경”의 영역은 var을 통해 정의된 변수를 포함하고 있는 함수 전체이다. 따라서 변수의 영역은 함수 전체이며, 같은 이름의 변수를 한 함수 안에서 여러 번 정의했다면 모두 동일한 하나의 변수를 정의한다. 그러므로 아무리 함수 안에서는 변수 선언을 많이 해도 한 이름의 변수는 한 번만 정의된다. 이 설명에서 알 수 있듯이 자바스크립트에서 var로 정의한 변수의 의미는 파이썬의 변수의 의미와 유사하다. 함수 전체가 변수의 영역이며, 변수가 한 번 정의되고 나면 그 다음부터 정의처럼 보이는 것이 실제로는 변수를 수정하는 효과를 낸다. 따라서 앞에서 본 파이썬 프로그램과 비슷하게 작성한 자바스크립트 프로그램은 같은 결과를 낸다. 이미 파이썬의 변수의 의미를 자세히 보았으므로 긴 설명 없이 코드 예시 위주로 보도록 하겠다.

코드

function main() {
    var x = 1;
    console.log(x);
}

main();

출력

1

var x = 1;을 통해 x라는 이름의 변수를 정의했으며 그 값은 1이다. console.log(x); 에서 x를 사용하며 이 xvar x = 1;x에 묶여 있다. 따라서 프로그램이 1을 출력한다.

코드

function main() {
    {
        var x = 1;
        console.log(x);
    }
    console.log(x);
}

main();

출력

1
1

x의 영역이 함수 전체이므로 두 번째 console.log(x); 역시 오류 없이 1을 출력한다. 따라서 1이 두 번 출력된다.

코드

function main() {
    var x = 1;
    {
        var x = 2;
        console.log(x);
    }
    console.log(x);
}

main();

출력

2
2

var x = 2;를 통해 x를 다시 정의함으로써 먼저 정의한 x를 가린 것처럼 보이지만 실제로는 그렇지 않다. var x = 2;는 이미 정의한 x의 값을 수정하므로 2가 두 번 출력된다.

코드

function main() {
    var x = 1;
    for (var x = 0; x < 10; x++) {
    }
    console.log(x);
}

main();

출력

10

이런 이유로, 자바스크립트를 사용할 때도 변수를 var을 통해 정의했다면 반복문이나 조건문 안에서 변수를 정의할 때 주의가 필요하다. 잘못하면 그 전에 정의한 변수의 값을 의도치 않게 바꾸어 버릴 수 있다.

코드

function main() {
  console.log(x);
  var x = 1;
}

main();

출력

undefined

변수의 영역이 함수 전체이므로 변수를 정의하는 줄 위에서 그 변수를 사용하는 것도 가능하다. 파이썬과 한 가지 차이가 있다면, 명세에 나와 있는 것처럼 아직 값이 정해지지 않은 변수의 값은 일단 undefined로 초기화된다는 것이다. undefined는 자바스크립트에 존재하는 기본(primitive) 값 중 하나이다.

var로 정의된 변수는 그 변수를 포함하는 “정적 영역”이 생성될 때 만들어지며 그 값은 undefined로 초기화된다.5

그러므로 파이썬에서 값이 정해지지 않은 변수를 사용할 때 오류가 발생한 것과 달리, 자바스크립트에서 값이 아직 정해지지 않은 변수는 undefined라는 값을 가진다. 이런 의미에 따라 위 프로그램은 undefined를 출력하고 오류 없이 종료된다.

코드

function main() {
  console.log(x);
}

main();

출력

ReferenceError: x is not defined

단, 아예 정의하지 않은 변수를 사용하는 경우에는 그 변수가 자유 변수이므로 오류와 함께 프로그램이 종료된다.

for 문을 사용하는 예시에서 본 것처럼, var를 통해 정의된 변수는 함수 전체를 영역으로 하기 때문에 프로그래머가 실수하기 쉽게 만든다. 이 문제를 해결하고자 ECMA스크립트 2015에서는 letconst로 변수를 정의하는 방법을 추가했다. letconst를 통해 정의된 변수는 블록 단위로 영역이 정해지며 한 블록에 같은 이름의 변수를 두 번 이상 정의할 수 없다. (여기서 블록이란 하나의 중괄호 쌍 사이를 의미한다.)

letconst 선언은 현재 실행 중인 실행 문맥의 “정적환경”을 영역으로 하는 변수를 정의한다.6

앞서 본 var이 “변수환경”을 사용하는 것과는 달리 letconst는 “정적환경”을 사용하며, 그 덕분에 함수 단위가 아니라 블록 단위 영역을 가진다. 따라서 letconst를 통해 정의한 변수의 의미는 산술x의 변수의 의미와 비슷하다. 지금부터 예시 코드를 보겠다. letconst의 차이는 let은 수정 가능하고 const는 수정 불가능하다는 점뿐이다. 이번 장에서 수정 불가능한 변수를 다룬 것에 맞게 예시 코드에서는 const만 사용하지만 let을 사용하더라도 같은 결과가 나올 것이다.

코드

function main() {
    const x = 1;
    console.log(x);
}

main();

출력

1

var 대신 const를 사용해 변수를 정의한 것이다. 예상대로 프로그램이 1을 출력한다.

코드

function main() {
    {
        const x = 1;
        console.log(x);
    }
    console.log(x);
}

main();

출력

1
ReferenceError: x is not defined

const x = 1;을 통해 정의된 x의 영역은 const x = 1;부터 const x = 1;을 포함하는 가장 가까운 중괄호 쌍이 끝날 때까지이다. 첫 번째 console.log(x);x의 영역 안에 있으므로 1을 출력한다. 그러나 두 번째 console.log(x);x의 영역 밖이므로 여기의 x는 자유 변수이고 오류가 발생한다. 변수의 의미가 산술x에서와 비슷함을 알 수 있다.

코드

function main() {
    const x = 1;
    {
        const x = 2;
        console.log(x);
    }
    console.log(x);
}

main();

출력

2
1

영역이 블록 기반으로 정해지므로 가리기도 정상적으로 일어난다. 첫 번째 console.log(x);xconst x = 2;를 통해 정의된 x에 묶여 있으므로 2를 출력한다. 반면, 두 번째 console.log(x);xconst x = 1;을 통해 정의된 x에 묶여 있으므로 1을 출력한다. 산술x와 비슷하게 가리기가 일어남을 볼 수 있다.

코드

function main() {
    let x = 1;
    for (let x = 0; x < 10; x++) {
    }
    console.log(x);
}

main();

출력

1

(반복문에서 x의 값을 증가시키기 위해 const대신 let을 사용했다.) 가리기가 정상적으로 일어나므로 반복문이나 조건문에서 사용한 변수 때문에 바깥의 변수의 값이 바뀌는 일이 없다.

다만, 한 블록 안에서 같은 이름의 변수를 두 번 이상 정의할 수는 없으므로 블록을 중첩해 만들 때만 가리기가 가능하다. 예를 들면, 아래의 프로그램에서는 가리기가 일어나는 것이 아니라 오류가 발생한다.

코드

function main() {
    const x = 1;
    const x = 2;
}

main();

출력

SyntaxError: Identifier 'x' has already been declared

“묶는목록”의 “묶인이름”이 동일한 항목을 가지고 있으면 “문법 오류”이다.7

재미있는 점은 발생한 오류가 문법 오류라는 점이다. 즉, 위의 코드는 사실 자바스크립트 프로그램조차 아닌 것이다. 이처럼, 아주 단순한 오류의 경우 코드를 실행하지 않고도 확인할 수 있기 때문에 파싱 단계에서 검사되고는 한다. 같은 이름의 변수를 두 번 정의하는 위와 같은 오류가 그런 간단한 오류의 예시이다. 이런 오류를 파싱할 때 찾을지, 아니면 실행하면서 찾을지는 언어를 설계한 사람의 선택일 뿐이므로 이 오류가 문법 오류냐 실행 시간 오류냐에 대해 크게 신경 쓸 필요는 없다.

코드

function main() {
    console.log(x);
    const x = 1;
}

main();

출력

ReferenceError: x is not defined

let이나 const를 사용함으로써 변수가 정의되기 전에 사용되는 것도 막을 수 있다. 앞서 var을 사용해 작성한 비슷한 코드에서는 undefined가 출력되었다. 그러나 const를 사용하자 오류가 발생한 것을 볼 수 있다. 상황에 따라 아무렇게나 실행하는 편이 오류가 발생해 프로그램이 종료되는 것보다 나을 수도 있지만, 프로그램을 개발 중인 상황을 가정해 보면, 변수를 정의하기 전에 사용했는지도 모르고 넘어가는 것보다는 오류가 발생해서 코드를 수정할 기회를 얻는 것이 실수를 줄이는 데 도움이 될 것이다.

산술x와 자바스크립트의 또 한 가지 차이점은 자바스크립트의 변수는 var, let, const중 무엇을 사용해 정의했든 상관없이 재귀적 정의를 허용한다는 것이다.

코드

function main() {
    const sum = x => x ? (x + sum(x - 1)) : 0;
    console.log(sum(10));
}

main();

출력

55

sum은 0부터 주어진 자연수까지의 합을 계산하는 재귀 함수이다. 아까와 마찬가지로 위 프로그램은 이 장의 주제를 벗어나므로 자세히 다루지 않겠다.

C

C의 변수의 의미는 자바스크립트에서 letconst를 통해 정의된 변수의 의미와 거의 같다. 물론 C가 자바스크립트 아버지뻘이니 자바스크립트의 의미가 C의 의미를 닮았다고 말해야 옳기는 하겠지만, 요즘은 자바스크립트가 C보다 친숙한 사람들이 많을 것임을 고려해 자바스크립트를 먼저 설명하다 보니 이렇게 되었다. 아무튼 이미 설명한 내용과 대부분 겹치므로 C에 대한 설명은 길게 하지 않겠다.

다음은 C18 명세에서 발췌한 것이다.

선언자가 어떤 블록 안에 식별자를 정의하면 그 식별자는 그 블록이 끝나는 지점에서 종료되는 블록 영역을 가진다. 두 식별자의 영역이 같은 곳에서 끝날 때 두 식별자의 영역이 같다고 말한다.8 같은 영역을 가지는 식별자를 정의하는 선언은 한 개보다 많을 수 없다.9

(식별자가 변수랑 같은 뜻이라고 이해하면 된다.) 변수의 영역은 블록 단위로 결정되며 영역이 동시에 끝나는 같은 이름의 변수가 둘 이상 존재할 수 없다.

코드

#include <stdio.h>

int main() {
    int x = 1;
    printf("%d\n", x);
}

출력

1

int x = 1;x라는 이름의 변수를 정의하며 x의 값은 1이다. printf("%d\n", x);x를 사용하며 그 값은 1이므로 1이 출력된다. int x = 1;에서 intx의 타입을 나타낸다. 이렇게 코드 중에 특정 대상의 타입을 표시한 것을 타입 표시(type annotation)이라 부른다. 산술x, 파이썬, 자바스크립트는 동적 타입 언어(dynamically typed language)이기 때문에 타입 표시가 필요 없다. 반면, C는 정적 타입 언어(statically typed language)이기 때문에 타입 표시가 필요하다. 타입은 11장에서 다룰 예정이므로 자세히 이야기하지 않겠다.

코드

#include <stdio.h>

int main() {
    {
        int x = 1;
        printf("%d\n", x);
    }
    printf("%d\n", x);
}

출력

error: use of undeclared identifier 'x'

두 번째 printf("%d\n", x);x는 자유 변수이므로 오류가 발생한다. 앞서 산술x와 자바스크립트에서 본 오류와 다른 점은 이 오류는 실행 시간 오류가 아니라 컴파일 시간 오류라는 점이다. C 프로그램은 실행되기 전에 타입 검사 과정을 거치며 타입 검사를 통해 자유 변수를 찾을 수 있다. 즉, 이 오류는 실행 전에 발생한 오류이다. 실행 중에 발생한 오류가 아니므로 실행 중 오류와도 다르고, 파싱이 성공한 다음 타입 검사 중에 찾은 오류이기 때문에 문법 오류와도 다르다. 자세한 이야기는 역시 11장에서 할 것이다.

코드

#include <stdio.h>

int main() {
    int x = 1;
    {
        int x = 2;
        printf("%d\n", x);
    }
    printf("%d\n", x);
}

출력

2
1

변수의 영역이 블록 단위이므로 가리기가 정상적으로 일어난다. 따라서 반복문이나 조건문 안에서 정의한 변수 때문에 바깥의 변수의 값이 바뀌지 않는다.

코드

#include <stdio.h>

int main() {
    int x = 1;
    int x = 2;
}

출력

error: redefinition of 'x'

명세에도 나온 것처럼 같은 이름의 변수가 동시에 끝나는 영역을 가질 수 없다. 따라서 오류가 발생하며, 이 오류 역시 실행 시간 오류가 아닌 컴파일 시간 오류이다.

코드

#include <stdio.h>

int main() {
    printf("%d\n", x);
    int x = 1;
}

출력

error: use of undeclared identifier 'x'

변수를 정의하기 전에 사용해도 컴파일 시간 오류가 발생한다.

코드

#include <stdio.h>

int main() {
    int x;
    printf("%d\n", x);
}

출력

(정의되지 않음)

단, 값을 정의하지 않고 선언만 한 변수를 사용할 수는 있다. 이 경우 컴파일 시간 오류가 발생하지는 않지만, 값이 정의되지 않은 변수를 사용하는 것은 정의되지 않은 동작(undefined behavior)이라고 명세에 규정되어 있다.

lvalue가 계산될 때 어떤 객체도 지정하지 않는다면 그 동적은 정의되지 않는다.10

(변수는 lvalue의 일종이다.) 정의되지 않은 동작을 발생시키는 프로그램은 실행 중에 아무 동작이나 할 수 있다. 비정상적인 종료, 임의의 값으로 변수를 초기화하고 실행, 임의의 함수 호출 등 어떤 일도 할 수 있다. 따라서 위와 같은 프로그램은 작성해서는 안 된다. 누구도 예상할 수 없는 아무런 동작이나 하기에 존재 가치가 없는 프로그램인 것이다. 이러한 정의되지 않은 동작은 자바스크립트에서 값을 정의하지 않은 변수의 값이 undefined라는 값으로 자동으로 초기화되는 것과는 전혀 다르다. 값을 undefined라고 이름 붙였을 뿐 자바스크립트의 동작은 매우 잘 정의되어 있다.

코드

#include <stdio.h>

struct a {
    struct a *p;
};

int main() {
    struct a sa = { &sa };
}

출력

(없음)

C 역시 재귀적인 변수 정의를 허용한다. C에 익명 함수는 없지만 포인터 등과 함께 유용한 재귀적 정의를 만들 수 있다.

자바

자바의 변수의 의미 역시 C와 비슷하다. 자바도 C처럼 정적 타입 언어이기 때문에 타입 표시가 필요하고 여러 오류를 컴파일 시간에 확인할 수 있다는 점까지도 동일하다. (단, 자바 10부터는 타입 표시를 생략할 수 있게 되었으나 이 점은 고려하지 않겠다.) 다음은 자바 14의 명세에서 발췌한 것이다.

블록 안에 있는 지역 변수 선언의 영역은 그 선언이 포함된 블록이 끝날 때까지이다.11

블록 기반으로 영역이 결정됨을 확인할 수 있다.

코드

class Example {
    public static void main(String[] args) {
        int x = 1;
        System.out.println(x);
    }
}

출력

1

x의 값을 1로 정의하고 사용했으므로 1이 출력된다.

코드

class Example {
    public static void main(String[] args) {
        {
            int x = 1;
            System.out.println(x);
        }
        System.out.println(x);
    }
}

출력

error: cannot find symbol
symbol: variable x

두 번째 System.out.println(x);x가 자유 변수이므로 컴파일 시간 오류가 발생한다.

자바와 C의 한 가지 차이점은 자바가 가리기를 허용하지 않는다는 것이다.

지역 변수의 이름 v가 v의 영역 안에서 새로운 변수를 정의하는 데 사용되었다면 컴파일 시간 오류이다.12

자바는 한 변수의 영역 안에서 같은 이름의 변수를 또 정의하는 것을 아예 허용하지 않는다.

코드

class Example {
    public static void main(String[] args) {
        int x = 1;
        {
            int x = 2;
            System.out.println(x);
        }
        System.out.println(x);
    }
}

출력

error: variable x is already defined in method main(String[])

가리기가 허용되지 않으므로 컴파일 시간 오류가 발생한다. 가리기가 불가능한 것이 불편한 경우도 있겠지만 실수를 줄이는 데 도움이 되기도 한다. 만약 위 코드에서 첫 번째 System.out.println(x);의 의도가 int x = 1;에서 정의한 x의 값을 출력하는 것이었다면, 위 프로그램은 잘못 작성한 프로그램이다. 가리기가 허용되었다면 잘못 작성했다는 사실을 모르고 지나칠 수 있겠지만, 자바에서는 가리기를 시도하면 컴파일 시간 오류가 발생하므로 개발 중에 잘못 작성했음을 깨닫고 코드를 수정할 수 있다. 이처럼 가리기를 못하게 하는 것은, 반복문이나 조건문 등에서 변수를 정의해서 바깥에 있는 같은 이름의 변수를 가린 뒤, 그 사실을 잊은 채 바깥에 있는 변수를 사용하려는 코드를 작성하여 의도와 다른 변수를 사용하는 실수를 막기 위함이다.

코드

class Example {
    public static void main(String[] args) {
        System.out.println(x);
        int x = 1;
    }
}

출력

error: cannot find symbol
symbol: variable x

변수를 정의하기 전에 사용하는 것도 불가능하다.

자바와 C의 또 다른 차이점은 자바가 변수의 값을 정의하지 않고 사용하는 것을 허용하지 않는다는 것이다.

지역 변수는 사용되기 전에 그 값이 명시적으로 주어져야 한다.13

코드

class Example {
    public static void main(String[] args) {
        int x;
        System.out.println(x);
    }
}

출력

error: variable x might not have been initialized

x의 값을 정의하지 않은 채로 x를 사용하자 컴파일 시간 오류가 발생한 것을 볼 수 있다. 자바스크립트처럼 임의의 값으로 변수를 자동 초기화하거나 C처럼 정의되지 않은 동작을 허용하면 프로그래머가 변수의 값을 정의하지 않았다는 사실을 알아내기 어렵다. 그러나 자바는 변수의 값을 정의하지 않고 사용했을 때 무조건 오류를 발생시킴으로써 프로그래머가 이를 개발 중에 확인하고 수정할 수 있게 한다.

코드

class Example {
    public static void main(String[] args) {
        int x = (x = 1);
        System.out.println(x);
    }
}

출력

1

자바 역시 재귀적인 변수 정의를 허용한다. 그러나 정의되지 않은 변수를 사용하는지 엄격히 검사하다 보니 흥미로운 재귀적인 변수를 만들기는 쉽지 않다.

지금까지 파이썬, 자바스크립트, C, 자바의 변수의 의미를 살펴보았다. 이번 장에서 변수에 관해 논의한 내용들이 실제 언어의 예시와 함께 잘 정리되었기를 바란다. 다음 장으로 가기 전에 실제 언어를 살펴본 것으로부터 생각할 수 있는 점 몇 가지를 짚고 넘어가려 한다.

첫째로, 산술x의 변수와 다른 언어들의 변수 사이의 차이에 대한 것이다. 파이썬, 자바스크립트, C, 자바 모두 산술x와는 다른 변수의 의미를 가진다. 파이썬과 자바스크립트는 변수의 영역부터 아예 다르게 지정한다. 또, 네 언어 모두 블록이라는 개념을 가지고 있고 자바스크립트, C, 자바는 블록의 개념을 변수의 영역을 정하는 데 사용한다. 블록은 분명 산술x에 없는 개념이다. 그 외에도, 산술x는 재귀적인 변수 정의를 허용하지 않지만 나머지 언어는 모두 재귀적인 변수 정의를 허용하는 등, 크고 작은 여러 차이점이 있다. 이렇게 실제 언어에서 변수의 의미와 차이가 날 것이면 굳이 산술x를 왜 정의했는지 의문인 사람도 있을 것이다.

일단, 산술x와 비슷하게 변수의 의미를 정의하는 언어가 없는 것은 아니다. 리스프(LISP)와 오캐멀 같은 함수형 언어에서 변수의 의미는 산술x와 거의 같다. 더 정확하게 말하자면, 산술x의 변수의 의미가 리스프와 오캐멀로부터 유래했다고 할 수 있다. 물론 이는 속 시원한 대답은 아니다. 많이 사용하는 언어인 파이썬, 자바스크립트, C, 자바 같은 언어를 굳이 놓아두고 상대적으로 비주류 언어인 리스프나 오캐멀 같은 함수형 언어의 의미를 가져온 것이 이상해 보인다.

이 책에서 산술x의 의미를 이렇게 정의한 이유는 변수의 핵심만을 간단하고 명료하게 드러내기 위함이다. 그리고, 함수형 언어가 대개 변수의 핵심을 잘 드러내는 의미를 가지고 있을 뿐이다. 예를 들어, 블록 기반 영역을 산술x에 넣으려고 했다면, 우선 블록을 잘 정의하기 위해 노력을 들여야 했을 것이다. 이는 언어를 더 복잡하게 할 뿐 아니라 블록이 변수를 정의하는 데 있어서 필수적인 개념처럼 보이게 만드는 문제가 있다. 그러나, 앞에서 본 것처럼 변수의 핵심은 블록이 아니다. 변수를 정의하는 것과 사용하는 것, 그 두 가지가 변수를 이해할 때 가장 중요한 개념이다. 그 이외의 것들은 어디까지나 곁가지일 뿐이다. 재귀적 정의를 허용하지 않은 것도 같은 맥락에서 이해할 수 있다. 재귀는 분명히 매우 중요한 개념이지만, 변수와는 구분되는 별개의 개념이고, 잘 정의되려면 의미가 더 복잡해져야 한다. 따라서 처음부터 재귀적 변수를 언어에 넣기보다는, 우선 변수를 잘 정의한 것이다. 앞으로 언어를 조금씩 확장하는 과정 중에 재귀를 추가할 것이다.

이렇게 변수의 핵심만을 잘 요약하여 이해하는 것은 실제 언어에서 변수의 의미를 이해하는 데 도움이 된다. 비록 실제 언어의 변수가 조금 다른 의미를 가질지언정, 산술x에서 배운 개념을 충분히 적용할 수 있다. 이번 절에서 이미 우리는 파이썬, 자바스크립트, C, 자바의 변수를 산술x를 통해 알아본 개념인 변수 정의와 사용, 묶는 등장과 묶인 등장, 자유 변수, 영역 등을 통해 이해했다. 이처럼, 한 번 핵심을 깨닫고 나면 여러 언어의 의미를 쉽게 이해할 수 있다. 앞으로도 책에서 정의하는 언어의 의미가 실제 언어의 의미와는 다소 차이가 있을 것이다. 이는 언어에 추가하는 기능의 핵심만을 잘 드러내어 기능을 이해하기 쉽게 만들고 여러 언어에 일반적으로 적용될 수 있게 하기 위함이다.

또 다른 짚고 넘어갈 점은, 언어의 명세를 읽는 것의 중요함이다. 널리 사용되는 대부분의 언어는 언어를 사용하는 프로그래머가 언어의 의미를 파악하는 데 문제가 없도록 명세를 자세하게 작성해 놓았다. 따라서, 언어의 의미가 궁금하다면 명세를 보는 것이 가장 확실한 해결책이다. 간단하고 많이 사용되는 개념에 관해 궁금하다면 명세를 반드시 볼 필요는 없다. 명세가 아니더라도 다양한 책이나 온라인 자료에서 설명을 찾을 수 있기 때문이다. 그러나 복잡하고 어려운 개념을 이해할 때는 명세가 큰 도움이 된다. 명세가 아닌 자료는 그 정확성을 완전히 신뢰할 수 없다. 특히, 어려운 개념일수록 자료의 내용에 잘못된 부분이 있을 가능성이 커진다. 또, 사람들이 자주 사용하지 않는 개념일수록 자료의 양 자체도 부족하다. 이럴 때 가장 믿을 수 있고 확실하게 존재하는 참고 자료는 언어의 명세이다. 언어의 명세야 말로 그 언어를 정의한 것이니 틀린 경우가 거의 없고 대부분 온라인에서 쉽게 구할 수 있다. 이 책에서 실제 언어에 대해 설명할 때는 가급적 언어 명세에서 발췌하는 이유이기도 하다. 명세의 내용을 그대로 보여줌으로써 언어의 의미를 정확하게 전달할 수 있다.

한 가지 문제는 언어의 명세가 읽기 쉬운 글은 아니라는 것이다. 여러 명세가 예시 코드까지 넣어가며 자세한 설명을 제공하지만, 기본적으로 어려운 용어가 여럿 등장하며 분량이 매우 긴 경우가 많다. 초심자가 언어의 명세를 읽으려 하면 어디를 읽어야 할지 찾기도 힘들고 처음 보는 용어가 잔뜩 있어 어렵게 느껴질 수 있다. 따라서 언어 명세를 읽는 데도 연습이 필요하다. 이 책에서 발췌한 부분을 찾아서 읽어 보는 것도 좋은 연습이 될 수 있다. 책에서는 필요에 따라 한두 문장 정도만 발췌하니, 명세에서 앞뒤 문단을 같이 읽어 보면 발췌한 내용을 더 정확하게 이해하는 데 도움이 된다. 또, 그렇게 명세를 조금씩 읽다 보면 차츰차츰 명세의 전체 구조가 눈에 들어오면서 원하는 내용을 찾으려면 어디를 보아야 할지 감이 생길 것이다.

지금까지 변수에 대해 알아보았다. 변수 정의와 변수 사용을 언어에 추가했으며 변수의 의미를 표현하기 위해 환경이라는 새로운 개념을 도입했다. 또, 모든 산술 식이 결과를 내는 것과 달리, 산술x에서는 자유 변수로 인해서 결과를 내지 못하고 오류를 발생시키는 식이 존재함도 보았다. 변수는 값에 이름을 붙이고 여러 번 사용할 수 있게 함으로써 잘 사용하면 프로그램의 성능에 도움을 주고 코드를 읽기 쉽게 만든다. 다음 장의 주제는 함수이다. 4장과 5장, 두 장에 걸쳐 함수에 관해 볼 것이다. 변수가 단순히 값에 이름을 붙였다면, 함수는 여러 식을 요약해 한 번에 표현할 수 있게 하는 훨씬 강력한 기능이다. 함수는 프로그래밍 언어에서 가장 강력하고 중요한 기능인 만큼 산술x에 함수를 추가하면 더 언어다운 언어가 만들어질 것이다.


  1. If a local variable is defined in a function block, the scope extends to any blocks contained within the defining one. (https://docs.python.org/3.9/reference/executionmodel.html#resolution-of-names)↩︎

  2. 사실 파이썬 명세에 “대입 문은 값을 이름에 (다시) 묶기 위해 사용된다[assignment statements are used to (re)bind names to values]”고 나와 있어, x = e 꼴의 식이 나올 때마다 변수가 새롭게 정의되는 것이라고 볼 여지도 있다. 그러나, 아래 프로그램이 2를 출력하는 점을 고려할 때, 변수를 매 번 새로 정의하는 것이라고 하기보다는 이미 정의된 변수의 값이 수정되는 것이라 이해하는 편이 자연스럽다. def main(): x = 1 def f(): print(x) x = 2 f() main() x = 2에서 x가 새롭게 정의된다고 이해하면 함수 fprint(x)2를 출력하는 이유를 잘 설명하기가 어렵다. 그러나 이미 정의된 x의 값이 수정되었다고 이해하면 print(x)2를 출력하는 것이 쉽게 설명된다.↩︎

  3. If the name refers to a local variable that has not yet been bound to a value at the point where the name is used, an UnboundLocalError exception is raised. (https://docs.python.org/3.9/reference/executionmodel.html#resolution-of-names)↩︎

  4. A var statement declares variables that are scoped to the running execution context’s VariableEnvironment. Within the scope of any VariableEnvironment a common BindingIdentifier may appear in more than one VariableDeclaration but those declarations collectively define only one variable. (https://www.ecma-international.org/ecma-262/11.0/index.html#sec-variable-statement)↩︎

  5. Var variables are created when their containing Lexical Environment is instantiated and are initialized to undefined when created. (https://www.ecma-international.org/ecma-262/11.0/index.html#sec-variable-statement)↩︎

  6. let and const declarations define variables that are scoped to the running execution context’s LexicalEnvironment. (https://www.ecma-international.org/ecma-262/11.0/index.html#sec-variable-statement)↩︎

  7. It is a Syntax Error if the BoundNames of BindingList contains any duplicate entries. (https://www.ecma-international.org/ecma-262/11.0/index.html#sec-let-and-const-declarations-static-semantics-early-errors)↩︎

  8. If the declarator that declares the identifier appears inside a block, the identifier has block scope, which terminates at the end of the associated block. Two identifiers have the same scope if and only if their scopes terminate at the same point. (ISO/IEC 9899:2017, 6.2.1 Scopes of identifiers)↩︎

  9. There shall be no more than one declaration of the identifier with the same scope. (ISO/IEC 9899:2017, 6.7 Declarations)↩︎

  10. if an lvalue does not designate an object when it is evaluated, the behavior is undefined. (ISO/IEC 9899:2017, 6.3.2.1 Lvalues, arrays, and function designators)↩︎

  11. The scope of a local variable declaration in a block is the rest of the block in which the declaration appears. (https://docs.oracle.com/javase/specs/jls/se14/html/jls-6.html#jls-6.3)↩︎

  12. It is a compile-time error if the name of a local variable v is used to declare a new variable within the scope of v. (https://docs.oracle.com/javase/specs/jls/se14/html/jls-6.html#jls-6.4)↩︎

  13. A local variable must be explicitly given a value before it is used. (https://docs.oracle.com/javase/specs/jls/se14/html/jls-4.html#jls-4.12.5)↩︎