클로저 – 02.클로저

람다 함수와 클로저(closure)

람다 함수를 사용하지 않더라도 람다 함수의 기능을 create_function()으로 수행할 수 있으며, 또한 기명 함수를 사용하여 콜백 함수를 정의하여 콜백 기능을 수행할 수 있습니다.

이러한 이유로 2007년 말 PHP에 (클로저 없이) 람다 함수를 추가하자는 의견이 제시되었을 때, 클로저를 지원하지 않으면 람다 함수를 PHP에 추가하는 것은 별로 유용하지 않다고 판단하여 2009년 6월 30일 PHP 5.3.0에 람다 함수를 확장한 클로저를 지원하기 시작하였습니다.

클로저(Closure)

클로저는 람다로부터 파생된 개념이지만, 첫째, 람다 함수가 적용 받는 변수 스코프를 넘어 렉시컬 스코프(lexical scope)를 가지며, 둘째, 람다 함수와 달리 상태를 유지(stateful)합니다.

클로저(Closure)

클로저는 클로저의 외부 렉시컬 스코프를 기억하여 클로저가 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 해줍니다. 즉, 클로저는 외부 함수의 실행이 종료되어 외부 함수가 소멸된 후에도 외부 함수의 지역 변수에 접근할 수 있습니다. 이러한 메커니즘을 클로저라 합니다.

변수 스코프(variable scope)

변수는 스크립트의 어느 곳에서도 선언될 수 있습니다. 변수 스코프(variable scope)는 변수가 정의된 위치에서 변수가 접근할 수 있는 스크립트의 유효 범위를 말합니다. PHP는 세가지 다른 변수 스코프를 갖습니다.

  • 전역 스코프(global scope)
  • 지역 스코프(local scope)
  • 정적 스코프(static scope)

변수 스코프(variable scope)

함수 내에서 사용되는 변수는 기본적으로 지역 스코프(local scope)로 제한되며, 함수 외부에서 사용되는 변수는 전역 스코프(global scope)를 가지게 됩니다.

지역 스코프(local scope)는 함수 내로 제한되므로 이를 지역 함수 스코프(local function scope) 또는 함수 스코프(function scope)라고도 합니다.

정적 변수는 지역 스코프에서만 존재하기 때문에 외부에서 접근할 수 없지만. 함수 실행이 종료되어 지역 스코프를 벗어나더라도 그 값을 잃지 않고 유지됩니다.

PHP의 스코프 개념은 다른 언어가 정의하는 스코프 개념과는 상당히 다릅니다. 그 중에 하나가 PHP에서는 함수 내부에서 전역 변수에 접근하려면 global 키워드를 명시적으로 사용해야 하지만, JavaScript, C++에서는 PHP와 달리 자동적으로 함수 내부에서 전역 변수에 접근할 수 있습니다.

따라서 이 문서에서 ‘접근할 수 있다’는 표현은 명시적인 방법을 포함하는 것 입니다.

일반 함수는 전역 스코프를 가지고 있기 때문에 스크립트 내 모든 곳에서 호출될 수 있습니다. 그러나 클로저는 익명 함수라서 변수에 바인딩되어 변수 스코프의 영향을 받으며 호출될 수 있습니다.

렉시컬 스코프(lexical scope)

PHP RFC 문서 ‘closures’를 살펴보면 렉시컬 변수(lexical variable)라는 용어가 자주 등장합니다. 이를 이해하기 위해서는 JavaScript에서 사용하고 있는 렉시컬 환경(lexical environment)를 이해할 필요가 있습니다.

클로저 $hello()는 전역 스코프에 정의할 수도 있고, 아래 예와 같이 함수 내에 정의할 수도 있습니다.

렉시컬 환경(스코프)

클로저에는 세가지 스코프가 있으며, 전역 스코프, 내부  및 외부 지역 스코프는 자신만의 환경을 가지며, 그 환경에는 자신에게 속한 지역 변수를 저장하고 있습니다.

  • 지역 스코프(inner local scope) – $i
  • 외부 함수 스코프(outer local scope) – $o1, $o2
  • 전역 스코프(global scope) – $g1, $g2, $g3

이러한 환경은 함수를 호출한 곳이 아니라 함수가 정의된 위치에 따라 결정됩니다. 클로저의 렉시컬 환경은 클로저가 정의된 위치에서 결정된 자신과 자신의 외부 환경을 통털어서 일컫는 말입니다.

외부 환경을 외부 렉시컬 환경이라고 하며, 자신의 렉시컬 환경보다 더 상위의 렉시컬 환경을 의미합니다. 전역 스코프의 스크립트는 최상위 렉시컬 환경입니다.

