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

트래픽 대응

D DX
2026.05.10 16:12(수정됨) 118 0

1장. 트래픽 대응 전략 개요

DXCMS는 공유호스팅부터 멀티서버 클라우드 환경까지 단계적으로 스케일 가능하도록 설계되어 있습니다. 모든 트래픽 대응 기능은 기본으로 활성화되어 있으며, Redis 등 추가 인프라를 도입할수록 처리 능력이 비례해서 향상됩니다.


1.1 트래픽 대응 5대 레이어

요청 수신
  ↓
① WAF + IP 차단 (Secure.php)
   → 공격 트래픽 즉시 차단 (Redis 30분 IP 차단)
   → 정상 트래픽만 통과
  ↓
② Rate Limit (Secure::rateLimit)
   → 동일 IP/키의 과다 요청 제한
   → 로그인 무차별 대입, 스팸 등록 방어
  ↓
③ 세션 최적화 ($_dxNeedSession)
   → 비로그인 GET 요청: 세션 시작 안 함
   → 세션 파일 잠금 제거 → 병렬 처리 가능
  ↓
④ 캐시 레이어 (DxCache)
   → 게시판 목록: 비로그인 사용자 60초 캐시
   → DB 쿼리 80% 절감
  ↓
⑤ 출력 버퍼링 + 비동기 DB 기록
   → ob_start() → 응답 먼저 → 통계 나중에
   → fastcgi_finish_request(): PHP-FPM 응답 즉시 완료
  ↓
응답 완료 (사용자 화면 표시)
  → 방문자 로그 DB INSERT (응답 후 처리)


1.2 트래픽 규모별 권장 설정

규모 환경 핵심 설정
소규모 일 방문자 ~1천명 공유호스팅 파일 캐시 활성화, 세션 최적화, 봇 차단 (기본 설정으로 충분)
중규모 일 방문자 ~1만명 VPS/클라우드 단일 Redis 도입 (캐시+Rate Limit+세션), PHP-FPM 최적화, MySQL 쿼리 캐시
대규모 일 방문자 ~10만명 멀티서버 Redis 클러스터, 로드밸런서, CDN, MySQL 레플리케이션, 읽기/쓰기 분리
초대규모 일 방문자 ~100만+ マルチ리전 Kubernetes, 오토스케일링, 분산 캐시, 글로벌 CDN, 데이터 샤딩


2장. Rate Limiting — 요청 속도 제한

Rate Limit은 같은 IP 또는 같은 키에서 단시간에 너무 많은 요청이 오는 것을 제한합니다. 로그인 무차별 대입 공격, 댓글 스팸, API 남용 등을 방어합니다.


2.1 기본 설정값

// Secure.php 기본 설정
private $rateWindow  = 10;   // 측정 윈도우: 10초
private $rateLimit   = 60;   // 10초 내 최대 요청 수
private $ipRateLimit = 200;  // IP별 1분 최대 요청 수

// rateLimit($key, $limit, $window, $ipLimit)
// $key:     기능별 식별 키 (문자열)
// $limit:   $window 초 내 최대 요청 수
// $window:  측정 시간 윈도우 (초)
// $ipLimit: IP별 추가 제한 (0 = IP 제한 없음)
// 반환:     true = 허용, false = 초과 (차단)


2.2 Redis 슬롯 방식 (원자적 카운터)

// Redis 기반 Rate Limit 동작 원리

// 시간을 윈도우 크기($window)로 분할한 슬롯 번호
$slot = (int)floor(time() / $window);
// 예: window=30, time=1746957750
//     → slot = 1746957750 / 30 = 58231925
//     → 30초마다 새 슬롯 → 카운터 자동 초기화

// Redis 원자적 증가
$sKey  = "rl:{$key}:{$slot}";   // 예: "rl:login:58231925"
$count = $redis->incr($sKey);   // 원자적 증가 (동시성 안전)
if ($count === 1) {
    $redis->expire($sKey, $window + 5);  // 슬롯 만료 설정
}
if ($count > $limit) return false;  // 초과 → 차단

// IP별 추가 제한 ($ipLimit > 0)
$iKey = "rl:ip:{$key}:" . substr(md5($ip), 0, 8) . ":{$slot}";
$iCnt = $redis->incr($iKey);
if ($iCnt > $ipLimit) return false;

// Redis 오류 시 → 허용 (가용성 우선)
// → Redis가 죽어도 서비스 중단 없음

💡 원자적 연산이 중요한 이유
동시에 100개의 요청이 들어올 때 일반 카운터(read → +1 → write)는 경쟁 조건 발생.
99개가 동시에 read(0) → 모두 1로 증가 → 100개 모두 허용되는 버그.

Redis INCR은 단일 원자적 연산 → 동시 요청도 정확하게 카운팅.
100개 동시 요청 → 1,2,3...100 순서대로 카운팅 → 제한 초과 시 정확히 차단.


2.3 파일 기반 폴백

// Redis 없는 환경의 파일 기반 Rate Limit

// 저장 위치: data/cache/rl_{md5(key+ip)}.tmp
$file = DX_ROOT . "/data/cache/rl_" . md5($key . $ip) . ".tmp";

