메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

한빛랩스 - 지식에 가능성을 머지하다 / 강의 콘텐츠 무료로 수강하시고 피드백을 남겨주세요. ▶︎

IT/모바일

XML과 CGI 애플리케이션

한빛미디어

|

2002-03-08

|

by HANBIT

11,085

저자: 킵 햄튼, 역 정직한

개요

펄이 널리 퍼지게 된 것에는 웹의 도움이 컸다. 웹 초기에는 정적인 HTML 이 주로 서비스 되었지만 그렇게 활발하게 상호작용을 하지는 않았다. 그렇지만 곧 CGI (Common Gateway Interface) 가 소개 되었다. 펄의 강력함과 관대함은 이 새로운 환경에 자연스럽게 받아들여졌고, 펄은 곧 CGI 스크립트의 공용어로 자리잡게 되었다.

그렇다고 해서 CGI에 약점이 전혀 없는 것은 아니다. 많은 소프트웨어 회사들이 많은 돈을 들여 이 점을 공격했지만 CGI는 아직까지 널리 쓰이고 있으며 짧은 시간 내에 없어질 기미는 보이지 않는다. 이 기사에서는 CGI 코딩에 새로운 기회를 가져다 줄 크리스찬 글란 (Christian Glahn) 의 CGI::XMLApplication 모듈을 살펴보기로 하겠다.

모델-뷰-컨트롤러(Model-View-Controller) 패턴에 깊이 의존하고 있는 CGI::XMLApplication은 기존 CGI 스크립팅 대신 모듈화된 XML 기반의 대안을 제시한다. 전형적인 CGI::XMLApplication 프로젝트는 다음과 같이 세 부분으로 구성된다. 작은 실행 스크립트는 애플리케이션에 접근할 수 있도록 해주며, 로직 모듈은 애플리케이션의 현재 상태에 따라 호출되는 다양한 핸들러 메소드들을 구현한다. 거기에 하나 이상의 XSLT 스타일시트가 애플리케이션의 상태에 따라 모듈로부터 받아온 자료를 브라우저가 사용자에게 보여줄 수 있는 형태로 바꾸어 준다.

첫번째 예 - CGI XSLT 게이트웨이

CGI::XMLApplication 은 주어진 프로젝트에 관련되어 있는 디자이너들과 개발자들이 애플리케이션 로직과 프리젠테이션을 분리하기 위해 XSLT 스타일시트를 선택했다는 가정 하에 이 분리를 가능한 한 직설적이면서 무리 없이 이루어내고자 한다. 개발자들은 setStylesheet 콜백이 현재 애플리케이션의 상태에 적합한 XSLT 스타일시트의 위치를 되돌려 준다는 것만 확실히 하면 된다. 애플리케이션이 만들어 내는 doc 트리의 변환, 변환 엔진에 xslt 파라미터들을 선택적으로 넘겨주는 것, 그리고 변환된 내용을 브라우저에게 (적절한 HTTP 헤더와 함께) 전달해 주는 과정은 사용자에게는 전혀 보이지 않는다.

분리를 강조하기 위해서 첫번째 예는 전통적인 (HTML 폼을 이용한 자료입력) 의미에서의 웹 애플리케이션이 아닌, 서버의 cgi-bin 에 추가되어 요청을 보내온 브라우저에게 보내주기 위해 XML 내용의 전체 디렉토리 트리를 변환하는 일반적인 XSLT 게이트웨이가 될 것이다. 이 변환은 스타일시트나 문서의 저자들이 사용자에게 보이지 않는 것과 마찬가지로 눈에 보이지는 않을 것이다.

