Programming-[Backend]/php, codeigniter

생활코딩 codeigniter - 5. 이메일 전송, CLI, queue&cron, caching

컴퓨터 탐험가 찰리 2024. 4. 7. 15:51
728x90
반응형

1. 이메일 전송

 

이메일 라이브러리 사용

새로운 글이 등록되었을 때 이메일을 전송해준다는 기능을 가정하여 처리해본다. 기본적으로 제공하는 라이브러리를 사용하는 것이여서, 라이브러리를 사용하고 확장해보는 예제라고 생각하고 처리해보면 된다.

 

$this->load->model('user_model');
$users = $this->user_model->gets(); // user 모델에 정의한 모든 user 정보들을 가져오는 메서드

$this->load->library('email');
foreach($users as $user){
  $this->email->from('admin@gmail.com', 'name');
  $this->email->to($user->email); //모든 유저에게 전송
  $this->email->cc('참조할 사람@gmail.com');
  $this->email->bcc('비밀 참조할 사람@gmail.com');
  
  $this->email->subject('제목');
  $this->email->message('본문 내용');
  
  $this->email->send();
}

 

mailtype 속성값 등을 통해 email의 형식 등을 자세하게 설정해줄 수 있다.

https://ciboard.co.kr/user_guide/kr/libraries/email.html

 

 

다만 메일서비스들은 실제로 존재하는 이메일 서버에서 온 메일이 아니라면 스팸처리해버리기 때문에 이메일과 관련한 서버를 호스팅하여 처리해야 실제로 동작하는 메일 기능을 사용할 수 있음을 참고로 알고 있자.

 

그리고 동기적으로 처리할 경우 서버에 부하를 줄 수 있기 때문에 나중에 배울 queue에 쌓아두고 cron으로 처리한다.

 

 

라이브러리 확장

1. config 활용하기

이메일을 실수로라도 실제 모든 유저에게 전송해버린다면 시스템의 신뢰도가 크게 떨어지게 된다. 이를 방지하면서 테스트하기 위해서 테스트용 이메일을 설정하고 테스트 환경인 경우 그 쪽으로만 메일이 가도록 설정할 수 있다. config는 git으로 처리되지 않도록 해놨으므로 config에 아래 내용을 추가한다.

 

$config['dev_receive_email'] = 'tester@gmail.com';

 

그런 뒤에 위 email 라이브러리를 사용하는 부분에서 if문으로 $this->config->item('dev_receive_email') 값을 체크하여 사용하면 된다.

 

2. 라이브러리 확장

그러나 1번 방법도 실수로라도 config에 if문을 추가하지 않으면 사용자들에게 잘못된 이메일이 전송될 수 있다. 이를 방지하기 위해서 기본 email 라이브러리를 확장하여 수정처리하면 된다.

 

system/libraries/Email.php에 들어가서 to 라는 메서드를 확인한다.

 

root_path/libraries 디렉토리에서 MY_Email.php 파일을 만든다. 그리고 to 메서드를 갖고 있는 CI_Email 클래스를 상속한다. 그 다음 to 메서드를 override 해주면 된다. return 값에 parent::to($to); 를 통해 부모의 메서드를 다시 사용함에도 유의한다.

<?php if( ! defined('BASEPATH')) exit('No direct script access allowed');
class MY_Email extends CI_Email {
  public function to($to) {
    
    //변경할 로직
    $this->ci = &get_instasnce();
    $_to = $this->ci->config->item('dev_receive_email');
    $to = !$_to ? $to : $_to;
    return parent::to($to);
  }
}

 

$this->ci = &get_instance() 부분은 해당 MY_Email 라이브러리를 사용할 클래스의 context를 불러오는 코드이다. 여기서는 controller에서 MY_Email을 불러올 것이기 때문에 $this->ci는 controller가 된다. 그 다음 controller에서 config를 통해 'dev_receive_email') 값을 불러오게 된다. 마지막으로 삼항연산자를 통해 config의 속성값이 있는 경우에만 $_to로 그 값이 설정되도록 해주는 것이다.

 

xdebug_break();

해당 MY_Email 클래스에 정상적으로 요청이 오는지 볼려면 remote debug 방식을 지원하는 xdebug를 사용하면 된다. 라이브러리를 불러오는 쪽에 이 코드를 삽입하고 한 줄씩 실행하다보면 MY_Email 클래스에 진입하는 것을 확인할 수 있다.

 

//...중략
foreach($users as $user) {
  $this->email->from(...);
  xdebug_break();
  //...중략
}

 

