Code Metaphor

Programming, Writing, Reading, Thoughts…

PHP에서의 DSEL 메타프로그래밍

PHP는 지저분한 언어다. $a = f(); $a[0]는 되는데 f()[0]은 구문 오류를 낸다거나, 메서드 체이닝을 PHP 5 이후에서 지원하기 시작했다는 사실, 클래스 메서드의 바인딩이 정적이어서 self 키워드나 __CLASS__ 상수 등이 의도대로 작동 안 하는 것, 초전역(superglobal scope) 같은 이상한 존재, 기존 PHP 관점에서는 매우 뜬금없는 protected, private 키워드 등……. PHP는 지금까지 언어적으로 새로운 기능이 요구될 때마다 심사숙고해서 기존의 것들과 조화를 유지하며 추가해온 것이 아니라, 주먹구구식으로 다른 언어로부터 어울리지 않는 문법이나 개념을 어설프게 따라해서 추가해왔기 때문에, 절대 깨끗하다고 할 수 없다.

그렇다면 저렇게 마구잡이로 기능 추가를 했다면, 언어적으로 지원되는 것이 많은가 하면 그것도 아니다. PHP는 부족한 언어다. 더구나 DSEL 메타프로그래밍을 하는 데는 너무나 부족한 것이 많다. 매크로가 있는 LISP, 런타임에 없는 연산자를 추가하거나 우선순위를 조절하고 메세지(표현식)를 데이터로 취급하는 Io 등과는 비교할 수 없다. Perl, Python, Ruby, JavaScript 등 요즘 유행하는 언어들에 비해서도 한참이나 모자란게 많다. Perl처럼 전처리기가 있는 것도 아니고, JavaScript처럼 함수를 객체로 다룰 수 있지도 않고 클로져도 없다, Python처럼 연산자 재정의가 가능한 것도 아니다. Ruby처럼 클래스가 열려 있어서 런타임에 메서드의 추가나 덮어쓰기가 되는 것도 아니다. PHP가 컴파일 타임이 따로 있는 언어도 아닌데 말이다. 그렇다고 타입 시스템이 괜찮게 되어 있냐면 그것도 아니다.

게다가 사용자 커뮤니티도 매우 수준이 낮다. Ruby, Python 진영에서 메타프로그래밍, DSEL 등의 얘기가 오갈 무렵에도 PHP 커뮤니티의 대다수는 “경력 8년차인데요, 솔직히 프레임워크의 필요성을 모르겠더군요” 같은 얘기를 하고 있고(아, 이 얼마나 솔직한 이야기인가!), 일부 사람들이 MVC 패턴이니 OOP 같은 말들을 하며 으시대지만 실상 제대로 이해하고 사용하는 사람은 없다고 봐도 좋을 정도다.

만약 원해서든 그렇지 않든 PHP를 써야 하는 상황이고, DSEL을 작성해야겠다는 사람들을 위해 몇가지 팁을 써보려고 한다. 나 역시 나름대로 Phunctional이라는 변태 같은 프레임워크를 만들고 있고, 람다(lambda)와 클로져(lexical closure)를 PHP에서 구현하는 등 갖가지 DSEL을 PHP로 작성해보면서 알아낸 게 좀 있다. 다만 PHP에서의 DSEL 메타프로그래밍은 다음과 같은 여러 의미에서 힘들다는(!) 점을 알아야 한다.

  1. DSEL의 필요성을 아는 사람들한테조차 PHP를 쓴다고 무시를 받는다.
  2. 다른 언어에서 쉽게 되는 것도 PHP에서는 아주 변태 같은 방식으로 해야 겨우 구현할 수 있다.
  3. 다른 PHP 사용자로부터는 “그건 PHP 스타일이 아냐”라는 소리를 듣는다.

(마지막 항목을 쓰다보니 좀 화가 나는데, 대체 PHP way라는 것이 실체가 있는 말인가? 그들 사이에서도 새로운 것을 두려워하고 학습을 매우 귀찮아하는 태도 말고는 별 공통점이 없는데, 그게 바로 PHP way를 말하는 것인가?)

마법 메서드 (Magic Methods): __call(), __get(), __set(), __isset(), __unset()

