환영합니다!

웹상에는 오래되어 잘못된 정보들이 많습니다. 이런 정보들 때문에 새로 PHP를 접하는 사용자들이 잘못된 길로 접어들고, 잘못된 습관과 안전하지 못한 코드를 양산하게 됩니다. PHP: The Right Way 는 PHP 를 사용하는 사람들에게 널리 받아들여진 코딩 표준, 권위있는 튜토리얼로의 링크를 제공하며, 이 프로젝트에 참여하고 있는 공헌자들이 생각하기에 현 시점에서 PHP의 베스트 프랙티스라고 할 수 있는 내용들을 포함하고 있습니다.

이것이 PHP를 사용하는 올바른 방법이다 라는 규범은 존재하지 않습니다. 이 웹사이트는 PHP를 사용하는 사람들이 그동안 생각해보지 않았을지도 모를 몇몇 주제들을 너무 늦지 않게 제공할 수 있기를 바라고 있으며, 생각해 볼 틈도 없이 몇년이고 반복적으로 해 왔을 몇몇 주제들에 대한 새로운 아이디어를 전달하려고 합니다. 또한 이 웹사이트는 여러분에게 어떤 도구를 사용하라고 이야기하지는 않을 것입니다. 대신 여러 가지 선택 가능한 대안을 제시하고 각각의 선택지가 취하는 접근 방식과 사용 패턴의 차이점을 설명하려고 노력할 것입니다.

이 웹사이트는 살아있는 문서이고자 합니다. 좀 더 도움되는 정보와 예제가 있다면 항상 업데이트 될 것입니다.

번역본

PHP: The Right Way 는 다양한 언어로 변역되어 있습니다.

Book

The most recent version of PHP: The Right Way is also available in PDF, EPUB and MOBI formats. Go to Leanpub

프로젝트에 기여하기

여러분의 도움이 있으면 이 사이트를 PHP 프로그래밍을 새로 시작하는 사람들을 위한 최고의 문서로 만들 수 있습니다! GitHub 저장소를 통해서 도와주세요.

널리 알려주세요!

여러분의 웹사이트에 걸어둘 수 있는 PHP: The Right Way 배너 이미지들이 있습니다. 여러분이 PHP: The Right Way 를 성원하고 있다는 것을 보여주고, 또 새로운 PHP 개발자들이 어디에 가면 좋은 정보를 찾을 수 있는지 알 수 있게 해주세요.

배너 이미지 보기

Back to Top

시작하기

안정된 최신버전(7.1)을 사용하세요

지금 PHP를 새로 시작하려는 경우, 현재 안정된 버전인 PHP 7.1를 사용하는 것이 좋습니다. PHP 7.1은 최근에 출시되었으며, 오래된 5.x 버전에 비해서 놀라운 많은 새로운 기능이 추가되었습니다. 엔진은 대부분 재작성되어 현재 PHP는 이전 버전들보다 훨씬 빠릅니다.

가까운 기간 동안에는 대부분 PHP 5.x가 사용되는 것을 볼 수 있을텐데, 최신 5.x 버전은 5.6입니다. PHP 5.6도 나쁘지는 않지만 2018년 이후로는 보안 업데이트가 지원되지 않기때문에, 최신 안정버전으로 빨리 업그레이드하는 것이 좋습니다. 하위 호환성 문제가 많지 않기때문에 업그레이드는 정말 간단합니다. 어떤 버전에 함수나 기능이 추가되었는지 잘 모르겠다면 php.net 웹사이트에서 PHP 문서를 확인할 수 있습니다.

내장 웹 서버

PHP 5.4 이상의 버전은 다른 웹서버의 설치와 설정 없이 PHP를 바로 시작할 수 있습니다. 웹서버를 실행하려면 여러분의 프로젝트 폴더에서 다음과 같은 커맨드를 실행하면 됩니다.

> php -S localhost:8000

맥(Mac)에서 사용하기

OS X 에는 기본적으로 PHP 가 포함되어 있지만, 최신의 안정된 버전이 포함되어 있지는 않습니다. Mavericks 에는 PHP 5.4.17 버전이 포함되어 있고, Yosemite 에는 5.5.9, El Capitan 에는 5.5.29, Sierra에는 5.6.24 버전이 포함되어 있습니다. 그러나 PHP 7.1이 나왔기 때문에 이러한 기본 버전으로는 충분치 않습니다.

OS X에 PHP를 설치하기 위해선 여러가지 방법이 있습니다.

Homebrew를 사용하여 설치하기

Homebrew는 OSX에서 PHP와 다양한 익스텐션을 쉽게 설치할 수 있도록 도와주는 패키지 관리자 도구입니다. Homebrew PHP는 Homebrew에서 사용되는 PHP와 관련된 다양한 “formulae”를 포함하고 있는 저장소입니다. 그리고 이는 PHP를 설치할 수 있도록 해줍니다.

brew install이라는 커맨드를 이용하여 php53, php54, php55, php56, php70, php71 을 설치할 수 있습니다. 그리고 PATH 변수를 수정하여 설치되어있는 다양한 버전의 PHP를 변경할 수 있습니다. 이러한 과정이 번거롭다면, brew-php-switcher를 사용하여 자동으로 버전을 변경할 수 있습니다.

Macports를 사용하여 설치하기

MacPorts는 OS X에서 커맨드라인이나 X11, Aqua기반의 오픈소스 소프트웨어를 컴파일하고, 설치하고, 업그레이드하는 작업을 간편하게 할 수 있는 시스템을 설계하는 오픈소스 커뮤니티 프로젝트입니다.

MacPorts는 미리 컴파일된 바이너리를 지원하여 의존성 패키지들을 설치할 때마다 매번 재컴파일하지 않아도 됩니다. 그래서 시스템에 어떠한 패키지도 갖고 있지 않다면 굉장한 시간을 절약할 수 있습니다.

port install이라는 커맨드를 이용하여 php53, php54, php55, php56, php70, php71 을 설치할 수 있습니다. 예를 들면:

sudo port install php56
sudo port install php71

그리고 select 커맨드를 이용하여 활성화된 PHP 버전을 변경할 수 있습니다.

sudo port select --set php php71

phpbrew를 사용하여 설치하기

phpbrew는 여러가지 PHP버전을 설치하고 관리하기 위한 도구입니다. 두 개의 서로 다른 어플리케이션/프로젝트가 다른 버전의 PHP를 요구하지만, 가상 머신은 사용하고 있지 않을때 매우 유용합니다.

Liip’s binary installer를 사용하여 PHP 설치하기

다른 유명한 방법은 5.3에서 7.1 버전까지 한줄로 설치할 수 있는 방법인 php-osx.liip.ch가 있습니다. 이 방법은 Apple에서 설치한 PHP를 덮어쓰지 않고 별도의 위치(/usr/local/php5)에 설치합니다.

소스 컴파일하기

설치한 PHP 버전을 조작할 수 있는 또 다른 옵션으로는 직접 설치가 있습니다. 이때 애플 맥 개발자 센터에서 내려받기 가능한 Xcode 또는 XCode를 위한 커맨드라인 도구를 설치하셔야 합니다.

통합(All-in-One) 솔루션

위에 설명된 방법은 PHP 그 자체만을 다루며, Apache, Nginx 혹은 SQL 서버 등은 제공하지 않습니다. MAMPXAMPP와 같은 통합 솔루션은 이러한 소프트웨어들을 함께 사용하기 쉽게 한번에 설치됩니다. 그러나 쉽게 설치할 수 있는 만큼 유연하지 못한 단점이 존재합니다.

윈도우(Windows)에서 사용하기

PHP 바이너리 다운로드 사이트에서 바이너리를 다운로드 받을 수 있습니다. PHP 압축을 푼 뒤에는 PHP 루트 폴더(php.exe가 있는 폴더)를 PATH에 설정하여 PHP를 어느 곳에서나 실행할 수 있도록 하는 것이 좋습니다.

학습 혹은 로컬에서의 개발이 목적이라면 PHP 5.4 부터 내장되어 있는 웹서버를 사용하면, 웹서버 설치와 설정에 대한 걱정없이 시작할 수 있습니다. 완전한 웹서버와 MySQL 데이터베이스 등을 포함한 “통합(All-in-One)” 패키지를 선호한다면 Web Platform InstallerXAMPP, EasyPHP, OpenServer, WAMP 등을 사용하여 빠르게 윈도우에서 개발 환경을 갖출 수 있습니다. 이런 패키지를 사용할 때는, 윈도우에서 개발하고 리눅스에 배포하는 식의 개발환경과 배포환경이 다른 환경의 차이에 주의해야 합니다.

윈도우에서 PHP 웹어플리케이션을 운영할 생각이라면 IIS7을 사용하는 편이 안정적이고 좋은 성능을 보여줄 것입니다. phpmanager(IIS7용 GUI 플러그인)을 사용하면 PHP를 설정하고 관리하는 작업이 쉬워집니다. IIS7에는 FastCGI가 포함되어 있어 단지 PHP를 핸들러로 설정해주기만 하면 됩니다. 자세한 정보는 iis.net에 있는 PHP 전용 섹션에서 볼 수 있습니다.

일반적으로 개발 환경과 프로덕션 환경이 다르면 라이브 할 때 이상한 버그가 발생할 수 있습니다. 윈도우즈에서 개발하고 리눅스(혹은 윈도우즈가 아닌 다른 어떤 환경)에 배포하고 있다면 버추얼 머신을 사용하는 것이 좋습니다.

Chris Tankersley 가 윈도우즈를 이용한 PHP 개발시 그가 사용하는 도구에 대한 매우 도움이 되는 블로그 글을 썻습니다.

Back to Top

코딩 스타일 가이드

PHP 커뮤니티는 매우 거대하고, 수많은 라이브러리와 프레임워크, 컴포넌트들이 존재합니다. 이렇게 많은 선택지들 중에 몇 가지를 골라 프로젝트에 적용하는 일은 PHP 개발자에게 일상적인 일입니다. 다양한 라이브러리를 조합하여 사용하는 일을 수월하게 하려면, 가능한한 공통적인 코드 스타일을 적용하는 일은 매우 중요합니다.

프레임워크 운용 그룹(Framework Interoperability Group)에서는 PSR-0, PSR-1, PSR-2, PSR-4라는 권장 스타일 가이드를 발표했습니다. 이들 가이드는 Drupal, Zend, Symfony, Laravel, CakePHP, phpBB, AWS SDK, FuelPHP, Lithium 등의 프로젝트에서 적용하기 시작한 스타일 규칙입니다. 여러분의 프로젝트에서 이러한 스타일 가이드를 사용할 수도있고, 혹은 여러분 자신만의 스타일을 그대로 사용할 수도 있습니다.

가능하다면 여러분은 알려진 표준 스타일에 맞춰 코드를 작성해야 합니다. 몇 가지 PSR을 조합하거나, PEAR나 Zend에서 만든 스타일 표준을 사용할 수도 있습니다. 그렇게 함으로써 다른 개발자들도 여러분의 코드를 쉽게 읽고 사용할 수 있으며, 많은 써드파티 라이브러리를 사용하면서도 어플리케이션 코드의 일관성을 유지할 수 있습니다.

PHP CodeSniffer라는 도구는 코드가 이들 가이드를 따르는지 확인할 수 있습니다. Sublime Text와 같은 텍스트 편집기에는 실시간으로 코드를 확인해주는 플러그인도 제공합니다.

다음 두가지 도구 중 하나를 사용하여 코드 구조를 자동으로 고칠 수 있습니다.

그리고, 쉘에서 phpcs를 다음과 같이 수동으로 실행할 수 있습니다.

phpcs -sw --standard=PSR2 file.php

이 명령은 에러를 보여주고, 어떻게 고쳐야할지 설명을 보여줄 것입니다. 이것을 git hook에 포함시키면 유용합니다. 그렇게 해서 정한 기준을 위반한 것을 포함한 브랜치는 위반한 것들을 고치기 전까지는 저장소에 넣지 못하게 할 수 있습니다.

PHP_CodeSniffer 가 있다면, 확인된 코드 레이아웃 문제를 PHP Code Beautifier and Fixer 를 사용하여 자동으로 바로잡을 수 있습니다.

phpcbf -w --standard=PSR2 file.php

다른 방법은 PHP Coding Standards Fixer 를 사용하는 것입니다. 이것은 코드 구조에 어떤 종류의 에러들이 존재하는 지 바로잡기 전에 보여줍니다.

php-cs-fixer fix -v --level=psr2 file.php

코드에 사용되는 모든 기호는 영어로 작성하는 것이 좋습니다. 주석은 코드를 사용할 사람들이 편하게 읽고 쓸 수 있다면 어떤 언어로 기록해도 됩니다.

Back to Top

주목할만한 언어 특징

프로그래밍 패러다임

PHP는 다양한 프로그래밍 테크닉을 지원하는 유연하고 동적인 언어입니다. PHP는 해가 지날수록 크게 진화해왔습니다. PHP 5.0(2004)에서 개체지향 프로그래밍(Object-oriented Programming) 개념이 추가되었고 PHP 5.3(2009)에서는 익명 함수와 네임스페이스가, PHP 5.4(2012)에서는 트레이트(trait) 개념이 추가되었습니다.

개체지향 프로그래밍

PHP는 클래스, 추상 클래스, 인터페이스, 상속, 생성자, 복제(cloning), 예외 등 완전한 개체지향 프로그래밍 개념을 가지고 있습니다.

함수형 프로그래밍

PHP는 일급 함수(first-class function)를 지원합니다. 이는 함수가 변수에 할당될 수 있다는 이야기입니다. 사용자가 정의한 함수나 내장 함수 모두 변수에 의해서 참조될 수 있고 동적으로 호출될 수 있습니다. 함수는 다른 함수의 인자로 전달될 수 있고 (_고차함수(Higher-order function)_라고 하는 기능) 함수가 다른 함수를 리턴값으로 리턴하는 것도 가능합니다.

함수가 자기 스스로 다시 호출하는 재귀 호출(Recursion)도 지원하지만, 대부분의 PHP 코드는 재귀보다는 반복(iteration)하는 형태로 작성됩니다.

익명 함수(와 클로저)는 2009년에 발표된 PHP 5.3부터 지원됩니다.

PHP 5.4에서는 클로저를 특정 개체의 영역에 바인딩하는 기능이 추가되었습니다. 또한 대부분의 경우 익명 함수와 동일하게 사용할 수 있는 호출가능한 타입(callable) 지원이 강화되었습니다.

메타 프로그래밍

PHP는 Reflection API와 특수 매서드(Magic Method)같은 메커니즘을 통해서 다양한 형태의 메타 프로그래밍을 지원합니다. __get(), __set(), __clone(), __toString(), __invoke() 등 개발자가 클래스의 동작에 끼어들 수 있도록 해주는 다양한 특수 매서드가 있습니다. Ruby 개발자들은 종종 PHP에는 method_missing 같은 기능이 없다고 얘기하는데, __call()__callStatic()을 이용하면 동일한 작업이 가능합니다.

네임스페이스

앞에서 얘기한 것처럼 PHP 커뮤니티에서는 많은 개발자들이 수많은 코드를 만들고 있습니다. 그렇기 때문에 서로 다른 PHP 라이브러리에 같은 이름의 클래스가 포함되어 있을 수 있습니다. 두 라이브러리가 같은 네임스페이스를 사용한다면 서로 충돌이 발생하여 문제가 될 수 있습니다.

네임스페이스 기능은 이런 문제를 해결할 수 있습니다. PHP 매뉴얼에 설명된대로, 네임스페이스는 OS의 디렉토리에 비교하여 설명할 수 있습니다. 두 개의 파일을 각각 다른 디렉토리에 넣는다면 두 파일의 이름이 같아도 상관이 없듯이, 두 개의 PHP 클래스를 각각 다른 네임스페이스에 둔다면 두 클래스의 이름이 동일해도 상관이 없습니다. 참 쉽죠?

다른 개발자가 사용할 가능성이 있는 코드를 작성한다면, 네임스페이스로 잘 감싸서 다른 라이브러리의 이름과 충돌이 발생하지 않게 하는 것이 좋습니다.

네임스페이스를 사용할 때 추천할 만한 방법이 PSR-4에 대략적으로 설명되어 있습니다. PSR-4는 파일 이름, 클래스 이름, 네임스페이스 이름에 대한 표준화된 규칙을 제공하여, 명시적으로 일일이 파일을 불러오지(include) 않아도 클래스를 자동으로 불러오는 등 플러그 앤 플레이 방식처럼 코드를 사용할 수 있게 하는데 목표를 두고 있습니다.

2014년 10월에 PHP-FIG는 기존의 오토로드(autoloading) 표준인 PSR-0를 폐지(deprecated)했습니다. PSR-0, PSR-4 두 가지 모두 사용가능한 상태인데, PSR-4는 PHP 5.3 이상에서만 적용이 되기 때문에 아직도 PHP 5.2 호환 프로젝트들은 PSR-0를 구현해 둔 상태입니다.

새로운 어플리케이션이나 패키지를 만들면서 오토로더(autoloader) 표준을 따를 생각이라면, PSR-4를 사용하면 됩니다.

표준 PHP 라이브러리

표준 PHP 라이브러리(SPL, Standard PHP Library)는 PHP와 함께 제공되는 클래스 인터페이스의 모음입니다. SPL에는 일반적으로 많이 사용되는 데이터 구조(스택, 큐, 힙 등)와 이러한 SPL 데이터 구조나 SPL 인터페이스를 여러분이 직접 구현한 클래스를 순회(traverse)하는데 사용할 수 있는 이터레이터들이 포함되어 있습니다.

커맨드라인 인터페이스

PHP는 웹어플리케이션 작성을 주요 목적으로 삼고 있지만, 커맨드라인 인터페이스(CLI) 프로그램을 만드는 데에도 유용하게 사용할 수 있습니다. 커맨드라인 PHP 프로그램은 테스트, 배포, 관리 등 일반적인 작업을 자동화하는데 도움을 줄 수 있습니다.

여러분의 웹어플리케이션 코드를 그대로 사용할 수 있다는 점이 CLI PHP 프로그램의 장점입니다. 웹 GUI 만들기나 웹 보안에 신경쓰지 않고도 말이죠. CLI PHP 스크립트를 웹어플리케이션 경로에 두지 않도록 조심하여아 합니다.

커맨드라인에서 PHP를 실행해봅시다.

> php -i

-i 옵션은 phpinfo() 함수와 동일하게 PHP 설정을 출력합니다.

-a 옵션은 Ruby의 IRB나 Python의 대화형 쉘 같은 대화형 쉘을 제공합니다. 이것 말고도 유용한 커맨드라인 옵션이 많이 있습니다.

