스무번째 날: Plack으로 웹 서버 없이 CGI 스크립트 실행하기

저자

@gypark - 개인 자료라고 믿기 어려울 정도의 방대한 Perl 자료를 제공하고 있는 gypark.pe.kr의 주인장, Raymundo라는 닉을 사용하기도 한다.

들어가며

예전에 리눅스 서버에서 만든 CGI를 윈도우에서 써 보고 싶은데, 짧은 CGI 스크립트를 하나 쓰자고 Apache 웹 서버를 설치하는 건 과하다 싶었습니다. 그렇다고 요즘 많이 쓴다는 Dancer나 Mojolicious 같은 웹 프레임워크를 따로 배워서 새로 스크립트를 작성하자니 이것도 배보다 배꼽이 더 큰 느낌이었죠. 그런데 Plack을 사용하여 기존 CGI 스크립트를 그대로 실행할 수 있었습니다. 이 글에서는 간단한 예를 통해 그 과정을 설명합니다.

준비물

사용하는 모듈은 아래와 같습니다.

PSGI와 Plack은 무엇인가

사실 저는 여전히 잘 모르겠습니다. :-) 다만 문서에서 읽은 설명에 의하면 아래와 같습니다.

설치

윈도우7에 Strawberry Perl 5.14를 쓰고 있는데, Plack 모듈의 경우 설치할 때 테스트에서 무수히 많은 에러가 나서 설치가 안 되었습니다. 정확한 이유는 모르겠고, 일단은 notest force install Plack 명령을 써서 강제로 설치해 주었는데, 큰 문제는 없었습니다.

간단한 스크립트 예제

간단한 CGI 스크립트 예제입니다. 이 코드 자체가 중요한 건 아니지만, 읽는 분들이 곧바로 실험해 볼 수 있게 코드 전체를 적었습니다. 이 CGI 스크립트는 간단한 텍스트 파일 관리자입니다. 다음과 같은 일을 합니다.

원래는 실험 결과 이미지를 포함한 HTML 파일 여러 개를 한 눈에 보면서 불 필요한 걸 지우고 중요한 건 다른 폴더로 옮기는 용도로 만들었던 스크립트였습니다.

#!/usr/bin/env perl
# index.pl
# 현재 폴더 안의 모든 텍스트 파일의 내용을 보여주거나
# 검색한 단어가 들어 있는 파일만 보여주거나
# 선택한 파일들을 삭제하는 스크립트

use strict;
use warnings;
# no warnings 'redefine';
use CGI;

my $HEADING_STYLE = 'background-color: #aaaacc; margin-top: 10pt; padding: 2pt';
my $q = new CGI;

main:
{
    print $q->header(-type => "text/html; charset=UTF-8");
    # html 헤더
    print <<EOF;
<head>
<title>Manage text files</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<p>
EOF

    # 검색 form
    print
        $q->start_form(-action=>"./index.pl"). "\n"
        . "Text Search   : "
        . $q->textfield('content'  , '', 20, 20)
        . "\n"
        . "<br /><br />\n"
        . $q->submit('search', '검 색')
        . "\n"
        ;

    # 선택한 파일 삭제
    if ( $q->param('delete') ) {
        print "<hr />\n";
        print "<ul>\n";
        foreach my $file ( $q->param('delete_files') ) {
            unlink $file;
            print "<li>delete $file</li>\n";
        }
        print "</ul>\n";
    }

    # 모든 파일 또는 검색 텍스트를 포함한 파일들 출력
    my @matches = sort glob("*.txt");
    my $content  = $q->param('content') || "";
    if ( $q->param('search') and $content ) {
        # 파일내용 검색
        @matches = grep { text_match($_, $content) } @matches;
    }

    print "<hr />\n";
    foreach my $file ( @matches ) {
        print $q->h1({-style=>$HEADING_STYLE}, $file);
        print $q->checkbox(-name => 'delete_files', label => 'Delete?', -value => $file),"<p>\n";
        print "<pre>\n", file_content($file), "\n</pre>\n";
    }
    print "<p>";
    print $q->submit('delete', '선택한 파일 삭제');
    print $q->end_form();

    # html 나머지 부분 출력
    print <<EOF;
</body>
</html>
EOF

}

# $file 을 읽어서 $search 가 매치되는지 여부 반환
sub text_match {
    my ( $file, $search ) = @_;

    open my $in, "<", $file;
    my $text;
    {
        local $/ = undef;
        $text = <$in>;
    }
    close $in;

    if ( $text =~ /$search/i ) {
        return 1;
    }
    return 0;
}

