15년간 PHP가 걸어온 길을 되돌아 보면 PHP가 그 당시에 널리 사용되었던 CGI 스크립트에 대한 대안으로 개발된 단순한 동적 스크립팅 언어에서 오늘날에는 완전한 프로그래밍 언어로 성장했다는 것을 알 수 있다. 코드 기반이 성장해감에 따라 수동으로 코드를 테스트한다는 것은 불가능하게 되었고, 작건 크건, 코드를 변경하게 될 때마다 전체 애플리케이션이 영향을 받게 되었다. 이에 따른 효과는 페이지가 로드되지 않거나 양식이 저장되지 않는 것과 같이 단순할 수 있다. 또는 발견하기 어려운 무엇이거나 특정 상황에서만 나타나는 것일 수도 있다. 심지어는 이로 인해 이전에 발생했던 문제점이 애플리케이션에서 다시 나타날 수도 있다. 이러한 문제점을 해결하기 위해 다양한 테스트 도구가 개발되었다.
일반적으로 사용되는 한 가지 방법은 기능 테스트 또는 적합성 테스트로 이 방법에서는 애플리케이션과 사용자의 일반적인 상호 작용을 통해 애플리케이션을 테스트한다. 이 방법은 애플리케이션에서 다양한 프로세스를 테스트할 수 있는 우수한 기술이지만, 매우 느린 프로세스이며 하위 레벨 클래스와 함수를 테스트하여 프로세스가 계획대로 작동하는지 확인하는 작업만큼 유용하지는 않다. 이러한 경우에는 또 다른 방법인 유닛 테스트를 사용하는 것이 적합하다. 유닛 테스트의 목표는 애플리케이션에서 기본 코드의 기능을 테스트하여 애플리케이션을 실행했을 때 올바른 결과가 나타나는지 확인하는 데 있다. 웹 애플리케이션이 이렇게 발전하면 시간이 지나면서 테스트하기가 어려워지는 레거시 코드가 많이 생기고 이로 인해 애플리케이션의 테스트 범위를 충분히 확보하기 위한 개발 팀의 기능이 축소되게 된다. 이러한 코드를 일반적으로 "테스트 불가능한 코드"라고 한다. 애플리케이션에서 이러한 코드를 식별하고 수정하는 방법을 살펴보도록 하자.
코드가 작성되었을 때는 테스트 불가능한 코드 기반의 문제점 영역이 명백하지 않은 경우가 많다. PHP 애플리케이션 코드를 작성할 때는 웹 요청이 흘러가는 방향에 따라 코드를 조정하는 경향이 있고 애플리케이션을 설계하는 데 있어 더 절차적인 접근 방식을 취하는 경우가 많다. 프로젝트를 긴급하게 완료해야 하고 애플리케이션을 신속하게 수정해야 하는 상황으로 인해 개발자들은 코드를 신속하게 완료하기 위해 "안이한 방법"을 취하게 된다. 개발자들은 장래에 지원 문제점을 악화시킬 수 있는 경우에도 가능한 가장 위험이 적은 수정을 시도하려고 하는 경우가 많기 때문에 이전에 서툴게 작성한 코드나 혼동을 일으킬 수 있는 코드로 인해 애플리케이션의 테스트 불가능 문제가 더 악화된다. 특별한 조치를 취하지 않으면 이러한 문제점 영역은 모두 유닛 테스트로 테스트할 수가 없다.
PHP 애플리케이션에서는 글로벌 변수가 편리하게 사용된다. 글로벌 변수를 사용하면 애플리케이션에서 변수나 오브젝트를 일찍 초기화하여 애플리케이션의 다른 위치에서 활용할 수 있다. 그러나 이렇게 유연해지는 반면에 글로벌 변수를 많이 사용하면 테스트 불가능한 코드가 생기는 문제점이 일반적으로 발생한다. 목록 1에서 이러한 문제점을 확인할 수 있다.
목록 1. 글로벌 상태를 따르는 함수
<?php
function formatNumber($number)
{
global $decimal_precision, $decimal_separator, $thousands_separator;
if ( !isset($decimal_precision) ) $decimal_precision = 2;
if ( !isset($decimal_separator) ) $decimal_separator = '.';
if ( !isset($thousands_separator) ) $thousands_separator = ',';
return number_format($number, $decimal_precision, $decimal_separator,
$thousands_separator);
}
|
이러한 글로벌 변수로 인해 두 가지 서로 다른 문제점이 발생한다. 첫 번째 문제점은 테스트 과정에서 각 글로벌 변수를 기술해야 하고 글로벌 변수가 해당 함수에서 요구하는 유효한 값으로 설정되었는지 확인해야 한다. 첫 번째보다 더 심각한 두 번째 문제점은 후속 테스트에서 글로벌 상태를 변경하여 결과를 무효화시키지 않도록 테스트하기 전에 글로벌 상태를 이전 상태로 다시 설정해야 한다는 점이다. PHPUnit에는 글로벌 변수를 백업했다가 테스트를 실행한 후에 다시 복원할 수 있는 기능이 있으며, 이 기능을 이용하면 위에서 언급한 문제점을 완화할 수 있다. 그러나 더 나은 접근 방법은 메소드에서 사용할 수 있는 이러한 글로벌 변수의 값을 직접 전달하는 테스트 클래스 방식을 사용하는 것이다. 목록 2에는 이러한 작업을 수행하는 방법이 예제로 표시되어 있다.
목록 2. 글로벌 변수를 대체하도록 이 함수를 수정함
<?php
function formatNumber($number, $decimal_precision = null, $decimal_separator = null,
$thousands_separator = null)
{
if ( is_null($decimal_precision) ) global $decimal_precision;
if ( is_null($decimal_separator) ) global $decimal_separator;
if ( is_null($thousands_separator) ) global $thousands_separator;
if ( !isset($decimal_precision) ) $decimal_precision = 2;
if ( !isset($decimal_separator) ) $decimal_separator = '.';
if ( !isset($thousands_separator) ) $thousands_separator = ',';
return number_format($number, $decimal_precision, $decimal_separator,
$thousands_separator);
}
|
이렇게 하면 코드의 테스트 가능성이 더 증가할 뿐만 아니라 코드가 메소드의 글로벌 변수에 종속되지 않게 된다. 또한, 나중에 이 코드를 리팩토링하여 글로벌 변수를 전혀 사용하지 않도록 할 수도 있다.
싱글톤은 애플리케이션에서 동시에 하나의 인스턴스만 존재하도록 설계된 클래스이다. 싱글톤은 애플리케이션의 글로벌 오브젝트(예: 데이터베이스 연결 및 구성 설정)에 사용되는 공통 패턴이다. 사용 가능한 오브젝트가 항상 있는 것이 유용하기 때문에 애플리케이션에서는 이러한 싱글톤을 사용하지 않는 것이 좋다고 생각했고 많은 개발자들은 이러한 싱글톤을 가치가 없다고 생각했다. 이러한 생각은 대부분 싱글톤을 남용하면서 생겨난 것이며 이러한 상황에서는 이러한 전지전능한 오브젝트를 확장할 수가 없다. 그러나 테스트 관점에서 보았을 때 중요한 문제점은 싱글톤을 변경할 수 없는 경우가 있다는 점이다. 목록 3에 있는 예제를 살펴보도록 하자.
목록 3. 테스트할 싱글톤
<?php
class Singleton
{
private static $instance;
protected function __construct() { }
private final function __clone() {}
public static function getInstance()
{
if ( !isset(self::$instance) ) {
self::$instance = new Singleton;
}
return self::$instance;
}
}
|
싱글톤이 처음 인스턴스화된 후에는 getInstance() 메소드를 호출할 때마다 새로운 오브젝트가 리턴되는 것이 아니라 동일한 오브젝트가 다시 리턴된다는 것을
알 수 있다. 오브젝트를 변경해야 하는 경우에는 이점이 큰 문제가 된다. 가장 손쉬운 해결책은 다시 설정할 오브젝트에 메소드를 추가하는 것이다. 목록 4에는
이에 해당하는 예제가 표시되어 있다.
목록 4. reset 메소드가 추가된 싱글톤 오브젝트
<?php
class Singleton
{
private static $instance;
protected function __construct() { }
private final function __clone() {}
public static function getInstance()
{
if ( !isset(self::$instance) ) {
self::$instance = new Singleton;
}
return self::$instance;
}
public static function reset()
{
self::$instance = null;
}
}
|
그러면 테스트를 실행할 때마다 reset 메소드를 호출하여 테스트를 실행할 때마다 싱글톤 오브젝트를 초기화하는 코드가 수행되도록 할 수 있다. 이 방법을 사용하면 싱글톤을 쉽게 변경할 수 있으므로 애플리케이션에서 일반적으로 유용하다.
계획하고 있는 테스트가 유닛 테스트뿐인 경우에는 필요한 것보다 더 많은 오브젝트와 변수를 설정하지 않는 것이 좋다. 또한, 설정한 모든 오브젝트와 변수는 사후에 제거해야 한다. 이 때문에 파일 및 데이터베이스 테이블과 같은 성가신 항목에 문제가 발생할 수 있으므로 테스트를 완료하고 나서 상태를 수정해야 하는 경우에는 추적 파일을 정리할 때 특히 주의해야 한다. 이러한 규칙이 손상되지 않도록 하는 데 있어 가장 큰 장애물은 오브젝트 자체의 생성자이다. 이 생성자는 테스트와 관계가 없는 모든 유형의 작업을 수행한다. 아래 목록 5에 있는 예제를 생각해 보자.
목록 5. 대형 싱글톤 메소드가 있는 클래스
<?php
class MyClass
{
protected $results;
public function __construct()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$this->results = $dbconn->query('select name from mytable');
}
public function getFirstResult()
{
return $this->results[0];
}
}
|
여기에서는 오브젝트에서 fdfdfd 메소드를 테스트하기 위해 데이터베이스 연결을 설정하고 테이블에 레코드를 저장한 다음, 테스트가 끝난 후에 이러한 자원을
모두 정리한다. 이 중에서 아무 것도 fdfdfd 메소드를 테스트하는 데 필요하지 않은 경우에는 이렇게 하는 것이 과도한 것처럼 보인다. 그러므로 목록 6에
표시된 것처럼 생성자를 수정한다.
목록 6. 불필요한 모든 초기화 논리를 필요에 따라 건너 뛰도록 수정된 클래스
<?php
class MyClass
{
protected $results;
public function __construct($init = true)
{
if ( $init ) $this->init();
}
public function init()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$this->results = $dbconn->query('select name from mytable');
}
public function getFirstResult()
{
return $this->results[0];
}
}
|
여기에서는 생성자에서 많은 코드를 리팩터링하여 init() 메소드에 삽입했다. 이 메소드는 여전히 기존 코드와 충돌하지 않도록 기본적으로
생성자에서 호출된다. 그러나 이제는 테스트 과정에서 부울 false를 생성자에게 전달하여 init() 메소드와
필요하지 않은 모든 초기화 논리가 호출되지 않도록 할 수 있다. 이와 같이 클래스 리팩터링을 수행하면 코드가 개선되어 초기화 코드를 오브젝트 생성자 코드와
분리할 수 있다.
이전 섹션에서 살펴본 바와 같이 클래스를 설계하는 데 있어서 테스트를 어렵게 하는 커다란 문제점은 테스트하지 않아도 되는 모든 유형의 오브젝트를 초기화해야 한다는 데 있다. 앞에서 초기화 논리가 복잡하면 테스트를 작성하는 과정에서 매우 다양한 오버헤드가 발생할 수 있고, 특히 이러한 논리 중에 테스트를 수행할 필요가 없는 것이 있는 경우에는 더욱 그렇다는 점을 확인했다. 그러나 테스트할 클래스의 메소드 내부에서 직접 오브젝트를 새로 작성하면 또 다른 문제점이 발생할 수 있다. 이러한 문제점이 발생하는 예제 코드는 목록 7에서 확인할 수 있다.
목록 7. 또 다른 오브젝트를 직접 초기화하는 메소드가 있는 클래스
<?php
class MyUserClass
{
public function getUserList()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$results = $dbconn->query('select name from user');
sort($results);
return $results;
}
}
|
위에 있는 getUserList 메소드를 테스트하지만, 테스트 목적은 리턴된 사용자 목록이 알파벳순으로 제대로 정렬되는지 확인하는 데 있다고 가정한다. 이 경우에는
리턴된 레코드를 정렬하는 기능을 테스트하려는 것이기 때문에 데이터베이스에서 레코드를 가져올 수 있다는 점은 사실상 중요하지 않다. 문제는 메소드 내에서
데이터베이스 연결 오브젝트를 직접 인스턴스화하기 때문에 메소드를 제대로 테스트하려면 이러한 기초 작업을 모두 수행해야 한다는 데 있다. 그러므로
오브젝트를 끼워 넣을 수 있도록 목록 8과 같이 수정한다.
목록 8. 또 다른 오브젝트를 직접 초기화하는 메소드가 있지만, 이 오브젝트를 오버라이드하는 방법을 제공하는 클래스
<?php
class MyUserClass
{
public function getUserList($dbconn = null)
{
if ( !isset($dbconn) || !( $dbconn instanceOf DatabaseConnection ) ) {
$dbconn = new DatabaseConnection('localhost','user','password');
}
$results = $dbconn->query('select name from user');
sort($results);
return $results;
}
}
|
이제는 예상되는 데이터베이스 연결 오브젝트와 호환 가능한 오브젝트를 전달할 수 있으며 오브젝트를 새로 작성하는 대신 이 오브젝트를 사용하게 된다. 전달하는 오브젝트는 모의(Mock) 오브젝트이다. 이 오브젝트는 사용자가 사용할 데이터를 다시 직접 제공할 호출된 메소드의 여러 가지 리턴 값이 하드 코딩된 오브젝트이다. 이 경우에는 데이터베이스에 결과를 요청하지 않고 데이터베이스 연결 오브젝트의 쿼리 방법을 모방하여 결과를 다시 리턴할 수 있다. 이러한 형태의 리팩터링을 수행하면 메소드를 개선할 수 있으며 애플리케이션을 지정된 기본 데이터베이스에만 연결하지 않고 다양한 데이터베이스 연결에 끼워 넣을 수 있다.
특히, 테스트 가능한 코드를 작성하면 이 기사에서 살펴본 예제에서 알 수 있듯이 PHP 애플리케이션을 위한 유닛 테스트를 더 손쉽게 작성할 수 있을 뿐만 아니라 이 과정에서 더욱 잘 설계되고 안정된 모듈식 애플리케이션을 작성할 수 있다. 이 기사에서는 비즈니스를 서로 단단하게 뒤엉키게 하고 PHP 애플리케이션의 논리를 혼란스럽게 하는 다양한 레벨의 "복잡한" 코드를 모두 살펴보았다. 이러한 코드를 사용하게 되면 누군가가 이 코드를 깊이 탐구하여 지원한다는 것은 거의 불가능해진다. 코드를 테스트 가능하게 만드는 프로세스를 통해 이전에 문제점이 있었던 코드를 리팩터링했다. 이 코드는 설계상에 문제가 있었을 뿐만 아니라 기능에도 문제가 있었다. 이 기사에서는 하드 코딩된 종속 항목을 제거하여 애플리케이션의 다른 영역에서 함수와 클래스를 다양한 목적으로 더욱 재사용 가능하게 함으로써 코드의 재사용성을 개선할 수 있는 옵션을 살펴보았다. 또한, 품질이 좋지 않은 코드를 제거하고 훨씬 더 나은 코드로 대체함으로써 나중에 코드 기반을 더 손쉽게 지원할 수 있도록 했다.
이 기사에서는 PHP 애플리케이션에서 테스트 불가능한 코드를 고전적으로 찾는 여러 가지 예제를 통해 PHP 코드의 테스트 가능성을 더 개선하는 과정을 살펴보았다. 애플리케이션에서 코드를 테스트할 수 없는 상황이 어떻게 발생하는지 확인했으며 그 다음에는 문제점이 있는 코드를 수정하여 테스트가 가능하게 하는 방법을 살펴보았다. 또한, 코드를 이렇게 변경하여 코드의 테스트 가능성과 코드의 품질을 더 개선하고 리팩터링된 코드 섹션에서 코드의 재사용성을 촉진하는 방법을 설명했다.
| 설명 | 이름 | 크기 | 다운로드 방식 |
|---|---|---|---|
| Article source code | code_examples.zip | 3KB | HTTP |
교육
- developerWorks 튜토리얼 시리즈 "Learning PHP"는
가장 기본적인 PHP 스크립트에서부터 데이터베이스로 작업하고 파일 시스템으로부터 스트리밍에 이르기까지 알려준다.
-
PHP.net은 PHP 개발자들이 주로 이용하는 웹 사이트이다.
- "Recommended PHP reading list"를 확인하자.
- developerWorks에 있는 PHP 컨텐츠를 모두 찾아보자.
- IBM developerWorks의 PHP
project resources를 활용하여 PHP 기술을 향상시키자.
-
A PHP V5 migration guide(Jack Herrington, developerWorks, 2006년 9월): PHP V4로 개발된 코드를 V5로 마이그레이션하는 방법을 배우자.
-
Planet PHP는 PHP 개발자 커뮤니티 뉴스 소스이다.
-
Safari 온라인 서점: 이 온라인 서점을 방문하여 특정 기술과 관련된 참고자료를 찾아보자.
- Twitter의 developerWorks 페이지를 팔로우하자.
- developerWorks 팟캐스트에서 소프트웨어
개발자의 흥미로운 인터뷰와 토론을 확인할 수 있다.
-
developerWorks
기술 행사 및 웹 캐스트: developerWorks 기술 행사 및 웹 캐스트를 통해 최신 정보를 얻을 수 있다.
제품 및 기술 얻기
- DVD로 제공되거나 다운로드할 수 있는 IBM 시험판 소프트웨어를 사용하여 차기 오픈 소스 개발 프로젝트를 구현해 보자.
토론
- developerWorks 블로그를 통해 developerWorks 커뮤니티에 참여할 수 있다.
John Mertic은 SugarCRM의 소프트웨어 엔지니어이며, PHP 웹 애플리케이션과 관련하여 수 년간의 경험이 있다. 그는 SugarCRM에서 데이터 통합, 모바일 및 사용자 인터페이스 아키텍처를 전문으로 하여 재직 중이다. 왕성한 저술가로서 php|architect, IBM developerworks 및 Apple Developer Connector에 기고했으며, "The Definitive Guide to SugarCRM: Better Business Applications"이라는 책의 저자이기도 하다. 많은 오픈 소스 프로젝트에 참여했으며, 특히 PHP 프로젝트에 주력하고 있다. 또한 PHP Windows Installer를 개발 및 유지보수한다