물어보기Rhymix
조회 수 106 댓글 44
Extra Form

서버 부하 이슈 때문에;; 통합검색 자동완성 애드온에 캐시를 적용해보려고 하는데요

화제의 글 애드온의 캐시 적용 코드를 가져와서 재활용을 했습니다.

(애드온 제작자 Canto님께 감사 말씀 드립니다~)

 

근데 제가 캐시 적용을 처음 해보는 것이어서 이게 제대로 되는 건지 확신이 없네요;;;

 

1. 일단은 페이지 로딩 시에 json 데이터를 가져오는 게 아니라 검색창에 focus를 했을 때 가져오도록 작업해놨구요ㅎ

    1-1. 데이터는 클라이언트의 로컬저장소에 보관을 해놔서 가급적 서버와의 통신 시도를 제한하고 있어요.

2. (로컬저장소에 데이터가 없을 경우에 한해) js에서 별도의 php 파일로 json 데이터를 호출하는데 캐시 여부에 따라 쿼리를 실행하게끔 하는 의도입니다.

 

아래는 php 파일 전체 소스인데요.

캐시 적용을 제대로 한 건지 살펴봐주시면 감사하겠습니다.

 

<?php

define('__XE__', true);
require_once '../../config/config.inc.php';
$oContext = &Context::getInstance();
$oContext->init();

// 키워드 수집 대상 : tag or title
$target = $_REQUEST['target'];

// 캐시 타임 설정
if ( !$_REQUEST['cache_time'] )
{
    $cache_time = 0;
}
else
{
    $cache_time = 60 * (int)$_REQUEST['cache_time'];
}

// 자동완성 JSON 데이터 반환용 더미 변수
$autocompleteIS = array();

$args = new stdClass();
$args->list_count = (int)$_REQUEST['list_count'];
$args->module_srl = $_REQUEST['module_srl'];


// 캐시 관련 ( 캐시 설정이 되어있는 상태에서 캐시가 만료 되지 않았을 경우에는 캐시에서 데이터를 취득 )
$oCacheHandler = CacheHandler::getInstance();

// 사이트가 캐시를 지원하고 자동완성 캐시가 존재 할 때
if( $cache_time && $oCacheHandler->isSupport() && $oCacheHandler->isValid('autocompleteIS', $cache_time) )
{
    // 캐시를 가져와서 더미 변수에 입력
    $cache = $oCacheHandler->get('autocompleteIS', $cache_time);
    $autocompleteIS = $cache;
}
// 캐시가 만료 되거나 캐시 시간이 설정 되어 있지 않는 경우 DB를 통해 데이터 취득
else
{
    // 쿼리로 게시물 데이터 가져오기
    if ( $target === 'tag' )
    {
        $output = executeQueryArray('addons.ap_autocompleteIS.getTagList', $args);
    }
    else if ( $target === 'title' )
    {
        $output = executeQueryArray('addons.ap_autocompleteIS.getDocumentTitle', $args);
    }

    // 결과 값이 있을 때 해당 키워드를 더미 변수에 저장 && 중복값 회피
    if( $output->toBool() && $output->data )
    {
        foreach( $output->data as $val ) {
            if ( !in_array($val->$target, $autocompleteIS) )
            {
                $autocompleteIS[] = $val->$target;
            }
        }
        // 캐시를 지원하고 캐시 타임이 설정 되어 있을 경우 수집된 데이터를 캐시로 만들기
        if( $oCacheHandler->isSupport() && $cache_time !== 0 )
        {
            $oCacheHandler->put('autocompleteIS', $autocompleteIS, $cache_time);
        }
    }
}

echo json_encode($autocompleteIS, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE);

unset($output);
unset($args);
unset($autocompleteIS);

$oContext->close();