이건 다들 아는 기능이다. 메서드 호출, 멤버 접근 등을 저 메서드를 통해서 속일 수 있다. 예를 들어 PHP에서는 원래 메서드 오버로딩이 안되는데, 이걸 이용하면 오버로딩을 흉내낼 수도 있다.

SPL

원래 확장으로 만들어져 PHP 5.2부터는 기본으로 포함하게 된 SPL은 엄청난 축복이다. 이름을 보면 짐작할 수 있듯, C++ STL의 반복자 개념에 영향을 받아 만든 라이브러리다(나도 STL을 좋아한다). 이것을 잘 이용하면 유사 배열 객체를 만들 수 있다.

Traversable

이 인터페이스를 구현하면 foreach문에 객체를 집어넣어서 배열처럼 돌릴 수 있다. Traversable은 유사 인터페이스고, 실제로는 Iterator 혹은 IteratorAggregate를 구현하면 된다. 전자는 반복자를 직접 구현하는 방식이고, 후자는 IteratorAggregate->getIterator() 메서드를 구현하여 반복자 객체를 위임하는 방식이다. 간단하게 배열처럼만 작동하는 객체를 의도한다면 후자가 좋고, 좀더 정교한 것을 원하면 직접 Iterator를 구현하는 것이 좋다.

Countable

이 인터페이스를 구현하면 count() 함수에 넣었을 때 배열처럼 원소 개수를 반환하게 할 수 있다.

ArrayAccess

정말 유용하다. 이거 모르는 사람이 매우 많은데, 첨자 연산자([])를 오버로드할 수 있다. 단순히 값을 반환하는 것 말고도 해당 키가 존재하는지 확인하는 것(isset($obj[$key])), 해당 키의 원소를 삭제하는 것(unset($obj[$key])), 원소를 덧붙이는 것($obj[] = $value), 해당 키의 원소 값을 수정하거나 추가하는 것($obj[$key] = $value)이 가능하다.

아, 중요한 것 하나. 배열과 달리 ArrayAccess를 구현한 객체는 키 값으로 스칼라 외에 배열이나 객체도 받을 수 있다.

가변 인자: func_get_args()

가변적인 갯수의 인자를 전달받을 때 사용한다. PHP는 함수 선언시에 명시한 시그너쳐에 맞지 않는 호출을 하면 오류를 내는데, 시그너쳐의 갯수보다 많은 인자를 받을 땐 오류를 내지 않는다. 이걸 이용하면 가변 인자를 받으면서 쉽게 오류 처리를 할 수 있다. 예를 들어 인자 갯수는 가변적이지만, 꼭 맨 처음의 인자 하나는 배열로 받아야 한다는 제약 사항이 있다면?

