플러그인 엔진 컨셉: 자동 훅 전수자와 전수자 응용

자동 혹 전수자(AutoHookInitiator, 오토 훅 이니시에이터)의 메소드 이름 규칙에 의해 액션과 필터가 자동으로 등록됩니다. 동작은 자동 발견 개시자(AutoDiscoverLauncher, 오토 디스커버 론처)가 동적으로, 알아서 시켜 줍니다. 프로그래머는 자잘한 콜백 선언은 엔진에 맡겨 두고 콜백 로직에만 집중하면 됩니다.

개발하면서 느끼는 훅과 콜백의 문제점이 있습니다. 바로 훅의 선언과 콜백의 구현이 구현 차원에서 서로 떨어져 있다는 점입니다. 물론 액션 필터 선언 후 바로 콜백을 선언함으로써 그 코드의 거리를 좁혀 놓을 수는 있습니다. 그러나 코드가 점점 커질수록 이 거리를 완전히 붙여놓기 쉽지 않아집니다.

콜백끼리 서로 공유해야 할 데이터가 있는 경우나, 보다 좋은 코드를 위해 클래스를 사용하는 경우 액션, 필터의 선언과 그 콜백은 더욱 그 거리를 가까이 하기 쉽지 않습니다. 그래서 나중에 유지 보수를 하는 중에 계속 에디터 창을 위아래로 옮겨 가면서 훅 선언과 콜백을 대조해야만 합니다.

그런데 잘 생각해 보면 액션, 필터의 선언과 그 콜백은 거리로는 떨어져 있지만 의미적으로는 한 덩어리라고 볼 수 있습니다. 단지 언어적으로 그 둘을 한번에 표현할 방법이 없기에 구현상의 이유로 떨어진 것 뿐이라고 생각할 수 있습니다.

AutoHookInitiator 등장

자동 혹 전수자(AutoHookInitiator, 오토 훅 이니시에이터)는 이런 문제를 해결하기 위하여 고안하였습니다. 제가 이전 포스팅을 통해 전수자란 존재를 소개하면서 전수자는 기능적으로 관련 있는 훅들을 하나의 클래스로 묶어 통일성을 꾀하는 장치라고 말씀드린 적이 있습니다. 그리고 이런 전수자를 잘 응용하면 꽤 재미난 것들을 보여줄 수 있다고도 하였는데요, 이번에 소개해드릴 이 자동 훅 전수자가 그 대표격입니다.

보통 훅과 콜백을 이렇게 선언합니다.

add_action( 'admin_init', 'my_callback' ) ;

function my_callback() {
....
}

앞에서 말씀드렸듯이 add_action()과 my_callback()의 가독성을 위하여 그 둘을 딱 붙여서 놓는 경우가 많습니다만, 어쩔 때는 거대한 함수들에 둘러싸여있는 통해 add_action()이 잘 보이지 않을 때도 있고, my_callback() 또한 항상 add_action()과 가깝게 있을 수 없는 경우가 많습니다.

그러나 AutoHookInitiator는 이런 개념을 송두리째 바꾸어버립니다. 단지 콜백 함수를 작성하는 것만으로도 액션과 필터는 자동으로 등록됩니다. 아래는 자동 훅 전수자의 예입니다.

class SampleInitiator extends AutoHookInitiator
{
    public function action_init()
    {
        ....
    }
    
    public function action_5_admin_init()
    {
        ....
    }

    public function action_10_3_save_post($post_id, $post, $updated)
    {
        ....
    }

    public function filter_the_title($title)
    {
        ....
        return $title;
    }
}

…. 으로 생략한 부분은 콜백이 구체적으로 할 부분입니다. 클래스 코드 내부에서 전혀 add_action, add_filter를 쓰지 않았지만, 이 클래스를 통해 분명히 저는 3개의 액션과 1개의 필터를 선언하였습니다. 비밀은 메소드의 이름에 있습니다. 이 클래스 메소드의 이름을 부모 클래스인 AutoHookInitiator에서 정한 규칙에 맞게 지어주기만 하면 액션과 필터는 알아서 등록됩니다. 신기하지 않나요?

