전역 변수/지역 변수

C/C++ 프로그램에 익숙한 분들은 변수를 크게 전역 변수/지역 변수로 구분하는데 익숙하실 겁니다. 하지만 클로저를 지원하는 JavaScript에서는 함수 안에 함수를 중첩해서 많이 선언하기 때문에 지역 변수의 스펙트럼이 다양해집니다.

예를 들어, JavaScript에서는 전역 네임스페이스를 오염시키지 않기 위해 최상단에 익명 함수를 하나 정의하고 곧바로 호출하는 용법을 많이 사용합니다.

(function () {
    var x = 1;

    function foo() {
        return x;
    }

    x++;

    function bar() {
        return x;
    }

    console.log(foo());
    // 2
    console.log(bar());
    // 2
})();

여기서 변수 x는 엄밀한 의미에서 익명 함수의 지역 변수이지만, foo()bar() 함수 입장에서 봤을 때는 두 함수가 서로 공유하는 전역 변수가 됩니다. foo() 함수에서 x가 1일때 값을 캡춰했지만, x++이 실행되었기 때문에 foo() 함수는 2를 리턴합니다.

이번에는 foo() 함수를 다음과 같이 확장해 봅시다. 여기서 변수 yfoo() 함수의 지역 변수이지만, foo1(), foo2() 함수의 전역 변수가 됩니다.

    function foo() {
        var y = 2;

        function foo1() {
            return y;
        }

        y++;

        function foo2() {
            return y;
        }

        console.log(foo1());
        // 3
        console.log(foo2());
        // 3

        return x;
    }

즉, 클로저가 있는 언어에서는 전역 변수와 지역 변수의 구분이 모호해지는 것을 알 수 있습니다. 앞서 왜 변수가 나쁜가?라는 글에서 제기한 문제도 여기에 있습니다. 클로저가 캡춰한 변수 i는 루프 내의 모든 익명 함수가 공유하는 전역 변수가 되기 때문입니다.

function makeAdders(n) {
    var adders = [];
    for (var i = 0; i < n; i++) {
        adders.push(function (x) { return i + x; });
    }
    return adders;
}
 
var adders = makeAdders(10);
var add5 = adders[5];
var r = add5(2);
console.log(r);
// We expect 7, but 12 is printed.

참고로 프로그래밍 언어가 클로저가 캡춰하는 변수를 복사할 것인지, 공유할 것인지는 정답이 없습니다. 어느 쪽을 선택하든 임의의 선택이 되는 거죠. 실제로 프로그래밍 언어마다 방식이 다릅니다. 문제는 변수라는 개념이 클로저와 결합하여 프로그래밍 언어 자체를 복잡하게 만드는 요소가 되고, 개발자 입장에서는 프로그래밍 언어마다 다른 임의의 규칙을 기억하고 써야 하기 때문에 프로그래밍의 복잡도를 더 하는 요인이 됩니다.

Advertisements

6 thoughts on “전역 변수/지역 변수

  1. c++11 람다 펑션은 정의시 참조를 할지 복사를 할지 명확히 정합니다. 이 모호함의 문제가 클로저의 문제가 아니라, 언어가 c++같이 참조와 복사를 명확하게 구분해야 하는 문제 아닐까요?

    Liked by 1명

      • 트윗으로도 짧게 답변을 남겼는데 C++ 람다가 클로저와 같습니다. 다만 dshwang님 말씀처럼 i를 어떻게 읽을지 C++은 강제를 합니다. 람다가 정의 되는 주변 지역 변수가 어떻게 캡쳐될지를 표기 안 하고 i를 쓰면 컴파일 오류가 납니다. 위의 예를 C++로 옮긴다면 x, y를 그냥 묵시적으로 람다(클로저)에서 쓸 수 없고 반드시 얘가 값으로 읽힐지, 참조로 읽힐지 명시를 해야 합니다.

        좋아요

      • 선택할 수 있는 옵션이 있는 건 좋지만, 어쨌거나 참조로 지정할 수 있으면 해당 변수는 클로저 입장에서는 서로 공유하는 전역 변수가 되고, 클로저들 사이에 커플링이 존재하게 됩니다. A라는 클로저에서 값을 바꾸면 B 클로저의 행동에 영향을 미치는 상황 자체가 “전역 변수”의 문제점에서 지적한 내용과 일치합니다.

        좋아요

  2. 이전의 글들을 포함한 전체 맥락을 살펴보면,
    “변수는 클로저의 동작에 큰 영향을 미치기 때문에 변수의 이용은 좋지 않다”라는 주장이 있는 것 같습니다.
    하지만 거꾸로 생각해보면, “클로저는 변수의 동작에 큰 영향을 받기 때문에 클로저의 이용은 좋지 않다”라는 주장도 가능합니다.

    예를 들어, 글쓴이께서는 add5(2)가 12를 리턴하는 것과 같은 상황이 발생하기 때문에 변수는 좋지 않은 것이라고 하셨지만, add5(2)가 12를 리턴하므로 클로저는 좋지 않다 라는 주장도 무리가 없어 보입니다.

    따라서 광범위하게 “변수가 좋지 않다” 라고 주장하는 것 보다는, “클로저와 변수의 조합은 좋지 않다”라는 주장이 더 설득력있을 것이라고 생각합니다.

    좋아요

    • 변수를 포함해서 mutable state가 많은 코드는 이해하기 어렵고 수정하기도 어렵습니다. 많은 분들이 “전역 변수”는 해롭다고 생각하지만, “지역 변수”는 괜찮다고 생각하셔서, 클로저가 있는 언어는 전역 변수/지역 변수 구분 자체가 애매하다는 말씀을 드리기 위해 클로저를 예로 든 것일뿐, 변수(혹은 mutable state)는 항상 문제가 됩니다.

      또 다른 예로, 변수는 멀티쓰레드 프로그램과도 상성이 안 맞고, lazy evaluation (C# LINQ) 같이 실행 순서가 명확하지 않는 경우에도 문제를 일으킬 확률이 높습니다.

      프로그램이라는 게 요구사항에 따라 mutable state가 반드시 필요한데, 대안도 없이 변수 사용을 줄이라고만 이야기해서 많은 분들이 받아들이기가 힘들어하시는 것 같습니다. state를 잘 관리하기 위한 방법에는 클래스의 encapsulation처럼 관련된 state를 하나로 묶거나, 중첩 함수(nested function)이나 접근 제한자(acces modifier)를 사용해서 변수에 접근을 최소하는 것도 해당됩니다.

      좋아요

댓글이 닫혀있습니다.