// 파일 내용: serialize(["count"=>N, "start"=>timestamp])
$data = array("count" => 0, "start" => time());

// 기존 데이터 읽기
if (is_file($file)) {
    $saved = unserialize(file_get_contents($file));
    if (is_array($saved) && ($now - $saved["start"]) < $window) {
        $data = $saved;  // 윈도우 내 데이터 재사용
    }
    // 윈도우 초과 → $data 초기화 (카운터 리셋)
}

$data["count"]++;
file_put_contents($file, serialize($data), LOCK_EX);  // 잠금 쓰기
return $data["count"] <= $limit;

// 주의: 트래픽이 많으면 파일 I/O 병목 발생 가능
// 고트래픽 환경에서는 반드시 Redis 사용


2.4 API별 Rate Limit 적용 예시

적용 위치 limit window 설명
로그인 시도 5 30 30초에 5회. 무차별 대입 공격 차단
회원가입 3 60 60초에 3회. 대량 계정 자동 생성 방어
비밀번호 찾기 3 300 5분에 3회. SMS/이메일 발송 남용 방지
댓글 작성 10 60 60초에 10회 + IP 20회 제한
게시글 작성 5 60 60초에 5회. 스팸 게시글 방어
파일 업로드 20 600 10분에 20회. 스토리지 남용 방지
소셜 로그인 시작 10 60 60초에 10회. OAuth CSRF 방어
API 일반 30 60 60초에 30회. 기본 API 보호


2.5 Rate Limit 적용 방법

// core/api/login.php (로그인 Rate Limit 예시)

$secure = Secure::getInstance();

// 30초에 5회 초과 시 429 반환
if (!$secure->rateLimit("login_attempt", 5, 30)) {
    dx_json(array(
        "success" => false,
        "message" => "로그인 시도가 너무 많습니다. 잠시 후 다시 시도해주세요.",
    ), 429);  // HTTP 429 Too Many Requests
}

// 플러그인에서 Rate Limit 추가
// plugins/my-plugin/api/submit.php
if (!Secure::getInstance()->rateLimit("my_plugin_submit", 10, 60, 30)) {
    dx_json(array("success"=>false, "message"=>"너무 많은 요청입니다."), 429);
}

// extend/top/에서 전역 Rate Limit (모든 요청에 적용)
// extend/top/99_global_rate_limit.php
$ip = Secure::getInstance()->clientIp();
if (!Secure::getInstance()->rateLimit("global_" . md5($ip), 200, 60)) {
    http_response_code(429);
    exit("Too Many Requests");
}


3장. 봇 트래픽 차단

봇 트래픽은 실제 방문자 통계를 왜곡하고 DB와 서버 자원을 낭비합니다. DXCMS는 두 단계의 봇 차단 로직으로 정상 크롤러는 허용하고 악성 봇은 걸러냅니다.


3.1 방문자 트래커 봇 차단 (01_visit_tracker.php)

extend/middle/01_visit_tracker.php에서 모든 요청의 User-Agent를 검사합니다. 봇으로 판별되면 방문자 로그를 아예 기록하지 않습니다.
// 봇 판별 패턴 (visit_tracker.php)
// 아래 패턴 중 하나라도 UA에 포함되면 봇 처리

// 검색엔진 및 크롤러
"googlebot","bingbot","slurp","duckduckbot","baiduspider",
"yandexbot","sogou","exabot","facebot","ia_archiver",
"ahrefsbot","semrushbot","dotbot","rogerbot","mj12bot",
"blexbot","seznambot","linkdexbot","petalbot","bytespider",

// 일반 봇 패턴
"bot","spider","crawl","scraper","checker","monitor",
"fetcher","scanner","analyzer","collector","harvester",

// HTTP 클라이언트 라이브러리
"curl","wget","python-requests","python-urllib","python",
"java","httpclient","go-http-client","okhttp","axios",
"libwww-perl","lwp-trivial","php","ruby","scrapy",

// 모니터링 서비스
"uptimerobot","pingdom","newrelic","datadog","statuspage",
"site24x7","zabbix","nagios","gomez",

// 헤드리스 브라우저 / 자동화 도구
"headlesschrome","phantomjs","selenium","puppeteer","playwright"

// User-Agent 비어있으면 → 무조건 봇으로 처리
if ($_vt_ua === "") { $_vt_isBot = true; }

// 봇이면 → 함수 즉시 return → visit_logs INSERT 없음
if ($_vt_isBot) return;

💡 봇 차단이 DB 성능에 미치는 영향
실제 운영 환경에서 봇 트래픽은 전체 요청의 30~60%를 차지합니다.
봇 차단 없이 visit_logs에 모두 기록하면:
  → 하루 10만 요청 중 5만 건이 봇 → 90일 × 5만 = 450만 봇 로그
  → 테이블 비대화 → 인덱스 효율 저하 → 통계 쿼리 속도 저하

DXCMS는 봇을 visit_logs에 전혀 기록하지 않아 테이블 크기를 최소화합니다.


3.2 WAF 봇 감지 (Secure.php — detectSuspiciousBot)

