메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

PHP 애플리케이션 분석을 통한 성능 향상

한빛미디어

|

2002-04-26

|

by HANBIT

9,249

저자: Joao Prado Maia, 역 이호재

PHP 스크립트 언어의 가장 큰 특징은 배우기 쉽다는 것입니다. 모든 것이 잘 조화되어 있기 때문에 사용 해야 할 함수에 대해 충분히 알지 못한다고 하더라도 원하는 작업을 어떻게 해야 할 것인지 대충 추측할 수 있습니다. 어떤 일을 하고자 할 때, PHP에 이미 이를 위한 함수가 있다는 사실은 그렇게 놀라운 일도 아닐 것입니다.

지금까지 언급한 것처럼 PHP는 매우 훌륭한 언어입니다. 하지만 성능이나 메모리 소비와 관련된 PHP 트릭을 아는 데에는 당연히 시간이 걸립니다. PHP에는 많은 메모리를 절약할 수 있는 방법도 있으며 속도 향상을 가져올 수 있는 미묘한 점들도 많이 있습니다. 이 기사는 여러분에게 PHP 애플리케이션을 분석하는 안내자의 역할을 할 것입니다. 그리고 기존의 스크립트를 최적화하고 그 차이점에 대해서도 살펴 볼 것입니다.

Programming PHP
사전을 찾아보면 분석(profile)이란 데이터의 두드러진 특징이나 성질을 나타내기 위해 그래프나 테이블의 형태 등으로 데이터를 분석하고 요약하는 것을 뜻합니다. 코드 내에 타이머를 두고 반복해서 수행해 봄으로써, 실행되는 프로그램의 특정 부분이 얼마나 빠르고 느린지에 관한 분석결과를 얻을 수 있습니다. 이 데이터를 가지고서 우리는 병목지점을 찾아 낼 수 있으며 프로그램의 성능 향상을 위해서 최적화해야 할 부분을 찾아낼 수도 있습니다.

필자는 여러분이 이미 PEAR와 관련하여 필자가 이미 쓴 다른 기사들을 읽어봤다는 전제 하에 이 기사를 전개해 나가겠습니다. PHP 스크립트 분석은 PEAR 라이브러리를 통해 손쉽게 할 수 있습니다. 우리는 이 라이브러리를 통해 분석 시나리오를 위한 예제를 만들어 볼 것입니다. 이렇게 하는 이유는 가장 사용하기 쉽고, 코드를 분석할 때에도 그대로 적용할 수 있기 때문입니다. 물론 PHP 코드를 분석하는 데는 특정 상용 프로그램도 필요 하지 않으며, 확장 모듈을 컴파일 할 필요도 없음을 알려두는 바입니다.

이 라이브러리의 이름은 PEAR::Benchmark로 코드를 분석하거나 간단한 벤치마킹을 수행하는 데 아주 유용하게 사용됩니다. 이 라이브러리에 있는 클래스중에 Benchmark_Timer()는 이 함수를 부르고 나중에 다시 한번 부를 때, 그 두 함수 사이의 호출 시간 상의 차이를 알아낼 수 있습니다. benchmark를 수행한 후, 스크립트 실행에 관한 자세한 결과를 얻을 수 있습니다. 이와 같이 분석하는 것은 다음과 같이 아주 간단합니다.
start();
$bench->setMarker("Start of the script");

// now sleep for a few seconds
sleep(5);

$bench->stop();

// get the profiling information from this timer
print_r($bench->getProfiling());
?>
이 스크립트는 다음과 같은 결과물을 출력할 것입니다.
Array
(
    [0] => Array
        (
            [name] => Start
            [time] => 1013214253.05751200
            [diff] => -
            [total] => 0
        )

    [1] => Array
        (
            [name] => Start of the script
            [time] => 1013214253.05761100
            [diff] => 9.8943710327148E-05
            [total] => 9.8943710327148E-05
        )

    [2] => Array
        (
            [name] => Stop
            [time] => 1013214258.04920700
            [diff] => 4.9915959835052
            [total] => 4.9916949272156
        )
)
위의 결과물은 의미없는 숫자 뭉치처럼 보이지만 실생활에서는 아주 유용하게 사용됩니다.

