클로저 – 06.내장 클래스 ‘Closure’

PHP의 클로저가 함수로 표현된 익명 함수와 화살표 함수로 구현될 때,  내장된 Closure라는 클래스를 이용하여 클로저 객체를 생성합니다. 익명 함수와 화살표 함수로 구현되는 클로저는 내장 클래스 Closure의 객체입니다.

클래스의 구성

PHP 5.4부터 지원되는 bind 및 bindTo 메소드를 이용하면 지정된 클로저를 객체에 바인딩 한 후 새로운 클로저를 생성하여 반환 받을 수 있습니다. bind는 bindTo 메소드의 정적 버전입니다.

PHP 7.0부터 지원되는 call 메소드를 이용하면 새로운 클로저를 반환할 필요 없이 간편하게 다른 객체에 바인딩 한 후 바로 매개변수와 함께 호출하여 그 결과 값을 반환 받을 수 있습니다.

PHP 7.1부터 지원되는 fromCallable 정적 메소드를 이용하면 기존의 함수로부터 클로저를 생성할 수 있습니다.

private 생성자

new 키워드에 의해 Closure 클래스의 객체가 생성되지 않도록 __construct 생성자는 private로 지정되어 있습니다. 따라서 Closure 클래스의 객체는 반드시 익명 함수 또는 화살표 함수에 의한 방식으로 생성되어야 합니다.

바인딩 및 스코핑

Closure 클래스의 바인딩 메소드(bindTo, bind, call)를 다루기 전에 바인딩 메소드에서 다루고 있는 바인딩과 스코핑 개념에 대하여 명확히 이해하고 넘어갈 필요가 있습니다.

객체 생성 과정

바인딩 메소드에서 처리하는 바인딩 개념을 이해하기 위하여 먼저 객체 생성 과정을 살펴보겠습니다.

객체 생성 과정

객체 생성 과정은 위와 같이 크게 3 단계로 구분될 수 있습니다. 첫 번째 과정에서 클래스 객체가 생성(new Foo)되며, 생성된 객체 자신을 가리키는 참조 변수가 $this입니다. $this는 클래스 스코프 안에서 사용할 수 있기 때문에 가시성(private, protected, public)에 관계없이 해당 클래스 멤버에 자유롭게 접근할 수 있습니다.

두 번째 과정에서 앞서 생성된 객체 $this에 대한 참조값(인스턴스 핸들)을 반환합니다.

세 번째 과정에서 객체 외부에 위치한 객체 변수 $obj에 $this 참조값(인스턴스 핸들)이 저장되며, 객체 변수 $obj를 통해 객체의 멤버를 접근하게 되는데, 객체 변수 $obj의 위치가 객체 외부에 존재하기 때문에 클래스에서 접근을 허용하는 가시성(public, 특별한 경우의 protected)을 가진 멤버에 한하여 접근할 수 있습니다.

객체 멤버의 접근성

클로저 바인딩 처리 과정

클래스 메소드에 정의된 클로저의 예를 가지고, Closure 클래스의 바인딩 메소드(bindTo, bind, call)가 클로저를 객체에 바인딩 시키는 과정을 순서대로 살펴보겠습니다.

첫 번째, 클래스 메소드에 정의된 클로저는 해당 클래스로부터 생성된 $this 객체가 자동 바인딩 되어, 클로저 본문의 $this 객체는 가시성에 관계 없이 해당 클래스의 모든 멤버에 자유롭게 접근할 수 있습니다.

클래스 메소드에 정의된 클로저의 객체 멤버의 접근성

두 번째, 클로저가 다른 객체에 바인딩 하게 되면 $this 객체는 새로 바인딩 된 다른 객체를 참조합니다. 이 때 클로저 내의 $this는 다른 객체의 외부에 존재하기 때문에 클래스 캡슐화, 정보 은닉화 개념에 따라 public 가시성 멤버에 한하여 접근할 수 있습니다.

다른 객체에 바인딩 된 클로저의 객체 멤버의 접근성

세 번째, 클로저의 스코프를 다른 객체의 스코프로 변경하게 되면 클로저 내의 $this는 다른 객체 스코프에 포함되어 동작합니다. 클로저의 위치가 다른 객체의 스코프로 이동하므로 클로저의 $this는 가시성에 관계 없이 다른 객체의 모든 멤버에 자유롭게 접근할 수 있습니다.