// Secure.php::detectSuspiciousBot()
// visit_tracker와 별도로 WAF 레벨에서 동작

// 허용 봇 화이트리스트 (차단 없이 통과)
Googlebot, Yeti (네이버), bingbot, DuckDuckBot,
Baiduspider, kakaotalk-scrap, facebookexternalhit,
Twitterbot, LinkedInBot, AhrefsBot, SemrushBot

// 의심 봇 패턴 (차단 아님 — security.log에만 기록)
curl, wget, python, scrapy, headless, selenium, phantomjs

// 차단하지 않는 이유:
// 실제 Googlebot을 잘못 차단하면 검색 노출 손실
// → 로그만 기록하고 관리자가 판단하도록 설계


3.3 추가 봇 차단 방법 (extend/top/ 활용)

// extend/top/02_bot_block.php — 커스텀 봇 차단
if (!defined("DX_CMS")) exit;

$ua = strtolower(isset($_SERVER["HTTP_USER_AGENT"]) ? $_SERVER["HTTP_USER_AGENT"] : "");

// User-Agent 없는 요청 즉시 차단 (봇/스크래퍼 특성)
if (empty($ua)) {
    http_response_code(403);
    exit("403 Forbidden");
}

// 특정 IP 대역 차단 (예: 알려진 악성 봇 대역)
$ip = Secure::getInstance()->clientIp();
$blockedRanges = array("185.220.", "193.32.", "171.25.");
foreach ($blockedRanges as $range) {
    if (strpos($ip, $range) === 0) {
        http_response_code(403); exit();
    }
}

// Referer 없는 POST 요청 차단 (CSRF 추가 방어)
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $referer = isset($_SERVER["HTTP_REFERER"]) ? $_SERVER["HTTP_REFERER"] : "";
    $siteUrl = dx_config("site_url", "");
    if ($siteUrl && $referer && strpos($referer, $siteUrl) !== 0) {
        http_response_code(403); exit("Cross-Origin Request Denied");
    }
}


4장. 출력 버퍼링 및 비동기 처리

사용자에게 응답을 먼저 보내고 DB 기록 등 부가 작업은 그 이후에 처리합니다. 사용자가 느끼는 응답 시간을 최소화하는 핵심 기법입니다.


4.1 ob_start() 출력 버퍼링

// index.php 최상단 — 출력 버퍼 시작
ob_start();

// ob_start() 효과:
// - PHP가 출력하는 모든 내용을 메모리 버퍼에 쌓음
// - 최종적으로 ob_end_flush()가 호출될 때 한 번에 전송
// - 중간에 header() 함수를 어디서든 호출 가능 (HEADER_SENT 방지)
// - 출력 전에 응답 크기 계산 가능 (Content-Length 헤더 설정)

// index.php 마지막 — 출력 버퍼 플러시
if (ob_get_level() > 0) {
    ob_end_flush();
}
// IIS/CGI 환경에서도 정상 동작하도록 조건부 flush


4.2 register_shutdown_function — 응답 후 처리

register_shutdown_function은 PHP 스크립트가 종료될 때(응답 완료 후) 실행됩니다. 통계 기록, 비동기 이메일 발송, GC 등 사용자 응답에 영향을 주지 않아야 하는 작업에 적합합니다.
// extend/middle/01_visit_tracker.php

// ① 사용자에게 응답 먼저 완료
register_shutdown_function(function() use ($_vt_data) {

    // PHP-FPM 환경: fastcgi_finish_request()가 가장 빠름
    // → 응답을 즉시 클라이언트에게 전송하고 프로세스는 계속 실행
    if (function_exists("fastcgi_finish_request")) {
        fastcgi_finish_request();
    } elseif (ob_get_level() > 0) {
        ob_end_flush();  // 일반 Apache 환경
    }
    if (function_exists("flush")) flush();

    // ② 응답 완료 후 DB 기록 (사용자 대기 없음)
    $db  = Database::getInstance();
    $pdo = $db->pdo();

    // dx_visits UPSERT (1회 쿼리)
    $pdo->prepare(
        "INSERT INTO dx_visits (visit_date, visit_count, unique_count)",
        " VALUES (?, 1, ?)",
        " ON DUPLICATE KEY UPDATE",
        "   visit_count = visit_count + 1,",
        "   unique_count = unique_count + ?"
    )->execute([$date, $uniqueInc, $uniqueInc]);

    // dx_visit_logs INSERT
    $pdo->prepare(
        "INSERT INTO dx_visit_logs (visit_date, ip, page_url, ...) VALUES (?,...)"
    )->execute([...]);

    // 1/200 확률 GC: 90일 이전 로그 삭제
    if (mt_rand(1, 200) === 1) {
        $pdo->prepare(
            "DELETE FROM dx_visit_logs WHERE visit_date < ?"
        )->execute([date("Y-m-d", strtotime("-90 days"))]);
    }
});

💡 fastcgi_finish_request()의 성능 효과
PHP-FPM 환경에서만 동작합니다. Apache mod_php는 효과 없음.