여기서 한가지 의문점이 생깁니다. 렉시컬 환경은 자신만의 환경이므로 클로저는 클로저 만의 렉시컬 환경이 있는 것이고 외부 렉시컬 환경은 클로저 렉시컬 환경과 분리되어 외부에 존재하는 것으로 이해가 되는데, 앞에서 클로저 렉시컬 환경은 클로즈 자신의 렉시컬 환경과 외부 렉시컬 환경을 통털어서 일컫는다고 하였습니다.

두 가지 설명 다 맞는 얘기입니다만, 클로저의 렉시컬 환경이 만들어질 때 클로저 렉시컬 환경 내에 외부 렉시컬 환경을 등록하는 프로퍼티가 생성됩니다. 즉 클로저 렉시컬 환경에 등록된 프로퍼티에 의해 외부 렉시컬 환경을 참조하게 됩니다.

따라서 클로저 렉시컬 환경이라고 할 때는 클로저 자신만의 렉시컬 환경을 의미할 수도 있고, 외부 렉시컬 환경까지 포함한 의미일 수도 있습니다. 때에 따라서는 외부 렉시컬 환경 만을 렉시컬 환경이라고 표현하기도 합니다. 글의 전후 문맥에 의해 어느 것을 의미하는지 살펴보아야 합니다.

  1. 렉시컬 환경 = 클로저 자신의 렉시컬 환경
  2. 렉시컬 환경 = 클로저 자신의 렉시컬 환경 + 외부 렉시컬 환경
  3. 렉시컬 환경 = 외부 렉시컬 환경

PHP에서 렉시컬 환경(스코프)을 설명할 때는 외부 렉시컬 환경(스코프)을 의미하며 클로저 자신만의 렉시컬 환경은 클로저의 지역 스코프로 설명하고 있습니다.

결국 PHP RFC 문서 ‘closures’에서 설명하는 렉시컬 변수(lexical variable)는 클로저의 외부 렉시컬 환경에 저장된 지역 변수를 의미합니다.

클로저 환경의 캡슐화

클로저가 생성될 때 클로저 자신의 환경에 외부 변수들을 등록하게 되며, 이후 등록된 외부 변수들은 특별한 인터페이스(참조 &)를 통하지 않는 이상 외부에서 클로저 내로 접근할 수 없으며,  외부 환경에 영향을 받지 않습니다.

closure 용어가 내포하는 의미와 같이 클로저 자체는 캡슐화 되어 사용되기 때문에 정보 은닉도 보다 용이하게 처리할 수 있습니다.

변수에 바인딩 되어 실행되는 클로저는 자신의 환경이 저장된 변수이며, 이 변수라는 캡슐을 타고 스크립트 내의 임의의 위치에서 호출될 수 있습니다.

이동만 하면 무슨 의미가 있겠습니까? 오히려 어디서나 실행할 수 있는 함수보다 불편합니다. 중요한 것은 캡슐 안에는 클로저가 정의된 위치에서 형성된 외부 환경을 고스란히 담고 있다는 것입니다. 즉 외부 환경인 createClosure 함수 내의 지역 변수들의 상태를 캡슐화 하여 이동할 수 있는 것입니다.

클로저가 정의된 함수가 아닌 다른 함수 world에서 클로저를 실행하였는데, world 함수가 아닌 다른 함수 createClosure 함수의 지역 변수 $env의 값을 출력하고 있습니다.

일반적인 변수 스코프에서는 있을 수 없는 일입니다. 그러나 클로저는 생성될 때 외부 환경을 기억하고 있기 때문에 가능합니다. 클로저 정의에서 $env를 복사가 아닌 참조로 등록한다면 다른 함수에서 createClosure 함수의 지역 변수를 변경할 수도 있습니다.

클로저 스코프(closure scope)

지역 스코프에서는 자신의 변수에 접근할 수 있으며, global 키워드를 명시적으로 지정하면 전역변수에 접근할 수 있습니다.

지역 스코프(local scope)

함수 내에서 사용되는 변수는 기본적으로 지역 스코프(local scope)로 제한되지만 클로저는 이러한 스코프의 제한을 넘어 외부 환경에 접근할 수 있습니다.

반대로 외부 변수를 클로저에 등록하여 캡슐화하게 되면 외부에서는 등록한 외부 변수를 참조(&)라는 인터페이스를 통하지 않고서는 접근하지 못합니다.

클로저의 복사 메커니즘

클로저가 생성될 때 외부 변수들을 복사하여 등록하게 되면, 이후 클로저는 완전히 밀폐(closure)되어 외부 환경에 영향을 받지 않습니다.

복사 대신에 참조(&)로 연결한다면 이것이 클로저와 외부 환경 간의 인터페이스 역할을 하여 클로저와 외부 환경 상호 간에 영향을 미칩니다. 그러나 이러한 인터페이스라는 출입문을 제외하고는 완전히 밀폐(closure)되어 있습니다.

