최적화 기술
이제 병목 구간의 원인에 따라 어떠한 방법으로 스크립트의 실행 시간을 최적화할 수 있는지 알아보자.
- 코드 최적화 : 이 방법은 잘못된 for() 또는 while() 루프와 같이 많은 연산이 이루어지는 부분을 최적화하는 것이다.
- 출력 버퍼링 및 압축 : 만일 브라우저로 출력해야 하는 내용이 많아서 속도 저하가 생긴다면 이 방법을 사용한다.
- 데이터베이스 최적화 : 속도 저하의 원인이 데이터베이스 쿼리 또는 테이터베이스 연결 및 함수 사용에 있다면 이 방법을 사용한다.
- 캐싱 : 이 방법은 문제의 원인이 페이지를 생성하는 시간에 있으며 데이터베이스 쿼리를 최적화할 수 없을 때 사용된다. 캐시를 사용하는 또 다른 이유는 정적인 데이터의 빈번한 요청 때문이다.
코드 최적화
보통 코드 자체가 성능 문제의 원인이 되지는 않는다. 대부분의 최적화 문제는 데이터베이스 또는 데이터 입출력에서 발생한다.
문제의 원인이 함수나 특정 코드 때문이라는 것이 확실해 지기 전에는 PHP 코드를 최적화하면 안된다. 주어진 PHP 스크립트에서 실행 시간의 10%는 코드 실행에 사용되면 90%는 입출력과 데이터베이스 작업에 사용된다.
그러나 어떤 사이트나 애플리케이션을 느리게 만드는 대규모 연산을 하는 특별한 경우가 있다. 이럴 경우 코드를 최적화하는 방법은 다음과 같다.
- 루프 점검
- 가능하면 빠른 함수 사용
- 데이터를 출력하는 최선의 방법 선택
- 데이터를 입력하는 최선의 방법 선택
- echo() 문을 적게 사용
- 젠드 옵티마이저 사용
루프 검사
코드 최적화를 시작하는 좋은 방법은 반복적으로 실행하는 루프의 내부를 검사하는 것이다. 어떠한 연산이나 문자열 처리도 루프 외부에서 일어나지 않는지 확인한다.
만일 파일 입출력이 루프 내부에서 처리된다면 루프 외부에서는 입출력이 일어나지 않도록 한다. 파일 전체를 메모리에서 읽으면 루프 내부에서 블럭 열기, 읽기, 닫기 작업을 하는 것보다 빠르다. 파일의 일부분을 읽기 위해 루프를 사용한다면 전체 파일을 순차적으로 읽고 필요 없는 부분을 버리는 것이 효과적이다.
Import! |
file() 함수는 텍스트 파일을 처리할 경우에 적합하다. |
보다 빠른 함수의 사용
PHP에서 어떤 함수들은 보다 많은 연산을 필요로 한다. 이러한 함수들은 보다 간단한 함수로 대체될 수 있다.
- ereg() 대신 strstr() 사용 : 만일 정규표현식을 사용하지 않는다면 ereg() 대신 더 빠른 strstr()를 사용한다.
- ereg_replace() 대신 str_replace() 사용 : 위와 마찬가지로 문자열 치환에 정규표현식을 사용하지 않는다면 ereg_replace() 보다 속도가 바른 str_replace()를 사용한다.
만일 정규표현식을 사용해야 한다면 PHP의 ereg_*() 함수보다 속도가 빠른 PCRE(Perl compatible regular expression!s) 함수를 사용한다. 자세한 것은 7장을 참고하자.
데이터를 출력하는 최상의 방법을 선택
데이터를 브라우저로 출력하는 방법은 세 가지가 있다.
직접 출력
<?php
echo("Echoing output\n");
print("Printing output\n");
?>
만일 PHP 변수나 동적인 내용이 필요 없다면 직접 출력을 사용하는 것이 바람직하다. 이 방법은 PHP가 코드를 해석하지 않기 때문에 가장 빠르다.
데이터를 입력하는 최상의 방법 선택
데이터를 입력하는 방법은 세 가지가 있다.
- readfile()
- include()
- require()
만일 파일에 PHP 코드가 포함되어 있지 않다면 readfile()을 사용한다. 이 방법은 파일을 인클루드하기 전에 데이터를 파싱하지 않으므로 좀더 빠르다. include()와 require()은 PHP 코드가 발견되면 실행한다. PHP 스크립트에서 HTML 헤더는 readfile()을 사용하는 것이 바람직하다.
echo 문 적게 사용하기
echo() 문은 실행에 시간이 걸리므로 적게 사용하고 긴 echo() 문이 가독성이 좋다. 예를 들면:
echo("hello\n
this is a test\n
of the echo statement");
위의 코드가 아래 코드보다 빠르다:
echo("hello\n");
echo("this is a test\n");
echo("of the echo statement");
젠드 옵티마이저
코드를 최적화하는 마지막 방법은 http://www.zend.org/에서 제공하는 무료 젠드 옵티마이저(Zend Optimizer)를 사용하는 것이다. 젠드 옵티마이저는 http://www.zend.com/store/products/zend-optimizer.php에서 구할 수 있다.
이것은 PHP를 위한 플러그인으로 여러 가지 기술을 통해 다양한 방법으로 코드를 최적화한다.
일반적으로 입출력을 많이 하는 스크립트는 젠드 옵티마이저를 사용해도 별 효과가 없고 대규모 연산을 하는 스크립트는 성능이 크게 향상된다.
출력 버퍼링과 압축
대부분의 페이지 생성 엔진은 브라우저에 동적 내용을 보내기 위해 출력 함수를 많이 사용한다. 이러한 스크립트는 변수를 가진 print() 문을 많이 사용하며 출력을 많이 하므로 성능에 문제가 된다. 따라서 동적으로 생성되는 내용이 많을수록 스크립트의 실행은 느려진다.
출력 버퍼링(Output buffering)은 스크립트의 입출력 시간을 감소시킨다. 이 기능은 PHP4.0.3 이상의 버전에서 제공되는 출력 버퍼링 함수를 이용해 입출력이 많은 프로그램에 적용할 수 있는 일반적인 기술이다.
출력 버퍼의 개념은 버퍼의 내용을 출력하기 전에 출력할 내용을 모두 메모리 버퍼에 저장하는 것이다. 이 방법은 다음과 같은 장점을 가지고 있다.
- 입출력 작업이 줄어 성능이 크게 향상된다.
- 브라우저로 보내기 전에 출력할 내용을 조작할 수 있다.
- 입출력 작업이 순차적으로 빠르게 이루어진다.
이 방법은 단점은 스크립트가 종료될 때까지 클라이언트가 기다려야 한다는 것이다. 따라서 애플리케이션 설계방식이나 스크립트 실행 시간에 따라 사용자는 시스템에 문제가 있다고 판단하고 브라우저를 닫거나 무엇인가 잘못되었다는 결론을 내릴 수 있다.
출력 버퍼링 예제
다음은 PHP에서 출력 버퍼링 사용 방법을 보여주는 간단한 예제이다:
<?php
ob_start();
echo("This is a test\n");
echo("More content\n");
ob_end_flush();
?>
ob_start() 함수는 출력 버퍼링을 시작한다. ob_start() 뒤에는 브라우저로 출력하는 모든 함수가 데이터를 데이터 버퍼로 보낸다.
버퍼의 내용은 ob_end_flush() 함수를 이용해 브라우저로 출력되는데, 다음과 같은 두가지 작업을 한다. 우선 사용 중인 출력 버퍼를 받고, 그 다음 버퍼의 내용을 브라우저로 출력한다.
Import!ant |
위의 예제에서는 ob_end_flush() 함수가 필요하지 않는데, 그 이유는 PHP 인터프리터가 스크립트의 실행이 종료될 때 버퍼의 내용을 출력하기 때문이다. 그러나 코드의 신뢰성을 높이기 위해서는 이 함수를 사용하는 것이 좋다. |
출력 버퍼링 함수
void ob_start([string output_callback])
이 함수는 출력을 만들어 내는 모든 함수들이 사용할 새로운 출력 버퍼를 시작한다. 선택사항으로 콜백 함수를 전달할 수도 있다. 콜백 함수가 전달되면 스크립트가 종료되거나 ob_end_flush()가 실행될 때 이 함수가 호출된다. 이 함수는 버퍼의 내용을 받고 이것을 바탕으로 한 작업 결과를 리턴한다.
예를 들어, 스크립트의 출력 결과에서 foo를 bar로 변경하는 검열(censorship) 콜백 함수를 사용하려면 다음과 같이 한다:
<?php
function censorship($buffer)
{
return str_replace('foo', 'bar', $buffer);
}
ob_start('censorship');
echo("This is a foo test of our program\n");
echo("I can't write foo!\n");
ob_end_flush();
?>
위 스크립트의 실행 결과는 다음과 같다.
[image]
이것은 스크립트의 최종 출력을 처리해야 할 경우에 유용하다.
string ob_get_contents(void)
위의 함수는 사용 중인 출력 버퍼의 내용을 리턴한다. 만일 출력 버퍼가 시작되지 않았다면 false를 리턴한다.
string ob_get_length(void)
위의 함수는 사용 중인 출력 버퍼의 길이를 리턴한다. 만일 사용 중인 출력 버퍼가 없다면 false를 리턴한다.
void ob_end_flush(void)
위의 함수는 사용 중인 버퍼를 종료하고 그 내용을 브라우저로 출력하거나 사용 중인 버퍼에 우위를 갖는다(버프 스택 참조). 이 함수가 실행되면 버퍼는 버려지므로 이를 처리하려면 ob_end_flush()를 호출하기 전에 ob_get_contents()를 사용해야 한다.
버퍼 스택
PHP에서 사용하는 버퍼링 방식은 스택(stack)이다. 이것은 이미 ob_start()를 호출한 블럭 내부에서 ob_start()를 호출할 수 있다는 것을 의미한다. 두 번째 ob_start() 호출은 새로운 버퍼를 생성하고 ob_end_flush()가 실행되면 이 버퍼의 내용은 부모 버퍼로 전단된다. 두 번째 ob_end_flush()를 호출하면 버퍼의 내용은 브라우저로 출력된다.
이것은 페이지 생성 엔진에서 출력 버퍼링을 사용하고 함수나 모듈의 출력을 처리해야 할 경우에 유용하다.
PHP 출력 압축하기
PHP가 데이터를 브라우저로 출력할 때는 HTML 헤더(content type header)를 이용해 가공되지 않은 데이터(raw data)를 보낸다. 최근의 브라우저는 압축된 데이터도 지원하므로 서버에서 gzip으로 압축된 데이터를 브라우저로 보내면 브라우저는 이 데이터의 압축을 풀어 출력하게 된다.
이 방법을 이용하면 스크립트의 성능과 많은 내용을 만들어 내는 페이지 생성 엔진의 성능을 증가시킬 수 있다. 이 방식으로 테스트한 결과 실행 시간이 약 60% 정도 감소했다.
PHP에서는 ob_gzhandler() 콜백 함수를 사용할 수 있다. 만일 우리가 ob_gzhandler() 함수를 ob_start()로 보내면 ob_end_flush() 함수가 실행되었을 때 PHP는 다음 작업을 한다.
- 버퍼의 내용을 ob_gzhandler() 함수로 전달한다.
- ob_gzhandler() 함수는 브라우저가 보낸 헤더를 이용해 브라우저가 gzip 인코딩을 지원하는지 판단한다.
- 만일 gzip 데이터를 사용할 수 있다면 버퍼의 내용을 압축하고 브라우저로 보낼 헤더가 만들어진다.
- 압축된 데이터와 헤더를 브라우저로 전송한다.
출력할 내용을 압축하면 성능이 크게 개선되고 대역폭도 감소한다. 출력 버퍼를 압축하려면 다음과 같이 한다:
ob_start('ob_gzhandler');
브라우저는 gzip으로 압축된 내용을 받는다. 또한 php.ini 파일에서 아래와 같이 설정하면 ob_gzhandler() 함수를 ob_start()의 디폴트 콜백 함수로 사용할 수 있다:
output_handler = 'ob_gzhandler'
Import!ant |
ob_gzhandler() 콜백 함수는 PHP4.0.5 이상에서만 사용할 수 있다. 이전 버전의 PHP는 ob_gzhandler() 함수에 매우 큰 메모리 누수 문제가 있다. |
만일 압축된 내용을 전송하는 것에 문제가 있다면 vary HTTP 헤더를 사용한다. 또한 일부 브라우저는 POST 요청의 결과로 압축된 내용이 올 때 문제가 있는 것으로 알려져 있다.
데이터베이스 최적화
이 방법은 최적화 문제에서 가장 중요한 부분이다. 스크립트는 실행 시간의 대부분을 데이터베이스 작업에 사용하므로 몇 가지 방법을 통해 데이터베이스 작업의 성능을 개선하는 방법을 살펴보자.
여기서는 MySQL을 이용해 설명하지만 이 개념은 PostgreSQL, 오라클 등 다른 데이터베이스로도 확대될 수 있다.
쿼리 분석
일단 성능 저하의 문제가 쿼리에 있다는 것이 밝혀졌다면 각 쿼리에서 무엇이 문제인지를 알아내야 한다. 우선 불필요한 조인을 사용하지 않는지 확인하고 쿼리를 빠르게 만들 다른 방법이 있는지 확인해야 한다. 두 개의 큰 테이블을 조인하는 것보다 쿼리를 두 번하는 것이 빠르다.
쿼리 실행 방법
만일 제대로 만들어진 쿼리의 실행 시간이 매우 길다면 다음의 MySQL EXPLAIN 문을 이용해 MySQL이 쿼리를 어떻게 처리하는지 확인한다:
EXPLAIN SELECT ... FROM ... WHERE ...
간단히 SELECT 문 앞에 EXPLAIN만을 추가하면 된다. MySQL은 쿼리를 실행하고 다음과 같은 처리 정보를 가진 테이블을 리턴한다.
- table : 출력된 행에서 참조하는 테이블
- type : 사용되는 조인 형식. 조인 형식에 대한 자세한 정보는 뒤에 나오는 조인 형식을 참고한다.
- possible_keys : 쿼리를 수행하기 위해 MySQL이 사용할 수 있는 컬럼. 만일 비어있다면 관련된 색인이 없다는 것이다. 이 경우 WHERE 절을 분석해 적절한 색인을 추가하면 성능이 개선된다.
- key : 쿼리를 수행하기 위해 MySQL이 선택한 색인. 만일 NULL이면 색인이 사용되지 않은 것이다. 만일 MySQL이 잘못된 색인을 선택한다면 USE INDEX/IGNORE INDEX를 이용해 강제로 색인을 지정할 수 있다.
- key_len : MySQL이 사용하기로 결정한 키의 길이
- ref : 테이블에서 행을 선택하기 위해 사용된 컬럼 또는 상수
- rows : 테이블에서 행을 선택하기 위해 MySQL이 반드시 검사해야 하는 행의 수
- Extra : MySQL이 쿼리를 실행하는 방식에 대한 추가 정보. 다음과 같은 정보가 사용된다.
- Distinct : 조건에 맞는 첫 번째 행을 발견하면 더 이상 검색을 진행하지 않는다.
- Not exits : MySQL은 쿼리에 LEFT JOIN 최적화를 할 수 있는데, LEFT JOIN 조건에 맞는 행의 조합을 찾아내면 더 이상 테이블의 행을 검사하지 않는다.
- Using filesort : MySQL은 검색된 행을 정렬해서 가져오기 위해 추가적인 작업이 필요하다. 이 작업은 조인 형식에 따라 모든 행을 검사하고 WHERE 절에 해당하는 모든 행이 "정렬 키 + 포인터"를 저장함으로써 가능하다. 각 행을 순서대로 가져오기 전에 이 키가 먼저 정렬된다.
- Using index : 실제 행을 검사하지 않고 테이블의 색인 트리에 있는 정보만을 사용하는 경우이다. 테이블에서 가져오는 모든 컬럼이 같은 색인의 일부분인 경우를 의미한다.
- Using temporary : MySQL은 결과를 저장하기 위해 임시 테이블을 만들 필요가 있다. 이것은 하나의 테이블에서 서로 다른 컬럼에 사용된 GROUP BY의 결과 행을 ORDER BY로 정렬할 때 사용된다.
쿼리를 가능한 빠르게 만들려면 Using filesort와 Using temporary를 살펴본다. 만일 이러한 부분이 발견된다면 쿼리를 최적화할 필요가 있다.
조인 형식
아래는 조인(JOIN) 형식을 좋은 것부터 차례대로 나열한 것이다.
- system : 테이블에 오직 하나의 행만을 가지는 경우이다. 시스템 조인은 일반적이 아니며(보통 테이블은 하나 이상의 행을 갖는다) 검사해야 할 행이 하나이므로 가장 빠르다.
- eq_ref : 한 테이블의 행이 다른 테이블의 각 행과 조합을 이루는 경우. 이 형식은 시스템 조인을 제외하고 가장 좋은 조인 형식이다. 이것은 색인의 모든 부분이 조인에 참여하고 색인이 UNIQUE 또는 프라이머리 키인 경우이다.
- ref : 다른 테이블의 행과 조합하기 위해 색인 값에 매치되는 모든 행이 읽혀진다. ref는 조인이 키의 가장 좌측 부분(leftmost prefix)만을 사용하거나 키가 UNIQUE 또는 PRIMARY KEY가 아닐 경우이다. 만일 비교에 사용되는 키의 행이 적다면 훌륭한 조인 형식이다.
- range : 색인에서 행을 선택하기 위해 지정된 범우의 행만 검색한다. key 컬럼은 사용되는 색인을 나타낸다.
- ALL : 다른 테이블의 행과 조합을 위해 테이블 전체를 스캔하는 경우이다. 이 방식은 다른 경우에 비해 좋지 않다. 적절한 색인을 추가해 ALL 형식을 피하고 컬럼이 상수 값을 통해 검색되도록 하는 것이 좋다.
- index : 색인 트리만 스캔된다는 점을 빼면 ALL과 동일하다. 보통 색인 파일이 데이터 파일보다 작기 때문에 ALL 보다 빠르다.
EXPLAIN의 출력 테이블에 있는 rows 컬럼 값을 이용해 훌륭한 조인 형식인지를 알 수 있다. 이 컬럼은 쿼리를 수행하기 위해 얼마나 많은 행을 검색해야 하는지를 나타낸다.
MySQL 문서에 있는 다음 예는 EXPLAIN 명령의 결과를 이용해 어떻게 조인을 최적화하는지 보여준다. 우리는 여기서 몇 개의 필드를 가진 가상의 데이터베이스를 이용하므로 데이터베이스, 테이블, 필드의 이름은 무시해도 된다. 이 예제의 목적은 EXPLAIN의 결과를 어떻게 분석하는지를 아는 것이다.
SELECT 구문을 조사하기 위해 EXPLAIN을 사용한다:
EXPLAIN SELECT tt.TicketNumber, tt.TimeIn,
tt.ProjectReference, tt.ExtimatedShipDate,
tt.ActualShipDate, tt.ClientID,
tt.ServiceCodes, tt.RepetitiveID,
tt.CurrentPRocess, tt.currentDBPerson,
tt.RecodeVolume, tt.DPPprinted, et.COUNTRY,
et_1.COUNTRY, do.CUSTNAME,
FROM tt, et,et AS et_1
WHERE
tt.SubmitTime IS NULL
AND tt.ACtualIPC = et.EMPLOYID
AND tt.AsignedPC = et_1.EMPLOYID
AND tt.ClientID = do.CUSTOMER;
비교하려는 컬럼이 다음과 같이 정의되었다고 가정해 보자.
테이블 |
컬럼 |
컬럼 타입 |
tt |
ActualPC |
CHAR(10) |
tt |
AssignedPC |
CHAR(10) |
tt |
ClientID |
CHAR(10) |
tt |
EMPLOYID |
CHAR(10) |
tt |
CUSTNMBR |
CHAR(10) |
그리고 각 테이블은 다음과 같은 색인을 가지고 있다.
테이블 |
색인 |
tt |
ActualPC |
tt |
AssignedPC |
tt |
ClientID |
tt |
EMPLOYID (primary key) |
tt |
CUSTNMBR (primary key) |
tt.ActualPC 값은 골고루 분포되어 있지 않다.
아무런 최적화도 수행하지 않았을 경우 EXPLAIN 문의 실행 결과는 다음과 같다.
Table |
Type |
Possible Keys |
Key |
Key Length |
Ref |
Rows |
et |
ALL |
PRIMARY |
NULL |
NULL |
NULL |
74
|
do |
ALL |
PRIMARY |
NULL |
NULL |
NULL |
2135
|
et_1 |
ALL |
PRIMARY |
NULL |
NULL |
NULL |
74
|
tt |
ALL |
AssignedPC, ClientID, ActualPC |
NULL |
NULL |
NULL |
3872
|
보는 바와 같이 모든 테이블에 ALL이 사용된다. 이것은 MySQL이 조인을 위해 모든 테이블에 대해 전체를 스캔하는 것을 의미하며 개선이 필요한 쿼리이다. 이 예에서는 45,268,558,720 행(74*2135*74*3872
)을 검사해야 하는데, 이것은 상당한 시간이 걸리게 된다.
한가지 문제점은 컬럼 형식이 다르게 정의되어 있으면 MySQL이 컬럼의 색인을 효과적으로 이용할 수 없다는 것이다. 이 경우, 길이가 다르게 선언되지만 않는다면 VARCHAR와 CHAR는 같다. 이 예제에서 tt.ActualPC는 CHAR(10)이고 et.EMPLOYID는 CHAR(15)이므로 길이에서 차이가 난다. 따라서 ActualPC의 길이를 10에서 15로 변경할 필요가 있다:
mysql> ALTER TABLE tt MODIFY ActualPC VARCHAR(15);
이제 tt.ActualPC와 et.EMPLOYID는 둘 다 VARCHAR(15)가 되었다. EXPLAIN을 다시 한 번 실행해 보자.
Table |
Type |
Possible Keys |
Key |
Key Length |
Ref |
Rows |
tt |
ALL |
AssignedPC, ClientID, ActualPC |
NULL |
NULL |
NULL |
3872
|
do |
ALL |
PRIMARY |
NULL |
NULL |
NULL |
2135
|
et_1 |
ALL |
PRIMARY |
NULL |
NULL |
NULL |
74
|
et |
eq_ref |
PRIMARY |
PRIMARY |
15
|
tt.ActualPC |
1
|
검사해야 하는 행의 수가 74만큼 줄어들었다. 두 번째는 tt.AssignedPC = et_1.EPLOYID와 tt.ClientID = do.CUSTNBR의 비교 부분에서 컬럼 길이가 맞지 않는 문제를 해결해야 한다:
mysql> ALTER TABLE tt MODIFY AssignedPC VARHCAR(15), MODIFY ClientID VARCHAR(15);
다시 EXPLAIN을 실행한 결과는 다음과 같다.
Table |
Type |
Possible Keys |
Key |
Key Length |
Ref |
Rows |
et |
ALL |
PRIMARY |
NULL |
NULL |
NULL |
74
|
tt |
ref |
AssignedPC, ClientID, ActualPC |
ActualPC |
15
|
et.EMPLOYID |
52
|
et_1 |
eq_ref |
PRIMARY |
PRIMARY |
15
|
tt.AssignedPC |
1
|
do |
eq_ref |
PRIMARY |
PRIMARY |
15
|
tt.ClientID |
1
|
이 쿼리는 매우 바람직하게 최적화되었다. 대부분의 쿼리에 있어 이러한 방법이 적용되므로 EXPLAIN을 이용해 색인을 생성하고 길이가 다른 문제를 해결할 수 있다. 데이터 모델을 설계할 때 길이가 다른 키에 대해 MySQL이 어떻게 동작하는지를 염두에 둔다면 쿼리의 실행 속도가 느려졌을 때 테이블을 변경해야 하는 일은 발생하지 않을 것이다.
테이블 최적화
우리가 테이블에서 레코드를 삭제하면 MySQL은 나중에 INSERT 작업에서 데이터를 재사용하도록 링크 목록으로 유지한다. 만일 DELETE 작업을 많이 한다거나 가변 길이를 가진 행에 큰 변화가 생겼다면 우리는 이 테이블의 분산된 데이터를 모으는 작업(defragment)을 해야 한다. 이 작업은 OPTIMIZE TABLE 문을 이용한다:
OPTIMIZE TABLE tbl_name [, tbl_name]
OPTIMIZE TABLE은 다음과 같은 작업을 한다.
- 삭제되거나 분산된 행을 수정한다.
- 정령되지 않은 모든 색인 페이지를 정렬한다.
- 테이블 통계 정보를 업데이트한다.
Import!ant |
OPTIMIZE 명령을 실행하는 동안에는 테이블이 잠기므로 이용자가 많은 시간에는 실행하지 않도록 한다. |
데이터 모델 최적화
데이터베이스 쿼리를 최적화하는 가장 좋은 단계는 데이터베이스 모델을 설계하는 단계이다. 다음 팁을 염두해 두고 데이터베이스를 설계하면 성능이 향상될 거이다.
- 가장 효율적인 데이터 형식을 사용한다. MySQL은 다양한 데이터 타입을 지원한다. 작은 데이터형을 사용하면 쿼리 속도가 빨라진다. 가능한 작은 정수형을 사용한다. 예를 들어 MEDIUMINT는 INT보다 좋다. 사용하기에 적당한 숫자형은 다음과 같다.
컬럼 형식 |
필요한 공간 |
TINYINT |
1 바이트
|
SMALLINT |
2 바이트
|
MEDIUMINT |
3 바이트
|
INT |
4 바이트
|
INTEGER |
4 바이트
|
BIGINT |
8 바이트
|
FLOAT(X) |
X <= 24이면 4 바이트, 25 <= X <= 53이면 8 바이트 |
FLOAT |
4 바이트
|
DOUBLE |
8 바이트
|
DOUBLE PRECISION |
8 바이트
|
REAL |
8 바이트
|
DECIMAL(M,D) |
D>0이면 M+2 바이트, D=0 이면 M+1 바이트 (M |
NUMERIC(M,D) |
D>0이면 M+2 바이트, D=0 이면 M+1 바이트 (M |
- 가능하면 컬럼을 NOT NULL로 선언한다. 속도를 빠르게 하고 컬럼 당 1비트를 아낄 수 있다.
- 가능하면 TEXT, BLOB, VARCHAR 컬럼을 피한다. 만일 불가피하게 가변폭 컬럼을 사용해야 한다면 가능하면 많이 사용한다. 한 테이블에 가변폭 컬럼이 존재한다면 고정폭 컬럼이 이미 쓸모없게 된다. 또한 고정폭 컬럼 사이에 블럽(blob) 컬럼이 끼어 있다면 이 블럽 컬럼을 별도의 테이블로 분리하고 고정폭 컬럼을 가진 테이블을 참조하도록 한다.
- 테이블의 프라이머리 키는 가능하면 작아야 한다. 이 방법은 각 해의 식별자로 효과적으로 처리할 수 있게 만든다.
- 색인의 효율을 증가시킨다. 만일 우리가 어떤 컬럼의 앞자리 X번째 문자가 고유하다는 것을 안다면 이 부분만을 이용하는 색인을 만들도록 한다. 이것이 좀더 효율적이다.
- 테이블의 모든 컬럼을 이용해 색인을 만들지 않는다. 색인은 SELECT 문을 빠르게 해주지만 UPDATE, INSERT, DELETE 작업의 속도는 느려진다. 필요한 색인만을 생성한다.
색인 사용
색인은 데이터베이스 엔진이 쿼리를 실행할 때 사용하는 기본적인 해결 방법이다. 테이블 전체를 검색하는 것보다 색인을 검색하는 것이 몇 배 이상 빠르다.
Import!ant |
테이블에 하나 이상의 색인을 추가하면 쿼리의 90%를 최적화할 수 있다. |
많은 데이터베이스 설계자들이 색인에 신경을 쓰지 않고 속도 문제가 생겼을 경우에 색인을 만든다. 물론 이 방법도 문제를 해결할 수 있지만 좋은 방법은 아니다. 제대로 설계된 데이터베이스는 반드시 색인을 염두에 두고 만들어져야 한다. 가장 좋은 색인은 CREATE TABLE 문에서 만들어 져서 많은 쿼리에 사용된 것이다. 그러나 불필요하게 만든 쿼리는 업데이트 속도를 저하시킨다.
MySQL은 자동적으로 색인을 사용한다. 주어진 쿼리에서 어떤 색인이 사용되었는지 확인하려면 앞서 설명된 EXPLAIN 명령을 사용하면 된다.
SELECT 쿼리 최적화
SELECT 쿼리를 최적화하려면 색인을 추가할 수 있는지 확인한다. EXPLAIN을 사용해 색인이 사용되고 있는지를 확인한다.
INSERT 쿼리 최적화
테이블에 레코드를 삽입하는 작업은 다음과 같은 단계를 거친다.
- 연결
- 서버로 쿼리 전송
- 쿼리 분석
- 레코드 삽입
- 색인 삽입(각 색인마다 한 번씩)
- 연결 종료
INSERT 대신 INSERT DELAYED를 사용하면 보다 빠른 속도를 얻을 수 있다. 서바상의 작업 종료와는 무관하게 클라이언트는 insert 명령이 성공했다는 확인 메시지를 받는다. MySQL 서버는 클라이언트에게 실행이 완료되기를 기다리라는 메시지를 보내지 않고 insert 작업을 나중에 처리한다.
파일에서 데이터를 로딩할 때 LOAD DATA INFILE을 사용하면 INSERT 문을 여러 번 사용하는 것 보다 20배 정도 빠르다(ORACLE/MSSQL에서는 BCP라고 부른다).
쉼표로 구분된 파일을 데이터베이스로 로드하려면 다음 명령을 사용한다:
LOAD DATA INFILE data.txt INTO TABLE foo FIELDS TERMINATED BY ',';
LOAD DATA INFILE은 다음과 같은 형식이다:
LOAD DATA [LOW_PRIORTY | CONCURRENT] [LOCAL] INFILE 'file_name.txt'
[REPLACE | IGNORE]
INTO TABLE tbl_name
[FIELDS
[TERMINATED BY '\t']
[[OPTIONALLY] ENCLOSED BY '']
[ESCAPED BY '\\']
]
[LINES TERMINATED BY '\n']
[IGNORE number LINES]
[(col_name,...)]
LOAD DATA INFILE을 이용해 데이터를 MySQL 테이블에 넣은 자세한 방법은 MySQL 메뉴얼을 참고하면 된다.
insert 작업에 앞서 테이블을 잠그면 작업속도가 빨라진다:
LOCK TABLES a WRITE;
INSERT ....
INSERT ....
UNLOCK TABLES;
UPDATE 쿼리 최적화
UPDATE 문은 SELECT 문을 실행한 뒤에 쓰기 작업을 하는 것으로 생각할 수 있다. 따라서 UPDATE xx FROM yy WHERE zz 쿼리를 최적화하는 것은 쓰기 작업은 항상 존재하므로 SELECT xx FROM yy WHERE zz 쿼리를 최적화하는 것과 같다. 따라서 UPDATE 쿼리의 최적화는 SELECT 쿼리의 최적화와 동일하다.
DELETE 쿼리 최적화
레코드를 삭제하는 시간은 색인의 수에 정확히 비례한다. 이것는 MySQL이 레코드를 테이블과 각각의 색인에서 지워야 하기 때문이다. 테이블의 전체 레코드를 삭제하려면 TRUNCATE TABLE tb_name을 사용하는 것이 DELETE from tb_name을 사용하는 것보다 빠르다. TRUNCATE TABLE은 각 행의 색인과 데이터를 지울 필요 없이 전체 테이블과 색인을 지운다.
연결 최적화
테이터베이스 쿼리를 실행할 때 시간을 소요하는 마지막 원인은 바로 데이터베이스 연결 과정이다. 몇몇 애플리케이션에서 우리는 매번 쿼리를 할 때마다 데이터베이스 연결을 하고, 사용하고, 종료하는 것을 볼 수 있다. 그러나 데이터베이스에 일단 연결이 되면 여러 개의 쿼리를 실행할 수 있으므로 이것은 비효율적인다. PHP를 이용하면 쿼리를 실행할 때마다 데이터베이스에 연결하지 않고 지속적인 연결을 할 수 있다.
지속적 연결
PHP에서 MySQL 데이터베이스로 지속적 연결을 하려면 다음 함수를 사용한다:
int mysql_pconnect([string hostname [:port] [:/path/to/socket/] [, string username] [, string password]])
접속을 할 때 이 함수는 우선 동일한 호스트 명, 사용자 아이디, 암호를 사용한 지속적 연결이 있는지 찾는다. 만일 발견이 된다면 새로 연결하지 않고 기존의 연결 식별자를 리턴한다. 또한 이 연결은 스크립트가 종료되어도 끊어지지 않고 나중에 다시 사용할 수 있도록 연결 상태를 유지한다.
그 밖의 최적화 팁
최적화에 사용할 수 있는 그 밖의 팁은 다음과 같다.
- 업데이트가 많이 발생하는 테이블에 복잡한 SELECT 쿼리를 사용하지 않는다.
- 변경이 빈번하게 발생하는 데이블에는 VARCHAR와 BLOB 컬럼을 사용하지 않는다.
- 단지 크다는 이유만으로 하나의 큰 테이블을 서로 다른 테이블로 분리하는 것은 옳지 않다.
- 해시 컬럼 소개
만일 어떤 컬럼이 짧고 고유하다면 여러 컬럼을 사용하는 색인보다 빠르다. MySQL에서는 이 특별한 컬럼을 쉽게 사용할 수 있다.
SELECT * FROM tb_name WHERE hash=MD5(concat(col1,col2)) AND col1='x' AND col2='y';
- 카운터는 실시간으로 업데이트한다. 만일 카운트와 같이 많은 행을 기반으로 하는 정보를 처리해야 한다면 별도의 테이블을 만들고 이 카운트를 실시간으로 업데이트하는 것이 바람직하다. 다음과 같은 업데이트는 매우 빠르다.
UPDATE tb_name SET count=count+1 WHERE col='x';
- 거대한 테이블을 검색하는 대신 요약 테이블을 이용한다. 실시간으로 통계를 내는 것보다 요약 정보를 유지하는 것이 훨씬 빠르다.
- 디폴트 값을 가지는 컬럼을 이용한다. 입력되는 값이 디폴트가 아닐 경우에만 명시하면 된다.
- 고유한 값이나 키를 만들려면 AUTO_INCREMENT 컬럼을 사용한다.
캐싱
스크립트 실행 시간의 대부분은 데이터베이스 작업과 입출력 작업에서 소모된다. 입출력과 데이터베이스 쿼리를 최적화한 뒤에는 스크립트의 실행 시간을 개선해야 한다. 만일 느려지는 작업이 있다면 가장 좋은 최적화 방법은 해당 작업을 피하는 것이다. 이것은 캐싱을 사용하면 가능하다.
캐싱의 정의
캐싱(Caching) 처리 작업이나 생성 작업 없이 데이터를 다시 사용할 수 있도록 저장하는 것을 말한다. PHP 프로그래밍에서는 캐싱은 두 번씩 생성할 필요가 없도록 동적으로 만들어진 데이터를 저장하는 것을 의미한다. 데이터를 만드는 과정이 복잡할수록 캐싱의 효과가 크다.
캐싱의 중요성
캐싱은 매우 중요한 기술이다. 무엇보다 데이터를 동적으로 생성하는 작업이 단순한 파일 읽기 작업으로 대체되어 실행 시간이 줄어든다. 또한 캐싱은 웹 서버와 데이터베이스 서버의 부하를 줄이는 좋은 방법이다. 수많은 연결과 트랜잭션이 발생하는 대형 사이트에서 캐싱은 필요한 데이터베이스 쿼리의 수를 줄여준다.
캐싱의 또 다른 중요한 장점은 외부 데이터에 대한 사이트의 의존성을 줄여준다는 것이다. 만일 데이터의 일부가 데이터베이스 혹은 다른 사이트에서 제공된다면 해당 사이트가 다운되어도 캐시에 저장된 데이터를 이용해 작업을 계속할 수 있다.
캐싱의 장점
캐싱의 다음과 같은 장점을 가지고 있다.
- 일반적으로 파일에서 데이터를 읽는 작업은 데이터베이스나 다른 소스에서 데이터를 생성하는 것보다 빠르므로 성능이 향상된다.
- 서버와 데이터베이스의 부하를 감소시킨다.
- 독립성이 좋아진다. 만일 데이터베이스가 다운되어도 사이트는 영향을 받지 않는다.
캐싱의 단점
물론 캐싱도 몇 가지 단점을 가지고 있다. 그 중 가장 나쁜 것은 사이트가 복잡해진다는 것이다. 캐시 데이터를 이용하기 위해 데이터를 캐시에 저장하고, 데이터가 유효한지 확인하고, 필요한 경우 캐시를 업데이트하는 작업이 추가해야 한다. 캐시에 저장되는 데이터의 형식에 따라 캐싱 로직은 그리 간단하지 않고 캐시를 위한 특별한 함수를 만들어야 하는 경우도 있다.
일반적인 캐싱 정책
우선 캐싱 일반적인 캐싱 메커니즘의 로직을 살펴보자. 이것은 대부분의 캐싱 구조에 적합하고 캐싱 시스템을 설정하기에 앞서 결정을 내리는 데 도움이 된다.
[이미지]
- 접근 데이터 확인
캐싱의 원리는 간단한다. 다시 요청되었을 때 다시 반복할 필요 없도록 어떤 작업 결과를 저장하는 것이다. 데이터를 저장할 때 어디에서 생성되었는지는 상관하지 않으므로, 데이터가 이미 캐시에 있는지 검사하기 위해 데이터의 이름을 부여해야 한다. 예를 들어, 함수의 실행 결과는 cache_funcName.dat(명명 규칙 부분 참조)와 같은 이름을 부여할 수 있다. 만일 printTable() 함수를 사용한다면 이 함수의 실행 결과는 cache_printTable.dat라는 이름의 파일로 저장할 수 있다.
- 캐시 데이터의 유효성 확인
만일 캐시에 이미 주어진 이름의 데이터가 존재한다면 이 데이터가 유효한 지를 검사해야 한다. 예를 들어, 캐시의 데이터가 저장된 지 5분 미만이라면 유효하다고 판단할 수 있는데 따라서 5분마다 새로운 데이터를 만들어야 한다. 다른 방법은 변화가 있기 전까지는 캐시 데이터가 유효하다고 판단하는 것이다. PHP에서 파일의 최종 수정 시간 filemtime() 함수를 이용해 알 수 있다.
- 캐시에서 데이터 가져오기
여러분은 캐시에 접근하고 주어진 이름의 캐시 데이터를 가져온다. 데이터에 접근하는 방법은 저장 방식에 따라 다르다. 파일 기반의 캐싱 시스템의 경우 캐시에서 데이터를 가져오는 것은 파일을 열고 그 내용을 읽는 것을 의미한다. 만일 캐시가 다른 저장 방법을 사용한다면 접근 방법은 바뀔수 있다.
- 데이터 생성
이것은 캐싱 시스템의 외부에서 동적으로 데이터를 만드는 과정이다. 이것은 실제로 캐시에 저장되는 코드 부분이면 동적인 데이터를 만들어 내는 PHP 스크립트에 해당된다.
- 캐시에 데이터 저장 또는 업데이트
만일 캐시에 데이터가 없거나 유효하지 않으면 생성된 데이터를 캐시에 저장한다. 그 다음 불필요한 작업을 하지 않고 저장된 데이터를 이용할 수 있다.
- 데이터 출력
print(), echo() 등의 함수를 이용해서 클라이언트에게 데이터를 전송하는 일반적인 작업을 의미한다.
따라서 우리는 다음과 같은 것들을 정의해야 한다.
- 데이터를 저장하고, 업데이트하고, 가져오는 함수로 이루어진 캐시 저장 메소드
- 캐시 데이터를 구별할 수 있는 명명 규칙
- 캐시 데이터의 유효성을 판단할 수 있는 기준
- 주기적으로 캐시의 내용을 갱신하는 정책
캐시를 위한 저장 방식
캐싱 시스템에서 정의해야 하는 첫 번째 요소는 캐시 데이터를 저장하는 방식이다. 몇 가지 방식이 있지만 가장 흔한 것은 각각의 캐시 데이터를 하나의 파일로 저장하는 것이다. 일반적으로 다음과 같은 방식이 사용된다.
- 데이터베이스 사용
- 각각의 캐시 자료를 하나의 파일로 나타냄
- 모든 캐시 자료를 하나의 DBM 파일로 나타냄
- 공유 메모리(shared memory) 사용
데이터베이스 사용
우리는 주로 데이터베이스에 만들어낸 데이터를 저장하기 때문에 이 방법은 그다지 바람직하지 않다. 그러나 데이터를 다른 사이트에서 가져오거나 매우 느린 방법을 통해 만들어 내는 시스템의 경우 데이터베이스를 이용하는 방법은 신뢰성이 있다.
데이터베이스의 명명규칙은 각각의 데이터에 접근할 수 있는 프라이머리 키가 만들어지는 것이어야 한다. 데이터 저장, 검색, 업데이트, 삭제 작업은 SQL문을 통해 이루어진다. 캐시 데이터의 검색 작업을 빠르게 하기 위해 테이블은 프라이머리 키로 색인되어 있어야 한다. 또한 테이블에는 캐시 데이터의 유효성을 확인할 수 있도록 타임스탬프(timestamp)가 필요하며, 캐시 데이터의 유효성 나타내는 플래그 컬럼을 둘 수도 있다.
파일 사용
파일을 사용할 경우에는 각각의 캐시 데이터를 하나의 파일로 생성한다. 명명 규칙은 각각의 데이터에 고유한 이름을 부여할 수 있어야 한다. 파일을 업데이트하는 작업은 파일을 삭제하고 새로 생성하는 것을 의미한다. 유효성은 파일의 최종 수정일 등의 시스템 정보를 이용해 검사할 수 있다.
캐시 데이터가 몇만개씩 있다면 이 방식은 그다지 효율적이지 못하다. 파일의 수가 많으면 아이노드(inode)가 부족할 수도 있고 파일 검색이나 저장 성능에 문제가 있을 수 있기 때문에 파일 시스템에 수천 개의 파일을 두는 것은 바람직하지 못하다. 캐시 데이터의 수가 적다면 이 방법이 유용하다.
DBM 파일 사용
데이터베이스를 사용하기를 원하지 않거나 캐시 데이터를 각각의 파일로 만들기를 원하지 않는다면 DBM 파일은 좋은 방법이다. DBM 파일은 데이터베이스에서 사용하는 함수를 제공하며 속도가 빠르다. PHP에서는 다양한 DBM 방식을 사용할 수 있다. 예를 들어, SleepyCat의 DB2(http://www.sleepcat.com/)는 캐싱 시스템에 성공적으로 사용되어 왔다.
비록 DBM 파일이 일반적인 파일보다 약간 느리지만 캐시 데이터의 수가 증가함에 따라 발생하는 문제는 없다.
공유 메모리 사용
우리는 또한 PHP의 공유 메모리 함수를 이용해 캐시 데이터를 공유 메모링 저장할 수 있다. 공유 메모리를 이용함으로써 우리는 스크립트에서 접근할 수 있는 공유 메모리 세그먼트를 정의할 수 있다. PHP 스크립트는 이 세그먼트를 이용해 캐시 데이터를 저장한다. 이 방법은 다소 복잡하지만 상당히 빠르다. 그러나 메모리는 상당히 비싼 자원이기 때문에 방대한 메모리를 캐시 데이터로 낭비할 수는 없고 다른 저장 방법을 찾아보아야 한다.
메모리 캐시
만일 정말 빠른 캐싱 시스템을 원하고 서버가 리부팅되었을 경우의 데이터 손실이 중요하지 않다면 캐시 데이터를 메모리에 저장할 수 있다. 이것을 위한 가장 좋은 방법은 메모리로 맵핑되어 있는 파일 시스템에 캐시 데이터를 만드는 것이다. 리눅스 시스템에서는 이 작업이 간단하게 이루어진다.
그 다음 우리는 디렉토리에 있는 파일이나 DBM 파일을 메모리에 직접 저장하면 된다. 디스크 입출력 작업이 발생하지 않기 때문에 이 방식의 작업은 상당히 빠르다.
Import!ant |
웹 서버가 많은 캐시 파일보다는 메모리를 사용하도록 하는 것이 바람직하다. 그러나 많은 메모리를 가지고 있고 성능이 매우 중요하다면 이 방법을 선택할 수 있다. |
명명 규칙
명명 규칙(naming convention)은 데이터를 캐시로 저장하기 위해 사용되는 단계이며 어떤 종류의 데이터를 캐싱하느냐에 따라 다르다.
데이터 종류 |
처리 방법 |
함수 결과 |
캐시 이름을 cache_funcName.dat와 같은 방식으로 사용한다. |
인크루드 파일 결과 |
캐시 이름을 lcache_fileName.dat와 같은 방식으로 사용한다. |
스크립트 결과 |
캐시 이름을 cache_fileName.dat와 같은 방식으로 사용한다. |
생성된 파일 |
캐시 이름을 $REQUEST_URI의 md5() 해시를 이용한다. 만일 사이트에 사용자 등록 시스템이 있다면 md5() 해시 작업을 하기 전에 사용자의 아이디를 URI에 추가해야 한다. 그렇지 않으면 모든 사용자가 같은 정보를 보게 된다. |
md5()는 메시지를 암호화하는 함수이다. 이 함수는 문자열을 몇 번의 작업을 통해 128비트 암호문으로 만들어 주며 다음과 같은 특징을 가지고 있다.
- 만일 문자열의 일부만을 변경해도 md5 값은 변경된다.
- 동일한 md5 값을 갖는 두 문자열을 찾아내는 것이 어렵다.
- md5 값을 이용해 원래의 문자열을 알아내는 것은 불가능하다.
따라서 md5()는 고유한 값을 가지는 문자열을 만들어내는 훌륭한 함수이며 동일한 md5 값을 가지는 두 개의 문자열을 만들어 낼 확률은 상당히 낮다. 23장에는 보안을 위해 md5()와 비슷한 기능을 하는 다른 함수가 설명된다.
유효성 검사
유효성 검사는 캐시 데이터가 유효인지 아닌지를 검사하는 가장 좋은 방법이다. 가장 일반적인 방법은 캐시 파일이나 데이터의 최종 수정일을 검사해 지정된 시간보다 나중에 만들어졌으면 유효한 것이다. 이것은 지정된 주기마다 캐시 데이터를 갱신할 수 있는 좋은 방법이다. 예를 들어, 어떤 홈페이지를 캐시에 저장해 놓고 10분마다 캐시의 내용을 갱신할 수 있다.
다른 유효성 검사는 캐시에 저장되는 데이터와 저장 방식에 따라 다르다. 예를 들어, XML 파일을 처리하고 결과를 출력하는 과정을 캐시에 저장했다면 XML 파일이 변경되었을 경우에는 쓸모가 없어진다. 이 경우에는 XML 파일의 최종 수정일을 검사하고 지정된 기간을 넘겼는지 확인해서 캐시를 갱신해야 한다.
데이터 갱신 정책
캐시 데이터가 무한정 커지기를 바라지 않는다면 캐시를 갱신하는 정책을 만들어야 한다. 오래된 캐시 데이터를 검사하는 과정은 주기적으로 실행되어야 한다. 만일 오래된 데이터가 발견되면 삭제해야 한다. 이러한 작업을 위한 알고리즘은 여러 가지가 있다.
예를 들면, "x"분 이상된 데이터, 최근에 "n"번 이하로 사용된 데이터 등을 삭제할 수 있다. 이러한 방법을 사용하려면 접근 횟수, 최근 사용 시간 등의 추가적인 정보를 캐시에 저장해야 한다. 어떤 경우에 시스템에서 이러한 정보를 제공하기도 하며 어떤 경우에는 직접 이 정보를 저장해야 한다.
일반적으로 캐시를 갱신하는 것에는 LRU(Least Recently Used) 알고리즘이 가장 적합하다. 갱신 작업은 보통 cron을 이용해 주기적으로 실시한다. 이 과정은 20장에서 설명된 crontab(UNIX 시스템)을 이용해 설정할 수 있다.
LRU 알고리즘은 캐시에서 n개의 데이터를 삭제해야 한다면 가장 오랜 시간동안 사용되지 않은 데이터를 삭제한다. 이 정책은 최근에 사용된 파일은 데이터가 사용될 경우 다시 접근하게 된다는 것을 가정하고 있다. 오랫동안 사용되지 않은 데이터는 다시 사용될 가능성이 낮기 때문에 삭제될 것이다.
캐시되어야 하는 데이터
일반적으로 컨텐츠와 데이터베이스 쿼리 두 종류의 데이터가 캐시에 저장된다.
컨텐츠 캐싱하기
컨텐츠를 캐싱한다는 것은 동적으로 생성된 컨텐츠를 하나의 파일이나 파일 그룹에 저장하고 나중에 데이터를 만들어 내지 않고 이 데이터를 가져오는 것을 의미한다. 보통 데이터의 생성 과정이 복잡하거나 느리고 다른 외부 요인에 영향을 받는 경우 캐싱이 필요하다. 컨텐츠를 캐싱하려면 두 가지 방법을 사용할 수 있는데, 생성되는 모든 컨텐츠를 캐싱하는 것과 모듈이나 코드의 일부분을 캐싱하는 것이다.
일반적인 캐싱 구조
다음은 모든 동적 페이지를 만들어 내는 페이지 엔진을 가지고 있다고 가정한 경우의 일반적인 캐싱 구조이다. 우리는 명명 규칙의 URI를 만들어 내기 위해 md5() 해시를 이용한다. 또한 PHP 출력 버퍼링을 이용해 캐시에 유효한 데이터가 없을 경우 모든 출력을 버퍼에 저장하고, 이 데이터를 파일로 저장한다. 스크립트가 다시 실행되면 이 파일을 검사한다. 만일 지정된 URI의 파일이 존재하고 10분 이상 경과하지 않았다면 데이터를 생성하지 않고 이 파일을 읽어 그 내용을 브라우저로 보낸다:
<?php
// 첫 번째로 캐시 데이터의 이름을 만든다.
$cache_name = md5($REQUEST_URI);
$time = date('U');
// 캐시에 유효한 데이터가 있는지 검사한다.
// 캐시 데이터는 10분(600초) 동안 유효하다.
if (file_exists($cache_name) && ($time - filemtime($cache_name)) < 600) {
$data = readfile($cache_name);
echo($data);
} else {
ob_start();
// 컨첸츠를 생성하는 일반적인 코드
echo("Hello world\n");
$data = ob_get_contents();
$fh = fopen($cache_name, 'w+');
fwrite($fh, $data);
fclose($fh);
ob_end_flush();
}
?>
데이터베이스 쿼리 캐싱
만일 모든 컨텐츠를 캐시로 저장할 필요가 없다면 복잡한 SELECT 쿼리의 결과만을 저장할 수도 있다. 이 경우에는 데이터베이스 쿼리를 캐싱하는 일반적인 캐시 구조를 만들기가 어려우므로 적절한 다른 방법을 이용한다. 또한 캐시 데이터의 유효성을 나타내는 플래그가 필요하고 캐시 데이터를 업데이트하는 스크립트가 필요하다.
일반적인 데이터베이스 캐싱 구조
데이터베이스 추상화 계층을 이용하면 한 곳에서 모든 데이터베이스 작업을 제어할 수 있다. 모든 쿼리를 위한 일반적인 캐시 시스템을 만드는 것도 가능하다. 어려운 점은 쿼리를 위한 명명 규칙을 위한 일반적인 캐시 시스템을 만드는 것도 가능하다. 어려운 점은 쿼리를 위한 명명 규칙을 정의하는 일과 쿼리가 데이터베이스를 업데이트했을 때 캐시 데이터가 유효하다는 표시를 하는 것이다. 이를 위해 아래와 같은 방법을 생각해볼 수 있다.
캐시 데이터 이름을 만들기 위해 SELECT 문의 md5() 해시를 이용하고 "유효한 데이터"를 나타내는 플래그를 각각의 캐시 데이터에 저장한다. 그리고 쿼리가 어떤 테이블에서 데이터를 가져오는지를 저장한다. 만일 UPDATE, INSERT, DELETE 쿼리일 경우 유효하지 않다는 표시를 하거나 변경된 테이블을 사용하는 모든 캐시 데이터를 삭제한다.
데이터베이스 쿼리 캐시를 위한 명명 규칙은 다음과 같다:
md5hash_table1_table2_table3_table4.dat
md5hash 부분은 SELECT 문의 md5() 값이며, 그 뒤에 이 쿼리에서 사용되는 모든 테이블이 나온다(사용되는 테이블을 알기 위해 쿼리를 파싱해야 한다).
SELECT 문은 md5()로 변환하기 쉽고, 리턴하면서 value_*로 시작하는 파일이 있는지 확인할 때 사용된다. 만일 없다면 쿼리를 실행하고 결과 값을 적당한 파일에 저장한다. 만일 이 파일이 존재한다면 그냥 파일에서 데이터를 가져온다.
UPDATE, DELETE, INSERT 문의 경우, 각 SQL 문에 포함된 테이블을 이용하는 모든 캐시 데이터를 검사해야 한다. 사용되는 테이블을 알기 위해 다시 쿼리를 파싱해야 하며, 이 데이블을 사용하는 캐시 데이터를 지워야 한다(이 작업은 파일 시스템 명령어를 이용하면 간단하다).
데이터베이스를 위한 일반적인 캐싱 시스템은 컨텐츠 캐싱 시스템보다 적용과 유지가 어려운 경향이 있다. 만일 컨텐츠를 캐싱하는 것이 불가능하다면 데이터베이스 쿼리의 특별한 캐싱은 비교적 간단하지만 유지하기가 힘들다.
이럴 경우 하드웨어를 업그레이드하거나 라운드 로빈(round robin), 생산자-소비자 모델(producer-consumer model)을 이용해야 한다.
PHP 엔진 최적화
마지막 최적화 방법은 PHP 엔진 자체에 관한 것이다. PHP는 실행을 위해 스크립트를 파싱해서 중간 단계(intermediate)의 코드로 변형한다. 그 다음 젠드 엔진은 이 코드를 토큰(tokens)으로 파싱하고, 내부 구조를 처리하고, 나머지 부분은 PHP로 전달한다. 최적화가 가능한 부분은 중간 단계의 코드를 캐시로 만드는 것이다. 만일 중간 단계의 코드를 캐싱해주는 적절한 제품을 사용한다면 소스코드가 변경되지 않는 이상 PHP 엔진은 소스코드를 파싱할 필요가 없다.
이러한 종류의 최적화를 하는 제품은 다음과 같다.
위의 세 가지 제품은 PHP 엔진이 동일한 소스 파일을 반복적으로 파상하지 않도록 중간 단계의 코드를 파일로 저장한다. 테스트 결과 이 세가지 제품은 모두 실행시간이 10에서 20% 정도 빨라졌다. Zend Accelarator는 차세대 젠드 캐시로, 성능 개선을 위한 고급 기능을 제공하고 스트리밍을 관리할 수 있는 기능이 추가되었다. 또한 젠드 옵티마이저도 통합되어 있다.