Module::Pluggable 플러그인 프레임워크를 이용한 확장성 있는 프로그램 작성

주의: [Module::Pluggable 플러그인 프레임워크를 이용한 확장성 있는 프로그램 작성]의 가장 최근 판은 이곳에서 확인할 수 있습니다.

시작하며

프로그램이 확장성이 좋다는 것은 변화에 유연하게 대처할 수 있으며
기능을 추가하는데 비용이 적게 든다는 것을 의미합니다.
확장성 있는 프로그램을 만드는 여러가지 방법이 있지만
플러그인 구조는 그 중에서도 널리 쓰이고 있는 기법 중 하나입니다.
보통 전산 분야에서 플러그인(plug-in)은 기능 확장용 소프트웨어를 지칭합니다.
플러그인 구조를 지원하는 소프트웨어는 글자 그대로 플러그에 꽂듯이
새로운 기능의 모듈을 제작한 후 원래의 소프트웨어에 꽂으면
별다른 수고없이 해당 기능을 사용할 수 있는 것이 특징입니다.
펄의 Module::Pluggable 모듈을 이용하면 플러그인 구조를
손쉽게 지원할 수 있기 때문에 플러그인 구조를 만들기 위해 수고를 들일 필요없이
최소한의 코드로도 가독성 있고 확장성 있는 프로그램을 작성할 수 있습니다.

관련 연구

Module::Pluggable은 플러그인 형식의 서브 모듈을
지원하는 모듈을 만들 수 있게 도와주는 간단한 프레임워크입니다.
원래는 기본 내장 모듈이 아니었으나 5.9.5 개발 버전부터
기본 내장 모듈로 포함되어서
근래에 배포되고 있는 5.10 안정 버전에서는 추가 설치없이 사용 가능합니다.
만약 자신이 사용하고 있는 펄의 버전이 5.8 이하라면
CPAN을 통해 설치해야 합니다.

사용법은 간단합니다.
플러그인 기능을 지원하려는 모듈에 다음의 예제처럼
Module::Pluggable을 적재하는 코드를 넣어주면 됩니다:

  package MyClass;

  use strict;
  use warnings;
  use Module::Pluggable require => 1;

  sub new {
      my ( $class, @args ) = @_;

      # ...
  }

너무 간단하지만 이것으로 모든 준비는 끝났습니다.
MyClass 모듈은 이제 플러그인 모듈들을 마음껏 사용할 수 있습니다.
MyClass 모듈이 자신의 모듈 하부에 적재되는
플러그인 모듈을 사용하는 방법은 다음과 같습니다:

  use strict;
  use warnings;
  use MyClass;

  my $mc = MyClass->new;
  my @plugins = $mc->plugins;

MyClass 모듈에 Module::Pluggable을 적재하면 Module::Pluggable
MyClass 이름 공간에 plugins 함수를 자동으로 끼워넣습니다.
일종의 이름 공간 침해라고 생각할 수도 있지만
펄에서는 매우 유용하게 사용할 수 있는 기교 중 하나입니다.
plugins 함수는 MyClass::Plugin::* 하부의 모든 모듈의 이름 목록을 반환합니다.

MyClass::Plugin::Add 모듈의 예제는 다음과 같습니다:

  package MyClass::Plugin::Add;

  use strict;
  use warnings;

  sub run {
      my ( $self, $num1, $num2 ) = @_;

      return $num1 + $num2;
  }

  1;

MyClass::Plugin::Add 플러그인은 run 함수를 가지고 있으며
MyClass 모듈 사용자는 다음처럼 이 플러그인을 사용할 수 있습니다:

  use strict;
  use warnings;
  use MyClass;

  my $mc = MyClass->new;

  my $result;
  foreach my $plugin ($self->plugins) {
      next $plugin eq 'MyClass::Plugin::Add';
      last $plugin->can('run');
      $result = $plugin->run(4, 5);
      last;
  }

실제 플러그인 모듈을 어떤 식으로 구성해야 하는지에
대해서는 어떠한 제약 사항도 없습니다.
모든 것은 MyClass 모듈을 설계하는 프로그래머에게 달려있습니다.
또한 Module::Pluggable은 다양한 옵션을 지원합니다.
옵션을 이용해서 plugins 대신 자동으로 추가할 함수의 이름을 바꿀 수 있으며
플러그인 하부 모듈의 위치도 MyClass::Plugin:: 하부가 아닌 다른 위치로
변경할 수 있습니다.
그 외 다른 유용한 옵션들이 많으므로
자세한 것은 문서를 참조하세요.

구현

구성