다른 객체에 스코핑 된 클로저의 객체 멤버의 접근성

다른 객체로 바인딩과 스코핑을 마친 클로저는 마치 다른 객체에 정의된 메소드와 같은 동작을 보입니다. 또한 PHP의 클로저는 외부 환경을 기억하는 객체입니다. 따라서 다른 객체로 스코핑 될 때 클로저는 초기 등록된 모든 외부 변수의 연결 고리를 그대로 가지고 다른 객체의 스코프로 이동합니다.

  1. $closure = $seoul->getClosure();
  2. $b1 = $closure->bindTo($busan);
  3. $b2 = $closure->bindTo($busan, Busan::class);

1번이 클로저 바인딩 첫 번째 과정으로 클래스 메소드에 정의된 클로저에 클래스 객체 $this가 자동 바인딩 되는 과정입니다. $this 객체를 통해 클래스 Seoul의 멤버를 가시성에 관계 없이 모두 접근할 수 있습니다.

2번이 클로저 바인딩 두 번째 과정으로 클로저가 다른 객체에 바인딩 되는 과정으로, 이 과정에서 $this 객체는 다른 객체를 의미하게 됩니다. 그러나 $this 객체의 위치가 다른 객체 외부에 존재하므로 $this 객체를 통해 클래스 Busan의 public 멤버만 접근할 수 있습니다. 이 과정에서 private 또는 protected 멤버에 접근하려고 하면 아래와 같은 치명적인 오류가 발생합니다.

  • Fatal error: Uncaught Error: Cannot access protected property Busan::$protected_var

3번이 클로저 스코핑 과정으로 클로저의 스코프 위치가 다른 객체로 이동됩니다. 이를 통해 $this 객체는 클래스 Busan의 멤버를 가시성에 관계 없이 모두 접근할 수 있습니다.

Closure 클래스의 바인딩 메소드(bindTo, bind)는 두 번째 과정과 세 번째 과정인 다른 객체로의 바인딩과 스코핑을 처리합니다.

Closure::bindTo

지정된 클로저의 함수 본문(function body; 코드 블록)과 바인딩 변수(등록된 외부 변수)를 그대로 가지고 다른 객체에 바인딩하고 새로운 클래스 스코프를 적용시킨 후 새로운 클로저를 생성하여 반환합니다.

첫 번째 매개변수 $newthis는 클로저와 바인딩 하려는 객체이며, 두 번째 매개변수는 클로저에서 접근할 수 있는 새로운 스코프입니다.

두 번째 매개변수 $newscope를 지정하지 않으면 기본값으로 ‘static’이 적용되며, ‘static’이 적용되면 현재의 클로저 스코프를 변경하지 않고 유지합니다.

정적 클로저의 경우에는 객체와 바인딩 할 수 없으므로 첫 번째 매개변수를 null로 지정하여 언바인딩(unbinding) 되어야 합니다.

bindTo 메소드는 새로 생성된 Closure 객체를 반환하며, 실패시 null을 반환합니다.

비정적 클로저(non-static closure)의 바인딩

클로저에 바인딩 될 객체에 해당하는 첫 번째 매개변수 $newthis는 클로저 함수 본문에서 $this로 접근하게 되는 객체를 의미합니다.

bindTo 메소드의 두 번째 매개변수 $newscope는 클로저의 스코프를 변경하기 위하여 지정합니다. 지정하지 않으면 현재의 클로저 스코프를 그대로 유지합니다. 이 경우 바인딩 된 객체 $seoul, $busan의 멤버는 클로저 스코프 밖에 있기 때문에 private, protected 멤버에 접근할 수 없으며 오직 public 멤버만 접근할 수 있습니다.

$hello 프로퍼티를 private 또는 protected로 지정하면 접근할 수 없다는 오류가 발생합니다.

두 번째 매개변수에 클래스명을 지정함으로 클로저를 클래스 스코프로 이동하게 되면 private, protected 멤버를 포함한 클래스에 정의된 모든 멤버에 접근할 수 있습니다. 이와 같이 스코프를 변경하게 되면 클로저는 바인딩 된 객체의 메소드인 것처럼 동작하여 지정된 클래스 스코프 내에 있는 멤버를 자유롭게 접근할 수 있습니다.