클로저 스코프는 클로저가 생성될 때 그 외부 환경에 따라 결정됩니다. 클로저가 전역 스코프에 정의되었다면 클로저의 상위 스코프(parent scope)는 전역 스코프이며, 클로저 스코프는 그 외부 환경인 전역 스코프까지 확장됩니다.

전역 스코프에 정의된 클로저의 스코프 확장

클로저가 특정 함수 내에 정의되었다면 클로저의 상위 스코프(parent scope)는 특정 함수의 지역 스코프이며, 클로저 스코프는 그 외부 환경인 특정 함수의 지역 스코프와 전역 스코프까지 확장됩니다.

함수 내에 정의된 클로저의 스코프 확장

클로저의 부모 스코프는 클로저가 (호출이 절대 아닌) 선언된 함수의 스코프를 의미합니다.

클로저가 전역 스코프에 정의된 경우는 일반 함수와 비교할 때 스코프 측면에서는 별 차이점을 볼 수 없습니다. 일반 함수도 함수 내에 변수에 global을 지정하면 전역 변수에 접근할 수 있습니다.

그러나 상위 스코프에서 변수를 상속하는 것은 전역 변수를 사용하는 것과 동일하지 않습니다. 전역 변수는 전역 스코프에 존재하며 어떤 함수에서 실행되든 동일합니다.

더구나 함수 내에 정의된 경우의 클로저는 일반 함수가 완전히 구별됩니다.

중첩 함수(function within function)

PHP에서 위와 같이 스크립트에 중첩 함수(function within function, inner function, nested function)를 스크립트 상에 기술할 수 있도록 허용하고 있지만 기능적으로는 이를 구현하지 않고 내·외부 구분 없이 모든 함수는 전역 스코프를 가집니다.

따라서 중첩 함수로 기술되어 있다고 해도 중첩 함수로의 역할을 하지 않고 전역 함수로 전개되어, 내부 함수도 중첩 없는 지역 스코프를 가집니다. global 키워드를 명시적으로 지정하면 아래와 같이 전역 스코프에 접근할 수 있으나 외부 함수인 foo의 지역 스코프에는 접근할 수 없습니다.

함수 내에 정의된 내부 함수의 스코프 확장

외부 환경의 변수 등록

클로저는 전역 스코프까지 확장되는 외부 환경에 있는 변수들 중에 임의로 선택하여 클로저에 등록합니다. 외부 환경에서 가져올 변수는 클로저 함수 정의 구문식의 use 절에 지정됩니다.

클로저 생성할 때 외부 변수 복사

이와 같이 클로저는 상위 스코프의 변수를 상속 받을 수 있으나, PHP 7.1부터 superglobals , $this 또는 매개 변수와 동일한 이름의 변수를 use 절에 포함해서는 안됩니다.

PHP 8.0.0부터 use 절에 지정된 스코프 상속 변수 목록 끝에 쉼표가 포함될 수 있으나, 이는 컴파일할 때 무시됩니다.

그러나 PHP 7 이하 버전에서는 아래와 같은 오류가 발생합니다.

  • Parse error: syntax error, unexpected ‘)’, expecting ‘&’ or variable (T_VARIABLE)

상태 유지(stateful)

람다 함수는 변수에 바인딩되는 임시 함수이므로 지역 스코프를 가지며 따라서 상태를 유지하지 않습니다.

일반적으로 함수 안의 지역 변수들은 그 함수가 실행되는 동안에만 존재하기 때문에 실행이 종료되면 그 함수의 지역 변수는 소멸되어 접근할 수 없습니다.

상태 등록(state registration)

그러나 클로저는 클로저가 생성될 때의 외부 환경을 기억하고, 호출할 때마다 기억하고 있는 외부 환경에 따라 실행됩니다.

이를 이해하기 위하여 위의 예제를 클로저가 생성된 직후의 상태로 의미 상 변환 시켜 보겠습니다.

클로저가 생성될 때의 외부 환경의 변수 $sep, $wor 변수가 그 상태(값)를 그대로 가지고 클로저에 복사 되었음을 알 수 있습니다. 따라서 클로저를 호출할 때마다 클로저는 생성된 상태를 그대로 유지할 수 있게 됩니다.

클로저를 생성할 때 use 절에 등록된 외부 변수의 상태를 그대로 클로저의 환경에 등록하게 되며, 이는 마치 클로저에 복사된 변수의 초기 상태와 같은 효과를 가져오게 됨으로 클로저를 호출할 때마다 클로저를 생성할 때의 외부 환경을 가지고 실행되는 것입니다.

또한 클로저 생성할 때의 클로저 환경의 모든 변수(외부 변수 복사본, 클로저 자체 변수)는 지역 스코프의 적용을 받음으로 static으로 정적 변수로 지정하지 않는 이상은 항상 초기화 상태에서 실행됩니다.

