PHP Web Framework Phalcon




개요


 간혹 프로젝트를 하다보면, 웹 작업을 필요로 할 때가 많습니다. 사이트 개발은 물론이고, 앱을 만들더라도 RESTFul 을 써야 한다면 웹은 필요합니다. 그럴때마다 저는 어떤 프레임웤을 쓸지 고민이 많았습니다. 작은 규모의 웹이라면 프레임웤 없이 개발해도 되긴 하나, 기본 뼈대를 만드는게 여간 귀찮은 작업이 아니죠. 동작상의 문제라기보다 유지보수와 보안성 문제가 얽히기 시작하면, 그냥 기존 프레임을 쓰는게 나은것 같습니다. :)


 그래서 프로젝트를 할때면, 이것저것 항상 다시 프레임웤을 조사해서 쓰곤 합니다. 물론 윈도우 서버라면 Entity Framework 를ASP .NET 과 함께 씁니다. 하지만 대규모의 서버환경이라면 그렇게 하기 힘듭니다. 아시다시피 윈도우를 쓰려면, 라이센스 비용이 만만치 않거든요. 서버를 한개 두개 돌리는게 아니라, 글로벌한 사이트를 돌리려면 수십에서 많게는 100대 가까이 서버를 돌려야 하니까요. 예산문제도 무시할 수 없습니다. 이런 이유로 기업에서는 linux 를 쓰길 지향합니다. :)


 Yii, Laravel, Codeigniter 등을 모두 써 보았는데, 그중 가장 으뜸은 Phalcon 이 아닌가 생각합니다. 제가 타 프레임웤으로 개발하면서 중요했던 부분은 'ORM 을 지원하는가?', '속도는 빠른가?', '사용하기 편리한가?', 'Database 의 Column mapping 을 지원하는가?' 정도 였습니다. 그런데 이 모든걸 지원하는게 많치 않더군요. Phalcon 하나 뿐이었습니다. Yii 는 쓰기가 불편했고, Laravel 은 속도가 느리고, Codeigniter 는 Column mapping 이 불편했습니다. 네, 속도는 거의다 느리고요.


 Phalcon (http://phalconphp.com)은 Column Mapping 이 너무나 쉽고 잘 동작하며, 무엇보다 C 모듈로 빌드되어 있어서(*.so) 속도가 엄청나게 빠릅니다. phalcon-devtools 라는것도 지원하여, 개발의 일 부분을 자동화 시켜 주기도 합니다. 물론 쓰다보면 제가 원하는 기능을 완벽하게 100% 지원하지는 않아서, 추가로 커스터마이징 해 줘야 했지만요 ^-^ 현재로서는 가장 만족하며 쓰고 있습니다. 하여 간단하게나마 소개하고, 커스터마이징 한 부분을 공유하려고 합니다.




설치


 서두가 너무 길었던것 같네요. :) 설치는 매우 간단합니다. 참, 제 개발 환경은 CentOS v6.6 입니다.

홈페이지에 너무 설명이 잘 되어 있으므로, 아래 주소를 참고하시기 바랍니다.

http://docs.phalconphp.com/en/latest/reference/install.html#linux-solaris


간단히 설명을 드리자면, 본 프레임웤은 php 기반으로 당연히 php, apache(웹서버 아무거나) 가 설치 되어 있어야 합니다. 더불어 build 를 해야하므로 gcc 가 필요하며, git 를 통해서 다운받아야 하므로 git 도 설치되어 있어야 합니다. :) 각각의 설치방법 또한 위 사이트에서 다루고 있습니다.


요약하자면 아래 순서입니다.


1. php 가 5.3 이상인지 확인한다.

2. gcc 가 설치되어 있는지 확인한다.

3. git 가 설치되어 있는지 확인한다.

4. git 를 통해서 phalcon 프로젝트를 다운받는다.

5. 다운받은 phalcon 을 인스톨 한다.

6. /etc/php.d/ 폴더에 설정파일(phalcon.ini)를 만든다.

7. 서버(apache)를 재시작 합니다.


6 번에 파일을 만드는 부분은 CentOS 의 경우 설명입니다. 그리고 파일안에 어떤 내용을 넣어야 하는지는 위에 언급한 phalcon 사이트에서 확인하세요 :)

이렇게 phalcon 을 install 해서 직접 phalcon.so 를 만드셔도 되지만, 동일한 os 환경이라면 빌드된 phalcon.so 를 복사하는 것만드로도 잘 동작합니다.





Phalcon Devtools


 phalcon 을 설치한 것만으로도 개발하시는데 아무런 지장이 없습니다. 네, 다 끝났습니다. 하지만 좀 귀찮을 뿐입니다. 왜냐면 phalcon 은 특정한 프로젝트 hierarchy 를 가지고 있어야 하는데, 이것을 일일이 작업해 주어야 하거든요. 물론 .htaccess 파일오 직접 만들어 주거나, 받아와야 합니다. ORM 을 쓰려면, 직접 테이블 구조에 맞게 모델을 구성해야 하고요.

 보통 타 프레임웤은 기본적으로 이러한 부분을 제공하거나, 이미 만들어진 상태로 배포하는데 phalcon 은 이런 기능을 devtools 라는 것으로 분리해 놓았습니다. 필요한 사람만 별도로 설치해서 사용하라는 것이죠.