두 번째 매개변수에 클래스 대신에 객체를 지정할 수 있습니다. 객체를 지정하더라도 메소드 내부적으로 객체의 타입인 클래스가 대신 사용됩니다.

정적 클로저(static closure)의 언바인딩(unbinding)

정적 클로저는 동적 특성을 지닌 객체를 바인딩 할 수 없으므로 bindTo 메소드의 첫 번째 매개변수 $newthis를 null로 지정하여야 합니다. 객체를 바인딩 할 수는 없지만 두 번째 매개변수에 객체 또는 클래스명을 지정함으로 스코프를  지정된 클래스로 변경할 수 있습니다.

클로저의 스코프를 변경할 필요가 없으면, 즉 클래스 멤버에 접근할 필요가 없다면 두 번째 매개변수를 지정하지 않아도 됩니다.

정적 클로저에서 첫 번째 매개변수에 객체를 지정하여 바인딩 하려고 하면 Warning 오류가 발생합니다.

  • Warning: Cannot bind an instance to a static closure

객체와 복수 클로저의 연계 처리

객체 내부에서 복수의 클로저를 전달 받아 바인딩 하여 생성된 클로저를 객체 멤버에 저장하여 관리할 수 있으며, 복수의 클로저는 객체의 관리 하에 $this를 통해 객체 멤버에 접근할 수 있습니다.

익명 클래스(anonymous class)와의 바인딩

PHP 7.0부터 지원하는 익명 클래스에 클로저를 바인딩 하게 되면 일회성 객체에도 클로저의 특성을 활용할 수 있습니다. 참조(&)까지 활용하면 익명 클래스 멤버를 외부 클래스인 Hello 멤버와 연동 시킬 수 있습니다.

익명 클래스(anonymous class)와의 바인딩 및 스코핑

Hello 클래스와 익명 클래스 프로퍼티 $hello의 가시성이 private 또는 protected인 경우 바인딩만 하게 되면 치명적인 오류가 발생합니다.

  • Fatal error: Uncaught Error: Cannot access protected property Hello::$hello
  • Fatal error: Uncaught Error: Cannot access protected property class@anonymous::$hello

bindTo 메소드의 두 번째 매개변수를 지정하여 스코핑까지 지정하여 $hello 클로저 객체의 스코프를 Hello 클래스 또는 익명 클래스로 이동시켜야 합니다.

  • $hello = $hello->bindTo($seoul, ‘Hello’);
  • $hello = $hello->bindTo($dongdaemun, $dongdaemun);

Closure::bind

bind 메소드는 bindTo 메소드의 정적 버전이며, 호출 방법만 다르고 다른 객체에 바인딩과 스코핑 하는 과정은 동일합니다.

bind 메소드의 사용 방법은 bindTo 메소드와 동일하기 때문에 자세한 사용법은 bindTo 메소드를 참조하시기 바라며, 여기서는 bind 메소드와 bindTo 메소드의 차이점을 살펴보겠습니다.

bind 메소드와 bindTo 메소드의 차이점

bind 메소드는 정적 메소드(②)이기 때문에 클로저 객체가 아닌 ‘Closure::’로 멤버에 접근(③)할 수 있습니다. 대신에 클로저 객체(④)는 bind 메소드의 첫 번째 매개변수로 지정하여야 합니다. 새로운 Closure 객체 생성에 실패하면 bindTo 메소드가 null을 반환하는 것과 달리 bind 메소드에서는 false를 반환합니다.

아래 예에서와 같이 정적 클로저 $cl1과 비정적 클로저 $cl2를 클래스 Hello에 바인딩 할 때 Case #1, #2, #3 중 어느 방식으로 호출하더라도 바인딩과 스코핑 처리 과정은 동일합니다. 상황에 따라 선택하여 사용하시면 될 것 같습니다.

Closure::call

PHP 7.0부터 지원되는 call 메소드를 이용하면 새로운 클로저를 반환 받을 필요 없이 간편하게 다른 객체에 바인딩 한 후 바로 매개변수와 함께 호출하여 그 결과 값을 반환 받을 수 있습니다.

첫 번째 매개변수 $newthis는 클로저와 바인딩 하려는 객체이며, 두 번째 매개변수 …$values는 바인딩 후 즉시 호출하게 되는 클로저에 전달할 매개변수로서 가변 길이 인수(variable-length argument)이며 0개 이상의 인수를 전달할 수 있습니다.

