생활코딩

Coding Everybody

Caching

토픽 생활코딩 > 서버 > PHP > CodeIgniter

Caching

캐슁이란 저장한다는 뜻이다. 컴퓨팅에서 캐슁이란 오랜시간이 걸리는 작업의 결과를 저장해서 시간과 비용을 필요로 회피하는 기법을 의미한다. 캐슁은 고성능 에플리케이션을 만드는데 가장 중요한 요소 중의 하나다. 

다음은 캐슁의 대표적인 사례다.

  • 시험을 볼 때 원리를 이해하고 문제를 푸는 것이 아니라, 덤프를 외워서 답안을 작성한다. 
  • 웹브라우저는 한번 다운로드 한 이미지 파일은 임시저장 디렉토리에 저장했다가 다음 요청이 있을 때 다운로드 하지 않고 다운받아 둔 이미지를 사용한다.
  • 웹페이지를 탐색할 때 도메인을 이용하면 내부적으로는 그 도메인에 해당하는 IP를 알아내기 위해서 네임서버에 접속을 한다. 이 때 네임서버가 알려준 IP 주소를 운영체제나 브라우저는 그 결과를 기억하고 있다가 동일 도메인에 대한 접근 시에 저장된 IP 주소를 사용한다.
  • CPU는 연산의 결과를 빠르게 저장하기 위해서 메인 메모리 보다 빠른 캐쉬 메모리를 사용해서 빠르게 작업을 처리한다. 

Caching의 예

예를들어 heavy_job이라는 무거운 작업이 있다. 이 작업을 처리하는데 24시간이 걸린다고 치자. 이 작업을 매번 수행하면 많은 시간과 자원이 필요할 것이다. 그 결과를 저장했다가 동일한 처리를 할 때 그 결과를 사용할 수 있다. 아래 코드를 보자. 

<?php
function heavy_job($input){
    sleep(5); // 5초가 걸리는 작업
    return $input+1;
}

$cache = array();
$input = 10;
if(empty($cache[$input])){
    $cache[$input] = heavy_job($input);
}
echo $cache[$input];

if(empty($cache[$input])){
    $cache[$input] = heavy_job($input);
}
echo $cache[$input];

?>

위의 코드는 캐쉬의 기본적인 메커니즘을 보여준다. 시간이 오래 걸리는 heavy_job이라는 함수를 호출한 후에 그 결과를 $cache 배열에 담고 같은 작업이 실행될 때 $cache에 결과가 저장되어 있는지를 확인한다. 저장되어 있지 않다면 heavy_job을 실행 한 후에 그 결과를 $cache에 담고, 저장되어 있다면 저장된 결과를 그냥 출력한다. 이를 통해서 동일한 연산이 반복되는 것을 피할 수 있다. 

Caching의 난제

캐쉬의 가장 큰 문제는 연산의 결과가 달라졌을 때의 처리다. 웹브라우저 캐쉬를 예로 들어보자. 생활코딩의 로고를 임시저장소에 저장했고, 생활코딩에 접속 할 때 임시저장소의 이미지를 사용하고 있는 상황에서 생활코딩의 로고가 리뉴얼되서 새로운 것으로 바꼈다면 어떻게 될까? 이것이 캐쉬의 일반적인 문제인 '갱신의 어려움'이다. 

TTL

TTL은 Time To Live라는 의미로 캐쉬를 생성할 때 캐쉬의 만료기간을 정해두는 것이다. 지정된 만료일이 지나면 캐쉬를 삭제하고 다시 캐쉬를 생성하는 기법이다. 

캐쉬의 명시적인 삭제

캐쉬가 유효하지 않을 때 캐쉬를 명시적으로 삭제해서 새로운 캐쉬가 만들어지도록 하는 것이다. 

고성능 웹에플리케이션을 위한 캐슁