override 클래스 호출

위 email 라이브러리 호출부에서 $this->email로 email 라이브러리를 호출하면 override된 MY_Email 클래스가 먼저 호출된다. 이것은 CI상 규칙이며, 이렇기 때문에 CI_Email 클래스의 메서드를 override하는 것이 정상적으로 동작하는 것임을 기억하자.

 

 

2. CLI, Queue & Cron

 

CLI

CLI 사용하기

php를 이용하면 CLI 툴로 바로 실행할 수 있다. 루트 디렉토리에서 다음 코드대로 실행하면 프로그램이 실행된다.

php index.php {class 이름} {메서드 이름}

 

 

CLI 만들기

직접 CLI로 동작하는 프로그램을 만들 수도 있다. 우선 강의에서는 autoload로 등록한 'session' 라이브러리를 제외했다. 10년전 강의라 session 라이브러리를 로드하고 CLI로 어떤 메서드를 호출했을 때 에러가 나는 경우가 있었다고 한다. 그래서 강의에서는 아래의 코드를 추가해주었다. 실제 지금 시점에서 session 라이브러리가 로드된다고 해서 에러가 날지는 잘 모르겠다.

if(!$this->input->is_cli_request()) {
  $this->load->library('session');
}

 

 input->is_cli_request() 메서드를 통해 cli로 오는 요청인지 감별할 수 있음을 기억하자.

 

 

 

Queue  & Cron

queue

queue는 많은 시간을 요구하는 작업을 비동기로 처리하기위해서 queue에 작업 내용을 쌓아두는 것이다. cron과 함께 사용하여 batch 작업을 수행한다. 앞서 모든 사용자에게 이메일을 전송하는 로직 등에서 사용할 수 있는 기술이다. queue를 처리하는 서비스로는 Sparrow, Starling, Kestrel, RebbitMQ, Apache ActiveMQ, Beanstalkd, Amazon SQS, Kafka, ZMQ 등이 있다. 대규모의 queue 작업이 필요하지 않다면 CI에서 제공하는 기능만 활용해도 충분하다.

 

queue에 처리해야할 작업을 데이터베이스에 저장한다. 파일에 저장할 수도 있으나, 다른 컴퓨터도 접속하여 queue에 걸려있는 작업을 처리할려면 데이터베이스에 쌓아두는 편이 좋다.

 

Cron

cron은 리눅스 계열에 기본적으로 포함된 스케줄러다. 정해진 시간이 어떤 작업을 수행하도록하는 소프트웨어를 말한다.

 

batch 작업 처리해보기

앞서 배운 내용들을 기반으로 batch 작업을 만들어본다. 우선 아래 코드를 통해 할 일들을 기록할 batch용 테이블을 생성한다.

 

CREATE TABLE `batch` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `job_name` varchar(50) NOT NULL,
  `context` text NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 

그리고 아래처럼 batch 작업을 위한 batch_model을 생성한다.

<?php
class Batch_model extends CI_Model {
 
    function __construct()
    {       
        parent::__construct();
    }
 
 
    function gets(){
        return $this->db->query("SELECT * FROM batch")->result();
    }
 
    function add($option)
    {
        $this->db->set('job_name', $option['job_name']);
        $this->db->set('context', $option['context']);
        $this->db->insert('batch');
        $result = $this->db->insert_id();
        return $result;
    }
 
    function delete($option){
        return $this->db->delete('batch', array('id'=>$option['id']));   
    }
}

 

 

batch 작업 저장

그리고 메일을 보내던 부분의 코드를 아래처럼 변경해준다. job_name을 지정해주고, context에 새로 등록된 글(topic)의 id 값을 json_encode()를 통해 저장해놓는다.

//...중략
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->load->helper('url');
        redirect('/topic/get/'.$topic_id);
    }

 

 

저장 결과 아래 그림처럼 batch 테이블에 json 형식으로 저장되었다.(강의 내용 캡쳐)

 

 

background에서 batch 실행