자체 환경으로만 평가되는 함수

복사에 의한 외부 변수 등록

클로저는 캡슐화되어 자체 환경 내에서 평가되어 수행되는 함수이며, 클로저가 생성될 때 외부 변수가 복사로 클로저에 등록되면 해당 외부 변수는 외부 환경의 영향을 받지 않습니다. use 절에 지정된 외부 변수는 기본적으로 복사*에 의해 클로저에 등록됩니다.

* PHP에서 적용되는 원칙이며, 언어에 따라 복사가 아니라 외부 변수에 직접 접근합니다.

클로저가 생성된 이후에 전역 스코프에 있는 $sep, 외부 함수에 있는 $wor 변수의 값을 변경시켰지만 클로저의 $sep, $wor 복사본의 값은 클로저 생성할 때의 상태(값)을 유지하고 있습니다.

외부 변수의 값이 클로저 내외에서 변경되더라도, 클로저가 생성될 때의 외부 환경을 기억하여 클로저가 호출될 때마다 등록된 외부 변수의 상태에 따라 실행됩니다.

참조에 의한 외부 변수 등록

위 카운터 예제는 클로저가 호출될 때마다 클로저가 생성될 때의 외부 환경에 따라 항상 상태 등록된 초기 상태로 호출되기 때문에 카운터로서의 기능을 수행하지 못합니다.

복사(copy) 대신에 참조(reference)로 연결한다면 클로저와 외부 환경 상호 간에 영향을 미치기 되며 클로저와 외부 환경 간에 상호 작용할 수 있습니다.

클로저 생성할 때 외부 변수 참조(&)

일반적인 변수 스코프에 적용을 받으면 클로저의 상위 함수인 myCount에 있는 변수 $parent는 $c=myCount(); 행을 실행된 후 소멸되며, 이후 클로저 $c($i);이 실행될 때는 상위 스코프에 있는 변수 $parent에 접근할 수 없는 것이 정상입니다.

그러나 클로저에서는 상위 함수가 종료된 이후에도 그 변수에 정상적으로 접근하고 있으며, 그 값도 이전 호출에서 변경된 최신 상태를 유지하고 있습니다.

외부 환경에 존재하는 클로저에 참조(&)로 등록된 외부 변수의 경우에는 외부 환경이 소멸되더라도 클로저가 소멸될 때까지 없어지지 않고 메모리에 존재하며, 그 값이 초기화되지 않고 변경된 값을 유지합니다.

JavaScript에서와 같이 참조를 지원함으로 클로저를 온전히 구현할 수 있게 되지만, 외부 함수의 지역 변수를 클로저에 의해 수정될 수 있다는 것은 원치 않는 문제가 발생할 수 있다는 개연성이 상존하기 때문에, PHP에서는 기본적으로 복사에 의해 외부 변수를 등록하여 사용하는 것이 바람직하다는 것에 유의할 필요가 있습니다.

외부 환경을 기억한다

이러한 클로저의 상태 유지 특성에 따라 복사와 참조 모두 클로저가 생성될 때 외부 환경의 상태를 초기 값으로 클로저에 저장된다는 것은 동일하나, 복사는 호출할 때마다 외부 환경의 상태, 즉 초기 값으로 시작되지만, 참조는 초기 값을 클로저 내, 외부에서 변경할 수 있으며, 다음 호출에서는 변경된 최신의 값을 가지고 시작된다는 것입니다.

결국 클로저가 ‘외부 환경을 기억한다’, ‘만들어진 환경을 기억한다’, ‘원래의 환경에 따라 호출된다’라는 것은 클로저가 생성될 때 외부 환경에 있는 지역 변수의 상태가 클로저의 초기 상태로 등록(복사 또는 참조)하게 된다는 의미입니다.

앞 ‘클로저 환경의 캡슐화’에서 보았던 예제를 다시 보게 되면 클로저가 어떻게 외부 환경을 기억하고 있는지 잘 알 수 있습니다.

외부 환경 기억

클로저 $closure()가 정의된 위치인 myCount 함수 내가 아닌 다른 함수 otherCount에서 실행되고 있지만 클로저는 생성될 때의 외부 변수의 상태, 즉 상위 스코프의 지역 변수 $count가 참조(&)로 연결되었으며 그 값이 1인 외부 환경을 기억하여 이 환경에 따라 실행되고 있음을 알 수 있습니다.

외부 함수가 종료되어 먼저 소멸되더라도 클로저에 등록된 외부 함수의 지역 변수는 소멸되지 않으며, 외부 함수 밖에서 내부에 있는 클로저를 호출하더라도 외부 함수의 지역 변수에 접근 할 수 있습니다. 클로저는 자신이 생성될 때의 환경(lexical environment)을 기억하는 함수입니다.

답글 남기기