고성능 웹에플리케이션을 만들기 위해서는 캐쉬가 매우 중요하다. 웹에플리케이션은 다양한 측면에서 캐쉬 메커니즘이 사용되는데 어떤것이 있는지 알아보자. 

Web Caching

앞서서 언급한 것과 같이 이미지나 자바스크립트와 같은 파일을 웹브라우저가 캐쉬하도록 하는 것이다. 이를 위해서는 웹서버에서 캐쉬 대상이 되는 데이터에 대한 특별한 처리가 필요하다. 아래는 Web Caching와 관련된 웹서버 별 설정이다. 

HTTP Reverse Proxy Caching

리버스 캐슁이란 웹서버로 유입되는 HTTP 트래픽을 캐슁 시스템이 저장하고 있다가 동일 요청이 들어왔을 때 캐슁 시스템이 이 데이터를 돌려줌으로서 빠른 응답 성능을 제공하는 방법이다. 아래의 이미지는 Reverse Proxy Caching의 메커니즘을 보여주는 그림이다. 

웹서버들은 기본적으로 리버스 프록시 기능을 제공한다. 아래 링크는 웹서버들의 리버스 프록시 사용법들이다.

리버스 프록시를 전문적으로 수행하는 솔루션들이 있다. 이런 솔루션들은 웹서버 보다 더 다양하고 높은 성능을 제공하는 것으로 알려져있다. 

OPCODE Caching

OPCODE는 기계어의 로우레벨에서 컴퓨터 상에 하달되는 일종의 신호인데, 이 신호를 캐슁하는 기법이다. PHP와 같은 인터프리터 언어의 성능상의 단점을 비약적으로 향상시켜준다. 이름과는 다르게 설치해서 사용하기 매우 쉽다. 아래는 OPCODE Caching을 지원하는 PHP 가속기들의 리스트. 필자는 APC를 추천한다. 

http://en.wikipedia.org/wiki/List_of_PHP_accelerators

Web Page Caching

우리가 배울 내용이다. Page Caching은 페이지 전체를 캐슁하는 방법인데, CodeIgniter는 Page Caching 기능을 제공해서 동일 요청 페이지에 대한 캐슁을 생성해서 빠른 속도로 페이지를 제공할 수 있다. 

CodeIgniter에서 웹페이지 아래의 코드를 컨트롤러에 추가하면 된다. 아래 코드가 실행되면 /application/cache/ 디렉토리에 캐쉬가 만들어진다. 한번 캐쉬가 만들어지만 이후 동일한 URL로 진입하는 요청은 캐쉬 파일의 내용을 읽어서 사용하게 되고 처리가 종료되기 때문에 성능 향상에 도움이 된다. 인자 n은 캐쉬를 유지할 시간(TTL)으로 단위는 분이다. 자세한 내용은 메뉴얼을 참고하자.

$this->output->cache(n);

Partial Caching

이 역시 우리가 배울 내용이다. (주로 데이터베이스의) 데이터를 저장했다가 동일 요청이 있을 때 저장된 데이터를 사용함으로서 데이터베이스에 부담을 경감시키고 반응속도를 높여서 사용자 경험을 향상시킨다. 

아래 예제는 캐쉬 저장소로 일차적으로 APC를 사용하고 APC를 사용할 수 없을 때 파일을 사용하도록 구성한 예제다. 자세한 내용은 캐쉬 클래스 메뉴얼을 참고하자. 

$this->load->driver('cache', array('adapter' => 'apc', 'backup' => 'file'));

if ( ! $foo = $this->cache->get('foo'))
{
     echo 'Saving to the cache!<br />';
     $foo = 'foobarbaz!';

     // Save into the cache for 5 minutes
     $this->cache->save('foo', $foo, 300);
}

echo $foo;

Database Caching

데이터베이스에 대한 캐슁이다. SQL 요청에 대한 결과를 저장 했다가 동일 요청이 있을 때 저장된 결과를 제공한다. 자세한 내용은 데이터베이스 캐쉬에 대한 CI 메뉴얼을 참고한다.

