회원가입 | 고객센터 |
DESIGNONEX
dxcms.kr
로그인 회원가입
고객센터
12. 성능 / 최적화

캐싱 전략

D DX
2026.05.10 16:11(수정됨) 111 0

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 . "개 만료됨";
 

댓글0

로그인 후 댓글을 작성할 수 있습니다.
12. 성능 / 최적화 트래픽 대응 2026.05.10 12. 성능 / 최적화 정적 리소스 관리 2026.05.10 12. 성능 / 최적화 캐싱 전략 2026.05.10
31
전체 회원
503
전체 게시글
775
전체 댓글
442
오늘 방문
33,174
전체 방문
3
현재 접속
인기글 7일 이내
최신글
최신댓글
목록