동작 방식:
  1. fastcgi_finish_request() 호출 시 브라우저로 응답 즉시 전송
  2. PHP 프로세스는 계속 실행 (DB 기록, 이메일 발송 등)
  3. 사용자는 이미 응답을 받아 페이지를 보는 중
  4. PHP가 백그라운드에서 나머지 작업 처리

효과: 방문자 로그 DB INSERT가 응답 시간에 포함되지 않음
예시: 응답 시간 80ms → fastcgi_finish_request 사용 시 50ms (38% 단축)


4.3 순방문자 판단 — DxCache 활용 (DB 조회 없음)

// visit_tracker.php — 순방문자 판단 최적화

// 기존 방식 (DB 조회 필요)
// SELECT COUNT(*) FROM dx_visit_logs WHERE ip=? AND visit_date=?
// → 매 요청마다 DB 쿼리 실행

// DXCMS 방식 (DxCache 활용 — DB 조회 없음)
$ttl = strtotime("tomorrow") - time();  // 자정까지 남은 시간
$cacheKey = "vt_u:" . $date . ":" . substr(md5($ip . $browser), 0, 12);
// 예: "vt_u:2026-05-12:a1b2c3d4e5f6"

if (!DxCache::get($cacheKey, false)) {
    // 캐시 없음 → 오늘 첫 방문 → unique
    DxCache::set($cacheKey, 1, $ttl);  // 자정까지 유지
    $isUnique = true;
} else {
    // 캐시 있음 → 오늘 재방문 → not unique
    $isUnique = false;
}

// ttl = strtotime("tomorrow") - time():
// 자정이 지나면 캐시 자동 만료 → 다음날 다시 unique 판정
// → 일별 순방문자 집계 정확성 유지


5장. DB 쿼리 최적화 — 인덱스 전략

트래픽이 늘어날수록 DB 쿼리 속도가 병목이 됩니다. DXCMS는 실제 쿼리 패턴을 분석하여 복합 인덱스를 설계했습니다.


5.1 dx_posts 복합 인덱스

-- dx_posts 인덱스 구성

-- 게시판 목록 쿼리: WHERE board_id=? AND status=1 ORDER BY id DESC
KEY idx_board_status_id (board_id, status, id)
-- → board_id + status 필터 후 id DESC 정렬을 인덱스만으로 처리
-- → filesort 없음 → 빠른 목록 조회

-- 인기글 정렬: WHERE board_id=? AND status=1 ORDER BY popular_score DESC
KEY idx_popular_score (board_id, status, popular_score)
-- → 인기 정렬도 인덱스 사용 → 테이블 전체 스캔 없음

-- FULLTEXT 전문 검색: MATCH(title, content) AGAINST(?)
FULLTEXT KEY ft_title_content (title, content)
-- MySQL 5.6+ InnoDB FULLTEXT 지원
-- LIKE "%검색어%" 대비 성능 수십 배 향상

-- 작성자별 게시글 조회
KEY idx_member_id (member_id)

-- 날짜 범위 조회 (최신글 위젯, 인기글 7일 필터)
KEY idx_created (created_at)


5.2 dx_visit_logs 복합 인덱스

-- dx_visit_logs 인덱스 구성 (v8.1.0)

-- 일별 순방문자 집계: WHERE visit_date=? AND is_bot=0
KEY idx_date_bot (visit_date, is_bot)
KEY idx_bot_date (is_bot, visit_date)

-- 현재 접속자 수: WHERE created_at >= ? AND is_bot=0
KEY idx_created_bot (created_at, is_bot)

-- IP별 중복 방문 확인
KEY idx_ip_date (ip, visit_date)

-- 브라우저별 통계
KEY idx_browser (browser)

-- 유입 도메인 통계
KEY idx_referer_domain (referer_domain(100))

-- 최적화된 통계 쿼리 (main.php 위젯)
-- 기존: COUNT(*) FROM visit_logs (풀스캔)
-- 개선: SUM(unique_count) FROM dx_visits (집계 테이블)
SELECT SUM(unique_count) FROM dx_visits;  -- 행 수 적음 → 빠름


5.3 인기글 점수 계산 (popular_score)

// 인기글 점수 = (조회×1 + 좋아요×5 + 댓글×3) × 시간감쇠

// 조회수가 증가할 때마다 점수 갱신
$daysPassed = max(0, (time() - strtotime($post["created_at"])) / 86400);
$decay = max(0.1, 1.0 - floor($daysPassed / 7) * 0.1);
// 시간감쇠 계산:
// 0~7일:  decay = 1.0  (감쇠 없음)
// 7~14일: decay = 0.9  (10% 감쇠)
// 14~21일:decay = 0.8  (20% 감쇠)
// ...
// 63일+:  decay = 0.1  (90% 감쇠, 최소값)

$raw = (int)$post["view_count"] * 1
     + (int)$post["like_count"] * 5
     + (int)$post["comment_count"] * 3;

$newScore = (int)round($raw * $decay);
UPDATE dx_posts SET popular_score=? WHERE id=?

// 장점: popular_score가 DB에 저장됨
// → ORDER BY popular_score DESC 로 인덱스 활용 가능
// → 런타임에 복잡한 계산 없이 단순 정렬


