조한석 |마소 Jr.에 PHP를 연재하던 필자는 최근 Smarty에 흠뻑 빠져 살고 있다. 많은 PHP 프로그래머들과 Smarty를 함께 공유하고 싶은 마음이 간절하며, 지금도 틈틈이 Smarty 매뉴얼을 번역중이고, 최근 들어 학교 프로젝트를 수행하기 위해 파이썬을 공부하는 중이다.
지금까지 템플릿 파일들은 $template_dir에만 위치해 있었고, 이것을 읽어 들이는 방법은 다음과 같았다.
// PHP Script에서
$smarty->display(‘index.tpl’);
{* 템플릿에서 *}
{include file=’home/index.tpl’}
만일 $template_dir에 없는 템플릿들을 불러오기 위해서는 다음과 같이 절대경로를 이용해 템플릿을 가져오면 된다.
// PHP Script에서(Unix)
$smarty->display(‘/home/sizer/template/index.tpl’);
{* 템플릿에서(Windows) *}
{include file=’C:/My Website/template/index.tpl’}
앞의 표현들은 사실 웹 브라우저에서 ‘http://’를 생략하고 URL을 입력하는 것처럼 완전한 것은 아니다. 웹 브라우저들이 프로토콜 타입인 ‘http://’가 생략된 URL에 대해 기본적으로 ‘http://’라고 가정하는 것처럼, Smarty 엔진은 앞에서 생략된 리소스 타입으로 로컬 머신의 파일 시스템에 있는 파일들을 가리키는 리소스 타입인 ‘file’을 가정하고 있다. 그리고 앞의 표현들은 다음과 같이 좀더 길게 타이핑해줄 수도 있다.
$smarty->display(‘file:index.tpl’);
{include file=’file:home/index.tpl’}
$smarty->display(‘file:/home/sizer/template/index.tpl’);
{include file=’file:C:/My Website/template/index.tpl’}
리소스 플러그인 작성하기
Smarty를 사용하는 프로그래머는 파일 시스템이 아닌 데이터베이스나 LDAP, 혹은 공유 메모리 같은 저장장치에 저장할 수 있도록 별도의 리소스 플러그인을 작성하는 것이 가능하다. 그러나 다른 저장장치를 템플릿의 저장소로 사용하는 것은 아주 규모가 큰 카페와 같은 기능을 구현할 때나 고려해 볼 만한 것이고, 일반적인 성격의 웹 사이트에서는 거의 쓰일 일이 없다고 생각된다. 그런 이유로 데이터베이스에서 템플릿을 읽는 리소스 플러그인은 이 글을 다 읽고나서 독자 여러분들이 스스로 해보기를 바라며(Smarty 매뉴얼에 그렇게 실전적이지는 않은 예제가 있으니 참조하기 바란다), 이번 호에는 스킨 기능을 이용하는 웹 애플리케이션에서 필요한 템플릿들을 좀더 편리하게 지정할 수 있도록 리소스 플러그인을 추가하는 예제를 알아보도록 하겠다. <리스트 1>에 나와 있듯이 리소스 플러그인에 필요한 함수는 다른 플러그인과는 달리 4개가 필요하다.
◆ smarty_[resource_name]_template($tpl_name, &$tpl_source, &$smarty) : 템플릿 내용을 &$tpl_source를 통해 반환하는 함수, 지정한 템플릿을 찾는 데 성공하면 true를, 실패하면 false를 반환한다.
◆ smarty_[resource_name]_timestamp($tpl_name, &$tpl_source, &$smarty) : 템플릿이 만들어진 시간을 &$tpl_timestamp를 통해 반환하는 함수, 지정한 템플릿을 찾는 데 성공하면 true를, 실패하면 false를 반환한다.
◆ smarty_[resource_name]_secure($tpl_name, &$smarty) : 해당 리소스 타입이 안전한 것인지를 체크하는 함수(뒤에 나오는 보안과 관련된 함수)
◆ smarty_[resource_name]_trusted($tpl_name, &$smarty) : 해당 리소스 타입이 신뢰할 수 있는 것인지를 체크하는 함수(뒤에 나오는 보안과 관련된 함수)
<리스트 1>은 이와 같은 형식으로 작성된 리소스 플러그인을 plugins 디렉토리에서 자동으로 읽어 들이게 하는 방식이 아니라, 수동으로 register_resource() 함수로 등록시켰는데(이런 경우 플러그인 함수 이름 앞에 ‘smarty_’ 접두어를 붙일 필요가 없다), 일부 특정한 Smarty 자식 클래스에서만 사용하는 플러그인의 경우 이런 식으로 플러그인을 등록하는 방법이 종종 이용되므로 참조하기 바란다. 그리고 스킨을 순수한 템플릿만으로 만드는 것이 아니라 설정파일도 같이 사용하고 싶다면, 스킨 디렉토리의 설정파일을 로드할 수 있는 {skin_config_load} 같은 함수도 만들어 등록해줘야 할 것이다.
기본적인 캐시 사용법
사실 템플릿 리소스들은 컴파일된 이후에는 사용되지 않으므로 아무리 빠른 저장장치(예를 들어, 공유 메모리와 같은)에 저장한다고 해도 웹 사이트에서 얻는 성능상의 이익은 거의 없다고 봐야한다. 실제로 Smarty로 된 웹 애플리케이션의 성능을 개선하기 위해서는 지난 호에 배운 필터와 이번 호의 캐시 기능을 잘 활용해야 한다. 간단하게 Smarty에서 캐시를 활성화시키기 위해서는 다음과 같이 $cache_dir에 웹 서버가 읽기/쓰기가 가능하게 디렉토리의 퍼미션을 줄 필요가 있다.
// ‘/any_cache_path’는 읽기 쓰기가 가능해야 한다.
$smarty->cache_dir = ‘/any_cache_path’;
$smarty->caching을 1이나 2로 설정해주면 된다.
// 캐시가 종료되는 시간을 $cache_lifetime에 설정된 값으로 결정
$smarty->caching = 1;
// 캐시가 종료되는 시간을 캐시가 생성될 때 기록된 cache_lifetime에 의해 결정
$smarty->caching = 2;
또한 $cache_lifetime은 기본적으로 3600초(1시간)로 설정되어 있으며, 0일 경우 항상 캐시를 다시 만들어내고 -1일 경우 명시적으로 캐시를 지우지 않는 이상 영속적으로 사용된다.
$smarty->cache_lifetime = 60; // 캐시 종료시한을 1분으로 결정
$smarty->cache_lifetime = -1; // 캐시 만료시한을 두지 않는다.
$smarty->cache_lifetime = 0; // 캐시를 항상 다시 만들어낸다(테스트할 때 유용).
Smarty는 캐시를 활성화시킨 뒤 템플릿이나 설정파일에 변화가 있을 때마다 새롭게 캐시를 생성하게 되는데 $compile_check를 false로 설정하면 템플릿이나 설정파일이 바뀌어도 이전의 캐시를 그대로 사용하게 되며, 이것은 실제 사이트를 운영할 때 약간의 성능 개선 효과를 준다.
// 템플릿, 설정파일이 바뀌어도 캐시가 갱신되지 않는다.
$smarty->compile_check = false;
그럼 여기서 캐시의 종료시한을 15분으로 설정하고 템플릿과 설정파일의 변화유무를 감지할 수 있도록 설정한 <리스트 2>를 살펴보도록 하자. 얼핏 보면 <리스트 2>에는 별 문제가 없어 보인다. 하지만 이 스크립트는 성능상으로 그다지 캐시의 효과를 얻을 수 없고 사이트의 갱신된 내용을 효과적으로 반영하지도 못하는데, 그 이유는 다음과 같다.
짾 첫째, 만약 $mode가 ‘list’인 상태라면 캐시가 되었든 안되었든 항상 데이터베이스에 접속해서 결과를 가져오는 루틴이 필요하다. 데이터베이스에 저장된 내용의 변화가 없는 상태라면 이런 동작은 할 이유가 전혀 없고, 귀중한 데이터베이스 서버의 리소스만 낭비할 뿐이다.
짿 둘째, 템플릿이나 설정파일의 변화는 캐시에 반영되지만 PHP 스크립트의 변화는 캐시에 반영되지 않으므로 막상 데이터베이스에 변화가 있다고 하더라도 사이트 방문자는 왜 새로운 글이 올라가지 않는지 화를 낼 것이다(운 나쁘면 15분 정도 기다려야 새로운 글이 게시판에 확인할 수 있을 것이다).
스크립트 내에서 캐시 행동 제어하기
먼저 필요 없는 데이터베이스 연결 루틴 부문을 캐시 유무에 따라 수행여부를 결정할 수 있도록 개선해 보자. 특정한 템플릿이 캐시가 되었는지 안되었는지는 is_cached($tpl_name) 메쏘드를 통해 알아낼 수 있다. 다음과 같이 하면 캐시가 된 상태에서는 쓸데없이 데이터베이스에 접속하지 않게 될 것이다.
if (!$smarty->is_cached(‘list.tpl’)) {
// 데이터베이스에 접속해 결과를 가져온다.
}
$smarty->display(‘list.tpl’);
그 다음에 clear_cache() 메쏘드를 이용하면 해당 템플릿의 캐시된 결과를 명시적으로 지울 수 있다. <리스트 3>은 이 두 개의 메쏘드를 이용해서 기존의 캐시를 사용할지 아니면 데이터베이스에 업데이트된 내용을 반영해서 새롭게 캐시를 만들어내야 할지 처리하는 스크립트이다.
템플릿마다 별도의 캐시를 만들어내고 제어하기
<리스트 3>을 보면 $mode가 ‘list’인 경우 리스트가 길어도 그냥 한 페이지에 다 출력한다. 그러나 게시판과 같은 기능을 구현할 때는 리스트를 일정한 길이로 잘라 페이지별로 보여줄 필요가 있다. 이런 경우 $page_num과 같은 별도의 변수에 따라 출력을 달리해서 출력해야 한다.
$smarty->display(‘list.tpl’);
같은 식으로 템플릿을 출력한다면 ‘list.tpl’에 대해 오직 하나의 캐시만을 가지기 때문에 제대로 된 페이지 네비게이션을 구현할 수 없게 된다. 마찬가지로 게시판에서 특정한 글을 읽는 동작 역시 다음과 같이 하면 ‘view.tpl’에 대해 오직 하나의 캐시만을 가지므로 게시판에 접속하는 사용자들은 언제나 같은 글만을 읽게 될 것이다.
$smarty->display(‘view.tpl’);
이것을 해결하는 가장 간단한 방법은 특정한 매개변수에 따라 변동이 심한 페이지 영역에 대해서 캐시 기능을 끄는 것일 테지만, 게시판과 같은 웹 애플리케이션에서 리스트를 보여주는 부분과 글을 읽은 부분에서 캐시 기능을 끄는 것은 사실상 캐시 기능을 사용하지 않겠다는 소리나 마찬가지이다. 지금까지 $smarty->display($tpl_ name)과 같은 형식으로만 템플릿을 읽어 들여 출력했지만, 사실 display() 메쏘드의 완전한 형식은 다음과 같다.
display($tpl_name, $cache_id, $compile_id);
여기서 $cache_id 매개변수를 달리 설정하면 같은 템플릿에 대해 별도의 캐시를 만들어줄 수 있다. ? 과 같은 코드는 같은 ‘list.tpl’에 대해 각각 다른 캐시를 만들어 내는데, ? 와 같이 하면 $page_num에 따라 별도의 캐시들이 생성하게 된다.
$smarty->display(‘list.tpl’, ‘1’); → ?
$smarty->display(‘list.tpl’, ‘2’); → ?
$smarty->display(‘list.tpl’, $page_num); → ?
이렇게 생성된 별도의 캐시들을 지우기 위해서는 clear_cache()를 display()와 같은 형식으로 호출하면 된다.
$smarty->clear_cache(‘list.tpl’, $page_num);
그러나 아쉽게도 이런 방법으로 진행해도 문제는 발생한다. 만약 게시판에 새로운 글이 올라온다면, 리스트와 관련된 모든 캐시를 지워줘야 하는데, 앞과 같이 캐시를 생성했다면 다음과 같은 코드로 캐시를 하나하나 지워줘야 할 것이다.
for ($i=0; $i < $page_total; $i++) {
$smarty->clear_cache(‘list.tpl’, $i);
}
이런 식으로 하면 캐시 제어가 너무 복잡해질 가능성이 있다. 하지만 관련된 페이지들을 묶어서 $cache_id로 표현할 수 있으므로 걱정하지 말기 바란다. 게시판의 예를 계속해서 들어보면, 게시판의 리스트나 글 조회로 인해 생성되는 캐시들은 ‘|’를 이용해서 다음과 같이 그룹핑해주는 것이 가능하다(‘|’로 그룹핑하는 깊이에는 한계가 없다).
$smarty->display(‘list.tpl’, “board|list|$page_num”);
$smarty->display(‘view.tpl’, “board|view|$id”);
만일 특정한 글이 수정되었다면 리스트에는 변동이 없을 테니, “board|view|$id”에 해당하는 캐시만 새롭게 생성해 주면 될 것이다.
$smarty->clear_cache(‘view.tpl’, “board|view|$id”);
그러나 새로운 글이 올라왔다면 리스트 전체에 해당하는 캐시를 갱신해 줄 필요가 있는데, 다음과 같이 “board|list|$page_num”에서 $page_num을 생략한 채 캐시를 clear_cache()를 호출해 주면 자동으로 ‘board|list’에 속하는 모든 캐시들이 만료된다.
$smarty->clear_cache(‘list.tpl’, ‘board|list’);
또한 게시판 전체의 모든 캐시를 깨끗이 지우고 싶다면 clear_all_cache()를 사용해도 어느 정도 비슷한 효과를 낼 수 있겠지만, 그것보다는 다음처럼 $tpl_name을 null로 지정한 채 clear_cache()를 호출하면 list.tpl과 view.tpl과 상관없이 ‘board’에 속하는 모든 하위 캐시들을 만료시키게 된다.
$smarty->clear_cache(null, ‘borad’);
캐시 핸들러 제작과 등록하기
앞에서 리소스들은 아무리 빠른 장치에 저장하더라도 그로부터 발생하는 성능상의 이득은 그렇게 크지 않다고 했다. 하지만 캐시의 경우는 저장하고자 하는 장치가 빠르면 빠를수록 좋다. 여건만 된다면 모든 캐시된 내용들을 메모리에 저장해 보는 것도 생각할 수 있다. 로컬 파일 시스템이 아닌 다른 저장소를 캐시로 이용하기 위해서는 $cache_handler_func에 적당한 캐시 핸들러 함수를 만들어 등록시키면 된다.
$smarty->cache_handler_func = ‘database_cache_handler’;
등록할 캐시 핸들러 함수의 레이아웃은 다음과 비슷한 모습으로 작성하게 된다.
function any_cache_handler($action, &$smarty, &$cache_content,
$tpl_file = null,
$cache_id = null,
$compile_id = null) {
switch ($action) {
case ‘read’: // 캐시로부터 캐시된 내용을 읽는 루틴
case ‘write’: // 캐시에 새로운 캐시 내용을 쓰는 루틴
case ‘clear’: // 캐시 내용을 삭제하는 루틴
default: // ‘read’, ‘write’, ‘clear’이외의 행동은 에러이다.
}
}
<리스트 5>는 Smarty 메뉴얼에 있는 MySQL용 캐시 핸들러 예제인데, 이것은 앞에서 소개한 캐시 그룹핑 기능을 지원하지 못한다. 따라서 앞에서 언급한 대로 캐시들을 하나하나 제거하는 루틴을 구현해 주거나 좀더 기능을 추가할 필요가 있다. $CacheId 이외에 $tpl_file, $cache_id, $compile_id를 나타내는 컬럼을 테이블에 추가한 뒤 그에 맞는 쿼리문을 수행하는 루틴을 핸들러에 삽입하면 될 텐데, 이것은 다음 시간까지 독자 여러분들의 숙제로 남기도록 하겠다(기본 캐시 핸들러의 알고리즘을 살펴보려면 ‘Smarty.class.php’를 참조하면 된다).
Smarty를 안전하게 실행시키기
Smarty는 템플릿 안에 {php}..{/php}나 {include_php}..{/include_ php}를 통해 PHP 코드들이 삽입돼 실행될 수 있다. 그리고 말한 적은 없지만 {if}..{/if} 동적 블럭문이나 변수 변환자에서 일반 PHP 함수를 사용할 수도 있다.
{if count($array) > 1}
..
{/if}
즉, 템플릿 내에서 이와 같은 문장을 사용할 수 있다는 의미인데, 여기서 물론 count는 템플릿 함수가 아닌 PHP 함수 count를 가리킨다. 이런 특징들은 잘만 사용되면 나름대로 편리함을 줄 수도 있겠지만, 편리함을 위해 여러분의 웹 사이트의 안전을 포기할 수는 없는 법이다. Smarty는 보안에 민감한 영향을 줄 수 있는 특징들을 템플릿에서 사용할 수 없도록 하거나, 또 신뢰할 수 있는 위치에 있는 템플릿만을 처리할 수 있도록 멤버 변수들을 설정해 줄 수 있다. 먼저 가장 기본적인 보안 관련 멤버 변수에는 $security가 있는데, 이것을 true로 하면 기본적으로 Smarty 템플릿 엔진은 다음과 같이 행동하게 된다(다음에 나오는 행동들은 $security_setting에 의해 조정될 수 있다).
◆ $secure_dir에 설정된 디렉토리들 밑에 있는 템플릿들만을 사용할 수 있다. (display(), fetch() 메소드와 {include} 템플릿 함수 등에서 이 값을 체크한다)
◆ 만일 $php_handling가 SMARTY_PHP_ALLOW로 설정되어 있으면, 이 값은 묵시적으로 SMARTY_PATH_PATHTHRU로 교체된다.
◆ {php}..{/php} 템플릿 함수는 허용되지 않는다.
◆ {if}..{/if} 동적 블럭문에서 $security_setting에서 허용하는 함수를 제외한 일반 PHP 함수를 사용할 수 없다.
◆ 변수 변환자에서 $security_setting에서 허용하는 함수를 제외한 일반 PHP 함수를 사용할 수 없다.
앞에 나오는 멤버 변수들을 하나하나 살펴보도록 하자. 우선 $secure_dir의 값은 허용할 템플릿 디렉토리를 다음과 같이 설정하면 되는데, 허용할 템플릿 디렉토리가 많다면 배열로 설정해주면 된다.
$smarty->template_dir = ‘/home/sizer/public_html/source/template’;
$smarty->compile_dir = ‘/tmp/site/localhost/compile’;
$smarty->security = true;
$smarty->secure_dir = ‘/home/sizer/public_html/source/template’;
$smarty->display(‘/home/sizer/public_html/source/not_allowed_template/index.tpl’);
$smarty->display(‘/home/sizer/public_html/source/template/index.tpl’);
허용되지 않는 템플릿을 포함하는 부분에서 다음과 같은 메시지를 볼 수 있다.
◆ PHP Warning: Smarty error : (secure mode) accessing “/home/sizer/ public_html/source/not_allowed_template/index.tpl” is not allowed in /usr/local/share/php/smarty/Smarty.class.php on line 595
두 번째로 $php_handling 멤버변수는 템플릿 내에 삽입된 {php}..{/php} 문장을 어떻게 처리할지 결정하는 함수로서 다음과 같이 네 가지 동작을 따른다.
짾 SMARTY_PHP_PASSTHRU : 해당 태그 내용을 있는 그대로 출력한다.
짿 SMARTY_PHP_QUOTE : 해당 태그 내용을 HTML 태그로 감싸 출력한다.
쨁 SMARTY_PHP_REMOVE : 해당 태그 내용을 삭제한다.
쨂 SMARTY_PHP_ALLOW : 태그 안에 있는 PHP 코드를 실행한다.
다음으로 $security_setting 멤버변수는 $security가 true로 설정된 상태에서 보안 수준을 결정할 수 있는 배열로서 다음과 같은 요소들을 가지고 있다.
◆ INCLUDE_ANY : true로 설정하면 $secure_dir에 있는 값을 무시한다. 기본 값은 false이다.
◆ PHP_TAGS : true로 설정하면 템플릿 내에서 {php}..{/php}를 사용할 수 있다. 기본 값은 false이다.
◆ PHP_HANDLING : true로 설정하면 $php_handling에 있는 값을 체크하지 않고 PHP 코드를 항상 실행한다.
◆ IF_FUNCS : {if}..{/if} 문에서 사용할 수 있는 함수들을 설정하며, 기본적으로 허용되는 함수로는 ‘array’, ‘list’, ‘isset’, ‘empty’, ‘count’, ‘sizeof’, ‘in_array’, ‘is_array’가 있다.
◆ MODIFIER_FUNCS : 변수 변환자에서 사용할 수 있는 함수들을 설정하며, 기본적으로 허용되는 함수는 ‘count’이다.
그리고 $trusted_dir이라는 멤버변수가 있는데, 이 변수로 설정한 디렉토리 밑에 있는 템플릿들은 {include_php}를 통해 PHP 코드를 실행할 수 있게 되는데, $secure_dir과 $trusted_dir에 같은 디렉토리가 중복되어 있다면 $secure_dir이 우선권을 가진다.
다음에는 스킨 메모장 제작
이번 호에는 리소스 플러그인과 캐시 제어법, 그리고 보안과 관련된 멤버변수들에 대해 알아보았다. 여러분들은 이제 Smarty 템플릿 엔진에 대해 대부분의 이론적인 지식을 쌓은 셈인데, 막상 Smarty를 이용해서 웹 사이트를 어떻게 구축할 것인가에 대해 고민이 될 것이다. 사실 필자도 아직까지 짙은 안개 속에서 방황하는 감이 없지 않았는데, 아무래도 Smarty를 이용한 실전 코드들이 아직 그렇게 많이 살펴보지 않은 탓이라고 생각한다(Smarty를 이용한 사이트나 웹 애플리케이션은 아직까지 그렇게 많지 않은 편이기도 하다). 약속한 커리큘럼을 따르려면 다음 호에는 Smarty를 이용해서 간단한 스킨 메모장을 만들어 봐야겠지만, 사정이 된다면 PEAR와 Smarty를 이용한 좀 더 큰 규모의 웹 사이트 구축 방법을 소개할지도 모르겠다.
|
|
0