PHP: 잘못된 디자인의 프랙탈

서문

저는 까칠한 사람입니다. 주위의 많은 것에 불평합니다. 세상에는 제가 좋아하지 않는 기술들이 많이 있는데, 그것은 당연한 것입니다. 프로그래밍은 우스울 정도로 역사가 짧은 학문이고 우리 중 누구도 우리가 지금 무엇을 하고 있는지조차 갈피를 잡지 못합니다. 스터전의 법칙을 감안하면 주변에 불평할 것들이 평생에 걸쳐서 널려 있습니다.

모든 것이 같지는 않습니다. PHP는 그저 쓰기 어색하다거나 제가 원하는 부분과 맞지 않다거나 차선책이라거나 신념에 맞지 않는 것이 아닙니다. 저는 일반적으로 좋은 방식이라고 여겨지는 것 중에 제가 싫어하는 것들과 나쁜 방식이라고 여겨지는 것 중에 좋아하는 것들을 말씀드릴 수 있습니다. 자, 질문하세요! 이런 주제로 흥미로운 대화를 나눠 볼수도 있을 겁니다.

PHP는 유일한 예외입니다. 실질적으로 PHP의 모든 부분은 어딘가 잘못되어 있습니다. 언어, 프레임워크, 생태계 모두 그냥 개판입니다. 단 하나만 콕 집어서 얘기할 수도 없는 것이 아닙니다. 이 모든 폐해는 시스템 전반에 걸쳐 있기 때문입니다. 매번 PHP의 불만을 정리할 때마다 한번 쭉 훑은 상태에서도 자꾸만 트리비아가 질릴 정도로 발견되어서 이내 막혀버리고 맙니다. (그래서 프랙탈)

PHP는 나의 기술을 망치는 골칫거리입니다. PHP는 완전 개판이지만 아직 다른 것들을 배워보지도 않은 (권력이 있는) 아마추어들이 칭찬을 해대는 통에 미치겠습니다. 결점을 상쇄시킬만한 장점은 쥐꼬리만하고 저는 이 언어의 존재 자체를 잊어버리라고 권하고 싶습니다.

하지만 일단 모든 것을 정리해야 할 것 같군요. 자 갑니다, 이게 마지막이예요.

비유

저는 막 Mel에게 저의 좌절감을 내뱉었고 그녀는 여기에 (그것에 대해) 써 보길 권했습니다.

심지어 저는 PHP의 뭐가 잘못되었는지조차 말할 수가 없어요. 왜냐 하면... 음.... 공구상자가 있다고 해 봐요. 각종 공구가 들어있는. 뭐 그런대로 괜찮아 보이겠죠. 표준적인 것들이 들어 있을거구요.

그런데 거기서 드라이버를 꺼냈는데 끝이 이상한 삼각형 모양인 거예요. 뭐 좋아, 나한테 유용하진 않겠지만 언젠간 쓸만할 때도 있겠지.

이제 망치를 꺼냈는데 경악스럽게도 양쪽 끝에 못뽑이가 달려 있네요. 뭐 어쨌든 쓸 수야 있겠죠. 옆으로 잡고 망치의 가운데 부분으로 못을 박는다거나 하는 식으로.

이제 펜치를 꺼냈는데 톱니 무늬가 없어요. 평평하고 매끈하네요. 유용하진 않지만 어쨌든 볼트를 돌릴 수야 있을테니 뭐 어때요.

그리고 계속 봅시다. 공구상자 안의 모든 것들이 괴상하지만 완전히 쓸모없진 않을 거예요. 그리고 전체적으로 보면 문제가 아니예요. 왜냐하면 일단 공구가 있긴 하니까.

이제 그 공구상자를 쓰면서 "이봐 대체 이 공구상자에 무슨 문제가 있다고? 난 지금까지 이것들을 써 왔고 이것들은 쓸만하다고!"라고 하는 말하는 수백만명의 목수들을 보게 됩니다. 그리고 목수들이 자기가 지은 집을 보여주는데, 방들은 죄다 오각형 모양이고 지붕은 거꾸로 뒤집혀 있군요. 그리고 정문을 두드리면 집이 폭삭 무너져 내리고 안에서 그 사람들이 왜 문을 부수냐고 소리를 지릅니다.

이게 PHP의 문제점이예요.

입장

저는 아래의 요소들이 프로그래밍 언어를 생산적이고 가치있게 만드는 데 중요한 요소라고 주장하지만 PHP는 이 모든 것들을 심각하게 위반합니다. 만약 이것들이 중요하다는 것에 동의하지 않는다면 대체 어떤 것에 동의하실지 짐작할 수 없군요.

  • 언어는 예측 가능해야 합니다. 언어는 사람의 아이디어를 표현하고 컴퓨터로 하여금 이를 실행하도록 하는 매체 역할을 합니다. 따라서 인간이 프로그램을 이해하고 있는 것이 실제로 들어맞는지는 중요합니다.
  • 언어는 일관성이 있어야 합니다. 비슷한 것들은 비슷하게 생겨야 하고 다른 것들은 다르게 생겨야 합니다. 언어의 일부분을 통해 나머지를 배우는데 어려움이 없어야 합니다.
  • 언어는 간결해야 합니다. 새로운 언어들은 낡은 언어들에 내재된 보일러플레이트*를 줄이기 위해 존재합니다. (우리는 기계어로 코드를 짤 수도 있겠죠) 언어는 그 자신이 새로운 보일러플레이트를 만들어내는 것을 지양해야 합니다.
  • 언어는 신뢰성이 있어야 합니다. 언어는 문제를 해결하기 위한 도구입니다. 언어 자신이 문제를 만들어내지 말아야 합니다. "버그 발견했다!"는 상황이 오는 것은 큰 집중력의 손실입니다.
  • 언어는 디버깅이 가능해야 합니다. 만약 뭔가가 잘못된다면 프로그래머는 그것을 고쳐야 하며 그 과정에서 최대한의 도움을 받아야 합니다

제 입장은 이렇습니다.

  • PHP는 놀라움으로 가득합니다: mysql_real_escape_stringE_ACTUALLY_ALL
  • PHP는 일관적이지 못합니다: strposstr_rot13
  • PHP는 보일러플레이트가 필요합니다: C API 호출시 오류 체크, ===
  • PHP는 신뢰할 수 없습니다: ==for ($foo as &$bar)
  • PHP는 불투명합니다: 스택 추적 불가, 복잡한 에러 보고

저는 매 문제 하나하나마다 왜 이것이 이 범주에 들어가는지 따로 부연설명을 하지 않을 겁니다. 한다면 아마 끝이 없겠지요. 아마 독자 여러분들이 스스로 생각하실 수 있을 거라고 믿습니다.

노 코멘트