5.4 비정규화 캐시 컬럼

like_count, comment_count는 실시간으로 집계 쿼리를 실행하면 부하가 크므로, 변경 시마다 dx_posts 테이블에 직접 업데이트합니다.
// dx_posts 비정규화 컬럼 (집계 캐시)
like_count     INT  DEFAULT 0   -- 좋아요 수 캐시
comment_count  INT  DEFAULT 0   -- 댓글 수 캐시
view_count     INT  DEFAULT 0   -- 조회수

// 좋아요 추가 시 즉시 업데이트
INSERT INTO dx_likes (target_type, target_id, member_id) VALUES (...)
UPDATE dx_posts SET like_count = like_count + 1 WHERE id=?

// 댓글 작성 시 즉시 업데이트
INSERT INTO dx_comments (...)
UPDATE dx_posts SET comment_count = comment_count + 1 WHERE id=?

// 목록 쿼리에서 서브쿼리 없이 바로 읽기
// 기존 방식 (느림): 
//   SELECT *, (SELECT COUNT(*) FROM dx_comments WHERE post_id=p.id) AS cmt_cnt
// DXCMS 방식 (빠름):
//   SELECT *, p.comment_count


5.5 방문자 통계 — visits 집계 테이블

-- dx_visits 일별 집계 테이블
-- visit_logs 풀스캔 대신 집계 테이블 사용 → 극적 성능 향상

-- 매 요청마다 UPSERT (1회 쿼리)
INSERT INTO dx_visits (visit_date, visit_count, unique_count)
VALUES (?, 1, ?)
ON DUPLICATE KEY UPDATE
    visit_count  = visit_count + 1,
    unique_count = unique_count + ?

-- 전체 방문자 합계 (빠름)
SELECT SUM(unique_count) FROM dx_visits;
-- 행 수: 날짜 수 = 수년치 해도 1825행 이하
-- vs 풀스캔: COUNT(DISTINCT ip) FROM visit_logs
--            수백만 행 → 수십 초 소요

-- 오늘 방문자 (인덱스 사용)
SELECT unique_count FROM dx_visits WHERE visit_date = ?
-- 단 1행 조회 → 즉시 반환


6장. PHP-FPM 최적화

PHP-FPM은 PHP 요청을 처리하는 프로세스 관리자입니다. 트래픽 증가에 따라 워커 프로세스 수와 메모리를 최적화하면 응답 속도와 동시 처리 수가 크게 향상됩니다.


6.1 권장 PHP-FPM 설정

; /etc/php/8.x/fpm/pool.d/www.conf

; 프로세스 관리 방식: dynamic (트래픽에 따라 자동 조정)
pm = dynamic

; 최대 자식 프로세스 수 (= 동시 처리 가능 요청 수)
; 공식: (서버 메모리 - OS용 메모리) / PHP 프로세스 메모리
; 예: 4GB RAM, PHP 50MB/프로세스 → (4096-512) / 50 = 71
pm.max_children = 50

; 항상 유지할 최소 대기 프로세스 수 (트래픽 급증 시 즉시 대응)
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35

; 프로세스당 최대 요청 처리 수 (메모리 누수 방지)
pm.max_requests = 500

; 요청 처리 제한 시간
request_terminate_timeout = 60s

; 느린 요청 로깅 (10초 이상 처리되는 요청 추적)
slowlog = /var/log/php-fpm-slow.log
request_slowlog_timeout = 10s


6.2 PHP.ini 성능 설정

; /etc/php/8.x/fpm/php.ini

; OPcache: PHP 코드 컴파일 결과 캐싱 (핵심 성능 최적화)
opcache.enable = 1
opcache.memory_consumption = 128       ; MB
opcache.interned_strings_buffer = 16   ; MB
opcache.max_accelerated_files = 10000  ; 캐싱할 최대 파일 수
opcache.revalidate_freq = 60           ; 파일 변경 체크 주기 (초)
opcache.fast_shutdown = 1              ; 빠른 종료

; 메모리 제한 (대용량 파일 업로드, 복잡한 페이지 처리)
memory_limit = 256M

; 업로드 제한
upload_max_filesize = 50M
post_max_size = 55M

; 실행 시간 제한
max_execution_time = 60
max_input_time = 60

; 세션 파일 잠금 최소화
session.lazy_write = 1   ; 세션 데이터가 변경된 경우만 파일 쓰기

💡 OPcache 효과
OPcache 없이: 매 요청마다 PHP 파일 읽기 → 파싱 → 컴파일 → 실행
OPcache 있을 때: 첫 요청만 컴파일 → 이후 메모리에서 바이트코드 직접 실행

효과: PHP 처리 속도 3~5배 향상
DXCMS 기준: 50ms → 15ms (코드 처리 시간 70% 단축)

주의: 코드 수정 후 OPcache를 비워야 변경사항 반영
  opcache_reset() 또는 PHP-FPM 재시작


7장. MySQL 최적화


7.1 권장 MySQL 설정 (my.cnf)

# /etc/mysql/mysql.conf.d/mysqld.cnf