숫자의 의미

대부분의 사람들이 추측하는 것처럼 각 배열들은 Benchmark_Timer() 클래스의 함수인 $bench->start(), $bench->setMarker(), 그리고 $bench->stop()이 호출될 때 생성됩니다. 각 배열에 들어 있는 숫자들의 의미는 직관적입니다. 자세히 살펴보도록 하겠습니다.
    [0] => Array
        (
            [name] => Start
            [time] => 1013214253.05751200
            [diff] => -
            [total] => 0
        )
여기에서 time 항목은 Benchmark_Timer() 클래스의 start() 함수가 호출될 때의 UNIX timestamp 값을 가리킵니다. diff 항목은 이전 함수 호출 시간과 이번 시간의 차이를 나타내는데, 지금 처음 실행한 것이므로 대시(-)가 들어가 있습니다. total 항목은 벤치마크가 시작된 시간으로부터 지금까지 경과한 시간을 나타냅니다. 또 다른 배열을 살펴보도록 합시다.
    [1] => Array
        (
            [name] => Start of the script
            [time] => 1013214253.05761100
            [diff] => 9.8943710327148E-05
            [total] => 9.8943710327148E-05
        )
우리는 이 데이터로부터 $bench->setMarker(...) 함수 호출이 $bench->start()를 호출 한 뒤 9.8943710327148E-05초 이후에 이루어 졌음을 알 수 있습니다. 무슨 뜻인지 이해하기 위해 계산기를 사용할 필요는 없습니다. 0.0000989 초를 의미하는 것이니까요. :)

Benchmark에 관한 일화

위에서 알아본 예제는 이해하기는 쉬웠지만 분석하고자 하는 웹사이트의 코드에 적용하기에는 무리가 따를 수 있습니다. 필자는 상당히 큰 웹 커뮤니티를 운영하고 있는 회사에서 일한 경험이 있는데, 이 사이트는 성능상의 문제가 있었습니다.

이 때 필자는 대부분의 코드를 이해하지 못했는데, 이는 이 코드가 오랜 기간에 걸쳐 특별한 필요(사이트 전환을 위한 파일들을 포함하는 블럭이나, 사이트의 사용 현황을 기록하는 블록 등)에 따라 만들어졌기 때문이었습니다. 필자를 비롯한 핵심 프로그래머는 무엇인가 최적화되어야 한다고는 생각했지만 정확히 무엇이 문제인지는 알아내지 못했습니다.

요약하자면 문제 해결을 위해 모든 메인 스크립트와 모든 포함 된 파일에 $bench->setMarker() 라인을 추가해 주었습니다. 그리고 나서 $bench->getProfiling()의 결과를 분석했는데, 그 결과에 놀라지 않을 수 없었습니다. 특정 언어의 코드(예를 들어 "english"의 "en")을 얻기 위해 모든 페이지에서 수 백번 이상 사용되는 함수 호출에 문제가 있는 것으로 나타났기 때문이었습니다. 함수가 호출 될 때마다 MySQL 데이터베이스의 테이블에서 실제 이름을 가져오기 위해 데이터베이스에 접속하는 것이 문제였습니다.

이런 형태의 정보를 위해 캐시 시스템을 구축함으로써 문제가 해결되었습니다. 이틀도 채 안돼서 우리는 상당한 성능 향상을 느낄 수 있었으며 첫 일주일동안 페이지뷰가 40%나 늘었습니다. 결국 이 예제를 통해 우리는 코드를 분석하는 것이 웹 애플리케이션이나 웹사이트에 얼마나 도움이 될 수 있는지에 대해 알아 보았습니다.