좀 더 실질적인 예제인 Wiki 모듈과 Wiki::Plugin::* 모듈을 작성해보겠습니다.
위키를 만들다보면 해당 페이지의 체크썸을 확인해야할 일이 있습니다.
이 때 MD5 해시라던가 SHA1 등 다양한 체크썸을 기호에 맞게 사용할 수 있으면
유용할 것입니다. 이런 체크썸 기능을 플러그인 방식으로 사용할 수 있게 구성합니다.

또한 위키의 각 페이지는 보통 위키 문법을 이용해서 작성한 후 HTML로 변환합니다.
취향에 따라 위키의 페이지를 위키 문법이 아닌 마크다운(markdown)이나
펄의 POD 문법을 사용하고 싶은 경우도 있습니다.
이 뿐만 아니라 자신만의 즐겨쓰는 문법을 정의해서 사용할 수 있으면 유용할 것입니다.
플러그인 구조를 이용해서 위키가 다양한 문법을 지원할 수 있게 구성합니다.

Wiki 모듈의 플러그인 구조는 크게 Wiki::Plugin::Digest::* 모듈과
Wiki::Plugin::Renderer::HTML::* 모듈로 구성합니다:

  Wiki.pm
  Wiki/
    Plugin/
      Digest/
        HelloWorld.pm
        MD5.pm
        SHA1.pm
      Renderer/
        HTML/
          GoogleWiki.pm
          Markdown.pm
          MediaWiki.pm
          PlainText.pm
          Pod.pm
          WikiText.pm

최상위 모듈

다음은 각 모듈들의 코드입니다.
먼저 최상위 모듈인 Wiki 모듈입니다:

  package Wiki;

  use strict;
  use warnings;
  use Module::Pluggable require => 1;

  use base qw(Class::Accessor::Fast);
  Wiki->mk_ro_accessors(qw(digest renderer));

  sub new {
      my $class  = shift;
      my %params = (
          @_,
      );

      my $self = bless \%params, $class;
      $self->init;

      return $self;
  }

  sub init {
      my $self = shift;

      for my $plugin ( $self->plugins ) {
          if ($plugin =~ m/^Wiki::Plugin::Renderer::HTML::(.*)$/) {
              $self->{renderer}->{lc $1} = $plugin;
          }
          elsif ($plugin =~ m/^Wiki::Plugin::Digest::(.*)$/) {
              $self->{digest}->{lc $1} = $plugin;
          }
      }
  }

  sub handle_renderer {
      my ( $self, $src ) = @_;

      my ( $format, $content ) = split "\n", $src, 2;
      if ( $format =~ m/^#!(.*)/ ) {
          $format = lc $1;
      }
      else {
          $format = 'plaintext';
      }

      my $plugin = $self->renderer->{$format};

      my $dest;
      if ( $plugin && $plugin->can('run') ) {
          $dest = $plugin->run($src);
      }
      else {
          $dest = "<pre>Error: Cannot find [$format] renderer plugin.</pre>";
      }

      return $dest;
  }

  sub handle_digest {
      my ( $self, $type, $src ) = @_;

      my $plugin = $self->digest->{lc $type};
      return unless $plugin->can('run');

      my $dest = $plugin->run($src);

      return $dest;
  }

  1;

체크썸 플러그인

Wiki::Plugin::Digest::HelloWorld 플러그인입니다:

  package Wiki::Plugin::Digest::HelloWorld;

  use strict;
  use warnings;

  sub run {
      my $self = shift;
      my $msg  = shift || q{};

      return "Hello $msg!";
  }

  1;

Wiki::Plugin::Digest::MD5 플러그인입니다:

  package Wiki::Plugin::Digest::MD5;

  use strict;
  use warnings;
  use Digest;

  my $d = Digest->new("MD5");

  sub run {
      my $self = shift;
      my $msg  = shift || q{};

      $d->reset;
      $d->add( $msg );

      return $d->hexdigest;
  }

  1;

Wiki::Plugin::Digest::SHA1 플러그인입니다:

  package Wiki::Plugin::Digest::SHA1;

  use strict;
  use warnings;
  use Digest;

  my $d = Digest->new("SHA-1");

  sub run {
      my $self = shift;
      my $msg  = shift || q{};

      $d->reset;
      $d->add( $msg );

      return $d->hexdigest;
  }

  1;

HTML 변환 플러그인

Wiki::Plugin::Renderer::HTML::GoogleWiki 플러그인입니다:

  package Wiki::Plugin::Renderer::HTML::GoogleWiki;

  use strict;
  use warnings;
  use Text::GooglewikiFormat;

  sub run {
      my ( $self, $src ) = @_;

      my $dest = Text::GooglewikiFormat::format($src);

      return $dest;
  }

  1;