메소드 이름 규칙은 이렇습니다.

  1. 모든 메소드는 public이어야 합니다. 당연히 코어에서 콜백을 실행해야 하는데 public이지 않으면 코어가 접근할 수 없겠죠.
  2. 액션은 반드시 action, 필터는 filter로 시작해야 합니다.
  3. action, filter 다음에는 우선순위를 뜻하는 정수를 붙일 수 있습니다. 선언하지 않으면 기본인 10입니다.
  4. 우선순위를 지정하면 인자의 개수를 이어 붙일 수도 있습니다 선언하지 않으면 기본인 1입니다.
  5. 그 다음 훅의 이름을 쓰면 됩니다.
  6. 각 문자열은 모두 언더바(_)로 이어 줍니다.
  7. 한 전수자에 같은 훅을 중복하는 경우, 훅 이름 부분 뒤에 두 개의 언더바를 붙인 다음 별도의 문자열로 구분을 할 수 있습니다.
    예) public function action_admin_menu__main_menu(); // 메인 메뉴 추가 액션
    public function action_admin_menu__sub_menus(); // 서브 메뉴 추가 액션

위 코드에서 선언된 훅을 분석해 보고, 종래의 방식으로 다시 풀어 쓰면 이렇습니다.

  • action_init: action으로 시작하므로 액션입니다. 훅의 이름은 ‘init’입니다. 그러므로,
add_action( 'init', array( $this, 'action_init' ) );
  • action_5_admin_init: 마찬가지로 액션입니다. 5는 우선순위입니다. 훅의 이름은 ‘admin_init’입니다. 그러므로,
add_action( 'admin_init', array( $this, 'action_init' ), 5 );
  • action_10_3_save_post( … ): 우선 순위 10, 인자 3개를 받는 콜백 함수입니다. 훅의 이름은 ‘save_post’입니다. 그러므로,
add_action( 'save_post', array( $this, 'save_post' ), 10, 3 );
  • filter_the_title: 이번에는 filter로 시작하므로 필터입니다. 훅 이름은 ‘the_title’ 입니다. 그러므로,
add_filter( 'the_title', array( $this, 'filter_the_title' ), 10, 1 );

AutoHookInitiator의 장점

간결함

메소드의 이름을 지음과 동시에 액션/필터의 선언이 동시에 지원됩니다. 코드가 분리되지 않아 가독성도 좋습니다. 저는 어떤 기능을 구현하면서 몇가지 훅을 시험적으로 사용해 봅니다. 경험에 따라 최적의 훅을 단번에 지목해 구현하는 경우도 있지만, 어쩔 때는 몇 번이고 액션과 필터를 수정하면서 해당 기능을 개선하려고 애를 씁니다. 그럴 때마다 훅과 콜백을 수정하는 것도 상당히 귀찮은 일입니다. 그러나 AutoHookInitator를 이용하면 그 불편이 많이 완화됩니다. 메소드의 이름과 시그니쳐만 살짝 변경하면 되니까요.

콜백 메소드 작명도 상당히 귀찮은 일이고, 어쩌다 이름을 잘못 지으면 나중에 고치기도 쉽지 않습니다. 액션, 필터 선언한 곳까지 찾아가서 또 고쳐야 합니다. 그러나 AutoHookInitiator에서는 작명의 고통도 많이 완화됩니다. 별로 신경 쓰지 않아도 꽤 사려 깊게 이름이 지어집니다.

한편 PSR을 사용하여 메소드나 변수는 camelcase가 기본인데, 이렇게 언더바를 사용하면 상당히 이질적일 것입니다. 저는 오히려 이 이질적인 것이 더욱 “콜백”임을 강조하게 되어 긍정적으로 생각합니다. 이런 메소드를 보면 단박에 ‘아 이거 콜백 함수구나’라고 알아차릴 수 있습니다.

add_action, add_filter를 생략한다고 해서 개발 속도가 극적으로 빨라지는 것은 아닙니다. 그냥 기존의 방식을 써도 무방하지만, 그럼에도 불구하고, AutoHookInitiator는 편합니다. 사실 add_action, add_filter는 겉치레거든요. 이걸 생략하고 바로 콜백 구현에 들어가는 것은 개발 단계에 있어서는 상당히 실리적입니다.

