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

캐싱 전략

D DX
2026.05.10 16:11(수정됨) 105 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

로그인 후 댓글을 작성할 수 있습니다.
7. 테마 DXCMS 테마 개발 AI 프롬프트 스킬과 멀티사이트 체험 2026.05.23 6. 게시판 DXCMS 게시판 스킨 만들기 Prompt Skill 2026.05.23 16. 이슈 가이드 막코딩 필수 규칙 2026.05.21 16. 이슈 가이드 그누보드의 `_common.php` 처럼, `dx_load.php` 한 줄로 DXCMS의 모든 기능을 사용하는 방법입니다. 2026.05.21 15. 마켓 개발자 가이드 마켓 다운로드 보호 설정 가이드 2026.05.20 6. 게시판 게시판 여분 필드 (Board Extra Fields) 사용 가이드 2026.05.19 14. 데이터베이스 Database 직접 쿼리 개발 2026.05.19 14. 데이터베이스 DB스키마 2026.05.12 13. 보안 기본 보안 구조 2026.05.10 12. 성능 / 최적화 트래픽 대응 2026.05.10 12. 성능 / 최적화 정적 리소스 관리 2026.05.10 12. 성능 / 최적화 캐싱 전략 2026.05.10 11. 인증 / 로그인 시스템 세션 처리 구조 2026.05.10 11. 인증 / 로그인 시스템 인증 흐름 2026.05.10 11. 인증 / 로그인 시스템 소셜 로그인 2026.05.10 11. 인증 / 로그인 시스템 일반 로그인 2026.05.10 10. 마이페이지 마이페이지 구조 2026.05.10 9. 채팅 채팅 제작 2026.05.10 9. 채팅 채팅 구조 2026.05.10 3.8 Extend 구조 Extend 실제 소스 코드 완전 분석 • 12가지 실전 사례 2026.05.02
31
전체 회원
502
전체 게시글
767
전체 댓글
441
오늘 방문
33,173
전체 방문
2
현재 접속
인기글 7일 이내
최신글
최신댓글
목록