[perf]html_purifier() 메모리 사용량 최적화

한 요청에서 html_purifier()가 N번 호출되면 매번 약 10~15MB의 HTMLPurifier
인스턴스를 새로 생성하여, 게시글 목록처럼 반복 호출되는 페이지에서
수백 MB~GB 단위로 메모리가 낭비되었음.

- HTMLPurifier 인스턴스를 요청 단위로 캐싱 (admin/normal 2개 변형만 유지)
- safeiframe.txt 파일 I/O + 파싱 결과도 요청 단위로 캐싱
- G5_HTMLPURIFIER_NO_CACHE 상수로 캐싱 opt-out 가능
  ($html 내용에 따라 config를 동적 변경하는 플러그인 호환용)

동작 변경:
- html_purifier_config / html_purifier_safeiframes hook은 캐시 미스 시점에만
  실행됨 (요청당 최대 2회). 정적 config를 설정하는 일반적인 hook에는 영향 없음.
- html_purifier_result hook은 매 호출마다 정상 실행됨.

효과 (게시글 목록 30건 + 첨부파일 평균 3개 = 120회 호출 기준):
  기존 ~1.8GB → 개선 후 ~15MB
This commit is contained in:
thisgun
2026-04-16 01:36:59 +00:00
parent 4ebdd8ba30
commit 92a052fa7a
+50 -11
View File
@@ -645,20 +645,51 @@ function html_purifier($html)
{
global $is_admin, $write;
$f = file(G5_PLUGIN_PATH . '/htmlpurifier/safeiframe.txt');
$domains = array();
foreach ($f as $domain) {
// 첫행이 # 이면 주석 처리
if (!preg_match("/^#/", $domain)) {
$domain = trim($domain);
if ($domain) {
array_push($domains, $domain);
// 요청 단위로 HTMLPurifier 인스턴스를 캐싱한다.
// HTMLPurifier 인스턴스 하나는 약 10~15MB의 메모리를 사용하므로,
// 같은 요청 내에서 여러 번 호출되는 경우(예: 게시글 목록의 첨부파일 설명)
// 매번 새 인스턴스를 만드는 것은 메모리 사용량을 수십~수백 MB까지 증가시킨다.
//
// config에 영향을 주는 변수는 "글쓴이가 관리자인지" 하나뿐이므로
// 캐시 키를 admin/normal 두 가지로 분리하여 최대 2개의 인스턴스만 유지한다.
//
// 캐싱이 동작 변경을 일으키는 경우(html_purifier_config / html_purifier_safeiframes
// hook이 $html 내용에 따라 동적으로 config를 바꾸는 플러그인을 사용하는 경우)
// G5_HTMLPURIFIER_NO_CACHE 상수를 정의하여 비활성화할 수 있다.
static $purifier_cache = array();
static $domains_cache = null;
$cache_enabled = !(defined('G5_HTMLPURIFIER_NO_CACHE') && G5_HTMLPURIFIER_NO_CACHE);
$is_admin_post = (isset($write['mb_id']) && $write['mb_id'] && is_admin($write['mb_id']));
$cache_key = $is_admin_post ? 'admin' : 'normal';
// 캐시 적중 시 인스턴스 재사용
if ($cache_enabled && isset($purifier_cache[$cache_key])) {
$purifier = $purifier_cache[$cache_key];
return run_replace('html_purifier_result', $purifier->purify($html), $purifier, $html);
}
// safeiframe.txt 파싱 결과를 요청 단위로 캐싱 (파일 I/O + 정규식 비용 절감)
if ($domains_cache === null) {
$domains_cache = array();
$f = @file(G5_PLUGIN_PATH . '/htmlpurifier/safeiframe.txt');
if ($f !== false) {
foreach ($f as $domain) {
// 첫행이 # 이면 주석 처리
if (preg_match("/^#/", $domain)) continue;
$domain = trim($domain);
if ($domain !== '') {
$domains_cache[] = $domain;
}
}
unset($f);
}
}
$domains = $domains_cache;
// 글쓴이가 관리자인 경우에만 현재 사이트 도메인을 허용
if (isset($write['mb_id']) && $write['mb_id'] && is_admin($write['mb_id'])) {
array_push($domains, $_SERVER['HTTP_HOST'] . '/');
if ($is_admin_post) {
$domains[] = $_SERVER['HTTP_HOST'] . '/';
}
$safeiframe = implode('|', run_replace('html_purifier_safeiframes', $domains, $html));
@@ -682,7 +713,10 @@ function html_purifier($html)
/*
* HTMLPurifier 설정을 변경할 수 있는 Event hook
* 리스너에서는 첫번째 인자($config)로 `HTMLPurifier_Config` 객체를 받을 수 있다
* 리스너에서는 첫번째 인자($config)로 `HTMLPurifier_Config` 객체를 받을 수 있다.
* NB: 인스턴스 캐싱이 활성화된 경우(기본값) 이 hook은 캐시 미스 시점에만 실행됨
* (요청당 최대 2회). $html 내용에 따라 동적으로 config를 변경해야 한다면
* G5_HTMLPURIFIER_NO_CACHE 상수를 정의해 캐시를 비활성화하라.
*/
run_event('html_purifier_config', $config, array(
'html' => $html,
@@ -697,6 +731,11 @@ function html_purifier($html)
$purifier = new HTMLPurifier($config);
// 인스턴스 캐싱
if ($cache_enabled) {
$purifier_cache[$cache_key] = $purifier;
}
return run_replace('html_purifier_result', $purifier->purify($html), $purifier, $html);
}