캐쉬의 저장소

캐쉬 데이터를 어디에 저장할 것인가의 문제는 어떤 방식의 캐쉬를 사용할 것인가 만큼 중요한 문제다. 다음은 캐쉬를 저장하는 주요 저장소이다. 

파일

캐슁을 저장할 때 가장 기본적으로 고려되는 저장소다. 장점은 저렴하다는 점이다. 단점은 캐쉬 데이터를 여러 시스템에서 공유하기가 어렵다는 점과 메모리 대비 느리다는 점을 들 수 있다. 또한 캐쉬 메커니즘을 직접 구현해야 하는 어려움이 있다. 

메모리

대용량의 메모리를 지원하는 하드웨어가 늘어나면서 메모리를 캐쉬 저장소로 이용하는 사례가 많아지고 있다. 특히 Memcached와 같은 솔루션을 이용하면 캐쉬의 생성, 소멸을 솔루션에게 위임할 수 있고, 또 네트워크를 통해서 접근 하는 기능을 지원하기 때문에 단일 캐쉬에 대해서 여러 머신에서 엑세스 할 수 있다는 장점이 있다. 무엇보다도 큰 장점은 파일 보다 훨씬 빠르게 데이터를 처리 할 수 있다는 점이다. 단점은 비싸다.

데이터베이스

데이터베이스도 데이터를 캐쉬하기에 좋은 공간이다. 자체적인 보안 시스템을 갖추고 있고, 네트웍을 통해서 접근 할 수 있기 때문에 캐슁 데이터를 공유할 수 있는 장점도 있다. 메모리 보다 느린 것이 단점이다.

예제

이번 예제는 캐쉬를 사용해서 사용자의 대기시간을 줄이면서 시스템에 대한 부담도 줄이는 방법에 대해서 알아본다. 

웹페이지 캐슁의 도입

캐쉬는 평시에도 중요 하지만 트래픽이 급증했을 때도 매우 중요하다. 서비스의 트래픽은 전반적으로 높아지기도 하지만, 특정 컨텐츠가 매스 미디어에 노출되는 등의 이유로 폭발적으로 급증하는 경우도 있다. 이런 경우 고려해볼만한 방법이 웹페이지 캐슁이다. 웹페이지를 캐슁하면 복잡한 프로세스를 타지 않고 파일에 저장된 내용을 그대로 전송하기 때문에 훨씬 많은 트래픽을 감당할 수 있다. 문제는 페이지 전체가 캐슁 되기 때문에 로그인한 사용자에 따라서 다르게 보여져야 할 부분이나 수정된 컨텐츠의 내용을 반영하기가 어렵거나 불가능하다. 이런 경우 폭증한 요청에 대해서만 웹페이지 캐슁을 사용한다면 전반적인 서비스의 품질을 유지하면서도 높은 응답속도를 유지할 수 있을 것이다.

/application/config/config.php

/*
|--------------------------------------------------------------------------
| 부분 웹 페이지 캐슁
|--------------------------------------------------------------------------
|
| 본 캐쉬는 특정한 페이지에 대해서만 web caching을 적용한다. 
| 값이 없거나 false면 페이지를 캐쉬하지 않는다. 
| 캐쉬의 지속시간은 5분이다. 
| 
*/
$config['peak_page_cache'] = 'http://ooo2.org/index.php/topic/get/9';

/application/core/MY_Controller.php

function __construct()
{
    parent::__construct();
    if($peak = $this->config->item('peak_page_cache')){
        if($peak == current_url()){
            $this->output->cache(5);
        }
    }
    $this->load->database();
    if(!$this->input->is_cli_request())
        $this->load->library('session');      
}

부분 캐쉬

웹페이지 캐쉬는 모든 페이지를 캐쉬하기 때문에 데이터의 갱신이 자주 일어나지 않고, 사용자 인증 기능이 없는 페이지를 캐슁하는데 적합하다. 하지만 일부 데이터만을 저장할 때는 다른 방법을 사용해야 한다. 