함수 호출을 벤치마킹하기

Benchmark_Timer() 클래스는 스크립트나 페이지 그리고 include 파일의 코드를 분석하는데 아주 유용하게 사용됩니다. 하지만 분석 결과의 평균을 구하기 위해서는 스크립트를 리로드(reload) 해야 하기 때문에 그렇게 과학적인 방법은 아닙니다. 그러므로 클래스나 함수 호출에 있어서 특화되지 못했습니다.

PEAR::Benchmark 라이브러리에 있는 클래스 중 Benchmark_Iterator는 클래스나 함수 호출시 사용할 수 있는 클래스입니다. 이는 어떤 클래스나 함수 내에서 여러 번 실행한 후 분석 결과를 표시할 수 있는 클래스입니다. 어떤 스크립트를 분석할 때 한번 실행한 후 얻은 결과가 항상 모든 경우에 일치하는 것은 아닙니다. 그래서 Benchmark_Iterartor가 필요한 것이며 이를 통해 보다 정확한 분석 결과를 얻을 수 있습니다.

다음 예제를 살펴보도록 하겠습니다.
 "mysql",
    "hostspec" => "localhost",
    "database" => "database_name",
    "username" => "user_name",
    "password" => "password"
);
$dbh = DB::connect($dsn);

function getCreatedDate($id)
{
    global $dbh;

    $stmt = "SELECT created_date FROM users WHERE id=$id";
    // as always, let"s use PEAR::DB here
    $created_date = $dbh->getOne($stmt);
    if ((PEAR::isError($created_date)) || 
	    (empty($created_date))) {
        return false;
    } else {
        return $created_date;
    }
}

include_once "Benchmark/Iterate.php";
$bench = new Benchmark_Iterate;

// getDate 함수를 10번 실행합니다.
$bench->run(10, "getCreatedDate", 1);

// 분석 결과를 표시합니다.
print_r($bench->get());
?>
위의 코드는 아래와 유사한 결과물을 출력할 것입니다.
Array
(
    [1] => 0.055413007736206
    [2] => 0.0012860298156738
    [3] => 0.0010279417037964
    [4] => 0.00093603134155273
    [5] => 0.00094103813171387
    [6] => 0.00092899799346924
    [7] => 0.0010659694671631
    [8] => 0.00096404552459717
    [9] => 0.0010690689086914
    [10] => 0.00093603134155273
    [mean] => 0.0064568161964417
    [iterations] => 10
)
위의 각 값이 무엇을 의미하는지는 쉽게 이해할 수 있습니다. 즉 mean 항목은 getCreateDate() 함수를 10번 동안 실행했을 때 걸린 평균 시간을 의미합니다. 대게 벤치마킹을 하는데 있어서는 대략 1000번 이상 반복해서 실행해야 좋은 결과를 얻을 수 있겠지만 이번 예제에서는 10번으로도 충분히 여러분의 이해를 도울 수 있었으리라 생각합니다. :)

필자는 이 기사를 통해 여러분들이 PHP 스크립트를 빠르게 분석하는 방법에 관한 실질적인 개념을 제공했기를 희망합니다. 하지만 실제로 코드를 분석하는 것이 그리 간단한 일은 아닙니다. 이를 위해서는 언어의 특정 부분에 대해서도 많은 것을 알아야 하기 때문입니다. 그렇지만 코드를 테스트할 때 간단하게 timer를 추가해 줌으로써 문제가 있는 함수를 찾아낼 수 있으며, iteration을 사용하여 올바른 최적화를 이룰 수 있다는 사실을 반드시 숙지해 놓기 바랍니다.

Joao Prado Maia는 휴스턴에 살고 있는 웹 개발자입니다. 웹 기반의 애플리케이션을 4년 이상 개발한 경험이 있고 새로운 기술과 프로그래밍 언어를 배우는 것을 좋아합니다.
TAG :
댓글 입력
자료실