간단한 “Hello, $name” CLI 프로그램을 만들어봅시다. 다음과 같이 hello.php 파일을 만들어 봅시다.

<?php
if ($argc !== 2) {
    echo "Usage: php hello.php [name].\n";
    exit(1);
}
$name = $argv[1];
echo "Hello, $name\n";

PHP는 스크립트가 실행될 때 주어진 인자를 가지고 특별한 변수 두 개를 설정합니다. $argc는 인자 개수를 나타내는 정수값이고, $argv는 각 인자 이 들어있는 배열입니다. 첫 번째 인자는 항상 PHP 스크립트 파일 이름입니다. 이같은 경우에는 hello.php가 들어있습니다.

exit() 표현식은 0이 아닌 숫자와 함께 사용하여, 커맨드가 실패했다는 것을 쉘에 알려주는데 사용합니다. 흔히 사용되는 종료 코드를 이곳에서 볼 수 있습니다.

커맨드라인에서 다음과 같이 실행해봅시다.

> php hello.php
Usage: php hello.php [name]
> php hello.php world
Hello, world

Xdebug

소프트웨어 개발에 가장 유용한 툴이라면 적절한 디버거를 꼽을 수 있습니다. 디버거는 코드의 실행을 따라가거나 스택의 내용을 살펴볼 수 있게 해줍니다. PHP용 디버거인 XDebug를 다양한 IDE와 함께 사용하여 중단점(Breakpoint)을 설정하거나 스택 내용을 볼 수 있습니다. 또한 PHPUnit과 KCacheGrind 등의 도구와 같이 사용하면 코드 커버리지 분석이나 프로파일링을 할 수도 있습니다.

여러분이 지금 var_dump()/print_r() 로는 해결할 수 없는 곤란한 상황에 빠져있는 상태라면 아마도 디버거를 사용해야만 하는 상황일지도 모릅니다.

XDebug 설치는 복잡할 수 있겠지만, 여러분이 로컬에서 개발하고 가상 머신이나 다른 서버에서 테스트하는 방식으로 일하고 있다면 XDebug가 제공하는 리모트 디버깅 기능을 당장 사용하고 싶을 것입니다.

일반적으로 Apache VHost 나 .htaccess 파일을 아래와 같이 변경합니다.

php_value xdebug.remote_host 192.168.?.?
php_value xdebug.remote_port 9000

“remote host”와 “remote port”에는 여러분의 IDE가 대기하는 로컬 컴퓨터와 포트 번호를 설정합니다. 그후 IDE를 “연결 대기 상태”로 만들어두고 아래와 같은 URL을 로딩하면 됩니다.

http://your-website.example.com/index.php?XDEBUG_SESSION_START=1

이제 IDE가 PHP 스크립트 수행을 가로채서, 코드에 중단점을 설정하거나 메모리에 있는 변수값을 살펴볼 수 있게 됩니다.

GUI가 제공되는 디버거를 사용하면 아주 쉽게 코드를 한 줄씩 실행해보거나 변수 값을 조사하고 실행 중에 코드를 실행시켜볼 수 있습니다. 다수의 IDE가 내장하고 있거나 플러그인 방식으로 제공하는 GUI 디버거는 xdebug 기반으로 동작합니다. MacGDBp는 IDE없이 독자적으로 동작하는, 오픈소스로 제공되는 Mac 용 GUI 디버거입니다.

Back to Top

의존성 관리

여러분이 선택하여 사용할 수 있는 수많은 라이브러리, 프레임워크, 컴포넌트가 존재하고 있습니다. 여러분의 프로젝트에서는 그 많은 것들 중에서 몇 가지를 사용하게 될 것입니다. 프로젝트의 의존성을 관리해야 할 필요가 생기는 것이죠. 얼마전까지도 PHP에는 프로젝트 의존성을 관리할 좋은 방법이 없었습니다. 여러분이 스스로 의존성을 관리한다고 하더라도 오토로더(autoloader)에 대해 신경을 써야만 합니다. 하지만 이제는 그러지 않아도 됩니다.

요즘은 ComposerPEAR라는 패키지 관리 시스템이 주로 사용되고 있습니다. Composer는 PHP를 사용하는데 있어 주된 패키지 관리자입니다. 그러나 옛날에는 PEAR가 그 역할을 꿰차고 있었습니다. PEAR를 직접 사용하지 않더라도, PEAR 패키지를 사용하고 있는 것들을 볼 수도 있기 때문에 PEAR에 대해서는 알아두는 것이 좋을 것입니다.

Composer와 Packagist

Composer는 훌륭한 PHP 의존성 관리자입니다. 프로젝트의 의존성을 composer.json 파일에 기록하고 간단한 커맨드 몇 개를 사용하면 Composer가 알아서 다운로드하고 오토로딩(autoloading) 설정을 해줍니다. Composer는 node.js의 NPM이나 Ruby의 Bundler와 같습니다.

이미 Composer와 호환되는 많은 PHP 라이브러리가 있기 때문에 프로젝트에서 바로 사용할 수 있습니다. Composer 패키지들은 Packagist라는 공식 저장소에서 관리됩니다.

Composer 설치

Composer를 다운로드 받는 가장 안전한 방법은 공식 설명을 따르는 것 입니다. 이 방법은 인스톨러가 손상되거나 부정하게 변경되지는 않았는지 확인합니다. 인스톨러는 Composer를 현재 작업 디렉토리에 로컬 설치합니다.

Composer를 (/usr/local/bin 등에) 글로벌 설치하기 권장하는데, 그렇게 하기 위해서는 다음 명령어를 추가로 실행합니다.

mv composer.phar /usr/local/bin/composer

참고: 권한때문에 실패한 경우 sudo를 붙여서 실행하세요.

To run a locally installed Composer you’d use php composer.phar, globally it’s simply composer.

윈도우에 Composer 설치

윈도우에 설치하는 가장 간단한 방법은 ComposerSetup 인스톨러를 사용하는 것입니다. 링크에서 설치 파일을 받아 설치하면, 시스템의 프로그램 설치 경로에 설치를 해주고 PATH 환경변수에도 추가해주기 때문에 어느 디렉토리에서 커맨드라인을 실행하든지 그냥 composer 명령을 실행할 수 있게 됩니다.

Composer 설치 (수작업으로)

Composer를 수작업으로 설치하는 건 고급 기술에 속한다고 할 수 있습니다. 하지만 자동설치에 비해서 수작업 설치를 더 좋아할 만한 이유는 다양합니다. 대화형 자동 설치 과정에서는 다음과 같은 내용을 확인합니다.

수작업으로 설치할 때에는 이러한 내용을 자동으로 확인해주는 과정이 전혀 없기 때문에, 자동 체크 과정을 포기하고도 충분한 이점이 있는지 결정하는 것은 여러분의 몫입니다. 수작업으로 설치하기로 결정했으면 아래와 같이 실행하면 됩니다.

curl -s https://getcomposer.org/composer.phar -o $HOME/local/bin/composer
chmod +x $HOME/local/bin/composer

$HOME/local/bin (이나 여러분이 설치하기로 결정한 다른 디렉토리)는 환경변수 $PATH에 포함되어 있어야 합니다. 그래야 composer 명령어를 실행할 수 있습니다.

이렇게 설치하고 나면 앞에서 Composer를 실행하려고 php composer.phar install라고 했던 것을 다음과 같이 줄여서 실행할 수 있습니다.

composer install

여러분이 Composer를 시스템에 글로벌하게 설치하였다고 가정하고 다음 항목을 보면 됩니다.

의존관계를 정의하고 설치하기

Composer는 프로젝트의 의존관계 정보를 composer.json라는 파일에 기록하여 관리합니다. 이 파일을 직접 수정할 수도 있고 Composer를 사용하여 수정할 수도 있습니다. composer require라는 명령어는 의존관계 정보를 추가해줍니다. composer.json 파일이 아직 없으면 파일을 생성해서 의존관계 정보를 추가해줍니다. Twig를 프로젝트 의존관계에 추가하는 예제가 아래에 있습니다.

composer require twig/twig:~1.8

이렇게 하는 대신 composer init 명령어를 사용하면 여러분의 프로젝트를 위한 완전한 composer.json 파일을 만들 수 있게 도와줍니다. 둘 중 어느 방법을 사용하든지, composer.json 파일을 만들고 나면 이제 Composer 를 이용하여 패키지를 vendors/ 디렉토리에 설치할 수 있습니다. 이는 composer.json 파일이 제공되는 다운로드 받은 프로젝트에도 동일하게 적용됩니다.

composer install

다음으로, 여러분이 작성하는 어플리케이션의 PHP 파일에 아래와 같은 내용을 추가하여 Composer의 오토로더(autoloader)를 사용한다는 것을 PHP에게 알려줍니다.

<?php
require 'vendor/autoload.php';

이제 여러분은 필요한 의존 라이브러리를 사용할 수 있습니다. 그 라이브러리들은 필요할 때 자동으로 로드될 것입니다.

의존관계 정보 업데이트하기

composer install 명령어를 처음 실행하면 Composer는 설치한 패키지들의 버전을 기록한 composer.lock 파일을 생성합니다. 프로젝트를 공유할 때 composer.lock 파일을 같이 포함시켜서, 다른 사람이 composer install 명령어를 실행했을 때 동일한 버전의 패키지를 받도록 하세요. 의존관계 정보를 업데이트하고 싶으면 composer update 명령어를 실행하면 됩니다. 배포할 때에는 composer update를 사용하지 말고 composer install를 사용하세요. 그렇지 않으면 배포 환경에는 다른 버전의 패키지를 사용하게 될 수도 있습니다.

이런 점은 여러분이 사용하는 패키지 버전을 유연하게 관리하려고 할 때 가장 유용할 것입니다. 예를 들어 ~1.8 이라고 버전을 지정한 것은 “1.8.0 보다는 높은 버전이지만 2.0.x-dev 버전보다는 낮은 버전”을 의미합니다. * 와일드카드 문자를 사용해서 1.8.* 이라고 표현하는 것도 동일한 의미입니다. composer update 명령어를 실행하면 지정된 제한 사항에 맞는 최신 버전으로 의존관계 정보를 업데이트해 줍니다.

업데이트 알림 받기

VersionEye라는 웹 서비스에 가입하여 새 버전 알림을 받을 수 있습니다. 여러분의 GitHub나 BitBucket 계정의 저장소에서 composer.json 파일을 모니터링하다가 새로운 패키지 업데이트가 나오면 메일로 알려주는 기능을 하는 서비스입니다.

의존 패키지들의 보안 이슈 확인하기

Security Advisories Checkercomposer.lock 파일을 확인하여 여러분의 의존관계를 업데이트해야 하는지 알려주는 웹 서비스와 커맨드라인 도구입니다.

Composer를 이용하여 전역 의존 패키지들 관리하기

Composer는 전역 의존성과 바이너리(실행파일) 또한 관리 가능합니다. 사용법은 아주 간단합니다. 그저 모든 커맨드 앞에 global만 붙이면 됩니다. PHPUnit을 인스톨 하고 싶고, 이것이 전역에서 사용가능하다면 다음과 같이 커맨드를 입력할 수 있습니다.

composer global require phpunit/phpunit

위 명령어는 의존 패키지들을 위치할 ~/.composer 폴더를 생성합니다(이미 있다면 그냥 넘어가겠죠?). 설치된 패키지가 어디에서든 실행되어야 할 바이너리(실행파일)를 갖고 있다면, 이를 실행하기 위해서 ~/.composer/vendor/bin폴더를 $PATH변수에 추가해야 합니다.

(역주 : 그리고 놀랍게도 Composer는 한국어 메뉴얼이 존재합니다!)

PEAR

몇몇 PHP 개발자들을 즐겁게 할 베테랑 패키지 매니저는 PEAR입니다. Composer와 비슷하게 동작하지만 몇 가지 살펴볼만한 차이점이 있습니다.

PEAR는 각 패키지들이 정해진 구조에 따르도록 하고 있습니다. 그래서 패키지 제작자들은 PEAR에서 사용할 수 있도록 준비해야 합니다. PEAR에 맞게 준비되지 않은 패키지들을 PEAR로 설치하는 것은 불가능합니다.

PEAR는 패키지를 시스템에 글로벌하게 설치합니다. 그러므로 한 번 패키지를 설치하면 모든 프로젝트에서 설치한 패키지를 사용할 수 있게 됩니다. 이런 점은 많은 프로젝트들이 같은 버전의 패키지를 사용한다면 장점으로 작용하지만 프로젝트들 사이에 버전 충돌이 난다면 문제가 될 수 있습니다.

PEAR 설치하기

.phar 인스톨러를 다운로드하여 실행하면 PEAR를 설치할 수 있습니다. PEAR 문서에 모든 OS에 대한 상세한 설치과정이 설명되어 있습니다.

리눅스를 사용한다면 각 OS의 패키지 매니저에서 찾아보는 것도 좋습니다. Debian과 Ubuntu는 php-pear 패키지를 제공합니다.

패키지 설치하기

원하는 패키지가 PEAR 패키지 리스트에 포함되어 있다면 패키지 이름을 지정해서 설치할 수 있습니다.

pear install foo

패키지가 다른 채널에서 호스팅되고 있다면 우선 채널을 discover해서 패키지 설치 시 같이 지정해주어야 합니다. 채널 사용안내 문서에서 관련된 정보를 볼 수 있습니다.

PEAR 의존관계를 Composer로 관리하기

Composer를 이미 사용하고 있을 때 PEAR 패키지도 설치하고 싶다면, PEAR 패키지 의존관계를 관리하는 데에 Composer를 사용할 수도 있습니다. 아래 예제는 pear2.php.net에서 코드를 설치하는 예제입니다.

{
    "repositories": [
        {
            "type": "pear",
            "url": "http://pear2.php.net"
        }
    ],
    "require": {
        "pear-pear2/PEAR2_Text_Markdown": "*",
        "pear-pear2/PEAR2_HTTP_Request": "*"
    }
}

"repositories" 섹션은 Composer가 PEAR 저장소를 “initialise”(PEAR 용어로는 “discover”)해야 한다는 것을 알려주는 역할을 합니다. 그리고 require 섹션은 패키지 이름을 아래와 같은 형식으로 정의합니다.

pear-채널/패키지

“pear”라는 접두사는 혹시 모를 충돌을 방지하기 위해서 하드코딩된 것입니다. 예를들어 pear 채널 이름이 다른 패키지 벤더 이름과 동일하다면 짧은 이름이나 전체 URL을 사용하여 채널을 지정할 수 있습니다.

이 코드를 설치하면 vendor 디렉토리에 설치가 되어 Composer 오토로더(autoloader)에 의해 자동적으로 로드될 수 있습니다.

vendor/pear-pear2.php.net/PEAR2_HTTP_Request/pear2/HTTP/Request.php

이 PEAR 패키지를 사용하려면 아래와 같이 간단히 할 수 있습니다.

<?php
$request = new pear2\HTTP\Request();

Back to Top

코딩 프랙티스

기초

PHP는 모든 레벨의 개발자들이 빠르고 효과적으로 프로그램을 제작할 수 있게 해주는 방대한 언어입니다. 언어에 적응하여 고급 레벨로 발전하다보면 우리는 종종 처음에 배운(혹은 대강 보고 넘어간) 기본기를 잊고 나쁜 습관에 물들기도 합니다. 이런 흔한 이슈에 맞서기 위해서 이 섹션에서는 PHP의 기초적인 코딩 프랙티스를 다시 떠올려보기로 합니다.

날짜와 시간

날짜와 시간을 읽고, 쓰고, 비교하고, 계산할 때 사용할 수 있는 DateTime 이라는 클래스가 있습니다. PHP에는 DateTime 외에도 날짜와 시간을 다루는 많은 함수들이 있지만 DateTime 클래스는 일반적인 사용 시나리오에 맞는 괜찮은 개체지향적인 인터페이스를 제공합니다. 시간대(time zone)을 다루는 기능도 갖추고 있지만 이 사이트의 짧은 소개에서 다룰만한 범위는 아닙니다.

DateTime 클래스를 사용하려면 문자열 형태로 표현한 날짜와 시간 정보를 createFromFormat() 팩토리 메소드를 사용해서 개체 인스턴스로 변경하거나, new DateTime 생성자로 현재 날짜와 시간 정보를 얻으면 됩니다. 그리고 DateTime 개체를 다시 문자열로 변경하려면 format() 메소드를 사용하면 됩니다.

<?php
$raw = '22. 11. 1968';
$start = DateTime::createFromFormat('d. m. Y', $raw);

echo 'Start date: ' . $start->format('Y-m-d') . "\n";

DateInterval 클래스를 이용해서 DateTime을 이용한 계산이 가능합니다. DateTime 클래스에는 DateInterval을 인자로 받는 add()sub() 같은 메소드가 있습니다. 하루의 길이가 매일 동일할 것이라고 가정하고 코드를 작성하지 않아야 합니다. 일광절약시간제(daylight saving time)나 시간대(timezon)의 변경에 따라서 하루의 길이는 달라질 수가 있기 때문입니다. 그러므로, 날짜 계산을 할 때 초단위의 숫자를 사용하는 대신 DateInterval을 사용하세요. 날짜의 차이를 계산하려면 diff() 메소드를 사용하면 됩니다. diff() 메소드는 DateInterval을 리턴하는데, 표시하기에도 아주 쉽습니다.

<?php
// $start를 복제한 다음 한 달하고 6일을 더한다.
$end = clone $start;
$end->add(new DateInterval('P1M6D'));

$diff = $end->diff($start);
echo 'Difference: ' . $diff->format('%m month, %d days (total: %a days)') . "\n";
// Difference: 1 month, 6 days (total: 37 days)

DateTime 개체를 일반적인 비교 연산자를 이용해서 비교하는 것이 가능합니다.

<?php
if ($start < $end) {
    echo "Start is before end!\n";
}

DateTime에 대한 마지막 예제는 DatePeriod 클래스에 대한 것입니다. 이 클래스는 반복적으로 발생하는 사건을 다루는데 사용됩니다. start와 end라는 두 개의 DateTime 개체와 시간 간격을 나타내는 DateInterval 개체 하나를 받아서, 지정된 기간에 발생하는 모든 사건을 리턴해줍니다.

<?php
// $start 와 $end 사이의 모든 목요일을 얻는다.
$periodInterval = DateInterval::createFromDateString('first thursday');
$periodIterator = new DatePeriod($start, $periodInterval, $end, DatePeriod::EXCLUDE_START_DATE);
foreach ($periodIterator as $date) {
    // 지정된 기간 안에 있는 모든 날짜를 출력한다.
    echo $date->format('Y-m-d') . ' ';
}