첫번째 단계는 클라이언트의 요청을 애플리케이션과 연결시키는 CGI 스크립트를 만드는 것이다. 우리가 원하는 것은 XML 문서를 URL에 의해 쉽게 네비게이션 할 수 있도록 만드는 것이므로 이 문서들 사이의 하이퍼링크를 만드는 작업을 직접적으로 보이게 하고 싶은 것이다. 따라서 CGI 스크립트의 이름에 확장자가 들어가지 않게 하여 스크립트 이름이 URL 경로의 디렉토리 이름인 것처럼 보이게 할 것이다. 스크립트 이름의 오른쪽에 오는 것은 모두 XML 내용을 담고 있는 가상 도큐먼트 루트의 문맥 안에서 해석될 것이다. 이 경우, CGI 스크립트의 이름은 "stylechooser"로 붙여보자.
use strict;
use lib "/path/to/secure/webapp/libs";
use XSLGateway;
use CGI qw(:standard);
my $q = CGI->new();
my %context = ();
my $gateway_name = "stylechooser";
적절한 모듈들과 스크립트 전체에서 사용될 몇몇 변수부터 설정한 후, 애플리케이션 로직 전체를 핸들링할 클래스에게 넘겨줄 %context 해시에 필드들을 추가하는 것으로 작업을 시작하자. 이 애플리케이션을 위해 요청된 URL 스크립트 이름 오른쪽에 있는 부분 (REQUEST 엔트리)과, 쿼리스트링 중 "style" 이라는 이름으로 저장된 자료를 넘겨주게 될 것이다.
$context{REQUEST} = $q->url(-path => 1);
$context{REQUEST} =~ s/^$gateway_name\/?//;
$context{REQUEST} ||= "index.xml";
$context{STYLE}   = $q->param("style") if $q->param("style");
마지막으로 XSLGateway 로직 클래스의 인스턴스를 만들고 run 메소드를 호출하여 %context 해시를 유일한 인자로 넘겨주어 요청을 처리한다.
my $app = XSLGateway->new();
$app->run(%context);
CGI 스크립트는 이것이 전부다. 이제 대부분의 작업을 처리하는 XSLGateway 모듈을 만들어보자.
package XSLGateway;

use strict;
use vars qw(@ISA);
use CGI::XMLApplication;
use XML::LibXML;

@ISA = qw(CGI::XMLApplication);
앞에서 언급했듯이 CGI::XMLApplication은 콜백 이벤트들을 이용해 동작한다. 애플리케이션 클래스에 주어진 메소드는 특정 입력 필드 (보통은 폼을 제출하는데 사용된 버튼의 이름)의 값에 따라 실행된다. 두 가지 상이한 콜백이 구현되어야 하는데, 하나는 selectStylesheet 메소드, 또 하나는 requestDOM 메소드다.

selectStylesheet 메소드는 적절한 XSLT 스타일시트의 전체 파일시스템 상에서의 경로를 되돌려 주어야 한다. 일을 간단하게 만들기 위해 스타일시트는 하나의 디렉토리에만 존재한다는 가정 하에 진행하겠다. 유연성을 더하기 위해 $contest->{STYLE} 필드를 (쿼리 파라미터의 "style"을 통해 전달된 자료인 것을 기억할 것임.) 이용해 다른 스타일시트를 사용하는 것도 허용하도록 하겠다.
sub selectStylesheet {
    my $self = shift;
    my $context = shift;
    my $style = $context->{STYLE} || "default";
    my $style_path = "/opt/www/htdocs/stylesheets/";
    return $style_path . $style . ".xsl";
}
이제는 requestDOM 메소드를 만들어야 한다. 이 메소드는 사용자에게 전달되기 위해 변환될 XML 문서의 XML::LibXML 형식 DOM을 리턴해 주어야 한다. 만들고 있는 게이트웨이가 정적인 파일만 서비스할 것이기 때문에 적절한 문서를 XML::LibXML을 이용해 파싱하고 그 결과로 만들어지는 트리를 돌려주기만 하면 된다.
sub requestDOM {
    my $self = shift;
    my $context = shift;
    my $xml_file = $context->{REQUEST} || "index.xml";
    my $doc_path = "/opt/www/htdocs/xmldocs/";
    my $requested_doc = $doc_path . $xml_file;

    my $parser = XML::LibXML->new;
    my $doc = $parser->parse_file($requested_doc);
    return $doc;
}
방금 만든 CGI 스크립트가 서버의 cgi-bin에서 안전하게 실행되는지 확인하고 XML 문서 몇 개와 XSLT 스타일시트 한 두 개를 적당한 디렉토리에 업로드하자. 이제 준비가 끝났다. http://localhost/cgi-bin/stylechooser/mydocs/somefile.xml을 요청하면 /opt/www/htdocs/xmldocs/ 디렉토리의 mydocs/somefile.xml 파일이 선택되어 /opt/www/htdocs/stylesheets/ 폴더의 default.xsl 스타일시트를 이용해 변환되고 변환된 자료가 클라이언트에게 전달될 것이다.

