1장. 캐싱 전략 개요
DXCMS의 캐싱은 DxCache(v5.0.0) 클래스 하나로 통합 관리됩니다. Redis, APCu, 파일 캐시를 자동으로 감지하여 최선의 드라이버를 선택합니다. 개발자는 드라이버를 신경 쓰지 않고 DxCache::get/set/delete 세 가지 메서드만 사용합니다.
1.1 캐싱이 필요한 이유
// 캐싱 없을 때 — 매 요청마다 DB 쿼리 실행
GET /free (게시판 목록)
→ SELECT * FROM dx_settings (설정 로드)
→ SELECT * FROM dx_boards WHERE key=free (게시판 정보)
→ SELECT posts FROM dx_posts ... (게시글 목록)
→ SELECT COUNT(*) FROM dx_posts ... (전체 수)
→ SELECT * FROM dx_categories ... (카테고리)
합계: 5개 이상의 DB 쿼리
// 캐싱 있을 때 — 첫 요청 이후 캐시 히트
GET /free (게시판 목록, 비로그인 사용자)
→ DxCache::get("board_list_free_1") (캐시 히트!)
→ SELECT * FROM dx_global_notices ... (실시간 공지만 쿼리)
합계: 1개 쿼리 (DB 쿼리 80% 절감)
1.2 드라이버 선택 우선순위
| 순위 | 드라이버 | 활성화 조건 | 특징 |
| 1 | Redis | REDIS_SESSION_URL 상수 + Redis 확장 + 연결 성공 | 멀티서버 공유, 원자적 연산, TTL 자동 만료, 가장 빠름 |
| 2 | APCu | apcu_fetch() 존재 + apc.enabled=1 | PHP-FPM 공유 메모리, 파일 I/O 없음, 단일 서버 |
| 3 | 파일 | data/cache/ 폴더 쓰기 가능 | 저가형 호스팅 기본, 원자적 쓰기(tmp→rename), 범용 |
| 4 | none | 위 모두 실패 | 캐시 없이 동작. 기능은 유지되나 성능 저하 |
💡 드라이버 자동 선택의 의미
개발자가 환경을 신경 쓰지 않아도 됩니다.
공유호스팅: 파일 캐시로 자동 동작
VPS/클라우드: Redis 설치 후 상수 정의만 하면 자동 전환
멀티서버: Redis로 모든 서버가 동일 캐시 공유
드라이버 전환 시 코드 변경 불필요 — DxCache::set/get 그대로 사용
2장. DxCache 클래스 상세
2.1 초기화 (init)
최초 메서드 호출 시 자동으로 드라이버를 감지하고 초기화합니다. 이후 호출부터는 감지 로직을 건너뜁니다.
// 초기화 흐름 (최초 1회만 실행)
// 1. Redis 시도
REDIS_SESSION_URL 상수 존재?
└ Redis 익스텐션(class_exists("Redis")) 존재?
└ connect(host, port, timeout=2.0)
└ auth($password) (비밀번호 있을 때)
└ select($db_no) (DB 번호 있을 때)
└ ping() 연결 확인
└ 성공: $driver = "redis"
└ 실패: 다음으로
// 2. APCu 시도
apcu_fetch() 함수 + apc.enabled = 1
└ 조건 충족: $driver = "apcu"
// 3. 파일 캐시 시도
DX_DATA 상수 존재?
└ data/cache/ 폴더 생성 (없으면)
└ 폴더 쓰기 가능?
└ 가능: $driver = "file", $cacheDir = ".../data/cache"
└ 불가: $driver = "none"
2.2 캐시 키 안전화
// 모든 메서드 진입 시 키를 안전하게 변환
$safeKey = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $key);
// 변환 예시
"board_list_free_1" → "board_list_free_1" (변경 없음)
"dx_settings" → "dx_settings" (변경 없음)
"cat_board_5" → "cat_board_5" (변경 없음)
"site_mysite.com" → "site_mysite_com" (점 → 밑줄)
"user:123:profile" → "user_123_profile" (콜론 → 밑줄)
// Redis 키에는 "dxc:" 접두사 추가
"dx_settings" → Redis 키: "dxc:dx_settings"
"board_list_free_1" → Redis 키: "dxc:board_list_free_1"
// → 다른 앱과 Redis를 공유해도 충돌 없음
2.3 set() — 캐시 저장
// DxCache::set($key, $value, $ttl)
// $key: 캐시 키 (문자열)
// $value: 저장할 값 (모든 PHP 타입 — serialize 직렬화)
// $ttl: 유효 시간(초). 0 = 영구 저장
// Redis
ttl > 0: SETEX dxc:{key} {ttl} {serialize($value)}
ttl = 0: SET dxc:{key} {serialize($value)} (영구)
// APCu
apcu_store($safeKey, $value, $ttl)
// apcu는 직렬화 불필요 — PHP 값 직접 저장
// 파일
$expire = time() + $ttl; // ttl=0이면 PHP_INT_MAX (사실상 영구)
$data = serialize(["expire"=>$expire, "data"=>$value]);
// 원자적 쓰기: tmp 파일에 먼저 쓰고 rename으로 교체
$tmp = "{$file}.tmp.{getmypid()}";
file_put_contents($tmp, $data);
rename($tmp, $file); // 원자적 교체 → 동시 쓰기 충돌 방지
💡 원자적 파일 쓰기(tmp → rename)가 중요한 이유
파일에 직접 쓰는 도중 다른 프로세스가 읽으면 불완전한 데이터를 읽을 수 있습니다.
tmp 파일에 먼저 완전히 쓴 후 rename()으로 교체하면
읽기 프로세스는 항상 완전한 데이터 또는 이전 데이터만 읽습니다.
→ PHP-FPM 멀티 프로세스 환경에서 캐시 파일 손상 방지
2.4 get() — 캐시 조회
// DxCache::get($key, $default)
// $default: 캐시 없거나 만료 시 반환값 (기본 null)
// Redis
$data = $redis->get("dxc:{key}");
// false 반환 = 캐시 미스 → $default 반환
// 정상값 = unserialize($data) 후 반환
// APCu
$val = apcu_fetch($safeKey, $success);
// $success=false = 캐시 미스 → $default 반환
// 파일
// 파일 없음 → $default 반환
// 파일 있음 → unserialize → expire 확인
// expire < time() → unlink(파일 삭제) → $default 반환
// expire >= time() → $arr["data"] 반환 (캐시 히트)
2.5 delete() — 캐시 삭제
// DxCache::delete($key)
// 특정 키 하나 삭제
DxCache::delete("dx_settings"); // 설정 캐시 삭제
DxCache::delete("board_list_free_1"); // 특정 게시판 1페이지 캐시
// Redis: DEL dxc:{key}
// APCu: apcu_delete($safeKey)
// 파일: unlink("{cacheDir}/{safeKey}.cache")
2.6 deletePrefix() — 접두어 일괄 삭제
// DxCache::deletePrefix($prefix)
// 접두어로 시작하는 모든 캐시 삭제
// 게시글 작성/수정/삭제 시 해당 게시판 전체 페이지 캐시 무효화에 사용
DxCache::deletePrefix("board_list_free_");
// → board_list_free_1, board_list_free_2, board_list_free_3 ... 모두 삭제
// Redis: SCAN 방식 (KEYS 명령 사용 안 함)
// KEYS 명령은 Redis를 순간 블로킹 → 프로덕션에서 위험
// SCAN은 커서 기반 분할 순회 → 블로킹 없음
do {
[$cursor, $keys] = $redis->scan($cursor, "dxc:{prefix}*", 100);
if ($keys) $redis->del($keys);
} while ($cursor != 0);
// APCu: 패턴 삭제 미지원 → 무시 (APCu 자체 TTL로 자연 만료)
// 파일: glob("{cacheDir}/{prefix}*.cache") → 각 파일 unlink
2.7 flush() — 전체 초기화
// DxCache::flush()
// 모든 CMS 캐시 삭제 (소켓 키 갱신, 관리자 긴급 초기화 시 사용)
// Redis: FLUSHDB 대신 SCAN+DEL("dxc:*")
// → Redis를 다른 앱과 공유할 때 타 앱 캐시 삭제 방지
// APCu: apcu_clear_cache()
// 파일: glob("*.cache") → 전체 삭제
// 사용 사례
// 1. 소켓 API 키 갱신 시 (receive_socket_key.php)
DxCache::flush();
// 2. 관리자 → 캐시 초기화 버튼
DxCache::flush();
echo "캐시가 초기화되었습니다.";
2.8 getDriver() — 현재 드라이버 확인
// DxCache::getDriver()
// 현재 활성 드라이버 반환: "redis" | "apcu" | "file" | "none"
$driver = DxCache::getDriver();
echo "캐시 드라이버: " . $driver;
// 관리자 대시보드에서 현재 캐시 상태 표시
if ($driver === "redis") {
echo "✅ Redis 캐시 (최고 성능)";
} elseif ($driver === "apcu") {
echo "✅ APCu 캐시 (고성능)";
} elseif ($driver === "file") {
echo "⚠️ 파일 캐시 (기본)";
} else {
echo "❌ 캐시 없음 (성능 저하)";
}
3장. 시스템 내장 캐시 패턴
DXCMS 핵심 로직에서 DxCache를 사용하는 실제 패턴을 모두 분석합니다. 각 캐시가 언제 생성되고 언제 무효화되는지 파악하면 성능 최적화와 디버깅에 도움이 됩니다.
3.1 시스템 설정 캐시 (dx_settings)
사이트의 모든 설정값을 담는 핵심 캐시입니다. data/config.php가 실행될 때마다 캐시 여부를 확인하여 DB 쿼리를 줄입니다.
// data/config.php 내 설정 로드 패턴
// 1순위: 캐시에서 로드
if (class_exists("DxCache")) {
$_cached = DxCache::get("dx_settings", null);
if (is_array($_cached) && !empty($_cached)) {
$dx_config = $_cached; // 캐시 히트 → DB 쿼리 없음
}
}
// 2순위: 캐시 미스 시 DB에서 로드
if (empty($dx_config)) {
$rows = $db->rows("SELECT setting_key, setting_value FROM dx_settings");
foreach ($rows as $s) {
$dx_config[$s["setting_key"]] = $s["setting_value"];
}
// 정상 로드된 경우에만 캐시 저장 (빈 배열은 캐시 금지)
if (!empty($dx_config)) {
DxCache::set("dx_settings", $dx_config, 300); // 5분 캐시
}
}
// 무효화 시점: 관리자에서 설정 저장 시
DxCache::delete("dx_settings");
| 항목 | 값 | 설명 |
| 캐시 키 | dx_settings | 전체 설정을 하나의 배열로 저장 |
| TTL | 300초 (5분) | 5분마다 DB 재조회로 최신 설정 반영 |
| 무효화 | DxCache::delete("dx_settings") | 관리자 설정 저장 시 즉시 삭제 |
| 절감 효과 | DB 쿼리 1개/요청 제거 | 100명 동시 접속 시 초당 수십 쿼리 절감 |
3.2 게시판 목록 캐시 (board_list_{key}_{page})
비로그인 사용자의 게시판 목록 조회를 캐싱합니다. 가장 큰 성능 효과를 내는 캐시입니다. 로그인 사용자, 검색, 카테고리 필터 적용 시에는 캐시를 사용하지 않습니다.
// boards/handler.php — 게시판 목록 캐시 조건
// 캐시 사용 조건 (모두 충족해야 함)
$_brdCacheOk = (
class_exists("DxCache") &&
!$search && // 검색어 없음
!$cat && // 카테고리 필터 없음
!$tag && // 태그 필터 없음
!$auth->isLoggedIn() && // 비로그인 사용자
$action !== "search" // 검색 액션 아님
);
// 캐시 키 생성
$_brdCacheKey = "board_list_{$board["board_key"]}_{$page}";
// 예: "board_list_free_1", "board_list_free_2"
// 캐시 조회
if ($_brdCacheOk) {
$_brdCached = DxCache::get($_brdCacheKey, null);
if (is_array($_brdCached)) {
extract($_brdCached, EXTR_SKIP); // 캐시 히트
// 전체 공지는 기간 체크 필요 → 캐시 제외, 매번 DB 조회
$globalNotices = $db->rows("SELECT * FROM dx_global_notices WHERE ...");
_brd_render("list", $ctx);
break;
}
}
// 캐시 저장 (DB 조회 완료 후)
if (isset($_brdCacheOk) && $_brdCacheOk) {
DxCache::set($_brdCacheKey, array(
"posts" => $posts,
"notices" => $notices,
"total" => $total,
"categories" => $categories,
), 60); // 60초 캐시
}
| 항목 | 값 | 설명 |
| 캐시 키 패턴 | board_list_{게시판키}_{페이지} | 예: board_list_free_1, board_list_notice_2 |
| TTL | 60초 | 게시글 작성/수정 후 최대 60초 이내 반영 |
| 캐시 제외 | 로그인/검색/카테고리/태그 | 개인화된 콘텐츠는 캐시 제외 |
| 무효화 | deletePrefix("board_list_{key}_") | 글 작성/수정/삭제/댓글 시 해당 게시판 전체 삭제 |
| 캐시 제외 항목 | globalNotices (전체 공지) | 게시 기간 실시간 체크 필요 → 항상 DB 조회 |
💡 로그인 사용자를 캐시 제외하는 이유
로그인 사용자는 권한(level, role)에 따라 볼 수 있는 게시글이 다를 수 있습니다.
또한 "내가 쓴 글" 표시, 읽은 글 표시 등 개인화 요소가 포함됩니다.
같은 URL이라도 사용자마다 다른 렌더링이 필요하므로 공유 캐시 불가.
→ 트래픽의 대부분인 비로그인 방문자만 캐싱해도 큰 효과.
3.3 카테고리 캐시 (cat_board_{board_id})
// DxCategory::getFlatByBoard($boardId, $slug)
$cacheKey = "cat_board_" . (int)$boardId;
$cached = DxCache::get($cacheKey, null);
if ($cached === null) {
// DB에서 카테고리 조회 + 계층 정렬
$rows = $db->rows("SELECT * FROM dx_categories WHERE board_id=? ...", [$boardId]);
DxCache::set($cacheKey, $rows, 300); // 5분 캐시
return $rows;
}
return $cached;
// 무효화: 관리자 카테고리 수정 저장 시
DxCache::deletePrefix("cat_board_"); // 전체 게시판 카테고리 초기화
3.4 멀티사이트 캐시 (site_{domain})
// DxSite::getInstance() 내부
$cacheKey = "site_" . md5($domain); // 도메인 → MD5 키
$cachedSite = DxCache::get($cacheKey, "MISS"); // 기본값 "MISS"로 null 캐시 구분
if ($cachedSite === "MISS") {
// DB에서 사이트 설정 조회
$site = $db->row("SELECT * FROM dx_sites WHERE domain=?", [$domain]);
DxCache::set($cacheKey, $site, 300); // 5분 캐시 (null도 캐싱)
// null 캐싱: DB에 없는 도메인도 매번 쿼리 안 함
}
// 무효화: 관리자 멀티사이트 설정 저장 시
DxCache::deletePrefix("site_");
💡 "MISS" 센티넬 패턴
DxCache::get()의 기본값을 null 대신 "MISS"로 설정하면
null이 실제 캐시 값인지 캐시 미스인지 구분할 수 있습니다.
예: 존재하지 않는 도메인 → DB 결과 null → null을 캐싱
다음 요청: get()이 null 반환 → "MISS"와 다름 → 캐시 히트로 판단
→ DB 쿼리 없이 "없는 도메인"임을 알 수 있음 (Negative Caching)
3.5 팝업 캐시 (popup_active_{date})
// DxPopup::render() 내부
$today = date("Ymd");
$cacheKey = "popup_active_" . $today;
$cached = DxCache::get($cacheKey, null);
if ($cached === null) {
$now = date("Y-m-d H:i:s");
$rows = $db->rows(
"SELECT * FROM dx_popups WHERE status=1",
" AND (start_date IS NULL OR start_date <= :d)",
" AND (end_date IS NULL OR end_date >= :d) ORDER BY sort_order"
);
DxCache::set($cacheKey, $rows, 60); // 60초 캐시
}
// 키에 날짜 포함: 자정이 지나면 자동으로 새 키 생성
// → 날짜별 팝업이 자동으로 전환됨
// 무효화: 관리자 팝업 저장 시
DxCache::delete("popup_active_" . date("Ymd"));
3.6 회원 관련 캐시
| 캐시 키 패턴 | TTL | 내용 및 무효화 |
| auth_remember_cols | 3600초 | remember_token 컬럼 존재 여부. SHOW COLUMNS 결과 캐싱. 컬럼 추가 후 관리자 캐시 초기화 필요 |
| dx_member_cols_{hash} | 3600초 | dx_members 테이블 컬럼 목록. 프로필 저장 시 컬럼 존재 체크용 |
| friend_cnt_{memberId} | 60초 | 특정 회원의 친구 수. 친구 추가/삭제 시 자동 만료 |
| unread_memo_{memberId} | 30초 | 미읽음 쪽지 수. 30초마다 갱신으로 실시간성 확보 |
| mon_table_ready | 3600초 | dx_member_sessions 테이블 존재 여부. 설치 여부 1회 확인 |
3.7 사이트맵 캐시
// /api/sitemap 처리 시
$_cacheKey = "sitemap_" . md5($host);
$_cached = DxCache::get($_cacheKey, null);
if ($_cached === null) {
// 사이트맵 XML 생성 (게시글 수백~수천 개 조회)
$_xml = "<?xml version='1.0'...";
DxCache::set($_cacheKey, $_xml, 600); // 10분 캐시
}
// 사이트맵은 생성 비용이 크고 자주 바뀌지 않으므로 10분 캐시
// 무효화: 글 작성/삭제 시 또는 관리자 캐시 초기화 시
DxCache::deletePrefix("sitemap_");
4장. 캐시 무효화 전략
캐시의 가장 어려운 문제는 "언제 지울 것인가"입니다. DXCMS는 이벤트 기반 즉시 무효화 방식을 사용합니다.
4.1 이벤트별 무효화 목록
| 이벤트 | 무효화 코드 |
| 게시글 작성 (write) | DxCache::deletePrefix("board_list_{board_key}_") |
| 게시글 수정 (edit) | DxCache::deletePrefix("board_list_{board_key}_") |
| 게시글 삭제 (delete) | DxCache::deletePrefix("board_list_{board_key}_") |
| 댓글 작성/삭제 | DxCache::deletePrefix("board_list_{board_key}_") |
| 관리자 설정 저장 | DxCache::delete("dx_settings") |
| 관리자 카테고리 수정 | DxCache::deletePrefix("cat_board_") |
| 관리자 팝업 수정 | DxCache::delete("popup_active_" . date("Ymd")) |
| 소켓 API 키 갱신 | DxCache::flush() — 전체 초기화 |
| 관리자 캐시 초기화 버튼 | DxCache::flush() — 전체 초기화 |
4.2 무효화 시점별 전략
// 전략 1: 쓰기 시 즉시 무효화 (Write-Invalidate)
// 가장 간단하고 안전한 방식
// 다음 읽기 시 DB에서 최신 데이터 조회 후 재캐싱
// 게시글 작성 후
$db->query("INSERT INTO dx_posts ...");
DxCache::deletePrefix("board_list_{$boardKey}_"); // 즉시 무효화
// 전략 2: TTL 자연 만료 (Time-Based Expiry)
// 데이터 변경이 드물고 약간의 지연이 허용될 때
// 팝업(60초), 사이트맵(600초) 등
// 전략 3: Negative Caching
// 존재하지 않는 데이터도 캐싱 (DB 쿼리 반복 방지)
// 멀티사이트에서 없는 도메인 → null 캐싱
DxCache::set($cacheKey, null, 300); // null을 300초 캐싱
5장. 드라이버별 상세 동작
5.1 Redis 드라이버
// 설정 (data/config.php에 추가)
define('REDIS_SESSION_URL', 'tcp://127.0.0.1:6379');
// 비밀번호 사용 시
define('REDIS_SESSION_URL', 'tcp://127.0.0.1:6379?auth=비밀번호');
// 특정 DB 번호 사용 시 (0~15)
define('REDIS_SESSION_URL', 'tcp://127.0.0.1:6379/2'); // DB 2번
// Redis 키 구조
"dxc:dx_settings" → 설정 캐시
"dxc:board_list_free_1" → 게시판 목록 1페이지
"dxc:cat_board_5" → 게시판 5번 카테고리
"dx:ban:1.2.3.4" → IP 차단 (Rate Limit, WAF)
"rl:login:1234567890" → Rate Limit 슬롯
// 직접 확인 방법
redis-cli KEYS "dxc:*" // 모든 캐시 키 조회
redis-cli GET "dxc:dx_settings" // 설정 캐시 값 조회
redis-cli TTL "dxc:dx_settings" // 남은 만료 시간(초)
redis-cli DEL "dxc:dx_settings" // 직접 삭제
⚠️ Redis 운영 주의사항
maxmemory 설정: Redis 메모리가 가득 차면 LRU(Least Recently Used) 방식으로 자동 삭제.
→ 갑작스러운 캐시 미스가 발생할 수 있음. maxmemory-policy allkeys-lru 권장
Redis 재시작: 메모리 캐시이므로 재시작 시 모든 캐시 사라짐.
→ 자동으로 DB에서 재로드됨. 일시적 성능 저하만 발생
Redis FLUSHALL 금지: DxCache::flush()는 dxc:* 키만 삭제. FLUSHALL은 세션 등 모두 삭제.
5.2 APCu 드라이버
// APCu 활성화 (php.ini 또는 php.d/apcu.ini)
extension=apcu
apc.enabled=1
apc.shm_size=128M ; 공유 메모리 크기 (기본 32M, 운영은 128M+ 권장)
// APCu 특징
// - PHP-FPM 워커 프로세스 간 공유 메모리
// - 파일 I/O 없음 → Redis에 버금가는 속도
// - 단일 서버 전용 (멀티서버 공유 불가)
// - PHP-FPM 재시작 시 캐시 사라짐
// APCu 상태 확인
var_dump(apcu_cache_info()); // 캐시 통계
// 주의: APCu는 deletePrefix 미지원
// → DxCache::deletePrefix() 호출 시 APCu 드라이버에서는 무시됨
// → TTL 만료로 자연 정리에 의존
// → 게시글 작성 후 최대 60초 내 반영 (TTL 만료 시)
5.3 파일 캐시 드라이버
// 파일 위치: data/cache/
// 파일 형식: {safeKey}.cache
// 파일 내용 (직렬화된 배열)
a:2:{s:6:"expire";i:1746957742;s:4:"data";a:3:{...}}
// 실제 파일 예시
data/cache/dx_settings.cache
data/cache/board_list_free_1.cache
data/cache/board_list_free_2.cache
data/cache/cat_board_5.cache
// 원자적 쓰기 확인
data/cache/dx_settings.cache.tmp.12345 // 쓰기 중인 임시 파일
// 쓰기 완료 후 rename → dx_settings.cache
// 파일 캐시 수동 초기화
rm -f data/cache/*.cache
// 또는 PHP에서
DxCache::flush();
// 주의: data/cache/ 폴더 권한
// chmod 755 data/cache/ (PHP 웹서버 사용자가 쓰기 가능해야 함)
6장. 캐시 적용 방법 — 플러그인•테마 개발자용
6.1 기본 Cache-Aside 패턴
가장 일반적인 캐싱 패턴입니다. 캐시를 먼저 확인하고, 없으면 DB에서 가져와서 캐시에 저장합니다.
// Cache-Aside (Lazy Loading) 패턴
function getPopularPosts($boardId, $limit = 10) {
$cacheKey = "popular_{$boardId}_{$limit}";
// 1. 캐시 확인
$cached = DxCache::get($cacheKey, null);
if ($cached !== null) {
return $cached; // 캐시 히트
}
// 2. DB 조회
$db = Database::getInstance();
$posts = $db->rows(
"SELECT * FROM dx_posts WHERE board_id=? AND status=1",
" ORDER BY popular_score DESC LIMIT ?",
[$boardId, $limit]
);
// 3. 캐시 저장 (5분)
DxCache::set($cacheKey, $posts, 300);
return $posts;
}
// 무효화 (게시글 수정 시)
function invalidatePopularCache($boardId) {
DxCache::deletePrefix("popular_{$boardId}_");
}
6.2 TTL 선택 가이드
| 데이터 유형 | 권장 TTL | 이유 |
| 게시판 목록 | 60초 | 게시글 작성 후 1분 이내 반영. 즉시 무효화도 병행 |
| 시스템 설정 | 300초 (5분) | 관리자 변경 시 즉시 무효화. TTL은 안전장치 |
| 카테고리 | 300초 (5분) | 자주 바뀌지 않음. 관리자 변경 시 무효화 |
| 사이트맵 | 600초 (10분) | 크롤러 부하 방지. 10분 지연 허용 |
| 인기 게시글 | 300초 (5분) | 인기점수 실시간성 불필요. 5분 지연 허용 |
| 팝업 | 60초 | 날짜 기반 키로 자정 자동 갱신 |
| 회원 친구 수 | 60초 | 약간의 지연 허용. 30~60초마다 갱신 |
| 미읽음 쪽지 수 | 30초 | 실시간성이 상대적으로 중요. 30초마다 갱신 |
| 컬럼 존재 여부 | 3600초 (1시간) | 스키마는 거의 안 바뀜. 캐시 초기화로 수동 갱신 |
| 존재하지 않는 데이터 | 300초 (5분) | Negative Caching. 반복 조회 방지 |
6.3 캐시 키 네이밍 규칙
// 권장 네이밍 패턴: {리소스}_{식별자}_{변형}
"board_list_free_1" // 게시판 목록 — board_list + 게시판키 + 페이지
"cat_board_5" // 카테고리 — cat_board + 게시판ID
"site_abc123" // 멀티사이트 — site + md5(domain)
"popular_3_10" // 인기글 — popular + 게시판ID + 개수
"friend_cnt_123" // 친구수 — friend_cnt + 회원ID
"unread_memo_123" // 미읽쪽지 — unread_memo + 회원ID
// 주의사항
// 1. 특수문자 금지 (자동으로 _ 로 변환되므로 키 충돌 가능)
// "site_a.com"과 "site_a_com"이 같은 키로 처리됨
// → md5()로 도메인을 해시화하여 충돌 방지
// 2. 너무 긴 키는 의미 없음 (자동으로 안전하게 변환되나 가독성 저하)
// 3. 삭제 편의를 위해 접두어 체계 통일
// board_list_ 로 시작하는 모든 키를 한 번에 삭제
DxCache::deletePrefix("board_list_free_"); // free 게시판만
DxCache::deletePrefix("board_list_"); // 모든 게시판
6.4 주의사항 — 캐시해서는 안 되는 데이터
- 개인 정보 — 회원 이메일, 비밀번호, 개인 설정 등 개인 식별 정보. 공유 캐시에 저장 금지
- 인증 상태 — 로그인 여부, 권한 정보. 세션으로 관리해야 함. 캐시로 인증 상태 공유 금지
- 결제/주문 정보 — 실시간 정합성이 필요한 금융 데이터. 캐시 지연으로 오류 발생 가능
- 빠르게 변하는 카운터 — 실시간 조회수, 좋아요 수. DB에 직접 업데이트하거나 Redis 원자 연산 사용
- 사용자별 콘텐츠 — 로그인한 사용자의 장바구니, 알림 등. 사용자 키를 포함시켜야 하며 공유 캐시 금지
7장. 세션 성능 최적화 — 조건부 세션 시작
캐시와 별개로 세션 시작 자체를 최적화하는 전략입니다. PHP 세션은 파일 잠금으로 인해 동시 접속에 병목이 생깁니다.
7.1 세션 파일 잠금 문제
// 문제: PHP 세션은 파일 잠금(lock)을 사용
// 동일 세션 ID의 요청이 여러 개 오면 직렬화 처리
// 예시: 사용자가 탭을 5개 열어 동시에 요청
요청 1: 세션 파일 잠금 → 처리 중
요청 2: 잠금 대기... (1 완료까지)
요청 3: 잠금 대기... (1, 2 완료까지)
요청 4: 잠금 대기...
요청 5: 잠금 대기...
→ 병렬 처리 불가 → 응답 시간 5배 증가
// 해결책: 세션이 필요 없는 요청에서 세션을 시작하지 않음
7.2 $_dxNeedSession 조건부 시작
// index.php — 조건부 세션 시작 로직
// 세션 스킵 조건 (세 가지 모두 충족 시)
$skip = (
$_SERVER["REQUEST_METHOD"] === "GET" && // GET 요청
empty($_COOKIE[session_name()]) && // 세션 쿠키 없음 (신규 방문자)
!isset($_SERVER["HTTP_X_REQUESTED_WITH"]) // AJAX 아님
);
// 세션이 반드시 필요한 경로
$needSession = strpos($uri, "/admin") !== false // 관리자
|| strpos($uri, "/auth") !== false // 인증
|| strpos($uri, "/view/") !== false // 게시글 뷰(조회수)
|| strpos($uri, "/api/") !== false // API
|| strpos($uri, "/write") !== false // 글쓰기
|| strpos($uri, "/edit") !== false // 글수정
|| strpos($uri, "/reply") !== false; // 답글
if ($skip && !$needSession) {
$_dxNeedSession = false; // 세션 시작 안 함
}
// 효과:
// 비로그인 + 게시판 목록/글 목록 GET 요청 → 세션 없음 → 파일 잠금 없음
// → 수백 명 동시 접속도 병렬 처리 가능
💡 세션 스킵 + 게시판 캐시 조합의 시너지
비로그인 사용자의 게시판 목록 요청:
① 세션 시작 안 함 (파일 잠금 없음)
② DxCache에서 캐시 조회 (DB 쿼리 최소화)
③ 캐시 히트 시 전체 처리 시간 수 밀리초
→ 동시 수백 명 접속에서도 응답 시간 50ms 이하 가능
8장. Redis 설정 최적화
8.1 권장 redis.conf 설정
# /etc/redis/redis.conf
# 바인딩: 로컬만 허용 (보안)
bind 127.0.0.1
# 비밀번호 설정
requirepass 강력한비밀번호
# 최대 메모리 설정
maxmemory 256mb
# 메모리 초과 시 정책: LRU (Least Recently Used) 방식 삭제
maxmemory-policy allkeys-lru
# 영속성 비활성화 (캐시 전용 Redis, 재시작 시 데이터 삭제 허용)
save ""
appendonly no
# 연결 시간 초과
timeout 300
tcp-keepalive 300
8.2 CMS 설정과 Redis 연동
// data/config.php — Redis 설정 추가
// 방법 1: REDIS_SESSION_URL (DxCache + 세션 모두 Redis 사용)
define('REDIS_SESSION_URL', 'tcp://127.0.0.1:6379?auth=비밀번호');
// 방법 2: 개별 상수 (Secure.php Rate Limit/차단용)
define('REDIS_HOST', '127.0.0.1');
define('REDIS_PORT', 6379);
define('REDIS_AUTH', '비밀번호');
// 확인
$driver = DxCache::getDriver();
// "redis" 이면 정상 연결됨
// 현재 캐시 키 수 확인
$redis = Secure::getRedis();
if ($redis) {
$info = $redis->info("keyspace");
echo "키 수: " . print_r($info, true);
}
9장. 성능 측정 및 캐시 상태 확인
9.1 DX_START를 이용한 처리 시간 측정
// index.php 최상단에 정의된 DX_START
define('DX_START', microtime(true));
// 페이지 하단에서 처리 시간 표시 (테마/extend 활용)
$elapsed = round((microtime(true) - DX_START) * 1000, 2);
echo "<!-- 처리 시간: {$elapsed}ms -->";
// 캐시 히트 시: 5~20ms (Redis/APCu)
// 캐시 히트 시: 10~50ms (파일)
// 캐시 미스 시: 50~200ms (DB 조회 포함)
9.2 extend/bottom을 이용한 캐시 모니터링
// extend/bottom/99_cache_monitor.php.disabled
// 활성화하려면 .disabled 제거
if (!defined("DX_CMS")) exit;
if (!dx_is_admin()) return; // 관리자만
$driver = DxCache::getDriver();
$elapsed = round((microtime(true) - DX_START) * 1000, 2);
$info = array(
"driver" => $driver,
"elapsed" => $elapsed . "ms",
);
// Redis면 메모리 사용량 추가
if ($driver === "redis") {
$redis = Secure::getRedis();
if ($redis) {
$mem = $redis->info("memory");
$info["redis_used_memory"] = $mem["used_memory_human"];
}
}
echo "<!-- CACHE: " . json_encode($info) . " -->";
9.3 캐시 히트율 확인
// Redis 캐시 히트율 확인
$redis = Secure::getRedis();
$stats = $redis->info("stats");
$hits = $stats["keyspace_hits"];
$misses = $stats["keyspace_misses"];
$rate = round($hits / ($hits + $misses) * 100, 1);
echo "캐시 히트율: {$rate}%";
// 일반적으로 80% 이상이면 캐싱이 잘 동작하는 것
// 50% 미만이면 TTL이 너무 짧거나 무효화가 너무 자주 발생
// 파일 캐시 히트율 확인 (간접 방법)
$files = glob(DX_ROOT . "/data/cache/*.cache");
$expired = 0;
foreach ($files as $f) {
$d = @unserialize(file_get_contents($f));
if ($d && $d["expire"] < time()) $expired++;
}
echo "총 " . count($files) . "개 캐시, " . $expired . "개 만료됨";