클로저 – 03.PHP의 클로저 구현

클로저 객체 생성

PHP의 클로저는 익명 함수와 화살표 함수로 구현됩니다. 익명 함수와 화살표 함수는 모두 함수의 구문식을 사용하여 표현되지만, PHP는 이 구문식을 클로저 객체를 생성하는 구문으로 컴파일합니다.

객체 생성 구문식 및 생성 과정

클로저 객체를 생성할 때 외부 환경의 변수를 클로저에 등록하고, 해당 클로저 $this 객체에 대한 참조를 만든 후 반환합니다.

클래스에서 객체를 생성할 때 new 키워드를 사용하는 것처럼 클로저 객체를 생성하기 위해서는 익명 함수 또는 화살표 함수를 사용합니다.

클래스 멤버를 호출하기 위하여 객체변수에 객체 연산자(->)를 붙여 호출하는 것처럼 클로저를 호출하기 위해서는 객체변수에 괄호를 붙여 호출합니다.

객체 멤버 또는 클로저 호출 구문식

PHP는 클로저 객체를 생성하기 위해 Closure 클래스를 추가하였습니다. 클래스 이름 Closure가 클로저를 저장하기 위해 PHP 엔진에 예약되어 있기 때문에 개발자는 Closure 이름을 가진 클래스를 정의하여 사용할 수 없습니다.

아래는 익명 함수와 화살표 함수로 작성된 클로저 객체입니다. 작성할 때 익명 함수와 화살표 함수 문장 끝에 세미콜론(;)을 붙여야 합니다. 생략하면 컴파일 오류가 발생합니다.

  • $foo = function () { return ‘Hello, world’; };
  • $foo = fn () => ‘Hello, world’;

생성된 클로저 객체는 객체변수 $foo에 할당되어 사용되며, 클로저는 객체변수 $foo에 괄호를 붙여 호출됩니다. 익명 함수는 PHP 5.3부터 지원되며, 화살표 함수(arrow function) ‘=>’는 PHP 7.4부터 지원됩니다.

화살표 함수는 fn 키워드로 정의 할 수 있으며, function 키워드와의 차이점은, 화살표 함수는 단일행으로만 사용되며, return, use와 같은 키워드를 사용하지 않아도 된다는 것입니다.

화살표 함수의 스코프 체인

JavaScript와 같은 다른 언어에서는 가장 안쪽에 있는 내부 함수 스코프부터 전역 스코프까지 순서대로 연결리스트 형식으로 관리하는 스코프 체인 메커니즘이 있습니다. 이 메커니즘에 따른다면, 가장 먼저 클로저의 지역 변수를 참조하게 되며, 없으면 상위 함수의 지역 변수, 없으면 최종적으로 전역 변수를 참조하게 됩니다.

다른 언어에서의 스코프 체인

PHP의 일반적인 스코프 사용 규칙을 보면, 함수를 둘러 싸고 있는 외부 스코프에 접근할 때는 명시적인 방법으로 접근 방법을 지정해 주어야 합니다.

예를 들어 함수 또는 클래스 메소드에서 전역 변수를 사용하기 위해서는 global 키워드로 명시적으로 선언해야 하고, 클로저에서 외부 환경의 변수를 사용하기 위해서는 use 절에 해당 변수를 명시적으로 지정하여야 합니다.

익명 함수로 정의된 클로저의 경우에는 PHP의 이러한 스코프 사용 규칙에 따라 global 키워드, use 절을 통해 해당 변수를 지정해 줍니다.

그러나 화살표 함수의 경우에는 명시적으로 지정하지 않더라도 외부 환경의 변수를 자동으로 스캔하여 사용할 수 있습니다. 단 전역 변수는 자동으로 인식하지 못하며 global 키워드로 지정하여야 합니다.

PHP 화살표 함수에서의 스코프 체인

편리성을 중시하는 PHP 입장에서 받아들인 예외 규칙(?)으로 보이지만 다른 언어와 같이 전역 스코프까지 연결시키지는 않았습니다. 반쪽 짜리 스코프 체인이라고 할 수 있습니다.

클로저를 클래스 메소드에 정의할 때도 예외 규칙(?)은 발생하는데, 해당 클래스의 객체가 별다른 명시 없이 클로저에 자동으로 바인딩 되어 클로저 내에서 $this를 사용할 수 있습니다.

클로저 수명(Closure lifetime)

지역 변수의 수명