call 메소드가 반환하는 값은 바인딩 후 즉시 호출되는 클로저의 실행 결과 값입니다.

$closure->call($three, 4)를 호출하게 되면, 먼저 $closure 클로저를 $three 객체에 바인딩 한 후 바인딩 된 $closure 클로저에 4를 인수로 전달하여 처리합니다.

$three 객체는 생성될 당시 생성자에 의해 3이라는 값을 가지고 있었고, 클로저에 4를 인수로 전달하여 클로저 내에서 3과 4를 합쳐 출력하게 되므로 7이 출력됩니다.

Closure::fromCallable

PHP 7.1부터 지원되는 fromCallable 정적 메소드를 이용하면 기존의 함수로부터 클로저를 생성할 수 있습니다.

첫 번째 매개변수 $callable는 클로저로 변환하려는 callable 함수를 지정합니다.

지정된 callable 함수가 현재 스코프에서 호출할 수 있는 경우에는 callable 함수를 클로저로 변환하여 생성된 새로운 클로저를 반환합니다. 호출할 수 없는 경우에는 TypeError를 발생시킵니다.

PHP RFC: Closure from callable function 문서에 따르면 fromCallable 메소드를 제안한 Dan Ackroyd라는 분이 짧은 이름 대신 Closure::fromCallable라는 긴 이름의 정적 메소드 명을 제안한 이유를 설명하면서 네임스페이스의 충돌 문제를 피하기 위해서 였다고 합니다.

당연한 설명이기는 하지만 짧은 이름을 선호하는 애호가들을 위해 아래와 같이 정의하여 짧은 이름을 사용할 수도 있다고 위트 있게 첨언하고 있습니다.

RFC 문서에는 이외에도 fromCallable 메소드를 사용하여 callable 함수를 클로저로 변환해야 할 3가지 이유를 설명하고 있습니다.

  1. Better API control for classes
    클래스에 대한 API 제어 개선
  2. Easier error detection and static analysis
    오류 검출 및 정적 분석 용이
  3. Performance
    성능 향상

클래스에 대한 API 제어 개선

클래스가 콜백으로 처리하려는 메소드를 반환하려면 해당 메소드는 public으로 외부에 노출시켜야 합니다.

반면에 클로저로 변환하여 반환하게 되면 클로저에 자동 바인딩되는 $this 기능에 의해 해당 메소드를 private로 지정할 수 있으므로 외부에 노출 시킬 필요가 없습니다.

오류 검출 및 정적 분석 용이

callable 함수를 문자열로 반환할 때, 문자열이 유효한 함수 이름이 아니면 잘못된 함수 이름이 호출될 때 오류가 발생합니다. 오류가 발생한 위치는 원래 문제가 발생한 위치가 아닌 다른 곳에서 발생하게 되므로 디버그 하기가 쉽지 않습니다.

fromCallable 메소드를 사용하면 메소드를 실행할 때 매개변수로 지정된 callable 함수가 현재 스코프에서 호출할 수 있는지 확인하게 되며 호출할 수 없는 경우에는 즉시 치명적인 오류를 발생 시킵니다.

  • Fatal error: Uncaught TypeError: Failed to create closure from callable: function “food” not found or invalid function name

이에 따라 오류가 발생한 위치와 문제가 있는 위치가 동일한 위치가 되기 때문에 디버깅이 훨씬 수월해지며, 프로그램에 잠재적 버그가 있는지 확인하는 정적 분석(static analysis)*이 더 용이해 집니다.

* 정적 프로그램 분석(static program analysis)은 악성 코드의 위험을 피해 프로그램을 실행시키지 않고 프로그램의 기능을 파악하고 코드를 분석하는 방법입니다.

성능 향상

callable 함수는 호출될 때마다 유효한 callable 함수인지 확인하는 작업이 소요되기 때문에 다른 유형에 비해 처리 속도가 상당히 느립니다.

RFC 문서에서 수행한 실험에서 클로저로 수행했을 때와 callable 함수로 수행했을 때를 비교해 보면, 클로저의 연산 처리 성능이 약 16~19% 정도 향상되었다고 보고하고 있습니다.

답글 남기기