우리의 예제에서 모든 페이지에서 공통적으로 사용되는 부분은 사이드 바의 토픽 리스트다. 현재 예제에서는 토픽 리스트를 출력하기 위해서 비교적 간단한 작업을 하지만, 이 작업이 매우 복잡한 연산과 데이터베이스 질의를 해야 하는 로직이라고 상상해보자. 다음 코드는 5분에 한번씩 사이드 바의 데이터를 서버에서 가져오는 방법을 보여준다. 

아래 코드에서 $this->load->driver 부분은 나중에 부분 캐쉬를 제어하기 위해서 추가된 부분이다. 뒤에서 살펴볼 것이다. 

/application/core/MY_Controller.php

부분 캐쉬의 데이터 갱신

캐슁의 가장 큰 난제가 데이터를 갱신하는 문제다. 즉 데이터는 변했는데 캐쉬는 여전히 살아있는 문제다. 그래서 TTL이라는 것이 있지만, TTL이 만료되기 전까지는 예전의 데이터가 계속 보인다는 것이 문제다. 이 때 사용할 수 있는 것이 데이터의 업데이트가 이루어졌을 때 해당 캐쉬를 명시적으로 삭제(무효화)시키는 것이다. 이 방법을 알아보자. 

삭제 기능 구현

겸사겸사 삭제 기능을 구현해보자. 우선 삭제 버튼을 먼저 만들자. 아래 코드를 보면 알겠지만 삭제 버튼은 추가 버튼과 다르게 링크를 사용하지 않았다. 왜 그럴까? 삭제는 특정 페이지로 사용자를 이동시키는 것이 아니고, 삭제라는 작업(operation)이기 때문이다. 작업을 위한 요청을 할 때는 delete/2 와 같은 방법을 사용하지 않는다. post 방식으로 데이터를 전송해야 한다. 

/application/views/get.php

<div>		
	<form action="/index.php/topic/delete" method="post">
		<input type="hidden" name="topic_id" value="<?=$topic->id?>" />
		<a href="/index.php/topic/add" class="btn">추가</a>		
		<input type="submit" class="btn" value="삭제" />
	</form>
</div>

위에서 삭제버튼을 누르면 /index.php/topic/delete 페이지로 사용자가 이동하게 된다. 따라서 컨트롤러 topic에 delete 메소드를 추가해보자. 

function delete(){
    $topic_id = $this->input->post('topic_id');
    $this->_require_login(site_url('/topic/get/'.$topic_id));
    $this->load->model('topic_model');
    $this->topic_model->delete($topic_id);
    redirect('/');
}

$this->_require_login 메소드가 있으면 로그인 여부를 검사해서 로그인 되어 있지 않다면 로그인 페이지로 사용자를 이동시킨다. 이 때 인자로 전달된 값으로 로그인 후 사용자를 돌려보낸다. 이 메소드는 topic 컨트롤러의 add 메소드에서도 사용하기 때문에 add 메소드의 내용을 아래와 같이 수정한다. 