프로그램이 실행될 때 변수들이 저장되는 메모리 영역은 아래와 같습니다.

메모리 공간

  • 코드(code) 영역 – 코드 영역은 실행할 프로그램의 코드(함수, 제어문 등)가 저장되는 영역입니다.
  • 데이터(data) 영역 – 데이터 영역은 프로그램의 전역 변수와 정적 변수가 저장되는 영역입니다. 데이터 영역은 프로그램의 시작과 함께 할당되며, 프로그램이 종료되면 소멸합니다.
  • 힙(heap) 영역 – 힙 영역은 프로그래머가 필요에 따라 사용하는 메모리 영역입니다. 힙 영역은 사용자에 의해 메모리 공간이 동적으로 할당되고 해제됩니다.
  • 스택(stack) 영역 – 스택 영역은 함수의 호출시 생성되는 지역 변수와 매개변수가 저장됩니다. 스택 영역은 함수의 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸합니다.

스택 영역에 저장된 지역 변수와 매개변수는 함수 호출이 완료되면(return) 할당된 메모리가 반환 되어 소멸되기 때문에 접근할 수 없습니다.

클로저의 수명

그러나 함수 내에 정의된 클로저는 외부 함수 호출이 완료되더라도 소멸되지 않고 클로저를 정의한 외부 함수보다 더 오래 존속될 수 있습니다.

클로저 객체를 반환함으로 반환된 객체가 소멸할 때까지 클로저의 수명은 연장됩니다.

클로저에 참조로 등록된 외부 변수는 외부 함수가 소멸되더라도 클로저가 소멸될 때까지 없어지지 않고 메모리에 존속합니다. 클로저가 다시 호출될 때까지 그 값을 유지할 수 있으며, 외부 함수 스코프에 있는 지역 변수를 클로저에 의해 수정할 수 있습니다.

객체에서의 클로저 동작

클로저의 객체 바인딩은 2009년 6월 30일 PHP 5.3.0이 발표되기 전부터 일부 알파 릴리즈에 존재하였습니다. 그러나 객체 바인딩의 비헤이비어(behaviour)를 구현하기에는 해결하기 곤란한(합의하지 못한) 문제들이 있어서 2008년 1월 28일(?) 베타1이 릴리즈 될 때 객체 바인딩과 관련된 기능들이 전부 삭제*1된 채 발표되었습니다.

*1 PHP RFC 문서(2009.1.28 이전) – Removal of $this in closures

이러한 객체 바인딩과 관련된 문제들에 대한 해결 방안으로 2009년 1월 22일 새롭게 제안*2되었으며 이 제안이 받아들여져서 논의 끝에 2010년 4월 경 현재와 같은 객체 바인딩의 개념이 정립되어 2012년 3월 1일 PHP 5.4.0에 정식으로 포함되어 발표되기에 이르렀습니다.

*2 PHP RFC 문서(2009.1.22, 2010.8.10) – Closures: Object extension

$this 자동 바인딩

객체 내부에 정의된 클로저는 명시적으로 가져올 필요 없이 자동적으로 $this와 해당 클래스의 private 및 protected 멤버를 포함한 현재 객체에 대한 전체 접근 권한을 갖습니다. 이것은 중첩된 클로저에도 적용됩니다.

클래스 메소드 내에 클로저를 정의한다고 해서 클래스의 의미 체계(semantics)가 바뀌지는 않습니다. 이와 같이 클로저는 외부의 의미 체계(semantics)에 영향을 주지 않고 전역 스코프, 함수 또는 클래스 메소드 내에 정의할 수 있습니다.

클래스 메소드에 정의된 클로저는 $this를 통해 해당 클래스와 객체에 접근할 수 있습니다. $this가 클로저 내에 저장되기 때문에 해당 객체는 적어도 클로저가 살아있는 동안에는 계속 존속됩니다.

정적 클로저(static closure)

클래스 메소드에 정의된 클로저에서 $this를 사용하지 않을 때는 클로저를 정적(static)으로 선언할 수 있습니다. 정적으로 선언되면 클로저 내에서 $this를 사용할 수 없습니다.

해당 클래스의 $this가 필요하지 않은 경우와 런타임에 객체를 클로저에 바인딩할 필요가 없는 경우에 정적 클로저를 사용합니다.

  • 메소드 내에서 정의된 클로저에 대한 클래스의 자동 바인딩을 방지한다.
  • 런타임에 객체가 클로저에 바인딩 되는 것을 방지한다.