이렇게 직접 만들어야 한다고 생각하면, 끔찍하네요.

재미나게도 phalcon 은 프로젝트 특성에 맞게 hierarchy 가 다릅니다. 웹 사이트를 Full 로 개발하는 경우도 있지만, RESTful 처럼 view 가 별도로 없는 웹도 있지요. 그래서 각각에 맞게 구성이 다릅니다. 전 이 부분을 매우 좋게 보고 있습니다. :)


설치방법과 사용법은 아래 사이트를 참고해 주시기 바랍니다.


http://docs.phalconphp.com/en/latest/reference/tools.html


참고로 RESTful 인 경우에는 아래처럼 type 을 micro 로 생성해 주시는게 좋습니다.


phalcon project store micro


Phalcon-devtools 는 이렇게 Hierarchy 를 만들때 이외에도, Controller 를 만든다거나 Model(ORM)을 만들때 매우 유용하게 사용됩니다.





Phalcon Customizing


Phalcon 에서 model 쪽 작업을 하다보면 불편한 것들이 몇가지 있습니다.


1. DB 에 모델을 insert 할때, not null 필드에 대해서는 모두 default 를 명시적으로 넣어주어야 함.

2. phalcon 의 ORM 이 아닌, raw query 를 실행하려면 복잡함

3. 로그인 기능(session)에 대한 편의를 제공하지 않기 때문에, 직접 구현해야 함

4. 모델에 대해서, 출력을 위해 $model->toArray() 호출을 하는데 기능이 부족함


이런 것들이 저는 좀 불편했습니다. 1, 2, 3 번은 사실 크게 필요한 기능은 아닌데, 매번 긴 코드를 작성하기 귀찮아서 커스터마이징 한 부분입니다. 그리고 4번은 기능적으로 필요해서 넣은 것으로 핵심이라고 볼 수 있습니다.


4번에 대해서 좀더 설명드리자면, 다음과 같습니다.

아래와 같은 테이블이 있다고 가정해 봅시다.


CREATE TABLE users (

id INT(11) UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,

name VARCHAR(32) NOT NULL,

address VARCHAR(128) NOT NULL,

);


phalcon 에서 레코드를 뽑아올때 아래처럼 합니다.



// 결과 레코드가 1개 인 경우
$user = Users::findFirst('id = 3'); // id 가 3 인 레코드 1개를 뽑아옴
$user = $user->toArray(['id', 'name']);    // array 로 변환하는데, id 와 name 만 뽑아낼 경우.(address 는 생략됨)

// 여러개인 경우
$users = Users::find('id > 3'); // id 가 3보다 큰 레코드 모두를 뽑아옴
$users = $users->toArray();




 만약 id 와 name 이외에 존재하지 않는 age 를 함께 넣어서 array 로 만들고 싶다면 어떻게 해야 할까요?

phalcon 에서는 먼저 array 로 뽑아내고, 그 결과에 대해서 다시 loop 를 돌면서 일일이 추가시켜 주어야 합니다. 이게 꽤나 번거롭고 귀찮습니다. 혹은 name 뒤에 "M_" 을 붙이려면 어떻게 해야 할까요? array 를 먼저 뽑고, loop 를 돌면서 일일이 수정을 해 주어야 합니다.


 이런 귀찮은 부분을 해결하기 위해서, 아래처럼 동작이 가능하게 만들었습니다.



// 결과 레코드가 1개 인 경우
$user = Users::findFirst('id = 3'); // id 가 3 인 레코드 1개를 뽑아옴
$user = $user->toArray(['id', 'name' => 'M_' . $user->name, 'age' => 20]);    // array 로 변환하는데, name 앞에 'M_' 접두사를 붙여서 뽑아내고, age 라는 이름으로 20 값을 array 에 추가시킨다.

// 여러개인 경우
$users = Users::find('id > 3'); // id 가 3보다 큰 레코드 모두를 뽑아옴
$users = $users->filter(function($u) { return $u->toArray(['id', 'name' => 'M_' . $user->name, 'age' => 20]); });



이런 기능을 지원하기 위해 BaseModel 을 만들었습니다. 물론 다른 model 들은 이 BaseModel 을 상속받습니다.


아래는 BaseModel 의 Full Source 입니다.


    [BaseModel.php]


<?php

use Phalcon\Mvc\Model\Resultset\Simple as Resultset;

class BaseModel extends \Phalcon\Mvc\Model
{
    private static $defaultValue;

    protected static function defaultValue() {
        if(isset(self::$defaultValue) == false) {
            self::$defaultValue = new \Phalcon\Db\RawValue('default');
        }
        return self::$defaultValue;
    }