function add(){
 
    // 로그인 필요
 
    $this->_require_login(site_url('/topic/add'));
 
    $this->_head();
    $this->_sidebar();

그리고 _require_login 메소드를 전역적으로 사용하기 위해서 MY_Controller에 포함시킨다. 

/application/core/MY_Controller.php

function _require_login($return_url){
    // 로그인이 되어 있지 않다면 로그인 페이지로 리다이렉션
    if(!$this->session->userdata('is_login')){
        $this->load->helper('url');
        redirect('/auth/login?returnURL='.rawurlencode($return_url));
    }
}

/application/models/topic_model.php

그리고 topic 컨트롤러의 delete 메소드에서 topic 데이터를 삭제하기 위해서 topic_model 모델에 delete라는 이름의 메소드를 추가하자. 

function delete($topic_id){
    return $this->db->delete('topic', array('id'=>$topic_id));
}

부분 캐쉬의 적용

이렇게 해서 삭제와 관련된 부분을 구현했다. 그럼 이제 본격적으로 캐쉬를 적용해보자.

캐쉬를 하기 위해서는 캐쉬 라이브러리 클래스를 로드하는 것이 우선이다. 캐쉬는 에플리케이션 전반에서 사용할 것이기 때문에 컨트롤러의 공통조상인 MY_Controller.php에 캐쉬 로드 로직을 추가해야 한다. 이 부분은 위에서 언급한 바 있다. 

그 다음에는 캐쉬가 필요한 부분에 적용을 해야 하는데, 우리는 사이드 바의 토픽 리스트를 캐쉬해보자. 이를 위해서 아래의 코드의 코드를 보자. 

/application/controllers/topic.php

function _sidebar(){
    if ( ! $topics = $this->cache->get('topics')) {
        $topics = $this->topic_model->gets();    
        $this->cache->save('topics', $topics, 300);
    }
    $this->load->view('topic_list', array('topics'=>$topics));
}

위의 로직은 topics에 대한 정보를 데이터베이스로부터 알아오는 로직에 캐쉬를 적용한 것이다. $this->cache->get('topic's)를 통해서 topics라는 파일이 존재하는지 확인한 후에 만약 존재하지 않는다면 $this->topic_model->gets()를 이용해서 토픽 정보를 가져오고, $this->cache->save('topics', $topics, 300);를 이용해서 topics 파일에 300초의 TTL 타임으로 $topics 안의 토픽 리스트 정보를 저장한다. 동일 요청이 있을 때는 캐쉬된 데이터를 가져오는 메커니즘이다. 

캐쉬 무효화

$this->cache->save의 세번째 인자로 300의 값을 전달했는데, 이것은 300초 후에는 캐쉬를 새로 생성한다는 의미다. 하지만 300초가 데이터가 변경되었다면 사용자에게는 잘못된 정보가 전달된다. 이것을 방지하기 위해서 캐쉬의 변화가 필요할 때 즉각적으로 캐쉬를 갱신하는 것을 캐쉬의 무효화 또는 삭제라고 한다. 캐쉬를 삭제하는 방법을 알아보자. 

우리 예제에서 캐쉬의 무효화가 필요한 경우는 크게 토픽의 수정,삭제,추가다. 이러한 작업이 일어났는데도 사이드 바의 토픽 리스트가 그대로 출력되면 곤란하다. 수정 기능은 구현하지 않았기 때문에 토픽의 삭제와 추가 시에 캐쉬를 무효화하는 방법에 대해서 알아보자. 아래는 topic 컨트롤러에서 캐쉬를 삭제하는 방법에 대한 예제다. $this->cache->delete 부분을 주의 깊게 살펴보자. 이 구문이 실행되면 /application/cache/topics 파일이 삭제되면서 캐쉬가 무효화 된다. 

/application/controllers/topic.php

function delete(){
    $topic_id = $this->input->post('topic_id');
    $this->_require_login(site_url('/topic/get/'.$topic_id));
    $this->load->model('topic_model');
    $this->topic_model->delete($topic_id);
    $this->cache->delete('topics');
    redirect('/');
}
function add(){
 
    // 로그인 필요
 
    // 로그인이 되어 있지 않다면 로그인 페이지로 리다이렉션
    $this->_require_login(site_url('/topic/add'));
 
    $this->_head();
    $this->_sidebar();
     
    $this->load->library('form_validation');
 
    $this->form_validation->set_rules('title', '제목', 'required');
    $this->form_validation->set_rules('description', '본문', 'required');
     
    if ($this->form_validation->run() == FALSE)
    {
         $this->load->view('add');
    }
    else
    {
        $topic_id = $this->topic_model->add($this->input->post('title'), $this->input->post('description'));
         
        // Batch Queue에 notify_email_add_topic 추가
        $this->load->model('batch_model');
        $this->batch_model->add(array('job_name'=>'notify_email_add_topic', 'context'=>json_encode(array('topic_id'=>$topic_id))));
 
        $this->cache->delete('topics');

        $this->load->helper('url');
        redirect('/topic/get/'.$topic_id);
    }
     
    $this->_footer();
}

태그

댓글

댓글 본문
  1. jeisyoon
    2021.08.25 Cashing - OK

    개발 환경이 너무 달라 중간에서 포기 하였지만 강의는 완강 하였습니다.
    낼 부터 CodeIgniter 4. CKEditor 5, Twitter Bootstrap 2, PHP 8 등
    새로운 개발 환경에서 다시 시작 할 것입니다, 감사합니다.
  2. 미댈
    감사합니다^^
    자바스크립트와 리눅스 쪽 공부하고 다시한번 봐야겠네요
  3. 구르마도리
    와 다봤다!! 오래된 강의라서 하나하나 수정해가면서 하느라 20일 걸렸네요
  4. 홍콩돼지
    야호 고생하셨습니다!
  5. 덩어리
    예제 하나하나에 고민하신 흔적이 보이네요.
    덕분에 필수적인 부분부터 수월하게 배울 수 있었던 것 같습니다.
    감사합니다!!
  6. lunaman
    감사합니다.

    무사히 마쳤네요.

    https://github.com......ter

    강의 따라 만든 교재 소스 공유합니다.
    오래된 동영상 강의라 중간중간에 문제있던 부분 수정해 가면서 만들었습니다.
    공부하시는데 참고하시기 바랍니다.
  7. ㅎㅎ
    고맙습니다
  8. JustStudy
    고맙습니다.
    (1 회차 완료)
  9. 멍뭉이jsi
    좋은 강의 잘 봤습니다~
  10. hax0r
    확실히 알찬 내용입니다.
  11. Sujin Heo
    강의 정말 잘 보았습니다. 요새 트렌드에 맞게 개발환경을 다 바꿀려고 생각하였는데 여기 강의때문에 쉽게 바꾼것 같아요. 그리고 필드에서 사용할 것들을 다 짚어주셔서 고민이 많이 줄었네요. 감사합니다. 학원 다녀서 들을 수 없는 것들도 쉽게 강좌를 들을 수 있어서 너무 좋아요. 필요한 것들을 원하는 시간에 골라볼 수 있다는 것이 바쁜 한국의 개발자들에게 진짜 도움이 될 것 같군요.
  12. 자바몬
    두번완강했네염
    재밌어서 계속보게되는 ㅎㅎ
    고급강의 진행중이신데 노고에 감사드려여~
  13. will
    완주했습니다. 훌륭한 강의 감사합니다

    //의견: 개발환경에선 캐싱기능을 꺼놓는게 좋을꺼같습니다.
  14. 3볼트
    완주..
  15. 육점이
    드디어 CI부분 수업을 완강했네요!!! 나름 빠른 속도로 강의를 듣고, 공부하고 있다고 생각했지만 다른 분들의 댓글을 보면 늘 겸허해 지네요.
    항상 좋은 강의 감사합니다.
  16. 인텔리푸
    완주 했네요...이제 고급으로...
    감사합니다.
  17. 은시앙
    delete() 함수 부분에서 리디렉션이 안되길래 봤더니 helper('url')가 로드가 안되있더군요..
  18. 샤핀
    드디어 코드이그나이터 완강했네요. 제가 집중력이 약해서 시간이 좀 걸렸는데 정말 유익했습니다.
  19. Jungsic Ahn
    재미있는 강의 감사드립니다. ㅎㅎ
  20. 김승갑
    3일동안 완독했네요 감사합니다.
  21. powerwithlove
    덕분에 완주했습니다.