이 기본적인 프레임워크는 원하는 대로 확장해서 사용할 수 있다. 예를 들면 stylechooser CGI 스크립트에서 일종의 룩업 테이블을 통하여 파일이나 디렉토리를 특정한 XSLT 스타일시트에 매핑할 수 있다 또한 HTTP 쿠키를 셋팅하고 읽어들여 사용자가 좋아하는 스타일, 즉 스킨을 선택할 수 있도록 개인화된 웹사이트를 만들어 줄 수도 있다. 기본적인 XML/XSLT 웹 퍼블리싱을 시작하면서 불편을 최소로 줄이기 위해서는 이 자그마한 CGI 애플리케이션이 제공하는 것보다 더 단순하게 만들기는 힘들 것이다.

두번째 예 -- 간단한 장바구니

마지막으로 CGI::XMLApplication을 이용하여 웹 애플리케이션 데모의 대표 격인 간단한 버전의 장바구니를 만들어보자.

앞서 들었던 예와 같이 CGI-BIN에 노출되는 애플리케이션 부분은 굉장히 작다. 이것이 하는 일을 굳이 말하자면 CustomerOrder 애플리케이션 클래스를 초기화하고 run() 메소드를 호출하는 것 뿐이다. 하지만 이번에는 CGI.pm의 Vars 해시참조의 내용을 %context 해시의 PARAMS 필드로 넘겨주게 된다.
use strict;
use CGI qw(:standard);
use lib "/path/to/secure/webapp/libs";
use CustomerOrder;
my $q = CGI->new();
my %context = ();
$context{PARAMS} = $q->Vars;

my $app = CustomerOrder->new();
$app->run(%context);
여기서 든 예제를 살펴보기 위해, 주문 애플리케이션을 위한 제품정보는 관계형 데이터베이스에 저장되어 있다고 가정하자. 제품목록은 적절한 크기이기 때문에 우리가 만들 애플리케이션은 세 개의 화면이면 충분하다. 주 자료입력화면에서는 사용자들이 주문하기 원하는 하나 이상의 제품에 대한 양을 입력한다. 확인 화면에서는 장바구니의 내용을 보여주고 선택한 항목의 가격 합계를 보여준다. 완료 화면에서는 주문이 성공적으로 처리되었다는 메시지를 보여준다. 이미 제목에서 밝혔듯이 단순하게 만들기 위해 배송이나 과금정보를 입력하는 화면은 언급하지 않겠다.
package CustomerOrder;

use strict;
use vars qw(@ISA);
use CGI::XMLApplication;
use XML::LibXML::SAX::Builder;
use XML::Generator::DBI;
use DBI;

@ISA = qw(CGI::XMLApplication);
필요한 모듈들을 로드하고 CGI::XMLApplication에서 상속했다는 선언을 해 준 다음, 애플리케이션의 여러 상태(혹은 여러 화면이라고 할 수도 있겠다)와 연관되는 여러 개의 이벤트 콜백들을 만드는 것부터 해보자. 첫번째로 registerEvents() 콜백이 핸들러들을 돌려주도록 이벤트들을 등록해야 한다. registerEvents 콜백이 돌려주는 핸들러들은 기본으로 호출되는 핸들러들에 추가하여 우리가 만드는 애플리케이션이 구현할 것이다. 여기에서는 order_confirm과 order_send 콜백을 등록하여 %context 해시의 SCREENSTYLE 필드를 셋팅하도록 할 것이다. 나중에 이 프로퍼티를 사용하여 세 가지 XSLT 스타일시트 중 어느 것이 클라이언트에게 보내줄 주문 자료를 렌더링하는데 사용될 것인지 결정할 것이다.