저는 PHP 논쟁을 많이 벌였습니다. 그럴 때마다 논의를 중단시키려는 목적 말고 없는 뻔한 반론들을 엄청나게 봐 왔습니다. 여기서 제발 저한테 이런 소리는 들이밀지 말아주세요. :(

  • "좋은 목수는 연장 탓을 하지 않고 나쁜 목수는 어쩌구저쩌구…" 같은 소리는 하지 마세요. 아무런 의미 없는 말입니다. 좋은 목수라면 망치로든 바위로든 못을 박을 수야 있겠지요. 하지만 얼마나 많은 목수들이 바위로 못을 박을 수 있겠습니까? 좋은 개발자를 결정짓는 요소 중 하나가 자기에게 가장 좋은 도구를 선택하는 능력입니다.
  • 수없이 많은 이상한 예외나 이해할 수 없는 동작을 기억하는 것은 개발자의 책임이라고 말하지 마세요. 네, 이런 거야 어떤 시스템에서든 필연적이긴 합니다. 컴퓨터는 기본적으로 똥이거든요. 하지만 그렇다고 그게 시스템의 바보같음을 포용할 수 있는데 제한이 없다는 것을 의미하지는 않습니다. PHP는 예외 말고는 아무것도 없습니다. 그리고 실제 프로그램을 작성하는 것보다 언어와 씨름하는데 드는 노력이 더 크다면 그건 전혀 좋은 것이 아닙니다. 내가 쓰는 도구는 내 일거리를 더 늘리지 말아야 합니다.
  • "그게 C API에서 동작하는 방식"이라고 말하지 마세요. 고작 C API의 래퍼나 헬퍼 함수 정도 제공하는게 전부라면 대체 고수준 언어를 쓰는 이유가 뭡니까? 그럴거면 그냥 C로 짜세요! 그럴 때를 위한 CGI 라이브러리도 있습니다.
  • "그게 니가 이상한 짓을 하기 때문에 생기는 것"이라고 말하지 마세요. 만약 두 개의 기능이 있다면 누군가가 언젠가 그것들을 함께 사용할 이유를 찾아내겠지요. 그리고 다시 한번, 이것은 C 언어가 아닙니다. 스펙도 없고 "정의되지 않은 동작"이 존재할 이유도 없습니다.
  • "페이스북이나 위키백과가 PHP로 만들어졌다"고 하지 마세요. 이미 알고 있다고요! 아마 Brainfuck으로도 그런 걸 만들 수 있겠죠. 하지만 그런 걸 만들어낼 수 있을 정도로 똑똑한 사람들이라면 그런 사람들은 플랫폼의 문제도 극복할 수 있습니다. 아시겠지만 어떤 다른 언어로 만들어진다면 개발 기간은 절반이 될 수도 있고 두배가 될 수도 있겠죠. 그러니까 그런 사실만으로는 어떤 의미도 내포하지 않습니다.
  • 이상적으로는 그냥 저한테 아무 말도 하지 마세요. 만약 이 목록에서 어떤 것도 당신의 PHP에 대한 견해를 상처입힐 수 없다면 아마 다른 어떤 것도 없을 겁니다. 그러니까 인터넷의 누군가랑 키배 뜰 생각 하지 마시고 멋진 웹사이트를 만들어서 제가 틀렸다는 것을 입증해 주세요.

여기서 사족, 저는 Python을 무척 좋아합니다. 원하신다면 저야 기쁘게 불평불만을 쏟아낼 수 있습니다. 저는 이게 완벽한 언어라고 말하지 않았습니다. 다만 저는 장점과 단점을 저울질해서 이것이 제가 쓰기에 가장 적합한 언어라는 결론을 내렸을 뿐입니다.

그리고 저는 PHP로 똑같은 걸 할 수 있는 개발자를 만나본 적이 없습니다. 하지만 이내 패배를 인정하고 사과하는 개발자들은 많이 봤습니다. 이런 사고방식은 대단합니다.

PHP

핵심 언어

CPAN은 "Perl의 표준 라이브러리"로 불려 왔습니다. 그게 Perl의 표준 라이브러리에 대해서 많은 것을 이야기하는 건 아니지만 탄탄한 기반에서는 많은 대단한 것들을 만들어낼 수 있다는 점을 내포하고 있습니다.

철학

  • PHP는 원래 대놓고 프로그래머가 아닌 사람들 (그리고 행간을 읽으면 프로그램이 아닌 것들) 을 위해 설계되었습니다. 하지만 그 근본으로부터 잘 벗어나진 못했습니다. PHP 2.0 문서에서 + 같은 연산자들의 형변환에 대해 다루는 부분을 인용합니다.

    각각 자료형에 대해서 연산자를 따로 두면 언어는 더 복잡해질 것입니다. 예를 들어 문자열형에 '==' 연산자를 쓰지 못하기 때문에 'eq' 같은 걸 쓴다고 칩시다. 그렇게 하는 의미를 잘 모르겠습니다. 특히나 PHP처럼 대부분의 스크립트가 단순하고 학습 곡선이 가파르지 않은 기본적인 문법으로 이루어진 언어를 원하는, 프로그래머가 아닌 사람들에 의해 만들어진 언어라면요.

  • PHP는 무슨 수를 써서라도 계속 돌아가도록 만들어졌습니다. 오류를 내면서 중단되거나 뭔가 알 수 없는 동작을 하는 상황이 오면 PHP는 뭔가 알 수 없는 동작을 하는 것을 택합니다. 아무것도 없는 것보다 아무거라도 있는 것이 낫다는 겁니다.
  • 명확한 디자인 철학이 없습니다. 초창기 PHP는 Perl의 영향을 받았습니다. "out" 파라미터가 있는 거대한 stdlib은 C에서 가져온 것이고 객체지향 부분은 C++와 Java에서 가져왔습니다.
  • PHP는 많은 부분을 다른 언어에서 차용합니다. 하지만 그 다른 언어를 알고 있는 사람들에게는 자꾸만 이해할 수 없는 모습을 보입니다. (int)는 C처럼 보이지만 int는 존재하지 않습니다. 이름공간은 \를 사용합니다. 새 배열 문법은 [key => value]와 같이 해시 리터럴을 가진 언어들과는 다른 모양을 지니고 있습니다.
  • 약타입 (역주: 예를 들면 문자열, 숫자 등등 간의 암시적 형변환)이 너무 복잡해서 어떤 사소한 프로그래머의 노력도 결코 가치 없는 것으로 만들어 버립니다.
  • 새로운 기능들 중 일부만이 새로운 문법으로 구현됩니다. 대부분은 함수나 함수처럼 보이는 형태로 구현됩니다. 예외적으로 클래스가 있는데, 클래스는 여러 가지의 새로운 연산자나 키워드가 들어갈 필요가 있었죠.
  • 여기서 나열한 문제 중 일부는 퍼스트 파티를 통한 해결책이 있습니다. Zend가 자신들의 오픈 소스 언어를 고치도록 비용을 지불할 의사가 있다면요.
  • 멀리서 보면 수많은 경우가 있습니다. PHP 문서의 어딘가에서 가져온 이 코드를 보시죠.

    @fopen('http://example.com/not-existing-file', 'r');

    이 코드는 어떻게 동작할까요?

    • PHP가 --disable-url-fopen-wrapper로 컴파일되었다면 이 코드는 동작하지 않을 것입니다. (문서에서는 '동작하지 않는다'라는 것이 뭘 의미하는지 말하지 않습니다. 널을 반환하나? 예외를 발생시키나?) PHP 5.2.5에서 이 플래그가 사라졌다는 것을 유념하세요.
    • php.ini에서 allow_url_fopen가 꺼져 있다면 역시 동작하지 않습니다. (어떻게? 몰라요.)
    • @ 때문에 존재하지 않는 파일이라는 경고 메시지는 출력되지 않을 것입니다.
    • 하지만 php.ini에서 scream.enabled가 켜져 있다면 출력됩니다.
    • 아니면 ini_set로 scream.enabled를 수동으로 설정했다면요.
    • 하지만 맞는 error_reporting 레벨이 설정되지 않았다면 출력되지 않을 겁니다.
    • 하지만 그게 출력된다면, php_ini나 ini_set를 통해 설정된 display_errors에 달려 있을 겁니다.

    저는 이 별다른 해를 끼치지 않는 함수 호출이 PHP 컴파일 옵션과 서버 전역 설정, 그리고 프로그램 내부에서의 설정을 모르고서는 어떻게 동작할지 전혀 말씀드릴 수가 없습니다. 그리고 이 모든 것들은 전부 기본적으로 내장된동작입니다.

  • PHP는 전역적이고 암시적인 상태로 가득 차 있습니다. mbstring는 전역 문자셋을 사용합니다. func_get_arg와 그 친구들은 전형적인 함수처럼 보이지만 현재 실행중인 함수 위에서 동작합니다. 오류와 예외 처리에는 전역 기본값을 사용합니다. register_tick_function 함수는 매 틱(tick)마다 실행될 전역 함수를 설정합니다. 잠깐, 뭐라구요?!

  • 스레딩이나 기타등등에 대한 지원이 전혀 없습니다. (이미 위에 나열한 것들을 봐서 놀랍진 않군요.) fork 가 없는 문제와 더불어 (나중에 언급합니다) 이것은 PHP를 통해 병렬 프로그램을 작성하는 것을 극히 어렵게 만듭니다.
  • PHP 코드 중 일부는 버그를 발생시키도록 설계되었습니다.

    • json_decode 함수는 잘못된 입력이 들어오면 null을 반환합니다. 하지만 문제가 전혀 없는 JSON 객체일지라도 null을 반환할 수 있습니다. 이 함수는 매번 사용할 때마다 json_last_error를 같이 호출하지 않는 한전혀 신뢰할 수 없습니다.
    • array_searchstrpos 함수는 문자열의 처음 위치에서 찾으면 0을 반환합니다. 하지만 찾지 못한다면 false를 반환합니다.

    두번째 경우를 조금 더 설명드리도록 하겠습니다.

    C 언어에서는 strpos 같은 함수는 서브스트링을 찾지 못하면 -1을 반환합니다. 만약 그런 경우를 체크하지 않는다면 당신은 엉뚱한 메모리 영역을 가리키게 될 것이고 프로그램은 터질 것입니다. (아니, 아마도요. 이건 C 언어잖아요. 씨발 대체 누가 알겠냐고. 하지만 최소한 이런 경우를 위한 도구는 있죠.)

    이를테면 파이썬에서 비슷한 역할을 하는 .index 메소드는 찾지 못했을 경우 예외를 발생시킵니다. 역시 그런 경우를 체크하지 않으면 프로그램은 터지게 되겠죠.

    PHP에서 이러한 함수들은 false를 반환합니다. 만약 FALSE를 배열 인덱스값으로 사용하거나 ===로 비교하지 않고 이런 저런 짓을 할 경우에 PHP는 false 값을 0으로 알아서 변환해 버립니다. 그럼 당신의 프로그램은 터지지 않겠죠. 대신 당신의 프로그램은 strpos 함수를 사용할 때마다 결과값을 일일히 체크하는 보일러플레이트 코드를 넣지 않는 한 일말의 경고도 없이 엉뚱한 짓을 하게 됩니다.

    이건 정말 나쁜 겁니다! 프로그래밍 언어는 나와 같이 일을 하기 위해서 만들어진 도구잖아요. 여기, PHP는 개발자가 빠지라고 스스로 찾기 힘든 곳에다가 함정을 파 버립니다. 그리고 저는 문자열 처리나 결과값이 같은지 비교하는 등의 따분한 작업을 하면서도 신경을 곤두세워야 합니다. PHP는 그야말로 지뢰밭입니다.

저는 PHP 인터프리터나 그것의 개발자들에 대해 수많은 멋진 이야기를 들었습니다. 그런 이야기들은 PHP 코어나 디버깅된 PHP 코어, 코어 개발자와 소통해본 사람들에게서 왔습니다. 하지만 그 중 칭찬의 말은 단 하나도 없었습니다.

저는 그래서 여기서 결론을 내립니다. 왜냐 하면 자꾸 반복되는 얘기라서요. PHP는 아마추어들의 커뮤니티입니다. PHP를 디자인하고 작업하고 코드를 짜는 사람들 중에 자기가 뭘 하는지 제대로 아는 것 같은 사람들은 극히 드뭅니다. (아 이것 참, 독자 여러분들은 물론 드문 예외죠!) 그리고 문제의 실마리를 잡을 수 있는 사람들은 다른 플랫폼으로 가 버리게 됨으로서 결국 전체에 있어서 평균적인 능력은 계속 줄어들게 됩니다. 바로 이것이 PHP가 가지고 있는 가장 큰 문제점입니다. 그야말로 장님이 장님의 무리를 이끌고 있는 상황.

좋아요, 일단 팩트로 돌아갑시다.

연산자

  • ==는 쓸모 없습니다.
    • 이것은 추이적 관계(transitive)가 아닙니다. "foo" == TRUE"foo" == 0… 하지만 물론 TRUE != 0.
    • == 연산자는 가능하다면 숫자, 그러니까 부동 소숫점 자료형으로 변환합니다. 그러므로 긴 16진수 문자열을 비교할 때 (이를테면 비밀번호 해쉬 값이라거나) 그렇지 않은 경우에도 참으로 비교해 버립니다.
    • 같은 이유로, "6" == " 6""4.2" == "4.20""133" == "0133"입니다. 하지만 133 != 0133이라는 건 명심하세요. 왜냐 하면 0133은 8진수 표기죠.
    • === 연산자는 값과 자료형을 같이 비교합니다… 객체만 제외하고요. ===는 실제로 같은 객체일 경우에만 참이 됩니다. 객체에 대해서만 == 연산자는 === 연산자가 다른 자료형에 하는 것처럼 값(과 그 안의 모든 어트리뷰트)과 자료형을 같이 비교합니다. 뭐?
  • 비교 연산자라고 상황이 낫지는 않습니다.
    • 일단 전혀 일관적이지가 않습니다. NULL < -1이 참이면서 NULL == 0도 참입니다. 따라서 정렬도 규칙적이지가 못합니다. 정렬 알고리즘이 배열 원소들을 어떻게 비교하냐에 따라 달라집니다.
    • 비교 연산자는 두 가지 다른 방법으로 배열을 정렬하려고 시도합니다: 첫번째는 길이를 기준으로, 두번째는 원소를 기준으로. 만약 서로 다른 키값으로 구성된 같은 개수의 원소들로 이루어져 있다면 비교가 불가능합니다.
    • 객체들은 다른 어떤 것 보다도 큽니다... 또다른 객체들을 제외하고요. 그것들은 심지어 다른 객체보다 크지도 작지도 않습니다.
    • 타입 안전한 ==은 ===가 있습니다. 타입 안전한 <는… 없네요. 당신이 무엇을 하든간에 무조건"123" < "0124"입니다.
  • 이상의 미친 동작들과 Perl처럼 문자열과 숫자 쌍 연산자들을 쓸 수 없는 점에도 불구하고 PHP는 + 연산자를 오버로딩하지 않습니다. +는 무조건 더하기 연산이고 문자열 결합은 무조건 .입니다.
  • [] 배열 첨자 연산자는 {}로 쓸 수도 있습니다.
  • []는 문자열과 배열 뿐만 아니라 어떤 변수에도 쓸 수 있습니다. 널을 반환하고 어떤 경고도 발생하지 않습니다.
  • []로 배열을 자를 수 없습니다. 오직 하나의 원소만 가져올 수 있습니다.
  • foo()[0]는 문법 오류입니다. (PHP 5.4에서 고쳐짐)
  • (말 그대로) 어떤 다른 비슷한 문법을 가진 언어들과 달리, ?:는 left-associative합니다. 예를 들어

    $arg = 'T';
    $vehicle = ( ( $arg == 'B' ) ? 'bus' :
                 ( $arg == 'A' ) ? 'airplane' :
                 ( $arg == 'T' ) ? 'train' :
                 ( $arg == 'C' ) ? 'car' :
                 ( $arg == 'H' ) ? 'horse' : 'feet' );
    echo $vehicle;

    는 horse를 출력합니다.

변수

  • 변수를 선언하는 것은 불가능합니다. 존재하지 않는 변수가 처음 참조되면 자등으로 널값으로 초기화됩니다.
  • 전역 변수는 사용되기 전에 global 선언이 필요합니다. 이건 위의 원인에 따른 자연스러운 결과이기 때문에 타당할 수도 있습니다. 다만 명시적인 선언 없이는 전역 변수를 읽을 수조차 없다는 점을 제외하면요. PHP는 대신 같은 이름의 지역 변수를 조용히 만듭니다. 제가 아는 것 중에 비슷한 스코핑 문제를 가지고 있는 다른 언어는 없습니다.
  • 참조가 없습니다. PHP에서 참조라고 불리는 것은 그냥 별명(alias)일 뿐입니다. Perl의 참조자 같이 한 걸음 물러나 생각해야 할 부분이 없습니다. Python처럼 pass-by-object도 없습니다.
  • "참조성"은 언어의 어떤 다른 부분과 달리 변수를 감염시킵니다. PHP는 동적 언어이기 때문에 변수에는 자료형이 없습니다... 함수 정의나 변수 문법, 대입을 꾸미는 참조를 제외하면요. 한번 변수의 참조를 만들면 (이것은 어디에서나 일어날 수 있습니다) 그것은 참조로 굳어져 버립니다. 이것을 감지할 수 있는 방법은 없고 참조를 해제하고 싶으면 변수 자체를 날려버리는 방법 외에는 없습니다.
  • 사실 거짓말을 했어요. SPL 자료형이란 것이 있는데 이것 역시 변수를 감염시킵니다:$x = new SplBool(true); $x = "foo";는 동작하지 않을 것입니다. 이건 보시다시피 정적 타이핑과 비슷한 것입니다.
  • 존재하지 않는 변수(나중에 배열이 될)의 키값의 참조를 만들 수 있습니다. 존재하지 않는 배열을 사용할 때에는 알림(notice)이 발생하지만 이 경우에는 그렇지 않습니다.
  • 상수는 문자열을 받는 함수 호출에 의해서 정의됩니다. 그 이전에 상수는 존재하지 않습니다. (이것은 아마 Perl의 use constant 동작을 흉내낸 것일 수도 있겠습니다.)
  • 변수 이름은 대소문자를 구분합니다. 함수와 클래스 이름은 구분하지 않습니다. 메소드 이름도 해당되는데, 이는 camelCase 네이밍을 이상한 선택으로 만들어 버립니다.

언어 구조

  • array()와 기타 유사한 것들은 함수가 아닙니다. array 그 자체는 아무 역할도 하지 않습니다.$func = "array"; $func();는 동작하지 않습니다.
  • 배열 unpacking은 list($a, $b) = ...로 할 수 있습니다. list() 역시 array처럼 함수 비스무리한 문법입니다. 왜 이것이 별개의 문법으로 구현되지 않고 혼란스럽게 되어 있는지는 모릅니다.
  • (int)는 분명히 C 언어처럼 보이기 위해 만들어졌습니다. 하지만 저것 자체가 하나의 토큰입니다. PHP에는int라는 것이 없습니다. 한번 직접 해 보세요. var_dump(int)는 동작하지 않을 뿐만 아니라 (int) 자체를 캐스팅 연산자로 해석해서 파싱 오류를 뱉어냅니다.
  • (integer)는 (int)와 같습니다. (bool)/(boolean)나 (float)/(double)/(real) 같은 것도 있습니다.
  • 배열로 강제 형변환을 하기 위한 (array)와 객체를 위한 (object) 연산자도 있습니다. 말도 안 되는 것처럼 보이겠지만 쓸 만한 구석이 있긴 있습니다. 만약 함수의 인자로 한 개의 아이템 또는 리스트를 넘기고 싶고 둘 다 똑같이 취급하고 싶을 때 쓸 수 있습니다. 하지만 그다지 신뢰성이 없는 것이, 만약 한개의 객체를 넘긴다면 그걸 배열로 캐스팅하면 배열 하나가 만들어져서 그 객체의 어트리뷰트를 담게 됩니다. (만약 객체로 캐스팅하면 그 반대의 과정이 일어납니다.)
  • include()와 그 친구들은 기본적으로 C의 #include와 같습니다. 다른 소스 파일을 읽어서 그 안에 그대로 때려박습니다. PHP에는 모듈 시스템이 없습니다. 심지어 PHP 코드에도요.
  • 함수나 클래스는 지역 범위(local scope) 안에 네스팅될 수 없습니다. 단지 전역으로 정의됩니다. 만약 파일을 인클루드하면 변수는 현재 함수 안에 들어갑니다. 하지만 함수나 클래스는 전역으로 정의됩니다.
  • 배열에 내용을 추가시키려면 $foo[] = bar;처럼 합니다.
  • echo는 함수가 아니라 선언문 같은 겁니다.
  • empty($var)는 함수같이 생겼지만 함수가 아닌 것의 극단적인 예일 것입니다. 예를 들어empty($var || $var2)는 파싱 오류를 냅니다. 대체 세상에 왜 파서가 empty라는 것의 존재에 대해서 알고 있어야 하는 겁니까?
  • 블록을 정의하는 쓸데없는 문법이 있습니다. if (...): ... endif; 등등...

오류 처리

  • PHP의 독특한 연산자 중 하나가 에러 출력을 씹는 데 사용되는 @입니다. (실제로는 DOS에서 가져옴)
  • PHP 에러는 스택 추적을 제공하지 않습니다. 스택 추적을 하려면 핸들러를 설치해야 합니다. (하지만 그것도 치명적인 오류(fatal error)에는 해당되지 않습니다. 자세한 건 아래를 보세요)
  • PHP 파싱 오류는 단지 파싱 상태만을 뱉을 뿐 더 이상 아무 것도 보여주지 않습니다. 따라서 코딩 중 뭔가를 빼먹었을 때 디버깅이 끔찍하게 어려워집니다.
  • 예를 들어 PHP 파서는 내부적으로 :: 연산자를 T_PAAMAYIM_NEKUDOTAYIM<< 연산자를 T_SL로 표현합니다. 제가 "내부적"이라고 적었지만 만약에 저것들을 엉뚱한 곳에 집어넣었다가는 에러 메시지에서 저 문구를 보게 될 겁니다.
  • 대부분의 에러 핸들링은 아무도 볼 일 없는 서버 로그에 에러 메시지를 출력하는 형태로 이루어집니다.
  • E_STRICT가 있지만 실제로 도움이 되는 것처럼 보이지는 않습니다. 게다가 이것이 실제로 무슨 역할을 하는지에 대한 문서도 없습니다.
  • E_ALL는 모든 에러 범주를 포함합니다. E_STRICT만 제외하고요. (5.4에서 수정됨)
  • 무엇이 허용되고 무엇이 허용되지 않는지에 대한 일관성이 전혀 없습니다. E_STRICT가 여기서 어떻게 작용하는지는 잘 모르겠지만 이것들은 허용됩니다.

    • 존재하지 않는 객체의 프로퍼티에 접근. 예를 들어 $foo->x. (경고)
    • 함수나 변수나 클래스의 이름에 변수를 사용 (출력되지 않음)
    • 정의되지 않은 상수를 참조 (알림)
    • 객체가 아닌 무언가의 프로퍼티에 접근하려고 할 때 (알림)
    • 존재하지 않는 변수를 참조하려고 할 때 (알림)
    • 2 < "foo" (출력되지 않음)
    • foreach (2 as $foo); (경고)

    그리고 이것들은 허용되지 않습니다.

    • 존재하지 않는 클래스 상수에 접근하려고 할 때. 예를 들어 $foo::x (치명적 오류)
    • 함수나 변수, 클래스의 이름에 상수를 사용 (파싱 오류)
    • 존재하지 않는 함수를 호출하려고 할 때 (치명적 오류)
    • 블록이나 파일의 맨 마지막에 세미콜론을 남길 때 (파싱 오류)
    • list나 기타 등등 내장스러운 것들을 메소드 이름으로 사용할 때 (파싱 오류)
    • 함수의 반환값의 원소값을 참조할 때. 예를 들어 foo()[0] (파싱 오류. 위에서 언급했다시피 PHP 5.4에서 해결되었습니다)

    그리고 여기서 언급한 것이 아니더라도 기타 이상한 파싱 에러가 있습니다.

  • __toString 메소드는 예외를 발생시키지 않습니다. 만약 발생시키려고 하면 PHP는 음... 어... 예외를 발생시키긴 합니다. (실제로는 치명적 오류입니다. 그런대로 괜찮긴 합니다만....)

  • PHP 에러(PHP error)와 PHP 예외(PHP exception)는 완전히 다릅니다. 이 둘은 전혀 상호작용하지 않습니다.

    • PHP 에러(내부적으로 발생하는, 그리고 trigger_error를 호출하는)는 try/catch로 잡을 수 없습니다.
    • 비슷하게 예외는 set_error_handler로 설치된 오류 핸들러를 타지 않습니다.
    • 대신 처리하지 못한 예외를 위해 set_exception_handler가 따로 있습니다. 왜냐하면 프로그램의 진입점부터 try 블록으로 감싸는 것은 mod_php 모델에서는 불가능하기 때문입니다.
    • 치명적 오류 (예를 들어 new ClassDoesntExist())는 어떤 것에도 잡히지 않습니다. 수많은 비교적 무해한 문제들도 치명적인 오류를 발생시키며 의문스러운 이유로 프로그램을 종료시켜 버립니다. 종료 함수는 계속 돌지만 스택 추적을 할 수가 없으며 (최상단에서 돌기 때문) 프로그램이 제대로 종료되었는지 오류로 인해 종료된 건지 쉽게 알 수가 없습니다.
    • Exception이 아닌 객체를 throw하면.. Fatal Error가 납니다. Exception이 아니라요..
  • finally 구문이 없습니다. 따라서 래퍼 코드(핸들러 설정 - 코드 실행 - 핸들러 해제나 몽키패칭 - 테스트 수행 - 몽키패치 해제)를 작성하기가 귀찮고 어렵습니다. 객체지향이나 예외처리의 많은 부분을 Java에서 가져왔음에도 불구하고 이것은 의도적입니다. 왜냐 하면 "PHP의 문맥에서는 별로 말이 안 된다."라나요. 엉? (5.5에서 고쳐짐)

함수

  • 함수 호출은 비용이 많이 듭니다.
  • 몇몇 내장된 함수들은 참조를 반환하는 함수와 같이 쓰면 음.. 이상하게.. 동작합니다.
  • 앞서 언급했던 수많은 '함수가 아니지만 함수처럼 보이는 구문'들은 함수와 상호작용하는 것들과는 결코 어울리지 못합니다.
  • 함수 인자들은 실제로는 정적 타이핑과 다름없는 "형 힌트"라는 것을 가지고 있습니다. 모든 내장 함수들이 이런 방식의 타이핑을 사용하는데도 형 힌트로 intstringobject나 기타 "핵심" 자료형을 줄 수가 없습니다. 아마도 PHP에 int 같은 것이 것이 없기 때문일 겁니다. (앞에서 이야기한 (int)를 보세요.) 내장 함수에서 엄청나게 쓰이는 mixednumbercallback 같은 유사 형들도 마찬가지입니다. (callable은 5.4에서 허용되게 수정되었습니다)

    • 그 결과,
    function foo(string $s) {}
    
    foo("hello world");

    는 이런 에러를 발생합니다.

    PHP Catchable fatal error: Argument 1 passed to foo() must be an instance of string, string given, called in...
    • 아마 "형 힌트"라는 것이 애시당초 존재할 필요가 없다는 걸 알아차리실 지도 모르겠습니다. string 같은 클래스는 없죠. 만약 ReflectionParameter::getClass()로 형 힌트를 동적으로 검사하려고 하면 그런 클래스는 존재하지 않는다고 할 겁니다. 따라서 실제 클래스 명을 받아오는 것은 불가능하게 되죠.
    • 함수의 반환값은 힌팅할 수 없습니다.
  • 현재 함수의 인자를 다른 함수로 넘기는(디스패치, 그렇게 보기 드문 것은 아닙니다) 것은call_user_func_array('other_function', func_get_args())를 통해 할 수 있습니다. 하지만 func_get_args함수는 실행 중에 이것이 함수 파라미터가 될 수 없다면서 치명적인 오류를 뱉습니다. 이게 대체 무슨 에러랍니까? (PHP 5.3에서 고쳐짐)

  • 클로져는 모든 변수가 에워싸지도록(close over) 요구합니다. 왜 인터프리터가 알아서 할 수 없는 걸까요? 아마 모든 기능들의 방해물이 아닐까 합니다. (네, 그건 다른 곳에서 명시적으로 이야기하지 않는 한 변수를 쓰는 것은 만드는 것이기 때문입니다.)
  • 에워싸진 변수들은 다른 함수 인자들과 같은 형식으로 "넘겨집니다". 배열, 문자열, 기타 등등 역시 "by value"로 넘겨집니다. &를 쓰지 않으면요.
  • 에워싸진 변수들은 실질적으로 자동으로 넘겨진 인자들이고 네스팅된 범위라는 것이 없기 때문에 클로져는 private 메소드에 접근할 수 없습니다. 심지어 클래스 내에 정의되었을지라도요. (PHP 5.4에서 고쳐졌을까요? 모르겠습니다.)
  • 함수 인자에 변수명을 명시적으로 줄 수 없습니다. 제안된 적은 있으나 "코드가 지저분해진다"는 이유로 거부당했습니다.
  • 기본값을 가진 함수 인자들은 기본값이 없는 함수 인자 앞에 올 수 있습니다. 심지어 문서에서조차 이것이 이상하고 쓸데없다는 점을 인정하고 있습니다. (그럼 대체 왜 허용하는 건데?)
  • 함수로 전달된 잉여 인자들은 무시됩니다. (내장 함수들은 제외하고요. 내장 함수들은 에러를 냅니다.) 없는 인자는 널로 취급됩니다.
  • 가변 인자 함수를 작성하게 위해서는 func_num_argsfunc_get_argfunc_get_args 같은 함수를 쓰면서 법석을 떨어야 합니다. 그런 것을 위한 문법은 존재하지 않습니다.

객체지향

  • PHP의 함수와 관련된 부분은 C와 비슷하게 설계되었고 객체(ㅋㅋㅋ)와 관련된 부분은 자바와 비슷하게 설계되었습니다. 저는 이것이 얼마나 거슬리는지 이루 말할 수 없습니다. 클래스 체계는 좀 더 저수준의 Java 언어를 본따서 설계되었고 동시대의 다른 언어들에 비해서 자연스럽고 의도적으로 더욱 제한되어 있습니다. 저는 당황했습니다.

    • 전역 함수 중에서 대문자가 들어간 것은 찾지 못했습니다만 중요한 내장 클래스는 camelCase 메소드명을 쓰고 있으며 Java 스타일의 getFoo 같은 접근자를 사용합니다.
    • Perl, Python, Ruby 모두 코드를 통한 "프로퍼티" 접근 같은 개념을 가지고 있지만 PHP 혼자 __get 같은 것들을 가지고 있습니다. (PHP 문서에서는 불가사의하게도 그러한 특수한 메서드들을 "overloading"이라고 부릅니다.)
    • 클래스는 클래스 어트리뷰트들에 대해서는 변수 선언(var이나 const)과 같은 느낌이 들지만, PHP의 다른 절차적인 부분에서는 그렇지가 않습니다.
    • 객체 개념이 꽤 불투명한 C++이나 자바에서 큰 영향을 받았음에도 불구하고, PHP는 종종 객체를 그냥 좀 괜찮은 해시인 것 처럼 처리합니다. 예를들어, foreach ($obj as $key => $value)는 그냥 객체의 모든 접근 가능한 어트리뷰트를 순회하는 것 뿐입니다.
  • 클래스는 객체가 아닙니다. 메타프로그래밍을 위해선 마치 함수처럼 문자열로 된 이름을 통해서 참조해야 합니다.

  • 내장형 자료형은 객체가 아니며 (Perl과는 다르게) 객체처럼 보이게 할 수 있는 방법도 없습니다.
  • 클래스가 나중에 추가된 기능이고 언어의 대부분은 함수 또는 함수스러운 문법으로 구현되어 있음에도 불구하고instanceof는 연산자입니다. 자바의 영향일까요? 클래스가 일급객체가 아니다? (저는 잘 모르겠군요.)
    • 하지만 is_a 함수는 있습니다. 그 객체가 주어진 클래스 이름 문자열과 같은지 확인하기 위한 추가적인 인자와 함께요.
    • is_subclass_of와 마찬가지로 get_class는 함수입니다. typeof는 없습니다.
    • 그리고 이것은 내장된 자료형과는 동작하지 않습니다. (다시 한번, int 같은 건 애시당초 없는 겁니다) 이런 경우를 위해, is_int 같은 것들이 필요해집니다.
    • 그리고 오른쪽 항은 변수나 문자열 리터럴이 되어야 합니다. 표현식은 올 수 없습니다. 만약 그렇게 하면... 파싱 오류가 발생합니다.
  • clone이 연산자라고?!
  • 객체 어트리뷰트는 $obj->$foo이지만 클래스 어트리뷰터는 $obj::foo. 이런 짓을 하는 다른 언어에 대해서는 들어본 적이 있고 이게 얼마나 유용할지도 모르겠습니다.
  • 또한 인스턴스의 메소드는 여전히 정적(Class::method())으로 호출할 수 있습니다. 만약 다른 메소드에서 호출된다면 현재 $this를 가진 일반적인 메소드 호출이 되어야 한다고 생각합니다.
  • newprivatepublicprotectedstatic 등등등... Java 개발자들을 이기기 위한 겁니까? 개인적인 취향이라는 건 알고 있지만 왜 이런 것들이 동적 언어에서 필요한 건지 잘 모르겠습니다. C++에서 이런 것들은 대부분 컴파일이나 컴파일 타임에서 이름을 찾는 것에 관련되어 있잖아요.
  • 하위 클래스는 private 메소드를 오버라이딩할 수 없습니다. 하위 클래스에서 오버라이딩된 public 메소드는 볼 수조차 없으며 혼자서 호출될 수도 없으며 상위 클래스의 private 메소드를 호출할 수도 없습니다.
  • 메소드는 예를 들어 "list"라는 이름을 가질 수 없습니다. 왜냐 하면 list()는 특수한 문법이라 (함수가 아니라) 파서가 혼란스러워 하기 때문입니다. 이게 모호할 이유가 전혀 없는데, 클래스에 몽키패칭은 잘 됩니다. ($foo->list()는 구문 오류가 아닙니다.)
  • 생성자의 인자를 평가하는 과정에서 예외가 발생하면 (예를 들어 new Foo(bar())에서 bar()가 예외를 생성할 경우) 생성자가 호출되지 않고 소멸자가 호출됩니다. (PHP 5.3에서 고쳐짐)
  • __autoload나 소멸자 안에서 발생한 예외는 치명적 오류를 발생시킵니다.
  • 사실 생성자와 소멸자는 없습니다. __construct는 Python의 __init__처럼 초기화 메소드(initializer)입니다. 클래스에 호출하여 메모리를 할당하고 객체를 생성하는 방법은 없습니다.
  • 기본 초기화 메소드는 없습니다. 만약 상위 클래스가 __construct를 정의하고 있지 않은 경우에parent::__construct()를 호출하면 치명적 오류가 발생합니다.
  • 객체지향은 보통 언어의 일부분 (예를 들어 for...as)이 알아들을 수 있는 형태의 반복자 인터페이스를 제공하는데 어떤 내장 형식 (예를 들면 배열)도 이를 구현하고 있지 않습니다. 만약 배열의 반복자를 원한다면ArrayIterator로 감싸야 합니다. 잇거나 나누거나 반복자를 일급객체로 사용하는 기본적 방법은 없습니다.
  • 클래스를 문자열로 변환하거나 변환 시 어떻게 행동할지를 재정의할 수 있으나 숫자나 기타 기본 형식에 대한 변환은 지원하지 않습니다.
  • 문자열, 숫자, 배열은 모두 문자열 변환 기능이 있습니다. 언어 전체가 이것에 크게 의존하고 있습니다. 함수와 클래스는 문자열입니다. 만약 __toString가 정의되어 있지 않다면 내장된 혹은 사용자 정의된 객체(심지어 클로져까지)를 문자열로 변환할 시 오류가 발생하게 됩니다. 심지어 echo까지 잠재적으로 오류를 낼 가능성을 가지고 있습니다.
  • 비교 연산자의 오버로딩을 지원하지 않습니다.
  • 인스턴스 메소드 안에 정의된 정적 변수는 전역입니다. 모든 인스턴스가 한 변수를 공유합니다.

표준 라이브러리

예를 들어 Perl은 "약간의 조립 과정이 필요"입니다. Python은 "배터리 포함"입니다. PHP는 캐나다산 키친 싱크대입니다. 게다가 양쪽 수도꼭지에 전부 C라고 적혀 있습니다.

일반

  • 모듈 시스템이 없습니다. PHP 확장을 컴파일할 수 있지만 php.ini에 등록해야만 불러올 수 있습니다. 그리고 당신이 할 수 있는건 확장을 켜고 (내용을 전역 네임스페이스에 주입하고) 끄는 것 뿐입니다.
  • 네임스페이스가 최근에 추가된 기능이긴 하지만 표준 라이브러리는 전혀 분류되지 않았습니다. 전역 이름공간에 수천개의 함수들이 있습니다.
  • 라이브러리 덩어리들은 전혀 일관성을 갖추고 있지 않습니다.

    • 밑줄의 여부: strpos / str_rot13php_uname / phpversionbase64_encode / urlencode,gettype / get_class
    • "to" vs 2: ascii2ebcdicbin2hexdeg2radstrtolowerstrtotime
    • 목적어+동사 vs 동사+목적어: base64_encodestr_shufflevar_dump vs create_function,recode_string
    • 인자 순서: array_filter($input, $callback) vs array_map($callbasck, $input),strpos($haystack, $needle) vs array_search($needle, $haystack)
    • 접두사 혼란: usleep vs microtime
    • 대소문자를 구분하지 않는 (문자열 관련) 함수들은 이름에 i가 붙냐 안 붙냐에 따라 달라집니다.
    • 배열 관련 함수의 절반 가까이가 array_로 시작하지만 나머지는 그렇지 않습니다.
  • 키친 싱크대. 라이브러리는 이런 것들로 구성되어 있습니다.

    • ImageMagick 바인딩, GraphicsMagick(ImageMagick의 fork) 바인딩, 그리고 EXIF 데이터를 조회하기 위한 여러가지 함수들 (이미 ImageMagick에서 제공하고 있는 기능인데도)
    • bbcode (특정 게시판 시스템에서 사용되는 종류의 마크업) 를 파싱하는 함수들.
    • 지나치게 많은 XML 패키지. DOM (OO), DOM XML (아님), libxmlSimpleXMLXML Parser,XMLReader/XMLWriter, 그리고 기억할 수 없는 수많은 약어들. 분명히 저것들 사이에 차이점은 있을거고 어떤게 어떤건지 알아보는 것은 자유겠죠.
    • 두개의 특정 신용 카드 처리기를 위한 바인딩. SPPLUS와 MCVE. 뭐라고?
    • MySQL 데이터베이스에 접속하기 위한 세 가지 방법: mysqlmysqli 그리고 PDO 추상화 뭐시기.

C의 영향

이 내용은 아주 중요합니다. 왜냐하면 전혀 말도 안되는 것들인데도 언어 전반에 퍼져있으니까요. PHP는 하이레벨, 동적타이핑 프로그래밍 언어라구요. 그런데도 스탠다드 라이브러리의 대다수가 C API를 아주 살짝 싸놓았을 뿐입니다. 그 결과 아래와 같은 일이 생기죠.

  • 함수 인자로 넘기는 "반환값" 파라미터. PHP도 임의의 해시를 반환하거나 여러 개의 인자를 한꺼번에 반환할 수가 있잖아요.
  • 특정 서브시스템을 위한, 가장 최근에 발생한 에러를 가져오기 위한 십여 개의 함수들. (아래를 보세요) 예외는 8년 전에도 PHP에 있었습니다.
  • mysql_real_escape_string 같은 함수들. 심지어 고장난 mysql_escape_string와도 같은 인자를 갖는데, 이는 단지 MySQL C API의 일부이기 떄문입니다.
  • 부분적인 기능을 위한 전역적 동작. (마치 MySQL처럼) 여러 개의 MySQL에 접속하여 동시에 쓰기 위해서는 매 함수 호출시마다 MySQL 핸들을 함수로 전달해야 합니다.
  • 그리고 래퍼는 정말, 정말로 얇습니다. dba_firstkey 없이 dba_nextkey를 호출하면 segfault로 죽습니다.
  • 심지어 어떤 래퍼는 플랫폼 의존성이 있습니다. fopen(directory, "r")는 리눅스에서는 잘 돌지만 윈도우에서는 false를 반환하고 워닝을 발생시킵니다.
  • C의 비슷한 문자 처리 함수들과 거의 일치하는 ctype_* 함수들(예를 들면 ctype_alnum)이 있습니다.

제너릭

그런거 없습니다. 만약 두가지의 미묘하게 다른 기능이 필요하다면 PHP에는 두 개의 함수가 있습니다.

배열을 어떻게 반대 방향으로 정렬하나요? Perl이라면 sort { $b <=> $a }로 하면 되겠죠. Python은?.sort(reverse=True)로. PHP에서는 rsort()라는 별개의 함수가 있습니다.

  • C 함수의 오류를 확인하기 위한 함수들: curl_errorjson_last_erroropenssl_error_string,imap_errorsmysql_errorxml_get_error_codebzerrordate_get_last_errors. 더 있나요?
  • 정렬 함수들: array_multisortarsortasortksortkrsortnatsortnatcasesortsort,uasortuksortusort
  • 문자열 검색 함수들: eregeregimb_eregmb_eregipreg_matchstrstrstrchrstrichr,strrchrstrposstriposstrrposstrriposmb_strposmb_strrpos + 기타 등등 바리에이션
  • 별 도움은 안 되는 이름만 다른 함수들도 많습니다. strstr / strchris_int / is_integer / is_long,is_float / is_doublepos / currentsizeof / countchop / rtrimimplode / joindie /exittrigger_error / user_error …
  • scandir는 주어진 디렉토리의 파일 목록을 반환합니다. 디렉토리의 순서대로 파일 목록을 반환하는 것이 아니라 파일을 알파벳 순서로 정렬해서 반환합니다. 그리고 역순으로 정렬하기 위해 추가적인 인자를 받습니다. 아마 정렬 함수로는 충분하지 않았나 봅니다.
  • str_split 함수는 문자열을 일정한 길이의 덩어리들로 나눕니다. chunk_split 함수는 문자열을 일정한 길이의 덩어리로 나누고 구분자로 합칩니다.
  • 압축 파일에서 읽기 위해서는 포맷에 따라서 서로 다른 함수를 써야 합니다. bzip2, LZF, phar, rar, zip, gzip/zlib를 위한 총 6개의 서로 다른 API들이 있습니다.
  • 배열을 인자로 해서 함수를 호출하는 방법이 너무 이상하기 때문에 (call_user_func_array), printf /vprintf나 sprintf / vsprintf 같이 나누는 경우가 있습니다. 전자는 인자들을 받고 후자는 인자들로 이루어진 배열을 받습니다.

텍스트

  • preg_replace에 /e (eval) 플래그를 걸면 문자열을 정규식 규칙에 맞게 변환하고, 그것을 평가(eval)합니다.
  • strtok는 해당하는 C 함수와 똑같이 만들어졌습니다. 이것은 여러 의미에서 나쁜 아이디어인데, PHP로는 배열을 쉽게 반환할 수 있다는 점을 명심하세요. (C에서는 그게 어렵죠) 그리고 strtok(3)이 쓰는 핵(문자열 내용을 그 자리에서 고치기)은 여기서 사용되지 않습니다.
  • parse_str는 쿼리 문자열을 파싱합니다. 이름만 가지고는 그걸 전혀 알 수 없죠. 그리고 이것은 따로 배열 인자를 주지 않으면 register_globals처럼 동작하여 쿼리 내용을 전역 범위에 변수로 넣어버립니다. (물론 아무것도 반환하지 않습니다.)
  • explode 함수에 빈 구분자를 줄 수 없습니다. 다른 대부분의 문자열 나누기 함수는 구분자를 주지 않으면 문자 단위로 나눈다는 것으로 간주합니다. 대신 PHP에선 완전히 다른 함수가 이 역할을 하는데, 이름도 혼란스럽게str_split입니다. 그리고 "문자열을 배열로 변환하는데 사용된다"고 설명되어 있습니다.
  • 날짜를 포매팅할 떄 C API와 똑같으며 시스템 날짜 설정을 따르는 strftime라는 함수가 있습니다. 완전히 다른 사용법의 date라는 함수도 있는데 영어로만 동작합니다.
  • gzgetss — gz 파일 포인터에서 한 줄을 읽어서 HTML 태그를 벗겨냅니다.” 저는 이런 함수가 존재하는 의미가 뭔지 궁금해 미칠 것 같습니다.
  • mbstring
    • 캐릭터 셋이 문제가 되는 경우에 사용하는 "멀티바이트"에 관한 함수들입니다.
    • 일반 문자열처럼 동작하지만 하나의 전역적인 "기본" 문자 셋 설정이 있습니다. 몇몇 함수들은 문자셋을 직접 줄 수 있지만 모든 인자와 반환값에 적용이 됩니다.
    • ereg_* 함수를 제공하지만 더 이상 지원하지 않습니다(deprecated). 전용 플래그를 걸어줘야 UTF-8을 인식할 수 있긴 하지만 preg_*는 운이 없는 편이죠.

시스템과 반영(reflection)

  • 전반적으로 텍스트와 변수의 경계를 모호하게 만드는 엄청나게 많은 함수들이 있습니다. compact와 extract는 빙산의 일각일 뿐이죠.
  • 실제로 PHP에서 '동적'이라는 개념을 위한 몇 가지 방식이 있습니다. 하지만 얼핏 보면 중요한 차이점이나 상대적 이득은 없는 것 같습니다. classkit로 사용자가 정의한 클래스를 바꿀 수 있습니다. runkit로 그것을 대체하고 사용자가 정의한 모든 것을 바꿀 수 있습니다. Reflection* 클래스로 언어의 거의 모든 부분을 반영할 수 있습니다. 하지만 이미 함수나 클래스의 프로퍼티에 접근하기 위한 수많은 개별 함수들이 있습니다. 이것들은 서브시스템에 독립적일까요, 관계가 있을까요, 아니면 단지 잉여일 뿐일까요?
  • get_class($obj)는 객체의 클래스명을 반환합니다. get_class()는 현재 호출되는 함수의 클래스명을 반환합니다. 하나의 함수가 두개의 완전히 다른 역할을 한다는 점은 차치하구요, 그럼 get_class(null)은? 후자의 역할을 합니다. 그러니 임의의 값을 믿으면 안 됩니다. 놀라셨죠?
  • stream_* 클래스로 파일스러운 내장 객체들과 비슷한 스트림 객체를 구현할 수 있습니다. 하지만 "tell"은 내부적인 문제 때문에 구현할 수 없습니다. (게다가 이 시스템에 연계된 함수들은 엄청나게 많습니다.)
  • register_tick_function는 클로져를 받아들일 수 있습니다. unregister_tick_function는 받아들이지 못하는데, 대신 클로져 객체를 문자열로 변환할 수 없다는 에러를 발생할 겁니다.
  • php_uname 함수는 현재 OS에 대해 출력합니다. PHP가 현재 작동중인 OS를 알 수 없으면 PHP를 컴파일한 시스템의 OS 정보를 알려 줍니다. 그리고 그 두가지 상황을 구분할 수 있는 방법은 없습니다.
  • fork와 exec는 내장 함수가 아닙니다. 이것들은 pcntl 확장에 들어 있으며 기본적으로 포함되어 있지 않습니다. popen는 pid를 제공하지 않습니다.
  • stat의 반환값이 캐시됩니다.
  • session_decode는 임의의 PHP 세션 문자열을 읽을 때 사용합니다. 하지만 이미 활동 중인 세션이 있을 때만 동작합니다. 그리고 결과값을 반환하는 것이 아니라 $_SESSION에다가 때려박습니다.

기타

  • curl_multi_exec는 실행 중 오류가 발생하면 curl_errno를 바꾸지 않습니다. 하지만 curl_error는 건드립니다.
  • mktime의 인자 순서는 이렇습니다: 시간, 분, 초, 월, 일, 년.

데이터 조작

프로그램은 데이터를 먹고 더 많은 데이터를 뱉어내는 것 이상도 아닙니다. awk부터 시작해서 Prolog, C까지 좋은 언어들은 조작할 데이터의 유형에 따라 설계됩니다. 만약 언어가 데이터를 만질 수 없다면 아무것도 할 수 없다는 겁니다.

숫자

  • 32비트 시스템에서 정수는 32비트의 부호 있는 정수입니다. PHP의 동시대 언어들과 달리 자동 bigint 변환이 없습니다. 그래서 CPU 아키텍쳐에 따라 수학이 조금씩 다를 가능성도 있습니다. 큰 정수를 다룰 경우에는 GMP나 BC 래퍼 확장을 이용하는 수밖엔 없습니다. (개발자들은 완전히 별개의 64비트 전용 자료형을 제안했습니다. 이건 미쳤어요!)
  • PHP는 0로 시작하는 8진수 문법을 지원합니다. 그러므로 예를 들어 012는 숫자 10이 되겠지요. 하지만 08는 숫자 0이 됩니다. 8이나 9, 그리고 그 뒤에 오는 숫자는 전부 사라집니다. 01c는 문법 오류입니다.
  • pi는 함수입니다. M_PI는 상수입니다.
  • 0x0+2의 결과는 4입니다. 파서는 2를 16진수 리터럴과 별도의 10진수 리터럴 양쪽 전부로 처리합니다. 즉,0x002 + 2로 처리한다는 뜻입니다. 0x0+0x2 역시 비슷한 문제를 발생합니다. 그런데 이상하게도 0x0 +2 역시4이지만, 0x0+ 2는 2입니다. (5.4에서 고쳐졌지만, 같은 버전에서 0b 리터럴에 대해서 다시 문제가 발생했습니다. 0b0+1가 2가 되는 걸로.)
  • 지수승 연산자가 없습니다. pow 함수만 있습니다.

텍스트

  • 유니코드 지원이 없습니다. ASCII만 신뢰성있게 동작합니다. 정말로요. 위에서 언급한 대로 mbstring 확장이 있는데 엉망입니다.
  • 즉 기본 문자열 처리 함수에 UTF-8 문자열을 집어넣으면 깨질 수도 있다는 각오를 해야 합니다.
  • 비슷하게 예를 들어 ASCII 밖의 문자의 대소문자 비교에 대한 고려도 없습니다. 대소문자를 구분하지 않는 함수들이 넘쳐나는데도 불구하고 é와 É를 비교하지 못합니다.
  • 변수 보간(variable interpolation)에서 키를 따옴표로 감쌀 수 없습니다. 예를 들어 "$foo['key']"는 문법 오류를 냅니다. 따옴표를 없애거나 (그럴 경우 경고문이 뜹니다!) ${...}/{$...}를 쓸 수밖에 없습니다.
  • "${foo[0]}"는 괜찮아요. "${foo[0][0]}"는 문법 오류를 냅니다. $를 안쪽에 넣으면 둘 다 문제가 없습니다. Perl 문법의 열화 카피인 걸까요? (거기다가 의미는 완전히 다른)

배열

Oh, man.

  • PHP의 배열은 리스트, 순서 있는 해쉬, 순서 있는 집합, 희소 리스트, 그리고 때때로 이것들을 이상하게 섞어놓은 것을 하나로 만든 것입니다. 이게 어떻게 동작할까요? 이걸 쓰면 메모리를 얼마나 먹을까요? 그걸 대체 누가 알까요? 어쨌든간에 다른 선택지는 없습니다.
  • =>는 연산자가 아닙니다. array(...)나 foreach 안에서만 존재하는 특별한 문법입니다.
  • 원소값에 음수를 줄 수 없습니다. -1는 0처럼 유효한 키값입니다.
  • 이것이 PHP의 유일한 자료 구조임에도 불구하고 단축 표현이 없습니다. 실은 array(...)가 단축 표현이죠. (PHP 5.4에서 "리터럴"이 생겼습니다. [...])
  • => 문법은 Perl에서 따 온 것입니다. Perl에서는 따옴표 없이 foo => 1처럼 쓸 수 있게 해 줍니다. (사실 이렇게 따옴표를 생략할 수 있게 해 주는 게 Perl에서 => 연산자가 존재하는 이유입니다. 이 차이 외에는 그냥 콤마와 같습니다) PHP에서는 경고를 발생시키지 않고 이렇게 하는 것이 불가능합니다. 문자열 키에 따옴표를 쓰지 않고 해시를 만드는 걸 이런 식으로 할 수 있는 언어는 없습니다.
  • 배열 함수들은 배열과 해시, 이 둘이 섞인 경우 혼란스럽고 일관성 없는 동작을 합니다. 예를 들어 "배열의 차이를 계산하는" array_diff 함수를 보시죠.

    $first  = array("foo" => 123, "bar" => 456);
    $second = array("foo" => 456, "bar" => 123);
    echo var_dump(array_diff($first, $second));

    이 코드는 어떻게 동작할까요? 만약 array_diff 함수가 인자를 해시로 인식한다면 분명히 이것들은 같은 키에 다른 값을 가지고 있으니 다르겠지요. 만약 리스트로 인식한다면 다를 겁니다. 배열의 순서가 다르거든요.

    사실 array_diff 함수는 이 둘을 같은 것으로 봅니다. 왜냐 하면 이것을 집합으로 인식하거든요. 값만 비교하고 순서는 무시해 버립니다.

  • 비슷한 맥락에서 array_rand는 배열의 랜덤한 키값을 선택하는 이상한 동작을 합니다. 목록에서 뭔가를 선택할 일이 있을 때의 대부분의 공통적인 케이스에서 도움이 안 되죠.

  • PHP 코드가 얼마나 키값의 순서를 보존하는데 의존하는지에도 불구하고

    array("foo", "bar") != array("bar", "foo")
    array("foo" => 1, "bar" => 2) == array("bar" => 2, "foo" => 1)

    만약 배열이 섞여 있으면 어떤 일이 벌어지는지 파악하는 것은 독자들에게 맡깁니다. (전 몰라요.)

  • array_fill 함수는 길이가 0인 배열을 만들 수 없습니다. 대신 경고를 발생시키고 false를 반환합니다.
  • (거의) 모든 정렬 함수들이 정렬을 안에서 하고 아무것도 반환하지 않습니다. 정렬된 새 배열을 만드는 것은 불가능합니다. 배열을 직접 복사하고 거기에 다시 정렬을 해서 써야 합니다.
  • 하지만 array_reverse 함수는 새 배열을 반환합니다.
  • 순서가 있는 것들의 목록과 키와 값의 매핑이라는 개념은 함수 인자를 다루는데 적합하게 보이지만 쓰이지 않습니다.

배열이 아닌 것

  • 표준 라이브러리에는 "Quickhash"라는 것이 있는데 해쉬를 구현하기 위한 "특정한 강타입 클래스"의 객체지향 구현이라고 합니다. 그리고 서로 다른 키와 값 자료형의 조합으로 이루어진 4개의 클래스가 있습니다. 상대적으로 성능이 얼마나 나오는지, 왜 기본 배열 구현이 이렇게 정말 흔한 케이스에 최적화되지 않았는지 모르겠습니다.
  • 배열을 감싸서 객체처럼 보이게 해주는 (5개의 서로 다른 인터페이스로 이루어진) ArrayObject 클래스가 있습니다. 사용자 클래스는 같은 인터페이스를 구현할 수 있습니다. 하지만 몇 개의 메소드만 있을 뿐이고 절반 가까이는 내장 배열 함수처럼 보이지 않으며 배열을 받는 내장 함수는 ArrayObject나 다른 배열과 비슷한 클래스를 어떻게 다룰 지 모릅니다.

함수

  • 함수는 데이터가 아닙니다. 클로져는 실제로 객체지만 일반 함수는 아닙니다. 심지어 그 함수의 이름만 가지고 참조할 수도 없습니다. var_dump(strstr)는 리터럴 문자열 "strstr"로 예상하고 경고를 출력합니다. 임의의 문자열과 함수 "참조"를 알아차릴 방법은 없습니다.
  • create_function은 기본적으로 eval의 래퍼입니다. 이것은 정규 이름을 가진 함수를 만들어서 전역에 설치합니다. (그래서 가비지 컬렉팅이 되지 않습니다. 루프 안에서 쓰지 마세요!) 이 함수는 현재 범위에 대해서 전혀 모르기 때문에 클로져가 아닙니다. 이름에는 NUL 바이트가 들어가 있기 때문에 일반 함수와 충돌하지 않습니다. (왜냐하면 파일의 어딘가에 NUL이 들어가 있으면 PHP 파서가 실패해버리기 때문입니다)
  • __lambda_func라는 함수를 선언하면 create_function가 동작하지 않습니다. 실제 구현은 __lambda_func라는 함수를 만들어서 내부적으로 깨진 이름을 바꾸는 eval인데, __lambda_func라는 이름이 이미 있으면 처음 단계에서 치명적 오류를 뱉게 되는 것입니다.

기타

  • NULL을 증가(++)시키면 1이 됩니다. 감소(--)시키면 NULL이 됩니다. 비슷하게 문자열을 감소시키면 아무 것도 변하지 않습니다.
  • 제네레이터(generator)가 없습니다.

웹 프레임워크

실행

  • php.ini 파일 하나가 PHP의 엄청나게 많은 기능들을 조종하고 무엇을 언제 오버라이드할지를 결정하는 복잡한 규칙을 도입합니다. 보통 임의의 장치에 설치되는 PHP 소프트웨어는 환경에 맞추도록 설정을 오버라이드해야 하기 때문에 php.ini 같은 메커니즘을 사용하는 것은 이해할 수 없습니다.
    • PHP는 php.ini파일을 여러 곳에서 읽기 때문에, (그렇지 않을 수도 있지만) 기본 파일을 덮어 씌울 수도 있습니다. 하지만 실제로 읽는 것은 파일 하나 뿐이기 때문에, 원하는 세팅만 덮어 씌우는 것은 불가능합니다.
  • PHP는 기본적으로 CGI 형식으로 동작합니다. 매번 페이지를 읽을 때마다 PHP는 실행하기 전에 모든 코드를 재컴파일합니다. 심지어 Python의 장난감 프레임워크의 개발 서버도 이런 식으로 돌지는 않습니다.

    이로 인해 한번 컴파일하고 PHP 코드를 어떤 다른 언어만큼 빠르게 가속시킨다는 "PHP 가속기" 시장이 생겨났습니다. PHP의 배후에 있는 회사인 Zend는 이것을 자신들의 수익 모델로 포함시켰습니다.

  • 한동안 PHP 오류는 기본적으로 클라이언트쪽으로 뿌려졌습니다. 아마 개발 중에 도움이 되라고 그렇게 했을 겁니다. 지금은 더이상 그렇지 않은 것 같지만 여전히 가끔씩 페이지의 상단에 mysql 오류를 볼 때가 있습니다.

  • PHP는 특정 쿼리 인자를 넘기면 PHP 로고를 출력하는 등의 이상한 "이스터 에그"로 가득합니다. 이것은 당신의 어플리케이션을 개발하는데 완전히 관계없는 것이긴 하지만 PHP를 사용하고 있는지 확인하는 수단이 될 수 있습니다. (거기에 대략적인 버젼까지) mod_rewrite를 쓰던 FastCGI를 쓰던 리버스 프록싱을 하던 Server: 설정을 바꾸던 관계 없습니다.
  • <?php … ?> 태그 바깥의 공백 (심지어 라이브러리까지)은 리터럴 텍스트로 간주되어 응답에 섞여집니다. (그리고 "헤더가 이미 보내짐" 오류를 일으키기도 합니다) 유명한 해결 방법으로는 ?> 토큰을 버리는 겁니다. PHP는 불평하지 않을 것이고 뒤에 줄넘김이 붙지도 않을 것입니다.

배치 (deployment)

배치는 주로 PHP의 가장 큰 장점 중 하나로 꼽혀 왔습니다. 그냥 파일만 몇개 놓으면 끝이라는 거죠. 정말로 Python이나 Ruby, Perl보다 모든 과정이 쉽긴 하지만 미비한 점이 많습니다.

전반에 걸쳐서 저는 웹 어플리케이션을 앱 서버로 돌리고 리버스 프록싱을 하는 것을 선호합니다. 설정하는데 그렇게 노력이 들지도 않고 장점은 충분합니다. 웹 서버와 앱을 따로 관리할 수 있으며 웹 서버를 더 설치할 필요 없이 여러 개의 앱 프로세스를 동시에 돌릴 수 있으며 다른 사용자로 앱을 돌리는 것도 쉬우며 웹 서버를 교체할 수도 있으며 웹 서버를 건드리지 않고 앱을 내릴 수도 있으며 FIFO 시점만 조정하는 것으로 빈틈없이 배치할 수 있습니다. 웹 어플리케이션을 웹 서버와 합치는 것은 불합리하며 그렇게 해서 얻을 수 있는 이점은 없습니다.

  • PHP는 기본적으로 Apache에 묶여 있습니다. 따로 실행시키거나 다른 웹 서버에서 돌리는 것은 다른 언어로 배치하는 것과 비슷한 (혹은 더 많은) 수준의 삽질을 요합니다.
  • php.ini는 어디에서나 동작하는 모든 PHP 어플리케이션에 적용됩니다. 단지 하나의 php.ini 파일이 있을 뿐이고 전역적으로 적용됩니다. 만약 공유된 서버에서 설정을 고쳐야 하거나 두개의 어플리케이션을 돌리는데 둘이 서로 다른 설정이 필요하다면? 당신은 운이 없는 겁니다. 만약 할 수 있다면 모든 필요한 설정들의 합집합을 만들고 앱 내부에서 ini_set를 사용하거나 .htaccess 파일을 체크하는 식으로 줄여가야 합니다. 그리고 와, 설정이 어떻게 값을 읽어오는지 확인하기 위해 체크해야 할 엄청나게 많은 곳들이 있네요.
  • 비슷하게 PHP 앱과 거기에 의존된 것들을 쉽게 "감쌀" 수 있는 방법은 없습니다. 두 개의 어플리케이션이 서로 다른 라이브러리나 PHP 버젼을 요구한다면? Apache를 새로 빌드하는 것부터 시작해야겠군요.
  • 이 "파일 묶음" 방식은 게다가 라우팅을 엄청나게 고통스럽게 만듭니다. URL 계층 구조가 전체 코드 트리이기 때문에 어떤 것들을 보이게 만들고 어떤 것들을 보이지 않게 만들어야 하는지를 결정할 화이트리스트와 블랙리스트를 신중하게 만들어야 합니다. 설정 파일이나 다른 "일부분"들은 직접 불러오는 것을 막기 위해 C 스타일의 가드를 넣어야 합니다. 버젼 컨트롤 찌꺼기 (예를 들어 .svn) 역시 보호가 필요합니다. mod_php에서는 파일시스템의 모든 것들이 잠재적인 진입점이 됩니다. 앱 서버에서는 단 하나의 진입점과 어떤 URL이 보여질 것인지에 대한 컨트롤만 있으면 됩니다.
  • CGI 스타일의 파일 묶음을 빈틈없이 업그레이드할 수 있는 방법은 없습니다. 만약 업데이트의 중간에 사이트에 접속하면 사이트가 깨지거나 이상한 동작을 하게 되겠죠.
  • Apache에서 PHP를 돌리도록 설정하는 것이 "간단"함에도 불구하고, 거기에조차 몇 가지의 보이지 않는 함정이 숨어 있습니다. PHP 문서에서 .php 파일을 PHP로 돌리기 위해서 SetHandler를 권장하는데, AddHandler는 잘 동작하는 것처럼 보이지만 여기에 문제가 있습니다.

    AddHandler를 사용할 때 당신은 Apache에게 "이것을 PHP로 실행하라"는 것이 .php 파일을 핸들링하기 위한 유일한 방법이라고 전달합니다. 하지만! Apache는 파일 확장자에 관해서 지구상의 어떤 인간들과도 다른 아이디어를 가지고 있습니다. Apache는 가령 index.html.en 같은 파일은 HTML과 영어로 인식하도록 설계되었습니다. Apache에게 파일은 여러 개의 확장자를 동시에 가질 수 있는 것입니다.

    만약 파일 업로드 폼이 있고 그것을 통해 어떤 public 디렉토리에 파일을 저장할 수 있다고 상상해 봅시다. 사람들이 PHP 파일을 올리지 못하게 하기 위하여 .php 확장자를 검사하는 기능을 넣을 것입니다. 만약 공격자가foo.php.txt라는 파일을 업로드하면 문제없이 업로드가 될 것입니다. 하지만 Apache는 그것을 PHP로 인식하고 실행하게 될 것입니다.

    문제는 "원래 파일 이름을 사용하지 않는다", "꼼꼼히 검사하지 않는다" 같은 것이 아닙니다. 문제는 웹 서버가 어디에나 널려 있는 코드를 실행시킬 수 있다는 점입니다. 이것은 PHP가 "배치하기 쉽다"는 것과 일치하는 속성이기도 합니다. CGI는 +x가 필요합니다. 하지만 PHP에서는 그런 것을 할 필요가 없습니다. 그리고 이것은 단지 이론적인 문제가 아닙니다. 이런 문제를 겪은 여러 사이트들을 봤습니다.

부재하는 기능들

아래의 기능들 모두 웹 어플리케이션을 개발할 때 여러가지 면에서 중요하다고 생각합니다. PHP가 "웹 프로그래밍 언어"로 팔리고 있기 때문에 이런 기능들의 일부라도 구현되어 있으면 합리적일 것입니다.

  • 템플릿 시스템의 부재. PHP 그 자체가 템플릿 시스템이지만 프로그램이 아닌 하나의 거대한 인터폴레이터 형식은 아니어야 합니다.
  • XSS 필터의 부재. "htmlspecialchars 쓰는 걸 잊지 마세요" 같은게 아니라 이런 거요.
  • CSRF 보호 기능의 부재. 직접 만들어 써야 합니다.
  • 일반화된 표준 데이터베이스 API의 부재. PDO 같은 것들은 모든 개별 데이터베이스 API를 추상화해서 차이를 없애야 합니다.
  • 라우팅의 부재. 당신의 웹사이트는 파일시스템과 일치할 것입니다. 많은 개발자들은 mod_rewrite (와 일반적으로.htaccess)를 사용하는 것이 괜찮은 대안이라고 속아 왔습니다.
  • 인증 허가 기능의 부재
  • 개발 서버 기능의 부재
  • 실시간 디버깅 기능의 부재
  • 일관성 있는 배치 기능의 부재. 단지 "파일들을 전부 서버로 복사하세요" 뿐.

보안

언어의 경계

PHP의 보안에 대한 나쁜 평판의 대부분이 임의의 언어로 된 데이터를 받아서 다른 언어로 그대로 내놓는다는 점에서 옵니다. "<script>"는 SQL에서는 아무런 의미가 없지만 물론 HTML에선 있습니다.

이걸 더욱 악화시키는 것은 "입력을 정화(sanitize)하십시오" 라는 공통적인 외침입니다. 이건 완전히 잘못된 것입니다. 한번 휘두르면 데이터 더미를 본질적으로 "깨끗하게" 만드는 마법의 지팡이 같은 것은 없습니다. 단지 필요한 것은 언어를 쓰는 것입니다. SQL에 플레이스홀더(placeholder)를 단다거나 프로세스를 호출할 때 인자 목록을 쓴다거나 등등....

  • PHP는 명시적으로 데이터를 "정화"하는 것을 권장합니다. 그리고 그것을 위한 데이터 필터 확장까지 있습니다.
  • addslashesstripslashes 같은 슬래쉬 관련 함수들은 완전히 말도 안되고 도움도 안되는 빨간 청어(역주: 중요한 것에서 집중을 분산시키는 것이라는 의미)입니다.
  • 제가 말씀드릴 수 있는 것은 안전하게 외부 프로세스를 호출할 수 있는 방법은 없다는 겁니다. 단지 쉘로 명령을 내려서 실행시킬 수 있을 뿐입니다. 미친 듯이 이스케이핑을 하고 기본쉘이 제대로 이스케이핑 해제를 하는 것을 기대하거나 pcntl_fork와 pcntl_exec를 쓰는 방법 외에는 없습니다.
  • escapeshellcmd와 escapeshellarg 두개가 거의 비슷한 설명과 함께 있습니다. 하지만 escapeshellarg는 윈도에서 돌지 않고 (Bourne 쉘의 형식을 가정하기 때문에) escapeshellcmd는 단지 마침표를 공백으로 치환할 뿐입니다. (뭔가를 하려고 하면 그냥 조용하게 죽어버릴 수도 있습니다)
  • 여전히 널리 쓰이는, 기본 내장된 MySQL 바인딩에서는 준비된 선언(prepared statement)을 만드는 방법이 없습니다.

지금까지 SQL 인젝션에 대한 PHP 문서에서는 타입 검사, sprintf나 is_numeric 사용,mysql_real_escape_string을 어디서든지 직접 사용, (심지어 도움이 될지 안될지 모르는!)addslashes를 직접 사용하는 등의 제 정신이 아닌 것 같은 대응 방법을 추천하고 있습니다. 유저 코멘트란을 제외하고 PDO나 파라미터화(parameterization)의 언급은 하나도 없습니다. 2년 전에 PHP 개발자들에게 이 점을 매우 구체적으로 이야기했고 개발자들은 들었습니다. 하지만 이 페이지는 아직까지 바뀌지 않았습니다.

기본 상태가 취약함

  • register_globals. 한동안 기본적으로 꺼져 있었고 5.4에서는 없어졌습니다. 뭐 상관 없습니다. 이건 부끄러워 할만한 것입니다.
  • include 또한 HTTP URL을 받아들일 수 있습니다.
  • Magic quote. 안전한 상태에 가깝긴 하지만 여전히 개념을 제대로 이해하고 있는 것과는 거리가 멀어 보입니다.
  • (진짜로) PHP의 XML지원을 이용해서 네트워크 정보를 얻을 수 있습니다. 파일 네임을 URL로 사용하는 것을 광범위하게 지원하고있는 점을 악용해서요. libxml_disable_entity_loader()만이 이걸 막을 수 있습니다. 더 문제는 이 내용이 소스코드 내 주석에만 적혀있다는 거죠.

핵심

PHP 인터프리터 자체에 정말 끝내주는 보안 문제가 몇 차례 있었습니다.

  • 2007년 인터프리터에 정수 오버플로우 취약점이 발견되었습니다. 수정은 if (size > INT_MAX) return NULL;로 시작되어서 그대로 막장으로 갔습니다. (C에 익숙하지 않은 분들을 위해서: INT_MAX는 하나의 변수에 담을 수 있는 가장 높은 숫자입니다. 이제 나머지를 이해하실 수 있을 거라고 믿습니다.)
  • 더 최근에, PHP 5.3.7에서 crypt() 함수로 인해 누구나 패스워드 없이 로그인을 할 수 있는 버그가 발견되었습니다.
  • PHP 5.4는 서비스 거부 공격(DoS)에 취약한데, Content-Length 헤더(임의로 설정가능한)를 받아서 그만큼의 메모리를 할당하려고 시도하기 때문입니다. 이건 매우 나쁜 아이디어입니다.

찾아보면 더 나오겠지만 X개의 보안 취약점이 있다는 것은 요지가 아닙니다. 소프트웨어는 버그가 있고 뭐든 일어날 수가 있기 때문이죠. 하지만 이것들의 본질이 끔찍하다는 겁니다. 그리고 이것들은 제가 찾은 것이 아닙니다. 최근 몇 달동안 갑자기 저희 집 문 앞에 나타났을 뿐입니다.

결론

몇몇 코멘트에서 저한테 결론이 없다는 것을 잘 지적했습니다. 네, 뭐 저는 결론이 없습니다. 여기까지 읽어보셨다면 시작하기도 전에 저에게 동의했을 거라고 추정했죠.

PHP만 알고 있는 상태고 다른 것을 배울 의향이 있다면 Python 튜토리얼을 읽어보시고 웹 개발에서는 Flask를 써 보세요. (저는 템플릿 언어의 팬은 아닙니다만 그래도 괜찮긴 합니다.) 그것은 여러분의 앱을 나누지만 모두 다 같은 조각들이고 충분히 친숙하게 보일 겁니다. 여기에 대해서는 나중에 여기에 속하지 않는 언어와 웹 스택 전반을 소개하는 블로그 포스팅에서 따로 다루겠습니다.

그리고 이후에 더 큰 프로젝트를 하실 거라면 중간 레벨에 있는 Pyramid가 좋습니다. Django도 있습니다. Django 사이트와 비슷한 것을 만들기에 적합한 엄청나게 거대한 괴물이죠.

만약 개발자가 아닌데 어떤 이유로 이 글을 읽으셨다구요? 지구상의 모든 사람들이 Learn Python The Hard Way를 정독하기 전까지 전 행복해지지 않을 겁니다. 그러니 읽으세요.

써본 적은 없지만 Ruby와 Rails나 경쟁자들이 있고 Perl 역시 Catalyst와 함께 여전히 잘 살아 있습니다. 끊임없이 읽고 배우고 만들고 열중하세요.

감사의 말

이하에 감사드립니다.