 |
|
난이도 : 초급 John Mertic, Software Engineer, SugarCRM
원문 게재일 : 2008 년 12 월 09 일 번역 게재일 : 2009 년 4 월 14 일 이 "PHP
V5.3의 새로운 기능" 시리즈에서는 곧 발표될 예정인 PHP V5.3의 흥미롭고 새로운 기능에 대해 설명합니다. Part 1에서는
PHP V5.3의 오브젝트 지향 프로그래밍 및 오브젝트 처리와 관련된 변경 사항을 살펴 보았으며 이제 Part 2에서는 클로저 및 람다 함수에 대해
설명합니다. 이러한 함수를 사용하면 여러 컨텍스트에서 사용하는 임시 함수를 쉽게 정의할 수 있기 때문에 프로그래밍 작업이 쉬워집니다.
엄밀히 말해서 클로저 및 람다 함수의 개념은 새로운 개념이 아니라 함수 프로그래밍 환경에서
사용되는 개념이다. 함수 프로그래밍은 명령 실행에서 표현식 평가로 관점을 바꾼 프로그래밍
양식이다. 이러한 표현식은 함수로 구성되며, 이러한 표현식을 결합해서 원하는 결과를 얻을 수
있다. 이 프로그래밍 양식은 주로 교육 현장에서 사용되기는 하지만 인공 지능 및 수학 분야에서도
사용하고 있으며 Erlang, Haskell 및 Scheme와 같은 언어를 사용하는 상용 애플리케이션에서도 볼 수
있다.
클로저는 1960년대에 가장 알려진 함수 프로그래밍 언어 중 하나인 Scheme의 일부로
개발되었다. 람다 함수 및 클로저는 함수를 퍼스트클래스 값으로 처리할 수 있는 즉, 함수를 임시로
만들어서 다른 언어에게 매개변수로 전달할 수 있는 언어에서 주로 사용된다.
그 이후로 클로저 및 람다 함수는 함수 프로그래밍 환경을 벗어나서 JavaScript, Python 및 Ruby와 같은
언어에서 사용되고 있다. JavaScript는 클로저 및 람다 함수를 지원하는 잘 알려진 언어 중 하나이다. 실제로
JavaScript에서는 함수가 다른 함수 내에 중첩되어 private 멤버로 작동하는 오브젝트 지향 프로그래밍을
지원하기 위해 이러한 함수를 사용하고 있다. Listing 1은 JavaScript에서 클로저를 사용하는 방법을 보여 주는 예제이다.
Listing 1. 클로저를 사용하여 빌드된 JavaScript 오브젝트
var Example = function()
{
this.public = function()
{
return "This is a public method";
};
var private = function()
{
return "This is a private method";
};
};
Example.public() // returns "This is a public method"
Example.private() // error - doesn't work
|
Listing 1에서는 Example 오브젝트의 멤버 함수가 클로저로 정의되어
있다. private 메소드의 범위는 로컬 변수로 제한되므로(this 키워드를 사용하여 Example
오브젝트에 연결된 public 메소드와 비교) 외부에서는 보이지 않는다.
지금까지 히스토리 관점에서 이러한 개념의 출처에 대해 살펴보았다. 이제 PHP의 람다 함수에 대해
알아보자. 클로저의 근간이 되는 람다 함수 개념은 PHP의 기존 create_function()
함수에 비해 훨씬 빠르게 함수를 생성할 수 있는 크게 향상된 방법을 제공한다.
람다 함수
람다 함수(또는 "익명 함수")는 간단히 말해서 언제라도 정의할 수 있고 일반적으로 변수에
바인딩되는 임시 함수이다. 함수 자체는 자신이 정의된 변수의 범위 내에만 존재하기 때문에 해당 변수가
범위를 벗어나면 함수도 범위를 벗어나게 된다. 람다 함수에 대한 아이디어는 1930년대 수학 연구에서
나왔다. 람다 계산이라고 하는 이 개념은 재귀 개념과 함께 함수 정의 및 응용을 조사하기 위해
설계되었다. 그 이후로 람다 계산에 대한 연구 결과가 Lisp 및 Scheme와 같은 함수 프로그래밍 언어를
개발하는 데 사용되었다.
람다 함수는 다양하게 활용되고 있으며, 특히 콜백 함수를 받는 PHP 함수에 많이 사용된다. 이러한
함수 중 array_map() 함수를 사용하면 전체 배열을 순환하면서 배열의 각
요소에 콜백 함수를 적용할 수 있다. 이전 버전의 PHP에서는 이러한 함수와 관련하여 콜백 함수를 정의하는
명확한 방법이 없다는 심각한 문제가 있었다. 다음과 같은 세 가지 방법 중 하나를 선택하여 문제를 해결했다.
- 코드 내 임의 위치에 콜백 함수를 정의할 수 있으며, 콜백 함수를 사용할 수 있다는 것을 알고
있다. 그러나 호출 구현 부분이 떨어져 있기 때문에 가독성이 떨어지고 유지 관리하기가 어렵다.
특히 이 함수를 사용하지 않으려는 경우에는 관리가 더욱 어려워진다.
- 같은 코드 블록 내에 콜백 함수를 정의하고 이름을 지정할 수 있다. 이렇게 하면 모든 내용을
함께 관리할 수 있지만 네임스페이스 충돌을 피하기 위해 정의 주변에
if
블록을 추가해야 한다. Listing 2는 이 방법을 보여 주는 예제이다.
Listing 2. 같은 코드 블록에서 이름지정된 콜백 정의
function quoteWords()
{
if (!function_exists ('quoteWordsHelper')) {
function quoteWordsHelper($string) {
return preg_replace('/(\w)/','"$1"',$string);
}
}
return array_map('quoteWordsHelper', $text);
} |
- PHP V4 이후로 제공되는
create_function()을 사용하여 런타임에 함수를 생성할
수 있다. 이 함수는 원하는 기능을 제공하지는 하지만 몇 가지 단점이 있다. 한 가지 중요한 단점은 컴파일
타임이 아닌 런타임에 컴파일되기 때문에 opcode 캐시에서 이 함수를 캐시할 수 없다는 것이다. 또한 구문이 정돈되지 않고
대부분의 IDE에서 제공되는 문자열 강조가 작동하지 않는다.
콜백 함수를 받는 함수가 강력하기는 하지만 일회성 콜백 함수를 번거로운 작업 없이 효과적으로
수행할 수 있는 방법이 없다. PHP V5.3에서는 람다 함수를 사용하여 위 예제를 보다 명확한 방법으로
수행할 수 있다.
Listing 3. 람다 함수를 사용하여 콜백 기능을 수행하는 quoteWords()
function quoteWords()
{
return array_map('quoteWordsHelper',
function ($string) {
return preg_replace('/(\w)/','"$1"',$string);
});
}
|
위 예제는 이러한 함수를 정의하기 위해 구문이 훨씬 더 정돈되었음을 보여준다. 따라서 opcode 캐시를 통해 성능을 최적화할
수 있다. 문자열 강조 기능을 통해 가독성과 호환성도 향상되었다. 지금까지의 내용을 바탕으로
PHP에서 클로저를 사용하는 방법에 대해 알아보자.
클로저
람다 함수 자체만으로 과거에 수행되지 않았던 작업이 가능해지는 것은 아니다. 지금까지
살펴본 모든 작업은 구문이 정돈되지 않고 최적의 성능을 얻을 수는 없지만 create_function()을
사용하여 수행할 수 있다. 그러나 람다 함수는 여전히 임시 함수이고 상태를 유지하지 않기
때문에 이러한 함수를 사용하여 수행할 수 있는 작업에 한계가 있다. 그러나 클로저 단계를
도입하여 람다 함수의 활용도를 높이면 이 한계를 극복할 수 있다.
클로저는 자체 환경 내에서 평가되는 함수이며 해당 함수가 호출될 때 액세스할 수 있는
바인딩된 변수를 한 개 이상 가지고 있다. 이 개념은 수많은 개념을 가지고 있는 함수 프로그래밍에 의해 생성되었다. 클로저는 람다 함수와 유사하지만
한 가지 차이점은 클로저가 정의된 환경의
외부에 있는 변수와 상호 작용할 수 있다는 것이다.
PHP에서 클로저를 정의하는 방법을 살펴보자. Listing 4는 외부 환경에서 변수를 가져와서
화면에 인쇄하는 클로저를 보여 주는 예제이다.
Listing 4. 간단한 클로저 예제
$string = "Hello World!";
$closure = function() use ($string) { echo $string; };
$closure();
Output:
Hello World!
|
외부 환경에서 가져올 변수는 클로저 함수 정의의 use 절에
지정된다. 기본적으로 이러한 변수는 값으로 전달된다. 즉, 클로저 함수 정의 내에서 전달된 값을
업데이트하더라도 외부 값은 업데이트되지 않는다. 하지만 함수 정의에서 참조로 전달을 나타내는
데 사용되는 & 연산자를 변수 앞에 추가하여 이를 수행할 수 있다. Listing 5는 이 방법을 보여 주는 예제이다.
Listing 5. 참조로 값을 전달하는 클로저
$x = 1
$closure = function() use (&$x) { ++$x; }
echo $x . "\n";
$closure();
echo $x . "\n";
$closure();
echo $x . "\n";
Output:
1
2
3
|
이 예제에서는 외부 변수 $x를 사용하는 클로저를 보여 주며 클로저가 호출될 때마다 값이 늘어남을
보여준다. 값으로 전달되는 변수와 참조로 전달되는 변수를 use 절 내에서 함께 사용할
수 있으며 이 경우 아무 문제 없이 처리된다. Listing 6과 같이 함수에서 클로저를 직접 리턴할 수도
있다. 이렇게 되면 클로저를 정의한 메소드보다 클로저의 수명이 더 길어진다.
Listing 6. 클로저를 리턴하는 함수
function getAppender($baseString)
{
return function($appendString) use ($baseString) { return $baseString .
$appendString; };
}
|
클로저 및 오브젝트
클로저는 절차적 프로그래밍뿐만 아니라 오브젝트 지향 프로그래밍에서도 유용하게 사용할 수
있는 도구이다. 클로저는 클래스 내부 또는 외부에서 사용하거나 상관 없이 작은 범위 내에서
바인딩할 특정 함수를 포함하기 위해 사용한다. 클로저는 오브젝트 외부에 있는 것처럼 오브젝트
내에서도 쉽게 사용할 수 있다.
오브젝트 내에서 클로저를 정의한 경우에는 명시적으로 가져오지 않더라도 클로저에서 $this
변수를 통해 오브젝트에 쉽게 액세스할 수 있다. Listing 7은 이 방법을 보여 주는 예제이다.
Listing 7. 오브젝트 내의 클로저
class Dog
{
private $_name;
protected $_color;
public function __construct($name, $color)
{
$this->_name = $name;
$this->_color = $color;
}
public function greet($greeting)
{
return function() use ($greeting) {
echo "$greeting, I am a {$this->_color} dog named
{$this->_name}.";
};
}
}
$dog = new Dog("Rover","red");
$dog->greet("Hello");
Output:
Hello, I am a red dog named Rover.
|
이 예제에서는 greet() 메소드 안에 정의된 클로저에서 이 메소드에
지정된 인사말을 명시적으로 사용한다. 또한 생성자에서 전달되어 오브젝트에 저장된 개의 색과 이름을
클로저 내에서 사용한다.
기본적으로 클래스 내에 정의된 클로저는 오브젝트 외부에 정의된 클로저와 같다. 유일한 차이점은
$this 변수를 통해 오브젝트를 자동으로 가져온다는 것이다. 클로저를
static으로 지정하여 이 기능을 비활성화할 수 있다.
Listing 8. Static 클로저
class House
{
public function paint($color)
{
return static function() use ($color) { echo "Painting the
house $color...."; };
}
}
$house = new House();
$house->paint('red');
Output:
Painting the house red....
|
이 예제는 Listing 5에서 정의한 Dog 클래스와 비슷하다. 큰
차이점은 클로저가 static으로 정의되어 있기 때문에 클로저 내에서 오브젝트의 특성을 사용하지
않는다는 것이다.
오브젝트 내에서 static 클로저를 사용할 때 얻을 수 있는 장점은 static이 아닌 클로저를
사용할 때보다 적은 메모리를 사용할 수 있다는 것이다. 오브젝트를 클로저로 가져올 필요가 없기
때문에 많은 양의 메모리를 사용하지 않아도 되며 특히 이 기능이 필요하지 않는 클로저가 많을
경우 큰 효과를 얻을 수 있다.
오브젝트의 유용한 기능 중 하나는 __invoke() 매직 메소드를 추가할
수 있다는 것이다. 이 매직 메소드를 사용하면 오브젝트 자체를 클로저로 호출할 수 있다. 이 메소드가
정의된 경우에는 오브젝트가 해당 컨텍스트에서 호출될 때 이 메소드가 사용된다. Listing 9는 이 방법을 보여 주는 예제이다.
Listing 9. __invoke() 메소드 사용하기
class Dog
{
public function __invoke()
{
echo "I am a dog!";
}
}
$dog = new Dog();
$dog();
|
Listing 9처럼 오브젝트 참조를 변수로 호출하면 자동으로 __invoke()
매직 메소드가 호출되면서 클래스 자체가 클로저로 작동한다.
클로저는 오브젝트 지향 코드뿐만 아니라 절차적 코드와도 통합할 수 있다. 클로저가 PHP의 강력한
리플렉션 API와 상호 작용하는 방법을 살펴보자.
클로저 및 리플렉션
PHP에는 클래스, 인터페이스, 함수 및 메소드를 역설계하는 기능을 제공하는 유용한 리플렉션 API가 있다. 설계 상 클로저는 익명 함수이기 때문에 리플렉션 API에 나타나지 않는다.
하지만 지정된 함수 또는 메소드에서 클로저를 동적으로 생성하기 위해 새로운 getClosure()
메소드가 PHP의 ReflectionMethod 및 ReflectionFunction 클래스에
추가되었다. 이 컨텍스트에서 이 메소드는 매크로처럼 작동하며, 클로저를 통해 함수의 메소드를 호출하면
정의된 컨텍스트에서 함수가 호출된다. Listing 10에서는 이 메소드의 작동 방법을 보여 준다.
Listing 10. getClosure() 메소드 사용하기
class Counter
{
private $x;
public function __construct()
{
$this->x = 0;
}
public function increment()
{
$this->x++;
}
public function currentValue()
{
echo $this->x . "\n";
}
}
$class = new ReflectionClass('Counter');
$method = $class->getMethod('currentValue');
$closure = $method->getClosure()
$closure();
$class->increment();
$closure();
Output:
0
1
|
이 방법을 사용할 때 부수적으로 얻게 되는 흥미로운 효과가 있다. 즉, 클로저를 통해 클래스의 private 및
protected 멤버에 액세스할 수 있으며 이 기능은 단위 테스트 클래스에서 유용하게 사용할 수 있다. Listing
11에서는 클래스의 private 메소드에 액세스하는 예제를 보여 준다.
Listing 11. 클래스의 private 메소드에 액세스하기
class Example
{
....
private static function secret()
{
echo "I'm an method that's hiding!";
}
...
}
$class = new ReflectionClass('Example');
$method = $class->getMethod('secret');
$closure = $method->getClosure()
$closure();
Output:
I'm an method that's hiding!
|
Listing 12와 같이 리플렉션 API를 사용하여 클로저 자체를 볼 수도 있다. 이 경우에는 단순히 클로저에
대한 변수 참조를 ReflectionMethod 클래스의 생성자에게 전달한다.
Listing 12. 리플렉션 API를 사용하여 클로저 자체 보기
$closure = function ($x, $y = 1) {};
$m = new ReflectionMethod($closure);
Reflection::export ($m);
Output:
Method [ <internal> public method __invoke ] {
- Parameters [2] {
Parameter #0 [ <required> $x ]
Parameter #1 [ <optional> $y ]
}
}
|
이전 버전과의 호환성 차원에서 한 가지 주의할 점은 클래스 이름 Closure가
클로저를 저장하기 위해 PHP 엔진에 예약되어 있기 때문에 이 이름을 사용하는 클래스의 경우 이름을 변경해야
한다는 것이다.
지금까지 살펴본 대로 리플렉션 API는 뛰어난 클로저 지원 기능을 제공한다. 즉, 기존 함수 및 메소드에서
동적으로 클로저를 생성할 수 있도록 지원한다. 일반적인 함수와 마찬가지로 클로저도 자체 보기를 수행할 수 있다.
클로저를 사용해야 하는 이유
람다 함수에 대한 예제에서 보았듯이 클로저를 사용해야 하는 가장 명확한 이유는 콜백 함수를 매개변수로
받는 PHP 함수가 거의 없기 때문이다. 하지만 자체 범위 내에서 논리를 캡슐화해야 할 경우 클로저를 유용하게
사용할 수 있다. 이러한 경우는 기존 코드를 단순하고 읽기 쉽게 만들기 위해 리팩토링할 때 발생한다. 일부
SQL 쿼리를 실행하는 동안 사용된 로거를 보여 주는 다음 예제를 살펴보자.
Listing 13. SQL 쿼리를 로그하는 코드
$db = mysqli_connect("server","user","pass");
Logger::log('debug','database','Connected to database');
$db->query('insert into parts (part, description) values ('Hammer','Pounds nails');
Logger::log('debug','database','Insert Hammer into to parts table');
$db->query('insert into parts (part, description) values
('Drill','Puts holes in wood');
Logger::log('debug','database','Insert Drill into to parts table');
$db->query('insert into parts (part, description) values ('Saw','Cuts wood');
Logger::log('debug','database','Insert Saw into to parts table');
|
Listing 13에서는 작업을 얼마나 많이 반복하고 있는지를 주의해서 봐야 한다. 모든 Logger::log()
호출의 처음 두 인수가 모두 동일하다. 이 문제를 해결하기 위해 해당 메소드 호출을 클로저에 넣은 다음
해당 클로저를 호출하는 방법을 대신 사용할 수 있다. 결과 코드는 다음과 같다.
Listing 14. SQL 쿼리를 로그하는 리팩토링된 코드
$logdb = function ($string) { Logger::log('debug','database',$string); };
$db = mysqli_connect("server","user","pass");
$logdb('Connected to database');
$db->query('insert into parts (part, description) values ('Hammer','Pounds nails');
$logdb('Insert Hammer into to parts table');
$db->query('insert into parts (part, description) values
('Drill','Puts holes in wood');
$logdb('Insert Drill into to parts table');
$db->query('insert into parts (part, description) values ('Saw','Cuts wood');
$logdb('Insert Saw into to parts table');
|
코드의 모습만 정돈된 것이 아니라 SQL 쿼리 로그의 로그 레벨도 이제는 한 번만 변경하면 되기 때문에 쉽게 변경할 수 있게 되었다.
요약
이 기사에서는 PHP V5.3 코드 내에서 클로저가 함수 프로그래밍 구문처럼 매우 유용하다는 것을
살펴보았다. 람다 함수를 설명한 후 람다 함수에 대한 클로저의 장점에 대해서도 다루었다. 오브젝트
지향 코드 내에서 클로저에 대한 특수 처리를 통해 오브젝트와 클로저를 효과적으로 함께 사용할 수
있다는 점도 확인했다. 리플렉션 API를 사용하여 동적 클로저를 생성할 수 있을 뿐 아니라 기존 클로저를
볼 수도 있다는 것도 살펴보았다.
참고자료 교육
제품 및 기술 얻기
-
DVD로 제공되거나 다운로드할 수 있는 IBM 시험판
소프트웨어를 사용하여 후속 오픈 소스 개발 프로젝트를 구현해 보자.
-
IBM 평가판 제품을
다운로드하여 DB2®, Lotus®, Rational®, Tivoli® 및 WebSphere®의 애플리케이션 개발 도구 및
미들웨어 제품을 사용하자.
토론
필자소개  | |  | John Mertic은 Kent State University의 컴퓨터 과학과를 졸업했으며,
SugarCRM에서 소프트웨어 엔지니어로 근무하고 있다. 여러 오픈 소스 프로젝트에 참여했으며, 특히 PHP 프로젝트에 주력하고 있다. 또한 PHP
Windows Installer를 개발 및 유지보수를 수행한다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|