이벤트들이 실제 서브루틴에 매핑될 때 event_<이벤트이름> 형태의 이름을 가지는 서브루틴이 그 이벤트를 구현한다는 것을 주의해서 보라. 예를 들어 "order_confirm" 이벤트는 event_order_confirm 서브루틴에 의해 구현된다. 또한 CGI::XMLApplication가 여러 이벤트들을 선택할 때는 등록된 이벤트와 같은 이름의 파라미터가 폼에서 넘어왔는지를 기준으로 선택한다는 것에 유의하라. order_confirm 핸들러를 기동시키려면 이전 화면에서 폼에 "order_confirm"이라는 필드 이름이 있어서 널이 아닌 값을 넘겨주어야 한다. 이 예에서는 각 폼의 제출(submit) 버튼을 이용하는 쉬운 방법을 써서 원하는 결과를 얻었다.
# event registration and event callbacks
sub registerEvents {
    return qw( order_confirm order_send );
}

sub event_order_confirm {
    my ($self, $context) = @_;
    $context->{SCREENSTYLE} = "order_confirm.xsl";
}

sub event_order_send {
    my ($self, $context) = @_;
    $context->{SCREENSTYLE} = "order_send.xsl";
}
event_default 콜백은 아무 핸들러도 요청되지 않을 경우 실행된다. 여기서는 SCREENSTYLE 필드를 적절한 값으로 설정하기 위해서만 사용한다.
sub event_default {
    my ($self, $context) = @_;
    $context->{SCREENSTYLE} = "order_default.xsl";
}
event_init 콜백은 요청때마다 다른 핸들러들이 호출되기 전에 호출된다. 따라서 다른 핸들러들에 의해 사용되는 애플리케이션의 일부분들을 초기화하는데 매우 유용하다. 이 경우 fetch_recordset() 메소드를 이용해 데이터베이스에서 가져온 제품정보를 담고 있는 초기 DOM 트리를 돌려주도록 하여 이 트리를 %context 해시에 저장해 나중에 사용할 수 있다.
sub event_init {
    my ($self, $context) = @_;
    $context->{DOMTREE} = $self->fetch_recordset();
}
상태 핸들러 메소드들을 완성했으면 필수적인 selectStylesheet와 requestDOM 메소드를 구현해야 한다.

첫번째 예에서와 마찬가지로 모든 애플리케이션의 스타일시트는 서버의 동일한 디렉토리에 있다고 가정한다. 우리가 할 일은 $context->{SCREENSTYLE} (상태 핸들러에 의해 셋팅됨)에서 가져온 값을 경로의 맨 끝에 붙여 되돌려 주기만 하면 된다.
# app config and helpers
sub selectStylesheet {
    my ($self, $context) = @_;
    my $style = $context->{SCREENSTYLE};
    my $style_path = "/opt/www/htdocs/stylesheets/cart/";
    return $style_path . $style;
}
requestDOM 핸들러를 살펴보기 전에 fetch_recordset라는 (도우미) 메소드를 먼저 보면 문맥을 보다 더 확실하게 이해할 수 있을 것이다.