# InnoDB 버퍼 풀: 서버 RAM의 70~80% 권장
# (자주 사용되는 데이터와 인덱스를 메모리에 캐싱)
innodb_buffer_pool_size = 2G   # 4GB RAM 서버 기준

# 버퍼 풀 인스턴스 수 (innodb_buffer_pool_size / 1GB 권장)
innodb_buffer_pool_instances = 2

# 로그 파일 크기 (큰 트랜잭션 성능 향상)
innodb_log_file_size = 256M

# 쓰기 성능: O_DIRECT로 OS 캐시 이중화 방지
innodb_flush_method = O_DIRECT

# 동시 I/O 스레드 수 (SSD: CPU 코어 수)
innodb_read_io_threads = 4
innodb_write_io_threads = 4

# 최대 연결 수 (PHP-FPM pm.max_children과 맞춤)
max_connections = 200

# 느린 쿼리 로깅 (1초 이상 쿼리 추적)
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1

# 쿼리 캐시 (MySQL 5.7 이하, 8.0에서 제거됨)
# query_cache_size = 64M
# query_cache_type = 1


7.2 EXPLAIN으로 쿼리 분석

-- 게시판 목록 쿼리 분석 예시
EXPLAIN
SELECT p.*, m.name
FROM dx_posts p
LEFT JOIN dx_members m ON m.id = p.member_id
WHERE p.board_id = 1 AND p.status = 1 AND p.is_notice = 0
ORDER BY p.id DESC
LIMIT 20;

-- EXPLAIN 결과 해석
-- type:
--   ref    → 인덱스 사용 (좋음)
--   range  → 인덱스 범위 스캔 (양호)
--   ALL    → 풀스캔 (나쁨, 인덱스 추가 필요)

-- key: 사용된 인덱스명 (NULL이면 인덱스 미사용)
-- rows: 예상 스캔 행 수 (적을수록 좋음)
-- Extra:
--   Using index        → 커버링 인덱스 (가장 빠름)
--   Using where        → WHERE 조건 있음
--   Using filesort     → 정렬에 파일 사용 (인덱스 추가 검토)
--   Using temporary    → 임시 테이블 사용 (개선 필요)


7.3 읽기/쓰기 분리 (레플리케이션)

트래픽이 많아지면 MySQL 레플리케이션으로 읽기(SELECT)와 쓰기(INSERT/UPDATE)를 다른 서버로 분리할 수 있습니다.
// data/config.php — 읽기/쓰기 분리 설정 (확장 구성)

// 마스터 (쓰기용)
$db->connect("master-db.host", "dxcms", "user", "pass", "utf8mb4", "dx_");

// 슬레이브 (읽기용) — 별도 설정 필요
// DXCMS 기본은 단일 DB 연결이므로
// 읽기/쓰기 분리는 커스텀 미들웨어 또는 ProxySQL 사용 권장

// ProxySQL 설정 예시 (MySQL 앞단에서 쿼리 라우팅)
// SELECT → 슬레이브로 자동 라우팅
// INSERT/UPDATE/DELETE → 마스터로 라우팅
// → PHP 코드 변경 없이 DB 레이어에서 분리


8장. 세션 병목 최적화

PHP 파일 세션의 가장 큰 문제는 동일 세션 ID의 요청이 직렬화된다는 것입니다. 한 탭이 처리되는 동안 다른 탭은 대기해야 합니다. DXCMS는 이 문제를 두 가지 방법으로 해결합니다.


8.1 조건부 세션 시작 ($_dxNeedSession)

// index.php — 세션 필요 여부 판단

// 세션 스킵 조건 (모두 충족 시 세션 없이 처리)
$_dxNeedSession = true;

if (
    $_SERVER["REQUEST_METHOD"] === "GET" &&  // GET 요청
    empty($_COOKIE[session_name()]) &&         // 세션 쿠키 없음
    !isset($_SERVER["HTTP_X_REQUESTED_WITH"]) // AJAX 아님
) {
    // 세션 필수 경로 확인
    $uri = $_SERVER["REQUEST_URI"];
    $needSession = strpos($uri, "/admin")  !== false
                || strpos($uri, "/auth")   !== false
                || strpos($uri, "/view/")  !== false
                || strpos($uri, "/api/")   !== false
                || strpos($uri, "/write")  !== false
                || strpos($uri, "/edit")   !== false
                || strpos($uri, "/reply")  !== false;

    if (!$needSession) {
        $_dxNeedSession = false;  // 세션 시작 안 함 → 파일락 없음
    }
}

// 효과:
// 비로그인 게시판 목록 접근 → 세션 없음 → 병렬 처리 가능
// 100명 동시 접속도 서로 대기 없이 처리


8.2 Redis 세션 (멀티서버 공유)

// data/config.php — Redis 세션 설정
define('REDIS_SESSION_URL', 'tcp://127.0.0.1:6379?auth=비밀번호');

// Redis 세션의 장점
// 1. 멀티서버: 모든 서버가 동일 세션 공유
//    서버 A에서 로그인 → 서버 B에서 요청 → 세션 유지

