메인 컨텐츠로 가기

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

테스트할 수 없는 PHP 코드를 리팩토링하는 전략

유닛 테스트와 레거시 PHP 코드 리팩터링을 통해 테스트를 더욱 쉽게 하고 코드의 품질을 개선하기

John Mertic, 소프트웨어 엔지니어, SugarCRM
John Mertic은 SugarCRM의 소프트웨어 엔지니어이며, PHP 웹 애플리케이션과 관련하여 수 년간의 경험이 있다. 그는 SugarCRM에서 데이터 통합, 모바일 및 사용자 인터페이스 아키텍처를 전문으로 하여 재직 중이다. 왕성한 저술가로서 php|architect, IBM developerworks 및 Apple Developer Connector에 기고했으며, "The Definitive Guide to SugarCRM: Better Business Applications"이라는 책의 저자이기도 하다. 많은 오픈 소스 프로젝트에 참여했으며, 특히 PHP 프로젝트에 주력하고 있다. 또한 PHP Windows Installer를 개발 및 유지보수한다

요약:  PHP가 간단한 스크립팅 언어에서 완전한 프로그래밍 언어로 성장하면서 일반적인 PHP 애플리케이션의 코드 기반이 더욱 복잡해졌습니다. 다양한 테스트 도구를 사용하면 이러한 애플리케이션을 지원하고 유지보수하는 작업을 제어하는 프로세스를 쉽게 자동화할 수 있습니다. 한 가지 방법인 유닛 테스트를 이용하면 직접 작성한 코드의 정확성을 테스트할 수 있습니다. 그러나 때로는 레거시 코드 기반이 이러한 유형의 테스트에 적합하지 않을 수도 있습니다. 이 기사에서는 문제가 있는 일반적인 PHP 코드를 리팩토링한 후, 인기 있는 유닛 테스트 도구를 사용하여 쉽게 코드를 테스트할 수 있게 하고 종속성을 줄여서 코드 기반을 개선하는 전략을 살펴봅니다.

기사 게재일:  2011 년 8 월 02 일
난이도: 중급 원문:  보기 PDF:  A4 and Letter (42KB | 11 pages)Get Adobe® Reader®
페이지뷰:  2595 회
의견:  


소개

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 codecode_examples.zip3KBHTTP

다운로드 방식에 대한 정보


참고자료

교육

제품 및 기술 얻기

  • DVD로 제공되거나 다운로드할 수 있는 IBM 시험판 소프트웨어를 사용하여 차기 오픈 소스 개발 프로젝트를 구현해 보자.

토론

필자소개

John Mertic은 SugarCRM의 소프트웨어 엔지니어이며, PHP 웹 애플리케이션과 관련하여 수 년간의 경험이 있다. 그는 SugarCRM에서 데이터 통합, 모바일 및 사용자 인터페이스 아키텍처를 전문으로 하여 재직 중이다. 왕성한 저술가로서 php|architect, IBM developerworks 및 Apple Developer Connector에 기고했으며, "The Definitive Guide to SugarCRM: Better Business Applications"이라는 책의 저자이기도 하다. 많은 오픈 소스 프로젝트에 참여했으며, 특히 PHP 프로젝트에 주력하고 있다. 또한 PHP Windows Installer를 개발 및 유지보수한다

잘못된 도움말 신고

부정사용 신고

감사합니다. 이 항목은 운영자가 관심을 표시했습니다.


잘못된 도움말 신고

부정사용 신고

제출실패 신고. 나중에 다시 실행해주세요.


디벨로퍼웍스 로그인


IBM ID가 필요하세요?
IBM ID를 잊으셨습니까?


비밀번호를 잊으셨습니까?
비밀번호 변경

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

화면상에 보여지는 닉네임을 정하세요.

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

3개의 &이나 대쉬를 포함해주시고 31글자내로 제한해주세요.


developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


아티클 순위

의견

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=20
Zone=오픈 소스, XML
ArticleID=750156
ArticleTitle=테스트할 수 없는 PHP 코드를 리팩토링하는 전략
publish-date=08022011
author1-email=jmertic@gmail.com
author1-email-cc=

태그

Help
검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오.

태그를 더 많이 보거나 적게 보기 위해 슬라이더 막대를 사용하십시오.

인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다.

내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.

검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오. 인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다. 내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.