?>

 

  • profile
    기진곰 2018.06.09 14:24:09

    캐시핸들러 호출 방식은 맞습니다. 그러나

     

    1. 달라질 가능성이 있는 값에 따라 캐시 키(autocompleteIS)도 달라져야 합니다. 위의 소스를 보면 module_srl, list_count 등이 달라지는 것 같네요. 검색어(target) 변수는 어디에 들어가는지 잘 모르겠고요... 아무튼 데이터가 달라질 수 있는 경우의 수에 따라 캐시 키도 구분하여 써야 합니다. 엉뚱한 게시판의 검색 결과가 나올 수 있어요.

     

    2. isValid()는 쓰지 마세요. 그냥 get() 해서 값이 있으면 쓰고, 없으면 버리면 그만입니다. 예전에 이것과 관련해서 어디에 댓글을 단 적이 있는데 찾을 수가 없네요. isValid()는 값이 있는지 없는지 가볍게 체크하는 함수가 아니라, 기껏 데이터 불러와서 버리는 매우 비효율적인 함수입니다.

  • profile
    기진곰 2018.06.09 14:33:37

    그런데 서버 부하는 꼭 DB에 쿼리가 들어가지 않아도 ajax로 XE를 호출하는 것만으로도 상당히 많이 발생합니다. 인덱스 잘 타는 간단한 쿼리라면 ajax로 XE를 호출한다는 사실만으로 발생하는 부하가 95% 이상, DB 쿼리에서 발생하는 부하는 5% 미만이라고 보시면 됩니다. 캐시를 적용해도 5%밖에 아껴지지 않는 거죠.

     

    따라서 ajax 호출 빈도를 근본적으로 줄이는 것이 훨씬 더 중요합니다. 꼭 ajax로 로딩하지 않아도 되는 데이터는 최초 페이지 로딩시 소스에 박아버린다던가, 키보드 입력을 하는 도중에는 잠시 기다렸다가 마지막에 한꺼번에 로딩한다던가... 그런 면에서 focus 이벤트와 로컬저장소 사용은 아주 좋은 방법입니다^^

  • profile
    웹지기 2018.06.09 14:52:11
    캐시 만료기간 전까지는 아예 요청하지 않고 캐시된걸 가져가게 하면 되는거 아닌가요? 그냥 무식한 일반인의 궁금증 입니다.

    캐시유지 기간이 20분 이라면..
    1명이 요청한 것으로 수천명이 사용하게 되는 즉 수천의 요청이 사라지는...
    이런것이 요청회수를 극적으로 줄이는 방법으로 이해했거든요.
  • profile
    기진곰 2018.06.09 14:57:58

    브라우저 캐시와 로컬저장소는 사용자마다 따로 있기 때문에, 말씀하신 것처럼 사용자들 사이에 공유되지는 않아요. 서버에 저장된 캐시는 모든 사용자가 서버에 요청해서 불러와야 하지요. json 데이터만 별도 파일로 만들어서 어디 CDN에다가 올려놓는다면 몰라도...

  • profile
    웹지기 2018.06.09 14:59:49
    로컬 저장소로 가지고 오는 데이터 자체를 서버에서 캐시해 둔 것을 가져온다는 것인데요.
    캐시해 놓은 것을 달라고 하는 것 자체가 요청인가요? 그럼 요청 횟수를 줄이는게 캐시의 역할이 되지 못하네요.

    그럼 요청 자체를 줄이는 방법은 없는거 아닌가요?
  • profile
    윤삼 2018.06.09 15:06:41

    캐시 요청마저도 1인당 1회로 한정하도록 하게 하는 게 로컬 저장소 담당이라 보시면 될 것 같아요.

  • profile
    웹지기 2018.06.09 15:08:39
    내 로컬저장소의 사용을 하는 이유는 알고 있는데요.

    제가 우려하는 것은 로컬저장소 사용을 하지 못하는 특수한 경우 계속 반복적인 요청의 경우 캐시가 도움이 될 줄 알았는데 그게 아니라 질문을 드렸어요,

    사실 제가 얼마전에 503을 만난건 부하가 굉장히 걸렸다기 보다는 우연하게 재수없게 내 차례에서 딱 끊어지고 만 그런 상황이었거든요.
  • profile
    기진곰 2018.06.09 15:07:59
    네, 뭘 달라고 하면 그게 요청(request)이지요. 서버에서 캐시를 쓰는 것은 수많은 요청을 조금이라도 더 빨리 처리하기 위한 방법이지, 요청 자체를 줄이는 방법은 아닙니다.

    요청 자체를 줄이려면 위의 댓글에 쓴 것처럼 아예 요청할 필요가 없도록 애드온에서 쓸 데이터를 페이지 소스에 박아버리거나, 아니면 json 파일을 따로 만들어 CDN으로 넘겨서 서버에 직접 요청할 필요가 없도록 해야 합니다. 물론 후자의 경우에는 서버 대신 CDN이 요청을 왕창 받겠지요.
  • profile
    웹지기 2018.06.09 15:11:25
    결론적으로는 지금 상황에서 캐시만 적용하면
    요청하는 횟수는 똑 같기 때문에 많은 요청이 몰려서 503이 발생하는 건 여전하다는 것이네요.
  • profile
    기진곰 2018.06.09 15:14:38
    캐시 사용: 1%
    로컬저장소 사용: 9%
    검색창에 focus 이벤트가 발생하기 전에는 아예 요청을 안 하도록 변경: 90%
    의 효과가 있을 것으로 예상됩니다.
  • profile
    웹지기 2018.06.09 15:17:53
    아... 지금 포커스 이벤트 전에 요청을 하는군요. 실제 검색할 의사가 있는 사람 포커스 이벤트가 발생할때 요청을 하도록 하면 굉장한 요청이 줄겠네요.
  • profile
    기진곰 2018.06.09 15:19:57
    네, 이것도 "요청 자체를 줄이는 방법"의 일종이라고 할 수 있겠네요 ㅎㅎ
  • profile
    윤삼 2018.06.09 15:30:46
    이렇게 해서 0.1.5를 올리게 됐습니다. 헉헉;;;
    https://xetown.com/rxe_point/989428
  • profile
    웹지기 2018.06.09 15:34:29
    통합검색은 접근 자체가 거의 없어서 문제가 없더라구요 ㅋ
    업그레이드 감사합니다!
  • profile
    윤삼 2018.06.09 15:41:05
    아, 게시판 검색;;;

    일단 0.1.6 버전 기준으로 js 파일에서 169행, 173행, 180행을 각각

    keyword.attr('autocomplete', 'off')one('focus', function() {
    autoComplete($(this), 'title');
    });

    keyword.attr('autocomplete', 'off')one('focus', function() {
    autoComplete($(this), 'nick_name');
    });

    keyword.attr('autocomplete', 'off')one('focus', function() {
    autoComplete($(this), 'tag');
    });

    로 바꾸시면 임시방편은 될 것 같습니다.
  • profile
    웹지기 2018.06.09 16:27:08
    말씀하신대로 고쳤더니 에러가 나네요.

    // 페이지 로드 시, 검색 대상 확인 후 애드온 설정에 따라 자동완성 키워드 수집
    if ( (target.val() === 'title_content' && autocomplete_title_content === 'Y')
    || (target.val() === 'title' && autocomplete_title === 'Y') )
    keyword.attr('autocomplete', 'off')one('focus', function() {
    autoComplete($(this), 'title');
    });
    autoComplete(keyword, 'title');
    }
    else if ( target.val() === 'nick_name' && autocomplete_nick_name === 'Y' )
    keyword.attr('autocomplete', 'off')one('focus', function() {
    autoComplete($(this), 'nick_name');
    });
    autoComplete(keyword, 'nick_name');
    }
    else if ( (target.val() === 'title_content' && autocomplete_title_content === 'T')
    || (target.val() === 'title' && autocomplete_title === 'T')
    || (target.val() === 'content' && autocomplete_content === 'T')
    || (target.val() === 'tag' && autocomplete_tags === 'Y') )
    keyword.attr('autocomplete', 'off')one('focus', function() {
    autoComplete($(this), 'tag');
    });
    autoComplete(keyword, 'tag');
    }
  • profile
    윤삼 2018.06.09 16:28:53
    아이고, 죄송해요.
    one 앞에 마침표(.)가 빠졌어요;;;
  • profile
    윤삼 2018.06.09 16:30:50
    요걸로 해보세요;;

    // 페이지 로드 시, 검색 대상 확인 후 애드온 설정에 따라 자동완성 키워드 수집
    if ( (target.val() === 'title_content' && autocomplete_title_content === 'Y')
    || (target.val() === 'title' && autocomplete_title === 'Y') )
    keyword.attr('autocomplete', 'off').one('focus', function() {
    autoComplete($(this), 'title');
    });
    }
    else if ( target.val() === 'nick_name' && autocomplete_nick_name === 'Y' )
    keyword.attr('autocomplete', 'off').one('focus', function() {
    autoComplete($(this), 'nick_name');
    });
    }
    else if ( (target.val() === 'title_content' && autocomplete_title_content === 'T')
    || (target.val() === 'title' && autocomplete_title === 'T')
    || (target.val() === 'content' && autocomplete_content === 'T')
    || (target.val() === 'tag' && autocomplete_tags === 'Y') )
    keyword.attr('autocomplete', 'off').one('focus', function() {
    autoComplete($(this), 'tag');
    });
    }
  • profile
    웹지기 2018.06.09 16:34:56
    // 페이지 로드 시, 검색 대상 확인 후 애드온 설정에 따라 자동완성 키워드 수집
    if ( (target.val() === 'title_content' && autocomplete_title_content === 'Y')
    || (target.val() === 'title' && autocomplete_title === 'Y') )
    keyword.attr('autocomplete', 'off').one('focus', function() {
    autoComplete($(this), 'title');
    });
    } <---- Uncaught SyntaxError: missing ) after argument list 라고 에러가 나요.
    else if (
  • profile
    윤삼 2018.06.09 16:43:05

    에잉? 중간에 중괄호가 사라졌는데요?

    이걸로 해주세요

        // 페이지 로드 시, 검색 대상 확인 후 애드온 설정에 따라 자동완성 키워드 수집
        if ( (target.val() === 'title_content' && autocomplete_title_content === 'Y')
            || (target.val() === 'title' && autocomplete_title === 'Y') )
        {
            keyword.attr('autocomplete', 'off').one('focus', function() {
                autoComplete($(this), 'title');
            });
        }
        else if ( target.val() === 'nick_name' && autocomplete_nick_name === 'Y' )
        {
            keyword.attr('autocomplete', 'off').one('focus', function() {
                autoComplete($(this), 'nick_name');
            });
        }
        else if ( (target.val() === 'title_content' && autocomplete_title_content === 'T')
            || (target.val() === 'title' && autocomplete_title === 'T')
            || (target.val() === 'content' && autocomplete_content === 'T')
            || (target.val() === 'tag' && autocomplete_tags === 'Y') )
        {
            keyword.attr('autocomplete', 'off').one('focus', function() {
                autoComplete($(this), 'tag');
            });
        }

     

  • profile
    웹지기 2018.06.09 15:41:27
    저희 검색 전용페이지 더미 검색창에서 동작하지 않는 것 같은데요.
    동작하는 class 같은게 있나요? 알려주시면 추가하겠습니다.
  • profile
    윤삼 2018.06.09 15:47:25
    /ssearch 말씀하시는 거죠? 로컬 저장소 비우고 다시 시도해보신 건가요?
    들어가서 보니까 무리 없이 동작하는 거 같은데요.
  • profile
    웹지기 2018.06.09 15:48:10
    로컬저장소 비우는건 못해봤습니다. 저는 되다가 반대로 안되던데요.. 흠....
  • profile
    웹지기 2018.06.09 16:00:39
    지연시간과 관련이 있을 것 같은데요. 방식이 바뀌었어도 구글검색창이 나타난 다음 함수가 실행되어야 하나요??

    지금 지연시간 설정한 것보다 타이핑이 먼저 들어가면 안되는 것 같은 느낌입니다.
  • profile
    웹지기 2018.06.09 16:05:56
    이전처럼 구글검색에서는 지연지키는js를 아닌 검색창에서는 지연없는 js 를 로드하도록 처리하겠습니다.
  • profile
    윤삼 2018.06.09 16:28:05
    음, focus 이벤트를 써서 그런지 웹지기님 구글검색에서는 그래도 지연시간 문제가 나타나긴 하네요. 다만 제가 테스트하고 있는 구글검색에서는 별다른 무리가 없는 편인데요.
    차이가 있다면 저는 지연시간을 기본값인 2000으로 설정한 거고 웹지기님은 4000
    그리고 수집된 태그 숫자가 웹지기님은 5000개 문서에서 36598개고 저는 10000개 문서에서 13000개예요.
    거기서 오는 차이 같기도 하고 그러네요.
  • profile
    웹지기 2018.06.09 16:50:34
    약간 다른 문제구요.
    저희 /ssearch 검색창은 구글검색창을 불러오는게 아니다 보니 실제 지연시간을 주면 타이핑 시도하는 시간보다 늦게 js 가 동작해서 문제가 발생되는 것이라서요.

    이번에 그것이 필요없나 해서 그냥 지연 있는 채로 해 봤더니 문제가 되네요.
    구글검색을 불러오는 곳에서는 2초,4초 때문에 큰 차이는 없습니다.

    구글 검색이 아닌 검색청은 워낙 빠르게 불러오기 때문에 js가 시작 안해서 문제가 되는것이라서요.
    특수한 경우이긴 하죠. 저희는 검색창을 다르게 쓰니까요.

    구글검색이 아닌 페이지에서는 js에 지연시간 없도록 하면 문제 없이 작동합니다~
  • profile
    윤삼 2018.06.09 15:05:09
    3. 보통은 autocomplete 관련 소스나 팁을 보면 keyup? keydown? 이벤트로 ajax 호출을 하던데요. 안 그래도 이게 효율이 떨어지는 것 같아서, 최초 시도에만 호출을 하고, 로컬 저장소에 json object 형태로 변환 가능한 문자열로 담아 두었다가, 클라이언트단에서 자동완성으로 뿌려주는 방식으로 짰었어요. 그리고 target 변수(tag, title)를 json 데이터에 키 값(?)으로 설정해놔서 검색대상에 따라 keywords.tag 또는 keywords.title가 없는 경우에만 서버에 호출해서 받아오는 방식이죠. 말씀하신대로 tag로 설정해놨는데 title이 주르륵 뜬다든가 엉뚱한 검색어가 뜨지 않도록 하기 위해서요.

    1. 그리고 module_srl, list_count는 애드온 파일 -> js -> php 이런 경로로 넘어온 건데요. 통합검색이다보니 module_srl은 현재 모듈값이 아니라 통합검색 모듈에서 설정된 대상 모듈값(아, 차라리 target_srls로 이름을 바꾸는 게 낫겠네요)이어서 굳이 캐시 키로 구분 안 해도 되지 않나 싶어서 뺐었습니다. 그리고 어지간한 건 클라이언트의 로컬 저장소의 json object를 확인하도록 해놔서 캐시는 관리자가 설정해놓은 상태의 쿼리 결과만 저장해두면 되겠다 싶기도 하구요.

    2. $oCacheHandler->isValid('autocompleteIS', $cache_time) 대신 $oCacheHandler->get('autocompleteIS', $cache_time)으로 바꾸면 되는 거겠죠? 감사합니다~!
  • profile
    기진곰 2018.06.09 15:09:32

    네, 그리고 get하신 결과를 if문에서 참/거짓 구분하는 용도(33줄)로만 사용하지 마시고 실제 변수에 저장해 두었다가 if문 안에서(36줄) 재사용하시면 됩니다. 동일한 키를 두 번 get할 필요는 없으니까요.

     

    keyup/keydown으로 하면 난리나죠;;; 사람들 타자 속도가 얼마나 빠른데... ㅋㅋㅋ

  • profile
    Paul 2018.06.09 16:17:15
    위에서 대화하신 내용들이 통합말고 "게시판 검색어 자동완성 애드온"과는 상관이 없는건가요? 그러니깐 이 애드온은 통합검색보다는 서버에 부담을 덜 주는지 궁금합니다. 일단 "게시판 검색어 자동완성 애드온"만 적용해서 사용을 해보고 있는데 부담없이 잘 작동하는 것 같아서요. 암튼 이 자리를 빌어 윤삼님께 감사 드립니다! ^^
  • profile
    윤삼 2018.06.09 16:33:50
    게시판 검색어 자동완성 애드온과도 관계가 있습니다.
    게시판 로딩할 때마다 ajax 호출이 되는 거니까요ㅜ

    위에서 이야기한대로 게시판 검색어 자동완성 애드온은 일단 (0.1.6 버전 기준으로) js 파일에서 169행, 173행, 180행을 각각

    keyword.attr('autocomplete', 'off').one('focus', function() {
    autoComplete($(this), 'title');
    });

    keyword.attr('autocomplete', 'off').one('focus', function() {
    autoComplete($(this), 'nick_name');
    });

    keyword.attr('autocomplete', 'off').one('focus', function() {
    autoComplete($(this), 'tag');
    });

    로 바꾸시면 임시방편은 될 것 같습니다.
  • profile
    Paul 2018.06.09 16:39:27
    아... 그렇군요. ^^ 아마도 문서와 태그 숫자가 적어서 부담없이 작동하나 봅니다. 알려주신 대로 바로 적용하겠습니다. 감사합니다.
  • profile
    웹지기 2018.06.09 16:45:17
    이제 이해했습니다.
    170행,174행,181행을 교체하라고 하시면 될 것 같습니다.

    {
    교체대상
    }


    { 를 교체하라고 하셔서 그걸 교체했네요.
  • profile
    윤삼 2018.06.09 16:46:55
    제가 가지고 있던 파일이 다소 다른 버전이었나봐요ㅜ
  • profile
    Paul 2018.06.09 16:50:47
    저는 169행, 173행, 180행이 맞던데요? ^^ 암튼 알려주신 대로 변경하였는데 이상없이 동작합니다. 감사합니다!!! ^^
  • profile
    웹지기 2018.06.09 16:46:17
    게시판 검색 애드온도 실제 검색대상자들에게만 요청이 되게 수정하고 다시 가동해 보겠습니다.
  • profile
    forest 2018.06.09 18:04:16
    덕분에 또 새로운걸 많이배워갑니다 ^^
  • profile
    윤삼 2018.06.09 21:58:50
    게시판 검색어 자동완성 애드온은 focus 이벤트만 적용하고 0.1.7로 버전업해서 올렸습니다.
    https://xetown.com/rxe_point/986687
    1
  • profile
    Paul 2018.06.09 23:15:53
    @윤삼 조금전에 자료실에서 0.1.7 버전을 다운받아서 0.1.6 에 그대로 덮어씌웠습니다. 그런데 PC 에서는 잘 작동하는데, 모바일에서는 안되네요... 0.1.6 에서는 모바일에서도 잘 됐는데 말이죠. 저도 조금 있다가 확인해 보겠지만 죄송하지만 확인 부탁드립니다.
  • profile
    윤삼 2018.06.09 23:22:21
    테스트로는 잘 되는데요ㅜ 혹시 로컬 저장소에 데이터를 비웠다가 다시 시도해보셨나요?
  • profile
    Paul 2018.06.09 23:25:02
    js 파일 내 url 들 중에 "./common... 과 같이 앞에 점(dot)을 붙여주니 작동을 잘 하네요. ^^ (생각해 보니 0.1.6 버전에서도 제가 점을 붙여줬었습니다. ^^;;) 다시한번 좋은 애드온 만들어 주셔서 감사합니다!! ^^
  • profile
    윤삼 2018.06.09 23:40:44
    아, 확인 감사합니다. 다음 버전에는 꼭 점 넣을게요 :)
    1
  • profile
    구름이 2018.06.11 18:08:01
    수고 많으세요!^^
  • profile
    윤삼 2018.06.11 18:09:42
    이번에 경험치 좀 획득했죠ㅋㅋ

서버에 요청 중입니다. 잠시만 기다려 주십시오...