Carbon은 유명한 PHP API 확장입니다. DateTime 클래스의 모든 것을 상속하여 코드 수정은 최소화했지만, 지역화(Localization) 지원, DateTime 객체의 더 나은 덧셈, 뺄셈 연산 및 포메팅 방식 등과 함께 날짜와 시간을 지정하여 코드를 테스트해볼 수 있는 방법과 같은 추가 기능을 포함하고 있습니다.

디자인 패턴

어플리케이션을 제작할 때에 코드와 프로젝트 전체의 구조를 잘 알려진 패턴에 따라 구성하는 편이 좋습니다. 여러분 스스로 코드를 관리하기에도 더 편하고, 다른 개발자들이 여러분의 코드를 이해하기에도 더 수월하기 때문입니다.

만약에 특정 프레임워크를 선택하여 여러분은 고수준의 로직 코드만을 작성하는 방식으로 어플리케이션을 제작하기 시작했다면 이미 상당부분의 패턴은 프레임워크에 의해서 결정된 상태일 것입니다. 하지만 프레임워크 위에서 코드를 작성할 때에도 여러분이 작성하는 코드가 어떤 패턴을 따르는 것이 가장 적절한지 결정하는 일은 계속될 수 밖에 없습니다. 혹은 프레임워크에 의존하지 않고 어플리케이션을 제작하기로 결정했다면 제작하려는 어플리케이션의 형태와 규모에 가장 잘 맞는 패턴을 여러분이 직접 찾아야 할 것입니다.

UTF-8 사용하기

이 섹션은 Alex Cabal이 작성한 PHP Best Practices에 포함되어 있는 내용을 기반으로 하여 작성되었습니다.

한 줄로 끝나는 팁은 없습니다. 주의깊고 일관성있게 작업해야 합니다.

PHP는 현재 유니코드 지원을 언어 내부적으로 해주고 있지 않습니다. UTF-8 문자열을 문제없이 처리할 수 있는 방법은 있지만, HTML 코드, SQL 쿼리, PHP 코드 등 웹 어플리케이션의 모든 레벨을 다뤄야 하는 수준의 이야기입니다. 여기서는 활용할 수 있는 간략한 설명을 제공하는데 초점을 두겠습니다.

PHP 코드 수준에서의 UTF-8

문자열을 변수에 할당하거나 두 문자열을 이어붙이는 등의 기본적인 문자열 연산은 UTF-8 문자열이라고 해도 특별할 것이 없습니다. 하지만 strpos()strlen()과 같은 대부분의 문자열 함수들은 신경을 써야 합니다. 이런 함수들은 mb_strpos()mb_strlen() 같이 원래 이름 앞에 mb_가 붙은 함수들이 별도로 존재합니다. 그런 함수들을 멀티바이트 문자열 함수라고 하고, 멀티바이트 문자열 익스텐션에 의해서 제공됩니다. 멀티바이트 문자열 함수들은 유니코드 문자열을 다루기 위해서 특별히 제공되는 함수들입니다.

유니코드 문자열을 다룰 때에는 항상 mb_로 시작하는 함수들을 사용해야 합니다. 예를 들어 substr() 함수를 UTF-8 문자열에 대해서 사용해보면, 결과물에는 이상하게 깨진 글자가 나온다는 사실을 알게 될 좋은 기회가 될 것입니다. UTF-8 문자열을 다룰 때에는 mb_substr() 함수를 사용해야 합니다.

유니코드 문자열을 다룰 때에는 항상 mb_로 시작하는 함수를 사용해야 한다는 걸 기억하는 게 어려운 일입니다. 한 군데라도 까먹고 일반 문자열 함수를 사용하면 깨진 유니코드 문자열을 보게 될 수가 있습니다.

모든 문자열 함수가 그에 해당되는 멀티바이트 버전의 mb_ 로 시작되는 함수를 가지고 있는 것은 아닙니다. 여러분이 사용하려고 했던 문자열 함수가 그런 경우라면, 운이 없었다고 할 수 밖에요.

모든 PHP 스크립트 파일(또는 모든 PHP 스크립트에 include 되는 공용 스크립트)의 가장 윗부분에 mb_internal_encoding() 함수를 사용해서 문자열 인코딩을 지정하고, 그 바로 다음에 mb_http_output() 함수로 브라우저에 출력될 문자열 인코딩도 지정해야 합니다. 이런 식으로 모든 스크립트에 문자열의 인코딩을 명시적으로 지정하는 것이, 나중에 고생할 일을 아주 많이 줄여줍니다.

추가로, 많은 문자열 함수들이 함수 파라미터의 문자열 인코딩을 지정할 수 있는 옵셔널 파라미터를 제공한다는 사실을 이야기해야겠습니다. 그런 경우마다 항상 UTF-8 문자열 인코딩을 명시적으로 지정해야 합니다. 예를 들어, htmlentities() 함수의 경우가 그렇습니다. UTF-8 문자열을 전달하는 경우 항상 UTF-8로 지정하십시오. 또한 PHP 5.4.0 부터는 htmlentities() 함수와 htmlspecialchars() 함수의 경우에 UTF-8이 기본 인코딩으로 변경되었다는 것을 알아두는게 좋겠습니다.

마지막으로, 만약 분산 배포되는 환경에서 동작하는 어플리케이션을 만들어야 하는데, mbstring 익스텐션이 활성화 되어 있는지 확신할 수 없는 상황이라면, patchwork/utf8이라는 Composer 패키지를 사용해보는 것도 좋겠습니다. 그 패키지를 사용하면, mbstring 익스텐션이 활성화되어 있으면 그쪽 함수들을 사용하고 활성화되어 있지 않으면 일반 문자열 함수가 대신 호출되는 식으로 동작하게 만들어줍니다.

데이터베이스 수준에서의 UTF-8

MySQL을 사용하는 PHP 스크립트가 있다면, 앞에서 이야기한 내용을 충실히 따르더라도 데이터베이스에 저장되는 문자열은 UTF-8이 아닌 다른 인코딩으로 저장될 가능성이 있습니다.

PHP에서 MySQL로 전달되는 문자열이 UTF-8로 확실히 전달되게 하려면, 사용하는 데이터베이스와 테이블이 모두 utf8mb4 캐릭터 셋과 콜레이션(collation)을 사용하게 해야합니다. 그리고 PDO 연결문자열에도 utf8mb4 캐릭터 셋을 사용한다고 명시해야 합니다. 다음의 예제를 보세요. 정말 중요합니다.

UTF-8 문자열을 제대로 사용하려면 반드시 utf8mb4 캐릭터 셋을 사용해야 합니다. utf8 캐릭터 셋이 아닙니다! (놀라워라…) 그 이유는 ‘더 읽어보기’를 참고하세요.

브라우저 수준에서의 UTF-8

PHP 스크립트가 UTF-8 문자열을 브라우저에 제대로 전송하게 하려면 mb_http_output() 함수를 사용하세요.

그리고 HTTP 응답이 UTF-8로 되어 있다는 것을 브라우저도 알 수 있게 해줘야 되겠지요. HTML 응답 내용의 <head> 태그에 charset <meta> 태그를 넣는 것이 전통적인 방법입니다. 이렇게 하는 데에 잘못된 점은 하나도 없지만, 성능을 좀 더 올릴 수 있는 방법도 있습니다. HTTP 응답의 Content-Type 헤더에 charset 설정을 하면 훨씬 빠르게 동작 한다고 합니다.

(역자 주: 현재 해당 링크는 charset 관련된 내용을 담고 있지 않네요. 오래된 글이긴 하지만 이 링크를 참고하면 도움이 될 것 같습니다.)

<?php
// 이 스크립트 파일의 끝까지 UTF-8 문자열을 사용할 것임을 PHP에게 알려줍니다.
mb_internal_encoding('UTF-8');

// UTF-8 문자열을 브라우저에 전송하려고 한다고 PHP에게 알려줍니다.
mb_http_output('UTF-8');
 
// UTF-8 테스트용 문자열
$string = 'Êl síla erin lû e-govaned vîn.';
 
// 멀티바이트 문자열 함수를 사용해서 문자열 자르기를 합니다.
$string = mb_substr($string, 0, 15);
 
// 자르기 해서 새로 만들어진 문자열을 데이터베이스에 저장하기 위해서 일단 접속을 합니다.
// 더 많은 정보를 얻으려면 이 문서의 PDO 관련 내용을 참고하세요.
// `charset=utf8mb4` 로 지정하고 있다는 점을 유의하세요!
$link = new PDO(
    'mysql:host=your-hostname;dbname=your-db;charset=utf8mb4',
    'your-username',
    'your-password',
    array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_PERSISTENT => false
    )
);
 
// 데이터베이스에 문자열을 저장합니다.
// DB와 테이블을 utf8mb4 캐릭터 셋과 콜레이션을 사용하도록 만들어 둔 상태인지 다시 한 번 확인하세요.
$handle = $link->prepare('insert into ElvishSentences (Id, Body) values (?, ?)');
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->bindValue(2, $string);
$handle->execute();
 
// 제대로 저장되었는지 검증하기 위해서 방금 저장한 문자열을 다시 읽어옵니다.
$handle = $link->prepare('select * from ElvishSentences where Id = ?');
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->execute();
 
// HTML에 출력하기 위해서 변수에 결과값을 저장해둡니다.
$result = $handle->fetchAll(\PDO::FETCH_OBJ);

header('Content-Type: text/html; charset=UTF-8');
?><!doctype html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>UTF-8 test page</title>
    </head>
    <body>
        <?php
        foreach($result as $row){
            print($row->Body);  // 자르기 한 문자열을 정확하게 표시해주기를 기대해 봅니다.
        }
        ?>
    </body>
</html>

더 읽어보기

Internationalization (i18n) and Localization (l10n)

Disclaimer for newcomers: i18n and l10n are numeronyms, a kind of abbreviation where numbers are used to shorten words - in our case, internationalization becomes i18n and localization, l10n.

First of all, we need to define those two similar concepts and other related things:

Common ways to implement

The easiest way to internationalize PHP software is by using array files and using those strings in templates, such as <h1><?=$TRANS['title_about_page']?></h1>. This is, however, hardly a recommended way for serious projects, as it poses some maintenance issues along the road - some might appear in the very beginning, such as pluralization. So, please, don’t try this if your project will contain more than a couple of pages.

The most classic way and often taken as reference for i18n and l10n is a Unix tool called gettext. It dates back to 1995 and is still a complete implementation for translating software. It is pretty easy to get running, while it still sports powerful supporting tools. It’s about Gettext we will be talking here. Also, to help you not get messy over the command-line, we will be presenting a great GUI application that can be used to easily update your l10n source files.

Other tools

There are common libraries used that support Gettext and other implementations of i18n. Some of them may seem easier to install or sport additional features or i18n file formats. In this document, we focus on the tools provided with the PHP core, but here we list others for completion:

Other frameworks also include i18n modules, but those are not available outside of their codebases:

If you decide to go for one of the libraries that provide no extractors, you may want to use the gettext formats, so you can use the original gettext toolchain (including Poedit) as described in the rest of the chapter.

Gettext

Installation

You might need to install Gettext and the related PHP library by using your package manager, like apt-get or yum. After installed, enable it by adding extension=gettext.so (Linux/Unix) or extension=php_gettext.dll (Windows) to your php.ini.

Here we will also be using Poedit to create translation files. You will probably find it in your system’s package manager; it’s available for Unix, Mac, and Windows, and can be downloaded for free on their website as well.

Structure

Types of files

There are three files you usually deal with while working with gettext. The main ones are PO (Portable Object) and MO (Machine Object) files, the first being a list of readable “translated objects” and the second, the corresponding binary to be interpreted by gettext when doing localization. There’s also a POT (Template) file, that simply contains all existing keys from your source files, and can be used as a guide to generate and update all PO files. Those template files are not mandatory: depending on the tool you’re using to do l10n, you can go just fine with only PO/MO files. You’ll always have one pair of PO/MO files per language and region, but only one POT per domain.

Domains

There are some cases, in big projects, where you might need to separate translations when the same words convey different meaning given a context. In those cases, you split them into different domains. They’re basically named groups of POT/PO/MO files, where the filename is the said translation domain. Small and medium-sized projects usually, for simplicity, use only one domain; its name is arbitrary, but we will be using “main” for our code samples.
In Symfony projects, for example, domains are used to separate the translation for validation messages.

Locale code

A locale is simply a code that identifies one version of a language. It’s defined following the ISO 639-1 and ISO 3166-1 alpha-2 specs: two lower-case letters for the language, optionally followed by an underline and two upper-case letters identifying the country or regional code. For rare languages, three letters are used.

For some speakers, the country part may seem redundant. In fact, some languages have dialects in different countries, such as Austrian German (de_AT) or Brazilian Portuguese (pt_BR). The second part is used to distinguish between those dialects - when it’s not present, it’s taken as a “generic” or “hybrid” version of the language.

Directory structure

To use Gettext, we will need to adhere to a specific structure of folders. First, you’ll need to select an arbitrary root for your l10n files in your source repository. Inside it, you’ll have a folder for each needed locale, and a fixed LC_MESSAGES folder that will contain all your PO/MO pairs. Example:

<project root>
 ├─ src/
 ├─ templates/
 └─ locales/
    ├─ forum.pot
    ├─ site.pot
    ├─ de/
    │  └─ LC_MESSAGES/
    │     ├─ forum.mo
    │     ├─ forum.po
    │     ├─ site.mo
    │     └─ site.po
    ├─ es_ES/
    │  └─ LC_MESSAGES/
    │     └─ ...
    ├─ fr/
    │  └─ ...
    ├─ pt_BR/
    │  └─ ...
    └─ pt_PT/
       └─ ...

Plural forms

As we said in the introduction, different languages might sport different plural rules. However, gettext saves us from this trouble once again. When creating a new .po file, you’ll have to declare the plural rules for that language, and translated pieces that are plural-sensitive will have a different form for each of those rules. When calling Gettext in code, you’ll have to specify the number related to the sentence, and it will work out the correct form to use - even using string substitution if needed.

Plural rules include the number of plurals available and a boolean test with n that would define in which rule the given number falls (starting the count with 0). For example:

Now that you understood the basis of how plural rules works - and if you didn’t, please look at a deeper explanation on the LingoHub tutorial -, you might want to copy the ones you need from a list instead of writing them by hand.

When calling out Gettext to do localization on sentences with counters, you’ll have to give him the related number as well. Gettext will work out what rule should be in effect and use the correct localized version. You will need to include in the .po file a different sentence for each plural rule defined.

Sample implementation

After all that theory, let’s get a little practical. Here’s an excerpt of a .po file - don’t mind with its format, but instead the overall content, you’ll learn how to edit it easily later:

msgid ""
msgstr ""
"Language: pt_BR\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

msgid "We're now translating some strings"
msgstr "Nós estamos traduzindo algumas strings agora"

msgid "Hello %1$s! Your last visit was on %2$s"
msgstr "Olá %1$s! Sua última visita foi em %2$s"

msgid "Only one unread message"
msgid_plural "%d unread messages"
msgstr[0] "Só uma mensagem não lida"
msgstr[1] "%d mensagens não lidas"

The first section works like a header, having the msgid and msgstr especially empty. It describes the file encoding, plural forms and other things that are less relevant. The second section translates a simple string from English to Brazilian Portuguese, and the third does the same, but leveraging string replacement from sprintf so the translation may contain the user name and visit date.
The last section is a sample of pluralization forms, displaying the singular and plural version as msgid in English and their corresponding translations as msgstr 0 and 1 (following the number given by the plural rule). There, string replacement is used as well so the number can be seen directly in the sentence, by using %d. The plural forms always have two msgid (singular and plural), so it’s advised to not use a complex language as the source of translation.

Discussion on l10n keys

As you might have noticed, we’re using as source ID the actual sentence in English. That msgid is the same used throughout all your .po files, meaning other languages will have the same format and the same msgid fields but translated msgstr lines.

Talking about translation keys, there are two main “schools” here:

  1. msgid as a real sentence.
    The main advantages are:
    • if there are pieces of the software untranslated in any given language, the key displayed will still maintain some meaning. Example: if you happen to translate by heart from English to Spanish but need help to translate to French, you might publish the new page with missing French sentences, and parts of the website would be displayed in English instead;
    • it’s much easier for the translator to understand what’s going on and make a proper translation based on the msgid;
    • it gives you “free” l10n for one language - the source one;
    • The only disadvantage: if you need to change the actual text, you would need to replace the same msgid across several language files.
  2. msgid as a unique, structured key.
    It would describe the sentence role in the application in a structured way, including the template or part where the string is located instead of its content.
    • it’s a great way to have the code organized, separating the text content from the template logic.
    • however, that could bring problems to the translator that would miss the context. A source language file would be needed as a basis for other translations. Example: the developer would ideally have an en.po file, that translators would read to understand what to write in fr.po for instance.
    • missing translations would display meaningless keys on screen (top_menu.welcome instead of Hello there, User! on the said untranslated French page). That’s good it as would force translation to be complete before publishing - but bad as translation issues would be really awful in the interface. Some libraries, though, include an option to specify a given language as “fallback”, having a similar behavior as the other approach.

The Gettext manual favors the first approach as, in general, it’s easier for translators and users in case of trouble. That’s how we will be working here as well. However, the Symfony documentation favors keyword-based translation, to allow for independent changes of all translations without affecting templates as well.

Everyday usage

In a common application, you would use some Gettext functions while writing static text in your pages. Those sentences would then appear in .po files, get translated, compiled into .mo files and then, used by Gettext when rendering the actual interface. Given that, let’s tie together what we have discussed so far in a step-by-step example:

1. A sample template file, including some different gettext calls

<?php include 'i18n_setup.php' ?>
<div id="header">
    <h1><?=sprintf(gettext('Welcome, %s!'), $name)?></h1>
    <!-- code indented this way only for legibility -->
    <?php if ($unread): ?>
        <h2><?=sprintf(
            ngettext('Only one unread message',
                     '%d unread messages',
                     $unread),
            $unread)?>
        </h2>
    <?php endif ?>
</div>

<h1><?=gettext('Introduction')?></h1>
<p><?=gettext('We\'re now translating some strings')?></p>

2. A sample setup file (i18n_setup.php as used above), selecting the correct locale and configuring Gettext

<?php
/**
 * Verifies if the given $locale is supported in the project
 * @param string $locale
 * @return bool
 */