장바구니에 들어있는 제품 정보를 관계형 데이터베이스에서 가져온다는 점을 명심해라. 하지만 XSLT 프로세서에 넘겨주어야 하는 것은 DOM 트리다. 프로그램을 이용해 바닥부터 DOM 을 만들어가는 것보다 매트 서전트가 잘 만들어 놓은 XML::Generator::DBI 를 이용하면 일이 훨씬 더 쉬워진다. 이 모듈은 팀 번스의 DBI 모듈을 통해 실행된 SQL SELECT 문이 넘겨준 자료들로부터 SAX 이벤트를 만들어낸다. 필요한 DOM 트리를 만들어 내는 것은 XML::LibXML::SAX::Builder(이 모듈은 SAX 이벤트들로부터 XML::LibXML 형태의 DOM 트리를 만들어줌)의 인스턴스를 해당 드라이버의 핸들러로 셋팅해주기만 하면 된다.
sub fetch_recordset {
    my $self = shift;
    my $sql = "select id, name, price from products";

    my $dbh = DBI->connect("dbi:Oracle:webclients",
                           "chico",
                           "swordfish")
      || die "database connection couldn"t
              be initialized: $DBI::errstr \n";

    my $builder = XML::LibXML::SAX::Builder->new();
    my $gen = XML::Generator::DBI->new(Handler      => $builder,
                                       dbh          => $dbh,
                                       RootElement  => "document",
                                       QueryElement => "productlist",
                                       RowElement   => "product");

    my $dom = $gen->execute($sql) || die "Error Building DOM Tree\n";
    return $dom;
}
fetch_recordset 메소드는 복잡한 일을 간단하게 만들어 준다. 하지만 여기에서 리턴되는 DOM 트리는 클라이언트에게 보내기 원하는 정보만을 담고 있다. 이 외에도 이 세션 안에서 이전에 입력되었던 제품 수량도 가져와야 하고 주문한 제품 가격의 총 합계도 보여주어야 한다.
sub requestDOM {
    my ($self, $context) = @_;
    my $root = $context->{DOMTREE}->getDocumentElement();
    my $grand_total = "0";
현재의 주문량을 큰 문서의 일부로 포함시키기 위해 제품 요소들을 루프로 돌리면서 자식 요소를 각 "로우(row)" 마다 덧붙여 줄 것이다. 주문량 값은 폼에서 제출된 자료가 모두 들어있는 $context->{PARAMS} 필드에서 얻을 수 있다. 폼 필드들이 어떻게 만들어지고 자료가 어떻게 전달되는지는 샘플코드에 있는 관련 스타일시트들을 보면 알 수 있다.
    foreach my $row ($root->findnodes("/document/productlist/product")) {
        my $id         = $row->findvalue("id");
        my $cost       = $row->findvalue("price");
        my $quantity   = $context->{PARAMS}->{$id} || "0";
        my $item_total = $quantity * $cost;
        $grand_total  += $item_total;

        # add the order quantity and item totals to the tree.
        $row->appendTextChild("quantity", $quantity);
        $row->appendTextChild("item-total", $item_total);
    }
마지막으로 루트 요소에 현재 선택된 아이템 값의 합계를 담고 있는 자식 요소를 가지는 요소를 덧붙여 주문에 대한 메타정보를 추가한다.
    $grand_total ||= "0.00";
    my $info = XML::LibXML::Element->new("instance-info");
    $info->appendTextChild("order-total", $grand_total);
    $root->appendChild($info);

    return $context->{DOMTREE};
}

1;
세심한 독자라면 방금 만든 간단한 장바구니가 order_send 이벤트를 수행하는 동안 전송된 자료를 가지고 실제로는 아무것도 하지 않는다는 것을 눈치챘을 것이다. 사실 자료가 어디로 어떻게 가는지를 프로그래밍하는 것은 각 사이트의 고유한 특화된 기능이기 때문에 여기서는 특별하게 언급하지 않았다. 스타일시트를 포함해 전체 애플리케이션은 예제코드를 참고하면 된다.

결론

필자는 처음에는 CGI::XMLApplication에 회의적이었다. 전형적인 AxKit 사용자이기 때문에 mod_perl에 기초한 AxKit의 속도에 익숙해져 있었고 데이터베이스와 AxKit의 eXtensible Server Page를 이용해 동적인 XML 컨텐트를 만들어 내는 것이 아주 편했기 때문이다. 그렇지만 실제로 AxKit과 같은 XML 전용 퍼블리싱/애플리케이션 서버를 사용하는 것고 같은 사치는 많은 개발자들의 권한 밖에 있는 것이며 그럴 필요가 많은 것도 아니다. 전통적인 CGI 스크립트의 "그저 출력만 하면 되는 방식"과 고난도의 XML 중심적인 AxKit같은 훌륭한 도구들 사이에는 넓은 간극이 있는 것이다. CGI::XMLApplication 은 그 간극을 잘 채워주고 있다.

CGI::XMLApplication 은 CGI 스크립팅에 깔끔하고 모듈화된 접근법을 제공하여 내용과 프리젠테이션 사이를 명확하게 구분 짓기 쉽게 할 있도록 해준다. 이것만으로도 한 번 살펴볼 만한 가치는 있다. 그러나 이것보다 더 중요한 사실은 저수준의 세부사항을 충분히 핸들링 해주면서도 걸리적거리지 않아 당장 눈앞에 있는 문제를 해결하는데 집중할 수 있게 해준다는 것이다. CGI::XMLApplication 은 분명히 좋은 도구다.
TAG :
댓글 입력
자료실

최근 본 상품0