Wiki::Plugin::Renderer::HTML::Markdown 플러그인입니다:

  package Wiki::Plugin::Renderer::HTML::Markdown;

  use strict;
  use warnings;
  use Text::MultiMarkdown;

  my $parser = Text::MultiMarkdown->new(
      empty_element_suffix => ' />',
      tab_width            => 2,
      use_wikilinks        => 1,
      img_ids              => 1,
      heading_ids          => 1,
  );

  sub run {
      my ( $self, $src ) = @_;

      my $dest = $parser->markdown( $src );

      return $dest;
  }

  1;

Wiki::Plugin::Renderer::HTML::MediaWiki 플러그인입니다:

  package Wiki::Plugin::Renderer::HTML::MediaWiki;

  use strict;
  use warnings;
  use Text::MediawikiFormat qw(wikiformat);

  sub run {
      my ( $self, $src ) = @_;

      my $dest = wikiformat(
          $src,
          {},
          { implicit_links => 1, },
      );

      return $dest;
  }

  1;

Wiki::Plugin::Renderer::HTML::PlainText 플러그인입니다:

  package Wiki::Plugin::Renderer::HTML::PlainText;

  use strict;
  use warnings;

  sub run {
      my ( $self, $src ) = @_;

      my $dest = <<"END_HTML";
  <pre>
  $src
  </pre>
  END_HTML

      return $dest;
  }

  1;

Wiki::Plugin::Renderer::HTML::Pod 플러그인입니다:

  package Wiki::Plugin::Renderer::HTML::Pod;

  use strict;
  use warnings;
  use Pod::Simple::HTML;
  use HTML::Entities;

  my $parser = Pod::Simple::HTML->new;
  $parser->index(1);
  $parser->html_header_before_title(0);
  $parser->html_header_after_title(0);
  $parser->html_footer(0);

  sub run {
      my ( $self, $src ) = @_;

      my $dest;
      $parser->output_string( \$dest );
      $parser->parse_string_document( $src );

      # http://chalow.net/2008-05-10-3.html
      $dest = decode_entities($dest);

      return $dest;
  }

  1;

Wiki::Plugin::Renderer::HTML::WikiText 플러그인입니다:

  package Wiki::Plugin::Renderer::HTML::WikiText;

  use strict;
  use warnings;
  use Text::WikiText;
  use Text::WikiText::Output::HTML;

  my $parser      = Text::WikiText->new;
  my $output      = Text::WikiText::Output::HTML->new;
  my %parser_opts = (
      full_page      => 0,
      heading_offset => 0,
  );

  sub run {
      my ( $self, $src ) = @_;

      my $document = $parser->parse($src);
      my $dest = $output->dump($document, \%parser_opts);

      return $dest;
  }

  1;

사용

Wiki 모듈은 두 가지 객체 함수인 메소드를 지원합니다:

  use strict;
  use warnings;
  use Wiki;

  my $wiki = Wiki->new;
  my $digest = $wiki->handle_digest  ( 'md5', $source );
  my $html   = $wiki->handle_renderer( $source        );

체크썸 플러그인과 HTML 변환 플러그인을 다루기 위해
handle_* 함수를 만든 것에 주목하시기 바랍니다.
체크썸 플러그인의 경우 호출하는 시점에 사용할 플러그인을 명시하며
변환 플러그인의 경우 인자의 값을 이용해서 적절한 플러그인을
동적으로 사용하고 있습니다.
물론 어떻게 사용할지는 전적으로 프로그래머에게 달려있습니다.

정리하며

펄은 매우 유연하며 강력한 기능을 많이 가지고 있습니다.
자유로운 이름 공간의 제약을 적극적으로 활용한
Module::Pluggable의 기교도 그러한 예 중의 하나입니다.
플러그인 구조를 지원하는 프로그램을 제작하는 것이 어렵지는 않다하더라도
매번 비슷한 형태의 목업(mock-up) 코드를 작성하는 것은 성가신 일입니다.
Module::Pluggable을 이용하면 최소한의 비용으로
확장성있는 플러그인 구조의 장점을 적극 활용할 수 있습니다.
더불어 여유로울때 Module::Pluggable 모듈의 소스 코드를 참고하면
펄에 대해서 더 자세히 알 수 있는 좋은 기회가 될 것입니다.

Creative Commons License
This work, unless otherwise expressly stated, is licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 2.0 Korea License.
This entry was posted in Development and tagged , , , . Bookmark the permalink.

Comments are closed.