function valid($locale) {
   return in_array($locale, ['en_US', 'en', 'pt_BR', 'pt', 'es_ES', 'es');
}

//setting the source/default locale, for informational purposes
$lang = 'en_US';

if (isset($_GET['lang']) && valid($_GET['lang'])) {
    // the locale can be changed through the query-string
    $lang = $_GET['lang'];    //you should sanitize this!
    setcookie('lang', $lang); //it's stored in a cookie so it can be reused
} elseif (isset($_COOKIE['lang']) && valid($_COOKIE['lang'])) {
    // if the cookie is present instead, let's just keep it
    $lang = $_COOKIE['lang']; //you should sanitize this!
} elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
    // default: look for the languages the browser says the user accepts
    $langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
    array_walk($langs, function (&$lang) { $lang = strtr(strtok($lang, ';'), ['-' => '_']); });
    foreach ($langs as $browser_lang) {
        if (valid($browser_lang)) {
            $lang = $browser_lang;
            break;
        }
    }
}

// here we define the global system locale given the found language
putenv("LANG=$lang");

// this might be useful for date functions (LC_TIME) or money formatting (LC_MONETARY), for instance
setlocale(LC_ALL, $lang);

// this will make Gettext look for ../locales/<lang>/LC_MESSAGES/main.mo
bindtextdomain('main', '../locales');

// indicates in what encoding the file should be read
bind_textdomain_codeset('main', 'UTF-8');

// if your application has additional domains, as cited before, you should bind them here as well
bindtextdomain('forum', '../locales');
bind_textdomain_codeset('forum', 'UTF-8');

// here we indicate the default domain the gettext() calls will respond to
textdomain('main');

// this would look for the string in forum.mo instead of main.mo
// echo dgettext('forum', 'Welcome back!');
?>

3. Preparing translation for the first run

To make matters easier - and one of the powerful advantages Gettext has over custom framework i18n packages - is its custom file type. “Oh man, that’s quite hard to understand and edit by hand, a simple array would be easier!” Make no mistake, applications like Poedit are here to help - a lot. You can get the program from their website, it’s free and available for all platforms. It’s a pretty easy tool to get used to, and a very powerful one at the same time - using all powerful features Gettext has available.

In the first run, you should select “File > New Catalog” from the menu. There you’ll have a small screen where we will set the terrain so everything else runs smoothly. You’ll be able to find those settings later through “Catalog > Properties”:

After setting those points you’ll be prompted to save the file - using that directory structure we mentioned as well, and then it will run a scan through your source files to find the localization calls. They’ll be fed empty into the translation table, and you’ll start typing in the localized versions of those strings. Save it and a .mo file will be (re)compiled into the same folder and ta-dah: your project is internationalized.

4. Translating strings

As you may have noticed before, there are two main types of localized strings: simple ones and the ones with plural forms. The first ones have simply two boxes: source and localized string. The source string can’t be modified as Gettext/Poedit do not include the powers to alter your source files - you should change the source itself and rescan the files. Tip: you may right-click a translation line and it will hint you with the source files and lines where that string is being used.
On the other hand, plural form strings include two boxes to show the two source strings, and tabs so you can configure the different final forms.

Whenever you change your sources and need to update the translations, just hit Refresh and Poedit will rescan the code, removing non-existent entries, merging the ones that changed and adding new ones. It may also try to guess some translations, based on other ones you did. Those guesses and the changed entries will receive a “Fuzzy” marker, indicating it needs review, being highlighted in the list. It’s also useful if you have a translation team and someone tries to write something they’re not sure about: just mark Fuzzy and someone else will review later.

Finally, it’s advised to leave “View > Untranslated entries first” marked, as it will help you a lot to not forget any entry. From that menu, you can also open parts of the UI that allow you to leave contextual information for translators if needed.

Tips & Tricks

Possible caching issues

If you’re running PHP as a module on Apache (mod_php), you might face issues with the .mo file being cached. It happens the first time it’s read, and then, to update it, you might need to restart the server. On Nginx and PHP5 it usually takes only a couple of page refreshes to refresh the translation cache, and on PHP7 it is rarely needed.

Additional helper functions

As preferred by many people, it’s easier to use _() instead of gettext(). Many custom i18n libraries from frameworks use something similar to t() as well, to make translated code shorter. However, that’s the only function that sports a shortcut. You might want to add in your project some others, such as __() or _n() for ngettext(), or maybe a fancy _r() that would join gettext() and sprintf() calls. Other libraries, such as oscarotero’s Gettext also provide helper functions like these.

In those cases, you’ll need to instruct the Gettext utility on how to extract the strings from those new functions. Don’t be afraid, it’s very easy. It’s just a field in the .po file, or a Settings screen on Poedit. In the editor, that option is inside “Catalog > Properties > Source keywords”. You need to include there the specifications of those new functions, following a specific format:

After including those new rules in the .po file, a new scan will bring in your new strings just as easy as before.

References

Back to Top

의존성 주입

위키백과에서 인용:

의존성 주입(Dependency Injection, DI)은 프로그래밍에서 구성요소간의 종속성을 소스코드에서 설정하지 않고 외부의 설정파일 등을 통해 컴파일 시점이나 실행 시점에 주입하도록 하는 디자인 패턴 중의 하나이다.

위와 같은 설명은 실제보다 훨씬 어렵게 느껴지게 만드는 점이 있습니다. 의존성 주입이라는 것은 특정 컴포넌트의 의존 관계를, 생성자에서 주입하거나 메소드 호출 혹은 프로퍼티 설정을 하는 방식으로 지정할 수 있게 하는 것입니다. 간단한 얘기입니다.

기본 개념

간단한 예제를 통해서 기본적인 개념을 보여드리겠습니다.

아래 코드를 보면 데이터베이스와 통신하기 위한 어댑터를 필요로 하는 Database라는 클래스가 있습니다. 생성자에서 어댑터 인스턴스를 생성하는 방식으로 되어 있어서 두 클래스는 서로 강한 의존 관계를 가지고 있습니다. 그래서 Database 클래스를 테스트하기도 어렵습니다.

<?php
namespace Database;

class Database
{
    protected $adapter;

    public function __construct()
    {
        $this->adapter = new MySqlAdapter;
    }
}

class MysqlAdapter {}

아래와 같이 리팩터링하여 의존성 주입을 사용하도록 하면 의존 관계를 약화시킬 수 있습니다.

<?php
namespace Database;

class Database
{
    protected $adapter;

    public function __construct(MySqlAdapter $adapter)
    {
        $this->adapter = $adapter;
    }
}

class MysqlAdapter {}

이제 Database는 내부에서 직접 의존 관계에 있는 클래스 인스턴스를 생성하지 않고, 외부에서 전달받게 되었습니다. 어댑터 인스턴스를 인자로 전달받는 메소드를 만들어서 해당 어댑터를 사용하도록 설정하는 방식을 적용하거나, $adapter 프로퍼티를 public 으로 만들어서 프로퍼티를 직접 설정하게 할 수도 있을 것입니다.

복잡한 문제

의존성 주입에 대한 글을 읽어본 적이 있다면 아마 “제어의 역전” 이나 “의존 관계 역전의 원칙” 이라는 말을 본 적이 있을 겁니다. 의존성 주입으로 풀 수 있는 복잡한 문제들이죠.

제어의 역전

제어의 역전(Inversion of Control)은 말 그대로, 시스템의 “제어권을 뒤집는” 것입니다. 우리가 다루는 개체들의 구성에 대한 제어를 해당 개체들과 완전히 분리시킴으로써 말이죠. 의존성 주입이라는 관점에서 보면, 우리가 다루는 개체들의 생성과 의존 관계의 설정을 시스템의 다른 곳에서 수행함으로써 개체들간의 의존성을 느슨하게 한다는 말이 됩니다.

지난 몇 년간, PHP 프레임워크들은 제어의 역전을 이루어 왔습니다. 하지만 이러한 의문이 생겨났죠. “어떤 부분의 제어권을 역전시켰고 그 제어권은 어디로 갔는가?” 예를 들어 MVC 프레임워크는 보통 수퍼 개체나 베이스 컨트롤러를 제공하는데, 우리가 구현하는 컨트롤러는 반드시 베이스 컨트롤러를 상속받아야 하고 그래야만 의존 관계에 있는 모델이나 뷰에 접근할 수 있게 됩니다. 이것은 제어의 역전이 맞기는 한데 의존 관계를 느슨하게 만들었다기 보다는 단순히 위치를 이동시킨 것 뿐입니다.

의존성 주입을 사용하게 되면 이런 문제를 유연하게 풀 수 있게 됩니다. 우리가 원하는 의존 관계를 우리가 필요할 때에만, 의존 관계를 전혀 하드코딩하지 않고서도, 의존성을 주입시킴으로써 말이죠.

의존 관계 역전의 원칙

의존 관계 역전의 원칙(Dependency Inversion Principle)은 흔히 S.O.L.I.D 라고 부르는 개체지향 설계 원칙 중 D에 해당합니다. “추상화된 것에 의존하고, 구체화된 것에 의존하지 마라” 라는 원칙입니다. 좀더 풀어서 설명하면 우리가 구현하는 클래스는 다른 구체화된 클래스 구현에 의존하지 말고 인터페이스나 추상 클래스에 의존하도록 만들어야 한다는 이야기입니다. 앞에서 본 예제 코드가 이런 원칙을 따르도록 리팩토링하는 것은 어렵지 않습니다.

<?php
namespace Database;

class Database
{
    protected $adapter;

    public function __construct(AdapterInterface $adapter)
    {
        $this->adapter = $adapter;
    }
}

interface AdapterInterface {}

class MysqlAdapter implements AdapterInterface {}

Database 클래스는 이제 구체 클래스가 아닌 인터페이스에 의존하게 되었습니다. 이렇게 하면 몇 가지 장점이 있습니다.

여러분이 팀 프로젝트를 하는 중이고, 어댑터는 팀의 동료가 만드는 중이라고 생각해봅시다. 이전의 예제 코드와 같은 상태라면 유닛 테스트를 작성하기 위해서 어댑터의 mock 개체를 만들려면, 여러분의 동료가 어댑터를 완성해주기를 기다려야 할 것입니다. 지금의 코드는 인터페이스에 의존하고 있으므로, 여러분의 동료가 해당 인터페이스의 규약을 잘 따를 것이라는 것을 알 수 있고, 우리는 해당 인터페이스를 사용해서 행복하게 mock 개체를 만들면 됩니다.

또한 인터페이스에 의존하는 코드의 큰 장점은 확장성입니다. 시간이 흘러 처음 사용하던 것과는 다른 DBMS로 마이그레이션 하려고 할 때, 해당 인터페이스를 구현하는 새로운 어댑터를 구현하여 원래의 어댑터를 사용하던 곳에 대신 넣기만 하면 됩니다. 어댑터들은 동일한 인터페이스의 규약을 따르고 있다는 것을 우리가 알고 있기 때문에 추가적인 리팩토링을 해야할 필요가 없습니다.

컨테이너

의존성 주입 컨테이너(Dependency Injection Containers)를 다룰 때, 의존성 주입 컨테이너는 의존성 주입과 같지 않다는 사실을 가장 처음으로 이해해야만 합니다. 컨테이너는 의존성 주입 패턴의 구현을 도와주는 편리한 유틸리티인 것은 맞지만, 의존성 주입 패턴의 안티패턴인 서비스 로케이터(Service Locator) 패턴을 구현하는데 잘못 사용되는 일도 잦습니다. 의존성 주입 컨테이너를 서비스 로케이터로서 클래스에 주입하게 되면, 해당 클래스는 서비스 로케이터에 강하게 의존하게 되기 때문에 줄어드는 의존성에 비해서 새롭게 생긴 의존성이 더 크다고 할 수 있습니다. 또한 코드를 이해하기 더 어려워지고 테스트하기에도 아주 어려운 코드가 되어 버립니다.

대부분의 현대적인 프레임워크들은 자체적인 의존성 주입 컨테이너를 가지고 있어서, 코드가 아닌 설정을 통해 의존 관계를 설정할 수 있게 되어 있습니다. 이러한 프레임워크를 잘 사용하면 깔끔하고 디커플링이 잘 된 어플리케이션 코드를 작성할 수 있습니다.

더 읽을거리

Back to Top

데이터베이스

대부분의 PHP 어플리케이션은 정보를 보존하기 위해서 데이터베이스를 사용합니다. 데이터베이스를 사용하기위한 선택지가 여러가지 있습니다. PHP 5.1.0 시절까지는 mysqli, pgsql, mssql 등의 네이티브 드라이버를 사용하는 것을 권장했습니다.

PHP 어플리케이션에서 단 하나의 데이터베이스만을 사용하는 경우라면 네이티브 드라이버가 훌륭하긴 합니다. 하지만 예를들어 MySQL을 주로 사용하지만 MSSQL을 사용할 일도 있다든지, Oracle 데이터베이스를 사용할 필요가 있는 경우라면 동일한 드라이버를 사용할 수가 없습니다. 각각의 데이터베이스에 대해서 전용 API를 익혀야만 하는데, 그건 좀 번거롭기 마련이죠.

MySQL 익스텐션

PHP의 MySQL익스텐션은 아주 오래되었고, 다음 두 익스텐션으로 대체되었습니다.

mysql 익스텐션 개발은 오래전에 개발 중단되었을 뿐만 아니라, PHP 5.5.0 부터는 공식적으로 사용을 권장하지 않는 상태(deprecated)였고, PHP 7.0에서는 공식적으로 제거 되었습니다.

어떤 모듈을 사용하고 있는지 php.ini을 뒤져보는 수고를 하지 않는 한가지 방법은, mysql_*을 여러분이 사용하는 에디터에서 검색해보는 것입니다. 만약 mysql_connect()mysql_query()와 같은 함수가 나온다면, mysql을 사용하고 있는 것입니다.

PHP 7.x을 아직 사용하지 않더라도 가능한 빨리 익스텐션 업그레이드를 고려하지 않으면, PHP 7.x 업그레이드를 해야할 때에 굉장히 고생하게 됩니다. 가장 좋은 방법은, 나중에 서두르게 되지 않도록 여러분의 개발 일정에 맞춰서 mysql을 사용하던 것을 mysqliPDO로 변경하는 것입니다.

mysql에서 mysqli로 업그레이드할때에는 단순히 mysql_*을 찾아서 mysqli_*로 치환하는 게으른 업그레이드 가이드를 조심하세요. 이것은 지나치게 단순화한 조잡한 방법일 뿐만 아니라, 파라미터 바인딩같은 mysqli가 제공하는(PDO도 제공하는) 혜택을 놓치게 됩니다.

PDO 익스텐션

PDO는 PHP 5.1.0부터 제공되는 데이터베이스 연결 추상화 라이브러리입니다. 많은 종류의 데이터베이스 연결에 대해서 공통적인 인터페이스를 제공합니다. 예를들어, 기본적으로는 MySQL을 사용하거나 SQLite를 사용할 때 동일한 코드를 사용할 수 있다는 의미입니다.

<?php
// PDO + MySQL
$pdo = new PDO('mysql:host=example.com;dbname=database', 'user', 'password');
$statement = $pdo->query("SELECT some_field FROM some_table");
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo htmlentities($row['some_field']);

// PDO + SQLite
$pdo = new PDO('sqlite:/path/db/foo.sqlite');
$statement = $pdo->query("SELECT some_field FROM some_table");
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo htmlentities($row['some_field']);

PDO가 SQL 쿼리를 데이터베이스에 맞게 번역해주거나, 특정 데이터베이스에는 존재하지 않는 기능을 에뮬레이션해 주는 것은 아닙니다. 여러 종류의 데이터베이스 연결을 동일한 API로 할 수 있게 해준다는 의미입니다.

PDO가 해주는 더 중요한 역할은 SQL 인젝션 공격을 걱정하지 않고도 외부 입력(사용자 ID 등) 값을 SQL 쿼리에 넣을 수 있게 해주는 것입니다. 이는 PDO statements와 바운드 매개변수를 통해 가능합니다.

숫자 형식의 ID를 쿼리 인자로 받는 PHP 스크립트가 있다고 해봅시다. 사용자 정보를 데이터베이스에서 가져오려면 그 ID가 필요한 상황이라고 하죠. 이렇게 하는 건 잘못된 방법입니다.

<?php
$pdo = new PDO('sqlite:/path/db/users.db');
$pdo->query("SELECT name FROM users WHERE id = " . $_GET['id']); // <-- NO!

이건 끔직한 코드라고 할 수 있겠습니다. SQL 쿼리에 외부 입력을 그대로 넣고 있습니다. 이는 당장이라도 SQL 인젝션 공격에 당할 것 같습니다. 해커가 http://domain.com/?id=1%3BDELETE+FROM+users 같이 창의적인 id 인자를 넣어서 이 코드를 호출했다고 생각해봅시다. $_GET['id']1;DELETE FROM users 가 되어서 모든 users 데이터가 삭제되어 버립니다! 이렇게 하지 말고 다음과 같이 PDO의 인자 바인딩을 사용해서 ID 입력값을 안전하게 전달해야 합니다.

<?php
$pdo = new PDO('sqlite:/path/db/users.db');
$stmt = $pdo->prepare('SELECT name FROM users WHERE id = :id');
$id = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT); // <-- 먼저 데이터를 필터링 ([데이터 필터링](#data_filtering) 참고), INSERT, UPDATE 등에 특히 중요.
$stmt->bindParam(':id', $id, PDO::PARAM_INT); // <-- PDO가 자동으로 SQL에서 위험한 요소 제거
$stmt->execute();

이렇게 하는게 올바른 코드입니다. PDO 구문과 거기에 바인딩한 인자를 사용하고 있죠. 이렇게 하면 데이터베이스에 외부 입력값을 전달하기 전에 잠재적인 SQL 인젝션 공격을 방지할 수 있게 됩니다.

먼저 데이터를 필터링하고, 다른 위험한 요소(HTML 태그, 자바스크립트 등)를 제거하는 것은 INSERT나 UPDATE와 같은 쓰기 작업에 특히 중요합니다. PDO는 여러분이 작성하는 어플리케이션이 아니라, SQL에서만 위험한 요소를 제거합니다.

데이터베이스 연결을 사용할 때에는 데이터베이스 연결을 잘 닫는데 신경쓰지 않았을 경우에 한 번 열어 둔 데이터베이스 연결이 닫히지 않아서 더이상 새로운 연결을 할 수 없는 경우가 발생하기도 합니다. PDO를 사용하면 PDO 개체가 파괴될 때 암시적으로 데이터베이스 연결을 닫아 줍니다. 여러분이 영구적인 데이터베이스 연결을 사용하도록 설정해서 어플리케이션을 돌리고 있는게 아니라면 스크립트의 실행히 종료될 때 PHP가 자동으로 데이터베이스 연결을 닫아줄 것입니다.