batch를 위한 controller를 만들고 CLI에서 백그라운드로 실행하면 된다. 각 job마다 topic_id를 저장하고 있고, 그 id값 마다 모든 유저에게 메일을 보내는 코드이다. 이해를 위해 코드 전문을 가져왔다.

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Batch extends MY_Controller {
    function __construct(){
        parent::__construct();
    }
    function process(){
        $this->load->model('batch_model');
        $queue = $this->batch_model->gets();
        foreach($queue as $job){
            switch($job->job_name){
                case 'notify_email_add_topic':
                    $context = json_decode($job->context);
                    $this->load->model('topic_model');
                    $topic = $this->topic_model->get($context->topic_id);
                    $this->load->model('user_model');
                    $users = $this->user_model->gets();     
                    $this->load->library('email');
                    $this->email->initialize(array('mailtype'=>'html'));
                    foreach($users as $user){
                        $this->email->from('master@ooo2.org', 'master');
                        $this->email->to($user->email);
                        $this->email->subject($topic->title);
                        $this->email->message($topic->description);
                        $this->email->send();
                        echo "{$user->email}로 메일 전송을 성공 했습니다.\n";
                    }
                    $this->batch_model->delete(array('id'=>$job->id));
                    break;
            }
        }
 
    }
}

 

cli로 실행하기 위해서 아래처럼 실행하면 된다.

php index.php cli/batch process

 

 

cron으로 실행하기

스케줄링하여 일정한 간격으로 위 메서드가 실행되도록 하면 된다. 참고로 리눅스 계열만 있어서 순수 cli 방식으로는 윈도우에서는 불가하다. cron 작업을 해주는 소프트웨어로 따로 처리해야한다.

 

루트 디렉토리에서 다음과 같이 입력한다.

sudo crontab -e //cron 설정

 

처음 사용 시 어떤 에디터를 사용하는지 물어보는데, 익숙한 vim이나 nano로 하는게 좋다. 편집화면에서 아래 내용을 입력하면 된다.

*/1 * * * * php /var/www/lecture/index.php cli/batch process > /var/www/lecture/application/logs/batch.access.log 2> /var/www/lecture/application/logs/batch.error.log

 

맨 처음 인자는 크론 시간이다. 매 분 실행하는 시간을 의미한다. 그리고 php를 통해서 process 메서드를 실행하도록 절대 경로를 적어준다. '>'는 redirection을 의미하는데, process 메서드의 실행 결과값을 log 파일에 기록하라는 것이 된다. 백그라운드로 동작하므로 log에 기록하는 것이 필요하다. 마지막으로 2> 부분은 에러내용을 경로에 해당하는 log 파일에 저장하라고 하는 의미가 된다.

 

상세한 로그를 보고 싶다면 아래 명령어를 입력하면 된다.

sudo tail -f /var/log/syslog

 

cron이 실행되면 syslog에 CMD ... 라는 접두어로 명령어가 실행되는 것을 확인할 수 있다고 한다. /var/log/syslog는 php를 로컬에 직접 설치했을 때의 경로이고, 이미지인 경우 다른 경로를 찾아봐야할 것 같다. docker image상에는 /var/log 경로 정도만 있고, local PC 상에는 /usr/bin/syslog가 있는데 local PC의 syslog는 정말 PC의 로그인 것 같다. 그리고 docker image상의 경로에는 접속이 안돼서, 상세히 알아봐야 알 수 있을 것 같다.

 

locking : 중복 batch 작업 방지

cron 작업이 1시간 걸리는데 1분마다 실행되면 1시간 동안 60번 중복 작업이 이루어질 수도 있다. 이를 막기 위해 locking 이라는 기법을 사용한다. 특정 작업이 실행되면 process_id를 기록하고 lock file을 생성한 다음 lock 파일이 없는 경우만 실행한다. 그리고 실행이 완료된 경우라면 lock 파일을 제거하여 중복 실행이 방지되도록 할 수 있다.

 

 

 

 

3. Caching

 

캐시 개요

오랜 시간이 걸릴 수 있는 작업의 결과를 저장해서 다음 호출 시에 그대로 보여주어 WAS 또는 DB에 접근하지 않고 처리하여 시간과 비용을 절약하는 기법이다.

 

캐시 무효화

다만 캐시는 결과가 달라졌을 경우 다른 결과를 출력해줘야하는데 이전 결과를 갖고 있는 경우 캐시 로직을 타서 이전 결과를 보여주게 된다. 이를 방지하기 위해 TTL로 유효시간 동안만 캐시가 유효하게 하거나, 캐시를 명시적으로 삭제하는 무효화처리를 하기도 한다.

 

 

캐시의 종류

php와 관련한 캐시의 종류는 다음과 같다.

 

OPCODE caching

OPCODE는 기계어의 로우레벨에서 컴퓨터 상에 하달되는 신호로, 이 신호를 캐싱하는 기법을 의미한다. PHP와 같은 인터프리터 언어의 성능상의 단점을 해결해준다. PHP Accelerators를 검색하여 찾아보면 여러 종류의 가속기들을 찾아볼 수 있다.

 