// 2. 파일 잠금 없음
//    Redis는 원자적 연산으로 동시 읽기/쓰기 지원
//    → 동일 세션의 여러 탭도 병렬 처리 가능

// 3. 자동 TTL
//    Redis TTL로 세션 자동 만료 → 파일 GC 불필요

// 4. 속도
//    파일 I/O 없음 → 세션 읽기/쓰기 10배 빠름

// session.lazy_write = 1 (php.ini)
// 세션 데이터 변경 없으면 Redis 쓰기 안 함 → 성능 향상


9장. 멀티서버 및 로드밸런서 구성


9.1 로드밸런서 구성 요소

// 멀티서버 아키텍처 (고트래픽 구성)

사용자 브라우저
    ↓
Cloudflare / AWS CloudFront (CDN)
    ↓ (정적 파일: CSS, JS, 이미지는 CDN에서 반환)
Load Balancer (Nginx / AWS ALB)
    ↓ (라운드로빈 또는 IP 해시)
┌──────────────────────────────────┐
│ WAS 1: Apache+PHP-FPM            │
│ WAS 2: Apache+PHP-FPM            │
│ WAS 3: Apache+PHP-FPM            │
└──────────────────────────────────┘
    ↓ (모든 서버가 공유)
┌─────────────┬─────────────────────┐
│ MySQL Master │ Redis Cluster       │
│ MySQL Slave  │ (세션+캐시+Rate Limit) │
└─────────────┴─────────────────────┘
    ↓
NFS / S3 / 분산 스토리지
(data/uploads/ 공유 스토리지)


9.2 멀티서버 필수 설정

항목 설정 방법
세션 공유 REDIS_SESSION_URL → Redis 세션 저장소 사용. 모든 서버가 동일 Redis 가리킴
캐시 공유 REDIS_SESSION_URL 설정 시 DxCache도 Redis 사용 → 캐시 자동 공유
업로드 파일 공유 data/uploads/ → NFS, AWS S3, GCS 마운트. 모든 서버에서 접근 가능해야 함
Rate Limit 공유 Redis 사용 시 자동으로 전체 서버 합산 카운팅. 서버별 독립 동작 불가
IP 차단 공유 Redis "dx:ban:{IP}" 키는 모든 서버에서 공유. 한 서버 차단 → 전체 차단
코드 동기화 rsync, Ansible, Docker 이미지. 모든 서버 코드 동일 버전 유지 필수


9.3 Nginx 로드밸런서 설정

# /etc/nginx/nginx.conf

# 업스트림 서버 정의
upstream dxcms_backend {
    # 라운드로빈 (기본)
    server 10.0.0.1:80 weight=3;   # 고성능 서버 가중치 3
    server 10.0.0.2:80 weight=2;
    server 10.0.0.3:80 weight=1;

    # 또는 IP 해시 (같은 IP는 항상 같은 서버로)
    # ip_hash;

    # 헬스 체크
    # server 10.0.0.4:80 backup;  # 장애 시 백업 서버
}