데이터베이스와 상호작용 하기

개발자들이 PHP를 배우기 시작할 때, 아마 대부분 데이터베이스에 접근하는 코드와 프리젠테이션 로직을 아래에 보이는 코드처럼 뒤섞어 놓는 경우가 많을 겁니다.

<ul>
<?php
foreach ($db->query('SELECT * FROM table') as $row) {
    echo "<li>".$row['field1']." - ".$row['field1']."</li>";
}
?>
</ul>

이것은 어떤 측면으로 보아도 나쁜 코딩 습관입니다. 디버깅하기도 어렵고 테스트하기도 어렵고 읽기도 어렵습니다. 그리고, limit을 추가하지 않으면 많은 양의 항목을 출력하게 됩니다.

여러분이 개체지향 프로그래밍을 선호하는지, 함수형 프로그래밍 을 선호하는지에 따라 여러가지 방법이 있을 수 있겠지만, 코드를 서로 분리하는 데에는 어떤 기본 요인이 있을 겁니다.

가장 기본적인 단계를 살펴봅시다.

<?php
function getAllFoos($db) {
    return $db->query('SELECT * FROM table');
}

foreach (getAllFoos($db) as $row) {
    echo "<li>".$row['field1']." - ".$row['field1']."</li>"; // BAD!!
}

이 코드는 훌륭한 출발점이라고 할 수 있겠습니다. 두 개의 함수를 서로 다른 파일로 분리시킨다면 깔끔하게 분리된 코드를 얻을 수 있겠죠.

데이터베이스에 접근하는 함수를 넣을 클래스를 하나 만들어서 함수를 메소드로 바꿔주면 이제 우리는 “모델(Model)”을 갖게 된 겁니다. 단순히 .php 파일 하나를 만들어서 프리젠테이션 로직을 넣으면 우리는 이제 “뷰(View)”를 갖게 됩니다. 거의 MVC 패턴 비슷하게 된거죠. MVC는 대부분의 프레임워크들에서 사용되고 있는 개체지향적 아키텍처 패턴입니다.

foo.php

<?php
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'username', 'password');

// 모델 클래스를 사용할 수 있게 포함시킨다.
include 'models/FooModel.php';

// 인스턴스를 만듭니다.
$fooModel = new FooModel($db);
// Foo의 리스트를 가져옵니다.
$fooList = $fooModel->getAllFoos();

// 뷰를 보여준다.
include 'views/foo-list.php';

models/FooModel.php

<?php
class FooModel
{
    protected $db;

    public function __construct(PDO $db)
    {
        $this->db = $db;
    }

    public function getAllFoos() {
        return $this->db->query('SELECT * FROM table');
    }
}

views/foo-list.php

<?php foreach ($fooList as $row): ?>
    <?= $row['field1'] ?> - <?= $row['field1'] ?>
<?php endforeach ?>

이런 방식은 조금 더 수동적이긴 하지만, 대부분의 현대적인 프레임워크들이 취하고 있는 접근법과 근본적으로 동일합니다. 데이터베이스 접근과 프리젠테이션을 모든 경우에 반드시 분리해야만 하는 것은 아니지만, 그 둘을 서로 많이 섞을수록 점점 문제가 발생하게 될 것입니다. 특히 유닛 테스트를 하려는 경우에는 더 심각할 것입니다.

PHPBridge에는 비슷한 주제를 다룬 Creating a Data Class라는 훌륭한 자료가 있습니다. 이제 막 데이터베이스를 사용하는 개발을 하기 시작한 개발자들에게는 반드시 도움이 될 좋은 자료 입니다.

추상화 레이어

많은 프레임워크들은 각자 자신만의 추상화 레이어를 가지고 있는데, PDO를 기반으로 사용하고 있는 것들도 있고, 그렇지 않은 것들도 있습니다. 어떤 프레임워크들은 PHP 메소드를 이용해서 쿼리를 하도록 하여, 특정 데이터베이스에는 존재하지 않는 기능을 에뮬레이션 해주는 등의 역할까지 하는 등 완전한 데이터베이스 추상화를 제공합니다. 물론 추상화라는 게 어느 정도의 오버헤드가 있기는 하지만, MySQL, PostgreSQL, SQLite 등의 여러 데이터베이스를 선택적으로 사용할 수 있는 유연한 어플리케이션을 만들기로 했다면 그 정도의 오버헤드는 코드의 간결함과 유지보수성을 위해서라면 문제가 되지 않는 수준일 것입니다.

몇몇 추상화 레이어는 PSR-0PSR-4 네임스페이스 표준에 따르고 있기 때문에 어느 어플리케이션에도 자유롭게 설치하여 사용할 수 있습니다.

Back to Top

템플릿

템플릿은 프레젠테이션 로직(Presentation Logic)에서 컨트롤러(Controller)와 도메인 로직(Domain Logic)으로 분리하는 편리한 방법을 제공합니다. 템플릿은 일반적으로 어플리케이션의 HTML을 포함합니다. 그러나 XML과 같은 또다른 포맷을 사용할 수 있습니다. 템플릿은 때때로 “뷰(view)”라고 언급되는데, 소프트웨어 설계 패턴인 MVC(모델-뷰-컨트롤러, Model-View-Controller)패턴의 두 번째 구성 요소 입니다.

장점

템플릿 사용의 주요 장점은 어플리케이션에서 프레젠테이션 로직이 나머지 부분들과 완전히 분리되는 것 입니다. 템플릿은 컨텐츠를 표시하는 책임만을 가집니다. 템플릿은 데이터 조회, 지속성 또는 더 복잡한 작업에 대한 책임을 지지 않습니다. 이는 개발자가 서버측 코드(컨트롤러, 모델)를 만들고, 디자이너가 클라이언트측 코드를 만드는(markup) 팀 환경에서 특히 유용하며 더 깔끔하고 더욱 읽기 쉬운 코드를 이끌어 냅니다.

템플릿은 프레젠테이션 코드의 구조를 향상시킵니다. 템플릿은 일반적으로 “views” 폴더에 위치하며, 각각 별도의 파일로 되어 있습니다. 이러한 접근 방법은 대규모의 코드를 더 작고, 재사용 가능한 (partials 라고 불리우는) 조각으로 나누어 재사용하는 것을 권장합니다. 예를 들어, 당신의 사이트의 헤더(header), 푸터(footer)를 각각 템플릿으로 정의하고, 이를 각 페이지 템플릿의 앞 뒤에 포함 시킬 수 있습니다.

또한, 사용하는 라이브러리에 따라 템플릿은 사용자가 만든 컨텐트를 자동으로 이스케이핑 함으로써 더 높은 보안성을 제공합니다. 어떤 라이브러리들은 샌드 박스 기능도 제공해서, 템플릿 디자이너로 하여금 특정 변수와 함수에만 접근할 수 있도록 하기도 합니다.

단순 PHP 템플릿

단순 PHP 템플릿은 순수한 PHP 코드를 사용한 간단한 템플릿입니다. 이는 PHP가 템플릿 언어 그 자체로 사용되기 시작한 이래로 자연스럽게 선택된 방법입니다. 간단히 말해 PHP 코드를 HTML과 같은 다른언어와 결합해 사용할 수 있다는 것 입니다. PHP개발자가 다른 새로운 문법을 배울 필요가 없기 때문에 도움이 됩니다. 그들은 자신이 쓸 수 있는 기능에 대해 알고있으며, 개발 툴에는 이미 PHP 구문 강조와 자동완성이 내장되어 있습니다. 더구나, 단순 PHP 템플릿은 컴파일 단계를 필요로 하지 않아서 매우 빠른 경향이 있습니다.

모든 현대의 PHP 프레임워크는 몇종류의 템플릿 시스템을 채택하고 있으며, 대부분 기본적으로 단순 PHP를 사용합니다. 프레임워크 외에, PlatesAura.View와 같은 라이브러리들은 상속, 레이아웃, 확장과 같이 현대적인 템플릿의 기능을 제공하여 단순 PHP 템플릿과 함께 작동되게 합니다.

단순 PHP 템플릿의 간단한 예제

Plates 라이브러리를 이용했습니다.

<?php // user_profile.php ?>

<?php $this->insert('header', ['title' => 'User Profile']) ?>

<h1>User Profile</h1>
<p>Hello, <?=$this->escape($name)?></p>

<?php $this->insert('footer') ?>

상속을 이용한 단순 PHP 템플릿 예제

Plates 라이브러리를 이용했습니다.

<?php // template.php ?>

<html>
<head>
    <title><?=$title?></title>
</head>
<body>

<main>
    <?=$this->section('content')?>
</main>

</body>
</html>
<?php // user_profile.php ?>

<?php $this->layout('template', ['title' => 'User Profile']) ?>

<h1>User Profile</h1>
<p>Hello, <?=$this->escape($name)?></p>

컴파일되는 템플릿

PHP는 성숙한, 객체 지향적 언어로 성장하고 있지만, 템플릿 언어로는 성장하지 않았습니다. TwigSmarty*와 같이 컴파일된 템플릿은 특히, 템플릿을 대상으로 제공되는 새로운 구문으로 기존의 부족했던 부분을 채워줬습니다. 자동 이스케이핑, 상속, 그리고 단순화 된 제어구조는 컴파일된 템플릿을 쉽게 쓸 수 있고, 읽기 쉽고, 사용하기 안전하게 설계되었습니다. 심지어 컴파일된 템플릿은 다른언어와 교환을 할 수 있습니다. Mustache가 좋은 예 입니다. 이러한 템플릿은 컴파일을 해야하기 때문에 약간의 성능저하가 있을 수 있지만, 적절한 캐싱을 사용하면 아주 미미한 차이 일 것입니다.

*Smarty는 자동 이스케이핑을 지원하지만, 이 기능은 기본적으로 비활성화 되어있습니다.

컴파일 된 템플릿에 대한 간단한 예제

Twig 라이브러리를 사용하였습니다.

{% include 'header.html' with {'title': 'User Profile'} %}

<h1>User Profile</h1>
<p>Hello, {{ name }}</p>

{% include 'footer.html' %}

상속을 이용한 컴파일된 템플릿 예제

Twig 라이브러리를 사용하였습니다.

// template.html

<html>
<head>
    <title>{% block title %}{% endblock %}</title>
</head>
<body>

<main>
    {% block content %}{% endblock %}
</main>

</body>
</html>
// user_profile.html

{% extends "template.html" %}

{% block title %}User Profile{% endblock %}
{% block content %}
    <h1>User Profile</h1>
    <p>Hello, {{ name }}</p>
{% endblock %}

더 읽을거리

읽을거리 & 튜토리얼

Libraries

Back to Top

에러와 예외

에러

“예외를 적극적으로 사용하는(exception-heavy)” 다수의 프로그래밍 언어에서는 뭔가가 잘못되면 항상 예외를 던집니다. 하지만 PHP는 “예외를 양념으로 사용하는(exception-light)” 프로그래밍 언어입니다. 물론 PHP에서도 예외를 사용할 수 있고 PHP 언어의 핵심적인 부분들에서도 함수가 아닌 클래스 인스턴스를 사용할 때에는 점점 예외를 많이 사용하는 분위기로 가고 있지만, 대부분의 경우에는 심각한 에러(fatal error)가 발생한 경우가 아니고서는 무슨 일이 발생하든지 최대한 처리를 계속 진행시키는 것이 PHP의 방식입니다.

예를 들면

$ php -a
php > echo $foo;
Notice: Undefined variable: foo in php shell code on line 1

이런 에러는 그냥 알림(notice)일 뿐, PHP는 행복하게 계속 작업을 수행합니다. “예외를 적극적으로 사용하는” 언어에 익숙한 분들에게는 헷갈리는 동작일수도 있습니다. 예를들어 Python 에서는 존재하지 않는 변수를 참조하는 경우 예외가 던져집니다.

$ python
>>> print foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'foo' is not defined

실제로 차이가 드러나는 부분은 단 하나입니다. Python은 작은 거 하나에도 깜짝 놀라게 만들어서, 개발자는 잠재적인 문제나 경계 조건에 걸린 상황이라는 것을 아주 확실하게 알 수 있는 반면 PHP 는 웬만하면 처리를 계속하다가, 뭔가 엄청난 일이 일어났을 때에 에러를 던지면서 보고한다고 할 수 있습니다.

에러 심각도

PHP에는 여러 레벨의 에러가 있습니다. 가장 일반적인 것으로는 에러(error), 알림(notice), 경고(warning)이 있습니다. 이것들의 심각도(severity)는 각각 E_ERROR, E_NOTICE, E_WARNING 입니다. ‘에러’는 치명적인 런타임 에러인데, PHP 스크립트의 실행을 중지시키는 수준이므로 코드를 수정해야 합니다. ‘알림’은 실행 중에 문제를 일으킬 가능성이 있는 정도의 코드에서 발생하며 역시 스크립트의 실행이 멈추지는 않습니다. ‘경고’는 치명적이지는 않은 에러라서 스크립트의 실행이 멈출 정도는 아닙니다.

컴파일 시에 발생하는 E_STRICT라는 에러는 추후에 나올 PHP 버전과의 호환성을 위해서 수정이 필요한 코드에서 발생합니다.

PHP의 에러 보고 방식을 변경하기

PHP 설정을 변경하거나 함수 호출을 하여 에러 보고 방식을 바꿀 수 있습니다. 내장 PHP 함수인 error_reporting()을 사용하면 해당 스크립트가 실행되는 동안 사용될 에러 레벨을 설정할 수 있습니다. 미리 정의되어 있는 에러 레벨 상수를 사용하는데, 경고와 에러만 나오게 하고 싶으면 아래와 같이 함수를 호출하면 됩니다.

<?php
error_reporting(E_ERROR | E_WARNING);

또한 에러가 웹 화면에 표시되게 할 것인지(개발 할때 좋습니다), 웹 화면에는 표시되지 않고 로그만 남기게 할 것인지(서비스 환경에 좋습니다) 제어할 수도 있습니다. 자세한 내용은 PHP 매뉴얼의 Error Reporting 섹션을 참고하시기 바랍니다.

코드 한 줄 안에서만 에러 보고 끄기

에러 제어 연산자인 @ 를 사용하여 에러를 표시하지 않도록 할 수 있습니다. 표현식의 시작 부분에 이 연산자를 붙이면 해당 표현식에서 발생하는 모든 에러가 표시되지 않습니다.

<?php
echo @$foo['bar'];

위 코드는 $foo['bar'] 가 정상적으로 존재한다면 그 값을 출력합니다. 여기까지는 특별한 차이가 없죠. 그런데 $foo 변수가 없다든지 'bar' 키가 없을 때에도 아무런 에러를 출력하지 않고 null 을 리턴합니다. 에러 제어 연산자가 없었다면, PHP Notice: Undefined variable: foo 라든지 PHP Notice: Undefined index: bar 라는 에러 메시지가 표시되었을 것입니다.

좋은 기능인 것처럼 보이기도 하지만, 별로 바람직하지 않은 트레이드오프가 있습니다. PHP는 @ 연산자가 붙은 표현식을 처리할 때 @ 연산자가 없는 표현식보다 좀 비효율적으로 처리합니다. 성급하게 최적화를 시도하는 것은 프로그래밍에 관한 논란의 중심이긴 하지만, 작성하고자 하는 어플리케이션의 성능에 특히 신경을 써야하는 상황이라면 에러 제어 연산자가 성능에 얼마나 영향을 주는지는 잘 이해하고 있어야 할 것입니다.

한 가지 더 이야기하자면, 에러 제어 연산자는 에러를 완전히 감춰버립니다. 에러 메시지가 표시되지 않고, 에러 로그에도 남지 않습니다. 또한 기본 상태의 PHP 시스템에서는 에러 제어 연산자를 비활성화시킬 수 있는 방법도 없습니다.

에러 제어 연산자를 사용하지 않을 수 있는 방법이 있다면, 가능하면 그렇게 하는 편이 좋습니다. 위에서 예로 보인 코드는 아래와 같이 다시 작성할 수 있을 것입니다.

<?php
echo isset($foo['bar']) ? $foo['bar'] : '';

에러 보고를 끄는 기능이 의미가 있을 수도 있습니다. 그러한 예로는 fopen() 함수가 파일을 읽지 못할 때를 들 수 있습니다. 파일을 로드하기 전에 파일이 있는지 체크할 수는 있지만 체크는 통과한 뒤 fopen()이 실행되기 전에 파일이 지워진다면 (불가능한 이야기 같겠지만 실제로 일어날 수 있는 일입니다) fopen()은 false를 리턴하면서 에러도 보고합니다. 이런 문제는 PHP가 해결해야하는 게 맞는 일인 것 같기는 하지만, @ 연산자를 이용해서 에러 보고를 끄는 것만이 적절한 해결책이라고 할 수 있는 한 가지 경우라고 볼 수 있습니다.

앞서 말한 것처럼 기본 상태의 PHP 에서는 에러 제어 연산자의 동작을 끌 수 있는 방법이 없습니다. 하지만 Xdebug에는 xdebug.scream 이라는 ini 설정이 있어서 에러 제어 연산자가 동작하지 않게 할 수 있습니다. php.ini 파일에 아래와 같이 설정하면 됩니다.

xdebug.scream = On

혹은 런타임에 ini_set 함수를 사용해서 설정할 수도 있습니다.

<?php
ini_set('xdebug.scream', '1')

Scream“이라는 PHP 익스텐션도 Xdebug와 같은 기능을 제공합니다. Scream의 ini 설정 이름은 scream.enabled 입니다.

이런 기능은 뭔가 도움이 될만한 중요한 에러 메시지가 표시되지 않는 것 같다고 의심될 때 아주 요긴합니다. 하지만 디버깅 용으로 잠시 사용하고 나서 다시 원래대로 설정을 돌려 놓아야 합니다. 에러 제어 연산자가 동작하지 않는 상태에서는 제대로 동작하지 않는 라이브러리들이 많이 있기 때문입니다.

ErrorException

PHP도 완벽하게 “예외를 적극적으로 사용하는” 프로그래밍 언어로서 동작할 수 있는 능력이 있습니다. 코드 몇 줄만 있으면 쉽게 전환시킬 수 있죠. 기본적으로는 “에러”를 “예외”로 던지는 식으로 하면 됩니다. Exception 클래스를 상속한 ErrorException 예외를 사용해서 말입니다.

이 방식은 Symfony나 Laravel 과 같은 많은 모던 프레임워크에서 사용하는 공통적인 방식입니다. 디버그 모드 (혹은 개발 모드) 에서 두 프레임워크 모두 멋지고 깔끔하게 stack trace 를 표시합니다.