아, 여기서는 add_action, add_filter 만 이야기했습니다. 그러나 엔진에 구현된 AutoHookInitiator는 activation, deactivation 훅에도 대응됩니다.

뷰로 바로 넘어가는 콜백 패턴: v-액션, v-필터

워드프레스 특성상 콜백의 역할이 그다지 두텁지 않은 경우에는 콜백에 로직을 모두 구현해도 큰 문제가 없는 경우가 종종 있습니다. 아니, 그냥 콜백에 로직을 구현해야 오히려 더 깔끔할 때가 있습니다.

반면 콜백에서 해야 할 일이 상당히 두터운 경우에는 별도의 뷰를 만들어 그 뷰에서 업무를 처리(dispatch)해야 할 경우가 생기기도 합니다. 예를 들어 어떤 어드민 메뉴를 만들 때입니다. 어드민 메뉴 페이지는 add_menu_page() 함수를 통해 삽입할 수 있습니다. 여기 인자 중에 콜백 함수도 포함되어 있는데, 이 콜백함수는 화면의 출력을 담당하므로 로직이 조금 복잡해질 가능성이 있습니다. 이걸 로직으로 작성하면 이렇게 됩니다.

class SampleInitiator extends AutoHookInitiator
{
    public function action_admin_menu()
    {
      $view = new AdminMenuView();
      $view->dispatch();
    }   
}

....

class AdminMenuView extends BaseView
{
    public function AdminMenuView()
    {
      add_menu_page(
        ...
        array($this, 'outputMenuPage')  // 메뉴 페이지가 보여 줘야 할 내용을 출력하는 콜백
        ....
      );
    }
    
    public function outputMenuPage()
    {
      // 페이지 렌더링
      .....
    }
}

콜백 함수에서 별도의 뷰를 사용할 때 상당히 많은 경우 뷰를 인스턴스화 하고, 약속된 메소드인 dispatch()를 호출하는 패턴을 만날 수 있습니다. 그렇다면 전수자에서 이렇게 축약할 수 있습니다. 저는 이것을 메소드 이름 앞에 ‘v_’를 붙였다고 해서 ‘v 액션’이나 ‘v 필터’라고 부릅니다.

class SampleInitiator extends AutoHookInitiator
{
    public function action_admin_menu()
    {
      $view = new AdminMenuView();
      $view->dispatch();
    }
    
    // 위 메소드와 같은 역할을 합니다.
    public function v_action_admin_menu()
    {
      return AdminMenuView::getClass();
    }
}

View 클래스가 BaseView를 상속받아서 dispatch()라는 메소드와 getClass()라는 FQCN을 리턴하는 스태틱 메소드를 선탑재하고 있습니다. 이렇게 전수자 – 뷰 간의 관계가 단순하게 명세됩니다. AdminMenuView또한 독립적인 뷰로서, 나중에 요구사항이 변경될 때, 이를 태면 AdminExtraMenuView 같은 객체로 대체되어야 할 경우 아주 간단하게 수정될 수 있습니다.

Replacement: 동적인 훅에 대비

훅의 이름은 사실 동적입니다. 예를 들어 save_post_{$post_type} 같은 훅이나 , wp_ajax_{$action} 같은 것들입니다. 변수의 이름에 따라 훅 이름이 변경되는 경우입니다. 이럴 땐 AutoHookInitiator를 쓰기 곤란합니다. 함수 이름을 동적으로 변경할 수는 없으니까요.

이 때를 대비하여 AutoHookInitiator는 setReplacement()  메소드를 가지고 있습니다.

class SampleInitiator extends AutoHookInitiator
{
    public __construct()
    {
      $this->setReplacement(
        'wp_ajax_my_action',
        'wp_ajax_action'
      );
    }
    
    public function action_wp_ajax_action()
    {
      ....
    }
    
    ....
}

setReplacement()의 첫번째 인자가 코어에 전달될 진짜 훅 이름입니다. 두번째 인자는 우리 전수자에서 찾을 수 있는 가칭 훅 이름입니다. 바로 밑의 메소드가 action_wp_ajax_action인 것이 보이죠? wp_ajax_action이라는 훅 이름은 실제로 add_action() 함수를 호출할 때는 wp_ajax_my_action으로 변경됩니다. 이렇게 동적인 훅 이름에도 대처할 수 있습니다.