# $file 파일의 내용 반환
sub file_content {
    my $file = shift;

    open my $in, "<", $file;
    my $text;
    {
        local $/ = undef;
        $text = <$in>;
    }
    close $in;

    return $text;
}

다음 스크린샷은 각각의 기능을 테스트하는 모습을 보여줍니다.

테스트하는 모습 그림 1. 테스트하는 모습 (원본)

plack을 써서 구동하기

이 스크립트를 윈도우에서 실행해 보겠습니다. 먼저, CGI 환경을 에뮬레이트해주는 CGI::Emulate::PSGI 모듈을 사용하여 기존 스크립트를 구동하는 PSGI 스크립트를 만듭니다.

#!/usr/bin/env perl
# app.psgi
use strict;
use warnings;

use CGI::Emulate::PSGI;
use Plack::Builder;

my $index = CGI::Emulate::PSGI->handler(
    sub {
        do "index.pl";
        CGI::initialize_globals() if defined &CGI::initialize_globals;
    }
);

이 스크립트를 plackup 프로그램을 써서 구동시킵니다.

D:\psgi_example> plackup app.psgi
HTTP::Server::PSGI: Accepting connections at http://0:5000/

디폴트로 5000번 포트를 사용합니다. 웹브라우저에서 http://127.0.0.1:5000 주소에 접속하면 짜잔~ 똑같은 화면이 나옵니다. 검색이나 파일 삭제 기능도 그대로 동작합니다.

둘 이상의 스크립트를 사용해야 할 때

위의 예제에서는 주소에 접속했을 때 무조건 index.pl 스크립트를 띄우게 됩니다. 두 개 이상의 스크립트를 사용할 경우는 Plack::Builder 모듈을 사용하여, 각 스크립트에 대응하는 URL을 장착(mount)해 줍니다.

#!/usr/bin/env perl
# multi.psgi
use strict;
use warnings;

use CGI::Emulate::PSGI;
use Plack::Builder;

my $cgi1 = CGI::Emulate::PSGI->handler(
    sub {
        do "cgi1.pl";
        CGI::initialize_globals() if defined &CGI::initialize_globals;
    }
);

my $cgi2 = CGI::Emulate::PSGI->handler(
    sub {
        do "cgi2.pl";
        CGI::initialize_globals() if defined &CGI::initialize_globals;
    }
);

builder {
    mount "/cgi1.pl" => $cgi1;   # URL => 핸들러
    mount "/cgi2.pl" => $cgi2;
}

또는, 아예 다수 스크립트가 들어있는 디렉토리를 통채로 지정하여 사용할 수도 있습니다. Plack::App::CGIBin 모듈을 사용합니다.

#!perl
# bin.psgi
use strict;
use warnings;

use Plack::App::CGIBin;
use Plack::Builder;

my $app = Plack::App::CGIBin->new(root => "./")->to_app;   # "./"은 실제 스크립트가 있는 디렉토리
builder {
    mount "/" => $app;      # 여기에 "/"는 URL에 해당하는 경로
};

http://127.0.0.1:5000/cgi1.pl와 같이 접속할 수 있습니다. 이렇게 해서, 과거에 만들어 사용하던 CGI 스크립트를, 무거운 웹 서버 설치없이 손쉽게 재사용할 수 있었습니다.

기타 사항

1. plackup으로 웹 서버를 띄운 상태에서도 CGI 스크립트를 수정하면 변경 사항이 곧바로 반영이 됩니다. 그러나 PSGI 스크립트 쪽을 수정할 경우는 plackup을 재시작해야 합니다. 이것이 불편하면 --reload | -r 옵션을 쓸 수 있습니다.

plackup -r app.psgi

2. CGI 스크립트에 서브루틴이 정의되어 있으면 웹 브라우저로 접속할 때마다 서브루틴이 재정의되었다는 경고문이 콘솔 창에 뜹니다. 이것은 CGI 스크립트 쪽에 no warnings 'redefine' 프라그마를 넣어서 없앨 수 있습니다.

3. 제가 실제로 사용했던 스크립트의 경우, 내부에 system() 함수를 써서 외부 프로그램을 실행하는 루틴이 있었는데, system()의 인자로 썼던 "path/to/binary" 경로명의 슬래시가 윈도우에서는 문제가 되어서, 백슬래시를 써서 "path\\to\\binary" 형태로 고쳐주어야 했습니다.

참고

blog comments powered by Disqus