개발 도중에 에러를 예외로 던지게 해 둠으로써 에러를 좀 더 잘 처리할 수 있는 기회를 얻게 됩니다. 예외를 catch 문으로 잡아서 해당 예외가 발생하는 상황에 대처하는 코드를 작성할 수 있으니까요. 예외가 발생하는 즉시 대응하는 식으로 프로그래밍 한다면 어플리케이션이 좀 더 안정적으로 동작하게 될 것입니다.

ErrorException Class에서 에러 처리와 관련된 ErrorException 사용법과 다른 정보들을 더 얻을 수 있습니다.

예외

예외(Exceptions) 개념은 대부분의 인기있는 프로그래밍 언어에 표준적으로 포함되어 있는 개념인데, PHP 프로그래머들에게는 과소평가되는 경우가 많은 것 같습니다. Ruby 같은 언어는 극단적으로 예외를 많이 사용하는 편이라서 HTTP 요청이 실패하거나 DB 쿼리 실행에 문제가 생겼을 때, 하다못해 이미지 리소스를 찾지 못했을 때에도 Ruby 프로그램(이나 그 프로그램에 사용한 gem들)은 예외를 던져서 뭔가 실수가 있었을 거라는 정보를 화면에 출력합니다.

PHP는 그러한 부분에 있어서는 상당히 느슨한 편이라서 file_get_contents() 함수 호출이 실패하는 경우라면 여러분은 FALSE 리턴값과 경고 메시지를 받게 됩니다. CodeIgniter 등 많은 수의 오래된 PHP 프레임워크들은 실패 시에 단순히 false를 리턴하고 프레임워크가 제공하는 로그 기록 시스템에 로그 메시지를 기록하며, $this->upload->get_error() 를 호출하면 무슨 이유로 실패했는지 알 수 있게 해주는 식으로 동작합니다. 무엇이 잘못되었는지 개발자가 알기 위해서는 그 클래스의 에러 정보를 얻을 수 있는 방법이 무엇인지 문서를 찾아보아야만 알 수 있다는 점이, 이러한 방식의 문제라고 할 수 있습니다.

또다른 문제는 실패 시에 화면에 에러를 출력하고 처리를 종료해버리는 것입니다. 여러분이 이런 식으로 코드를 작성했다면, 그 코드를 사용하는 다른 개발자가 에러 처리를 유연하게 할 수 없게 됩니다. 에러가 발생했을 때에는 예외(Exception)을 던지도록 작성해야 합니다. 그래야 어떤 에러가 발생했는지 개발자가 명확하게 알 수 있고, 어떤 방식으로 그 에러를 처리할 것인지 개발자 직접 결정할 수 있게 됩니다. 아래 코드를 보시죠.

<?php
$email = new Fuel\Email;
$email->subject('My Subject');
$email->body('How the heck are you?');
$email->to('guy@example.com', 'Some Guy');

try
{
    $email->send();
}
catch(Fuel\Email\ValidationFailedException $e)
{
    // 데이터의 유효성 체크에 실패한 경우
}
catch(Fuel\Email\SendingFailedException $e)
{
    // 메일 전송을 시도했는데 성공하지 못한 경우
}
finally
{
    // 이 부분은 예외가 발생했는지 아닌지와 상관없이 항상 실행됨
}

SPL 예외

Exception 클래스는 개발자가 디버깅에 사용할 수 있는 정보를 그리 많이 제공하지는 않습니다. 하지만 이런 점을 보완하기 위해서 Exception 클래스를 상속해서 좀 더 구체적인 예외 클래스를 만들 수 있습니다.

<?php
class ValidationException extends Exception {}

이렇게 예외 클래스를 구체적으로 만들면 여러 개의 catch 문을 달아서 다른 종류의 예외를 서로 다르게 처리할 수 있게 됩니다. 그러다보면 너무 많은 예외 클래스를 만들어야 하는 것이 문제가 될 수도 있는데, 앞서 얘기한 SPL 익스텐션에서 포함되어 있는 SPL 예외 클래스들을 적절히 사용한다면 그런 문제를 줄일 수 있습니다.

예를 들어 __call() 특수 매서드(Magic Method)에 잘못된 메소드 호출이 들어온 상황을 처리할 때, 두루뭉실하게 그냥 Exception을 던지거나 Exception 클래스를 상속받은 예외 클래스를 작성해서 던지는 대신에 throw new BadMethodCallException; 이라고 할 수 있겠습니다.

Back to Top

보안

웹 어플리케이션 보안

여러분의 웹 어플리케이션을 뚫으려는 의지를 가지고 준비하고 있는 나쁜 사람들이 있습니다. 그러므로 웹 어플리케이션 보안을 견고하게 미리 준비하는 일이 중요합니다. 운이 좋게도 The Open Web Application Security Project (OWASP)에 있는 좋은 사람들이 알려져있는 보안 이슈들과 그 대책을 상세하게 정리해 두었습니다. 보안 의식이 있는 개발자라면 반드시 읽어보아야 할 자료입니다. Padraic Brady의 Survive The Deep End: PHP Security는 또 하나의 좋은 PHP 웹 어플리케이션 보안 가이드입니다.

패스워드 해시

PHP 어플리케이션을 만드는 모든 사람은 언제가 사용자 로그인 기능을 구현하게 될 것입니다. 사용자명과 패스워드를 데이터베이스에 저장해서 다음번 로그인할 때 사용자를 인증하는데 사용해야 합니다.

패스워드는 저장하기 전에 적절하게 해시(hash) 해야 합니다. 패스워드는 원문을 복원할 수 없도록 단방향 함수로 해시되어야 합니다. 이러한 해시 함수는, 원문을 알아내는데 적절한 정도로 어려운 고정적인 길이의 문자열을 생성합니다. 만약 사용자의 패스워드를 해시하지 않고 데이터베이스에 저장한다면, 어떠한 경로를 통해서든 부적절하게 데이터베이스에 제 3자가 접근했을 때 모든 사용자 계정이 탈취당하게 될 것입니다.

패스워드는 해시하기 전에 각 패스워드 전에 임의의(random) 문자열을 추가하는 것으로 소금치기(솔팅,salting) 해야합니다. 이것은 사전 공격과 “레인보우 테이블”(일반적인 패스워드에 대한 암호 해시 목록)을 사용을 방어합니다.

사용자들이 흔히 복잡도가 낮은 동일한 패스워드를 여러 서비스에 사용하기 때문에 해싱과 솔팅은 필수적입니다.

다행스럽게도, 최근에 PHP는 이것을 쉽게 할 수 있습니다.

password_hash로 패스워드 해시하기

PHP 5.5에는 password_hash()가 도입되었습니다. 지금은 PHP에서 제공되는 가장 강력한 알고리즘인 BCrypt를 사용하게 구현되어 있는데, 아마도 미래에는 더 나은 알고리즘들을 지원하게 업데이트될 것입니다. PHP 5.3.7 이상의 버전들과의 호환성을 제공하기 위해서 password_compat 라이브러리도 제공됩니다.

아래 코드에서는 문자열을 해시하고, 다른 문자열의 해시와 비교합니다. 두 개의 문자열이 서로 다르기 때문에 (‘secret-password’ vs. ‘bad-password’) 이 로그인은 실패한 것으로 간주될 것입니다.

<?php
require 'password.php';

$passwordHash = password_hash('secret-password', PASSWORD_DEFAULT);

if (password_verify('bad-password', $passwordHash)) {
    // 맞는 패스워드
} else {
    // 틀린 패스워드
}

password_hash() 는 패스워드 솔팅을 처리합니다. 솔트는 알고리즘과 “cost”와 함께 해시의 한 부분으로 저장됩니다. password_verify() 가 이것을 추출하여 패스워드를 어떻게 확인할지 결정하기 때문에 솔트를 저장하기 위한 별도의 데이타베이스 필드는 필요없습니다.

데이터 필터링

외부로부터의 입력은 절대(절대로) 믿어서는 안됩니다. 외부에서 입력 받은 데이터를 사용하기 전에는 반드시 검증하고 위험한 요소를 제거해야 합니다. filter_var()filter_input() 함수를 사용하여 텍스트에서 위험한 내용을 제거하고, 규칙(메일 주소 등)에 맞는지 검증할 수 있습니다.

외부 입력은 어느 것이라도 될 수 있습니다. $_GET이나 $_POST 폼 데이터, $_SERVER 변수에 있는 값들, fopen('php://input', 'r')로 읽은 HTTP 요청 내용 등이 그 대상이라고 할 수 있습니다. 사용자가 입력한 폼 데이터만이 외부 입력이라고 생각하지 마세요. 어디선가 업로드되었거나 다운로드한 파일, 세션 값들, 쿠키 데이터, 써드파티 웹 서비스에서 받아온 데이터들도 모두 외부 입력입니다.

외부 데이터가 저장되거나, 다른 것들과 조합되어 나중에 사용되더라도 역시 외부 입력이라고 생각해야 합니다. 데이터를 처리하거나, 출력하거나, 조합하거나 여러분의 코드에 포함시킬 때마다 그 데이터를 적절히 필터링해서 믿을 수 있는 데이터로 처리한 상태인지 항상 스스로에게 확인하세요.

데이터는 사용 목적에 따라서 서로 다르게 필터링 되어야 할 것입니다. 예를 들어, 필터링되지 않은 외부 입력을 HTML 출력에 사용한다면, 의도치 않은 HTML이나 JavaScript가 여러분의 사이트에서 표시되거나 실행될 겁니다! 이런 것을 Cross-Site Scripting (XSS)이라고 합니다. 매우 위험한 공격 기법이죠. XSS를 막는 방법으로는 사용자가 생성한 데이터를 웹 페이지에 출력하기 전에는 항상 strip_tags() 함수를 사용해서 HTML 태그를 모두 제거한다든지, htmlentities()htmlspecialchars() 함수를 사용하여 HTML 엔티티로 변경하여 출력하는 방법이 있습니다.

또다른 위험한 상황은 커맨드라인 옵션으로 전달받은 값을 사용하는 상황입니다. 아주 위험하기 때문에 보통은 별로 좋지 않은 선택인 경우가 많지만, escapeshellarg() 함수를 사용하여 커맨드라인 옵션을 필터링한 후 사용하는 것이 좋겠습니다.

마지막으로 위험한 상황의 예로 설명할 것은, 외부 입력값을 바탕으로 로드할 파일을 결정하여 파일시스템으로부터 그 파일을 로드하는 상황입니다. 그런 상황이라면 파일 이름을 파일 경로로 변경하는 방법으로 뚫릴 가능성이 있겠습니다. 그러므로 입력 데이터에서 "/", "../", null bytes 문자를 제거해야 하고, 그 외에도 숨겨진 파일, 공개되지 않아야 하는 파일, 민감한 정보가 들어있는 파일의 경로들을 필터링해야 합니다.

위험한 입력 제거(Sanitization)

외부 입력으로 받은 문자열에서 위험하거나 필요없는 문자열을 제거하거나 이스케이프처리 하는 일을 Sanitization 이라고 합니다.

예를 들어 외부 입력 데이터를 HTML이나 SQL 쿼리에 넣기 전에 반드시 위험한 요소를 제거해야합니다. PDO의 파라미터 바인딩을 사용한다면 PDO가 자동으로 그러한 처리를 해 줍니다.

때로는 사용자 입력에 포함된 안전한 HTML 태그는 HTML 페이지에 출력하려고 할 때도 있습니다. HTML Purifier라는 라이브러리가 그러한 일을 도와주기는 하지만, 사실 쉬운 일은 아닙니다. 그래서 많은 사람들은 Markdown 이나 BBCode 같은 제한된 마크업 문법을 사용하여 위험을 회피하고 있습니다.

PHP의 Sanitization 필터 문서 보기

역직렬화(Unserialization)

사용자나 신뢰할 수 없는 곳으로부터 받은 데이터에 unserialize() 사용하는 것은 위험합니다. 객체 자체가 사용되지 않는다고 하더라도 이는 악의를 가진 사용자가 사용자 정의 프로퍼티를 가지고 소멸자(destructors)는 실행하게되는 객체를 만들어내도록 허용하게 됩니다. 따라서 신뢰할 수 없는 데이터는 역직렬화하지 말아야합니다.

신뢰할 수 없는 곳으로부터의 데이터를 역직렬화해야만 한다면, PHP 7의 allowed_classes 옵션을 사용하여 역직렬화 가능한 객체 형식을 제한하세요.

데이터 검증

데이터 검증을 통해서 외부 입력 데이터가 여러분이 기대한 입력 형태가 맞는지 확인하도록 합니다. 예를 들어 메일 주소, 전화 번호, 혹은 사용자의 나이 등의 입력을 검증하고 싶을 때가 있을 것입니다.

PHP의 검증 필터 문서 보기

설정 파일

여러분의 어플리케이션을 위한 설정 파일을 만들 때, 아래에서 제시하는 방법들 중 하나를 따르기를 권장합니다.

Register Globals

NOTE: PHP 5.4.0 부터 register_globals 설정은 제거되어서 더이상 사용되지 않습니다. 이 설정은 단지 기존 어플리케이션을 업그레이드하려는 사람들에게 경고하기 위한 목적으로 포함되어 있습니다.

register_globals 설정을 활성화하면 $_POST, $_GET, $_REQUEST 에 들어있는 값들을 글로벌 스코프의 변수로 등록해줍니다. 이 기능을 사용하면, 데이터가 어디에서 온 것인지 쉽게 파악할 수가 없기 때문에 쉽게 보안 이슈에 노출될 수 있습니다.

예를 들어 $_GET['foo']$foo라는 변수로 접근할 수 있게 됩니다. 아직 선언되지 않은 변수를 오버라이드하게 될 수도 있습니다. 만약 PHP 5.4.0 이전 버전을 사용한다면 register_globals 설정을 반드시 off 로 설정하시기 바랍니다.

에러 보고

에러 로깅은 어플리케이션의 문제 지점을 찾는데 유용하기도 하지만, 어플리케이션의 구조를 외부에 노출시키는 문제가 있기도 합니다. 그러므로 이러한 이슈가 발생하지 않게 하려면 개발용 서버와 운영(라이브) 서버의 설정을 다르게 해야 합니다.

개발 서버

개발 과정에서 발생할 수 있는 모든 에러를 보여주려면 아래와 같은 설정을 php.ini에 하면 됩니다.

display_errors = On
display_startup_errors = On
error_reporting = -1
log_errors = On

-1로 설정하는 것은 앞으로 나올 PHP 버전에서 새로운 레벨이나 상수가 추가되더라도, 새로 추가된 것들까지 포함해서 모든 에러를 표시하게 만드는 설정입니다. PHP 5.4에서는 E_ALL 상수가 같은 역할을 합니다. - php.net

PHP 5.3.0에서 E_STRICT 에러 레벨이 추가되었는데, E_ALL에 포함되지 않는 레벨이었습니다. 하지만 5.4.0 에서는 E_ALL에 포함되는 것으로 변경되었습니다. 이것은 무슨 의미일까요? 모든 가능한 에러를 표시하도록 설정하려면 PHP 5.3에서는 -1로 설정하거나, E_ALL | E_STRICT라고 설정해야 한다는 이야기입니다.

PHP 버전별로 모든 에러를 표시하게 설정하는 방법

운영 서버

운영 환경에서는 에러를 표시하지 않게 설정하려면 php.ini에 아래와 같이 설정합니다.

display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL
log_errors = On

이렇게 설정하면, 에러가 발생하면 웹 서버의 에러 로그에 여전히 기록은 되지만 사용자에게 표시하지는 않습니다. PHP 매뉴얼을 보면 더 많은 정보를 얻을 수 있습니다.

Back to Top

테스트

여러분이 작성한 PHP 코드에 대한 테스트를 자동화하는 것은 성공적인 어플리케이션을 만드는 최고의 방법이라 할만 합니다. 자동화된 테스트는 또한 여러분의 어플리케이션을 수정하거나 새로운 기능을 추가할 때, 기존에 작성된 부분이 망가지지 않도록 해주는 멋진 도구입니다.

PHP를 위한 여러가지 형태의 테스팅 도구나 프레임워크가 있습니다. 이들은 서로 다른 접근법을 취하고 있긴 하지만, 단순히 최근의 수정 사항이 기존 기능을 망가뜨리지 않았는지 확인하기 위해서 거대한 QA팀을 유지하면서 인력으로 테스트를 해야만 하는 상황을 피하게 해준다는 공통점이 있습니다.

테스트 주도 개발 (TDD)

위키백과에서 인용:

테스트 주도 개발(test-driven development, TDD)은 매우 짧은 개발 사이클을 반복하는 소프트웨어 개발 프로세스 중 하나이다. 우선 개발자는 바라는 향상 또는 새로운 함수를 정의하는 (초기적 결함을 점검하는) 자동화된 테스트 케이스를 작성한다. 그런 후에, 그 케이스를 통과하기 위한 최소한의 양의 코드를 생성한다. 그리고 마지막으로 그 새 코드를 표준에 맞도록 리팩토링한다. 이 기법을 개발했거나 ‘재발견’ 한 것으로 인정되는 Kent Beck은 2003년에 TDD가 단순한 설계를 장려하고 자신감을 불어넣어준다고 말하였다.

어플리케이션 작성 시 도움이 되는 여러가지 서로 다른 유형의 테스트 기법이 존재합니다.

유닛 테스트

유닛 테스트(Unit Testing)는 함수, 클래스, 메소드가 기대한 대로 동작하는지를 검증하는 프래그래밍 측면의 접근 방법으로서, 개발을 시작하는 시점부터 계속해서 유닛 테스트를 만들어 나가게 됩니다. 함수나 메소드의 입출력을 검사함으로써 내부 로직이 올바르게 동작하는지를 테스트하는 방식입니다. 의존성 주입(Dependency Injection) 기법을 활용하면서 “모의(mock)” 개체와 스텁(stub)들을 사용하여 서로 의존 관계에 있는 클래스들이 올바르게 연동되어 있는지 테스트함으로써 테스트 커버리지를 더 높일 수 있습니다.

클래스나 함수를 만들 때, 그 클래스나 함수의 동작을 검증하는 유닛 테스트도 항상 같이 만들어야 합니다. 아주 기초적인 레벨에서 보면, 잘못된 인자를 함수에 전달한 경우 그 함수는 에러를 발생시켜야 한다는 것을 테스트하고, 적절한 인자를 전달한 경우에는 올바르게 동작한다는 것을 테스트 합니다. 이렇게 해두면 나중에 이 클래스나 함수를 변경할 일이 생겼을 때에도 기존의 기능이 변함없이 제대로 동작하고 있다는 것을 확신할 수 있습니다. 또한 작든 크든 어플리케이션을 만들때 좋지 않은 방법인 test.php에서 var_dump()를 사용하는 것을 대체할 수 있습니다.