server {
    listen 80;
    server_name dxcms.example.com;

    # 정적 파일: 로드밸런서에서 직접 서빙 (WAS 부하 없음)
    location ~* ^/(assets|themes)/ {
        root /var/www/dxcms;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # 동적 요청: 백엔드로 전달
    location / {
        proxy_pass http://dxcms_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 5s;
        proxy_read_timeout 60s;
    }
}


9.4 Cloudflare 연동

// Cloudflare CDN 사용 시 실제 클라이언트 IP 처리
// Secure::clientIp()는 이미 CF-Connecting-IP 헤더를 우선 처리

// Cloudflare IP 대역 (신뢰 프록시 범위 — Secure.php 내장)
173.245.48.0/20
103.21.244.0/22
103.22.200.0/22
103.31.4.0/22
141.101.64.0/18
108.162.192.0/18
190.93.240.0/20
// ... 14개 대역

// Cloudflare에서 보내는 헤더
// CF-Connecting-IP: 실제 클라이언트 IP
// X-Forwarded-For: 프록시 체인 전체
// CF-Ray: Cloudflare 요청 ID (디버깅용)

// Cloudflare 레이어에서 차단 가능한 공격
// DDoS: 자동 흡수 (Cloudflare Magic Transit)
// 봇: Cloudflare Bot Management
// Rate Limit: Cloudflare Rules (CMS Rate Limit 보완)
// WAF: Cloudflare WAF (CMS WAF 보완)


10장. 트래픽 급증 대응 체크리스트


10.1 급증 전 사전 준비

  • [ ] Redis 설치 및 REDIS_SESSION_URL 설정 → 세션 + 캐시 + Rate Limit Redis 전환
  • [ ] PHP OPcache 활성화 확인 → phpinfo()에서 "Zend OPcache" 활성화 여부
  • [ ] MySQL innodb_buffer_pool_size → RAM의 70%로 설정
  • [ ] PHP-FPM pm.max_children → 서버 RAM에 맞게 설정
  • [ ] 게시판 캐시 TTL 확인 → 비로그인 목록 60초 캐시 정상 동작 여부
  • [ ] 봇 차단 로직 확인 → visit_tracker.php 정상 동작 여부
  • [ ] DB 인덱스 점검 → SHOW INDEX FROM dx_posts; idx_board_status_id 확인
  • [ ] 모니터링 설정 → CPU, RAM, DB 커넥션, Redis 메모리 실시간 모니터링


10.2 급증 중 즉각 대응

// 1. DxCache 전체 초기화 → 새 캐시로 새 출발
DxCache::flush();

// 2. 특정 게시판만 캐시 초기화
DxCache::deletePrefix("board_list_free_");

// 3. extend/top/ 에 유지보수 모드 임시 활성화
// extend/top/00_maintenance.php
$allowedIPs = array("1.2.3.4"); // 관리자 IP
$ip = Secure::getInstance()->clientIp();
if (!in_array($ip, $allowedIPs)) {
    http_response_code(503);
    exit("서비스 점검 중입니다. 잠시 후 다시 시도해주세요.");
}

// 4. Redis IP 임시 차단 (공격 IP)
$redis = Secure::getRedis();
if ($redis) {
    $redis->setex("dx:ban:공격자IP", 86400, "MANUAL"); // 24시간
}

// 5. MySQL 슬로우 쿼리 확인
SHOW FULL PROCESSLIST;          -- 현재 실행 중인 쿼리
SHOW STATUS LIKE "Threads_%";   -- 연결 스레드 현황
SHOW STATUS LIKE "Innodb_row_lock%"; -- 락 현황


10.3 주요 지표 모니터링

지표 확인 명령 정상 기준
PHP-FPM 활성 워커 php-fpm status (pm.status_path) max_children 80% 미만
MySQL 연결 수 SHOW STATUS LIKE "Threads_connected" max_connections 70% 미만
MySQL 슬로우 쿼리 tail -f /var/log/mysql/slow.log 1초 이상 쿼리 없음
Redis 메모리 redis-cli info memory maxmemory 80% 미만
캐시 히트율 redis-cli info stats (keyspace_hits/misses) 80% 이상
서버 CPU top / htop 50% 미만 (버스트 기준)
PHP OPcache 히트율 opcache_get_status()["opcache_statistics"] 95% 이상
DX_START 처리 시간 extend/bottom/ 로깅 200ms 미만


10.4 extend를 이용한 성능 모니터링

// extend/bottom/99_perf_monitor.php.disabled
// 활성화: .disabled 제거
if (!defined("DX_CMS")) exit;
if (!dx_is_admin()) return;  // 관리자만

$elapsed  = round((microtime(true) - DX_START) * 1000, 2);
$memPeak  = round(memory_get_peak_usage(true) / 1024 / 1024, 2);
$driver   = DxCache::getDriver();

$info = array(
    "time"   => $elapsed . "ms",
    "mem"    => $memPeak . "MB",
    "cache"  => $driver,
);

if ($driver === "redis") {
    $redis = Secure::getRedis();
    if ($redis) {
        $mem = $redis->info("memory");
        $info["redis_mem"] = $mem["used_memory_human"];
        $stats = $redis->info("stats");
        $h = (int)$stats["keyspace_hits"];
        $m = (int)$stats["keyspace_misses"];
        if ($h + $m > 0) {
            $info["hit_rate"] = round($h / ($h + $m) * 100, 1) . "%";
        }
    }
}

echo "<!-- PERF: " . json_encode($info) . " -->";
 

댓글0

로그인 후 댓글을 작성할 수 있습니다.
5. 관리자 기능 사용법 회원 랭킹 2026.04.21 5. 관리자 기능 사용법 포인트샵 2026.04.21 5. 관리자 기능 사용법 레벨 관리 2026.04.21 5. 관리자 기능 사용법 포인트 관리 2026.04.21 5. 관리자 기능 사용법 문자 서비스 2026.04.21 5. 관리자 기능 사용법 메일 보내기 2026.04.21 5. 관리자 기능 사용법 회원 관리 2026.04.21 5. 관리자 기능 사용법 메뉴 관리 2026.04.21 5. 관리자 기능 사용법 인기글 2026.04.21 5. 관리자 기능 사용법 카테고리 2026.04.21 5. 관리자 기능 사용법 게시판 그룹 2026.04.21 5. 관리자 기능 사용법 페이지 관리 2026.04.21 5. 관리자 기능 사용법 전체 공지 2026.04.21 5. 관리자 기능 사용법 팝업 관리 2026.04.21 5. 관리자 기능 사용법 게시판 관리 2026.04.21 4.2 관리자 시스템 구조 관리자 UI 구조 2026.04.21 4.2 관리자 시스템 구조 관리자 라우팅 2026.04.21 4.1 CMS 아키텍처 데이터 흐름 연결 2026.04.21 4.1 CMS 아키텍처 DX 위에 CMS가 올라가는 구조 2026.04.21 3.10 모듈 로딩 구조 자동 로딩 구조 2026.04.21
31
전체 회원
503
전체 게시글
770
전체 댓글
441
오늘 방문
33,173
전체 방문
2
현재 접속
인기글 7일 이내
최신글
최신댓글
목록