    public function onConstruct()
    {
        if(property_exists($this, 'create_date')) {
            $this->createDate = BaseModel::defaultValue();
        }
        if(property_exists($this, 'update_date')) {
            $this->updateDate = BaseModel::defaultValue();
        }
    }


    public function rawQuery($query, $useColumnMap = true) {
        $resultSet = $this->getReadConnection()->query($query, null);
        return new Resultset(($useColumnMap ? $this->columnMap() : null), $this, $resultSet);
    }


    public function __get($key){
        if($key == "session") {
            return $this->getDI()->getSession();
        } else if($key == "user") {
            return ($this->getDI()->getSession() ? $this->getID()->getSession()->user : NULL);
        } else if($key == "auth") { return $this->array2Object($this->session->get($key)); }


        $methodName = "get_".$key;
        if(method_exists($this, $methodName)) {
            return $this->$methodName();
        }
    }

    public function __set($key, $value) {
        if($key == 'auth') {
            if($value == null) { $this->session->remove($key); $this->session->destroy(); }
            else { $this->session->set($key, $value->toArray()); }
        } else {
            $this->$key = $value;
        }
    }

    private function array2Object($array) {
        if($array == null) { return null; }
        $object = new stdClass();
        foreach($array as $k=>$v) {
            $object->$k = $v;
        }
        return $object;
    }


	public function toArray($columns = NULL)
    {
        if($columns == NULL) { $columns = array_values($this->columnMap()); }
        $columnMap = $this->columnMap();
        $columnNames = array_values($columnMap);

        $origin_columns = array();
        $extend_columns = array();
        $extra_values = array();

        foreach($columns as $k=>$v) {
            if(is_numeric($k)) {
                if(array_key_exists($v, $columnNames)) {
                    array_push($origin_columns, $v);
                } else {
                    array_push($extend_columns, $v);
                }
            } else {
                $extra_values[$k] = $v;
            }
        }


        // push
        $array = parent::toArray($origin_columns);


        foreach($extend_columns as $key) {
            $array[$key] = $this->$key;
        }

        foreach($extra_values as $k=>$v) {
            $array[$k] = $v;
        }

        return $array;
    }

}



참, 만약에 아래의 hobby 처럼 column 명도 아니면서, 사용자가 따로 value 를 명시하지 않은 경우에는 해당 Model(이 경우 Users.php)에 get_hobby 함수의 리턴값을 사용합니다. 다시말해 get_ 접두사가 붙은 멤버 메소드를 호출하게 됩니다.


$user = $user->toArray(['id', 'name', 'hobby']);    // hobby 는 column 명도 아니고, value 도 명시하지 않았음



그리고 Controller 쪽에서는 RESTful 작성시 편리하게 하려고, 아래와 같이 BaseController.php를 마들어 커스터마이징 하였습니다.



    [BaseController.php]


<?php

class BaseController extends \Phalcon\Mvc\Controller
{
    public function json($data, $code = 200)
    {
        $this->response->setContentType('application/json', 'UTF-8');
        $this->response->setJsonContent($data, JSON_NUMERIC_CHECK);

        switch($code) {
            case 200: break;
            case 201: $this->response->setStatusCode($code, "Created"); break;
            case 400: $this->response->setStatusCode($code, "Bad request"); break;
            case 401: $this->response->setStatusCode($code, "Unauthorized"); break;
            case 404: $this->response->setStatusCode($code, "Not Found"); break;
            case 204: $this->response->setStatusCode($code, "No Content"); break;
            case 500: $this->response->setStatusCode($code, "Internal Server Error"); break;
        }
        $this->response->send();
    }

    private function array2Object($array) {
        if($array == null) { return null; }
        $object = new stdClass();
        foreach($array as $k=>$v) {
            $object->$k = $v;
        }
        return $object;
    }


    public function __get($key) {
        if($key == "config") { return $this->di->get('config'); }
        else if($key == "auth") { return $this->array2Object($this->session->get($key)); }

        return parent::__get($key);
    }


    public function __set($key, $value) {
        if($key == 'auth') {
            if($value == null) { $this->session->remove($key); $this->session->destroy(); }
            else { $this->session->set($key, $value->toArray()); }
        } else {
            $this->$key = $value;
        }
    }

}








맺음말


사실 절대적으로 좋은 프레임웤이라는건 존재할 수 없다고 생각합니다. 세상에서 가장 좋은 프로그래밍 언어도 없듯이 말이죠. 가장 좋은 프레임웤이란 현재 자신이 하려는 일에 따라 달라진다고 봅니다. 제 경우 phalcon 이 가장 좋은 이유는 '속도와 컬럼매핑' 라고 생각합니다. :)


기회가 되신다면, 이외에 많은 프레임웤을 사용해 보시면 좋을것 같네요. ^-^




'Web' 카테고리의 다른 글

jPsdReader - javascript library  (0) 2014.09.24
javascript 로 layer 드래그 기능 만들기  (0) 2012.05.23