function get_array_and() {
    $args = func_get_args();
    if(!count($args) or !is_array($args[0]))
        throw new InvalidArgumentException('First parameter must be array');
    $array = $args[0];

위와 같이 할 수도 있지만, 아래처럼 하면 굳이 제약 사항을 검사하고 오류를 내는 코드를 직접 작성할 필요가 없다.

function get_array_and(array $array) {
    $args = func_get_args();

주의할 것이 있다. 이 함수의 호출은 항상 맨 첫 문장, 대입식을 제외한 가장 바깥 표현식이어야 한다. 그렇지 않으면 의도한대로 작동하지 않는다. 이유는 나도 모른다. 하지만 이 버그는 PHP쪽에서는 유명하다.

eval()

전달된 PHP 코드 문자열을 해당 문맥에서 실행한다. eval('echo 1234;')1234를 출력한다. 그렇지만 표현식을 받지는 않는다.

$expr = '123 + 456';
$value = eval($expr);

위와 같이 하면 문장이 세미콜론(;)으로 종결되지 않았다며 구문 오류가 난다. 표현식을 평가하고 싶다면 아래와 같이 써야 한다.

eval("\$value = ($expr);");

Reflection

PHP 5에서 추가된 리플렉션 기능을 사용하면 클래스의 멤버와 메서드 목록을 얻는다던가, 함수의 시그너쳐를 추적하는 등의 일을 할 수 있다. 인자에 타입 힌트가 있는지, 있다면 어떤 타입인지, 기본값이 있는지, 변수 이름이 무엇인지 등도 알 수 있다. 이런걸 이용하면 Python의 키워드 인자도 흉내낼 수 있을 것이다.

__toString()

이것도 마법 메서드의 하나인데, PHP 5에서 추가된 기능이다. 이 메서드를 정의한 객체는 echo문으로 출력할 때 __toString()이 반환한 문자열을 출력한다. Python의 __str__ 속성과 비슷한 용도이다. 그런데 이게 PHP 5.2 이후로는 substr() 같이 문자열을 받는 함수에 전달될 때 자동으로 해당 문자열로 캐스팅도 된다. 물론 명시적으로 (string) 캐스팅 연산자를 쓰는 것도 잘 된다.

trigger_error()

웬만하면 예외를 던지는 것이 좋지만, PHP 에러를 내야할 필요가 있을 때 이걸 사용한다.

debug_backtrace()

이 함수를 사용하면 호출 스택을 추적하는 것이 가능하다. eval()을 통하는 것과, include, require를 통한 것도 모두 추적할 수 있다. 대체 이 함수를 어디에 쓰겠느냐고 무심코 지나치면 안된다. 이 함수를 기억하고 있는 것만으로도, 종종 우연히 이 함수를 이용하여 좀더 우아한 DSEL을 달성할 때가 많다.

이 함수를 이용하는 대신 예외 객체를 생성하여 Exception->getTrace() 메서드로 받아내는 방법도 있다.

__FILE__ 상수

__FILE__ 상수의 값은 include, require 등과 무관하게 해당 상수는 현재 코드가 위치한 실제 파일 경로이다. 예를 들어 require_once dirname(__FILE__).'/file.php'과 같이 해당 소스 파일과 같은 위치에 있는 file.php를 불러올 수 있다. (저렇게 하지 않으면 include 호출의 맨 위쪽에 있는 파일에 상대한 경로로 인클루드를 시도한다.)

operator 확장

PHP에 기본적으로 포함되지는 않았지만, 매우 유용한 확장이다. 연산자 재정의를 가능하게 해준다. C++의 Boost.Spirit 라이브러리 같은 것을 보면 연산자 재정의가 얼마나 언어의 표현력을 높여주는지 알 수 있다. 자세한 것은 내가 예전에 정리해둔 문서를 참고하시라.

This entry was posted on November 13, 2007 at 1:56 PM. You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.

7 Responses to “PHP에서의 DSEL 메타프로그래밍”

  1. 박진호 Says:

    php하면서 가장 아쉬운게… 객체지향으로는 가는데 패키지 개념이 없어서 .. 클래스를 관리하기가 힘들어요.

    단적으로 Zend Framework 같은 경우나 Pear 같은 경우를 봐도

    클래스 이름을 {분류}{분류}이름 이런식으로 가니 너무 길어지기도 하구여..

    패키지 개념은 php6 가서도 안 나올것 같기도 하고..

    패키지를 관리하는 먼가 좋은 방법이 없을까요? ^^

  2. Hong, MinHee (DAHLIA) Says:

    네임스페이스(namespace)는 PHP 6에서 추가된다고 합니다. 하지만 사실 Python, JavaScript 등처럼 객체로서의 클래스(class as object)와 람다(lambda)를 지원한다면 굳이 네임스페이스 지원을 따로 할 필요가 없겠죠. 저는 네임스페이스 개념보다는 그런 쪽으로의 지원이 훨씬 아쉽습니다.

    그리고 네임스페이스는 객체 지향 프로그래밍과 무관합니다. 가장 순수한 객체 지향 언어라고 하는 Smalltalk만 해도 네임스페이스 개념이 없습니다. 카테고리라는 것이 있긴 해도 현대적인 언어에서의 네임스페이스, 패키지 기능에 비하면 부족한 부분이 많죠. 그렇지만 누가 뭐래도 가장 객체 지향적인 언어라고 할 수 있습니다. 네임스페이스는 모듈화의 문제지, 다형성(polymorphism)의 문제는 아니니까요.

  3. 박진호 Says:

    네임스페이스가 예전에 php6 스펙을 봤을 때는 지원될지 미정이라고 했는데 되는군요. ^^ 어떤 키워드로 지원할지 궁금하군요, namespace를 쓰면 include를 안써도 되겠지요? 아닐까요? ^^

    php가 자바스크립트나 python과 같은 형태로 간다면 저두 대환영입니다. 언어 자체적으로 조금 더 활용성이 높아지겠죠. 코드를 디자인 할 때도 유용할테구여.

    php 도 필요하면 받아들일꺼 같아요. ^^

  4. Hong, MinHee (DAHLIA) Says:

    php 도 필요하면 받아들일꺼 같아요. ^^

    제 생각에는 적어도 5년 안에는 못 받아들일 것 같습니다. 하위호환성 때문에요. 생각해본 해결책 중에서 최대한 하위호환성을 유지할 수 있는 방식은, $로 시작하는 것은 변수, 그렇지 않은 것은 상수라는 네이밍 규칙을 정한 상태에서 기존 방식의 클래스 선언과 함수 선언을 상수 대입으로 보는 방식인데요. 문제는 클래스, 함수, 변수의 네임스페이스가 모두 개별적이라서 지금까지 함수 이름과 클래스 이름이 중복되서 사용되는 경우가 좀 있었습니다. 또 인스턴스 멤버와 메서드 이름이 똑같이 쓰는 경우는 더 많죠. (->abc->abc()가 아주 별개죠.) 이런 것들도 하위호환성을 보장할 수 없을 겁니다.

  5. Hong, MinHee (DAHLIA) Says:

    패키지를 관리하는 먼가 좋은 방법이 없을까요? ^^

    패키지를 관리하는 방법은… 현재로서는 __autoload()를 잘 사용하는 방법 정도가 최선일 듯합니다. 사실 자기만의 프레임워크라면 나름대로의 패키지 임포트 방식을 정해서 쓸 수도 있겠지만, 그런 방법은 다른 PHP 프로젝트에서도 범용적으로 쓸 수 없겠죠. PHP 5부터 __autoload() 함수가 생겼으니, PHP가 공식적으로 밀어주는 __autoload()PEAR 체계을 사용하는 것이 가장 합리적인 방식인 듯합니다.

    어떤 키워드로 지원할지 궁금하군요, namespace를 쓰면 include를 안써도 되겠지요? 아닐까요? ^^

    namespace 키워드를 사용합니다. C++에서의 네임스페이스와 거의 비슷합니다. include와 함께 사용하는 방식이죠. 네임스페이스는 기본적으로 이름 중복을 방지하는 것이 목적이기 때문에 그 정도로 심플하게 구현하는 것 같습니다. 저도 기존 레거시 코드가 많은 PHP에서 새로운 패키지 시스템을 만드는 것은 좋지 않다고 생각합니다. 차라리 PEAR와 표준 패키지 디렉토리 컨벤션, __autoload()를 묶어서 문화적으로 해결하는 것이 좋을 듯하네요. 아직 PEARCPAN에 비해서는 턱없이 부족하긴 합니다만…….

    PHP CVS 리포지토리에 커밋된 네임스페이스 관련 문서를 참고하세요.

  6. 박진호 Says:

    말씀 감사합니다.. ^^ 회사 일이 너무 정신이 없어서 아직 Phunctional 은 공부를 못 해봤네요. ㅠ.ㅠ

    그래도 php 나름대로의 심플함은 있으니깐 그걸 잃지 않았으면 좋겠습니다. 그걸 잃어버리면 php가 아니니깐요.

    이번주 끝나면 공부를 좀 해야겠네요.. php-gtk 를 한번 해보고 싶은데.. gtk 자체를 알아야 되니깐 이것도 양이 만만치가 않네요..

    기회되면 Phunctional이랑 합쳐봐야겠습니다. ^^

  7. daybreaker Says:

    민희옹 블로그에 거의 처음? 온 것 같은데 앞으로 텍스트큐브 DB 백엔드 전면 재작성 들어가게 되면 많이 참고해야겠군요. =3=3=3

Powered by WordPress. Styled by Hong, MinHee. XML Feed, Comments XML Feed.