Web Page Caching

CI가 자체적으로 갖고 있는 캐싱 도구이다. 페이지의 view 및 데이터를 가져오는 부분을 전체적으로 저장해놓는 방식이다.

 

Partial Caching

전체적으로 캐싱하지 않고 데이터별로 일부씩만 캐싱해놓는 방식이다.

 

Database Caching

SQL 요청에 대한 결과를 저장했다가 동일한 요청이 있을 때 저장된 결과를 제공한다. 다만 파일로 결과를 영구적으로 저장하기 때문에 DB 데이터가 갱신되는 경우 캐시를 완전히 삭제 후 다시 쌓아야하는 문제가 있다.

 

 

 

캐시의 저장 위치

  • 파일: 저렴하지만 여러 시스템에서 공유하기 어렵고 메모리대비 느리다. 직접 구현도 필요하다.
  • 메모리: Memcached 같은 솔류션을 사용하여 솔류션에 위임하고 편하다. 파일 방식보다 훨씬 빠르게 데이털르 처리할 수 있으나 비용이 비싸다.
  • 데이터베이스: 보안 시스템을 갖고 있고 네트워크를 통해 접근할 수 있어 공유가 가능하지만, 메모리보다 느리다.

 

캐시 적용하기

 

웹페이지 캐싱

결과 전체를 저장하는 방식이기 때문에 다양하게 저장하기는 어렵다. 예를 들어 사용자별로 '안녕하세요 {사용자 이름} 님' 이라는 문구가 포함된다면 웹페이지 캐싱은 어렵다. 따라서 특정 페이지에 대해서만 요청이 매우 급증하는 경우에 사용하기 좋다. 아래 처럼 설정해본다.

 

$config['peak_page_cache'] = '캐싱할 url'

 

config에 peak 요청이 발생할 수 있는 url을 캐싱했다. 이제 분기문을 통해 처리하면 된다. 앞서 살펴보았던 모든 controller들의 부모가 되는 MY_Controller의 생성자 코드에 예컨대 아래처럼 코드를 추가하는 것이다.

 

current_url() 메서드를 통해 현재 controller의 url을 확인할 수 있다. 그리고 output->cache(); 코드를 통해 TTL을 설정한다. 기본적으로 초 단위를 의미한다.

function __construct()
{
    parent::__construct();
    if($peak = $this->config->item('peak_page_cache')){
        if($peak == current_url()){
            $this->output->cache(5);
        }
    }

 

이후 해당 url로 접속하면 root_path/cache 디렉토리 부분에 페이지가 캐싱되는 것을 확인할 수 있다. 아래는 강의 내용 캡쳐

 

 

다만 이 방식은 reverse proxy caching 처럼 WAS에 접근 자체를 안하는 방식보다는 느릴 수 밖에 없다.

 

 

부분 캐싱

특정한 데이터에 대해서만 캐싱한다.

 

매뉴얼에서 cache driver 파트를 참고해서 공부해보면 된다. 예제 코드는 아래와 같다.

 

cache driver를 로드하면서 첫번째 인자로 apc라는 가속기를 사용한다고 명시해준다. 그리고 backup으로 file 형태로 사용하겠다고 지정한다. 만약 apc 가속기를 사용할 수 없는 경우 두번째 backup 인자는 제외하고 그냥 'adapter'=>'file'로 처리해주면 캐시가 저장될 때마다 웹 페이지 캐싱처럼 root_path/cache 디렉토리에 파일이 추가된다. 그 다음 cache->get()을 통해 캐시값이 있는지 확인하고 없다면 cache->save()를 통해 캐시에 저장하는 형태이다. 'foo'라는 이름으로 $foo라는 변수에 있는 값을 300초 동안 저장한다는 의미이다. 간단하다.

$this->load->drvier('cache', array('adapter'=>'apc', 'backup'=>'file'));
// 위 1행의 실행문은 모든 곳에서 공통적으로 사용될 코드일 수 있으므로 MY_Controller의 생성자 부분에 넣어줘도 좋다.

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;

 

 

다만 이 경우 데이터가 추가되거나 삭제되었을 때 캐시를 삭제처리해줘야 갱신이 된다는 것을 기억해야한다.

 

 

 


 

참조

생활코딩 유튜브 채널

https://www.youtube.com/@coohde

728x90
반응형