PHP는 언어적인 지원은 물론, 환경이나 커뮤니티도 계속 발전하고 있다. 최근 프레임워크 운용 그룹(Framework Interop Group, FIG)에서 제안하는 PSR 문서를 보면 알 수 있듯, 표준화된 라이브러리를 만들기 위해 라이브러리/패키지 개발에 대한 합의도 활발하게 진행되고 있어서 예전 그 난장판이던 분위기와는 사뭇 다르다. PSR에서 다뤄지는 내용은 미래에 사용할 기능이 아니라 지금 현재 PHP에서 당장 활용할 수 있는 기술이다. 더이상 미룰 수 없고, 미뤄서도 안된다는 이야기다.
구석기 PHP는 농담 짙은 표현이지만 이젠 마치 과거의 유물과도 같은 코드에 그만 집착하고 현대에 맞는 코드를 작성했으면 한다. 이 글은 예전 방식으로 PHP를 개발하고 있다면 자주 접했을 만 한 문제를 정리하고 있다. 이 글에서 PSR의 내용을 직접적으로 다루지는 않지만, PSR를 준수하는 것으로 여기서 말하는 현대적인 개발과 과거의 PHP 개발은 어떤 부분이 다르고, 어떻게 편리한지 확인해보자.
구석기 PHP
함수, 변수, 클래스의 전역적인 공해
파일을 불러오고 나면 각각의 파일이 갖고 있던 경계(스코프)가 전역으로 확장되고 어디서나 사용할 수 있는 함수, 변수, 클래스가 만들어지게 된다.
<?php
// lib/haruair/function.php
function HelloWorld() {
return "HelloWorld";
}
?>
<?php
// lib/wordpress/function.php
function HelloWorld() {
return "HelloWorld. I'm wordpress btw.";
}
?>
<?php
// app.php
include_once('./lib/haruair/helloWorld.php');
include_once('./lib/wordpress/helloWorld.php');
// PHP Fatal error: Cannot redeclare HelloWorld()
?>
같은 함수명이나 클래스명을 사용하면 다시 선언할 수 없는 문제가 발생한다. 특히 워드프레스 개발을 하다보면 플러그인 내에 동일한 함수나 클래스명을 사용하고 있어 이런 문제가 발생하는 경우가 자주 있다. 대부분 function_exists()
와 같은 함수를 이용해 미리 확인하고 선언하는 방식으로 처리되어 있는데 여전히 좋은 방법은 아니다.
그래도 함수나 클래스의 이름이 중복되는 경우에는 문제가 있는걸 바로 자각할 수 있지만, 동일한 명칭의 전역 변수가 있고 각각의 파일에서 그 변수를 활용하고 있다면 그 누구도 결과를 예상할 수 없게 된다. 이런 경우는 어느 하나의 이름을 모두 변경해야 하거나 언제 발생할지 모르는 에러를 감내해야 한다.
기본으로 모든 파일 로드하기
이전 방식의 개발에서는 다음과 같은 lib.php를 만들어 의존성을 갖는 모든 파일을 불러와 활용하는 경향이 있다. 이 파일 하나를 불러오면 각각 파일의 함수, 변수, 클래스를 모두 불러와서 사용할 수 있게 된다.
<?php
// lib.php
include_once('./lib/A/Orders.php');
include_once('./lib/B/Account.php');
include_once('./lib/B/AccountManagement.php');
include_once('./lib/B/AccountSomething.php');
include_once('./lib/C/Report.php');
include_once('./lib/E/Admin.php');
?>
이런 개발 방식은 오랫동안 큰 대안 없이 활용되고 있고, 지금까지도 많은 코드에서 발견되는 방식이다. 각각 필요에 따라 include하는 경우도 있지만, 각각의 파일끼리도 의존성이 있는 경우도 많기 때문에 하나의 파일에서 모두 불러오는 형태로 많이 사용한다. 다음 코드를 살펴보자.
<?php
// some-page.php
include_once( BASE_DIR . '/lib.php');
function HelloWorld() {
return speak("Hello World");
}
// `speak()` 함수가 어느 php 파일에서 나온지 알 수 없다.
?>
IDE를 활용하면 쉽게 speak()
가 선언된 부분을 찾을 수 있겠지만 코드만 봐서는 이 speak()
함수가 어디에서 나온 함수인지 알 수 없다. 이런 문제로 인해 대다수의 프레임워크나 CMS에서는 함수명에 접두사를 붙여 사용하는 등의 방식으로 해결했지만 함수를 호출할 때마다 접두사를 붙여 호출하는 일은 누가봐도 지저분한 일이다.
특히 이런 방식으로 개발된 코드는 함수와 실제 로직과의 결합도가 높아서 코드를 재활용하기도 어렵다. 그 결합도를 낮추기 위한 시도로 변수에 함수명을 넣고 실행하는 방법도 활용되지만 여전히 코드가 장황하고 지저분해지는 경향이 크다.
코딩 스타일의 차이
함께 개발하는 개발자가 모두 동일한 코딩 스타일을 준수하는 것은 중요하다. 때로는 공개된 라이브러리를 활용하게 되는 경우도 있는데 이런 라이브러리가 동일한 컨벤션을 준수하고 있지 않는다면 자연스럽게 불편함을 겪게 된다.
<?php
include_once('./some_pdf_gen/lib.php');
include_once('./someCalculatorLibrary/content/library/cal.php');
include_once('./my/lib.php');
$orders = array(
new My_Product(112, 2.5, 2),
new My_Product(2303, 30, 1),
new My_Product(4923, 30, 2)
);
$pdf = new AcmeSomePdfGen();
$calculation = new some_calculation($orders);
$total = $calculation->get_total_price();
$pdf->setTemplate("<div>Total: $total</div>");
$pdf->download();
?>
인위적으로 만든 예시지만 충분히 있을 법한 코드다. 일관성이 없는 코드는 개발자에게 고스란히 스트레스로 돌아온다.
현대인의 PHP
이제 앞서 본 코드와 어떻게 다른지 살펴볼 것이다. PHP 5.3 이후로 사용할 수 있게 된 namespace
와 autoloader를 활용하는 것으로 지저분한 문제를 대부분 해결할 수 있다. 이 중요한 두 가지 기능을 사용하는데 있어 어떻게 사용하고 활용하는지 PSR 문서로 정리되어 있다. PSR을 모두 설명하고 있지 않지만 어떤 방식으로 문제를 해결하는지 확인하자.
namespace
활용하기
네임스페이스를 다음과 같이 선언하는 것으로 클래스를 네임스페이스 아래로 배정할 수 있게 된다.
<?php // lib/haruair/helloWorld.php
namespace Haruair;
class HelloWorld {
function say() {
return "HelloWorld";
}
}
?>
<?php // lib/wordpress/helloWorld.php
namespace WordPress;
class HelloWorld {
function say() {
return "HelloWorld. I'm wordpress btw.";
}
}
?>
이러면 다음 코드와 같이 한 파일에서 사용하는데 전혀 문제가 없다.
<?php // app.php
include_once('./lib/haruair/helloWorld.php');
include_once('./lib/wordpress/helloWorld.php');
$haruair = new Haruair\HelloWorld();
$wordpress = new WordPress\HelloWorld();
echo $haruair->say(); // HelloWorld
echo $wordpress->say(); // HelloWorld. I'm wordpress btw.
?>
네임스페이스를 활용하면 Haruair\Order\Product
와 Haruair\Cart\Product
가 동일한 Product라는 이름의 클래스라도 하나의 파일에서 두 클래스 모두 처리할 수 있게 된다.
autoloader 활용하기
php에서 미리 선언한 함수나 클래스를 사용하려면 당연하게 include
나 require
같은 내장 함수를 활용했어야 했다. 하지만 spl_autoload_register
함수를 선언하면 파일을 필요로 할 때 불러오는 방식으로 구현할 수 있다. 다음 코드를 보자.
<?php
include_once('./src/haruair/event/ticket.php');
include_once('./src/haruair/event/attendee.php');
include_once('./src/haruair/event/coupon.php');
$ticket = new Haruair\Event\Ticket;
$attendee = new Haruair\Event\Attendee;
$coupon = new Haruair\Event\Coupon;
?>
이제 직접 include
하는 것이 아니라 autoloader를 활용해서 불러오도록 한다.
<?php
spl_autoload_register(function ($class) {
// 프로젝트에 따른 네임스페이스 프리픽스
$prefix = '';
// 네임스페이스 프리픽스를 위한 기본 디렉토리 경로
$base_dir = __DIR__ . '/src/';
// 네임스페이스 프리픽스에 해당하는지 검사
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
// 찾지 못했으므로 반환. 만약 다른 오토로드 함수가 등록되어 있다면 순차적으로 실행함.
return;
}
// 네임스페이스를 제외한 상대 클래스명 찾기
$relative_class = substr($class, $len);
// 네임스페이스 프리픽스를 기본 디렉토리 경로로 치환, 네임스페이스 구분자를 디렉토리 구분자로
// 전환하고 .php를 끝에 추가함
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
// 파일이 존재하면 불러옴
if (file_exists($file)) {
require $file;
}
});
$ticket = new Haruair\Event\Ticket;
$attendee = new Haruair\Event\Attendee;
$coupon = new Haruair\Event\Coupon;
// autoloader를 호출한다.
?>
$ticket
에 new Haruair\Event\Ticket
이 할당될 때, “Haruair\Event\Ticket” 문자열을 인자로 받는 spl_autoload_register
함수가 실행이 된다. 그래서 함수 내 정의된 방식대로 해당 문자열을 처리해 파일을 불러오게 된다. 이 예제 함수에서는 소문자로 전환한 후, 각각의 네임스페이스를 디렉토리 구조로 변환하고 끝에 .php를 붙여 해당 파일을 불러오는 식으로 작성되어 있다.
여기서 사용된 함수는 약식 구현이고 모두가 공용으로 사용할 수 있도록 PSR에서 PSR-0, PSR-4로 표준화된 문서를 제공하고 있다. Composer를 사용한다면 더 간편하게 활용할 수 있다. PSR-4의 구현 예시도 참고하자.
코딩 스타일 일치
PSR-1 Basic Coding Standard와 PSR-2 Coding Style Guide를 통해 표준적인 문법을 문서화하고 있다. 이 두 문서를 따라 개발하면 자연스럽게 앞서 다룬 네임스페이스와 autoload를 활용할 수 있게 된다. 특히 composer로 내려받을 수 있는 패키지는 이 두 문서를 준수할 것을 권장하고 있으므로 새로운 코드나 라이브러리, 패키지를 추가하더라도 일관적인 코딩 스타일을 유지하는데 도움이 될 것이다.
얕은 수준에서 비교한 글이지만 여기서 다룬 기능은 지금 당장에라도 활용할 수 있는 방법이다. 이 기법을 활용하는 것은 단순히 몇가지 기술을 배우는 것에 그치지 않고 더 나은 개발 기법을 학습하는데 도움이 된다. 아직 PSR이나 composer와 같은 도구가 생소하다면 이 글을 읽고서 꼭 살펴봤으면 좋겠다.