유닛 테스트는 오픈 소스 프로젝트에 기여하는 방식에도 도움을 줍니다. 만약 여러분이 기능 결함을 재현시키는 유닛 테스트 (즉 실패하는 유닛 테스트 케이스)를 작성할 수 있고 그 결함을 고친 후 테스트가 통과되는 것을 보여준다면, 여러분의 패치 제안을 오픈 소스 프로젝트 관리자가 받아들여줄 가능성이 훨씬 높을 것입니다. 만약 여러분이 GitHub 등에서 프로젝트를 관리하고 있고 다른 사람들의 pull request 를 받을 생각이라면, pull request 를 보내는 사람들에게 기본적으로 이러한 유닛 테스트를 포함시켜 보내라고 가이드하는 것이 좋습니다.

PHPUnit이라는 테스트 프레임워크가 PHP 에서는 거의 업계 표준적인 위치에 있지만 다른 유닛 테스트 프레임워크들도 상당수 존재합니다.

통합 테스트

위키백과에서 인용:

통합 테스트(Integration testing, 때로는 통합과 테스트(Integration and Testing)이라고 하며 줄여서 I&T)는 개별적인 소프트웨어 모듈을 결합시켜 테스트하는 단계이다. 통합 테스트는 유닛 테스트 이후와 검증 테스트(validation testing) 사이에 수행된다. 유닛 테스트가 된 모듈들을 그룹지어 더 큰 단위로 합친 후, 통합 테스트 계획에 따라 테스트를 수행하여 시스템 테스트를 수행할 수 있는 정도로 개발이 완료되었는지 확인한다.

유닛 테스트에 사용되는 도구들을 사용하여 통합 테스트를 수행할 수 있고, 대부분의 원칙들이 서로 동일하게 적용될 수 있습니다.

기능 테스트

기능 테스트(functional testing)는 인수 테스트(acceptance testing)라고도 합니다. 각 코드 유닛들이 올바르게 동작하는 지 검증하거나, 코드 유닛간에 서로 올바르게 대화하고 있는지 확인하는 것이 앞선 단계의 테스트였다면, 기능 테스트는 실제로 어플리케이션을 동작시켜주는 도구를 사용하여 자동화된 테스트를 만들어서 수행하는 것입니다. 기능 테스트에 사용되는 도구들은 실제 사용자가 어플리케이션을 사용하는 것을 흉내내면서 실제 데이터를 사용하여 어플리케이션을 테스트해 줍니다.

기능 테스트를 위한 도구

행위 주도 개발 (BDD)

행위 주도 개발(Behavior-Driven Development, BDD)에는 SpecBDD와 StoryBDD라는 두 가지 형태가 있습니다. SpecBDD는 코드의 기술적인 행위에 초점을 맞추는 반면, StoryBDD는 비즈니스 관점이나 기능 관점의 행위와 상호작용에 초점을 맞춥니다. 두 가지 타입의 BDD를 지원하는 PHP용 라이브러리가 모두 존재합니다.

StoryBDD 기법을 사용한다면, 어플리케이션의 행위를 기술하는 “스토리”를 작성하는데, 이 스토리라는 것은 사람이 읽을 수 있는 형태로 작성하는 것입니다. 이렇게 작성된 스토리들은 어플리케이션에 대한 테스트로서 실행될 수 있습니다. StoryBDD를 할 수 있게 해주는 Behat이라는 라이브러리는 Ruby의 Cucumber 프로젝트에 영향을 받았다고 하는데, 어플리케이션의 행위를 기술하는 데에는 Gherkin DSL을 구현하여 사용하고 있습니다.

SpecBDD 기법을 사용한다면 여러분의 코드가 어떻게 행위할 것인지 기술하는 스펙을 작성하게 됩니다. 함수나 메소드를 테스트한다는 개념으로 바라보는 것이 아니라, 그 함수나 메소드가 어떻게 행위하는지 기술한다는 개념으로 바라봅니다. PHPSpec 프레임워크가 바로 이런 목적으로 사용할 수 있는 프레임워크입니다. 이 프레임워크는 Ruby의 RSpec 프로젝트에 영향을 받았습니다.

BDD 관련 링크

보조적인 테스트 도구

테스트 주도 개발이나 행위 주도 개발 프레임워크들 외에도, 어떤 기법을 사용하는 프로젝트에든지 일반적으로 사용될 수 있는 프레임워크나 헬퍼 라이브러리들이 있습니다.

도구들 링크

Back to Top

서버와 배포

PHP 어플리케이션을 운영을 위한 웹 서버에 배포하는 여러가지 방법이 있습니다.

Platform as a Service (PaaS)

PHP 어플리케이션을 웹에 올리기 위한 시스템과 네트워크 아키텍처를 PaaS가 제공해줍니다. 그래서 PHP 어플리케이션이나 프레임워크를 실행시키기 위한 설정이 거의 혹은 전혀 필요없습니다.

최근들어 PHP 어플리케이션의 규모에 상관없이 배포와 호스팅, 규모 확장을 위한 방법으로 PaaS가 인기를 끌고 있습니다. 자료들 섹션에서 PHP PaaS(Platform as a Service) 제공자 항목을 살펴보세요.

가상서버 또는 전용서버

시스템 관리에 이미 익숙하거나 배우고 싶은 생각이 있다면 가상 서버나 전용 서버를 사용하는 것이 좋겠습니다. 어플리케이션의 운영 환경에 대한 모든 것을 직접 조절할 수 있으니까요.

nginx와 PHP-FPM

PHP는 내장된 FastCGI Process Manager (FPM) 덕분에 nginx에서 아주 잘 동작합니다. nginx(‘엔진 엑스’라고 읽습니다)는 가볍고 아주 빠른 웹서버입니다. Apache에 비해 더 적은 메모리를 사용하고, 더 많은 수의 요청을 동시에 처리할 수 있습니다. 메모리를 충분히 확보할 수 없는 가상 서버에서라면 이러한 특징이 더 중요합니다.

Apache와 PHP

PHP와 Apache는 함께한 역사가 아주 깁니다. Apache는 설정으로 변화할 수 있는 범위가 아주 넓고 기본 기능을 확장해주는 수 많은 모듈들이 있습니다. 여러 어플리케이션이 공유해서 사용하는 서버로서는 가장 많이 사용하는 서버이기도 하고, WordPress 같은 오픈소스 어플리케이션이나 PHP 프레임워크를 설치하기에도 쉽습니다. 하지만 nginx에 비교하면 기본적으로 Apache가 더 많은 리소스를 사용하고, 처리할 수 있는 동시접속 수도 적습니다.

PHP를 위한 다양한 설정을 Apache에서 할 수 있습니다. 공통적으로 가장 많이 사용하고 쉽게 할 수 있는 설정은 mod_php5 모듈을 사용하는 prefork MPM 방식입니다. 메모리 활용이 가장 효율적인 선택은 아니지만 가장 쉽게 어플리케이션을 설치하여 동작시킬 수 있습니다. 시스템과 서버 관리 측면을 너무 깊이 파고들고 싶지 않다면 이 방식이 가장 좋습니다. mod_php5 모듈을 사용한다면 prefork MPM도 반드시 사용해야 한다는 것을 기억하세요.

다른 방법으로, Apache 로부터 성능과 안정성을 더 짜내고 싶다면 nginx에서와 같이 FastCGI를 사용하는 방법이 있습니다. worker MPM이나 event MPM을 mod_fastcgi나 mod_fcgid 모듈과 함께 사용하는 것입니다. 앞에서 얘기한 방식이 비하면 메모리 측면에서도 현저하게 효율적이고 속도도 더 빠르긴 하지만 설정에는 손이 더 많이 갑니다.

If you are running Apache 2.4 or later, you can use mod_proxy_fcgi to get great performance that is easy to setup.

공유서버

PHP는 인기있는 언어라서 공유 서버에서 PHP를 사용할 수도 있습니다. PHP가 설치되지 않은 호스팅 서비스를 찾기는 쉽지 않겠지만 최신 버전 PHP가 설치되어 있는지는 확인을 해보아야 합니다. 여러분의 웹 어플리케이션과 다른 개발자들의 웹 어플리케이션을 같은 기기에 배포하여 동작시키는 것이 공유 서버 방식입니다. 비용이 싸다는 것이 이 방식의 장점입니다. 단점이라면 같이 입주한 이웃 개발자가 어떤 위험한 일을 할 지 알 수 없다는 것이죠. 서버를 다운시키거나 보안 허점을 노출시켜서 여러분도 같이 위험에 빠질 가능성이 있다는 것이 고려할만한 내용입니다. 공유 서버를 사용하지 않을 정도의 프로젝트 예산이 있다면 당연히 피하는 것이 좋겠습니다.

공유서버가 최신 버전의 PHP를 제공하는지 확인하기 위해서는, PHP Versions를 확인하세요.

어플리케이션 빌드와 배포

여러분이 어플리케이션을 업데이트할 때, 업데이트도 수작업으로 하는데 데이터베이스 스키마 변경도 수작업으로 하고 테스트도 수작업으로 하고 있다면 다시 한 번 생각해 보시기 바랍니다. 어플리케이션 업데이트 때마다 사람이 직접해야 하는 작업이 있고, 그 작업의 수만큼 치명적인 실수가 발생할 가능성도 올라갑니다. 간단한 업데이트를 하는 상황이든지, 복잡한 빌드 과정을 수행하고 있든지, 지속적인 통합 전략을 사용하려고 하든지 간에 빌드 자동화가 여러분에게 꼭 필요할 것입니다.

아마도 다음과 같은 작업을 자동화하고 싶다는 생각이 들 것입니다.

배포 도구

배포 도구라는 건 소프트웨어 개발 과정에서 공통적으로 일어나는 작업을 다루는 스크립트들의 모음이라고 할 수 있습니다. 배포 도구는 여러분이 개발한 소프트웨어의 일부가 아니라 소프트웨어의 ‘밖에서’ 필요한 동작을 수행해주는 녀석입니다.

빌드 자동화와 배포를 도와주는 많은 오픈소스 도구들이 있습니다. PHP로 작성된 것도 있고 그렇지 않은 것도 있죠. PHP로 작성되지 않았다는 것보다는 필요한 작업을 잘 수행해주느냐 하는 것이 중요할 것입니다. 여기에서 몇가지를 소개합니다.

Phing은 패키징, 배포, 테스팅 과정을 XML 빌드 파일 하나로 설정할 수 있습니다. Apache Ant를 기반으로 한 Phing은, 웹 어플리케이션을 설치하거나 업데이트할 때 필요한 PHP로 작성된 많은 종류의 작업을 기본적으로 지원하고 있고, 커스텀 작업을 추가하여 확장할 수도 있습니다. Phing은 오랜기간 유지된 단단하고 견고한 도구이지만, XML 파일로 설정을 다루는 방식때문에 약간 구식으로 보일 수도 있습니다.

Capistrano는 하나 혹은 다수의 리모트 컴퓨터에서 구조화되고 반복가능한 커맨드를 실행시키려고 하는 중고급 프로그래머를 위한 시스템입니다. 기본적으로는 Ruby on Rails 어플리케이션을 배포할 수 있도록 구성되어 있지만, PHP 어플리케이션도 성공적으로 배포하고 있습니다. Capistrano를 잘 사용하려면 Ruby와 Rake에 대해서도 잘 알아야 합니다. Capistrano에 관심이 있는 PHP 개발자에게는 Dave Gardner의 PHP Deployment with Capistrano라는 블로그 포스트가 좋은 출발점이 될 것입니다.

Rocketeer는 라라벨 프레임워크로부터 그 철학과 영감을 얻었습니다. 빠르고, 우아하며, 효과적인 기본값으로 쉽게 사용할 수 있는 것이 목표입니다. 여러 서버, 스테이지(stage), 원자적 배치와 배포를 병렬적으로 수행할 수 있습니다. 이 도구의 모든 것은 핫 스왑(hot swap)하거나 확장할 수 있으며, PHP로 작성되었습니다.

Deployer는 PHP로 작성된 배포도구이며, 단순하고 함수형으로 작성되었습니다. 병렬적으로 태스크들을 실행하고, 원자적으로 배포하며, 서버간의 일관성을 유지하는 기능들을 포함하고 있습니다. Symfony, Laravel, Zend Framework, Yii를 위한 일반적인 레시피가 있습니다. Younes Rafie의 Easy Deployment of PHP Applications with Deployer라는 글은 Deployer를 사용하여 어플리케이션을 배포하기 위한 좋은 튜토리얼입니다.

Magallanes는 YAML 파일로 간단하게 설정할 수 있는 PHP로 작성된 도구입니다. 여러 서버와 환경, 원자적 배포와 함께, 일반적인 도구와 프레임워크에 활용할 수 있는 몇몇 내장 태스크가 있습니다.

더 읽어볼 만한 것들

서버 프로비저닝

많은 수의 서버를 맡게 되었을때 서버를 관리하고 구성하는 일은 곤란한 일이 될 수 있습니다. 알맞은 서버를 적절히 구성할 수 있도록 인프라를 자동화하여 이것을 해결할 도구들이 있습니다. 이 도구들은 보통 매우 쉽게 어플리케이션을 스캐일링하는 것과 같은 인스턴스 관리를 위해 대형 클라우드 호스팅 제공업체(아마존 웹 서비스, 헤로쿠, 디지털오션 등)와 연동됩니다.

Ansible은 YAML 파일로 인프라를 관리하는 도구입니다. 시작하기 쉬우며, 복잡하고 커다란 규묘의 어플리케이션도 관리할 수 있습니다. 클라우드 인스턴스를 관리하기 위한 API가 있고, 특정 도구들을 사용하는 다이나믹 인벤토리(dynamic inventory)를 통해 관리할 수 있습니다.

Puppet은 서버를 관리하고 구성하기 위한 자체 언어와 파일 형식을 가진 도구입니다. 마스터/클라이언트 혹은 “master-less” 모드로 설치하여 사용할 수 있습니다. 마스터/클라이언트 모드에서 클라이언트는 중앙 마스터에서 새로운 설정을 일정 주기로 가져오고 필요할 경우 자신을 업데이트 합니다. master-less 모드에서는 노드들에 변경사항을 직접 전달합니다.

Chef는 전체 서버 환경이나 가상 머신을 만들 수 있는 루비 기반의 강력한 시스템 통합 프레임워크입니다. OpsWorks라고 불리는 아마존 웹 서비스 내의 서비스를 통해서 잘 연동됩니다.

더 읽어볼 만한 것들

지속적인 통합

지속적인 통합이란 팀 멤버들이 서로의 작업 결과물을 자주 통합하는 소프트웨어 개발 기법입니다. 보통 한 명이 적어도 하루에 한 번은 통합을 하는 수준을 얘기하는데, 결국 팀 전체로 보면 하루에도 여러 차례 통합을 한다는 얘기가 됩니다. 이런 기법을 사용함으로써 통합 시 발생하는 이슈가 눈에 띄게 줄어들고, 잘 응집된 소프트웨어를 만들 수 있다는 것을 많은 팀이 체험하고 있습니다.

– Martin Fowler

PHP 프로젝트에서 지속적인 통합 기법을 사용하는 방법에는 여러가지 방법이 있습니다. Travis CI가 작은 규모의 프로젝트에도 현실성있게 적용할 수 있도록 멋진 결과물을 내놓고 있습니다. Travis CI는 오픈소스 커뮤니티를 위한, 인터넷에서 호스팅되는 지속적인 통합 서비스입니다. GitHub와 통합되어 있기도 하고, PHP를 포함한 많은 프로그래밍 언어를 아주 잘 지원하고 있습니다.

더 읽어볼 만한 것들:

Back to Top

가상화

개발환경과 운영환경을 서로 다르게 사용하면 운영환경에 어플리케이션을 배포했을 때 의도하지 않은 이상한 버그를 만날 가능성이 높습니다. 또 여러 사람이 같이 개발할 때 서로 다른 환경에서 모두 동일한 라이브러리 버전을 최신으로 유지하며 관리하는 일은 성가신 일이 되기도 합니다.

Windows에서 개발하면서 Linux 등의 Windows가 아닌 환경에 배포하여 운영하거나, 팀을 이뤄 개발할 때에는 가상 머신을 사용하는 게 나을 수도 있습니다. 가상 머신을 사용한다는게 복잡하게 들릴 수도 있겠지만, 단지 몇 단계만으로 가상 머신을 구성할 수 있게 도와주는 도구들이 있습니다.

Vagrant

Vagrant는 간단한 설정 파일 하나만 있으면, VMware 등 잘 알려져 있는 가상 환경에 가상 머신을 만들어 줍니다. 이렇게 만든 깨끗한 가상 머신에서 출발하여 수작업으로 환경을 설정할 수도 있고, Puppet이나 Chef와 같은 “프로비저닝(provisioning)” 도구를 사용하여 가상 머신을 설정할 수도 있습니다.

프로비저닝 도구를 사용하면 여러개의 기본 가상 머신을 동일한 방식으로 설정할 수 있습니다. 또한 복잡한 “초기 설정” 커맨드 목록을 직접 관리하지 않아도되어 편리합니다. 그리고 기본 가상 머신을 “파괴”하고 “깨끗한” 상태로 새로 만드는 작업도 간단히 할 수 있게 도와줍니다.

Vagrant는 여러분의 호스트 컴퓨터와 가상 머신 사이에 공유 폴더를 만들어줍니다. 그래서 호스트 컴퓨터에서 파일을 수정하고 가상 머신에서 실행하는 식으로 작업할 수 있습니다.

도움되는 내용들

Vargrant 사용을 시작하는데 약간의 도움이 필요하신 분은 아래 서비스들이 유용할 것 같습니다.

Docker

Docker 는 완전한 가상 머신을 대체할 수 있는 경량의 도구로 “컨테이너”로 이루어져 있어서 그렇게 불립니다. 가장 간단한 경우에 컨테이너는 웹 서버 하나를 동작시키는 것과 같이 특정한 하나의 역할을 하는 구성 단위입니다. “이미지”는 컨테이너를 만들이 위해서 사용하는 패키지입니다. Docker는 이미지들로 채워진 저장소가 있습니다.

전형적인 LAMP 어플리케이션은 웹 서버, PHP-FPM 프로세스, MySQL, 이렇게 세 개의 컨테이너로 구성될 수 있습니다. Vagrant 의 공유 폴더와 같이 Docker가 어디에서 어플리케이션을 찾을 위치를 알려줄 수 있습니다.