Context: 선택적 전수자 실행

전수자는 단순히 훅만 그룹화하는 클래스가 아닙니다. 전수자 안에 조직화된 훅들은 문맥에 따라 그 실행을 조절할 수 있습니다. 엔진이 제공하는 기본적인 전수자 실행 문맥은 다음과 같습니다. 그리고 문맥은 사용자가 추가할 수 있습니다. 

  • admin: 관리자 화면 문맥. “관리자 화면일 때만”이란 의미를 가집니다.
  • ajax: ajax 문맥. “현재 AJAX 요청을 처리하는 중”이란 의미입니다.
  • autosave: 포스트를 자동 저장하는 문맥입니다.
  • cron: 크론 동작 문맥입니다.
  • front: “관리자 화면이 아닌 요청일 때”란 의미를 가지는 문맥입니다.

예를 들어 ‘<plugin-dir>/src/Initiators‘란 곳이 전수자들을 놓아 두는 디렉토리라면, 여기에 문맥과 동일한 이름의 디렉토리를 만들고, 그 디렉토리에 전수자 클래스를 배치하면 해당 전수자들은 해당 문맥에서만 실행됩니다.

이 디렉토리 아래 다음과 같은 상대 경로로 전수자가 존재한다고 생각해 봅니다.

  • admin/AdminInitiator.php
  • ajax/AjaxInitiator.php
  • front/FrontInitiator.php
  • frontNoAjax/MyInitiator.php
  • CustomPosts/CustomPostInitiator.php

admin, ajax, front는 기본 문맥입니다. 따라서 해당 디렉토리 안에 저장된 전수자는 각각 관리자, AJAX, 전단부 요청에만 훅을 등록하도록 동작합니다. 나머지 frontNoAjax, CustomPosts는 문맥에 포함되지 않으므로 ‘항상’ 훅을 등록하도록 동작합니다. 그러나 만일 frontNoAjax에 “front 요청이지만 AJAX 요청은 제외하는 문맥”이라는 문맥을 따로 정의하여 자동 발견 개시자(AutoDiscoverLauncher)에 등록하면 해당 문맥에서만 훅을 등록할 것입니다.

엔진에서 기본적으로 디렉토리에 기반하여 문맥 설정을 해 주므로 별도의 코딩 없이도 훅 등록을 적절히 조절할 수 있습니다. 가령 구분 없이 개발을 한 후, 재차 잘 정리합니다. 관리 화면에서만 등록해도 좋은 훅들만 따로 모아 하나의 전수자로 몰아 놓고, 이 전수자를 admin 디렉토리로 옮겨 두면 됩니다. 일일이 메인 로직에서 코딩하는 것보다 훨씬 더 간편합니다.

여기까지

전수자가 하나의 클래스이고, 객체라는 존재를 잘 활용하면 단순히 훅을 그룹화하는 것 이상의 능력을 발휘할 수 있습니다. 저는 플러그인을 위한  엔진을 개발하고 있고 위 내용은 실제 제 엔진에서 구현되어 사용중인 기능들입니다. 아직 소스를 일반에게 공개 배포하는 단계는 무리이지만, 어떻게든 이런 개념들을 제가 만들고 있고 발전시키고 있다는 사실은 알리고 싶었습니다.

두서없이 글을 적고 있고 이 글을 누가 읽고 이해할까… 이런 생각입니다. 그렇지만 앞으로 이런 개념들은 Ivy-MU라는 플러그인 엔진, 그리고 킹콩보드3 등을 통해 세상에 공개될 날이 올 테니, 그 동안에 해 둘 수 있는 또 하나의 준비라고 생각하고 있습니다. 그 날이 오기를 고대하면서, 계속 정진하고 있겠습니다.

 

댓글 2개

  1. 워드프레스왕초보인데 재미있게 보았어요. 이해는 다못햇지만.
    전문가가 나라 , 지역, 분야 , 이름 이런식으로 자기 프로필을 입력하고
    사용자가 또 이런식으로 검색을 할려는데 생 php는 가늠이 가는데 워드프레스는 감이 안가요.
    답변감사드립니다.

changwoo 에 응답 남기기응답 취소