정적 클로저를 사용함으로 코드를 최적화 하고, 메모리를 절약하며, 실행 시간을 개선할 수 있습니다.

$this 자동 바인딩 방지

클로저가 클래스 메소드에서 정의되면 해당 클래스가 자동으로 바인딩 되어 클로저가 $this를 사용할 수 있습니다. 그러나 클로저를 정적으로 선언하게 되면 해당 클래스가 바인딩 되지 않습니다.

이에 따라 정적 클로저 내에서 $this를 사용하려고 하면 오류가 발생합니다.

객체 런타임 바인딩 방지

클로저를 static로 지정하면 객체는 런타임에 바인딩 되지 않을 수 있습니다. 따라서 아래와 같이 Closure 클래스의 멤버인 bindTo 메소드로 정적 클로저에 객체를 바인딩 하려고 하면 오류가 발생합니다.

성능 향상(improving performance)

비정적(non-static)으로 정의된 클로저는 클로저 내에 저장되는 해당 객체를 더 오래 존속시키게 되며, 이로 인해 더 많은 메모리를 사용하게 됩니다.

반면 정적으로 선언되면 클로저 내에 객체를 가져올 필요가 없기 때문에 메모리를 절약할 수 있습니다. 따라서 가능한 모든 클로저를 정적 클로저로 변경하게 된다면 그만큼 더 많은 메모리를 절약할 수 있습니다.

실행 조건에 따라 결과 수치는 달라질 수 있겠지만, 비정적(non-static)과 정적(static) 클로저의 출력 결과를 비교해 보면 실행 시간과 메모리 소모가 비정적 클로저에서 매우 크게 나타나는 것을 볼 수 있습니다.

클래스 Foo 객체의 동적 메모리 크기가 100KB이며, 비정적 클로저가 실행될 때마다 해당 객체가 클로저로 전달되며, 클로저가 소멸되기 전까지 해당 객체가 메모리에 잔존하고 있음을 알 수 있습니다.

정적 클로저는 클래스 Foo의 객체를 저장하고 있지 않으므로 아무리 많은 정적 클로저가 저장된 클로저 객체를 보유하더라도 클래스 Foo의 객체를 위한 동적 메모리 소모가 전혀 없음을 알 수 있습니다.

전역 스코프의 정적 클로저

클래스 메소드는 static을 선택적으로 지정할 수 있으나 함수는 static을 지정할 수 없습니다.

PHP 5.4.0에서 정적 클로저를 지원하면서 함수의 구문을 가지고 있는 클로저에 static을 지정할 수 있도록 하였습니다.

정적 클로저가 클래스 메소드 내에 정의될 때는 유용할 수 있습니다. 그러나 전역 스코프(global scope) 또는 함수 스코프(local scope)에 정의된 클로저는 static 지정의 유무가 클로저 기능과 성능에 별 영향을 끼치지 않는 것으로 보입니다.

아래는 전역 스코프에 정의된 클로저가 정적일 때와 아닐 때의 성능(실행 시간, 메모리 소요)을 비교해 보기 위한 예제입니다. 결과를 보면 서버 성능 등 실행 조건에 따라 수치의 차이는 있겠으나 정적(static)과 비정적(non-static) 사이의 성능 차이가 거의 없음을 보여 주고 있습니다.

객체가 아닌 클로저의 외부 변수(전역 변수, 외부 함수 변수)는 정적(static) 지정에 따른 클로저의 성능(실행 시간, 메모리 소모) 향상과는 무관함을 알 수 있습니다.

PHP 5.4.0 부터 클로저를 특정 객체와 클래스 스코프에 바인딩하는 메소드가 추가되었습니다. 이에 따라 전역 스코프에 정의된 정적 클로저를 자주 볼 수 있습니다.

그러나 이러한 정적 클로저는 Closure::bind 메소드에 의해 특정 클래스에 정적으로 바인딩 됩니다.

정적 클로저가 비록 전역 스코프에 정의되어 있지만 클로저 객체가 실제로 사용될 때는 전역 스코프에서 사용되는 것이 아니라 클래스에 정적으로 바인딩 되어 클래스 스코프에서 사용되고 있는 것입니다.

정적 클로저는 클래스에서 사용하기 위해 개발된 것으로 보이며, 설사 전역 스코프와 함수 스코프 내에도 정의할 수 있다 하더라도 클래스 외의 스코프에서 정적 클로저를 사용하는 것은 별 이득이 없다고 할 수 있습니다.

답글 남기기