컨테이너를 명령어로 만들거나(아래 예제 참고), 프로젝트를 위하여 컨테이너를 생성하고 다른 것들과 어떻게 통신할지 정의한 docker-compose.yml 파일을 유지보수의 편의를 위해 만들 수 있습니다.

여러 웹사이트를 개발하면서 각 가상 머신에 분리하여 설치하고 싶은데, 충분한 디스크 공간이 없거나 모든 것을 최신으로 유지할 시간이 없다면, Docker 가 도움이 될 수 있습니다. 다운로드와 설치가 빠르고, 각 이미지 당 한 개만 저장하고 있으면 계속 사용할 수 있으며, 컨테이너는 적은 RAM을 사용하고 동일한 OS 커널을 공유하기 때문에 동시에 더 많은 서버를 동작시킬 수 있는데다가, 시작과 정지를 위해서 전체 서버가 부팅되는 것을 기다릴 필요없이 몇 초 정도면 됩니다.

예제: Docker 안에서 PHP 어플리케이션을 실행하기

Docker를 설치하고 나면, 웹서버를 명령어 하나로 시작할 수 있습니다. 아래의 명령어는 최신 버전의 PHP와 함께 완벽하게 동작하는 Apache를 내려받고, /path/to/your/php/files디렉토리를 document root로 설정하여 http://localhost:8080에서 볼 수 있도록 합니다.

docker run -d --name my-php-webserver -p 8080:80 -v /path/to/your/php/files:/var/www/html/ php:apache

이제 컨테이너는 초기화 후 실행될것입니다. -d 는 백그라운드로 실행되도록 합니다. 컨테이너를 멈추거나 다시 시작하고 싶다면, docker stop my-php-webserver, docker start my-php-webserver와 같이 (위에 나왔던 매개변수들을 모두 다시 입력할 필요 없이) 간단히 실행할 수 있습니다.

Docker에 대해 더 알아보기

앞서 언급된 명령어는 간단한 웹서버를 빠르게 실행하는 방법입니다. 그러나, Docker로 더 많은것들을 할 수 있습니다. (그리고 Docker Hub에는 많은 이미지가 미리 만들어져 있습니다) 용어를 습득하고, Docker 사용자 설명서를 천천히 읽어보세요. 비공식 이미지들은 최신 보안 패치를 적용하지 않았을 수도 있기 때문에, 안전한지 체크하지 않은 다운로드한 코드를 그냥 실행해서는 안됩니다. 잘 모르겠다면, 공식 레포지토리를 사용하세요.

PHPDocker.io 사이트는 선택한 PHP 버전과 확장을 포함하여 완전히 동작하는 LAMP/LEMP 스택에 필요한 모든 파일을 자동으로 생성합니다.

Back to Top

캐시

PHP는 별로 손을 대지 않아도 꽤 빠른 편이긴 하지만, 원격 시스템과 연결을 맺거나 파일을 불러들여야 할 때에는 병목지점이 발생할 수 있습니다. 고맙게도 이렇게 시간을 소요하는 작업을 줄여서 어플리케이션의 성능을 올리는 일을 도와주는 많은 도구들이 있습니다.

Opcode 캐시

PHP 파일을 실행하면 가장 먼저 이 파일은 컴파일해서 opcode (CPU를 위한 기계 언어 명령) 로 만들게 됩니다. 원본 PHP 파일이 수정되지 않으면 컴파일된 opcode는 항상 같기때문에, 이러한 컴파일 과정은 CPU 리소스 낭비가 됩니다.

opcode 캐시는 opcode를 메모리에 보관하고, 이후 호출 시에 재사용하여 불필요한 컴파일 과정이 일어나지 않게 합니다. 보통 시그니처나 파일의 수정 시각을 먼저 확인하여 변경이 있는지 검사합니다.

opcode 캐시는 아마 여러분의 어플리케이션을 상당히 빠르게 개선할 것입니다. PHP 5.5 부터는 Zend OPcache 라는 opcode 캐시가 내장되어 있습니다. PHP 패키지/배포판에 따라서 기본으로 활성화 되어있습니다. opcache.enable 에서 확인하고, phpinfo() 출력값으로 확인하세요. 이전 버전의 PHP를 위해서는 PECL 확장이 있습니다.

아래는 인기있는 opcode 캐시들입니다.

개체 캐시

개체(object) 인스턴스 중에 어떤 것을 캐시하는 게 성능 향상에 도움을 줄 때가 있습니다. 불러오는 비용이 큰 데이터나 한 번 데이터를 불러오면 그 뒤로는 별로 바뀔 일이 없는 데이터베이스 호출 같은 것들 말입니다. 개체 캐시 소프트웨어를 사용해서 이런 데이터를 메모리에 보관해두면 다음부터는 아주 빠르게 접근할 수 있습니다. 그래서 상당한 성능 향상을 얻을 수 있고 데이터베이스 접속 횟수가 줄어 데이터베이스 서버의 부담도 줄어듭니다.

인기있는 바이트코드 캐시 도구들은 여러분의 커스텀 데이터 또한 캐시해주는 기능을 가진 경우가 많습니다. 바이트코드 캐시 도구를 사용할만한 이유가 또 있었네요. APCu, XCache, WinCache 모두 메모리 캐시에 여러분의 데이터를 보관할 수 있는 API를 제공합니다.

가장 널리 사용되고 있는 메모리 개체 캐시 도구는 APCu와 memcached입니다. APCu는 개체 캐싱을 위한 훌륭한 선택지입니다. 여러분의 데이터를 캐시할 수 있는 간단한 API를 제공하는데다 설치와 설정도 쉽습니다. 딱하나 단점이라면 설치된 서버로만 범위가 한정된다는 것입니다. 반면에 memcached는 별도의 서버에 설치해서 네트워크로 접근할 수도 있습니다. 그래서 아주 빠른 데이터 저장소를 중앙에 두고 여러 시스템에서 데이터를 가져다 쓰는 구조를 만들 수도 있습니다.

PHP를 CGI나 FastCGI 형태로 실행할 때에는 모든 PHP 워커 프로세스가 개별적인 캐시를 가집니다. 그래서 APCu가 캐시한 데이터는 서로 다른 워커 프로세스 사이에 공유될 수 없습니다. 만약 공유해야하는 경우라면 memcached 를 사용해야 합니다. memcached는 별도의 프로세스로 동작하기 때문입니다.

보통 접근 속도 측면에서는 APCu가 memcached에 비해 더 나은 성능을 보여주지만, memcached가 확장성 측면에서는 더 낫습니다. 여러분의 어플리케이션을 여러 서버에 분산해서 사용한다든지 memcached의 고급 기능들을 활용할 필요가 없다면 아마도 APCu가 가장 좋은 선택일 것입니다.

APCu를 사용하는 예제 코드입니다.

<?php
// 캐시에 'expensive_data'가 저장되어 있는지 확인한다
$data = apc_fetch('expensive_data');
if ($data === false) {
    // 캐시에 저장된 데이터가 없다. 저장했다가 나중에 사용하자.
    apc_add('expensive_data', $data = get_expensive_data());
}

print_r($data);

PHP 5.5보다 낮은 버전에서는 APC가 개체 캐시와 바이트코드 캐시 기능을 모두 제공했습니다. 하지만 PHP 5.5부터는 내장된 바이트코드 캐시(OPcache)가 있으므로, APC 프로젝트는 APC의 개체 캐시 기능만을 분리하여 제공하는 APCu라는 프로젝트로 변화하였습니다.

인기있는 개체 캐시 도구들에 대해서 더 알아보기

Back to Top

코드 문서화하기

PHPDoc

PHPDoc은 PHP 코드에 코멘트를 다는 방식에 있어서 비공식 표준이라고 할 수 있습니다. 상당히 많은 태그들을 사용할 수 있죠. PHPDoc 매뉴얼에서 사용할 수 있는 전체 태그 목록과 예제를 볼 수 있습니다.

메소드 몇 개를 포함하고 있는 클래스를 어떻게 문서화할 수 있는지 보여주는 예제를 한 번 봅시다.

<?php
/**
 * @author A Name <a.name@example.com>
 * @link http://www.phpdoc.org/docs/latest/index.html
 */
class DateTimeHelper
{
    /**
     * @param mixed $anything \DateTime 오브젝트로 변환할 수 있는 모든 것
     *
     * @throws \InvalidArgumentException
     *
     * @return \DateTime
     */
    public function dateTimeFromAnything($anything)
    {
        $type = gettype($anything);

        switch ($type) {
            // \DateTime 오브젝트를 리턴하는 코드
        }

        throw new \InvalidArgumentException(
            "Failed Converting param of type '{$type}' to DateTime object"
        );
    }

    /**
     * @param mixed $date \DateTime 오브젝트로 변환할 수 있는 모든 것
     *
     * @return void
     */
    public function printISO8601Date($date)
    {
        echo $this->dateTimeFromAnything($date)->format('c');
    }

    /**
     * @param mixed $date \DateTime 오브젝트로 변환할 수 있는 모든 것
     */
    public function printRFC2822Date($date)
    {
        echo $this->dateTimeFromAnything($date)->format('r');
    }
}

문서 가장 처음에는 @author 태그와 @link 태그가 있습니다. @author 태그는 작성한 사람을 표현하는데 사용되는 태그입니다. 작성자가 여러 사람이라면 태그를 반복하여 여러번 쓸 수 있습니다. @link 태그는 코드와 관련이 있는 웹사이트를 표현하는 의미입니다.

클래스 안쪽으로 들어가보면, 첫 번째 메소드에는 @param 메소드에 전달되는 파라미터의 타입과 이름, 설명을 문서화하는데 사용되는 태그입니다. 그 뒤에는 @return 태그와 @throws 태그를 써서 메소드의 리턴 타입과 메소드 실행 시 발생할 수 있는 예외를 문서화 해 두었습니다.

두 번째와 세 번째 메소드에도 동일하게 하나의 @param 태그를 붙여 두었습니다. 두 메소드 중에 하나는 @return 태그가 있고 하나는 없는 차이가 있는 것을 볼 수 있습니다. @return void 라고 써두면 우리는 그 메소드에 리턴값이 없다는 것을 명시적으로 써 둔 셈입니다. 하지만 역사적으로 @return void를 생략하여도 리턴값이 없다는 동일한 의미로 누구나 인식하고 있습니다.

Back to Top

자료들

공식사이트

유명개발자

흥미롭고 유용한 PHP 커뮤니티 멤버들을 찾는 것을 처음 시작할 때는 어렵습니다. 광범위한 PHP 커뮤니티 멤버들과 그들의 트위터를 아래에서 찾을 수 있습니다.

멘토링

PHP PaaS 제공자

위의 PaaS 제공자들이 어떤 버전을 사용하는지 확인하려면, PHP Versions를 확인하세요.

프레임워크

대부분의 PHP 개발자들이 “바퀴를 또 발명하지” 않고, 프레임워크를 사용하여 웹 어플리케이션을 개발합니다. 프레임워크들은 개발 시에 일반적으로 해야하는 작업들을 사용하기 쉬운 인터페이스로 추상화해서 제공하기 때문에 저수준의 고민거리를 날려줍니다.

물론 모든 프로젝트에 프레임워크를 사용할 필요는 없습니다. 때로는 PHP를 그대로 사용하는 게 좋을 때도 있습니다. 존재하는 프레임워크들을 크게 세 가지 종류로 나눠볼 수 있습니다.

마이크로 프레임워크(Micro Framework)는 HTTP 요청을 콜백, 컨트롤러, 클래스의 메소드 등으로 최대한 빠르게 전달해주는 역할을 하는 정도입니다. 때로는 기본적인 데이터베이스 래퍼 등을 제공해서 개발을 도와주기도 합니다. 원격 HTTP 서비스를 개발할 때 주로 사용됩니다.

다수의 프레임워크들은 마이크로 프레임워크 위에 상당한 수의 기능을 덧붙여서 제공합니다. 이런 것을 풀스택(full-stack) 프레임워크라고 합니다. ORM이라든지 사용자 인증 방법에 관한 패키지 등을 포함하는 경우도 종종 있습니다.

컴포넌트 기반의 프레임워크는 한 가지 목적에 특화된 라이브러리들의 모음입니다. 마이크로 프레임워크나 풀스택 프레임워크를 만들어내기 위해서 서로 다른 컴포넌트 기반의 프레임워크를 사용할 수 있습니다.

컴포넌트

“컴포넌트”는 공유될 수 있는 코드를 작성하고 배포하기 위한 또다른 접근 방식입니다. 다양한 컴포넌트 저장소가 존재하지만 주요한 것은 두 가지가 있습니다.

두 저장소 모두 커맨드라인 도구를 사용하여 컴포넌트(혹은 패키지)를 설치하고 업그레이드할 수 있게 되어 있습니다. 자세한 내용은 의존성 관리 섹션에서 설명했습니다.

또한 컴포넌트 기반의 프레임워크들도 있으며, 프레임워크는 전혀 제공하지 않는 컴포넌트 제공사들도 있습니다. 그런 프레임워크나 컴포넌트들은 다른 프레임워크나 패키지에 대한 의존성이 전혀 없거나, 최소한의 의존성만으로 사용할 수 있는 패키지 소스가 된다고 할 수 있습니다.

예를 들면 FuelPHP Validation package는 FuelPHP 프레임워크를 사용하지 않더라도 여러분의 프로젝트에서 사용할 수 있게 되어 있습니다. 이러한 프로젝트들은 재사용 가능한 컴포넌트들을 별도의 저장소에서 관리하고 있습니다.

Laravel의 Illuminate 컴포넌트는 Laravel 프레임워크로부터 좀더 디커플링 될 것입니다. 현재로서는 위에 언급된 것들만이 Laravel 프레임워크로부터 가장 잘 디커플링 되어 있는 컴포넌트들입니다.

그외 유용한 자료들

치트시트

더 나은 사례들

PHP 와 웹 개발 커뮤니티에 대한 소식

주간 뉴스레터에 가입하여 새로운 라이브러리들과 최신 뉴스, 행사에 대한 소식, 일반적인 공지사항들, 그리고 때때로 나오는 다른 정보들에 대해서도 받아볼 수 있습니다.

PHP 세계

동영상 튜토리얼

YouTube 채널

유료 동영상

도서

PHP를 다루는 책은 아주 많지만, 슬프게도 너무 오래되어서 이제는 쓸모없거나 잘못된 정보를 담고 있는 책도 있습니다. 심지어 “PHP 6”라는 존재하지 않는 PHP 버전에 대한 책도 있습니다. PHP 5.6 이후의 메이저 버전이 “PHP 7”이라고 이름 붙여진 것도 어느정도는 이런 이유때문입니다.

이 섹션은 추천할만한 PHP 개발 서적을 추천하는 ‘살아있는’ 문서입니다. 여러분이 쓴 책을 추가하고 싶다면 PR을 보내주십시오. 검토해보겠습니다.

무료도서

유료도서

Back to Top

커뮤니티

PHP 커뮤니티는 매우 거대하고 널리 퍼져있습니다. 멤버들은 모두 새로운 PHP 프로그래머를 도와줄 준비가 되어 있습니다. 이 사이트에 정리된 베스트 프랙티스에 관해서 더 알아보고 싶다면 여러분의 근처에 있는 PHP 사용자 그룹에 가입하거나 PHP 컨퍼런스에 참석해보세요. irc.freenode.com에 있는 #phpc 채널에서 IRC로 대화를 하거나 @phpc 트위터 계정을 팔로우 할 수도 있습니다. 여러가지 방법으로 새로운 개발자들을 만나고, 새로운 내용을 배우고, 새로운 친구를 만드세요! Google+의 PHP 프로그래머 커뮤니티StackOverflow도 활용해 보세요.

PHP 공식 이벤트 달력 보기

사용자 그룹

만약 당신이 큰 도시에 산다면, PHP 사용자 그룹이 주변에 있을 가능성이 높습니다. 당신은 PUG를 PHP.ug를 기반으로 하는 php.net의 PUG 리스트에서 쉽게 찾을 수 있습니다. 그리고 Meetup.com 이나 좋아하는 검색엔진에서 php user group near me 라는 키워드를 통해 찾을 수 있습니다. (예 Google). 만약 당신이 작은 도시에 산다면, 그 지역에는 PUG 가 없을 수도 있습니다; 그런 경우, 새로 만들어보세요!

2개의 세계적인 그룹을 특별히 언급하겠습니다. : NomadPHP, PHPWomen. NomadPHP 한달에 2번, PHP 커뮤니티 최고의 발표자 몇몇 분이 진행하는 프레젠테이션과 함께 온라인 그룹 회의를 제공합니다. PHPWomen PHP 세계에서 여성을 타겟으로 하는 비독점적인 사용자 그룹입니다. 멤버십은 대부분의 커뮤니티에서 모두에게 열려있습니다. PHPWomen은 멘토링, 교육을 위한 네트워크를 지원합니다. 그리고 일반적으로 “여성 친화적”이고 전문적인 분위기를 형성하고 있습니다.

PHP 위키에서 사용자 그룹에 대해 읽어보기

역주: 국내에도 PUG를 찾을 수 있습니다. 여러분이 읽고 있는 이 문서 번역에 참가하였으며, 한달에 한번 정기적인 모임을 갖고 있습니다. 모임에서는 PHP에 대해 심도있는 토론을 나누고, PHP개발자들 간의 소통이 이루어지고 있습니다.

Modern PUG(모던퍼그, Modern PHP User Group)

컨퍼런스

세계 많은 나라에서 큰 규모의 PHP 컨퍼런스가 개최됩니다. PHP 커뮤니티의 유명한 인물들이 그런 큰 이벤트에서 발표를 하곤 합니다. 업계의 리더들로부터 직접 뭔가 배울 수 있는 좋은 기회가 될 것입니다.

PHP 컨퍼런스 찾아보기

ElePHPants

ElePHPant는 PHP 프로젝트의 아름다운 마스코트인 코끼리입니다. 1998년에 Vincent Pontier가 PHP 프로젝트를 위해서 처음 디자인했는데, 이것은 전세계의 수천의 elePHPant들의 정신적인 아버지가 되었고, 10년 후에는 플러시천으로 된 사랑스러운 코끼리 장난감까지 나오게 되었습니다. 이제 elePHPant는 많은 PHP 컨퍼런스에 등장하고 있고, 많은 PHP 개발자들의 컴퓨터에서 재미와 힘을 주기 위해서 함께하고 있습니다.

Vincent Pontier 인터뷰