Seoul.pm Perl Advent Calendarhttp://advent.perl.kr/2013/2013-12-24T13:48:06+09:00Keedi KimXML::Atom::SimpleFeedDAG 기반의 태스크 스케줄러http://advent.perl.kr/2013/2013-12-24.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/saillinux">@saillinux</a> -
마음씨 좋은 외국인 노동자, 현재 페이스북에서 옵 엔지니어로 재직 중이다.
<a href="http://www.yes24.com/24/goods/4433208">거침없이 배우는 펄</a>의 공동 역자,
Perl로 MMORPG를 만들어보겠다는 꿈을 갖고 있지만
요즘은 현실과 타협해 시스템 트레이딩에 푹 빠져있는 Perl덕후,
건강을 최고의 신조로 여기고 있다.</p>
<h2>시작하며</h2>
<p>지금까지 우리는 자동화를 할때면 스크립트를 작성하거나
모듈을 작성해 여러 군데에 적용해 일을 처리하곤 하죠.
허나 더욱 고난이도의 일을 처리하다보면 태스크 의존성을 고려해야 하는 경우가 많습니다.
이를 스크립트를 작성해서 처리하다보면 다른 곳에서도 같은 코드를 작성하게 되고
이런 패턴을 일일히 모듈화 해서 새로운 스크립트에 적용하여 작성하는 것도 번거롭습니다.
펄의 <a href="https://metacpan.org/module/Graph">Graph 모듈</a>과 <a href="http://en.wikipedia.org/wiki/Directed_acyclic_graph">DAG 알고리즘</a>을 조합해 태스크의
의존성을 해결할 수 있는 스케줄러를 만들어보면 어떨까요?</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Capture::Tiny">CPAN의 Capture::Tiny 모듈</a></li>
<li><a href="https://metacpan.org/module/Graph">CPAN의 Graph 모듈</a></li>
<li><a href="https://metacpan.org/module/JSON">CPAN의 JSON 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Capture::Tiny Graph JSON
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Capture::Tiny Graph JSON
</pre>
<h2>전략</h2>
<p>근래 관심을 두게 된 방법론이 <a href="http://en.wikipedia.org/wiki/Flow-based_programming">Flow-based programming</a>입니다만
쉽게 적용 할수 없는 이론이라 그중에 몇 가지 아이디어만을 적용해 프로그램을 작성해 보기로하죠.</p>
<ul>
<li>Unix 철학을 숙지해 하나의 기능만을 완벽히 잘하는
컴포넌트를 구현 합니다. (예. ls, grep, awk, sed, xargs, ...)</li>
<li>이렇게 해서 구현한 컴포넌트를 서로 연결시켜 프로그램을 작성하게끔 도와주는 툴을 만듭니다.
이를 통해 컴포넌트 재사용 및 가용성을 개선합니다.</li>
<li>프로그램 규모가 커지면 소수의 인원 만으로 기능 구현이 힘들어집니다.
간단하고 직관적인 프레임워크를 제공해 여러 인원들이 쉽게 컴포넌트를 작성해 수고를 덥니다.</li>
</ul>
<p><em>그림 1</em>과 같이 태스크 의존성을 존중하여 프로그램을 실행 해야 하는 서비스를 만들어 보도록 하겠습니다.</p>
<p><img src="2013-12-24-1_r.png" alt="stock winner test" id="stockwinnertest" />
<em>그림 1.</em> stock winner test (<a href="2013-12-24-1.png">원본</a>)</p>
<p>물론 스크립트 하나로 작성하면 편하겠지만 지속적으로 추가되는 기능 사항을 소화해내기 위해 새로운 시도를 해보죠! :)</p>
<h2>컴포넌트 준비</h2>
<p><em>그림 1</em>의 서비스는 야후 파이낸스에서 지정한 주식 중 어떤 것이 가장 많은 이득을 냈는지 비교하는 서비스입니다.
첫번째 태스크는 야후 파이낸스 API 서비스를 이용해 요즘 관심을 가지고
보고있는 주식인 Twitter, Facebook, Tesla등의 데이터를 가져 옵니다.</p>
<pre class="brush: bash;">
$ curl 'http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20yahoo.finance.quote%20where%20symbol%20in%20(%22TWTR%22%2C%22FB%22%2C%22TSLA%22%2C%22XOM%22)&format=json&diagnostics=true&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&callback='
</pre>
<p>데이터를 무사히 가져 오면 Task2,3은 <code>retrieve_stock</code> 컴포넌트를
실행해 Twitter와 Facebook의 변화값을 참고해 가져옵니다.</p>
<pre class="brush: perl;">
sub retrieve_stock {
my ( $self, $args ) = @_;
my $task = $args->{preds}[0];
my ( $stock, $field ) = @{ $args->{params} };
my $json = decode_json( $nodes{$task}{stdout} );
my @quotes = @{ $json->{query}{results}{quote} };
for my $entry (@quotes) {
if ( $entry->{symbol} eq $stock ) {
$nodes{$self}{stdout} = $entry->{$field};
$nodes{$self}{exit} = 0;
}
}
$nodes{$self}->{exit} = 1 unless exists $nodes{$self}{stdout};
}
</pre>
<p>이렇게 해서 얻은 변화값을 비교해 어떤 주식이 오늘 이득을 많이
볼 수 있었는지 <code>aggregator</code> 컴포넌트를 통해 출력 합니다.</p>
<pre class="brush: perl;">
sub aggregator {
my ( $self, $args ) = @_;
my %changes = ();
for my $task ( @{ $args->{preds} } ) {
my $stock = $nodes{$task}{params}[0];
my $change = $nodes{$task}{stdout};
$changes{$stock} = $change;
}
my @sorted = sort { $changes{$b} <=> $changes{$a} } keys %changes;
my $winner = $sorted[0];
$nodes{$self}{stdout} = $winner;
warn "OUTPUT: The winner is $winner by change $changes{$winner}\n";
}
</pre>
<p>이제 컴포넌트 준비는 끝났습니다. :)</p>
<h2>자료구조</h2>
<p>이를 구현 하기 위해서는 각 태스크가 사용하는 컴포넌트와 스케줄러를 설계해야겠죠.
첫째로 태스크 의존성을 저장할 수 있는 자료구조가 필요합니다.
여기서는 <a href="http://en.wikipedia.org/wiki/Directed_acyclic_graph">비유향 방향 그래프(Directed Acyclic Graph, 줄여서 DAG)</a>라는
그래프 구조체를 이용해 각 태스크(node)와 의존성(edge)을 저장하겠습니다.
<a href="https://metacpan.org/module/Graph">CPAN의 Graph 모듈</a>은 모든 언어를 통틀어 현존하는 그래프 라이브러리 중에서는
최고라고 자부할 수 있을 정도로 그래프를 다루는데 있어 필요한 모든 기능을 제공합니다.</p>
<p>첫째로 해야할 일은 <em>그래프 만들기</em>입니다.
이를 위해 <code>$g0</code>라는 그래프 구조체를 생성하고 여기에 <em>node</em>로
정의된 태스크를 <em>vertex</em>로 그래프에 저장합니다.</p>
<pre class="brush: perl;">
my $g0 = Graph->new;
</pre>
<p>각 노드는 해시 키로 지정했으며 각 노드 구조체는 다음과 같은 속성을 가집니다.</p>
<ul>
<li><em>action</em>: 실제로 태스크가 수행해야 하는 외부 명령어 혹은 콜백 함수를 정의 합니다.</li>
<li><em>params</em>: action을 실행하기 위해 필요한 인자를 정의 합니다.</li>
<li><em>start</em>, <em>end time</em>: 태스크가 실행 시작 했던 시점과 끝난 시점을 저장합니다.</li>
<li><em>state</em>: 현재 태스크의 상태를 저장합니다.(예. <code>WAITING</code>, <code>RUNNING</code>, <code>DONE</code>, <code>FAIL</code>)</li>
<li><em>stdout</em>, <em>stderr</em>, <em>error</em>: action의 수행 결과물들을 나중에 명령 수행 후 저장합니다. </li>
</ul>
<p>지금까지 정의한 자료구조를 펄로 표현하면 다음과 같습니다.</p>
<pre class="brush: perl;">
my %nodes = (
Task1 => {
action => 'curl',
params => [ "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20yahoo.finance.quote%20where%20symbol%20in%20(%22TWTR%22%2C%22FB%22%2C%22TSLA%22%2C%22XOM%22)&format=json&diagnostics=true&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&callback=" ],
start_time => 0,
end_time => 0,
state => WAITING,
},
Task2 => {
action => \&retrieve_stock,
params => [qw/ TWTR Change /],
start_time => 0,
end_time => 0,
state => WAITING,
},
Task3 => {
action => \&retrieve_stock,
params => [qw/ FB Change /],
start_time => 0,
end_time => 0,
state => WAITING,
},
Task4 => {
action => \&aggregator,
params => [],
start_time => 0,
end_time => 0,
state => WAITING,
},
);
</pre>
<p>이 자료구조를 그래프에 더해야겠죠.</p>
<pre class="brush: perl;">
# add each task to the graph as node
for my $task ( keys %nodes ) {
$g0->add_vertex($task);
}
</pre>
<p>그리고 의존성을 edge로 표현하면 다음과 같습니다.</p>
<pre class="brush: perl;">
my %edges = (
Task1 => [ "Task2", "Task3" ],
Task2 => ["Task4"],
Task3 => ["Task4"],
Task4 => [],
);
</pre>
<p>이제 실제로 각각의 태스크를 연결해보죠. :)</p>
<pre class="brush: perl;">
for my $task ( keys %edges ) {
for my $dep ( @{ $edges{$task} } ) {
$g0->add_edge( $task, $dep );
}
}
</pre>
<p>그래프 객체인 <code>$g0</code>를 출력해보면 그래프의 구조를 확인할 수 있습니다.</p>
<pre class="brush: perl;">
print "INFO: The graph is $g0\n";
</pre>
<p>출력 결과는 다음과 같습니다.</p>
<pre class="brush: bash;">
INFO: The graph is Task1-Task2,Task1-Task3,Task2-Task4,Task3-Task4
</pre>
<p>조금 감이 오시나요? :)</p>
<h2>그래프 유효성 점검</h2>
<p>태스크를 실행하기 앞서 그래프가 유효한지 검증이 필요합니다.
검증하는 항목은 다음과 같습니다.</p>
<ul>
<li>그래프가 directed 그래프인지 확인.
즉 노드와 노드 사이를 연결해 주는 모든 선이 방향성을 가지고 있는지 확인.</li>
<li>그래프 자체에 순환을 가지는 의존성이 있는지 확인.
그래프에 순환 구조가 있다면 무한 루프에 빠질 수 있음.</li>
<li>태스크를 시작 하는 지점이 여러군데가 존재하는지 확인.</li>
</ul>
<p>시작 지점의 경우 시작 지점이 여러군데인 것이 문제는 아니지만 간단한 구현을 위해 제한하도록 하죠.
시작 지점은 노드들 중 <code>in_degree</code>가 없는, 즉 의존성이 없는 태스크가 하나 이상 존재 한다면
시작 지점이 여러군데인것으로 간주 할 수 있습니다.</p>
<pre class="brush: perl;">
sub validate {
my $dag = shift;
die "FATAL: graph is not a directed and acyclic\n" unless $dag->is_dag;
die "FATAL: graph contains a cycle\n" if $dag->is_cyclic;
my @heads;
my @tasks = $dag->vertices;
for my $task (@tasks) {
my $in_degree = $dag->in_degree($task);
next if $in_degree;
push @heads, $task;
}
die "FATAL: more than one execution start points\n" if @heads > 1;
}
</pre>
<h2>스케줄러 구현</h2>
<p>유효성 검증을 통과했다면 이제 태스크를 실행 시켜주는 스케쥴러를 구현해보죠.
그래프에 <a href="http://en.wikipedia.org/wiki/Topological_sorting">위상정렬(topological sorting)</a>을
실행해 수행해야 하는 태스크의 순서를 가져옵시다.</p>
<pre class="brush: perl;">
sub scheduler {
my $dag = shift;
my @ts = $dag->topological_sort;
...
</pre>
<p>순서가 정해진 태스크를 <code>@ts</code>에 저장하여 이를 하나 하나 실행합니다.</p>
<pre class="brush: perl;">
for my $task (@ts) {
if ( $dag->in_degree($task) ) {
warn "INFO: check whether predecessors of [$task] were executed successfully\n";
for my $predecessor ( $dag->predecessors($task) ) {
my $state = $nodes{$predecessor}{state};
die "FATAL: $task.$predecessor: failed\n" if $state == FAIL;
die "FATAL: $task.$predecessor: wrong exiting\n" unless $state == DONE;
warn "INFO: $task.$predecessor: success\n";
}
}
else {
warn "INFO: $task is the head, starting this task now\n";
}
...
}
</pre>
<p>먼저 시작되어야 하는 태스크가 무엇인지 확인 하기 위해 <code>in_degree</code>로 확인합니다.
즉 <code>in_degree</code>가 0이면 시작 지점으로 간주해 바로 실행 합니다.
의존성을 지니고 있는 태스크가 있다면 <code>predecessors</code> 함수를 사용하여 현재
태스크가 실행 되기 전에 필요한 태스크를 리스트 하여 해당 태스크의 상태를 확인 합니다.
그중에 하나라도 실패한 태스크가 있다면 동작을 중단합니다.</p>
<p>현재 실행 해야 하는 태스크의 노드 구조체를 가져온후 상태값과 시작 시간을 업데이트 해줍니다.</p>
<pre class="brush: perl;">
my $node = $nodes{$task};
warn "INFO: running task [$task]\n";
$node->{state} = RUNNING;
$node->{start_time} = time;
</pre>
<p>태스크가 수행해야 하는 작업과 이를 위해 필요한 인자값및 이전에 실행 되었던 태스크 목록을 준비합니다.</p>
<pre class="brush: perl;">
my $action = $nodes{$task}->{action};
my @params = @{ $nodes{$task}->{params} };
my @predecessors = $dag->predecessors($task);
</pre>
<p><code>$action</code>은 콜백함수는 물론 외부 명령도 처리할 수 있도록 구현합니다.</p>
<pre class="brush: perl;">
if ( ref $action eq 'CODE' ) {
$action->($task, {
preds => \@predecessors,
params => \@params,
});
}
else {
@$node{ 'stdout', 'stderr', 'exit' } = capture {
system $action, @params;
};
}
</pre>
<p>여기서 <code>@predecessors</code>를 인자로 전달하는 것은 이전 태스크의 결과물을 참조하기 위해서입니다.
<code>$action</code>이 코드 레퍼런스가 아니라면 외부 명령어로 간주하고 실행합니다.
여기서는 크리스마스 달력 <a href="http://advent.perl.kr/2013/2013-12-12.html">열두번째 날: 펄에서 외부명령어 실행 시키기</a> 기사에서 소개된
<a href="https://metacpan.org/module/Capture::Tiny">Capture::Tiny 모듈</a>을 이용해 표준 출력, 표준 오류, 결과 상태를 간단히 가져옵니다.</p>
<p>수행이 끝났다면 완료 시간과 수행 결과 상태를 갱신합니다.</p>
<pre class="brush: perl;">
$node->{end_time} = time;
$node->{state} = !$node->{exit} ? DONE : FAIL;
</pre>
<h2>실행 결과</h2>
<p>실행 결과는 다음과 같습니다.</p>
<pre class="brush: plain;">
INFO: The graph is Task1-Task2,Task1-Task3,Task2-Task4,Task3-Task4
INFO: Task1 is the head, starting this task now
INFO: running task [Task1]
INFO: check whether predecessors of [Task3] were executed successfully
INFO: Task3.Task1: success
INFO: running task [Task3]
INFO: check whether predecessors of [Task2] were executed successfully
INFO: Task2.Task1: success
INFO: running task [Task2]
INFO: check whether predecessors of [Task4] were executed successfully
INFO: Task4.Task3: success
INFO: Task4.Task2: success
INFO: running task [Task4]
OUTPUT: The winner is TWTR by change +4.53
</pre>
<h2>전체 코드</h2>
<p>지금까지 작성한 DAG 알고리즘 기반의 스케줄러의 전체 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use v5.14;
use strict;
use warnings;
use Capture::Tiny ':all';
use Graph;
use JSON;
use constant {
WAITING => 0,
RUNNING => 1,
DONE => 2,
FAIL => 3,
};
my %nodes = (
Task1 => {
action => 'curl',
params => [ "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20yahoo.finance.quote%20where%20symbol%20in%20(%22TWTR%22%2C%22FB%22%2C%22TSLA%22%2C%22XOM%22)&format=json&diagnostics=true&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&callback=" ],
start_time => 0,
end_time => 0,
state => WAITING,
},
Task2 => {
action => \&retrieve_stock,
params => [qw/ TWTR Change /],
start_time => 0,
end_time => 0,
state => WAITING,
},
Task3 => {
action => \&retrieve_stock,
params => [qw/ FB Change /],
start_time => 0,
end_time => 0,
state => WAITING,
},
Task4 => {
action => \&aggregator,
params => [],
start_time => 0,
end_time => 0,
state => WAITING,
},
);
my %edges = (
Task1 => [ "Task2", "Task3" ],
Task2 => ["Task4"],
Task3 => ["Task4"],
Task4 => [],
);
my $g0 = Graph->new; # there is a song called zero g love in Macross
# add each task to the graph as node
for my $task ( keys %nodes ) {
$g0->add_vertex($task);
}
# connect each task
for my $task ( keys %edges ) {
for my $dep ( @{ $edges{$task} } ) {
$g0->add_edge( $task, $dep );
}
}
warn "INFO: The graph is $g0\n";
validate($g0);
scheduler($g0);
sub validate {
my $dag = shift;
die "FATAL: graph is not a directed and acyclic\n" unless $dag->is_dag;
die "FATAL: graph contains a cycle\n" if $dag->is_cyclic;
my @heads;
my @tasks = $dag->vertices;
for my $task (@tasks) {
my $in_degree = $dag->in_degree($task);
next if $in_degree;
push @heads, $task;
}
die "FATAL: more than one execution start points\n" if @heads > 1;
}
sub scheduler {
my $dag = shift;
my @ts = $dag->topological_sort;
for my $task (@ts) {
if ( $dag->in_degree($task) ) {
warn "INFO: check whether predecessors of [$task] were executed successfully\n";
for my $predecessor ( $dag->predecessors($task) ) {
my $state = $nodes{$predecessor}{state};
die "FATAL: $task.$predecessor: failed\n" if $state == FAIL;
die "FATAL: $task.$predecessor: wrong exiting\n" unless $state == DONE;
warn "INFO: $task.$predecessor: success\n";
}
}
else {
warn "INFO: $task is the head, starting this task now\n";
}
my $node = $nodes{$task};
warn "INFO: running task [$task]\n";
$node->{state} = RUNNING;
$node->{start_time} = time;
my $action = $nodes{$task}->{action};
my @params = @{ $nodes{$task}->{params} };
my @predecessors = $dag->predecessors($task);
if ( ref $action eq 'CODE' ) {
$action->($task, {
preds => \@predecessors,
params => \@params,
});
}
else {
@$node{ 'stdout', 'stderr', 'exit' } = capture {
system $action, @params;
};
}
$node->{end_time} = time;
$node->{state} = !$node->{exit} ? DONE : FAIL;
}
}
sub retrieve_stock {
my ( $self, $args ) = @_;
my $task = $args->{preds}[0];
my ( $stock, $field ) = @{ $args->{params} };
my $json = decode_json( $nodes{$task}{stdout} );
my @quotes = @{ $json->{query}{results}{quote} };
for my $entry (@quotes) {
if ( $entry->{symbol} eq $stock ) {
$nodes{$self}{stdout} = $entry->{$field};
$nodes{$self}{exit} = 0;
}
}
$nodes{$self}->{exit} = 1 unless exists $nodes{$self}{stdout};
}
sub aggregator {
my ( $self, $args ) = @_;
my %changes = ();
for my $task ( @{ $args->{preds} } ) {
my $stock = $nodes{$task}{params}[0];
my $change = $nodes{$task}{stdout};
$changes{$stock} = $change;
}
my @sorted = sort { $changes{$b} <=> $changes{$a} } keys %changes;
my $winner = $sorted[0];
$nodes{$self}{stdout} = $winner;
warn "OUTPUT: The winner is $winner by change $changes{$winner}\n";
}
</pre>
<h2>정리하며</h2>
<p>여러분이 태스크 의존도를 고려해 구현해야 하는 서비스가 있다면
아마 지금까지 소개한 방법과 크게 다르지 않게 구현할 수 있을 것입니다.
좀 더 욕심을 내본다면 다음과 같은 것을 적용해 실제로 사용할 수 있는
프레임워크로 구현해보세요.</p>
<ul>
<li><a href="http://noflojs.org/">NoFlo</a> UI를 이용해 태스크 생성 및 의존성을 정의하는 프론트 엔드 구현</li>
<li>각 태스크를 프로세스나 스레드로 구현해 지속적으로 작동하게 구현.</li>
<li><a href="http://zookeeper.apache.org/">ZooKeeper</a>나 <a href="http://redis.io/">Redis</a> 같은 백엔드를 이용해 태스크간의 상태 혹은 결과물을 공유</li>
</ul>
<p>지금까지 펄로 작성했던 툴과 서비스를 헤아려보면 끝이 없을 정도로 많은 일을 헤쳐왔습니다.
펄이 제공 하는 이득과 혜택을 맘껏 누린 셈인데 뒤돌아 보면
펄 없이는 어떻게 그 많은 일들을 처리했을까 싶어 아찔해하곤 합니다.
주로 펄을 이용해 자동화를 구현해온 제게는 펄이 주는 가능성은 정말로 끝이 없답니다.
덕분에 욕심을 부려 더욱 많은 것을 구현해보려고 추구하게 되더군요.
여러분도 펄과 함께 조금 더 욕심을 부려보면 어떨까요?</p>
<p>Enjoy Perl~!! ;-)</p>
2013-12-24T00:00:00+09:00saillinux오타를 허용하는 문자열 일치http://advent.perl.kr/2013/2013-12-23.html<h2>저자</h2>
<p>skyloader - 심지어 게으른 만년 Perl 초보자, 치킨을 먹기위해 Perl을 배운다는 후문,
<a href="http://twitter.com/JEEN_LEE">@JEEN_LEE</a>와 동갑, skyloader <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>펄은 문자열을 다루는데 아주 효율적인 언어입니다.
제 경우 업무와 관련해 문자열을 다뤄야 할 일이 생기면 으레
<code>use strict;</code>으로 시작하는 펄 코드를 작성해서 간단히 해결하곤 합니다.
하지만 회사의 모든 사람이 <em>펄</em>이나 파이썬, 루비와 같은 프로그래밍 언어를
능수능란하게 다루며 스크립트를 작성해 문자열을 일관되게 다루는 것은 아닙니다.
회사에서도 대부분의 사람들은 이런저런 이유로 인해 직접 손으로 문자열을 다루는 것이 대부분입니다.</p>
<p>슬프게도 문제는 여기에서 비롯되죠.
문자열을 직접 사람의 손을 거쳐 다루게 되면 <em>오타</em>가 생기거나
원래 주어진 문자열을 <em>주관을 가지고 해석해 다르게 사용</em>하는 경우가 빈번합니다.
이렇게 재생산된 문자열을 다시 넘겨받아 원본 문자열과 비교해 사용한다고 하면
사소한 오타와 미묘한 단어의 재배치로 인해 간단한 정규표현식을 통한 문자열 일치로는
원본과 정확하게 일치시키지 못하는 경우가 태반입니다.
이런 경우 문제가 있는 부분을 사람이 눈으로 확인하면 쉽게 구분할 수 있지만
스크립트를 통한 자동화를 하기에는 애매하기 짝이없죠.
이번 기사에서는 이러한 상황을 극복하고 원하는 작업을 수행할 수 있도록 도와주는 알고리즘과 펄 모듈을 소개합니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Path::Tiny">CPAN의 Path::Tiny 모듈</a></li>
<li><a href="https://metacpan.org/module/String::Trigram">CPAN의 String::Trigram 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Path::Tiny String::Trigram
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Path::Tiny String::Trigram
</pre>
<h2>당면한 문제</h2>
<p>예를 들어 다음과 같은 여러 요소를 담는 박스 목록이 있다고 해보죠.</p>
<pre class="brush: plain;">
Green Box : Bat
Green Box : Gloove
Green Box : Uniform
Purple Box : Chocolate
Purple Box : Mint Candy
Purple Box : Apple Jelly
Blue Box : Brush-middle
Blue Box : Brush-wide
Blue Box : Black Ink
</pre>
<p>동료 <em>A</em>는 이것을 굳이 자신만의 방식으로 기록했네요.</p>
<pre class="brush: plain;">
Box:Blue:Brush-middle
Box:Green:Gloove
Box:Purple:Mint Candy
Box:Purple:Apple Jelly
Box:Blue:Black Ink
Box:Green:Uniform
Box:Purple:Chocolate
Box:Green:Bat
Box:Blue:Brush-wide
</pre>
<p>아... 그리고 동료 <em>B</em>는 오타가 어마어마합니다.</p>
<pre class="brush: plain;">
Grean Box : Uniform
Purfle Cox : Chocholate
Purple Box : Mint Dandy
Purlpe Box : Aple Jelly
Grean Box : Bat
Grean Box : Glove
Bule Box : Brush-middle
Blue Bix : Brush-wide
Blue Box : Black Iink
</pre>
<p>휴... 어떤가요?
눈으로 살펴보면 동료 <em>A</em>와 <em>B</em>가 기록한 항목이 원본의 어떤 항목을
염두에 두고 기록했는지 대충 알수는 있을 것 같긴 하군요.
하지만 이 양이 몇 천 또는 몇 만줄이라면 어떨까요?
또 이렇게 비교해야 할 파일이 수십 개라면 어떨까요?
결국 이것을 프로그램으로 작성해 일관되게 일치되는 것끼리 엮어주어야 할텐데 방법이 잘 떠오르시나요?
물론 가능은 하겠지만 그리 녹녹하지만은 않아보입니다. :(</p>
<h2 id="n-gram">n-gram 알고리즘</h2>
<p><a href="http://en.wikipedia.org/wiki/N-gram">n-gram 알고리즘</a>은 통계와 확률을 바탕으로한 색인 분석 등에 널리 쓰이는
방식으로 초기 검색사이트가 취한 색인 검색 알고리즘의 하나입니다.
요즘엔 생명 공학 분야의 염기서열이나 분자구조 분석에 자주 등장하는 듯 합니다.
이론이 간단하고 빠르기 때문에 여러 학문에 적용하기 안성맞춤이지요.</p>
<p>n-gram 알고리즘의 기본적인 동작 방식은 다음과 같습니다.</p>
<pre class="brush: plain;">
[ ] : n == 3 window
algorithm
[ ]--------> alg
[ ]-------> lgo
[ ]------> gor
[ ]-----> ori
[ ]----> rit
[ ]---> ith
[ ]--> thm
</pre>
<p>n-gram 알고리즘은 <code>n</code>개의 문자열 크기만큼의 창(window)을 만들어 문자열을 왼쪽에서 오른쪽으로
한 단위씩 움직이며 추출되는 문자 요소 집합(character item set)의 출현을 수집합니다.
이러한 방식으로 두 개의 문자열 각각의 문자 요소 집합을 수집한 후
출현 빈도를 비교함으로써 두 문자열을 비교해서 그 결과를 수치로 표현합니다.
이때 <code>n</code>의 값은 의미가 있다고 생각하는 음절의 수로 지정하면 되는데
이 값이 <em>1인 경우</em>는 <em>unigram</em>, <em>2인 경우</em>는 <em>bigram</em>, <em>3인 경우</em>는 <em>trigram</em>이라고 부릅니다.</p>
<h2 id="string::trigram">String::Trigram 모듈</h2>
<p>오늘 소개할 <a href="https://metacpan.org/module/String::Trigram">String::Trigram 모듈</a>은 이름에서 알 수 있듯이
<code>n</code> 값이 3인 trigram 모듈이지만 원한다면 <code>n</code> 값을 변경해 n-gram으로 사용할 수도 있습니다.</p>
<p><code>String::Trigram</code> 모듈의 <code>compare()</code> 클래스 메소드는 인자로 받은
두 개의 문자열의 그 유사성을 <code>1</code>보다 작은 소수값으로 계산해 반환하는 함수입니다.
우리에게 필요한 것은 단지 이것 하나 뿐입니다! :)</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use v5.16;
use strict;
use warnings;
use Path::Tiny;
use String::Trigram;
my @raw = path('raw.input')->lines( { chomp => 1 } );
my @ed_a = path('a.input' )->lines( { chomp => 1 } );
my @ed_b = path('b.input' )->lines( { chomp => 1 } );
say "Text A";
say for get_similarity( \@raw, \@ed_a);
say "Text B\n";
say for get_similarity( \@raw, \@ed_b);
sub get_similarity {
my $src = shift;
my $dest = shift;
my @result;
for my $s (@$src) {
my $max_smlty = 0;
my $max_smlty_item;
for my $d (@$dest) {
my $smlty = String::Trigram::compare($s, $d);
next unless $smlty > $max_smlty;
$max_smlty = $smlty;
$max_smlty_item = $d
}
push(
@result,
sprintf(
"%-26s -> %-26s : similarity = %.2f",
$s,
$max_smlty_item,
$max_smlty,
);
);
}
return @result;
}
</pre>
<p>실행 결과는 다음과 같습니다.</p>
<pre class="brush: plain;">
Text A
Green Box : Bat -> Box:Green:Bat : similarity = 0.33
Green Box : Gloove -> Box:Green:Gloove : similarity = 0.41
Green Box : Uniform -> Box:Green:Uniform : similarity = 0.43
Purple Box : Chocolate -> Box:Purple:Chocolate : similarity = 0.47
Purple Box : Mint Candy -> Box:Purple:Mint Candy : similarity = 0.48
Purple Box : Apple Jelly -> Box:Purple:Apple Jelly : similarity = 0.50
Blue Box : Brush-middle -> Box:Blue:Brush-middle : similarity = 0.53
Blue Box : Brush-wide -> Box:Blue:Brush-wide : similarity = 0.50
Blue Box : Black Ink -> Box:Blue:Black Ink : similarity = 0.48
Text B
Green Box : Bat -> Grean Box : Bat : similarity = 0.70
Green Box : Gloove -> Grean Box : Glove : similarity = 0.63
Green Box : Uniform -> Grean Box : Uniform : similarity = 0.75
Purple Box : Chocolate -> Purfle Cox : Chocholate : similarity = 0.47
Purple Box : Mint Candy -> Purple Box : Mint Dandy : similarity = 0.78
Purple Box : Apple Jelly -> Purlpe Box : Aple Jelly : similarity = 0.63
Blue Box : Brush-middle -> Bule Box : Brush-middle : similarity = 0.73
Blue Box : Brush-wide -> Blue Bix : Brush-wide : similarity = 0.78
Blue Box : Black Ink -> Blue Box : Black Iink : similarity = 0.88
</pre>
<p>와우, 놀랍군요!
수정본에 오타가 있든 순서가 바뀌어 있든 꽤나 눈으로 비교하는 것과
비슷한 수준으로 원하는 것들을 찾아서 일치시켜주고 있죠? ;-)</p>
<h2>정리하며</h2>
<p>물론 <em>n-gram</em>이 모든 유사성 검색 문제를 해결해주는 도깨비 방망이는 아닙니다.
여러분이 Daver와 같은 새로운 검색 엔진을 만들겠다면 이런 기초적인 수준의 알고리즘만으로는 부족하겠죠.
하지만 적어도 꽤 많은 상황에서 <em>n-gram</em> 알고리즘과 <a href="https://metacpan.org/module/String::Trigram">String-Trigram 모듈</a>은
여러분이 갓 내린 커피 몇 잔과 비스켓을 즐기며 마음의 여유를 찾을 정도의 시간은 충분히 마련해줄 것입니다.
오타를 내거나 자신만의 형식으로 문서를 수정한 사람에게 분노 섞인 메일을 더이상 보내지 않아도 된다는 점은 덤이겠죠. ;-)</p>
2013-12-23T00:00:00+09:00skyloaderuse Hangout;http://advent.perl.kr/2013/2013-12-22.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>2013년을 기준으로 대한민국의 스마트폰 사용비율은 약 70%를 이미 넘었다고 합니다.
일반 피처폰 신제품을 구경하기 힘든 것을 감안하면 이 비율은 앞으로 더 증가할 것을 예측할 수 있죠.
더불어 정확하지는 않지만 스마트폰 사용자 중 약 80%는 안드로이드 폰을 사용한다고 하구요.
안드로이드 폰의 가장 큰 장점은 구글의 행아웃(구 GTalk)을 기본으로 사용할 수 있다는 점입니다.
안드로이드 폰에서 행아웃은 SMS 서비스와 거의 유사하게 동작하기 때문에
중요한 정보를 전달하기에는 메일보다 조금 더 유리합니다.
회사에서 발생하는 여러가지 이벤트를 기본적으로는 메일을 통해 전달하고는 있지만
중요도에 따라 이 행아웃을 통해 추가적으로 보내줄 수 있다면 편리하지 않을까요?
예를 들어 서버의 상태를 감시하다가 대부분의 경우는 메일로 보내지만
심각한 수준이라면 행아웃으로 보낸다던가, 데일리 빌드가 실패한 경우 행아웃으로 보낸다던가 말이죠. :-)</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Net::XMPP">CPAN의 Net::XMPP 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Net::XMPP
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Net::XMPP
</pre>
<h2>사용 방법</h2>
<p>구글이 사용하는 행아웃의 프로토콜은 다행히 열린 표준을 지향하는 <a href="http://en.wikipedia.org/wiki/XMPP">XMPP 프로토콜</a> 기반입니다.
XMPP 프로토콜은 예전 Jabber라고 불렸었으며 1999년 Jabber 오픈소스 커뮤니티에서 개발되었습니다.
따라서 행아웃에 참가해 메시지를 받거나 보내려면 XMPP 프로토콜을 따르기만 하면 됩니다.
펄에서는 <a href="https://metacpan.org/module/Net::XMPP">Net::XMPP 모듈</a>을 이용해 간단히 XMPP 프로토콜을 사용할 수 있습니다.</p>
<p><code>Net::XMPP</code> 모듈으로 메시지를 보내려면 <code>Net::XMPP::Client</code> 모듈 객체를 생성해야 합니다.</p>
<pre class="brush: perl;">
use Net::XMPP;
my $conn = Net::XMPP::Client->new;
my $status = $conn->Connect(
hostname => 'talk.google.com',
port => 5222,
componentname => 'gmail.com',
connectiontype => 'tcpip',
tls => 1,
) or die "Connection failed: $!\n";
</pre>
<p>그 뒤에는 로그인을 시도해야겠죠? 로그인 역시 간단합니다.</p>
<pre class="brush: perl;">
my ( $res, $msg ) = $conn->AuthSend(
username => $username,
password => $password,
resource => $resource, # client name
);
die sprintf "Auth failed %s $!", $msg // q{}
unless defined $res and $res eq 'ok';
</pre>
<p>여기까지 문제가 없다면 메시지를 보내거나 받을 준비는 모두 끝났습니다.
메시지를 보내는 것 역시 김빠지게 간단합니다. ;)</p>
<pre class="brush: perl;">
$conn->MessageSend(
to => $recipient,
resource => $resource,
subject => 'message via ' . $resource,
type => 'chat',
body => decode_utf8($message),
);
</pre>
<h2>전체 코드</h2>
<p>다양한 스케줄러나 프로그램과 연계할 수 있도록 간단한 스크립트로 작성해보았습니다.
필요하다면 모듈 형태로 작성하는 것도 어렵지는 않겠죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use v5.18;
use utf8;
use strict;
use warnings;
use Encode qw( decode_utf8 );
use Net::XMPP;
my $username = shift or die "$0: username needed";
my $password = shift or die "$0: password needed";
my $resource = shift or die "$0: client handle needed";
my $recipient = shift or die "$0: need recipient address";
my $message = shift or die "$0: need message to send";
my $conn = Net::XMPP::Client->new;
my $status = $conn->Connect(
hostname => 'talk.google.com',
port => 5222,
componentname => 'gmail.com',
connectiontype => 'tcpip',
tls => 1,
) or die "Connection failed: $!\n";
my ( $res, $msg ) = $conn->AuthSend(
username => $username,
password => $password,
resource => $resource, # client name
);
die sprintf "Auth failed %s $!", $msg // q{}
unless defined $res and $res eq 'ok';
$conn->MessageSend(
to => $recipient,
resource => $resource,
subject => 'message via ' . $resource,
type => 'chat',
body => decode_utf8($message),
);
</pre>
<h2>실행 결과</h2>
<p>제법 그럴듯하죠?</p>
<p><img src="2013-12-22-1_r.png" alt="행아웃 메시지 알림바" id="" />
<em>그림 1.</em> 행아웃 메시지 알림바 (<a href="2013-12-22-1.png">원본</a>)</p>
<p>메시지도 정상적으로 오는군요. :)</p>
<p><img src="2013-12-22-2_r.png" alt="행아웃 메시지" id="" />
<em>그림 2.</em> 행아웃 메시지 알림바 (<a href="2013-12-22-2.png">원본</a>)</p>
<h2>정리하며</h2>
<p>사실 <a href="http://windowspbx.blogspot.kr/2013/05/hangouts-wont-hangout-with-other.html">올해 5월 구글은 GTalk를 행아웃으로 변경하면서 XMPP 지원을 빼</a>버렸습니다.
덕분에 새로운 신규 가입자의 경우 XMPP로 메시지를 주고 받는 부분이 원활하지 않을 수 있습니다.
다행히 한동안은 기존 사용자들로 인해 XMPP 호환 레이어를 두었는지 아직까지는 기존 계정의 경우
큰 문제없이 XMPP를 이용해서 메시지를 주고받을 수 있습니다.
구글이 XMPP 프로토콜을 빼버린 것은 무척 유감입니다만 XMPP가 행아웃에서
고인이 될 그 순간까지는 유용하게 사용할 수 있길 바랍니다.</p>
<p>Enjoy Your Perl! ;-)</p>
<p><img src="2013-12-22-3.png" alt="EOT" id="eot" style="margin: 0" /></p>
<p><em>EOT</em></p>
2013-12-22T00:00:00+09:00keedi번역 - "Modification of a read-only value attempted" 오류가 나는 흔한 경우http://advent.perl.kr/2013/2013-12-21.html<h2>저자</h2>
<p><a href="http://twitter.com/gypark">@gypark</a> - Perl을 좋아합니다.
<a href="http://gypark.pe.kr/wiki/Perl">GyparkWiki의 Perl페이지</a>에서 Perl 관련 문서들을 정리해두고 있습니다.</p>
<h2>시작하며</h2>
<p>언제나 매개 변수나 반환값이 복사되어 전달되거나 모든 변수가 불변성을 갖는 다른 언어들과
달리, 펄에서는 변수가 단지 다른 값 또는 변수의 별칭(alias)으로 동작하는 경우가 있고,
이런 상황에서 그 변수의 값을 수정하려 시도할 때 예상하지 못한 결과를 낼 수 있습니다.
오늘 기사는 <a href="http://www.perlmonks.org">PerlMonks</a>의
<a href="http://www.perlmonks.org/?node_id=570712">좋은 글 하나(Common Causes for "Modification of a read-only value attempted" by imp, 2006.9.1)</a>를
번역하고 더불어 관련해 제가 겪은 문제에 대해서도 소개합니다.</p>
<h2 id="modificationofaread-onlyvalueattempted">"Modification of a read-only value attempted" 오류가 나는 흔한 경우</h2>
<p>여러분이 직접적으로 또는 간접적으로 상수의 값을 수정하려고 할 때 이 오류가 발생합니다.
이런 형태의 오류는 발생된 지점이 멀리 떨어져 있어서 추적하기 어려울 때가 많습니다.
때로는 <code>$_</code> 변수의 마술과 관련있지만 여기에 책임을 물을 수는 없습니다.</p>
<p>흔한 경우들을 언급해보면 다음과 같습니다.</p>
<ul>
<li>반복문 변수를 <em>lvalue 값</em>으로 다루는 경우</li>
<li><code>foreach</code>, <code>map</code>, <code>grep</code> 안에서 <code>$_</code> 변수를 수정하는 경우</li>
<li><code>@_</code>의 원소를 직접 수정하는 경우</li>
<li><code>sort</code> 안에서 <code>$a</code>,<code>$b</code>를 수정하는 경우</li>
<li><code>sort</code> 안에서 <code>$a</code>,<code>$b</code>를 autovivify하는 경우</li>
<li>지역화되지 않은 <code>$_</code>를 수정하는 경우</li>
</ul>
<h3 id="l">루프 변수를 L값으로 다루는 경우</h3>
<p>다음 예에서, <code>$x</code>는 상수 <code>1</code>의 별칭(alias)으로 연결되었기 때문에
반복문의 몸통에서 <code>$x</code>의 값을 증가시키려고 시도하면 오류가 납니다.
아래 <em>상수값이 포함된 목록</em> 절에서 더 자세한 내용을 참고하세요.</p>
<pre class="brush: perl;">
for my $x ( 1, 2 ) {
$x++;
}
</pre>
<h3 id="foreachmapgrep_"><code>foreach</code>, <code>map</code>, <code>grep</code> 안에서 <code>$_</code> 변수를 수정하는 경우</h3>
<p>다음 예제 모두 <code>$_</code>는 상수의 별칭이고, 루프의 몸통에서 <code>$_</code>의 값을 수정하려고 하면 오류가 발생합니다.
아래 <em>상수값이 포함된 목록</em> 절에서 더 자세한 내용을 참고하세요.</p>
<pre class="brush: perl;">
for ( 1, 2 ) {
chomp;
}
for ( "foo", @list ) {
s/foo/bar/;
}
@array = map { $_++ } (1,2);
@array = grep { $_++ } (1,2);
</pre>
<h3><code>@_</code>의 원소를 직접 수정하는 경우</h3>
<p><code>@_</code> 배열의 원소를 직접 수정하면 함수에 인자로 전달된 변수의 값을 수정할 수 있습니다.
예를 들어 위의 예문에서 <code>$n</code>의 값은 이제 2가 되었습니다.
그러나 두 번째 호출처럼 상수가 전달되었을 때는 오류가 날 것입니다.</p>
<pre class="brush: perl;">
sub incr {
$_[0]++;
}
my $n = 1;
incr($n); # 좋음
incr(1); # 나쁨
</pre>
<h3 id="sortab"><code>sort</code> 안에서 <code>$a</code>, <code>$b</code>를 수정하는 경우</h3>
<p><code>sort</code> 내부에서 <code>$a</code>나 <code>$b</code>를 수정하는 것(문제의 소지는 있지만)이 허용됩니다.
그러나, <code>$a</code>나 <code>$b</code>가 상수의 별칭이 된 경우에는 역시 오류가 납니다.</p>
<pre class="brush: perl;">
@array = sort { $a++ } (1,2);
</pre>
<h3 id="sortabautovivify"><code>sort</code> 안에서 <code>$a</code>,<code>$b</code>를 autovivify하는 경우</h3>
<p>변수 <code>$a</code>와 <code>$b</code>는 정렬할 목록의 각 원소에 별칭으로 연결되며, 이와 같이 수정하는 것도 가능합니다.
그러나 현재 연결된 원소가 수정할 수 없는 원소라면 오류가 납니다.</p>
<pre class="brush: perl;">
my @bad;
$bad[0] = [1];
$bad[2] = [2];
@bad = sort {$a->[0] <=> $b->[0]} @bad;
</pre>
<p>이런 경우가 생기는 흔한 이유 중 하나는 레퍼런스의 배열을 정렬하는데 배열의 중간에 빠진 부분이 있는 경우입니다.
이런 경우 <code>$a</code>는 <code>undef</code>이 되고, 여기에 디레퍼런스를 수행하여 autovivify하려고 하면 오류가 발생합니다.</p>
<h3>지역화되지 않은 <code>$_</code>를 수정하는 경우</h3>
<p>다음 예제에서는 <code>for</code> 루프에서 <code>$_</code>가 상수 <code>1</code>의 별칭으로 연결되고
이어서 <code>prompt_user</code>를 호출했는데 이 함수는 <code>STDIN</code>에서 한 라인을 읽고
그 내용을 여전히 <code>1</code>의 별칭인 <code>$_</code>에 저장하려고 시도하면서 오류가 발생합니다.</p>
<pre class="brush: perl;">
for ( 1, 2 ) {
my $data = prompt_user();
}
sub prompt_user {
print "Enter a number\n";
while (<STDIN>) {
# Do stuff
}
}
</pre>
<p>이 오류는 다음과 같이 더 간단하게 만든 시나리오에서도 나타납니다.</p>
<pre class="brush: perl;">
for ( 1, 2 ) {
while (<STDIN>) {
}
}
</pre>
<h3 id="read-only">read-only 오류를 피하기 위한 지침서</h3>
<p><em>read-only 오류</em>를 피하려면 다음 지침을 따르세요.</p>
<ul>
<li>상수값이 포함될 가능성이 있다면 루프 변수를 <em>lvalue</em>로 간주하여 다루지 마세요.</li>
<li>지역화되지 않은 <code>$_</code>를 수정하지 마세요.</li>
<li><code>map</code>이나 <code>grep</code> 안에서 <code>$_</code>를 수정하지 마세요.</li>
<li><code>sort</code> 안에서 <code>$a</code>나 <code>$b</code>를 수정하지 마세요.</li>
<li><code>sort</code> 안에서 <code>$a</code>나 <code>$b</code>를 디레퍼런스하기 전에 그 값이 존재하며 레퍼런스가 맞는지 검사하세요.</li>
<li>아주 단순한 루프가 아니라면 <code>$_</code>를 루프 변수로 쓰지 마세요.</li>
<li><code>@_</code>의 원소를 직접 수정하지 마세요.</li>
</ul>
<h3>참고 - 상수값이 포함된 목록</h3>
<p>이 문서의 내용을 따라가는 동안 수정 가능한 대상을 만드는
표현식이 어떤 것인지 이해하는 것이 중요합니다.</p>
<p>다음 표현식에는 상수가 들어 있습니다.</p>
<pre class="brush: perl;">
$_++ for ( 1, 2 );
$_++ for ( 1, @array );
@array = map { $_++ } ( 1, @array );
</pre>
<p>반면 다음 표현식은 안전합니다.</p>
<pre class="brush: perl;">
my @array = (1,2);
for (@array) {
$_++;
}
my ( $x, $y ) = ( 1, 2 );
for ( $x, $y ) {
$_++;
}
</pre>
<p>목록(list)와 배열(array)의 차이에 관해서는 다음 글을 읽어보세요.</p>
<ul>
<li><a href="http://www.perlmonks.org/?node_id=451421">Re: Differ. array and List (there is no List)</a></li>
<li><a href="http://perlmonth.com/modules.php?name=News&file=article&sid=60">"List" Is a Four-Letter Word</a></li>
</ul>
<h2>실제 사례</h2>
<p>오늘 제가 겪은 사례를 간단히 소개하겠습니다.</p>
<p>모듈을 <code>.pm</code> 파일로 작성한 후 샘플 코드를 만들어 <code>use</code>로 해당 모듈을 불러와
제대로 동작하는 것을 확인했는데 정작 이 모듈을 설치하려 하니 <code>Build test</code>
과정에서 오류가 발생하며 설치에 실패하더군요.</p>
<pre class="brush: plain;">
# Failed test 'use TestModule'
# at t/00_compile.t line 4.
# Tried to use 'TestModule'.
# Error: Modification of a read-only value attempted at /Users/gypark/perl5/perlbrew/perls/perl-5.18/lib/site_perl/5.18.1/Digest/Perl/MD5.pm line 64.
# Compilation failed in require at /Users/gypark/perl5/perlbrew/perls/perl-5.18/lib/site_perl/5.18.1/Spreadsheet/ParseExcel.pm line 23.
# BEGIN failed--compilation aborted at /Users/gypark/perl5/perlbrew/perls/perl-5.18/lib/site_perl/5.18.1/Spreadsheet/ParseExcel.pm line 23.
# Compilation failed in require at /Users/gypark/perl5/perlbrew/perls/perl-5.18/lib/site_perl/5.18.1/Spreadsheet/XLSX.pm line 14.
# BEGIN failed--compilation aborted at /Users/gypark/perl5/perlbrew/perls/perl-5.18/lib/site_perl/5.18.1/Spreadsheet/XLSX.pm line 14.
# Compilation failed in require at /Users/gypark/work/TestModule/.build/QoQk1vej/blib/lib/TestModule.pm line 9.
# BEGIN failed--compilation aborted at /Users/gypark/work/TestModule/.build/QoQk1vej/blib/lib/TestModule.pm line 9.
t/00_compile.t .. Dubious, test returned 1 (wstat 256, 0x100)
</pre>
<p>메세지를 보면 여러 모듈이 동시다발적으로 오류가 난 것 같이 보입니다.
그래서 당황했지요.
무엇보다도, 어째서 <em><code>use</code>로 불러와서 사용하는 것은 성공</em>했으면서
정작 <em>단지 <code>use</code>만 하고 끝내는 테스트는 실패</em>했는지 영문을 알 수가 없었습니다.
그래서 오류 메시지를 천천히 따라가보았습니다.</p>
<p><code>t/00_compile.t</code> 파일은 모듈 개발툴인 <a href="https://metacpan.org/module/Minilla">Minilla</a>가 자동으로 생성한 테스트 파일입니다.</p>
<pre class="brush: perl;">
use strict;
use Test::More;
# 여기를 주목
use_ok $_ for qw(
TestModule
);
done_testing;
</pre>
<p>보다시피, 평범하게 <code>use_ok TestModule</code>을 수행하여 테스트하는 것이 아니라
한 개 이상의 모듈 이름으로 구성된 리스트를 순회하면서 리스트의 원소를
<code>$_</code> 변수에 담아 <code>use_ok</code>의 인자로 넘겨줍니다.</p>
<p>그런데 제가 만든 <code>TestModule.pm</code> 파일에는 다음과 같은 구문이 있었습니다.</p>
<pre class="brush: perl;">
use Spreadsheet::XLSX;
</pre>
<p><a href="https://metacpan.org/module/Spreadsheet::XLSX">Spreadsheet::XLSX 모듈</a>은 다음과 같이
<code>Spreadsheet::ParseExcel</code> 모듈을 불러옵니다.</p>
<pre class="brush: perl;">
use Spreadsheet::ParseExcel;
</pre>
<p><a href="https://metacpan.org/module/Spreadsheet::ParseExcel">Spreadsheet::ParseExcel 모듈</a>은
또 <code>Digest::Perl::MD5</code> 모듈을 불러옵니다.</p>
<pre class="brush: perl;">
use Digest::Perl::MD5;
</pre>
<p><a href="https://metacpan.org/module/Digest::Perl::MD5">Digest::Perl::MD5 모듈</a>의 64행의 소스는 다음과 같습니다.</p>
<pre class="brush: perl;">
while(<DATA>) { # 64행
chomp;
...
}
</pre>
<p><code>DATA</code> 파일 핸들에서 각 줄을을 읽어 <code>$_</code> 변수에 저장하고
반복문 내부를 수행하도록 되어 있습니다.
그런데 테스트 코드에서 <code>$_</code> 변수는 문자열 <code>"TestModule"</code>의 별칭이고,
이 문자열은 상수이며, 따라서 상수의 별칭인 변수의 값을 수정하려
시도하면서 오류가 납니다.</p>
<p>하나의 코드가, <code>use</code>를 따라서 네 차례나 거슬러 올라가야 도달하는 지점의
코드와 만나면서 서로 반응을 하여 <em>펑!</em>하고 터진 것이죠!</p>
<p>이 문제를 해결하기 위해서 테스트 코드를 다음과 같이 수정했습니다.</p>
<pre class="brush: perl;">
for my $m ( qw/TestModule/ ) { # 렉시컬 변수 $m
use_ok $m;
}
</pre>
<p>이제는 아무런 문제가 없이 테스트를 통과합니다.</p>
<p>그리고 뒤늦게야 알았습니다만 <a href="https://metacpan.org/module/Digest::Perl::MD5">Digest::Perl::MD5 모듈</a>의
이 부분에 대해서는 <a href="https://rt.cpan.org/Public/Bug/Display.html?id=78392">2012년에 버그 리포트에 패치를 권장하는 의견</a>이
이미 올라와 있었고 불과 일주일 전인 2013년 12월 14일에
<a href="https://metacpan.org/changes/distribution/Digest-Perl-MD5">이 패치가 적용된 새 버전</a>이 업로드된 상태더군요.</p>
<pre class="brush: perl;">
while ( defined( my $data = <DATA> ) ) { # 렉시컬 변수 $data
chomp $data; # 여기도 $_ 대신 $data
...
}
</pre>
<h2>정리하며</h2>
<p>앞으로 <code>"Modification of a read-only value attempted..."</code> 오류가 발생하면 당황하지 말고
<em>이 기사에서 언급하고 있는 오류를 저지르지 않았는지 확인</em>해 보면 좋을 것입니다.
물론 처음 코드를 작성하는 시점에 주의할 수 있다면 더욱 좋겠죠.
더불어 자신이 만들지 않은 <em>다른 모듈에서 오류가 발생</em>한다면
우선 <em>해당 모듈이 최신 버전인지 확인</em>부터 해보세요! :-)</p>
2013-12-21T00:00:00+09:00gyparkGearman 사용 사례http://advent.perl.kr/2013/2013-12-20.html<h2>저자</h2>
<p><a href="http://twitter.com/gypark">@gypark</a> - Perl을 좋아합니다.
<a href="http://gypark.pe.kr/wiki/Perl">GyparkWiki의 Perl페이지</a>에서 Perl 관련 문서들을 정리해두고 있습니다.</p>
<h2>시작하며</h2>
<p>최근에 다수의 압축 파일을 열고 내용을 확인하는 작업을 하게 되었습니다.
순차적으로 파일 하나하나를 처리하다보니 시간이 꽤 걸리길래,
혹시나 하는 생각에 <a href="http://gearman.org/">Gearman</a>을 사용해 보았고
생각 이상으로 효과가 좋다는 것을 알 수 있었습니다.
이 기사에서 그 예제 코드와 결과를 공유하고자 합니다.</p>
<h2 id="gearman">Gearman에 대하여</h2>
<p><a href="http://gearman.org/">Gearman</a>에 관한 상세한 소개는 이미
<a href="http://advent.perl.kr/2012">Seoul.pm 2012년 크리스마스 달력</a>에서 다루고 있습니다.</p>
<ul>
<li><a href="http://advent.perl.kr/2012/2012-12-15.html">자랑스러운 우리 회사의 숨은 일꾼 만들기</a></li>
<li><a href="http://advent.perl.kr/2012/2012-12-16.html">Perl and Gearman</a></li>
</ul>
<p>따라서 이 기사에서는 이런 소개는 생략하고, 작년 기사의 코드를 응용해
작업을 나눠 워커에 분배하고 수행 결과를 보이는데 집중하도록 하겠습니다.</p>
<h2>준비물</h2>
<p>작년 기사와 마찬가지로 순수 펄로 구현된 Gearman 서버, 워커, 클라이언트를 사용했습니다.
또한 이후에 설명할 실제 작업을 위해서는 몇 가지 모듈이 추가로 필요합니다.
필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Archive::Zip">CPAN의 Archive::Zip 모듈</a></li>
<li><a href="https://metacpan.org/module/File::Slurp">CPAN의 File::Slurp 모듈</a></li>
<li><a href="https://metacpan.org/module/Gearman::Client">CPAN의 Gearman::Client 모듈</a></li>
<li><a href="https://metacpan.org/module/Gearman::Server">CPAN의 Gearman::Server 모듈</a></li>
<li><a href="https://metacpan.org/module/Gearman::Worker">CPAN의 Gearman::Worker 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
Archive::Zip \
File::Slurp \
Gearman::Client \
Gearman::Server \
Gearman::Worker
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
Archive::Zip \
File::Slurp \
Gearman::Client \
Gearman::Server \
Gearman::Worker
</pre>
<h2>작업 내용</h2>
<p>제가 처리해야 할 작업은 다음과 같습니다.
디렉터리 안에 다수의 ZIP 형식의 압축 파일이 있는 상태에서 각 압축 파일을 열고,
<code>LIST.txt</code> 파일을 추출한 후, 그 파일 안에 <code>Perl</code>이라는 문자열이 존재하는지 검사합니다.
이 문자열이 없다면 그 압축 파일에 문제가 있는 것이므로 로그에 따로 기록을 합니다.</p>
<h2>일반적인 순차 처리</h2>
<p>처음에 작성했던 스크립트입니다.
디렉터리 안의 모든 파일을 대상으로 루프를 돌면서
열고 추출하고 내용을 검사하는 과정을 반복합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
local $| = 1;
use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
use File::Slurp qw( read_file );
my $member = 'LIST.txt'; # ZIP 파일 내에서 꺼낼 파일
my $target = 'Perl'; # 꺼낸 파일 안에 들어 있어야 하는 문자열
my $zip = Archive::Zip->new();
open my $fh, '>', 'log_sequential.txt' or die $!; # 로그파일
for my $file ( glob '/Users/gypark/temp/gearman/data/*' ) {
print $fh "[$file]\n";
# 압축 파일을 읽고
die 'read error' unless $zip->read($file) == AZ_OK;
# 특정 파일을 추출한 다음
die 'extract error' unless $zip->extractMemberWithoutPaths($member) == AZ_OK;
# 그 파일 안에 특정한 문자열이 있는지 확인
my $str = read_file($member);
print $fh "ERROR! $file : $str\n" unless $str =~ m|$target|i;
# 추출했던 파일은 삭제
unlink $member or die "unlink:$!";
}
close $fh;
</pre>
<p>이 스크립트를 실행하면, 검사한 압축 파일의 이름이 로그 파일에 기록됩니다.</p>
<pre class="brush: bash;">
[/Users/gypark/temp/gearman/data/00001.zip]
[/Users/gypark/temp/gearman/data/00002.zip]
[/Users/gypark/temp/gearman/data/00003.zip]
...
</pre>
<p>기사 작성을 위해 테스트할 때는 디렉터리 아래 <em>ZIP 파일이 2만 개</em> 있었습니다.
그리고 이 스크립트가 수행을 마칠 때까지 <em>약 55초 정도 소요</em>되었습니다.
실제 업무 환경에서 작업했을 때는 압축 파일의 수도 훨씬 더 많았고, 시간도 수 분 걸렸습니다.</p>
<h2 id="gearman">Gearman으로 옮기기</h2>
<p>이제 <em>2만 개의 파일 목록</em>을 쪼개어서 여러 워커에서 작업할 수 있도록 옮겨보죠.</p>
<h3 id="gearman">Gearman 클라이언트</h3>
<p>해야할 일을 여러 개의 작은 작업으로 나누어 워커에게 맡기는 <em>클라이언트 코드</em>입니다.
주요 흐름은 작년 기사에서 예제로 나왔던 합계 구하기 코드와 거의 유사합니다.
실제로 문제가 되는 것은 <em>작업을 어떻게 쪼갤 것인가</em>입니다.
처리해야 할 작업이 다수의 파일에 동일한 일을 수행하는 것이므로
<em>2만개의 전체 파일을 1천개씩 나누어서 파일 이름의 목록 스무 개를 워커에 넘겨주는 것</em>으로
쉽게 작업을 분할할 수 있었습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use Gearman::Client;
use Storable qw( freeze thaw );
# 작업 대상 목록을 만든다 (현재 2만개)
my @list = glob '/Users/gypark/temp/gearman/data/*';
# Gearman 클라이언트를 생성하고
my $client = Gearman::Client->new;
$client->job_servers('127.0.0.1:20000');
# 클라이언트가 수행할 작업 집합을 만들고
my $tasks = $client->new_task_set;
# 이 작업 집합에 워커에 맡길 작업을 등록한다.
my $num = 0;
while ( @list ) {
# 눈으로 확인하기 위해 작업마다 일련번호를 붙이자.
$num++;
# 2만개의 파일이름 목록을 1000개씩 뽑아낸다.
my @sublist = splice @list, 0, 1000;
# 작업 등록
my $handle = $tasks->add_task(
# 워커에 넘겨 줄 인자는 Storable::freeze를 써서 직렬화한다.
check => freeze( { num => $num, list => [ @sublist ] } ),
{
# 각 워커가 종료될 때마다
# 화면에 그 사실을 출력하도록 하자.
on_complete => sub {
my $arg = ${ $_[0] };
print "task [$arg] done.\n";
},
},
);
}
$tasks->wait;
</pre>
<h3 id="gearman">Gearman 워커</h3>
<p><em>Gearman 워커</em>는 클라이언트로부터 파일 목록을 넘겨받아
그 목록에 있는 파일을 열고 검사하는 실제 작업을 수행합니다.</p>
<p>따라서 워커가 하는 일은 앞에서 보았던 순차 처리 스크립트의 내용을 고스란히 포함하고 있습니다.
다만 이번에는 여러 워커가 동시에 일을 하기 때문에 추출한 파일이 서로 겹치지 않도록
<em>각 워커가 임시 디렉터리를 만들어서 그 안에서 작업</em>하도록 합니다.
또한 <em>로그 파일도 각 작업마다 따로 만들어서 기록</em>하도록 합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
local $| = 1;
use Gearman::Worker;
use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
use Cwd;
use File::Slurp;
use File::Temp qw( tempdir );
use Storable qw( thaw freeze );
my $member = 'LIST.txt'; # ZIP 파일 내에서 꺼낼 파일
my $target = 'Perl'; # 꺼낸 파일 안에 들어 있어야 하는 문자열
# Gearman 클라이언트를 생성하고
my $worker = Gearman::Worker->new;
$worker->job_servers('127.0.0.1:20000');
# check 작업 요청이 왔을 때 check() 서브루틴을 실행하도록 등록
$worker->register_function( check => \&check );
# 야~ 야근이다! ;-)
$worker->work while 1;
# 호출될 서브루틴
sub check {
# 클라이언트는 freeze로 직렬화하여 인자를 넘겨줬다.
# 이 인자는 $_[0]->arg 를 통하여 얻어낼 수 있고,
# 이것을 다시 Storable::thaw()를 써서 원래의 형태로 변환해야 한다.
my $arg = thaw( $_[0]->arg );
# 원래 형태가 해시 레퍼런스였으므로
# 다시 여기서 num과 list를 뽑아내자
my $num = $arg->{num};
my @list = @{ $arg->{list} };
my $zip = Archive::Zip->new;
# 로그 파일 이름에 작업 번호를 붙여서 구분할 수 있게 한다.
my $log = sprintf( "log_gearman_%02d.txt", $num );
open my $fh, '>', $log or die $!;
# 현재 작업 디렉터리를 기억해두고
my $cwd = getcwd;
# 임시 디렉터리를 만들어서 그쪽으로 이동 후 작업한다.
my $dir = tempdir( CLEANUP => 1 );
chdir $dir;
# @list에 나열되어 있는 각 파일에 대해 이전과 동일한 작업을 수행한다.
for my $file (@list) {
print $fh "[$file]\n";
die "read error [$num][$file]" unless $zip->read($file) == AZ_OK;
die "extract error [$num][$file]" unless $zip->extractMemberWithoutPaths($member) == AZ_OK;
my $str = read_file($member);
print $fh "ERROR! $file : $str\n" unless $str =~ m/$target/i;
unlink $member or die "unlink:$!";
}
close $fh;
# 원래 작업 디렉터리로 복귀
chdir $cwd;
rmdir $dir or die "rmdir:$!";
# 방금 수행한 부분 작업의 일련 번호를
# 다시 클라이언트에 알려주자.
return "<$num>";
}
</pre>
<p>눈여겨 볼 부분은 <em>클라이언트와 워커 사이에 인자를 전달하는 형태</em>입니다.
작년 기사와 마찬가지로 클라이언트에서 워커로 복잡한 자료 구조를
넘겨주기 위해서 <a href="http://perldoc.perl.org/Storable.html">Storable 모듈</a>을 사용합니다.</p>
<p>또한 워커의 <code>check()</code> 사용자 함수가 종료할 때는 평범한 문자열을 반환하지만
클라이언트의 <code>on_complete</code> 익명 함수가 이것을 전달받을 때는
그 문자열의 레퍼런스가 인자로 전달됩니다.
따라서 <code>${ $_[0] }</code>처럼 역참조(dereference)해야 합니다.
평범한 문자열이 아니라 배열이나 해시 등 더 복잡한 자료 구조를
반환하고 싶을 때도 역시 직렬화해야 합니다.</p>
<h2>실행</h2>
<h3 id="gearman">Gearman 서버 실행</h3>
<p>여기서는 <code>20000</code> 포트로 통신하기 때문에 서버를 실행할 때도 <em>포트 번호</em>를 <em>지정</em>해줍니다.</p>
<pre class="brush: bash;">
$ gearmand -p 20000
</pre>
<h3 id="gearman">Gearman 워커 실행</h3>
<p><em>워커를 몇 대나 띄워야 비용 대비 효과가 제일 클 것인가?</em>는 아마도 상황마다 다를 것 같습니다.
이와 관련해서 <a href="http://advent.perl.kr/2012/2012-12-16.html">작년의 16일자 기사</a>에서는 워커를 관리하는 용도로 쓸 수 있는
<a href="https://metacpan.org/module/Gearman::SlotManager">Gearman::SlotManager</a> 모듈을 소개하고 있습니다.</p>
<p>지금은 단순하게 터미널에서 수작업으로 띄우도록 합니다.
일단 두 대를 띄우기로 합시다.
쉘에서 실행시킬 때 <code>&</code>를 붙여서 후면 작업(background)으로 실행합니다.</p>
<pre class="brush: bash;">
$ perl gearman_worker.pl &
[1] 81618
$ perl gearman_worker.pl &
[2] 81622
</pre>
<h3 id="gearman">Gearman 클라이언트 실행</h3>
<p>서버와 워커가 준비되었으니 이제 실제로 작업 수행을 요청할 클라이언트를 실행합니다.</p>
<pre class="brush: bash;">
$ perl gearman_client.pl
</pre>
<p>클라이언트가 실행되면 워커가 일을 하고 마칠 때마다 결과가 출력됩니다.</p>
<pre class="brush: bash;">
task [<2>] done.
task [<1>] done.
task [<3>] done.
task [<4>] done.
task [<6>] done.
task [<5>] done.
task [<8>] done.
task [<7>] done.
task [<9>] done.
task [<10>] done.
task [<11>] done.
task [<12>] done.
task [<13>] done.
task [<14>] done.
task [<15>] done.
task [<16>] done.
task [<17>] done.
task [<18>] done.
task [<19>] done.
task [<20>] done.
$
</pre>
<p>각 부분 작업에 일련 번호를 붙였기 때문에 어느 작업이 먼저 끝나는지를 확인할 수 있습니다.
일련 번호를 부여한 순서대로 종료되지 않는 것을 보면 여러 워커가 동시에 작업을 진행하는 것이 분명해 보입니다.
워커를 한 대만 띄웠다면 1번 작업부터 20번 작업까지 번호 순으로 순차적으로 진행될테니까요. :)</p>
<p>또한 각 <em>워커가 로그 파일을 제대로 남긴 것</em>을 확인할 수 있습니다.</p>
<pre class="brush: bash;">
$ ls
...
log_gearman_01.txt
log_gearman_02.txt
log_gearman_03.txt
...
</pre>
<p>각 로그 파일을 통해 <em>정확히 1000개씩 파일을 처리했다는 것</em> 역시 확인할 수 있습니다.</p>
<pre class="brush: bash;">
$ cat log_gearman_05.txt
[/Users/gypark/temp/gearman/data/04001.zip]
[/Users/gypark/temp/gearman/data/04002.zip]
...
[/Users/gypark/temp/gearman/data/04999.zip]
[/Users/gypark/temp/gearman/data/05000.zip]
</pre>
<h2>소요 시간</h2>
<p>이제 우리의 가장 큰 관심사인 <em>"얼마나 빨라졌는가?"</em>를 살펴보겠습니다.
<em>순차 처리</em>를 할 때 <em>55초</em>가 걸렸다고 말씀드렸습니다.
그럼 <em>워커 두 대</em>를 써서 수행했을 때는, 딱 절반의 시간이 걸렸을까요?</p>
<p>단 <em>9초</em> 만에 모든 작업이 완료되었습니다!</p>
<p>어째서 이렇게까지 단축될 수 있는지 영문을 알 수가 없었습니다.
지금 이 기사를 여기까지 쓰는 시점까지는 몰랐습니다.
그런데...</p>
<h3>무언가 이상하다?!</h3>
<p><em>워커가 한 대</em>밖에 없는 경우는 어떨까요?
이 경우는 순차 처리하는 것과 다를 바가 없고 게다가 서버와 워커 사이의
통신 등의 오버헤드가 있으니 거의 비슷하거나 더 느릴 것 같습니다.</p>
<p>그런데 막상 실행해보니까 소요 시간은 <em>14초</em>!</p>
<p>여전히 순차 처리 스크립트에 비해 압도적으로 빠르더군요.
이건 아무래도 이상하다 싶어서, 순차 처리 스크립트 쪽을 수정해보았습니다.
2만 번의 루프를 도는 것을 Gearman을 쓸 때처럼 천 번씩 20회 반복을 시켜보기도 하고,
렉시컬 변수의 스코프를 조절해보기도 하고,
로그 파일을 나눠서 기록하게 해보기도 하다가 결국은 무엇을 발견했냐 하면...</p>
<h3 id="archive::zip">Archive::Zip</h3>
<p>앞에서 소개한 순차 처리 스크립트를 살펴보면 다음과 같은 작업을 수행합니다.</p>
<ul>
<li>루프 진입 전 <code>Archive::Zip</code> 객체 생성</li>
<li>해당 객체가 반복문을 2만 번 돌면서 계속 <code>read()</code>로 파일을 읽고</li>
<li><code>extractMemberWithoutPaths()</code>로 특정 파일을 추출하는 과정을 반복</li>
</ul>
<p>이 때 반복문을 계속해서 순회함에 따라 <code>extractMemberWithoutPaths()</code>
메소드의 수행 시간이 점점 길어지는 것을 발견했습니다.</p>
<p>제 MacOS X 시스템에서 <a href="https://metacpan.org/module/Time::HiRes">Time::HiRes 모듈</a>의 시간 측정 루틴이
얼마나 작은 단위까지 측정할 수 있는지는 잘 모르겠으나 이 모듈을 사용한
측정 결과에 따르면 초반에는 <code>extractMemberWithoutPaths()</code> 메소드를
수행하는데 <em>400us</em>, 즉 <em>0.0004초</em> 안팎이던 것이 루프를 <em>5천 번 돌 때는 1ms</em>,
<em>1만 번 돌 때는 2ms</em>, <em>2만 번째 돌 무렵에는 5ms</em> 이상
즉 처음에 비해 열 배 이상 시간이 소요되더군요.</p>
<p>그래서 다음 코드를 반복문 안으로 넣어 매번 반복문을 순회할 때마다 새로 객체를
만들게 했더니 이제 실행 시간이 <em>15초</em> 정도로 줄어드는 것을 확인할 수 있었습니다.</p>
<pre class="brush: perl;">
my $zip = Archive::Zip->new();
</pre>
<p>좀 더 정확히 말하면, 2만 번의 반복문에서 <em>매번 객체를 만드니 19초</em> 정도 걸렸고,
<em>Gearman을 쓰는 경우와 최대한 유사하게</em>, 루프를 천 번 도는 동안은 하나의 객체를 재사용하고
천 번째마다 새로 객체를 만들게 했더니 워커 1대를 쓰는 경우와 거의 비슷하게 <em>15초</em> 정도가 나왔습니다.</p>
<p>결국 제가 이 기사를 쓰기 시작할 때는 Gearman의 신통한 수행 속도를 소개하려고 했던 것인데
기사를 쓰는 도중에 전혀 예상치 못했던 다른 모듈의 문제점을 발견해버리고 말았습니다. :-)
그리고 가슴 아프게도 Gearman의 임팩트가 작아져버리게 되었네요.</p>
<h3>제대로 된 소요 시간 비교</h3>
<p>어쨌거나 이제 제대로 된 비교를 할 수 있게 되었으니 최종적인 테스트 결과를 정리해보겠습니다.</p>
<p>실험 환경은 다음과 같습니다.</p>
<ul>
<li>맥북 프로, 2.4GHz Intel Core i7, 8GB RAM, SSD</li>
<li>Perl 5.18, Gearman 1.11</li>
<li>zip 파일 2만 개</li>
</ul>
<p>각 단계마다 3번씩 수행한 평균 실행 소요 시간은 다음과 같습니다.</p>
<table>
<caption>평균 실행 소요 시간(단위: 초)</caption>
<col align="right" />
<col align="right" />
<col align="right" />
<col align="right" />
<col align="right" />
<col align="right" />
<col align="right" />
<thead>
<tr>
<th>순차처리</th>
<th>1워커</th>
<th>2워커</th>
<th>3워커</th>
<th>4워커</th>
<th>5워커</th>
<th>6워커</th>
</tr>
</thead>
<tbody>
<tr>
<td align="right">14.81</td>
<td align="right">14.61</td>
<td align="right">9.04</td>
<td align="right">8.57</td>
<td align="right">6.64</td>
<td align="right">5.83</td>
<td align="right">5.78</td>
</tr>
</tbody>
</table>
<p>그래프로 나타내면 다음과 같습니다.</p>
<p><img src="2013-12-20-1_r.png" alt="실행소요시간" id="" />
<em>그림 1.</em> 워커의 수에 따른 실행 소요 시간 (<a href="2013-12-20-1.png">원본</a>)</p>
<p>워커가 1대일 경우는 직관적으로 생각할 수 있듯이 순차 처리의 경우와 거의 비슷합니다.
워커가 2대가 되면 1대의 경우보다 1.6배 정도 빨라집니다.
2배로 빨라지면 참 좋겠지만 아무래도 이런저런 오버헤드 때문에 무리이겠죠.
워커가 3대인 경우는 2대와 큰 차이가 없으나 4대가 되면 다시 빨라집니다.
이것은 아마도 부분 작업의 갯수가 20개인데 3개의 워커가 나눠가지면
6바퀴 만으로 끝나지 않고 한 바퀴를 더 돌아야 하고 4개의 워커가 나눠가지면
5바퀴 만에 정확히 끝나는 것 때문에 생기는 현상이 아닐까 추측합니다.
어쨌거나 워커가 1대에서 2대로 느는 것에 비해서 2대에서 4대로 늘 때는
속도 향상 효과가 더 줄어드는데 CPU를 최대한 쓸 수 있느냐 I/O에서 병목이 걸리느냐
등등 영향을 미치는 요건은 여러 가지가 있을 것입니다만
이 기사에서 그런 부분까지는 제대로 따져보지는 않았습니다.</p>
<h2>정리하며</h2>
<p>사실 이번에 제가 했던 작업은 굳이 Gearman을 사용하지 않더라도
그저 20,000개의 파일을 10,000개씩 나눠서 순차 처리 프로세스를 동시에
두 개를 실행하여 간단히 소요 시간을 단축시킬 수 있을지도 모릅니다.
이 기사를 작성하는 도중에 이것도 해봤는데 실제로도 제일 빨랐습니다. :-)</p>
<p>그렇지만 <em>전체 작업 중에 일부분만을 분산 처리</em>해야 된다거나
또는 <em>처리 결과를 다시 받아서 추가적인 작업</em>을 해야하는 경우,
<em>여러 컴퓨터를 동시에 작업에 투입</em>하고 싶은 경우,
또 <em>작업 대상의 갯수를 미리 알 수 없어서 정적으로 분배하기가 곤란</em>한 경우라면
작업 분할과 분배를 일관성 있게 처리할 수 있는
이런 프레임워크를 사용하는 것이 간편할 것입니다.
실제 사용 사례도 작년 기사에서 언급이 되고 있지요.</p>
<p>시간이 오래 걸리는 순차 반복 작업을 할 일이 생겼을 때
이 기사가 도움이 되기를 기원해 봅니다. ;-)</p>
2013-12-20T00:00:00+09:00gyparkQt 사용하기http://advent.perl.kr/2013/2013-12-19.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/yukinpl">@yukinpl</a> - 아는 것이 아무것도 없는 Perl 초보,
<a href="http://eriny.net/">eriny.net</a>을 운영하고 있다.</p>
<h2>시작하며</h2>
<p><em>Linux에서의 GUI / Perl 에서의 GUI</em></p>
<p>Linux에서 GUI 툴킷하면 두 가지, <a href="http://www.gtk.org/">GTK+</a>와 <a href="http://qt-project.org/">Qt</a>가 떠오르시나요?
그러나 펄에서는 <em>Qt</em>가 유독 힘을 쓰지 못하는 것 같습니다.
<a href="http://perl-begin.org/uses/GUI">GUI를 표현하기 위한 펄 라이브러리</a>로는 <a href="http://www.wxperl.it/">wxPerl</a>,
<a href="https://metacpan.org/module/Tk">Tk</a>, <a href="http://techbase.kde.org/Development/Languages/Perl">Perl/Qt와 Perl/KDE</a>, <a href="http://gtk2-perl.sourceforge.net/">gtk2-perl</a> 등이 있습니다.
그 중에서도 윈도우 사용자는 <em>wxPerl</em>을, Linux 사용자는 <em>GTK+</em>를 선호하는 것 같습니다.
대체 왜 그럴까요?
아무래도 <em>Qt</em>는 안정화 되어있다보니 모듈이 지속적으로 업데이트 되고 있지 않고
잘 알려진 모듈도 흔하지 않은 것이 가장 큰 이유인 것 같습니다.
더불어 라이센스 문제도 있겠지요(원인을 잘 아신다면 제게 알려주세요! :).
개인적으로는 Linux에서 C++ 프로그램 개발 시 <em>GTK+</em>보다 <em>Qt</em>로 더 많이 작업한 편이라
친근하기도 하고 펄에서 널리 사용되 못하는 것 같아 소개해보려 합니다.</p>
<h2>준비물</h2>
<p>펄에서 <em>Qt</em>를 사용하려면 필요한 항목은 다음과 같습니다.</p>
<ul>
<li>Qt 4: 펄의 Qt 모듈은 Qt 4를 기준으로 작성되어 있습니다.</li>
<li>SmokeQt: Scipting Meta Object Kcompiler의 약자로 KDE의 바인딩 모듈입니다.</li>
<li>cmake: <code>Makefile.PL</code>이 <code>cmake</code>를 기반으로 동작합니다.</li>
</ul>
<p>데비안 계열의 리눅스를 사용하고 있다면 다음 명령을 이용해서 개발 의존 패키지를 설치합니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install build-essentials cmake libqt4-dev
</pre>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Alien::SmokeQt">CPAN의 Alien::SmokeQt 모듈</a></li>
<li><a href="https://metacpan.org/module/List::MoreUtils">CPAN의 List::MoreUtils 모듈</a></li>
<li><a href="https://metacpan.org/module/Qt">CPAN의 Qt 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Alien::SmokeQt List::MoreUtils Qt
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Alien::SmokeQt List::MoreUtils Qt
</pre>
<h2>문제 해결</h2>
<h3>설치 순서</h3>
<p>모듈 설치가 실패하는 경우 다음 순서로 하나씩 모듈 설치를 진행하면서
어느 부분에서 문제가 생겼는지 확인하도록 합니다.</p>
<ul>
<li><a href="https://metacpan.org/module/List::MoreUtils">CPAN의 List::MoreUtils 모듈</a></li>
<li><a href="https://metacpan.org/module/Alien::SmokeQt">CPAN의 Alien::SmokeQt 모듈</a></li>
<li><a href="https://metacpan.org/module/Qt4">CPAN의 Qt4 모듈</a></li>
<li><a href="https://metacpan.org/module/Qt">CPAN의 Qt 모듈</a></li>
</ul>
<h3 id="libperl.so">libperl.so 확인</h3>
<p><a href="https://metacpan.org/module/Qt">Qt 모듈</a>을 설치하려면 사용하는 펄이 공유 라이브러리 형태로 빌드되어 있어야 합니다.
시스템에서 제공하는 대부분의 펄은 공유 라이브러리 형태로 빌드 되어 있습니다.
자신만의 펄을 사용하는 경우는 펄 빌드 옵션에서 <code>useshrplib</code> 항목을 확인 후
자신이 설치한 펄 디렉터리 내부에 <code>libperl.so</code> 파일이 있는지 확인해야 합니다.</p>
<pre class="brush: bash;">
$ perl -V | grep useshrplib
config_args='
-de -Dprefix=/home/askdna/perl5/perlbrew/perls/perl-5.18.1-so
-Dusethreads -Duseshrplib -Dcccdlflags=-fPIC
-Aeval:scriptdir=/home/askdna/perl5/perlbrew/perls/perl-5.18.1-so/bin'
libc=, so=so, useshrplib=true, libperl=libperl.so
</pre>
<h3 id="perl5lib">PERL5LIB 설정</h3>
<p>빌드 후 테스트 진행시 <a href="https://metacpan.org/module/Qt4">Qt4 모듈</a>과 <a href="https://metacpan.org/module/Qt">Qt 모듈</a>의 문제로
스스로가 빌드한 모듈을 제대로 찾지 못하는 경우가 있습니다.
각각의 모듈 빌드 디렉터리에서 <code>blib/lib</code> 디렉터리와 <code>blib/arch</code> 디렉터리를
검색할 수 있도록 설정해주어야 하는데 가장 간단한 방법은
<code>PERL5LIB</code> 환경 변수를 설정하는 것입니다.</p>
<pre class="brush: bash;">
$ export PERL5LIB=/path/to/blib/lib:/path/to/blib/arch:$PERL5LIB
$ make test
</pre>
<p>테스트가 끝나면 <code>PERL5LIB</code> 환경 변수에서 추가한 두 항목을 제거하도록 합니다.</p>
<h3 id="qstringlistmodel.tqabstractitemmodel.t"><code>qstringlistmodel.t</code>와 <code>qabstractitemmodel.t</code> 테스트 실패</h3>
<p>현재 <a href="https://metacpan.org/module/Qt4">Qt4 모듈</a>과 <a href="https://metacpan.org/module/Qt">Qt 모듈</a>의 문제로 인해 상속받은 가상 함수를 호출하는 테스트는 실패하는 경우가 있습니다.
따라서 다음 파일 목록을 확인하고 해당 테스트에서 실패할 경우 일단은 강제로 설치를 진행하도록 합니다.</p>
<pre class="brush: bash;">
$ rgrep 'Qt::this()->SUPER::' Qt4-0.99.0 Qt-0.96.0
Qt4-0.99.0/qtgui/t/qstringlistmodel.t: return Qt::this()->SUPER::buddy($index);
Qt4-0.99.0/qtgui/t/qstringlistmodel.t: return Qt::this()->SUPER::flags($index);
Qt4-0.99.0/qtcore/t/qabstractitemmodel.t: return Qt::this()->SUPER::buddy($index);
Qt4-0.99.0/qtcore/t/qabstractitemmodel.t: return Qt::this()->SUPER::flags($index);
Qt-0.96.0/qtgui/t/qstringlistmodel.t: return Qt::this()->SUPER::buddy($index);
Qt-0.96.0/qtgui/t/qstringlistmodel.t: return Qt::this()->SUPER::flags($index);
Qt-0.96.0/qtcore/t/qabstractitemmodel.t: return Qt::this()->SUPER::buddy($index);
Qt-0.96.0/qtcore/t/qabstractitemmodel.t: return Qt::this()->SUPER::flags($index);
</pre>
<h2>사용해보자!</h2>
<p>다음 예제를 실행해 보죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use QtCore4;
use QtGui4;
use QtCore4::debug qw( ambiguous );
my $app = Qt::Application(\@ARGV);
my $button = Qt::PushButton( 'Hello, world!', undef );
$button->resize( 200, 30 );
$button->show;
exit $app->exec;
</pre>
<p>앞의 예제를 실행했을 때의 결과는 다음과 같습니다.</p>
<p><img src="2013-12-19-1_r.png" alt="버튼 생성 예제" id="" />
<em>그림 1.</em> 버튼 생성 예제 (<a href="2013-12-19-1.png">원본</a>)</p>
<p>여기까지 잘 진행되었다면 <em>Qt</em>를 사용하는 것에 무리가 없을 것입니다.</p>
<h2 id="signalslot">SIGNAL과 SLOT</h2>
<p><em>Qt</em>를 사용하다보면 자주 만나게 되는 함수가 바로 <code>Qt::Object::connect()</code> 입니다.
그리고 <em>SIGNAL</em>과 <em>SLOT</em>이라는 개념을 이해해야합니다.
이벤트 지향 프로그래밍을 이해하고 있다면 간단한 내용입니다.
<em>SIGNAL</em>은 이벤트의 발생 조건이고, <em>SLOT</em>은 이벤트 처리자라고 생각하면 됩니다.
그리고 <code>connect()</code> 함수가 이 둘을 묶어주는 역할을 합니다.
버튼을 클릭하면 프로그램이 종료되는 간단한 예제를 살펴보죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use QtCore4;
use QtGui4;
use QtCore4::debug qw(ambiguous);
main();
sub main {
my $app = Qt::Application(\@ARGV);
my $btnQuit = Qt::PushButton("Quit");
$btnQuit->resize( 200, 30 );
Qt::Object::connect( $btnQuit, SIGNAL "clicked()", $app, SLOT "quit()" );
$btnQuit->show;
return $app->exec;
}
</pre>
<p><code>Qt::Object::connect()</code> 함수는 <em>이벤트 발생 객체</em>, <em>이벤트</em>, <em>이벤트 처리 객체</em>, <em>이벤트 처리자</em>를 인자로 받습니다.</p>
<h2>화면 구성 #1</h2>
<p>지금까지의 예제는 하나의 컨트롤(PushButton)만 만들었는데 버젓한 화면을 구성하려면 어떻게 해야할까요?
기본적으로 제공되는 컨트롤은 부모 컨트롤를 설정할 수 있습니다.
다음은 <code>$btnQuit</code> <em>PushButton</em>을 <code>$window</code> <em>Widget</em>에 붙이는 예제입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use QtCore4;
use QtGui4;
use QtCore4::debug qw(ambiguous);
main();
sub main {
my $app = Qt::Application(\@ARGV);
my $window = Qt::Widget;
$window->resize( 200, 50 );
my $btnQuit= Qt::PushButton( "Quit", $window );
$btnQuit->setGeometry( 10, 10, 180, 30 );
Qt::Object::connect( $btnQuit, SIGNAL "clicked()", $app, SLOT "quit()" );
$btnQuit->show;
return $app->exec;
}
</pre>
<h2>화면 구성 #2</h2>
<p>여기까지 다다르면 무언가 만들려고 할 때 고민이 한 가지 생깁니다.
<em>Widget</em>에 컨트롤을 붙이기는 했는데 버튼을 눌렀을 때
기존 컨트롤의 동작이 아닌 다른 동작을 수행하려면 어떻게 해야할 까요?
<em>Widget</em>의 형태를 미리 만들어 둘 수는 없을까요?
바로 이런 경우 사용하는 것이 <em>상속(Subclassing)</em>입니다.
<em>Qt</em>의 위젯을 상속하는 방법은 다음과 같습니다.</p>
<pre class="brush: perl;">
use QtCore4::isa qw( Qt::Widget );
</pre>
<p>앞의 코드와 같이 <code>QtCore4::isa</code> 함수를 사용해 상속받을 객체를 선택합니다.
그 후 생성자인 <code>NEW()</code> 메소드를 재정의(overloading)합니다.</p>
<pre class="brush: perl;">
sub NEW {
shift->SUPER::NEW(@_);
}
</pre>
<p>이로써 기본적인 상속 과정은 끝납니다.
더 필요한 부분이 있다면 <code>NEW()</code> 메소드를 수정하거나 다른 메소드를 추가합니다.
이 때 <em>기본적인 SIGNAL과 SLOT 이외의 것을 추가할 경우</em>
다음과 같은 방법으로 명시해야 한다는 점을 <em>유의</em>해야 합니다.</p>
<pre class="brush: perl;">
use QtCore4::slots viewMessage => [];
</pre>
<p>이제 버튼을 눌렀을 때 <em>Hello World</em>를 보여주는 프로그램을 작성해 봅시다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
package ExWidget;
use QtCore4;
use QtGui4;
use QtCore4::debug qw( ambiguous );
use QtCore4::isa qw( Qt::Widget );
use QtCore4::slots viewMessage => [];
sub NEW {
shift->SUPER::NEW(@_);
}
sub viewMessage {
my $msgbox = Qt::MessageBox();
$msgbox->setText("Hello World");
return $msgbox->exec;
}
package main;
use QtCore4;
use QtGui4;
use QtCore4::debug qw( ambiguous );
use ExWidget;
main();
sub main {
my $app = Qt::Application(\@ARGV);
my $window = ExWidget();
$window->resize( 200, 50 );
my $btnQuit= Qt::PushButton( "View", $window );
$btnQuit->setGeometry( 10, 10, 180, 30 );
Qt::Object::connect( $btnQuit, SIGNAL "clicked()", $window, SLOT "viewMessage()" );
$window->show;
return $app->exec;
}
</pre>
<p>실행한 결과는 다음과 같습니다.</p>
<p><img src="2013-12-19-2_r.png" alt="상속받은 창을 띄우는 예제" id="" />
<em>그림 2.</em> 상속받은 창을 띄우는 예제 (<a href="2013-12-19-2.png">원본</a>)</p>
<h2>정리하며</h2>
<p><a href="http://qt-project.org/">Qt</a>에 대한 모든 내용을 다루기보다는 기본적인 설치와 사용 방법에 중점을 두어보았습니다.
기사를 읽어보면 아시겠지만(물론 이미 알고 계시겠지만 :) <em>Qt</em>가 그리 어려운 GUI 프레임워크는 아닙니다.
개인적으로는 <em>Qt</em>가 펄에서도 많은 사랑을 받았으면 좋겠습니다.</p>
<p>Enjoy Your Perl! ;-)</p>
2013-12-19T00:00:00+09:00yukinplRex/Boxes로 개발 환경을 관리해보자http://advent.perl.kr/2013/2013-12-18.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/corund">@corund</a> - 요즘 체력이 부족함을 느끼고 있는 Computer Programmer</p>
<h2>시작하며</h2>
<p>가상화를 이용하여 개발 환경을 실제 운영될 서버 환경과 비슷하게 관리하는 일이 많아졌습니다.
개발자들의 데스크탑이 서로 다른 환경이더라도 가상화를 이용하면 개발 환경을 쉽게 통일할 수 있습니다.
<a href="http://box.rexify.org/">Rex/Boxes</a>를 이용해 이런 가상 환경을 쉽게 설정하고 공유할 수 있습니다.
같은 작업을 하는 도구로는 <a href="http://www.vagrantup.com/">Vagrant</a>가 유명합니다.</p>
<h2>준비물</h2>
<p><a href="http://box.rexify.org/">Rex/Boxes</a>는 <a href="http://www.rexify.org/">(R)?ex</a>라는 원격 관리 도구(Deployment and Configuration Management)의 일부분입니다.
<a href="http://www.getchef.com/chef/">Chef</a>와 같은 일을 하는 도구인데 펄로 만들어져 있으며 서버 측에는 오직 <em>SSH 서버</em>만 설치되어 있으면 동작합니다.
<em>Rex</em>는 <a href="https://metacpan.org/module/XML::Simple">XML::Simple 모듈</a>과 <a href="https://metacpan.org/module/Net::SSH2">Net::SSH2 모듈</a>에 의존성이 있으므로
<code>expat</code>과 <code>ssh2</code> 라이브러리가 설치되어 있어야 정상적인 설치가 가능합니다.
데비안 계열의 리눅스를 사용하고 있다면 다음 명령을 이용해서 개발 의존 패키지를 설치합니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install libexpat1-dev libssh2-1-dev
</pre>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Rex">CPAN의 Rex 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Rex
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Rex
</pre>
<p>윈도우라면 <a href="http://strawberryperl.com/">Strawberry Perl</a>을 설치한 다음 <code>perl</code> 명령창에서 다음 명령을 실행합니다.</p>
<pre class="brush: bash;">
$ cpan Rex
</pre>
<p>윈도우에서는 터미널 관련하여 설치 중 테스트 에러가 나는 경우가 있습니다.
테스트에서 에러일 뿐 동작 자체에 문제는 없으므로 다음과 같이 테스트를 무시하고 설치합니다.</p>
<pre class="brush: bash;">
$ cpan -T Rex
</pre>
<p><a href="http://box.rexify.org/">Rex/Boxes</a>는 <a href="https://www.virtualbox.org/">VirtualBox</a>를 이용해서 가상 머신들을 관리합니다.
따라서 VirtualBox를 설치합니다. 더불어 <em>VirtualBox Extension Pack</em>도 같이 설치합니다.
또한 <em>VirtualBox</em>의 <code>VBoxManage.exe</code>(리눅스의 경우 <code>VBoxManage</code>)가
반드시 <code>PATH</code>에 있어야 하므로 <code>PATH</code>도 적절하게 설정합니다.</p>
<h2>처음 실행해보기</h2>
<p>다음 명령을 통해 <a href="http://box.rexify.org/">Rex/Boxes</a>에서 제공하는 우분투 가상 머신을 등록하고 실행해 볼 수 있습니다.</p>
<pre class="brush: bash;">
$ rexify <project_name> --template=box
$ cd <project_name>
$ rex init --name=<vmname> --url=http://box.rexify.org/box/ubuntu-server-12.10-amd64.ova
</pre>
<p>이 경우 <a href="http://box.rexify.org/box/ubuntu-server-12.10-amd64.ova">Basic Ubuntu Server 12.10 파일</a>을 다운로드한 후
<code><vmname></code>이라는 이름으로 <em>Virtualbox</em>에 등록해 가상 환경을 구성합니다.
가상 환경의 부팅이 완료된 후 <code>id=root, password=box</code>로 로그인할 수 있습니다.
경우에 따라서 오류가 발생할 수 있는데 이것은 아직 설정 등이 정확하지 않기 때문입니다.
관련해서는 더 자세히 알아보죠.</p>
<h2 id="rexfile">Rexfile</h2>
<p><a href="http://www.rexify.org/">Rex</a>는 <code>Rexfile</code> 파일을 이용해 작업을 설정합니다.
이 파일은 일반 펄 구문이며 <code>rex</code> 패키지가 제공하는 서브루틴을 명령어처럼 사용할 수 있게 되어 있습니다.
위의 예제에서 실행한 <code>rexify ...</code> 명령은 box 작업을 하는 <code>Rexfile</code> 뼈대를 생성하는 작업입니다.
이 파일을 적절히 수정하여 자신이 원하는 작업을 합니다.</p>
<p><code>Rexfile</code>에서 작업의 단위는 <em>태스크(task)</em>입니다.
위의 예제에서 <code>rex init</code>도 <code>init</code> 태스크를 실행한 것입니다.
이 태스크는 <code>task</code> 명령어로 정의할 수 있습니다.
<code>task</code> 명령어는 다음과 같은 형식입니다.</p>
<pre class="brush: perl;">
task '<task_name>' => sub {
<...>
};
</pre>
<p>이제 기본 생성된 파일을 수정하여 원하는 작업을 수행해보겠습니다.
먼저 맨 아랫줄에 <code>require <project_name>;</code> 줄을 삭제합니다.
그리고 현재 디렉터리 밑에 <code>lib</code> 디렉터리도 삭제합니다.
<code>Rexfile</code>이 커질 경우 <code>lib</code> 디렉터리 밑에 따로 태스크를 정의해서
<code>Rexfile</code>을 좀더 구조화시킬 때 이용하는 것이기 때문이 지금은 필요 없습니다.
최종적으로 만들 <code>Rexfile</code>의 첫 부분과 <em>init 태스크</em>는 아래와 같습니다.</p>
<pre class="brush: perl;">
use strict;
use warnings;
use Rex -feature => 0.40;
use Rex::Commands::Box;
use Rex::Commands::Fs;
use Rex::Box::VBox;
use Cwd qw/abs_path/;
set user => 'root';
set password => 'box';
set -passauth;
use Cwd qw( abs_path );
my $my_vm_name = 'my_vm_name_wanted';
task 'init' => sub {
my $param = shift;
box {
my ($box) = @_;
# vm 이름 지정
$box->name($my_vm_name);
# vm 이미지를 다운로드할 주소를 지정
# 명령행으로 지정할 경우 그 값을 사용하며 없으면 미리 지정한 값을 사용
my $url = $param->{url} || 'http://<myserver>/<my_vm_image>.ova';
$box->url($url);
# 사용할 포트를 지정
$box->forward_port(
ssh => [2222 => 22],
http => [8080 => 80],
);
# 공유 폴더 지정
# 현재 디렉터리를 share 라는 이름으로 공유
$box->share_folder( share => abs_path() );
# vm 접속 정보
# 설정한 값으로 변경
$box->auth(
user => "root",
password => "box",
);
# on_init 태스크를 호출해 프로비전 작업을 수행
$box->setup(qw/ on_init /);
};
};
</pre>
<p>기본 생성한 <code>Rexfile</code>에서는 VM 이름을 명령행으로 지정하지만 여기에서는 따로 내부에서 정의해줍니다.
많은 경우 프로젝트 별로 이름을 지정하면 되기 때문에 이렇게 지정하는 것이 더 좋습니다.</p>
<h2 id="virtualbox">VirtualBox 이미지 만들기</h2>
<p><a href="http://box.rexify.org/">Rex/Boxes</a>에서 제공하는 기본 이미지로 가상 머신을 만들고
<em>프로비전(provision) 작업</em>으로 원하는 설정 작업을 하여 환경을 구성할 수 있습니다.
하지만 기본 이미지로 생성한 가상 머신을 원하는 환경으로 만들기 위해서 작업할 것이 꽤 많습니다.
특히 기본 이미지가 영어권에서 작성된 것이므로 한국 환경에 맞추는
작업(로케일, 시간대 설정, 저장소 설정 등)이 추가로 필요합니다.
더구나 설치하는 패키지가 많다면(특히 <a href="http://www.catalystframework.org/">Catalyst</a>처럼
대량의 <a href="http://www.cpan.org/">cpan</a> 모듈을 설치하는 경우) 프로비전 작업이 오래 걸립니다.
<code>rex init</code>를 수행할 때마다 모든 인원의 데스크탑에서 이런 작업을 반복하는 것은 비효율적입니다.
이런 류의 작업은 미리 환경을 구성한 이미지를 공유하는 것이 훨씬 효율적입니다.
<a href="http://box.rexify.org/">Rex/Boxes</a>를 사용하면 이런 작업을 매우 쉽게 처리할 수 있습니다.</p>
<p>잠깐 곁가지로 <a href="http://www.vagrantup.com/">Vagrant</a>는 이런 면에서 조금 불편합니다.
고유의 포맷을 사용하는데다 몇 가지 기능들을 편하게 작동시키기 위해 특별히 설정해야 할 것들이 있습니다.
<em>box</em>를 만드는 일이 보통의 유저에게는 거의 필요없는 일이라고 말하고 있지만 한국 유저에게는 해당되지 않는 말입니다.</p>
<p><a href="http://box.rexify.org/">Rex/Boxes</a>에서는 VirtualBox의 기본 이미지 파일(OVA)을 이미지로 사용합니다.
따라서 VirtualBox에서 가상 머신을 설치하고 필요한 설정을 마친 후
<code>파일 > 가상 시스템 내보내기(export)</code>로 OVA 이미지를 만들면 간단히 <em>box</em>를 만들 수 있습니다.
가상 머신에 설치할 운영체제는 <a href="http://www.rexify.org/">Rex</a>가 관리할 수 있는 운영체제이기만 하면 되며 <em>SSH 서버</em>만 실행되면 됩니다.
추가로 호스트 머신과 가상 머신 사이에 폴더를 공유하려면 <em>Virtualbox Guest Addition</em>을 설치해 두어야 합니다.</p>
<p>OVA 이미지가 준비되면 이 이미지를 HTTP나 FTP로 다운로드할 수 있도록 서버에 업로드합니다.
이 이미지의 주소를 사용해 가상 머신을 생성합니다.</p>
<pre class="brush: perl;">
$box->url('http://<my_server>/<my_vm_image_path>.ova');
</pre>
<p>앞의 예제에서는 명령행 인자로 URL을 지정하면 그곳에서 다운로드하고
따로 특별히 지정하지 않으면 미리 지정된 위치의 파일을 사용하도록 설정했습니다.
명령행 인자를 사용하는 예를 보이기 위해 추가한 것이므로 실제 사용할 때는
직접 지정하는 방식으로만 사용하는 경우가 많을 것입니다.</p>
<h2>가상 머신 설정</h2>
<p>보통 많이 사용하는 NAT 방식으로 네트워킹을 사용할 때에는 포트 포워딩 설정만 해주면 됩니다.
포트 포워딩 설정에서 SSH 포트 설정이 반드시 필요합니다.
<a href="http://www.rexify.org/">Rex</a>는 SSH로 접속해 프로비전 작업을 수행하기 때문입니다.
이 때 포트 이름으로 <code>ssh</code>라는 이름을 써야만 제대로 작동합니다.</p>
<p><code>$box->share_folder</code>로 호스트 머신과 가상 머신 사이에 폴더 공유를 지정할 수 있습니다.
<code>$box->share_folder( '<sharename>' => '<호스트 디렉터리 path>' );</code> 형식으로 지정합니다.
앞의 예제에서는 <a href="https://metacpan.org/module/Cwd">Cwd 모듈</a>의 <code>abs_path()</code> 함수를 이용해 현재 디렉터리를 공유하도록 설정했습니다.
<code><sharename></code>은 가상 머신에서 <em>마운트</em>할 때 사용합니다.</p>
<p>마지막 설정으로 <code>$box->setup</code> 명령으로 가상 머신이 최초로 기동된 후
<em>Rex</em>로 처리할 프로비전 작업을 설정합니다.</p>
<h2>가상 머신을 시작/중지하는 태스크 추가</h2>
<p>가상 머신을 시작/중지하는 태스크는 다음 코드를 이용해 추가할 수 있습니다.</p>
<pre class="brush: perl;">
task 'up' => sub {
run qq{VBoxManage startvm "$my_vm_name" --type=headless};
};
task 'down' => sub {
my $box = Rex::Commands::Box->new( name => $my_vm_name );
$box->stop;
}
</pre>
<p>가상 머신을 시동하는 태스크는 <code>VBoxManage</code> 명령을 직접 사용하였습니다.
현재 <a href="http://box.rexify.org/">Rex/Boxes</a> 구현에서는 <code>$box->start</code>로 가상 머신을 시동할 경우
윈도우즈의 경우 <em>headless 모드</em>로 동작하지 않고 가상 머신 창이 뜨게 됩니다.
그래서 직접 <code>VBoxManage</code> 명령을 사용해 <em>headless 모드</em>로 시동하게 하였습니다.</p>
<p>이제 다음과 같이 가상 머신을 시작/중지할 수 있습니다.</p>
<pre class="brush: bash;">
#
# 가상 머신 시작
#
$ rex up
#
# 가상 머신 중지
#
$ rex down
</pre>
<p>가상 머신을 실행한 후 접속은 포트 포워딩으로 설정한 SSH로 접속하면 됩니다.</p>
<h2>공유 폴더 마운트</h2>
<p><code>$box->share_folder</code>로 지정한 공유 폴더를 실제 가상 머신 안에서 마운트하려면 추가적인 작업이 필요합니다.
이 작업은 <code>on_init</code> 태스크에서 지정하고 <code>$box->setup</code>을 통해 실행되도록 하였습니다.</p>
<pre class="brush: perl;">
task 'on_init' => sub {
if (! is_dir('/srv/share')) {
mkdir '/srv/share';
chown '<normal_user>', '/srv/share';
chgrp '<normal_group>', '/srv/share';
}
if ( !is_readable("/srv/share/Rexfile") ) {
mount('share', '/srv/share',
fs => 'vboxsf',
options => [qw/ defaults uid=1000 gid=1000 /],
persistent => TRUE,
);
}
my $content = cat("/etc/rc.local");
if ( $content !~ /^mount share$/m ) {
run qq{echo "\\\$i\nmount share\n\n.\nw\nq" | ed /etc/rc.local};
}
};
</pre>
<p>여기서 <code><normal_user></code>, <code><normal_group></code>은 가상 머신에 접속할 때 사용할 일반 유저 아이디와 그룹입니다.
공유 폴더 <code>share</code>를 <code>/srv/share</code> 디렉터리에 이 일반 유저가 내용을 수정할 수 있도록 마운트합니다.</p>
<p>여기서 사용한 명령어들은 모두 <code>rex</code> 명령어들입니다.
자세한 사용법은 <code>rex</code> 매뉴얼을 참조하면 됩니다.
다만 <code>mount</code> 명령어로 마운트하고 이를 <code>/etc/fstab</code>에 등록해도
가상 머신이 부팅시에 자동으로 이 공유 폴더를 마운트하지 못합니다.
이는 <em>Virtualbox Guest Addition</em> 커널 모듈이 로드되는 시점이 마운트하는 시점보다 늦기 때문입니다.
따라서 부팅 스크립트 중 맨 마지막에 실행되는 <code>/etc/rc.local</code>에서 직접 마운트를 해야 합니다.
<code>rc.local</code>의 맨 마지막 명령어는 <code>exit 0</code>이어야 하므로 간단히 마운트 명령어를
추가할 수가 없어서 <code>ed</code>를 이용하여 조금 복잡하게 작업을 했습니다.</p>
<p>이어 프로비전할 내용을 <code>on_init</code> 태스크에 추가해도 되고 아니면
따로 태스크를 정의하고 <code>$box->setup</code>에서 순서대로 호출해도 됩니다.
이를테면 <code>apt-get update</code>를 하고 <code>upgrade</code>한다면 다음처럼 추가할 수 있습니다.</p>
<pre class="brush: perl;">
task 'init' => sub {
...
$box->setup(qw/on_init apt_upgrade/);
};
...
task 'apt_upgrade' => sub {
update_package_db;
update_system;
};
</pre>
<h2 id="git">git으로 관리하기</h2>
<p><code>Rexfile</code>은 일반 텍스트 파일이므로 쉽게 <code>git</code>으로 관리할 수 있습니다.
가상 머신을 생성할 때 다운로드받은 이미지나 가상 머신 디스크 파일들은 <code>tmp</code> 디렉터리 밑에 저장됩니다.
따라서 이 디렉터리를 <code>.gitignore</code>에 추가해서 버전 관리되지 않도록 할 필요가 있습니다.</p>
<h2>정리하며</h2>
<p>가상화를 이용하여 개발 환경을 구성하는 것에는 잇점이 많습니다.
최종 전개 환경에 맞추어 작업을 할 수 있기 때문에 프로젝트 막바지의 혼란을 줄일 수 있습니다.
또한 서버 사이트 개발에 익숙하지 않은 프론트엔드 개발자와 협업하기에도 좋습니다.
개발이 완료된 후 다른 개발자에게 유지 보수가 이관될 때에도 개발 환경을 쉽게 갖추게 할 수 있습니다.
하지만 일일이 가상 환경을 세팅하는 것도 번거로운 일입니다.
이런 번거로운 작업을 <a href="http://www.rexify.org/">Rex</a>와 <a href="http://box.rexify.org/">Rex/Boxes</a>를 이용해 쉽게 자동화할 수 있습니다. ;-)</p>
2013-12-18T00:00:00+09:00corundDBIx::Simplehttp://advent.perl.kr/2013/2013-12-17.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/luzluna">@luzluna</a> - luz + luna</p>
<h2>시작하며</h2>
<p>간단히 DB를 사용할 수 있는 모듈인 DBIx::Simple을 소개합니다.</p>
<h2>준비물</h2>
<p>데비안 계열의 리눅스를 사용하고 있다면 다음 명령을 이용해서 개발 의존 패키지를 설치합니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install libpq-dev
</pre>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/DBD::Pg">CPAN의 DBD::Pg 모듈</a></li>
<li><a href="https://metacpan.org/module/DBIx::Simple">CPAN의 DBIx::Simple 모듈</a></li>
<li><a href="https://metacpan.org/module/Data::Dump">CPAN의 Data::Dump 모듈</a></li>
<li><a href="https://metacpan.org/module/File::Slurp">CPAN의 File::Slurp 모듈</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 \
DBD::Pg \
DBIx::Simple \
Data::Dump \
File::Slurp \
Mojolicious
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
DBD::Pg \
DBIx::Simple \
Data::Dump \
File::Slurp \
Mojolicious
</pre>
<h2>데이터베이스 접속 준비</h2>
<p>PostgreSQL 데이터베이스를 기준으로 데이터베이스를 준비합니다.</p>
<p>데이터베이스에 접속하는 명령은 다음과 같습니다.</p>
<pre class="brush: plain;">
$ psql -U postgres
psql (9.3beta2)
Type "help" for help.
</pre>
<p>사용자 <code>test</code>를 생성하고 비밀번호를 <code>test$$</code>로 설정하는 명령은 다음과 같습니다.</p>
<pre class="brush: plain;">
postgres=# CREATE USER test WITH PASSWORD 'test$$';
CREATE ROLE
</pre>
<p><code>test</code> 데이터베이스 생성합니다.</p>
<pre class="brush: plain;">
postgres=# CREATE DATABASE test;
CREATE DATABASE
</pre>
<p>생성한 유저에게 접근 권한 부여합니다.</p>
<pre class="brush: plain;">
postgres=# GRANT ALL PRIVILEGES ON DATABASE test to test;
GRANT
</pre>
<p>완료되었습니다. 프로그램을 종료하려면 다음 명령을 입력합니다.</p>
<pre class="brush: plain;">
postgres=# \q
$
</pre>
<h2>테이블 생성</h2>
<p>테이블을 생성하기 위해 <code>test</code> 계정으로 <code>test</code> 데이터베이스에 접속합니다.</p>
<pre class="brush: plain;">
$ psql -U test test
psql (9.3beta2)
Type "help" for help.
</pre>
<p><em>postgres 9.3이후 디폴트로 포함된 데이터타입인 JSON 타입</em>으로 데이터 필드를 선언해보았습니다.
내부적으로 <code>TEXT</code> 타입과 같지만 타입 체크 및 요소별 접근이 가능하고
특정 요소에 인덱스를 걸 수 있어서 <em>NOSQL처럼 사용할 수 있다는 장점</em>이 있습니다.</p>
<pre class="brush: plain;">
test=> CREATE TABLE item ( id SERIAL PRIMARY KEY, data JSON );
CREATE TABLE
</pre>
<p>샘플 자료를 추가해보죠.</p>
<pre class="brush: plain;">
test=> INSERT INTO item (data) VALUES ('{"name":"test1", "value":31}');
INSERT 0 1
</pre>
<h2>웹 응용을 위한 설정 파일 생성</h2>
<p><em>Mojolicious</em>는 기본적으로 설정 파일을 지원합니다.
<code>Mojolicious::Lite</code>를 이용해서 웹 응용을 제작할 경우 웹 응용과
동일한 파일명에 확장자를 <code>.conf</code>로 설정하면 자동으로 읽어들입니다.
파일을 준비해보죠.</p>
<pre class="brush: bash;">
$ mkdir dbix-simple-example
$ cd dbix-simple-example
$ touch item.conf
$ touch item.pl
</pre>
<p><code>item.conf</code> 파일을 다음처럼 작성합니다.</p>
<pre class="brush: perl;">
{
db => [
'dbi:Pg:dbname=test;host=127.0.0.1;port=5432',
'test',
'test$$',
]
}
</pre>
<h2 id="mojolicious::lite">Mojolicious::Lite 어플리케이션</h2>
<p><code>item.pl</code> 파일을 다음처럼 작성합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Mojolicious::Lite;
use DBD::Pg;
use DBI;
use DBIx::Simple;
use JSON;
my $config = plugin Config;
my $dbh = DBIx::Simple->connect( @{$config->{db}} );
helper db => sub { $dbh };
get '/item/list' => sub {
my $self = shift;
my $items = $self->db->query("SELECT * FROM item")->hashes;
for my $item (@$items) {
$item->{data} = JSON::decode_json( $item->{data} );
}
$self->render( json => $items );
};
app;
</pre>
<h2>실행</h2>
<p>실행하는 명령은 다음과 같습니다.</p>
<pre class="brush: bash;">
$ morbo -l http://*:5000 item.pl
[Mon Dec 9 18:15:17 2013] [debug] Reading config file "/home/user/dev/dbix_simple/config.conf".
[Mon Dec 9 18:15:17 2013] [info] Listening at "http://*:5000".
Server available at http://127.0.0.1:5000.
</pre>
<p><code>-l http://*:5000</code> 옵션을 이용해 <code>5000</code>번 포트로 실행했으므로
웹 브라우저로 접속할 때도 <code>http://localhost:5000</code> 주소로 접속해야 합니다.
웹 브라우저로 <code>http://localhost:5000/item/list</code> 주소로 접속해보세요.</p>
<pre class="brush: plain;">
[{"id":1,"data":{"value":31,"name":"test1"}}]
</pre>
<p>예쁘게 잘 나옵니다. :)</p>
<h2 id="create">CREATE 핸들러 추가</h2>
<p><code>item.pl</code> 웹 응용 코드의 <code>app;</code> 바로 위에 다음 코드를 추가합니다.</p>
<pre class="brush: perl;">
get '/item/create' => 'item/create';
post '/item/create' => sub {
my $self = shift;
$self->db->query(
"INSERT INTO item (data) VALUES (?)",
$self->param('data'),
);
$self->render(json => {"status"=>"done"});
};
</pre>
<p>그리고 <code>item.pl</code>의 <code>app;</code> 바로 다음에 다음 코드를 추가합니다.</p>
<pre class="brush: perl;">
__DATA__
@@ item/create.html.ep
<form action="/item/create" method="post">
<input name="data" type="text">
<input type="submit">
</form>
</pre>
<p><code>morbo</code>는 변경한 소스를 자동으로 감지해서 새로 서버를 기동시키므로
별도로 종료 후 다시 기동할 필요는 없습니다.
이제 웹브라우저를 이용해서 <code>http://localhost:5000/item/create</code> 주소로 접속해보면
아이템을 추가하기 위한 간단한 양식을 볼 수 있습니다.
양식에 JSON 형식을 입력하면 다음과 같은 결과를 확인할 수 있습니다.</p>
<pre class="brush: plain;">
#
# 입력
#
{"babo":"eee"}
#
# 결과
#
{"status":"done"}
</pre>
<p>유효하지 않은 JSON 형식을 입력할 경우의 PostgreSQL에서 발생하는 오류를 확인할 수 있습니다.</p>
<pre class="brush: plain;">
#
# 입력
#
bad json format
#
# 결과
#
DBD::Pg::st execute failed: ERROR: invalid input syntax for type json
</pre>
<h2>명령줄에서 사용하기</h2>
<p>웹 페이지를 통하지 않고 일반 명령줄 유틸리티에서도 간단히 사용할 수 있습니다.</p>
<pre class="brush: perl;">
$!/usr/bin/env perl
use v5.14;
use strict;
use warnings;
use DBIx::Simple;
use Data::Dump qw( dump );
use File::Slurp;
my $config = eval( read_file('config.conf') );
my $dbh = DBIx::Simple->connect( @{$config->{db}} );
my $ret= $dbh->query("SELECT * FROM item")->hashes;
say dump($ret);
</pre>
<p>실행한 결과는 다음과 같습니다.</p>
<pre class="brush: bash;">
$ perl dbis.pl
[
{ data => "{\"name\":\"test1\", \"value\":31}", id => 1 },
{ data => "{\"babo\":\"eee\"}", id => 2 },
]
</pre>
<h2>정리하며</h2>
<p>웹페이지를 만드는 용도로 사용한다면 <a href="https://metacpan.org/module/DBIx::XTML_Table">DBIx::XHTML_Table 모듈</a>을 이용해서 테이블로 그려 볼수도 있고,
명령줄에서 사용한다면 <a href="https://metacpan.org/module/Text::Table">Text::Table</a>을 사용해 예쁘게 출력해볼 수도 있습니다.
이 부분은 과제로 남겨두죠. :)</p>
<p><a href="http://mojolicio.us/">Mojolicious</a>나 <a href="http://perldancer.org/">Dancer</a> 같은 경량 웹프레임워크를
사용하다보면 <a href="https://metacpan.org/module/DBIx::Class">DBIx::Class 모듈</a>이 조금 무겁게 느껴질 때가 있습니다.
그렇다고 그냥 <a href="https://metacpan.org/module/DBI">DBI 모듈</a>로 직접 쿼리를 날리는 것은 불편하구요.
이런때에 간단히 <a href="https://metacpan.org/module/DBIx::Simple">DBIx::Simple 모듈</a>을 이용해보는건 어떨까요? ;-)</p>
2013-12-17T00:00:00+09:00luzlunaTest::Mojo와 함께하는 보다 쉽고 편한 Web Application 테스트http://advent.perl.kr/2013/2013-12-16.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/JEEN_LEE">@JEEN_LEE</a> - 좋은 아빠 워너비, <a href="https://github.com/jeen">github:jeen</a></p>
<h2>시작하며</h2>
<p>업계에 들어오고, 엉망진창인 테스트 코드를 쓰면서도 계속되는 고민은
<em>어떻게 하면 테스트 코드를 잘 쓸 수 있을까?</em>였습니다.
그렇다면 <em>잘 쓴 테스트 코드</em>는 과연 어떤 것일까요?
물론 제가 그것을 알고 있다면 이런 기사를 쓰고 있지는 않겠죠.
<em>좋은 테스트 도구를 만나면</em> 이전과는 다른 <em>보다 좋은 테스트를 쓸 수 있지 않을까</em>와 같은 나름의 결론을 내려보았습니다.
바로 <a href="https://metacpan.org/module/Test::Mojo">Test::Mojo</a>를 사용하면서 말이죠.
따라서 이 기사에서 다룰 테스트 어플리케이션은
<a href="http://mojolicio.us/">Mojolicious</a>로 만들어졌다는 것을 전제로 합니다.</p>
<h2>예전의 테스트 코드</h2>
<p>그렇다면 <em>보다 좋게</em> 쓰기 이전의 테스트 코드는 어땠을까요?</p>
<h3>컨트롤러/모델 하나 하나의 컴파일 테스트</h3>
<p>컨트롤러 하나하나 마다 <code>.t</code> 파일을 만들어서 정상적으로 컴파일되는지에 대한 테스트를 확인했었습니다.</p>
<pre class="brush: perl;">
# t/100-controller-user.t
use strict;
use warnings;
use Test::More;
use Catalyst::Test 'MyApp::Web';
use_ok "MyApp::Web::Controller::User";
done_testing();
</pre>
<h3>컨트롤러의 기능별 추가</h3>
<p>그리고 각 컨트롤러의 기능(API endpoint) 별로 요청을 보내고 응답을
분석해서 <em>이런 값이 있어야 되겠지</em>하면서 한 줄, 한 줄 적어갔었습니다.</p>
<pre class="brush: perl;">
# t/101-controller-user-get.t
use strict;
use warnings;
use Test::More;
use HTTP::Request::Common;
use Catalyst::Test 'MyApp::Web';
use JSON::XS;
my $res = request(
GET "/user/1",
"X-MyApp-Token" => "f9a077fae03cf63bc4b351344531bde91f0f26c9",
);
my $data = JSON::XS::decode_json($res->content);
ok($data->{user}, "Got User Data");
ok(defined $data->{user}->{rate}, "Got User-Rate");
ok(defined $data->{user}->{photo}, "Got-User-Photo");
ok(defined $data->{user}->{count}, "Got-User-Counts");
ok(defined $data->{isFriend}, "ok");
ok(defined $data->{user}->{count}, "Ok");
ok(defined $data->{me}, "OK");
done_testing();
</pre>
<h3>모듈/어플리케이션의 의존성관리</h3>
<p>가끔 테스트를 돌리려면 <code>Can't locate XXX/XXX.pm ....</code> 와 같은 아주 친숙한 오류가
종종 발생하는데 이것은 바로 의존성 관리를 제대로 하지 못해서 발생하는 오류입니다.
어플리케이션의 배포를 위해 <code>Makefile.PL</code>에 일일이 의존 모듈을 추가하고
<code>cpanm --installdeps .</code> 명령을 사용해 의존 모듈을 매번 설치해야 했습니다.</p>
<h2>그렇다면 어떻게 바뀌었나?</h2>
<h3 id="lib"><code>lib</code> 디렉토리 아래 모듈 파일의 전체 컴파일 테스트</h3>
<p>프로젝트를 구성하는 모든 이름 공간의 모듈을 한번에 테스트하도록 합니다.
이를 통해 컨트롤러 하나 하나, 모델 하나 하나에 들였던 불필요한 시간이 사라집니다.
동시에 프로젝트에서 주로 사용하던 동적 클래스 적재로 호출되던 많은 모듈도 한번에 처리할 수 있습니다.</p>
<pre class="brush: perl;">
# t/000-compile.t
use utf8;
use Test::Ika;
use Test::More;
use Path::Class::Rule;
BEGIN { binmode STDOUT, ":utf8" };
describe "Compile" => sub {
it "모든 패키지는 정상적으로 로딩되어야 한다" => sub {
my $rule = Path::Class::Rule->new;
$rule->file->name( qr/\.pm$/ ); # .pm 파일만 추출
$rule->file->size('>10'); # 이름 공간 선정을 위해 가끔 개발 도중에
# 0 byte 모듈 파일을 형식상 만들어두던 습관 때문에...
my $iter = $rule->iter('lib'); # lib 디렉토리 하위의 모든 파일들에 대해
while ( my $file = $iter->() ) {
my $pkg = $file;
$pkg =~ s!^lib/!!;
$pkg =~ s!\.pm$!!;
$pkg =~ s!/!::!g;
use_ok $pkg;
}
};
};
</pre>
<h3>각 테스트 단위의 서술</h3>
<p><a href="http://rspec.info/">Ruby RSpec</a>의 표현식을 빌려 사용할 수 있는 <a href="https://metacpan.org/module/Test::Ika">Test::Ika 모듈</a>을
통해서 앞에서 살펴본 테스트 예제 코드처럼 정의할 수 있습니다.
<code>Test::Ika</code> 모듈을 통해 기존 단순 명료했던 <a href="http://en.wikipedia.org/wiki/Test_Anything_Protocol">TAP</a>의 출력 결과와는 달리
각 테스트 코드에서 서술된 표현들을 깔끔하게 표시해줍니다.</p>
<p><img src="2013-12-16-1_r.png" alt="Test::Ika 모듈의 테스트 결과 출력" id="test::ika" />
<em>그림 1.</em> Test::Ika 모듈의 테스트 결과 출력 (<a href="2013-12-16-1.png">원본</a>)</p>
<p>예전의 테스트 코드에서는 <a href="https://metacpan.org/module/Test::More">Test::More 모듈</a>의 <code>subtest</code>를 사용해서 이런 식의 서술을 했었지요.
이처럼 테스트 단위의 서술을 기재함으로써 테스트 코드를 통해 문서화를 병용할 수 있다는 점이 장점입니다.
테스트 코드에서 얻을 수 있는 부분과는 다른 문서를 별도로 쓰고 있긴 하지만,
<em>나름 잘 구분해서 사용하면 효과적이지 않을까</em>라고 생각합니다.</p>
<h3 id="test::mojo"><code>Test::Mojo</code></h3>
<p><a href="https://metacpan.org/pod/Mojo::UserAgent">Mojo::UserAgent</a>는 HTTP 클라이언트 중 정말 간결하게 사용할 수 있습니다.
특히나 웹 API 서버가 주로 다루는 응답 결과인 JSON의 각 항목을 테스트하는데는 정말 이만한 모듈이 없습니다.
물론 여기에서 사용하는 <code>Test::Mojo</code> 자체가 <code>Mojo::UserAgent</code>를 래핑하는 형태를 취하고 있죠.</p>
<pre class="brush: perl;">
use Mojo::Base -strict;
use Test::Ika;
use Test::Mojo;
BEGIN { binmode STDOUT, ":utf8" };
my $t = Test::Mojo->new('MyApp::Web');
describe "Noop" => sub {
it "서버간의 연결확인 등의 목적으로 하는 무작업 응답이 필요함" => sub {
$t
->get_ok('/noop')
->status_is(200)
->json_has('/noop')
;
};
it "인증토큰을 가지지 않은 채로 무작업 응답 요청시에 400을 표시해야 함" => sub {
$t
->get_ok('/noop/auth')
->status_is(400)
->json_has('/message')
;
};
it "인증토큰을 가지고 있을 때는 무작업 응답 요청에 답해야 함" => sub {
$t->ua->on( start => sub {
my ($ua, $tx) = @_;
$tx->req->headers->header('X-MyApp-Token', 'xxxxxxxxxxxx');
});
$t
->get_ok('/noop/auth')
->status_is(200)
->json_has('/noop')
->json_has('/result/ok')
->json_has('/devices/0/id')
;
};
};
</pre>
<p>앞의 테스트 코드처럼 HTTP 응답이 JSON일 경우 <code>json_has()</code> 메소드를 통해 반환되는 JSON 자료를
Perl 자료 형태로 변환해서 <code>/devices/0/id</code>(<code>devices</code> 배열 항목의 첫번 째 요소의 <code>id</code> 키값)가
있는지 여부 등을 확인할 수 있는 것입니다.
이런 데이터 접근방식을 <a href="http://tools.ietf.org/html/rfc6901">JSON 포인터(RFC 6901)</a>라고 부릅니다.</p>
<p>예제에서 볼 수 있듯이 <code>Test::Mojo</code> 객체는 메소드 연쇄(method chaining)로 동작하므로
나름 깔끔하게(또는 보통의 펄 코드같지 않게) 느껴지기도 합니다.</p>
<h3>프로젝트/모듈의 의존성 관리</h3>
<p><a href="https://metacpan.org/module/Carton">Carton</a>과 <code>cpanfile</code>을 사용합니다.
기본적으로 앞에서 살펴본 <code>t/000-compile.t</code> 테스트 코드에서 모든 모듈의
컴파일 테스트 시 반드시 의존 모듈의 누락 여부를 확인할 수 있기 때문에
그때 그때 큰 죄의식없이 <code>cpanfile</code> 에 필요한 의존 모듈을 추가하면 됩니다.</p>
<p>테스트를 수행하는 명령은 다음과 같습니다.</p>
<pre class="brush: bash;">
$ carton install
$ carton exec prove t
</pre>
<h3>테스트 자동화</h3>
<p>배포되는 모듈/어플리케이션의 모든 테스트는 <a href="http://jenkins-ci.org/">Jenkins</a> 상에서 <em>커밋 단위</em> 및 <em>일 단위</em>로 이뤄집니다.
관련한 이야기는 <a href="http://event.perl.kr/kpw2012/">Korean Perl Workshop 2012</a>의 <a href="http://www.slideshare.net/JEEN/perl-web-app">발표 자료</a>를 참고하세요.</p>
<h2>정리하며</h2>
<p><em>A를 고쳤는데 B가 안된다</em>라는 식은 땀이 나는 상황은 언제 어디서고 일어나기 마련입니다.
물론 이런 저런 테스트 코드를 쓰는 기법과 좋은 방법론이 세상에는 많지만
저처럼 배움이 느린 사람에게 있어서는 어려운 이야기인지라
좀 더 좋은 방법에 대해서 갈망하는 자세를 잊어서는 안되리라 생각합니다.</p>
<blockquote>
<p>Machine should work, People should think</p>
</blockquote>
<p>어디에선가 주워들은 글귀입니다만,
누가(기계가), 언제(커밋/특정시간), 어디서(개발서버), 무엇을(이런 저런 항목들을), 어떻게(요래조래), 왜(마음의 평화를 위해)...
두 번 이상 반복될 것들은 기계에게 맡기며 다른 일에 눈을 돌리는 조바심내는 개발자가 되기를 바랍니다(제가).</p>
2013-12-16T00:00:00+09:00JEEN_LEE출력 가능한 바코드http://advent.perl.kr/2013/2013-12-15.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>최근 <a href="http://theopencloset.net/">후원하는 단체</a>의 내부 물류를 전산화하는 시스템을 구축하는 부분을 맡아 작업하며
관련해 돕고 있다보니 자연스레 <a href="http://en.wikipedia.org/wiki/Barcode">바코드(barcode)</a>를 다룰 일이 많아졌습니다.
덕분에 <a href="http://advent.perl.kr/2013/2013-12-01.html">올해 크리스마스 달력 첫째 날 기사</a>에서는
바코드를 HTML로 표현하는 기법을 다루었는데, 이 HTML 형태로 바코드를 표현하면 웹브라우저를
사용할 수 있는 환경이라면 데스크탑은 물론 스마트폰까지도 문제없이 화면에 나타낼 수 있는 점이 장점입니다.
다만 이렇게 HTML 형태로 나타낼 경우 HTML 자체가 출력에 특화된 형식이 아니다보니
원하는 위치에 정확히 몇 가지의 바코드를 이렇게, 저렇게 출력하는 것은 여간 어렵지 않습니다.
이번 기사에서는 대량의 바코드를 프린터 출력에 적합한 형식으로 생성해내는 기법을 다룹니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/PDF::Reuse">CPAN의 PDF::Reuse 모듈</a></li>
<li><a href="https://metacpan.org/module/PDF::Reuse::Barcode">CPAN의 PDF::Reuse::Barcode 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan PDF::Reuse PDF::Reuse::Barcode
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan PDF::Reuse PDF::Reuse::Barcode
</pre>
<h2>요구 조건</h2>
<p>전산화 대상이었던 물류는 대부분이 의류였고 그 중에서도 상/하의였습니다.
이런 대부분의 의류에는 출력 전문 업체에 의류 전용 태그에
바코드 출력을 의뢰해 이 태그를 붙여 각각의 항목을 구분하죠.
다만 구두나 벨트 등 항목이 작고 그래서 의류 전용 태그가 눈에 너무 잘 띄거나
태그로 인해 착용이 불편해지는 경우 이러한 태그를 붙이기가 난감합니다.
이런 경우를 대비해 예외 항목들에 한해 카탈로그를 만들어 바코드를 정리한 다음
바코드를 식별해주는 조그마한 기호를 예외 항목에 작게 붙이거나 표시하기로 합니다.</p>
<pre class="brush: plain;">
> 보낸사람: Han Manil (theopencloset)
> 받는사람: Keedi Kim
> 참조: Yu Yongbin
> notifications@github.com
> 날짜: 2013년 12월 5일 오후 4:24
> 제목: 책자(?)로 만들 바코드 보내드립니다.
>
> 안녕하세요.
>
> 다른 제품의 경우에는 바코드를 직접 부착할 수 있을 것 같은데.
> 구두는 그게 어려울 것 같아요.
> 구두만 선택하여 뽑은 150개의 데이터만 보내드립니다.
>
> 바코드 만들어 주시면 저희가 폼텍으로 뽑아 정리한 뒤
> 코팅해서 만들겠습니다.
>
> 부탁드릴게요 :)
</pre>
<p>우선 선정된 150개의 데이터는 순차 데이터로 바코드로 변환되기 전의 문자열 값입니다.
이 값은 36진법 숫자로 <code>A000</code> 부터 <code>A045</code>까지의 150개의 항목입니다.</p>
<h2 id="pdf::reuse::barcode">PDF::Reuse::Barcode</h2>
<p><a href="http://www.cpan.org/">CPAN</a>에는 PDF를 생성할 수 있는 무척 많은 모듈이 있습니다.
사실 익숙하지 않다면 이 중 어떤 모듈을 사용해야 할지 알아내는 것조차 큰 일이죠.
<a href="http://advent.perl.kr/2013/2013-12-03.html">올해 크리스마스 달력 셋째 날 기사</a>를 참조하면 모듈을 고를 때 많은 도움이 됩니다.
하지만 어떤 경우는 모듈이 이미 필요한 기능이 구현이 완료되었거나 더이상
특별히 수정해야 할 버그가 없기 때문에 추가적인 릴리스가 없는 경우도 있습니다.
더불어 자신이 처한 상황이나 모듈이 필요한 이유가 그리 범용적이지 않기 때문에 추천 수가 많지 않을 수도 있죠.
따라서 항상 여러가지 요소는 참고는 하되 정말 쓸만한 모듈인지 여부는 역시 직접 부딪혀 보는 수 밖에 없습니다.</p>
<p><a href="https://metacpan.org/module/PDF::Reuse::Barcode">PDF::Reuse::Barcode 모듈</a>은 <a href="https://metacpan.org/module/PDF::Reuse">PDF::Reuse 모듈</a>의
플러그인 형태의 모듈로 <code>PDF::Reuse</code> 모듈을 사용해서 PDF 문서를 생성할 때 손쉽게
바코드 이미지와 텍스트를 넣을 수 있게 도와줍니다.
다음은 <code>A000</code> 문자열을 바코드 이미지로 나타내고 PDF 문서로 생성하는 코드입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use v5.14;
use utf8;
use strict;
use warnings;
use PDF::Reuse;
use PDF::Reuse::Barcode;
prFile('opencloset.pdf');
my $code = 'A000';
PDF::Reuse::Barcode::Code128(
value => $code,
);
prEnd();
</pre>
<p>놀라울 정도로 간단하지 않나요? :-)</p>
<h2>대량의 바코드</h2>
<p>프로그램을 작성하다보면 <em>0 또는 1의 상황</em>과 <em>1 또는 N의 상황</em>에 처하는 경우가 많습니다.
<em>0 또는 1의 상황</em>이라면 출력 가능한 바코드를 생성해내느냐 또는 못해내느냐의 문제일테고,
<em>1 또는 N의 상황</em>이라면 하나의 바코드를 생성해내느냐 또는 N개의 바코드를 생성해내느냐의 문제겠죠.</p>
<p>그렇다면 N개의 바코드를 생성해볼까요? :)</p>
<pre class="brush: perl;">
...
prFile('opencloset.pdf');
while ( my $code = <DATA> ) {
PDF::Reuse::Barcode::Code128(
value => $code,
);
}
prEnd();
__DATA__
A000
A001
A002
...
A043
A044
A045
</pre>
<h2>낭비되는 여백!</h2>
<p>여기까지 실제로 실행해보면 바코드를 담은 PDF가 쉽게(너무도 김빠지게) 생성되죠.
하지만 자세히 살펴보면 지금 그대로 출력을 했다간 아마 큰 일이 날지도 모릅니다.</p>
<pre class="brush: plain;">
> 바코드 만들어 주시면 저희가 폼텍으로 뽑아 정리한 뒤
> 코팅해서 만들겠습니다.
</pre>
<p>이거 잘못하다간 값비싼 폼텍 용지가 몇장이나 버려질지 모르겠군요. :)
아무런 설정없이 PDF로 찍어내보면 수직으로 바코드가 정렬이 되버립니다.
PDF는 출력을 위한 형식이기 때문에 꽤나 정확히 출력할 위치를 지정해야 합니다.
더불어 폼텍 용지를 아끼는 것도 의미가 있지만 너무 바코드를 다닥다닥 붙여서
바코드 리더기가 인식하는데 문제가 있다면 이것도 적절하지 않겠죠.
A4 용지 기준으로 3열, 즉 가로 방향으로는 3개 단위로 바코드를 출력해보죠.</p>
<pre class="brush: perl;">
...
prFile('opencloset.pdf');
for ( my $i = 0; $i < @codes / 3; ++$i ) {
prPage() if $i && $i % 9 == 0;
for ( my $j = 0; $j < 3; ++$j ) {
my $code = $codes[ $i * 3 + $j ];
last unless $code;
PDF::Reuse::Barcode::Code128(
x => 40 + 190 * $j,
y => 740 - 86 * ($i % 9),
value => $code,
size => 1.5,
);
}
}
prEnd();
__DATA__
A000
A001
A002
...
A043
A044
A045
</pre>
<p>이전 코드와 비교해 추가된 부분은 다음과 같습니다.</p>
<ul>
<li>A4 용지 기준 3열 단위로 구성</li>
<li>A4 용지 기준 9행 단위로 구성</li>
<li>27의 바코드 출력 후 강제 페이지 전환</li>
</ul>
<p>이것을 가능하게 하기 위해 <code>PDF::Reuse::Barcode::Code128()</code> 함수 호출시
<code>x</code> 좌표와 <code>y</code> 좌표를 세밀하게 계산 후 수동으로 지정해줍니다.
더불어 생성하는 바코드가 많으니 출력 후 재단하기 편하게 각각의 바코드 마다 음영을 약간 넣어보죠.</p>
<pre class="brush: perl;">
PDF::Reuse::Barcode::Code128(
x => 40 + 190 * $j,
y => 740 - 86 * ($i % 9),
value => $code,
size => 1.5,
background => '0.99 0.97 0.97',
);
</pre>
<p>완성입니다! :)</p>
<h2>글꼴 지정 패치</h2>
<p>반드시 필요한 부분은 아니지만 글꼴이 조금 아쉽군요.
시스템에 따라 다르지만 제 경우 생성된 PDF에서 기본으로 사용하는
글꼴이 <code>0</code>과 <code>O</code>를 구분하기에 부족합니다.
물론 구분이 가능하더라도 다른 글꼴을 사용하고 싶을 수도 있구요.
글꼴을 변경하려면 간단히 <code>prTTFont()</code> 함수를 이용합니다.</p>
<pre class="brush: perl;">
prFile('opencloset.pdf');
prTTFont( '/usr/share/fonts/truetype/nanum-coding/NanumGothic_Coding.ttf' );
chomp( my @codes = <DATA> );
</pre>
<p>어이쿠! 분명히 글꼴을 변경했는데 바코드에 변경한 글꼴이 적용되지 않는군요.
사실 이 부분은 <code>PDF::Reuse::Barcode</code> 모듈 제작자가 미처 고려하지 못한 부분입니다.
간단한 패치를 적용해 트루타입 폰트를 바코드용 문자열에 적용해보죠.</p>
<pre class="brush: perl;">
...
use PDF::Reuse;
use PDF::Reuse::Barcode;
{
package PDF::Reuse::Barcode;
use strict;
use warnings;
no warnings 'redefine';
our ($str, $xsize, $ysize, $height, $sPtn, @sizes, $length, $value, %default);
sub init {
%default = (
value => '0000000',
x => 0,
y => 0,
size => 1,
xsize => 1,
ysize => 1,
rotate => 0,
background => '1 1 1',
drawbackground => 1,
text => 'yes',
text_font => 'C',
prolong => 0,
hide_asterisk => 0,
mode => 'Type3'
);
$str = '';
$xsize = 1;
$ysize = 1;
$height = 37;
$sPtn = '';
@sizes = ();
$length = 0;
$value = '';
}
sub general1 {
$default{'xsize'} = 1 unless ( $default{'xsize'} != 0 );
$default{'ysize'} = 1 unless ( $default{'ysize'} != 0 );
$default{'size'} = 1 unless ( $default{'size'} != 0 );
$xsize = $default{'xsize'} * $default{'size'};
$ysize = $default{'ysize'} * $default{'size'};
$str = "q\n";
$str .= "$xsize 0 0 $ysize $default{'x'} $default{'y'} cm\n";
if ( $default{'rotate'} != 0 ) {
my $radian
= sprintf( "%.6f", $default{'rotate'} / 57.2957795 ); # approx.
my $Cos = sprintf( "%.6f", cos($radian) );
my $Sin = sprintf( "%.6f", sin($radian) );
my $negSin = $Sin * -1;
$str .= "$Cos $Sin $negSin $Cos 0 0 cm\n";
}
}
sub general2 {
$length = 20 + ( length($sPtn) * 0.9 );
my $height = 38;
my $step = 9;
my $prolong = 0;
if ( $default{'prolong'} > 1 ) {
$prolong = $default{'prolong'};
$height = 26 + ( $prolong * 12 );
}
if ( $default{'drawbackground'} ) {
$str .= "$default{'background'} rg\n";
$str .= "0 0 $length $height re\n";
$str .= 'f*' . "\n";
$str .= "0 0 0 rg\n";
}
prAdd($str);
@sizes = prFontSize(12);
if ( $default{'mode'} eq 'Type3' ) {
prBar( 10, $step, $sPtn );
}
else # graphic mode
{
$str = Bar( 10, $step, $sPtn );
}
$prolong--;
if ( $prolong > 0 ) {
$sPtn =~ s/G/1/go;
while ( $prolong > 0 ) {
if ( $prolong > 1 ) {
$prolong--;
$step += 12;
}
else {
$step += ( 12 * $prolong );
$prolong = 0;
}
if ( $default{'mode'} eq 'Type3' ) {
prBar( 10, $step, $sPtn );
}
else # graphic mode
{
$str .= Bar( 10, $step, $sPtn );
}
}
}
# print the graphic mode bars
if ( $default{'mode'} ne 'Type3' ) {
$str .= "B\n";
prAdd($str);
}
}
sub general3 {
$str = "Q\n";
prAdd($str);
prFontSize( $sizes[1] );
}
sub standardEnd {
general2();
if ( $default{'text'} ) {
my @vec = prFont($default{'text_font'});
prFontSize(10);
my $textLength = length($value) * 6;
my $start = ( $length - $textLength ) / 2;
prText( $start, 1.5, $value );
prFont( $vec[3] );
}
general3();
1;
}
sub Code128 {
eval 'require Barcode::Code128';
init();
my %param = @_;
for ( keys %param ) {
my $lc = lc($_);
if ( exists $default{$lc} ) {
$default{$lc} = $param{$_};
}
else {
print STDERR "Unknown parameter $_ , not used \n";
}
}
$value = $default{'value'};
general1();
my $oGDBar = Barcode::Code128->new();
if ( !$oGDBar ) {
die "The translation of $value to barcodes didn't succeed, aborts\n";
}
else {
$sPtn = $oGDBar->barcode($value);
$sPtn =~ tr/#/1/;
$sPtn =~ tr/ /0/;
}
standardEnd();
1;
}
}
prFile('opencloset.pdf');
prTTFont( '/usr/share/fonts/truetype/nanum-coding/NanumGothic_Coding.ttf' );
...
</pre>
<p><code>{ package PDF::Reuse::Barcode; ... }</code> 영역의 코드를 추가하고 <code>Code128()</code> 함수 호출 시
추가로 글꼴을 지정하면 <code>prTTFont</code>로 적재한 글꼴을 사용해서 바코드를 생성할 수 있습니다.</p>
<pre class="brush: perl;">
PDF::Reuse::Barcode::Code128(
x => 40 + 190 * $j,
y => 740 - 86 * ($i % 9),
value => $code,
size => 1.5,
background => '0.99 0.97 0.97',
text_font => 'NanumGothicCoding',
);
</pre>
<p>자! 이제 PDF 리더기로 한번 열어보죠.</p>
<p><img src="2013-12-15-1_r.png" alt="생성한 출력용 바코드" id="" />
<em>그림 1.</em> 생성한 출력용 바코드 (<a href="2013-12-15-1.png">원본</a>)</p>
<p>와우! 정말 끝났네요. ;-)</p>
<h2>정리하며</h2>
<p>다양한 장비에서 일관되게 보여주기 위해 HTML로 바코드를 표현하는 것 뿐만 아니라
바코드의 특성상 출력을 위해 출력에 적합하게 표현해야 할 때도 있습니다.
특히 출력을 대비한다면 대량의 바코드를 출력하거나 또는 각각의 항목을
고객 또는 사용자가 원하는 용도에 맞게 미려하게 꾸며야 하기도 합니다.
단순하게 <a href="https://metacpan.org/module/PDF::Reuse">PDF::Reuse 모듈</a>을 이용해서 PDF를 생성하고
<a href="https://metacpan.org/module/PDF::Reuse::Barcode">PDF::Reuse::Barcode 모듈</a>을 이용해서 쉽게 바코드를
출력한 것은 물론, 상황에 맞게 PDF 내에서 바코드를 적절히 배치해 대량의
바코드 출력을 대비하고 판독이 용이한 폰트를 사용하는 방법을 살펴보았습니다.
비단 PDF와 바코드 뿐만 아니라 어떤 경우든 펄은 여러분이 최소의 노력으로
최대의 효과를 낼 수 있도록 도와줄 것입니다.</p>
<p>Enjoy Your Perl! ;-)</p>
<p><img src="2013-12-15-2.png" alt="EOT" id="eot" style="margin: 0" /></p>
<p><em>EOT</em></p>
2013-12-15T00:00:00+09:00keedi초심자를 위한 Mojolicioushttp://advent.perl.kr/2013/2013-12-14.html<h2>저자</h2>
<p><a href="http://twitter.com/rumidier">@rumidier</a> - 서산 야생마, 가슴 속에 화난새 키우는 어른이</p>
<h2>시작하며</h2>
<p>펄에는 수 많은 웹프레임워크가 있습니다.
그 중 근래에 가장 많이 사용하는 프레임워크는 <a href="http://www.catalystframework.org/">Catalyst</a>와
<a href="http://perldancer.org/">Dancer</a>, <a href="http://mojolicio.us/">Mojolicious</a> 세 가지 입니다.
복잡한 엔터프라이즈 환경은 <em>Catalyst</em>를 많이 사용하고 비교적 규모가 작은
웹 사이트는 <em>Dancer</em>나 <em>Mojolicious</em>를 많이 사용한다고 합니다.
<em>Mojolicious</em>는 <a href="http://mojolicio.us/">공식 홈페이지</a>에 문서화가 잘 되있고 다루기 쉽다고 하지만
정말 아무 것도 모른채로 시작한다면 막히는 곳이 한 두 군데가 아닙니다.
문서를 보고도 어떻게 시작해야 될지 헤매실 분들에게 이 글을 바칩니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/DBD::mysql">DBD::mysql</a></li>
<li><a href="https://metacpan.org/module/DBI">DBI</a></li>
<li><a href="https://metacpan.org/module/Mojolicious">Mojolicious</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Mojolicious DBI DBD::mysql
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Mojolicious DBI DBD::mysql
</pre>
<h2>사용 방법</h2>
<p><code>Mojolicious</code> 모듈을 설치하면 같이 제공되는 <code>mojo</code> 명령은 다양한 기능을 제공합니다.
<code>help</code>를 인자로 주면 자세한 기능을 살펴볼 수 있습니다.</p>
<pre class="brush: bash;">
$ mojo help
usage: mojo COMMAND [OPTIONS]
Tip: CGI and PSGI environments can be automatically detected very often and
work without commands.
These commands are currently available:
daemon Start application with HTTP and WebSocket server.
cpanify Upload distribution to CPAN.
get Perform HTTP request.
inflate Inflate embedded files to real files.
eval Run code against application.
version Show versions of installed modules.
psgi Start application with PSGI.
prefork Start application with preforking HTTP and WebSocket server.
cgi Start application with CGI.
test Run unit tests.
routes Show available routes.
generate Generate files and directories from templates.
These options are available for all commands:
-h, --help Get more information on a specific command.
--home <path> Path to your applications home directory, defaults to
the value of MOJO_HOME or auto detection.
-m, --mode <name> Operating mode for your application, defaults to the
value of MOJO_MODE/PLACK_ENV or "development".
See 'mojo help COMMAND' for more information on a specific command.
</pre>
<p><em>Mojolicious</em>를 가장 간단히 사용할 수 있는 <code>Mojoliciois::Lite</code> 모듈을 이용한 웹 앱을
작성하기 위해 <code>mojo</code> 명령의 <code>generate</code>, <code>lite_app</code> 옵션을 이용해서 <code>myapp.pl</code>을 생성합니다.</p>
<pre class="brush: bash;">
$ mojo generate lite_app myapp.pl
[exist] /Users/mojo-test
[write] /Users/mojo-test/myapp.pl
[chmod] myapp.pl 744
</pre>
<p>앞의 명령으로 생성되는 <code>myapp.pl</code> 웹 앱을 실행하려면 <code>Mojolicious</code> 모듈과 함께
제공되는 <code>morbo</code> 명령을 사용해서 생성한 <code>myapp.pl</code> 파일을 인자로 줍니다.</p>
<pre class="brush: bash;">
$ morbo myapp.pl
Listening at "http://*:3000".
Server available at http://127.0.0.1:3000.
</pre>
<p>특별한 옵션을 지정하지 않을 경우 <code>3000</code> 포트를 사용합니다.
특정 포트를 지정하려면 <code>-l</code> 옵션을 사용합니다.</p>
<pre class="brush: bash;">
$ morbo myapp.pl -l http://*:5001
Listening at "http://*:5001".
Server available at http://127.0.0.1:5001.
</pre>
<p>실행 후 웹브라우저를 이용해 명령줄에 출력되는 주소로 접속하면
<code>myapp.pl</code> 웹 앱을 브라우저에서 확인할 수 있습니다.</p>
<p><img src="2013-12-14-1_r.png" alt="Mojolicious::Lite 실행화면" id="mojolicious::lite" />
<em>그림 1.</em> Mojolicious::Lite 실행화면 (<a href="2013-12-14-1.png">원본</a>)</p>
<p>이제 <em>Mojolicious</em>를 사용해 개발할 수 있는 환경이 마련되었습니다.
<code>GET</code>/<code>POST</code>를 이용한 <em>읽기</em>, <em>쓰기</em>, <em>수정</em>, <em>삭제</em> 기능을 지원하는
아주 간단한 게시판을 만들어보며 감을 잡아봅시다.</p>
<h2>뼈대 코드 작성</h2>
<p>우선 간단한 게시판을 만들기 위해 <code>myapp.pl</code> 파일을 열어 뼈대를 잡아보죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use v5.16;
use utf8;
use Mojolicious::Lite;
get '/' => sub { };
get '/list' => sub { };
get '/write' => sub { };
post '/write' => sub { };
get '/read/:id' => sub { };
get '/edit/:id' => sub { };
post '/edit' => sub { };
get '/delete/:id' => sub { };
app->start;
__DATA__
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<%= content %>
</body>
</html>
@@ write.html.ep
% layout 'default';
% title 'WRITE';
@@ list.html.ep
% layout 'default';
% title 'LIST';
@@ read.html.ep
% layout 'default';
% title 'READ';
@@ edit.html.ep
% layout 'default';
% title 'EDIT';
</pre>
<p>뼈대 코드는 크게 <code>__DATA__</code> 섹션 위와 아래로 구성됩니다.
<code>__DATA__</code> 섹션 위는 컨트롤러에 해당하며 아래는 템플릿에 해당합니다.
컨트롤러 즉 URL 라우팅에 해당하는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
get '/' => sub { };
get '/list' => sub { };
get '/write' => sub { };
post '/write' => sub { };
get '/read/:id' => sub { };
get '/edit/:id' => sub { };
post '/edit' => sub { };
get '/delete/:id' => sub { };
</pre>
<p>전체 페이지의 템플릿을 구성하는 코드는 <code>@@ layouts/default.html.ep</code> 영역입니다.</p>
<pre class="brush: perl;">
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<%= content %>
</body>
</html>
</pre>
<p>그 외의 템플릿은 모두 <code>% layout 'default'</code> 구문을 이용해 <code>layouts/default.html.ep</code>를
큰 틀로 사용하며 각각의 템플릿에서 표현하는 부분은 <code>layouts/default.html.ep</code> 템플릿에서
<code><%= content %></code> 영역에 해당하는 부분과 대치됩니다.</p>
<pre class="brush: perl;">
@@ XXXX.html.ep
% layout 'default'; # layouts/default.html.ep를 사용한다는 의미
% title 'WRITE'; # <%= title %> 영역과 대치됨
... 이 부분은 <%= content %> 영역과 대치됨
</pre>
<h2>데이터베이스 생성 및 연결</h2>
<p>간단한 게시판이라 하더라도 입력 받은 자료, 즉 글을 어딘가에 저장해야 합니다.
<a href="http://www.mysql.com/">MySQL</a>을 이용해 DB를 생성하고 <a href="https://metacpan.org/module/DBI">DBI 모듈</a>, <a href="https://metacpan.org/module/DBD::mysql">DBD::mysql 모듈</a>을
이용해 생성한 데이터베이스에 접속하겠습니다.</p>
<p>우선 <code>Book</code> 데이터베이스를 생성합니다.</p>
<pre class="brush: bash;">
$ mysql -u root -p 'create database Book;'
</pre>
<p><code>Book</code> 데이터베이스에 다음 스키마를 참고해서 <code>memo</code> 테이블을 생성합니다.</p>
<pre class="brush: sql;">
CREATE TABLE `memo` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(20) NOT NULL DEFAULT '',
`title` VARCHAR(70) NOT NULL,
`content` TEXT NOT NULL,
`wdate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
</pre>
<p><em>Mojolicious</em>에서 생성한 데이터베이스에 연결하기 위해 다음 코드를 추가합니다.
MySQL 접속 계정은 <code>memouser</code>, 비밀 번호는 <code>memopass</code>라고 가정합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use v5.16;
use utf8;
use Mojolicious::Lite;
use DBI;
my $DBH = DBI->connect(
'dbi:mysql:Book',
'memouser',
'memopass',
{
RaiseError => 1,
AutoCommit => 1,
mysql_enable_utf8 => 1,
},
);
get '/' => sub { };
...
</pre>
<h2>쓰기</h2>
<p>웹페이지에서 쓰기 기능을 사용하기 위해 필요한 것은 두 가지 입니다.</p>
<ul>
<li>글쓰기 양식을 확인하는 페이지</li>
<li>양식에서 입력 되는 정보를 저장하는 기능</li>
</ul>
<p>글을 쓰기 위한 페이지는 <code>GET /write</code> 요청을 통해 접근합니다.</p>
<pre class="brush: perl;">
get '/write' => sub {
my $self = shift;
$self->render('write');
};
</pre>
<p><img src="2013-12-14-2_r.png" alt="메모 쓰기화면" id="" />
<em>그림 2.</em> 메모 쓰기화면 (<a href="2013-12-14-2.png">원본</a>)</p>
<p>메모 쓰기 HTML 양식 입니다.
<em>이름</em>, <em>제목</em>, <em>내용</em> 세 가지를 입력합니다.</p>
<pre class="brush: perl;">
@@ write.html.ep
% layout 'default';
% title 'WRITE';
<form action="/write" method="post">
<table width=580 border=0 cellpadding=2 cellspacing=1 bgcolor=#777777>
<tr>
<td height=20 colspan=4 align=center bgcolor=#999999>
<font color=white><b>글쓰기</b></font>
</td>
</tr>
<tr>
<td bgcolor=white>
<table bgcolor=white>
<tr>
<td>이름</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>제목</td>
<td><input type="text" name="title"></td>
</tr>
<tr>
<td>내용</td>
<td colspan=4>
<textarea name="content" cols=80 rows=5></textarea>
</td>
</tr>
<tr>
<td colspan=10 align=center>
<input type="submit" value="저장">
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor=#999999>
<table width=100%>
<tr>
<td>
<a href='/list' style="text-decoration:none;"><font color=white>[목록보기]</font></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</form>
</pre>
<p>동일한 <code>/write</code> 주소라도 <code>GET</code> 요청이냐 또는 <code>POST</code> 요청이냐에 따라
<code>get '/write'</code>와 <code>post '/write'</code>로 구분이 되므로 주의하세요.
<code>POST</code>로 전달된 값은 <code>$self->param('name')</code>으로 값을 얻을 수 있습니다.</p>
<pre class="brush: perl;">
post '/write' => sub {
my $self = shift;
my $name = $self->param('name');
my $title = $self->param('title');
my $content = $self->param('content');
my $sth = $DBH->prepare(qq{
INSERT INTO `memo` (`name`,`title`,`content`) VALUES (?,?,?)
});
$sth->execute( $name, $title, $content );
$self->redirect_to( $self->url_for('/list') );
};
</pre>
<p><code>$self->param(...)</code>으로 얻은 값은 <code>DBI</code>를 통해 저장합니다.
사용법은 다음과 같습니다.</p>
<pre class="brush: perl;">
my $sth = $DBH->prepare(qq{
INSERT INTO `memo` (`name`,`title`,`content`) VALUES (?,?,?)
});
$sth->execute( $name, $title, $content );
</pre>
<p>문제가 없다면 데이터베이스에는 다음처럼 저장됩니다.</p>
<pre class="brush: plain;">
mysql> select * from memo;
+----+----------+--------------+-------------------------------------------------------+---------------------+
| id | name | title | content | wdate |
+----+----------+--------------+-------------------------------------------------------+---------------------+
| 1 | rumidier | book-test-01 | 웹페이지 제작을 위해 Mojolicious를 사용하고 있습니다. | 2013-11-01 20:00:00 |
+----+----------+--------------+-------------------------------------------------------+---------------------+
</pre>
<h2>읽기</h2>
<p>저장한 글 목록을 확인할 수 있는 페이지가 필요하겠죠.
<code>/</code>로 접속했을 때는 <code>/list</code>로 바로 이동하도록 변경합니다.</p>
<pre class="brush: perl;">
get '/' => sub {
my $self = shift;
$self->redirect_to( $self->url_for('/list') );
};
</pre>
<p><code>GET /list</code> 요청을 받을 수 있는 <code>/list</code>를 작성합니다.</p>
<pre class="brush: perl;">
get '/list' => sub {
my $self = shift;
my $sth = $DBH->prepare(qq{ SELECT id, name, title, content, wdate FROM memo });
$sth->execute();
my %articles;
while ( my @row = $sth->fetchrow_array ) {
my ( $id, $name, $title, $content, $date ) = @row;
my ($wdate) = split / /, $date;
$articles{$id} = {
name => $name,
title => $title,
content => $content,
wdate => $wdate,
};
}
$self->stash( articles => \%articles );
};
</pre>
<p><code>$self->stash()</code> 함수를 이용해 템플릿 쪽으로 <code>\%articles</code> 해시 레퍼런스를 전달합니다.
<code>\%articles</code> 해시 레퍼런스를 이용하는 템플릿 쪽 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
@@ list.html.ep
% layout 'default';
% title 'LIST';
<table width=580 border=0 cellpadding=2 cellspacing=1 bgcolor=#999999>
<tr height=20 colspan=4 align=center bgcolor=#CCCCCC >
<td color=white>No. </td>
<td>제목</td>
<td>글쓴이</td>
<td>date</td>
</tr>
% foreach my $id ( keys %$articles ) {
<tr bgcolor="white">
<td><%= $id %></td>
<td><a href="/read/<%= $id %>"><%= $articles->{$id}{title} %></a></td>
<td><%= $articles->{$id}{name} %></td>
<td><%= $articles->{$id}{wdate} %></td>
</tr>
% }
<tr>
<td colspan=4 bgcolor=#999999>
<table width=100%>
<tr>
<td width=2000 align=center height=20>
<a href='/write' style="text-decoration:none;"><font color=white>[글쓰기]</font></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</pre>
<p>컨트롤러로 부터 넘겨 받은 데이터를 출력해보면 데이터베이스의 <code>id</code>를 기준으로
출력이 되는데 이때 <code>id</code>는 정렬되지 않은 상태, 즉 무작위로 출력됩니다.</p>
<p><img src="2013-12-14-3_r.png" alt="해쉬 무작위 정렬" id="" />
<em>그림 3.</em> 해쉬 무작위 정렬 (<a href="2013-12-14-3.png">원본</a>)</p>
<p>정렬 연산자인 <code>sort</code>를 추가합니다.
<code>sort</code>만으로는 가장 처음에 입력된 <code>id=1</code>인 값이 맨위로 올라오므로
역순으로 출력하기위해 <code>reverse</code>도 같이 사용합니다.</p>
<pre class="brush: perl;">
% foreach my $id ( reverse sort keys %$articles ) {
</pre>
<p><img src="2013-12-14-4_r.png" alt="역순 정렬" id="" />
<em>그림 4.</em> 역순 정렬 (<a href="2013-12-14-4.png">원본</a>)</p>
<p>1번이 맨아래로 갔지만 10번 이상이 중간에 섞여 있습니다.
이것은 <code>sort</code> 함수가 문자열 기준 정렬을 하기 때문인데 이를 보완하기 위해 숫자 정렬을 해야겠죠.
우주선 연산자(<code><=></code>)를 사용해서 다시 정렬해보죠.</p>
<pre class="brush: perl;">
% foreach my $id ( reverse sort { $a <=> $b } keys %$articles ) {
</pre>
<p><img src="2013-12-14-5_r.png" alt="숫자 정렬" id="" />
<em>그림 5.</em> 숫자 정렬 (<a href="2013-12-14-5.png">원본</a>)</p>
<p>이제 저장한 글 목록이 화면에 출력됩니다.</p>
<p>각각의 글 내용을 확인하기 위한 페이지는 <code>/read/1</code>, <code>/read/2</code>, <code>/read/N</code> 등의 주소로 연결했습니다.
다음은 <code>/read/N</code>으로 접속했을 때 처리할 수 있는 컨트롤러 코드입니다.</p>
<pre class="brush: perl;">
get '/read/:id' => sub {
my $self = shift;
my $input_id = $self->param('id');
my $sth = $DBH->prepare(qq{ SELECT * FROM memo WHERE id=$input_id });
$sth->execute();
my %articles;
my ( $id, $name, $title, $content, $date ) = $sth->fetchrow_array;
my ($wdate) = split / /, $date;
$articles{$id} = {
name => $name,
title => $title,
content => $content,
wdate => $wdate,
};
$self->stash(
articles => \%articles,
id => $id,
);
$self->render('read');
};
</pre>
<p>다음은 <code>/read/N</code>으로 접속했을 때 처리할 수 있는 템플릿 코드입니다.</p>
<pre class="brush: perl;">
@@ read.html.ep
% layout 'default';
% title 'READ';
<table width=580 border=0 cellpadding=2 cellspacing=1 bgcolor=#777777>
<tr>
<td height=20 colspan=4 align=center bgcolor=#999999>
<font color=white><b><%= $articles->{$id}{title} %></b></font>
</td>
</tr>
<tr>
<td width=50 height=20 align=center bgcolor=#EEEEEE> 글쓴이 </td>
<td width=240 bgcolor=white> <%= $articles->{$id}{name} %> </td>
<td width=50 height=20 align=center bgcolor=#EEEEEE> 날짜 </td>
<td width=240 bgcolor=white> <%= $articles->{$id}{wdate} %> </td>
</tr>
<tr>
<td bgcolor=white colspan=4>
<font color=black>
<pre><%= $articles->{$id}{content} %></pre>
</font>
</td>
</tr>
<tr>
<td colspan=4 bgcolor=#999999>
<table width=100%>
<tr>
<td width=2000 align=left height=20>
<a href='/list' style="text-decoration:none;"><font color=white>[목록보기]</font></a>
<a href='/write' style="text-decoration:none;"><font color=white>[글쓰기]</font></a>
<a href='/edit/<%= $id %>' style="text-decoration:none;"><font color=white>[수정]</font></a>
<a href='/delete/<%= $id %>' style="text-decoration:none;"><font color=white>[삭제]</font></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</pre>
<p>이제 글 목록에서 각각의 글 제목을 클릭하면 글의 내용을 확인할 수 있습니다.</p>
<p><img src="2013-12-14-6_r.png" alt="글 내용 보기" id="" />
<em>그림 6.</em> 글 내용 보기 (<a href="2013-12-14-6.png">원본</a>)</p>
<h2>수정</h2>
<p>쓰고 읽은 후 내용을 수정하고 싶을 경우를 위해서 수정 기능이 필요하겠죠.
다음은 <code>/edit/N</code>으로 접속했을 때 처리할 수 있는 컨트롤러 코드입니다.</p>
<pre class="brush: perl;">
get '/edit/:id' => sub {
my $self = shift;
my $input_id = $self->param('id');
my $sth = $DBH->prepare(qq{ SELECT * FROM memo WHERE id=$input_id });
$sth->execute();
my %articles;
my ( $id, $name, $title, $content, $date ) = $sth->fetchrow_array;
my ($wdate) = split / /, $date;
$articles{$id} = {
name => $name,
title => $title,
content => $content,
wdate => $wdate,
};
$self->stash(
articles => \%articles,
id => $id,
);
$self->render('edit');
};
</pre>
<p>수정하는 페이지는 쓰기 페이지와 유사하지만 기존의 정보가 표시되야 한다는 차이점이 있습니다.
다음은 <code>/edit/N</code>으로 접속했을 때 처리할 수 있는 템플릿 코드입니다.</p>
<pre class="brush: perl;">
@@ edit.html.ep
% layout 'default';
% title 'EDIT';
<form action="/edit" method="post">
<input type="hidden" name="id" value="<%= $id %>">
<table width=580 border=0 cellpadding=2 cellspacing=1 bgcolor=#777777>
<tr>
<td height=20 colspan=4 align=center bgcolor=#999999>
<font color=white><b>수정</b></font>
</td>
</tr>
<tr>
<td bgcolor=white>
<table bgcolor=white>
<tr>
<td>이름</td>
<td><input type="text" name="name" value="<%= $articles->{$id}{name} %>"></td>
</tr>
<tr>
<td>제목</td>
<td><input type="text" name="title" value="<%= $articles->{$id}{title} %>"></td>
</tr>
<tr>
<td>내용</td>
<td colspan=4>
<textarea name="content" cols=80 rows=5><%= $articles->{$id}{content} %></textarea>
</td>
</tr>
<tr>
<td colspan=10 align=center>
<input type="submit" value="수정확인">
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor=#999999>
<table width=100%>
<tr>
<td>
<a href='/list' style="text-decoration:none;"><font color=white>[목록보기]</font></a>
<a href='/write' style="text-decoration:none;"><font color=white>[글쓰기]</font></a>
<a href='/read/<%= $id %>' style="text-decoration:none;"><font color=white>[취소]</font></a>
<a href='/delete/<%= $id %>' style="text-decoration:none;"><font color=white>[삭제]</font></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</form>
</pre>
<p>내용을 수정한 후 <code>POST</code>로 전달하므로 그에 맞는 컨트롤러가 필요합니다.</p>
<pre class="brush: perl;">
post '/edit' => sub {
my $self = shift;
my $id = $self->param('id');
my $name = $self->param('name');
my $title = $self->param('title');
my $content = $self->param('content');
my $sth = $DBH->prepare(qq{
UPDATE `memo` SET `name`=?,`title`=?,`content`=? WHERE `id`=$id
});
$sth->execute( $name, $title, $content );
$self->redirect_to( $self->url_for('/list') );
};
</pre>
<p>수정이 끝난 후 <code>/list</code>로 페이지를 이동시키는 부분을 유의해주세요.
페이지를 리다이렉트 시킬 때는 <code>$self->redirect_to()</code> 함수를 이용합니다.</p>
<p><img src="2013-12-14-7_r.png" alt="수정된 1번 내용" id="" />
<em>그림 7.</em> 수정된 1번 내용 (<a href="2013-12-14-7.png">원본</a>)</p>
<p>수정 기능도 이제 잘 동작하는군요.</p>
<h2>삭제</h2>
<p>삭제는 비교적 간단합니다.
다음은 <code>/delete/N</code>으로 접속했을 때 처리할 수 있는 컨트롤러 코드입니다.</p>
<pre class="brush: perl;">
get '/delete/:id' => sub {
my $self = shift;
my $id = $self->param('id');
my $sth = $DBH->prepare(qq{ DELETE FROM `memo` WHERE `id`=$id });
$sth->execute();
$self->redirect_to( $self->url_for('/list') );
};
</pre>
<p>삭제 후 <code>/list</code>로 이동하기 때문에 따로 템플릿 코드가 필요하지 않죠.</p>
<h2 id="helper">Helper 사용하기</h2>
<p>자세히 살펴보면 <code>/read/$id</code>와 <code>/edit/$id</code>에서 동일한 코드를 사용하고 있습니다.
코드가 중복되는 것은 좋지 않죠. 중복된 코드를 줄이기 위해 <code>helper</code>를 사용합니다.
다음과 같이 동일 코드를 <code>helper</code>로 작성합니다.</p>
<pre class="brush: perl;">
helper db_select => sub {
my ( $self, $input_id ) = @_;
my $sth = $DBH->prepare(qq{ SELECT * FROM memo WHERE id=$input_id});
$sth->execute();
my %articles;
my ( $db_id, $name, $title, $content, $date ) = $sth->fetchrow_array;
my ($wdate) = split / /, $date;
$articles{$db_id} = {
name => $name,
title => $title,
content => $content,
wdate => $wdate,
};
return \%articles;
};
</pre>
<p>방금 작성한 헬퍼 코드를 호출하려면 <code>$self->db_select()</code> 형식으로 사용합니다.</p>
<pre class="brush: perl;">
my $articles = $self->db_select($input_id);
my ($id) = keys %$articles;
$self->stash(
articles => $articles,
id => $id,
);
</pre>
<h2>전체 코드</h2>
<p>다음은 전체 코드와 SQL 스키마 코드입니다.</p>
<ul>
<li><a href="https://gist.github.com/rumidier/7940143">myapp.pl</a></li>
<li><a href="https://gist.github.com/rumidier/7940167">memo.sql</a></li>
</ul>
<p>잘못되거나 개선할 부분이 있다면 갱신이 될 예정이니 이 점 참고해주세요. :-)</p>
<h2>정리하며</h2>
<p>웹에 대한 많은 이해와 실전 경험이 있다면 잘 정리된 <code>Mojolicious</code> 문서만 보더라도 금방 이해하고 응용할 수 있습니다.
다만 웹이 익숙치 않다면 문서만 보았을때 힘든 부분이 꽤 많습니다(제가 힘듭니다).
비록 CSS나 자바스크립트도 사용하지 않고 구식의 HTML 코드를 사용하고는 있지만
간단하고 짧기 때문에 이해하기에는 더 용이하지 않을까 바래봅니다.
이 예제와 글이 저처럼 문서를 보다가 헤매시는 다른 분들께도 작은 도움이 되었으면 합니다.</p>
2013-12-14T00:00:00+09:00rumidier간단한 파일 공유http://advent.perl.kr/2013/2013-12-13.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>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Plack">CPAN의 Plack 모듈</a></li>
<li><a href="https://metacpan.org/module/Plack::App::Directory::Apaxy">CPAN의 Plack::App::Directory::Apaxy 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Plack Plack::App::Directory::Apaxy
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Plack Plack::App::Directory::Apaxy
</pre>
<h2 id="plack::app::directory">Plack::App::Directory</h2>
<p><a href="http://plackperl.org/">Plack</a>은 펄의 <a href="https://metacpan.org/module/PSGI">PSGI</a> 구현체 중 하나로 <a href="http://rack.github.io/">루비의 Rack</a>이나
<a href="http://wsgi.readthedocs.org/en/latest/">파이썬의 WSGI</a>에 대응하는 웹응용과 서버 사이의 인터페이스입니다.
<a href="https://metacpan.org/module/Plack">Plack 모듈</a>을 설치하면 <a href="https://metacpan.org/module/Plack::App::Directory">Plack::App::Directory 모듈</a>이 같이 설치되는데
이 모듈을 이용하면 손쉽게 특정 디렉터리를 공유할 수 있습니다.
더 정확히 말하면 아파치의 디렉터리 인덱스 페이지와 유사한 페이지를 제공합니다.</p>
<p>실행 방법은 무척 간단합니다.</p>
<pre class="brush: bash;">
$ cd path_to/what/you/want
$ plackup -MPlack::App::Directory -e 'Plack::App::Directory->new->to_app'
HTTP::Server::PSGI: Accepting connections at http://0:5000/
...
</pre>
<p>이제 브라우저를 이용해 <code>http://localhost:5000</code>으로 접속하면 현재 명령을 실행한
<code>path_to/what/you/want</code> 디렉터리의 모든 파일을 열람 및 다운로드할 수 있습니다.</p>
<p><img src="2013-12-13-1_r.png" alt="Plack::App::Directory로 공유한 디렉터리 목록" id="plack::app::directory" />
<em>그림 1.</em> <code>Plack::App::Directory</code>로 공유한 디렉터리 목록 (<a href="2013-12-13-1.png">원본</a>)</p>
<h2>크리스마스 선물 #1: 미려한 디자인</h2>
<p>사실 여기까지로도 충분합니다만 역시 좀 투박한 인덱스 페이지가 마음에 좀 걸립니다.
물론 미려해지기 위해선 css와 그림 파일등을 HTTP로 제공해야 하니 추가적인
HTTP 요청으로 인한 약간의 리소스 저하는 감안해야겠죠? :)
<a href="https://metacpan.org/module/Plack::App::Directory::Apaxy">Plack::App::Directory::Apaxy 모듈</a>은
<code>Plack::App::Directory</code> 모듈과 거의 유사하지만 <a href="http://adamwhitcroft.com/apaxy/">Apaxy</a> 테마를 입혀
조금 더 미려한 디자인을 제공합니다.</p>
<p>실행 방법은 거의 동일합니다.</p>
<pre class="brush: bash;">
$ cd path_to/what/you/want
$ plackup -MPlack::App::Directory::Apaxy -e 'Plack::App::Directory::Apaxy->new->to_app'
HTTP::Server::PSGI: Accepting connections at http://0:5000/
...
</pre>
<p>달라진 점이 느껴지시나요?</p>
<p><img src="2013-12-13-2_r.png" alt="Plack::App::Directory::Apaxy로 공유한 디렉터리 목록" id="plack::app::directory::apaxy" />
<em>그림 2.</em> <code>Plack::App::Directory::Apaxy</code>로 공유한 디렉터리 목록 (<a href="2013-12-13-2.png">원본</a>)</p>
<p>훨씬 낫군요! ;-)</p>
<h2>크리스마스 선물 #2: 처리량</h2>
<p>기본적으로 디렉터리 인덱스 페이지를 띄울 때 하나의 프로세스가 전담하는데
용량이 큰 파일을 상대방이 다운로드 받는다면 더이상의 요청을 처리할 수 없습니다.
이 경우 <em>Plack</em>의 백엔드 서버를 지정해서 HTTP 요청을 처리할 프로세스 수를 늘려주면
아주 간단히 처리량을 늘릴 수 있습니다.</p>
<p>다음은 <a href="https://metacpan.org/module/Starlet">CPAN의 Starlet 모듈</a>을 백엔드로 지정해서 다섯 개의 프로세스를
띄워 최대 다섯 개의 요청을 병렬로 처리할 수 있도록 실행하는 명령입니다.</p>
<pre class="brush: bash;">
$ plackup -MPlack::App::Directory::Apaxy \
-s Starlet --max-workers=5 \
-e 'Plack::App::Directory::Apaxy->new->to_app'
Plack::Handler::Starlet: Accepting connections at http://0:5000/
...
</pre>
<p>물론 <a href="https://metacpan.org/module/Starlet">Starlet 모듈</a>을 설치하는 것을 잊으면 안되겠죠? :)</p>
<h2>크리스마스 선물 #3: 간단한 실행</h2>
<p>실행 자체는 간단하지만 키보드로 입력해야 할 내용이 좀 많죠?
Bash 쉘을 기준으로 <code>alias</code>를 걸면 조금 더 간편히 실행할 수 있습니다.
다음 내용을 <code>.bashrc</code>나 그에 준하는 파일에 다음 내용을 추가합니다.</p>
<pre class="brush: bash;">
alias apaxy="plackup -MPlack::App::Directory::Apaxy -s Starlet --max-workers=5 -e 'Plack::App::Directory::Apaxy->new->to_app'"
</pre>
<p>이제 <code>apaxy</code> 명령으로 간단히 실행 가능합니다.</p>
<pre class="brush: bash;">
$ cd path_to/what/you/want
$ apaxy
Plack::Handler::Starlet: Accepting connections at http://0:5000/
...
</pre>
<p>물론 <em>Plack</em>이 기본으로 제공하는 다른 기능도 추가적으로 사용하면서 모듈이 제공하는
다양한 옵션을 처리하고 싶다면 <code>alias</code> 대신 간단한 래퍼 스크립트를 만들어서 실행하는 방법도 있겠죠.</p>
<h2>정리하며</h2>
<p>파일을 공유할 수 있는 방법은 무척 많습니다.
FTP 서버, 삼바 서버, 이메일, 클라우드 스토리지 등 너무 많지만
작정하고 파일을 공유하는 것이 아닌 이상 대부분의 경우 아주 찰나의 시간 동안
공유해야 하는 경우에 이런 서버를 띄우고 설정하는 것은 번거롭기 그지 없습니다.
<a href="http://plackperl.org/">Plack</a>과 <a href="https://metacpan.org/module/Plack::App::Directory::Apaxy">Plack::App::Directory::Apaxy 모듈</a>을
이용하면 명령줄에서 <code>apaxy</code> 명령 입력 하나로 현재 디렉터리를 브라우저를 통해 공유할 수 있습니다.
단, 네트워크에 따라 외부 네트워크에까지 공유를 지원해야 한다면
공인 IP가 필요하다던가 또는 특정 포트 포워딩 정도의 설정은 필요하겠죠.</p>
<p>Enjoy Your Perl! ;-)</p>
<p><img src="2013-12-13-3.png" alt="EOT" id="eot" style="margin: 0" /></p>
<p><em>EOT</em></p>
2013-12-13T00:00:00+09:00keedi펄에서 외부명령어 실행 시키기http://advent.perl.kr/2013/2013-12-12.html<h2>저자</h2>
<p>John_Kang - SE, Seoul.pm의 철권 2번 타자</p>
<h2>시작하며</h2>
<p>펄에서 외부 명령어를 실행시키는 방법은 여러 가지가 있습니다.
<a href="http://perldoc.perl.org/functions/system.html">system 내장 함수</a>, <a href="http://perldoc.perl.org/perlop.html#%60STRING%60"><code>...</code></a>처럼 역따옴표,
<a href="http://perldoc.perl.org/functions/open.html">open 내장 함수</a>로 파이프(pipe)를 생성하는 방법 등이 있습니다.
이러한 방법의 동작과 차이점을 살펴보고 <a href="https://metacpan.org/pod/Capture::Tiny">Capture::Tiny 모듈</a>을
이용해 외부 명령어를 손쉽게 실행시키는 방법에 대해 알아봅니다.</p>
<p>외부 명령어를 펄을 통해 실행시킬 때는 표준 출력/오류의 방향과 외부 명령어가
반환하는 종료값을 인지해야 할 경우가 많습니다.
간단하게 기본적으로 제공하는 방법을 이용하지 않고 복잡하게 모듈까지 설치해서
외부 명령어를 실행하는지에 대한 의문은 각각의 차이점을 먼저 비교해보고 설명하겠습니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/pod/Capture::Tiny">CPAN의 Capture::Tiny 모듈</a></li>
</ul>
<p>직접 <a href="https://metacpan.org">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Capture::Tiny
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Capture::Tiny
</pre>
<h2 id="system">system 내장 함수</h2>
<p><code>system</code> 함수는 펄의 내장 함수로써 외부 명령어를 실행시킬 수 있습니다.
사용방법은 다음과 같습니다.</p>
<pre class="brush: perl;">
#
# system LIST
# system PROGRAM LIST
#
my $exit_status = system('ls');
#
# return : $exit_status, $?
# stdout : STDOUT
# stderr : STDERR
#
</pre>
<p><code>system</code> 함수의 반환값은 외부 명령어가 반환한 종료값입니다.
따라서 <code>$exit_status</code>는 <code>ls</code> 명령어의 종료값을 담습니다.
대부분의 운영제체에서 외부 명령어의 정상적인 종료 코드는 <code>0</code>이기 때문에 <code>system</code> 함수에서
실행한 외부 명령이 성공적으로 종료되면 <code>system</code>의 반환값은 <code>0</code>이 됩니다.</p>
<p><code>system</code> 함수는 <code>fork</code>를 이용해 자식 프로세스를 생성하고 자식 프로세스에서 외부 명령어를 실행합니다.</p>
<pre class="brush: plain;">
init --> perl --> ls
</pre>
<p>이 때 사용자가 임의로 표준 출력과 표준 오류의 파일 핸들을 수정하지 않았다면,
<code>ls</code> 명령어의 표준 출력은 <em>펄의 표준 출력(1)</em>으로, 표준 오류는 <em>펄의 표준 오류(2)</em>를 상속 받습니다.
<a href="http://perldoc.perl.org/functions/fileno.html">fileno 내장 함수</a>를 이용하면 해당 파일 핸들의 파일 디스크립터 값을 알 수 있습니다.</p>
<pre class="brush: perl;">
print fileno STDOUT ## 1
</pre>
<h2>역따옴표(`)</h2>
<p>역따옴표으로 외부 명령어를 실행하는 방법은 쉘에서의 그것과 동일합니다!
펄에는 크게 스칼라 문맥(scalar context)과 목록 문맥(list context)
두 가지가 존재하며 영어의 단수와 복수 개념과 비슷합니다.
역따옴표의 결과는 펄의 두 문맥에 따라 달라집니다.</p>
<p>다음은 스칼라 문맥에서의 역따옴표를 사용한 경우입니다.</p>
<pre class="brush: perl;">
my $output_string = `ls`;
- return : $?
- stdout : $out_string
- stderr : STDERR
</pre>
<p>외부 명령어의 종료값은 <code>$?</code> 변수에 저장되며 역따옴표는 외부 명령어가 표준 출력에 출력한 모든 내용을
반환하기 때문에 앞의 예제의 경우 디렉토리와 파일의 목록이 <code>$output_string</code> 변수에 저장됩니다.
표준 오류는 역따옴표를 실행하기 전의 펄의 표준 오류와 동일하기 때문에 오류를 확인하고 싶다면
외부 명령어를 실행할 때 표준 오류의 방향을 변경(재지향, redirect)해야 합니다.</p>
<pre class="brush: perl;">
my $output_string = `ls 2 > &1`;
</pre>
<p>다음은 목록 문맥에서의 역따옴표를 사용한 경우입니다.</p>
<pre class="brush: perl;">
my @output_string = `ls`;
- return : $?
- stdout : @out_string line terminated by $/;
- stderr : STDERR
</pre>
<p>아니 똑같아 보이는데 왜 목록 문맥이냐고요?
<code>=</code> 연산자 왼쪽의 값이 <code>$output_string</code>이 아닌 <code>@output_string</code>이기 때문에 펄은 이 구문을 목록 문맥으로 처리합니다.
목록 문맥에서 역따옴표는 외부 명령어의 실행 결과를 <code>$/</code> 변수의 값(기본 값은 해당 운영체제의 줄바꿈 문자)을 구분자로
나누어 각각의 줄 단위로 실행 결과를 반환합니다.
따라서 <code>@output_string</code>은 한 줄 단위로 구분된 문자열 목록을 가집니다.</p>
<p><code>qx()</code>는 역따옴표의 또 다른 표현으로 둘은 완전히 동일합니다.</p>
<pre class="brush: perl;">
my $output_string = qx(ls);
</pre>
<p>실행할 구문에 쉘이 해석해야 할 메타 문자가 있다면 다음처럼 작은 따옴표를 괄호 대신 사용하세요.</p>
<pre class="brush: perl;">
my $output_string = qx'ps $$';
</pre>
<h2 id="open"><code>open</code> 함수로 파이프 열기</h2>
<p>전통적으로 파이프를 이용하면 외부 명령어와 데이터를 주고받을 수 있습니다.
<code>open</code> 내장 함수로 파이프를 열 수 있는데 이때 두 번째 인자에 따라
파이프 생성 및 모드를 지정할 수 있습니다.
두번째 인자의 앞 부분에 <code>|</code> 기호를 사용하면 쓰기 모드로 파이프를 생성하며
뒷 부분에 <code>|</code> 기호가 오면 읽기 모드로 파이프를 생성합니다.</p>
<p>다음은 현재 디렉터리에서 30일이 지난 로그 파일을 삭제하는 간단한 프로그램입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
open( my $pipe, 'ls |' ) or die "Can't open a pipe : $!\n";
while (<$pipe>) {
chomp; ## 파일명뒤의 개행(\n) 삭제
next unless -f; ## 파일만 추출
next unless /\.log$/i; ## *.log 파일만 추출
unlink if -M > 30; ## -M 으로 파일변경시간 확인(in day)후 삭제
}
</pre>
<p><code>open</code> 함수를 실행하는 중에 오류가 발생할 경우 <code>$!</code> 변수를 확인해 어떤 오류가 발생했는지 확인할 수 있습니다.</p>
<p>다음은 <code>/bin/mail</code> 명령을 이용해 메일을 보내는 예제입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use autodie;
open( my $body, '| /bin/mail -s Subject mail@domail.com' );
print {$body} 'Hi There, I hope you are doing well :)';
</pre>
<p>예제는 <code>$body</code> 파이프를 통해 입력 결과를 <code>/bin/mail</code>에게 전달합니다.
<a href="https://metacpan.org/favorite/autodie">autodie 프라그마</a>를 통해 <code>open</code> 함수의 오류 제어를 자동화하면 편리합니다.</p>
<h2 id="capture::tiny">Capture::Tiny</h2>
<p>사실 위의 방법만으로도 외부 명령어를 실행 시키기에 충분하며 별 무리는 없습니다.
하지만 어떤 외부 명령어는 실행 중 표준 출력과 표준 오류가 동시에 발생하며
이것을 각각 별도로 처리해야 할 경우도 있죠.
지금까지 설명한 방법으로 표준 출력과 표준 오류 및 반환값까지 각각 처리하려면
꽤 세부적인 이해와 많은 양의 코드가 필요(불가능하다는 뜻이 아님)합니다.</p>
<p>하지만 이런 류의 일은 꽤나 전형적이죠.
이미 이런 상황을 대비한 모듈이 CPAN에는 있지 않을까요? :-)</p>
<p>지금부터 살펴 볼 모듈은 <a href="https://metacpan.org/pod/Capture::Tiny">CPAN의 Capture::Tiny 모듈</a>입니다.
<a href="http://advent.perl.kr/2013/2013-12-03.html">2013년 펄 크리스마스 달력의 셋째 날 기사</a>를 참고해보면
<code>Capture::Tiny</code>는 <a href="https://metacpan.org/favorite/leaderboard">43위에 랭크</a> 되어있습니다.
수없이 많은 모듈 중 100위권 안의 모듈은 어느 정도 안심하고 사용할 수 있겠죠?</p>
<p><code>Capture::Tiny</code> 모듈은 다음과 같은 장점을 가집니다.</p>
<ul>
<li>이식성이 높음</li>
<li>펄과 XS 코드, 외부 명령어에서 반환하는 표준 출력과 표준 오류를 갈무리함</li>
<li><code>tee</code> 함수를 사용해 표준 출력, 표준 오류를 변수에 담는 것은 물론 동시에 원래의 표준 출력과 표준 오류로 출력함</li>
</ul>
<p>다음은 <code>Capture::Tiny</code> 모듈을 사용한 간단한 예제입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
use autodie;
use Capture::Tiny ':all';
my @cmd = qw( find /proc -type f );
my ( $stdout, $stderr, $exit ) = capture {
system @cmd;
};
</pre>
<p><code>capture</code>함수가 <code>system</code> 함수의 표준 출력, 표준 오류 그리고 외부 명령어의 종료값을 반환합니다.</p>
<p>심지어 <code>capture</code> 함수에 <code>stdout</code>과 <code>stderr</code> 인자를 넘겨주면 표준 출력과 표준 오류를 구분해서 사용할 수 있습니다.</p>
<pre class="brush: perl;">
my $out_fh = IO::File->new("out.log", "w+");
my $err_fh = IO::File->new("err.log", "w+");
my @cmd = qw( find /proc -type f );
capture { system @cmd } stdout => $out_fh, stderr => $err_fh;
</pre>
<p>어떤 명령어는 실행 도중에 출력 결과를 변수에도 저장하고 기존의 표준 출력과 표준 오류로
계속 출력시켜 실시간으로 확인하고 싶을 수도 있습니다.</p>
<pre class="brush: perl;">
my @cmd = qw( find /proc -type f );
my ( $stdout, $stderr, @result ) = tee {
system @cmd;
};
</pre>
<p><code>tee</code> 함수는 각각의 표준 출력과 오류를 <code>$stdout</code>과 <code>$stderr</code>에
저장함과 동시에 기존의 출력 방향으로도 그대로 출력해줍니다.</p>
<h2>그 밖의 주의 사항</h2>
<h3>보안</h3>
<p><code>system</code> 함수에 인자를 전달할 때는 명령어와 각각의 인자를 배열에 담아 목록 형태로
제공해야 추가적인 쉘의 실행을 막을 수 있기 때문에 보안상 유리합니다.
다음은 흔히 볼 수 있는 <code>system</code> 함수를 사용하는 평범한 예제입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use strict;
use warnings;
my $cmd = 'find / -name' . q{ };
print "Please input what file do you want to find : ";
chomp( my $input = <> );
my $exit_status = system( $cmd . $input );
</pre>
<p>사용자 입력에 <code>;rm -rf</code> 문자열이 있다면 어떻게 될까요?
<code>find / -name ;rm -rf</code> 명령이 어떤 상황을 만들어 낼까요?
목록 형태로 <code>system</code>에게 인자를 전달하면 각각의 인자를 단순 문자열로 간주합니다.
이때는 각각의 인자 중에 <code>;rm -rf</code>가 있다면 <code>;rm -rf</code> 그 자체를 문자열로만 인식합니다.</p>
<h3 id="windows">Windows 환경에서의 반환값</h3>
<p>리눅스, 유닉스 계열에 익숙한 관리자라면 윈도우에서 외부 명령어를 실행한 후
그 반환값을 확인해보고는 당황할 수 도 있습니다.
유닉스에서는 1 바이트인데 윈도우에서는 반환값의 크기가 2Byte이기 때문입니다.</p>
<p><code>hhhhhhhhllllllll</code>의 하위 8비트는 프로그램을 종료한 시그널이며 상위 8비트가 실제 프로그램의 종료 값입니다.
실제 종료 값을 얻기 위해서는 8비트 만큼 우측으로 쉬프팅하면 그 값을 얻을 수 있습니다.</p>
<pre class="brush: perl;">
my @cmd = 'dir asdf' # asdf is not existing
my $ret = system @cmd; # $ret = 512
$ret >>= 8; # $ret = $ret >> 8;
print "$! : [$ret]"; # No such file or directory : [2]
</pre>
<h3 id="sunossolarishp-uxsystem">SunOS, Solaris, HP-UX과 system()함수</h3>
<p>외부 명령어를 실행시킬 때는 내부적으로 외부 명령어를 위한 자식프로세스를 생성(fork)합니다.
<code>fork</code>로 생성된 자식 프로세스는 부모의 파일 디스크립터는 물론 아직 소모되지 출력 버퍼도 물려받습니다.
안전하게 <code>system</code> 함수를 사용하기 위해서는 출력 버퍼를 비워(flush)주고 사용해야 합니다.
리눅스와 윈도우 계열의 <code>system</code> 함수는 이를 알아서 처리하기 때문에 신경쓰지 않아도 되지만
SunOS, Solaris, HP-UX 등의 운영체제에서는 이를 고려해야 합니다.</p>
<p>다행히 처리하는 방법이 어렵지는 않습니다. :)</p>
<pre class="brush: perl;">
local $| = 1; ## autoflushing
.
.
coding...
.
my $ret = system ('command');
.
</pre>
<h2>정리하며</h2>
<p>전반적으로 펄을 이용해 외부 명령어를 실행하는 방법을 알아보았습니다.
더불어 <a href="https://metacpan.org/pod/Capture::Tiny">Capture::Tiny 모듈</a>을 이용해 표준 출력과 표준 오류를 자유롭게 다루어 보았습니다.
아마 이런 모듈이 없었다면 이런 기능을 복잡하게 직접 구현하기 보다는 표준 출력으로 통합하거나 또는
표준 출력만으로 문제를 해결하는 등의 선에서 적절히 타협을 보았을것 같습니다.
다시 한번 <a href="https://metacpan.org">CPAN</a>이라는 저장소에 놀라며 모듈 개발자에게 감사를 표합니다 :)</p>
<p><em>EOT</em></p>
2013-12-12T00:00:00+09:00John Kang유니코드를 활용한 정규 표현식http://advent.perl.kr/2013/2013-12-11.html<h2>저자</h2>
<p><a href="http://twitter.com/newbcode">@newbcode</a> - 사랑스러운 딸 바보 도치파파.
<a href="http://www.yes24.com/24/goods/11040637">리눅스의 모든것</a> 공동저자.</p>
<h2>시작하며</h2>
<p><a href="http://en.wikipedia.org/wiki/Unicode">유니코드(Unicode)</a>는 전 세계의 모든 문자를 컴퓨터에서 일관되게 표현하고 다룰 수 있도록 설계된 문자 집합입니다.
<code>펄</code>이라는 한국어 문자는 10진수 <code>54148</code>에 일대일 대응됩니다.
<em>코드포인트(codepoint)</em>라고 하는 이 숫자는 보통 앞에 <code>U+</code>를 덧붙인 16진수 형태로 표기합니다.
<code>54148</code>을 16진수로 표현하면 <code>D384</code>이며 유니코드 표현으로는 <code>U+D384</code>라고 씁니다.</p>
<p>정규 표현식을 사용하다보면 <em>"한글과 영어를 분리하고 숫자와 문자등을 쉽게 파싱을 하는 방법은 없을까?"</em>하는 고민을 자주하게 됩니다.
펄의 내부 유니코드 속성에 대해 조금 깊게 들여다 보고 유니코드의 속성을 활용해
조금 더 정확하고 효율적으로 정규 표현식을 활용하는 방법에 대해 알아봅니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="http://metacpan.org/module/Encode">CPAN의 Encode 모듈</a></li>
<li><a href="http://metacpan.org/module/HTTP::Tiny">CPAN의 HTTP::Tiny 모듈</a></li>
</ul>
<p><code>Encode</code> 모듈은 코어 모듈이므로 별도로 설치할 필요는 없습니다.
<code>HTTP::Tiny</code>의 경우 펄 5.14 이후 버전(정확히는 v5.13.9)의 경우
코어 모듈로 지정되었으므로 역시 별도로 설치할 필요는 없습니다.
하지만 <code>HTTP::Tiny</code>는 지속적으로 버전이 갱신되고 있으므로 가능하면 최신 버전으로 설치하도록 합니다.</p>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan HTTP::Tiny
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan HTTP::Tiny
</pre>
<h2 id="length">유니코드와 <code>length</code> 내장 함수</h2>
<p>펄에서의 문자열은 <em>바이트 문자열</em>과 <em>유니코드 문자열</em> 두 가지 유형이 있습니다.
<em>유니코드 문자열</em>일 경우 <em>바이트 문자열</em>과 비교했을 때 바이트당 글자 길이가 다릅니다.
다음은 <em>바이트 문자열</em>을 사용한 예제입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: unicode-1.pl
#
use strict;
use warnings;
my $x = '☺';
printf "%s(%d)\n", $x, length($x);
</pre>
<p>실행하면 <code>length</code> 내장 함수는 유니코드가 아닌 경우 해당 문자열의 바이트 크기를 반환합니다.</p>
<pre class="brush: bash;">
$ perl unicode-1.pl
☺(3)
</pre>
<p>다음은 <em>유니코드 문자열</em>을 사용한 예제입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: unicode-2.pl
#
use utf8;
use strict;
use warnings;
use Encode qw( encode );
my $x = '☺';
printf "%s(%d)\n", encode( 'utf-8', $x ), length($x);
</pre>
<p>실행하면 <code>length</code> 내장 함수는 유니코드일 경우 해당 문자열의 길이, 즉 글자 수를 반환합니다.</p>
<pre class="brush: bash;">
$ perl unicode-2.pl
☺(1)
</pre>
<p>유니코드 문자열을 사용하기 위해 <code>use utf8;</code> 선언해서 <code>utf8</code> 프라그마를 켜고 있으며
<code>Wide character ...</code> 경고를 방지하기 위해 출력 시점에 <code>Encode</code> 모듈을 이용해
명확하게 출력할 터미널의 문자셋에 맞게 인코딩(예제에서는 리눅스기 때문에 <em>UTF-8</em>로 인코딩)합니다.
<code>Wide character ...</code> 경고는 <a href="http://advent.perl.kr/2013/2013-12-02.html">2013년 펄 크리스마스 달력의 둘째 날 기사</a>를 참고하세요.</p>
<h2 id="w">유니코드와 <code>\w</code> 정규 표현식</h2>
<p>펄에서는 특정 유니코드 문자에 일치시키기 위한 용도의 <code>\u</code>라는 메타 문자가 있습니다.
보통 4자리 16진수로 표현하므로 <code>\uD384</code>는 유니코드 문자 <code>펄</code>에 일치시킬 수 있습니다.
이 때 <code>\uD384</code>는 <code>U+D384</code>라는 유니코드 문자를 일치시키라는 의미만 가지며
실제로 비교할 바이트에 대한 내용은 전혀 지정하지 않는다는 점이 중요합니다.
실제 비교할 바이트는 프로그램 내부에서 유니코드 코드 포인트를 표기하기 위해 사용하는 인코딩 방법에 따라 달라집니다.
원래는 유니코드는 코드 포인트와 문자를 1:1로 대응시키기 위해 만든 것이지만 하나의 문자를 다른 코드로 표현할 수도 있습니다.
예를 들어 <code>U+00E0</code>인 <code>à</code>는 <code>a</code>를 나타내는 <code>U+0061</code>과 <code>̀\</code>를 나타내는 <code>U+0300</code>으로 구성됩니다.
이렇게 여러 방법으로 인코딩될 수 있기 때문에 실제 정규 표현식에의 유니코드 일치 결과는 예상과 다를 수 있습니다.</p>
<p>다음은 입력한 문자에 대해 단어를 일치시키는 간단한 프로그램입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: unicode-3.pl
#
use v5.14;
use strict;
use warnings;
chomp( my $input =(<DATA>) );
if ( $input =~ /\w+/ ) {
say "matched";
}
else {
say "not matched";
}
__DATA__
ááá
</pre>
<p>실행 결과는 다음과 같습니다.</p>
<pre class="brush: bash;">
$ ./unicode-3.pl
not matched
</pre>
<p>보통 <code>\w+</code>를 이용해서 단어를 일치시키는데 <code>ááá</code>는 단어로 인식을 하지 못합니다.
<code>\w</code>는 <code>[A-Za-z0-9_]</code>로 63개의 문자를 의미하기 때문에 <code>á</code>를 일치시키지 못한 것입니다.
<code>length</code> 때와 마찬가지로 <code>utf8</code> 프라그마를 켜주면 <code>\w</code>가 알파벳 뿐만 아니라
유니코드의 글자(letter) 영역까지 일치를 시켜줍니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: unicode-4.pl
#
use v5.14;
use utf8;
use strict;
use warnings;
chomp( my $input =(<DATA>) );
if ( $input =~ /\w+/ ) {
say "matched";
}
else {
say "not matched";
}
__DATA__
ááá
</pre>
<p>이제는 원하는대로 동작합니다. :)</p>
<pre class="brush: bash;">
$ perl unicode-3.pl
matched
</pre>
<h2>유니코드 속성</h2>
<p>유니코드의 겉만 보면 문자와 코드 사이의 대응관계(코드포인트)에 불과하지만 이 안에는 사실 더 많은 내용이 들어있습니다.
예를 들어 <em>"이 문자는 소문자이다"</em>라거나 <em>"이문자는 오른쪽에서 왼쪽으로 써야 한다"</em>,
<em>"이 문자는 다른 문자와 함께 쓰기 위해 만들어진 기호이다"</em>와 같은 각 문자의 속성도 정의되어 있습니다.
펄은 이러한 유니코드의 속성을 활용 할 수 있게 설계되어 있으며 <code>\p</code>나 <code>\P</code>를 사용해서 표현할 수 있습니다.</p>
<p>기본적인 속성은 다음과 같습니다.</p>
<ul>
<li><code>\p{속성}</code>: 그 속성을 가지는 문자에 일치</li>
<li><code>\P{속성}</code>: 그 속성을 가지지 않는 문자에 일치</li>
<li><code>\p{L}</code> 또는 <code>\p{Letter}</code>: 글자(letter)로 간주되는 모든 문자</li>
<li><code>\p{M}</code> 또는 <code>\p{Mark}</code>: 그 문자만 쓸수는 없고 다른 기본 문자와 함께 써야만 하는 문자 (예: accents, umlauts, enclosing boxes 등)</li>
<li><code>\p{Z}</code> 또는 <code>\p{Separator}</code>: 분리하는 역할을 할 뿐 화면에 표시할 수는 없는 문자(예: 공백 문자 등)</li>
<li><code>\p{S}</code> 또는 <code>\p{Symbol}</code>: 딩뱃(dingbat) 문자 및 기호(딩뱃은 조판 시에 사용하는 장식용 문자 예: ■, ○, ♥, ☎ 등)</li>
<li><code>\p{N}</code> 또는 <code>\p{Number}</code>: 숫자를 나타내는 문자</li>
<li><code>\p{P}</code> 또는 <code>\p{Punctuation}</code>: 문장 부호</li>
<li><code>\p{C}</code> 또는 <code>\p{Other}</code>: 앞의 속성을 제외한 나머지 모든 문자</li>
</ul>
<p>보통 <code>\w+</code>를 이용해서 단어를 일치시키는데 <code>\w</code>는 <code>[A-Za-z0-9_]</code>로 63개의 문자를 의미합니다.
<code>utf8</code> 모드일 경우에는 알파벳 뿐만 아니라 유니코드의 글자(letter)까지 확장해서 일치를 시킵니다.
하지만 단어이지만 숫자를 제외하고 일치시키고 싶다면 어떻게 해야할까요?
<code>\w+</code>를 사용하는 대신 <code>\p{L}+</code>를 사용하면 되겠죠. :-)</p>
<h2>유니코드의 스크립트</h2>
<p>유니코드 스크립트란 특정 언어에만 적용되게 만든 문자(코드 포인트)의 그룹이라고 말 할수 있습니다.
시스템에 따라 <code>\p{......}</code>를 사용하여 이 스크립트에 속하는 문자를 찾을 수도 있습니다.
예를 들어 <code>\p{Hangul}</code>이라고 하면 <em>한글</em> 쓰기 체계에 속하는 문자에 매치됩니다.
한글, 영어, 그리스어를 분리하는 프로그램은 아래와 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: unicode-5.pl
#
use v5.14;
use strict;
use warnings;
binmode STDIN, "encoding(utf8)";
binmode STDOUT, "encoding(utf8)";
chomp( my $input =(<STDIN>) );
say "matched: korean($&)" while $input =~ /\p{Hangul}+/g;
say "matched: latin($&)" while $input =~ /\p{Latin}+/g;
say "matched: greek($&)" while $input =~ /\p{Greek}+/g;
</pre>
<p><code>binmode</code>의 두 번째 인자에 <code>encoding(...)</code>를 사용하면 지정한 인코딩에 맞게 적절히 인코딩/디코딩을 수행합니다.
예제의 코드는 표준 입력으로 들어오는 것은 자동으로 디코드하고, 표준 출력으로 나가는 것은 자동으로 인코드합니다.</p>
<p>다음은 실행 결과입니다.</p>
<pre class="brush: perl;">
$ perl a.pl
당신을 사랑합니다. I love you. Σ 'αγαπώ.
matched: korean(당신을)
matched: korean(사랑합니다)
matched: latin(I)
matched: latin(love)
matched: latin(you)
matched: greek(Σ)
matched: greek(αγαπώ)
</pre>
<p>실행 시 한글과 영어 그리스어를 모두 입력하면 펄이 멋지게 분리시켜줍니다. :)</p>
<h2>크리스마스 선물 #1: 사전에서의 언어구분</h2>
<p>웹 문서에서 영어나 일본어, 한자 등을 인식하고 싶을 때는 <code>\w</code>를 쓰거나 복잡한 정규 표현식을 사용해야 합니다.
하지만 <code>\p</code> 문법을 이용하면 간단히 일치시킬 수 있습니다.
<a href="http://dic.naver.com/">네이버 사전</a>을 예를 들어보죠.
다음은 한글과 영어, 일본어, 한자 등의 문자를 추출합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: naver-dic.pl
#
use strict;
use warnings;
use Encode qw( decode );
use HTTP::Tiny;
die "Usage: $0 <word>\n" unless @ARGV == 1;
my $word = shift;
my $url = "http://dic.naver.com/search.nhn?dicQuery=$word&query=$word&target=dic&ie=utf8&query_utf=&isOnlyViewEE=";
my $res = HTTP::Tiny->new->get($url);
die "failed($res->{status}): $res->{reason}\n" unless $res->{success};
my $html = decode( 'utf-8', $res->{content} );
my @korean = $html =~ m{\p{Hangul}+}gsm; # 한글만 가져오기
my @english = $html =~ m{\p{Latin}+}gsm; # 영어만 가져오기
my @japanese = $html =~ m{\p{Katakana}+}gsm; # 일본어만 가져오기
my @chinese = $html =~ m{\p{Han}+}gsm; # 한자만 가져오기
</pre>
<p>이처럼 유니코드와 정규표현식을 잘 활용하면 손쉽게 각 언어별로 글자를 모아 2차 가공을 할 수 있습니다. :)</p>
<h2>크리스마스 선물 #2: 본문 중간의 특수 문자 변경</h2>
<p>웹 문서나 일반 문서를 가공할 때 본문 중간에 특수 문자가 있어 애를 먹는 경우가 많습니다.
이런 특수 문자를 모두 찾아 변경하거나 제거하면 예외 상황이 줄어들어 후처리하기 용이합니다.
다음은 특수 문자를 변경하거나 제거하는 기교를 부린 짧은 예제입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: love-perl.pl
#
use v5.14;
use utf8;
use strict;
use warnings;
binmode STDIN, "encoding(utf8)";
binmode STDOUT, "encoding(utf8)";
chomp (my $input =(<DATA>));
say $1 while $input =~ s/(\p{Symbol})/Love/g;
say $input;
__DATA__
I ♡ Perl.
</pre>
<p>다음은 실행한 결과입니다.</p>
<pre class="brush: bash;">
$ perl a.pl
♡
I Love Perl.
</pre>
<h2>정리하며</h2>
<p>유니코드 문자열을 사용하면 간단하면서도 풍부한 정규 표현식을 사용할 수 있습니다.
펄은 내부의 유니코드 구현이 <em>UTF-8</em>로 되어있어 너무나도 자유롭게 유니코드 일치를 사용할 수 있죠.
마치 <em>유니코드를 사용</em>하는데 있어 <em>펄은 거리낌이 없다</em>는 느낌이랄까요?</p>
<p>Enjoy Unicode & Perl! ;-)</p>
<h2>참고문서</h2>
<ul>
<li><a href="http://gypark.pe.kr/wiki/Perl/%ED%95%9C%EA%B8%80">Perl과 한글</a> - <a href="http://twitter.com/gypark">@gypark</a></li>
<li><a href="https://github.com/aero/perl_docs/wiki/Unicode-in-Perl">Unicode in Perl</a> - <a href="https://twitter.com/aer0">@aer0</a></li>
<li><a href="http://perldoc.perl.org/perlunicode.html">perldoc perlunicode</a></li>
<li><a href="http://perldoc.perl.org/perlunifaq.html">perldoc perlunifaq</a></li>
<li><a href="http://perldoc.perl.org/utf8.html">perldoc utf8</a></li>
</ul>
2013-12-11T00:00:00+09:00newbcodedcmon: While You Were Sleepinghttp://advent.perl.kr/2013/2013-12-10.html<h2>저자</h2>
<p>@berise - @대전(펄 미팅에 한번 가보고 싶지만 여건상 못가는 지방남),
테니스, <a href="https://github.com/berise">github:berise</a>, berise <em>at</em> gmail.com</p>
<h2>시작하며</h2>
<p>소개해 드릴 내용은 지난 펄크리스마스 달력에도 여러 번 등장한 웹 이미지를 저장하는 스크립트입니다.
크리스마스 즈음이 되면 어김없이 찾아오는 스크립트입니다.
원래 신문 기사나 dcinside 등의 갤러리 글을 긁는 용도였지만, 지금은 모니터링으로 용도가 변경 되었죠.
그래서 이름이 <em>dcmon</em>입니다. 네! 여러분이 생각하는 그것! 일명 짤방 수집기가 맞습니다.
기존의 스크립트들과 비교해 최신 글을 모니터링하여 가져올 수 있다는 점이 약간의 차이겠군요.
우리가 잠을 자는 사이에 많은 글들이 생성되고 삭제되지요. :)</p>
<p>처음에 <em>dcmon</em>은 Python으로 시작하였으나 Python의 정규 표현식 및 <a href="http://docs.python.org/2/library/urllib2.html">urllib2</a>의
처리 방식이 썩 마음에 들지 않아 Perl로 변경해서 구현하게 되었습니다.
Python 버전을 포함한 전체 코드는 <a href="https://github.com/berise/dcmon">GitHub의 저장소</a>에서 내려받을 수 있습니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Encode">CPAN의 Encode 모듈</a></li>
<li><a href="https://metacpan.org/module/LWP::Simple">CPAN의 LWP::Simple 모듈</a></li>
<li><a href="https://metacpan.org/module/WWW::Mechanize">CPAN의 WWW::Mechanize 모듈</a></li>
<li><a href="https://metacpan.org/module/Web::Scraper">CPAN의 Web::Scraper 모듈</a></li>
<li><a href="https://metacpan.org/module/threads">CPAN의 threads 모듈</a></li>
</ul>
<p><code>Encode</code> 모듈과 <code>threads</code> 모듈의 경우 펄 코어 모듈이므로 별도로 설치할 필요는 없습니다.</p>
<p>직접 [CPAN][cpan]을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
LWP::Simple \
WWW::Mechanize \
Web::Scraper
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
[perlbrew][home-perlbrew]를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
LWP::Simple \
WWW::Mechanize \
Web::Scraper
</pre>
<p>처음에는 외부 모듈과의 의존성을 줄이려고 <code>LWP 모듈</code>나 <code>Scraper 모듈</code>을 사용하지 않고 직접 HTML을 파싱해
필요한 링크나 파일 이름을 분리했지만 이번에 코드를 수정하면서 선진(?) 모듈을 도입했습니다.</p>
<h2>전체 흐름</h2>
<p>우선 전체적인 흐름의 순서는 다음과 같습니다.</p>
<ul>
<li>갤러리 모니터링</li>
<li>게시물 목록 가져오기</li>
<li>최신 글 가져오기</li>
<li>파일 이름 링크 분리</li>
<li>이미지 다운로드</li>
</ul>
<p><em>dcmon</em>은 설정 파일을 이용해 모니터링할 갤러리를 관리합니다.
<code>dcmon.cfg</code> 파일에 모니터링할 겔러리 목록을 개행 문자로 구분해서 넣습니다.
<code>#</code>은 주석으로 인식합니다.
갤러리 목록은 해당 웹페이지의 URL에 표기되는 게시판의 이름을 기록해야 합니다.
다음은 설정 파일의 예입니다.</p>
<pre class="brush: plain;">
#
# dcmon.cfg
#
comedy_new2
game_classic
#leagueoflegends
</pre>
<p>파일에서 읽은 갤러리 목록을 이용해 이미지를 저장할 경로를 만듭니다.
그 후 갤러리마다 별도의 쓰레드를 생성해 게시판을 모니터링합니다.</p>
<pre class="brush: perl;">
for my $gallery (@lines) {
next if $gallery =~ /^#/;
chomp $gallery;
setup_directory($gallery);
print "Monitoring $gallery\n";
my $thread = threads->create( \&run_dcmon_with_given_name, $gallery );
push(@threads, $thread);
}
for my $t (@threads) {
$t->join();
}
</pre>
<p>갤러리의 게시물 번호를 가져온 후 새로운 게시물이 있는지 판단 후 새로운 게시물이 있을 경우 해당 게시물을 가져옵니다.
dcinside의 경우 너무 자주 웹페이지를 읽을 경우 차단되므로 5 ~ 15초 가량 쉬어가며 모니터링합니다.</p>
<pre class="brush: perl;">
while (1) {
my @new_list = get_recent_number_list($gallery);
$curr_index = determine_most_recent_page_number( \@new_list, \@prev_list );
$image_count = find_and_get_images($gallery, $new_list[$curr_index]);
@prev_list = @new_list;
sleep( 5 + rand(10) );
}
</pre>
<h2>첨부 파일 다운로드</h2>
<p>첨부 파일을 다운로드하는 부분은 <em>dcmon</em>에서 가장 중요한 부분(존재의 이유)입니다. :)
HTML 구조상 브라우저에 보이는 이미지와 파일 이름이 분리되어 있습니다.
따라서 각 페이지에서 첨부 파일의 이름과 이미지 링크를 추출해야 합니다.
dcinside의 게시물에서 첨부 파일은 다음처럼 구성되어 있습니다.</p>
<pre class="brush: xml;">
<div class="box_file">
<p><b>blablabla&nbsp;<span>(1)</span></b></p>
<ul class="appending_file" style="width:1086px; overflow:hidden; word-wrap:break-word;">
<li class="icon_pic"><a href="http://image.dcinside.com/download.php?id=3dbcd4&no=29bcc427b18177a16fb3dab004c86b6f202dc30d4da4684a3e1ac6a7c7ebd7c987208b949b05d1e6c1b5eb4f7980e1ec361160615f&f_no=74e58173b48607f237e98fec4083736dc6302f1fd1fc44dacabf19050a6fd757ff7c2ae69f9e06">981514_496996270395434_1700987382_o.jpg</a></li></ul>
</div>
</pre>
<p>이제 <code>Web::Scraper</code> 모듈의 도움을 얻을 때 입니다.
앞의 HTML 코드 조각의 경우 <code>Web::Scraper</code> 모듈을 이용해 간단히 추출할 수 있습니다.
추출하는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
sub scrape_href_links {
my $html_content = shift;
my $html_element = scraper {
process ".icon_pic > a", "html[]" => 'HTML', "text[]" => 'TEXT';
};
# get html text based on div class="con_substance"
my $res = $html_element->scrape( $html_content );
return $res->{text};
}
</pre>
<p>dcinside 갤러리 웹페이지 내부의 이미지 링크는 다음처럼 구성되어 있습니다.</p>
<pre class="brush: xml;">
<!-- con_substance -->
<div class="con_substance">
<div class="s_write">
<div id='zzbang_div' style='display:none;'></div><pre></pre><table border="0" width="100%"><tr><td><p><br></p><p style="text-align: left;"><img src="http://dcimg1.dcinside.com/viewimage.php?id=3dbcd4&no=29bcc427b18177a16fb3dab004c86b6f202dc30d4da4684a3e1ac6a7c7ebd7c987208b949b05d1e6c1b5eb4f7980e1ec361160615f" class="txc-image" style="clear:none;float:none;" /></p><p>blablablabla<br></p></td></tr></table>
</div>
</pre>
<p>추출하는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
sub scrape_links {
my $html_content = shift;
my $html_element = scraper {
process ".con_substance", html => 'HTML';
};
# [] for plural
my $img_element = scraper {
process "img", "src[]" => '@src';
};
# get html text based on div class="con_substance"
my $res = $html_element->scrape( $html_content );
# get img src link which shows real image (in javascript pop window)
my $res2 = $img_element->scrape( $res->{html} );
return $res2->{src};
}
</pre>
<p>이렇게 파일 이름과 이미지 링크를 추출을 완료하면 가장 중요한 작업이 끝난 셈입니다.
이제 게시물이 포함된 웹 페이지와 첨부 파일을 가져와보죠.
종종 파일 이름이 없는 경우도 있는데 이 경우는 무시하고 건너 뛰도록하죠.
다음 함수는 원본 파일의 이름을 그대로 사용해서 저장하기 때문에 파일 이름이 겹치는 경우 기존 파일이 삭제됩니다.
이를 방지하려면 파일 이름을 유일하게 만들어 저장하는 기능이 필요합니다.</p>
<pre class="brush: perl;">
sub find_and_get_images {
my ( $gallery, $url ) = @_;
my $html_contents = get($url);
my $h_filenames = scrape_href_links($html_contents);
my $h_links = scrape_links($html_contents);
# http://zzbang.dcinside.com/pad_temp.jpg is basic image
# for an article without any image attached
if ( !defined($h_filenames) ) {
print "[$gallery] No image\n" if $opt{debug};
return;
}
my $file_count = @{$h_filenames};
my $link_count = @{$h_links};
my $image_count = 0;
print "# of files : ($file_count), # of links : ($link_count)\n" if $opt{debug};
for (my $i = 0; $i < $file_count; $i++) {
my $filename_p;
$filename_p = encode( 'cp949', $h_filenames->[$i] );
if ( $opt{debug} ) {
print " - download $filename_p\n";
print "$h_filenames->[$i], $h_links->[$i]\n";
}
download_and_save_as( $h_filenames->[$i], $h_links->[$i] );
}
return $file_count;
}
</pre>
<p>마지막으로 주어진 파일이름과 링크를 이용하여 이미지를 다운로드 및 저장하는 부분이 남았네요.
이것 저것 시험하다 보니 <code>wget</code>, <code>mechanize</code>, <code>LWP</code> 각각을 사용해서 다운로드하는 코드를 만들어버렸네요.
각각의 코드를 참고하고 입맛에 맞는 것으로 사용하면 됩니다.
LWP를 사용하는 부분은 예전 <a href="http://lotus.perl.kr/2012/01.html">펄 석가탄신일 달력</a>의 내용을 참고했습니다.
파일 인코딩은 환경에 맞게 변환하면 됩니다.</p>
<pre class="brush: perl;">
sub download_and_save_as {
my ( $filename, $link ) = @_;
my $USE_WGET = 0;
my $USE_MECHANIZE = 0;
my $USE_LWP = 1;
my $filename_p = encode( 'cp949', $filename );
# 저장할 위치 지정
$filename_p = "dcmon/temp/$directory_name/$filename_p";
if ($USE_WGET) {
my $cmd_wget = "wget \"$link\"";
print "Execute $cmd_wget\n";
# download with wget
system $cmd_wget;
}
elsif ($USE_MECHANIZE) {
my $image = get($link);
next unless defined $image;
open my $fh, ">", $filename_p;
binmode $fh;
print $fh $image;
close $fh;
}
elsif ($USE_LWP) {
my $ua = LWP::UserAgent->new(
agent => 'Mozilla/5.0 '
. '(Windows; U; Windows NT 6.1; en-US; rv:1.9.2b1) '
. 'Gecko/20091014 Firefox/3.6b1 GTB5'
);
my $res;
eval { $res = $ua->get($link); };
if ($@) {
warn $@;
next;
}
open my $fh, ">", $filename_p;
binmode $fh;
print $fh $res->content;
close $fh;
}
my $filesize = -s $filename_p if -e $filename_p;
print " + $filename_p($filesize Bytes)\n" if $opt{debug};
}
</pre>
<p>다음 코드는 <code>Web::Scraper</code> 모듈을 사용하기 이전 정규 표현식을 이용해 파일 이름과 링크를 추출했던 코드입니다.
dcinside의 웹페이지가 개편되면서 코드는 더 이상 동작하지 않습니다.
참고 삼아서 살펴보세요. :)</p>
<pre class="brush: perl;">
sub extract_filenames {
my $html_contents = shift;
my $pattern = "<li class=\"icon_pic\"><a href=.*>(.*?.*?)<\/a>";
my @files = $html_contents =~ /$pattern/gi;
for my $file (@files) {
print "extract_filenames : $file\n" if $opt{debug};
}
return @files;
}
sub extract_links {
my $html_contents = shift;
my $pattern = "<li class=\"icon_pic\"><a href=\"(.*)\">.*?.*?<\/a>";
my @links = $html_contents =~ /$pattern/gi;
for my $link (@links) {
print "extract_links : @links\n" if $opt{debug};
}
return @links;
}
</pre>
<h2>실행 그리고 한계</h2>
<p>실행하면 현재 디렉토리에 <code>dcmon</code> 디렉터리를 만들어 이 디렉토리 하부에 이미지들을 저장합니다.
<em>그림 1</em>은 터미널에서 실행한 결과를 갈무리한 것입니다.</p>
<p><img src="2013-12-10-1_r.png" alt="go dcmon!" id="godcmon" />
<em>그림 1.</em> go dcmon! (<a href="2013-12-10-1.png">원본</a>)</p>
<p>아무래도 차단되는 것을 방지하기 위해 시간차를 주기 때문에
그 시간차 사이에 생성되었다가 지워지는 글은 놓칠 수 밖에 없습니다.
dcinside에서 API를 지원해주기 전까지는 어쩔수 없는 구조적인 문제이므로 이정도는 넘어가도록 하죠.
우린 관대하니까요. :)</p>
<p>큰 무리없이 사용할 수 있는 스크립트지만 간혹 모니터링하다가 죽어버리는 경우가 발생합니다.
(흠, 밤새 돌아야 하는 코드인데...)
아직 분석을 하지 않아 무슨 문제로 죽는지는 모르지만 언제나 <a href="https://github.com/berise/dcmon">패치는 환영</a>합니다. :)</p>
<h2>정리하며</h2>
<p>한동안 사용하지 않던 코드를 다시 꺼내 실행 가능하게 수정하는 것은 역시 쉽지 않네요.
그래도 펄의 유연함 덕분에 빠른 시간 안에 수정 하고 실행을 가능했답니다.
<code>LWP</code>, <code>Mechanize</code>, <code>Scraper</code> 등 멋진 모듈들이 있어 펄을 더욱 즐겁게 사용할 수 있는 것 같습니다. :)</p>
2013-12-10T00:00:00+09:00beriseplenv vs perlbrewhttp://advent.perl.kr/2013/2013-12-09.html<h2>저자</h2>
<p><a href="http://twitter.com/aanoaa">@aanoaa</a> - 홍형석, 사당동 펠프스, <a href="https://github.com/aanoaa">github:aanoaa</a></p>
<h2>시작하며</h2>
<p><a href="http://perlbrew.pl/">perlbrew</a>는 펄 커뮤니티 내에서 널리 사용되는 펄 설치 및 관리 도구입니다.
<a href="http://advent.perl.kr">Seoul.pm 크리스마스 달력</a>에서 여러번 다뤄지기도 했죠.</p>
<ul>
<li><a href="http://advent.perl.kr/2011/2011-12-13.html">2011 - 열세번째 날: How to Use CPAN, Actually</a></li>
<li><a href="http://advent.perl.kr/2011/2011-12-13.html">2011 - 열여섯번째 날: perlbrew, local::lib, smartcd 를 이용하여 Perl 환경 구축하기</a></li>
<li><a href="http://advent.perl.kr/2011/2011-12-13.html">2013 - 셋째 날: 좋은 모듈을 고르는 좋은 방법</a></li>
</ul>
<p><em>perlbrew</em> 릴리스 이후 거의 4년 만에 강력한 라이벌인 <a href="https://github.com/tokuhirom/plenv">plenv</a>가 나타났습니다.</p>
<h2 id="plenvvsperlbrew">plenv vs perlbrew</h2>
<p><a href="http://perlbrew.pl/">perlbrew</a>와 <a href="https://github.com/tokuhirom/plenv">plenv</a> 모두 홈 디렉토리에 펄을 설치하고,
사용하는 쉘에서 마음대로 버전을 바꿔가며 사용할 수 있습니다.
또 사용하는 펄에 따라 모듈 설치 경로가 바뀝니다.</p>
<p><em>plenv</em> 에는 <em>perlbrew</em> 와 차별화되는 두 가지 기능이 있습니다.</p>
<h3>경로별 펄 버전 지정</h3>
<p><em>perlbrew</em>와는 달리 <em>plenv</em>는 특정 경로에서 사용할 펄을 강제로 지정할 수 있습니다.
다음 명령은 <code>/path/to/.perl-version</code> 파일을 만듭니다.</p>
<pre class="brush: bash;">
$ cd /path/to
$ plenv local 5.18.1
</pre>
<p><code>/path/to/.perl-version</code> 파일에는 사용할 펄의 버전 정보가 단순한 텍스트 형식으로 담겨집니다.</p>
<pre class="brush: bash;">
$ cat /path/to/.perl-version
5.18.1
</pre>
<p><code>.perl-version</code> 파일이 있는 한 <code>/path/to</code> 디렉터리 및 그 하부 디렉터리까지 <code>5.18.1</code> 버전을 사용하게 됩니다.</p>
<h3>환경 변수로 펄 버전 지정</h3>
<p><code>PLENV_VERSION</code> 환경 변수를 사용하면 영구적이 아니라
환경 변수의 영향 아래서만 다른 버전의 펄을 사용할 수 있습니다.</p>
<pre class="brush: bash;">
$ PLENV_VERSION=system perl a.pl
</pre>
<h2>정리하며</h2>
<p>4년 전 혜성처럼 등장한 <a href="http://perlbrew.pl/">perlbrew</a> 덕분에 시스템의 펄과는 별개의 펄 환경을
너무도 손쉽게 구축할 수 있게 되면서 펄 개발 환경은 무척 편리해졌었습니다.
사실 <em>perlbrew</em>는 여전히 <a href="http://metacpan.org/favorite/leaderboard">CPAN의 최고 모듈 명단</a>의 10위권 안에 들 정도로 여전히 인기있는 모듈입니다.
앞서 언급하지 않았지만 <em>perlbrew</em>에는 <code>lib</code>, <code>upgrade-perl</code>, <code>self-upgrade</code> 등 <em>plenv</em>에는 없는 강력한 기능이 많습니다.
마찬가지로 <em>plenv</em> 역시 <em>perlbrew</em>를 사용하면서 아쉬웠던 부분을 구석구석 긁어주는 기능이 많죠.
필요에 따라 적절한 도구를 선택해서 사용하는 것은 여러분의 몫이겠죠? ;-)</p>
2013-12-09T00:00:00+09:00aanoaa동적 차트 그리기http://advent.perl.kr/2013/2013-12-08.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>근래에 들어 HTML5와 CSS, 자바스크립트를 비롯 웹 관련 라이브러리들이 비약적으로 발전했습니다.
그래프만 해도 예전에는 서버의 자료를 서버의 자원 및 라이브러리를 이용해서 그래프를 이미지로
제작한 다음 이를 브라우저에서 보여주었다면 최근에는 미려하면서도 화려한 그래프 라이브러리를
사용해 클라이언트의 자원을 이용해 보여주곤 합니다.
유명한 대부분의 그래프 라이브러리는 Ajax 호출을 통해 자료를 얻어와
거의 실시간에 가깝게 그래프를 갱신해주는 기능을 포함하고 있죠.
<a href="http://www.flotcharts.org/">Flot 그래프 라이브러리</a>와 <a href="http://mojolicio.us/">Mojolicious 웹프레임워크</a>를 조합해
CPU 사용률을 그래프로 표현하는 간단한 웹앱을 통해 실시간 그래프 그리기에 대해 감을 잡아 볼까요?</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/JSON">CPAN의 JSON 모듈</a></li>
<li><a href="https://metacpan.org/module/List::MoreUtils">CPAN의 List::MoreUtils 모듈</a></li>
<li><a href="https://metacpan.org/module/Mojolicious">CPAN의 Mojolicious 모듈</a></li>
<li><a href="https://metacpan.org/module/Path::Tiny">CPAN의 Path::Tiny 모듈</a></li>
<li><a href="https://metacpan.org/module/Sys::Info">CPAN의 Sys::Info 모듈</a></li>
<li><a href="https://metacpan.org/search?q=Sys%3A%3AInfo%3A%3ADriver">CPAN의 Sys::Info::Driver::* 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan \
JSON \
List::MoreUtils \
Mojolicious \
Path::Tiny \
Sys::Info
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
JSON \
List::MoreUtils \
Mojolicious \
Path::Tiny \
Sys::Info
</pre>
<h2 id="flot">Flot 다운로드</h2>
<p><a href="http://www.flotcharts.org/">Flot 그래프 라이브러리</a>를 <a href="http://www.flotcharts.org/downloads/">다운로드</a>한 후 압축을 해제합니다.</p>
<pre class="brush: bash;">
$ wget http://www.flotcharts.org/downloads/flot-0.8.2.tar.gz
$ tar xvzf flot-0.8.2.tar.gz
</pre>
<p>테스트에 필요한 <em>jQuery</em>와 <em>flot</em> 그래프 자바스크립트를 <code>public/</code> 디렉터리 하부로 옮기고 필요 없는 디렉터리는 지웁니다.</p>
<pre class="brush: bash;">
$ mkdir public
$ cp flot/jquery.min.js flot/jquery.flot.min.js public/
$ rm -rf flot
</pre>
<h2 id="flot">Flot 그래프 생성</h2>
<p>Flot 그래프는 다음과 같은 방식으로 생성할 수 있습니다.</p>
<pre class="brush: jscript;">
var plot = $.plot(container, series, {
grid: {
borderWidth: 1,
minBorderMargin: 20,
labelMargin: 10,
backgroundColor: { colors: ["#fff", "#e4f4f4"] },
margin: { top: 8, bottom: 20, left: 20 },
markings: function(axes) {
var markings = [];
var xaxis = axes.xaxis;
for (var x = Math.floor(xaxis.min); x < xaxis.max; x += xaxis.tickSize * 2) {
markings.push({ xaxis: { from: x, to: x + xaxis.tickSize }, color: "rgba(232, 232, 255, 0.2)" });
}
return markings;
}
},
xaxis: { tickFormatter: function() { return ""; } },
yaxis: { min: 0, max: 110 },
legend: { show: true }
});
</pre>
<p><code>plot</code> 함수에 넘겨주는 첫 번째 인자인 <code>container</code> 변수는 그래프를 품게될 HTML 요소 아이디입니다.
두 번째 인자인 <code>series</code> 변수는 다음과 같은 형식으로 구성합니다.</p>
<pre class="brush: jscript;">
var series = [
[
[ x1, y1 ],
[ x2, y2 ],
...
[ xn, yn ]
]
];
</pre>
<p>생성할 그래프에 옵션을 설정하고 싶다면 <code>series</code> 변수를 다음처럼 구성할 수도 있습니다.</p>
<pre class="brush: jscript;">
var series = [
{
data: [
[ x1, y1 ],
[ x2, y2 ],
...
[ xn, yn ]
],
lines: {
fill: true
}
}
];
</pre>
<p>자세한 내용은 <a href="https://github.com/flot/flot/blob/master/API.md">Flot 문서</a>를 참고하세요.</p>
<h2 id="cpu">CPU 사용률 확인</h2>
<p>그래프로 표현할 CPU 사용률 자료를 제공할 간단한 스크립트를 만들어볼까요?</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: cpu.pl
#
use v5.16;
use utf8;
use strict;
use warnings;
use JSON;
use List::MoreUtils qw( pairwise );
use Path::Tiny;
use Sys::Info::Constants qw( :device_cpu );
use Sys::Info;
my $info = Sys::Info->new;
my $cpu = $info->device('CPU');
my @y = ( 0 ) x 600;
while (1) {
shift @y if @y == 600;
push @y, $cpu->load(DCPU_LOAD_LAST_01) * 100;
my @x = 0 .. @y - 1;
my @data = pairwise { [ $a / 2, $b ] } @x, @y;
path('cpu.json')->spew_utf8( encode_json(\@data) );
sleep 1;
}
</pre>
<p><a href="https://metacpan.org/module/Sys::Info">Sys::Info 모듈</a>은 현재 스크립트를 실행하는 장비의 CPU 사용률을 얻기위해 사용한 모듈입니다.
CPU 사용률을 구할 때는 직접 <code>/proc/*</code> 하부의 값을 파싱한다던가 또는 다른 라이브러리를 사용해도 무방합니다.
<code>DCPU_LOAD_LAST_01</code> 값은 최근 1분 동안의 CPU 사용률을 얻어오라는 의미입니다.
매 1초마다 CPU 사용률을 구한 다음 순환 큐처럼 밀어내는 방식으로 총 600개의 <code>x</code>, <code>y</code> 좌표를 구합니다.
생성한 좌표 자료는 JSON 형식으로 <code>cpu.json</code> 파일에 저장합니다.
자료는 다음과 같은 형태로 저장됩니다.</p>
<pre class="brush: plain;">
[
[0,0], [0.5,0], [1,0], [1.5,0], [2,0], [2.5,0], [3,0], [3.5,0],
[4,0], [4.5,0], [5,0], [5.5,0], [6,0], [6.5,0], [7,0], [7.5,0],
[8,0], [8.5,0], [9,0], [9.5,0], [10,0], [10.5,0], [11,0], [11.5,0],
...
]
</pre>
<h2 id="mojolicious::lite">Mojolicious::Lite 웹앱 준비</h2>
<p><code>Mojolicious</code> 웹앱은 무척 간단합니다.
Ajax를 사용해 동적으로 그래프를 갱신하는 것이 목적이므로 다음 두 개의 컨트롤러가 필요합니다.</p>
<ul>
<li><code>GET /</code></li>
<li><code>GET /cpu</code></li>
</ul>
<p><code>/</code> 컨트롤러는 그래프를 화면에 보여주기 위한 것이며 <code>/cpu</code> 컨트롤러는 <code>/</code> 페이지에서
Ajax를 이용해 계속해서 새로운 자료로 갱신하기 위해 필요합니다.</p>
<p><code>/</code> 컨트롤러와 템플릿은 다음처럼 구성합니다.</p>
<pre class="brush: perl;">
get '/' => 'index';
...
__DATA__
@@ index.html.ep
% layout 'default';
% title 'Welcome';
<div id="content">
<div class="demo-container">
<div id="placeholder" class="demo-placeholder"></div>
</div>
</div>
</pre>
<p><code>/cpu</code> 컨트롤러 JSON 형태의 응답만 반환하면 되기 때문에 렌더링할 템플릿을 정할 필요가 없으므로 다음처럼 구성합니다.</p>
<pre class="brush: perl;">
get '/cpu' => sub {
my $self = shift;
return $self->respond_to(
json => {
status => 200,
text => path('cpu.json')->slurp_utf8,
},
);
};
</pre>
<p><code>/</code> 페이지를 렌더링할 때 Flot 그래프를 보여주기 위해 필요한 CSS와 자바스크립트를 적재합니다.</p>
<pre class="brush: xml;">
<style type="text/css">
.demo-container {
box-sizing: border-box;
width: 850px;
height: 450px;
padding: 20px 15px 15px 15px;
margin: 15px auto 30px auto;
border: 1px solid #ddd;
background: #fff;
background: linear-gradient(#f6f6f6 0, #fff 50px);
<style type="text/css">
.demo-container {
box-sizing: border-box;
width: 850px;
height: 450px;
padding: 20px 15px 15px 15px;
margin: 15px auto 30px auto;
border: 1px solid #ddd;
background: #fff;
background: linear-gradient(#f6f6f6 0, #fff 50px);
background: -o-linear-gradient(#f6f6f6 0, #fff 50px);
background: -ms-linear-gradient(#f6f6f6 0, #fff 50px);
background: -moz-linear-gradient(#f6f6f6 0, #fff 50px);
background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px);
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
-o-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
}
.demo-placeholder {
width: 100%;
height: 100%;
font-size: 14px;
line-height: 1.2em;
}
</style>
<script language="javascript" type="text/javascript" src="/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="/jquery.flot.min.js"></script>
background: -o-linear-gradient(#f6f6f6 0, #fff 50px);
background: -ms-linear-gradient(#f6f6f6 0, #fff 50px);
background: -moz-linear-gradient(#f6f6f6 0, #fff 50px);
background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px);
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
-o-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
}
.demo-placeholder {
width: 100%;
height: 100%;
font-size: 14px;
line-height: 1.2em;
}
</style>
<script language="javascript" type="text/javascript" src="/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="/jquery.flot.min.js"></script>
</pre>
<h2>1초 단위 갱신!</h2>
<p>1초 단위로 갱신하는 핵심 코드입니다.
<code>getCpuLoad()</code> 함수가 <code>/cpu</code> 컨트롤러로 접근해 가장 최신의 CPU 정보를 얻어온 다음
<code>plot.setData()</code> 메소드로 그래프에 들어갈 자료를 갱신하고 <code>plot.draw()</code> 메소드를
호출해서 갱신된 자료와 일치하도록 눈에 보이는 그래프를 갱신합니다.</p>
<pre class="brush: jscript;">
<script type="text/javascript">
$(function() {
var plot = $.plot("#placeholder", [], {
series: {
shadowSize: 4
},
yaxis: {
show: true,
min: 0,
max: 100
},
xaxis: {
show: false,
min: 0,
max: 300
}
});
function getCpuLoad() {
$.ajax("/cpu.json", {
type: 'GET',
success: function(data, textStatus, jqXHR) {
plot.setData([{ data: data, lines: { fill: true } }]);
plot.draw();
},
});
}
function update() {
getCpuLoad();
setTimeout(update, 1000); // 1초마다 갱신
}
update();
});
</script>
</pre>
<h2>전체 코드</h2>
<p>전체 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: cpu-web.pl
#
use Mojolicious::Lite;
use Path::Tiny;
get '/' => 'index';
get '/cpu' => sub {
my $self = shift;
return $self->respond_to( json => { status => 200, text => path('cpu.json')->slurp_utf8 } );
};
app->start;
__DATA__
@@ index.html.ep
% layout 'default';
% title 'R.I.P. @am0c - Seoul.pm 펄 크리스마스 달력 #2013';
<div id="content">
<div class="demo-container">
<div id="placeholder" class="demo-placeholder"></div>
</div>
</div>
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<style type="text/css">
.demo-container {
box-sizing: border-box;
width: 850px;
height: 450px;
padding: 20px 15px 15px 15px;
margin: 15px auto 30px auto;
border: 1px solid #ddd;
background: #fff;
background: linear-gradient(#f6f6f6 0, #fff 50px);
background: -o-linear-gradient(#f6f6f6 0, #fff 50px);
background: -ms-linear-gradient(#f6f6f6 0, #fff 50px);
background: -moz-linear-gradient(#f6f6f6 0, #fff 50px);
background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px);
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
-o-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
-webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
}
.demo-placeholder {
width: 100%;
height: 100%;
font-size: 14px;
line-height: 1.2em;
}
</style>
<script language="javascript" type="text/javascript" src="/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="/jquery.flot.min.js"></script>
<script type="text/javascript">
$(function() {
var plot = $.plot("#placeholder", [], {
series: {
shadowSize: 4
},
yaxis: {
show: true,
min: 0,
max: 100
},
xaxis: {
show: false,
min: 0,
max: 300
}
});
function getCpuLoad() {
$.ajax("/cpu.json", {
type: 'GET',
success: function(data, textStatus, jqXHR) {
plot.setData([{ data: data, lines: { fill: true } }]);
plot.draw();
},
});
}
function update() {
getCpuLoad();
setTimeout(update, 1000); // 1초마다 갱신
}
update();
});
</script>
</head>
<body>
<%= content %>
</body>
</html>
</pre>
<p>결과를 보려면 <code>cpu.pl</code>과 <code>cpu-web.pl</code> 두 코드 모두를 실행해야 합니다.
우선 CPU 사용률을 모으는 <code>cpu.pl</code> 스크립트를 먼저 실행합니다.</p>
<pre class="brush: bash;">
$ chmod 755 cpu.pl
$ ./cpu.pl
</pre>
<p>이후 <code>cpu-web.pl</code> 웹앱을 실행합니다.</p>
<pre class="brush: bash;">
$ morbo cpu-web.pl
</pre>
<p><em>그림 1</em>은 브라우저로 <code>http://localhost:3000</code>에 접속한 결과를 갈무리한 것입니다.</p>
<p><img src="2013-12-08-1_r.png" alt="실시간 CPU 사용률" id="cpu" />
<em>그림 1.</em> 실시간 CPU 사용률 (<a href="2013-12-08-1.png">원본</a>)</p>
<p>생각보다 그럴듯하죠? :)</p>
<h2>정리하며</h2>
<p>사실 차트를 그리는 핵심적인 부분은 자바스크립트 그래프 라이브러리가 대부분을 처리해줍니다.
<a href="http://www.flotcharts.org/">Flot 그래프 라이브러리</a> 말고도 수많은 미려한 그래프 라이브러리가 많으니 한 번 조사해보세요.
라이브러리는 서로 다르더라도 실시간으로 그래프를 갱신시켜주는 기법은 모두 대동소이합니다.
최소 그래프를 보여주기 위한 페이지 하나와 Ajax로 그래프의 자료에 해당하는 부분을
갱신시켜줄 수 있는 컨트롤러가 하나 필요한 것이 전부입니다.
자바스크립트 그래프 라이브러리의 장점은 렌더링 자원을 서버 대신 접속하는 클라이언트에게 전가시킬 수 있으며
최근 HTML과 CSS의 발전으로 이미지로는 한계가 있는 미려한 실시간 그래프를 그릴 수 있다는 점입니다.
민감한 자료라면 세부적인 수치 자체가 개방된다는 점이 단점인데 이런 부분만 유의한다면 다양하게 활용할 수 있을 것입니다.</p>
<p>Enjoy Your Perl! ;-)</p>
<p><img src="2013-12-08-2.png" alt="EOT" id="eot" style="margin: 0" /></p>
<p><em>EOT</em></p>
2013-12-08T00:00:00+09:00keediSeoul.pm 펄 크리스마스 달력 정리하기http://advent.perl.kr/2013/2013-12-07.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><a href="http://advent.perl.kr">Seoul.pm 펄 크리스마스 달력</a>이 시작된지도 벌써 4년째입니다.
매년 12월마다 크리스마스 바로 전 날까지 기사가 이어져 오며 펄은 물론 펄과 연관된 다양한 주제를 다루고 있습니다.
올해 역시 24개의 기사가 이어진다면 이제는 거의 100여개에 가까운 기사가 모이는 셈입니다.
매일 하나의 기사가 열리길 기다리며 클릭하는 즐거움은 크리스마스 달력의 취지에 맞기는 하나
첫 페이지에서 기사의 목록을 한 번에 훑어볼 수 있도록 시각적으로 제공하지는 않아 시간이 지난 후
특정 기사를 찾기는 여간 힘들지 않습니다.
편집진의 게으름 때문(!?)인지 올해까지도 여전히 전체 기사 목록은 제공되지 않고 있죠.</p>
<p><em>"배고픈 자가 우물을 판다"</em>라는 말도 있는데 <em>Seoul.pm 펄 크리스마스 달력</em>을 구미에 맞게 정리해볼까요?</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Mojo::UserAgent">CPAN의 Mojo::UserAgent 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Mojo::UserAgent
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Mojo::UserAgent
</pre>
<p><code>Mojo::UserAgent</code> 모듈은 <a href="https://metacpan.org/module/Mojolicious">Mojolicious 모듈</a>의
하부 모듈이기 때문에 설치를 하면 <code>Mojolicious</code> 모듈이 설치됩니다.
<a href="http://mojolicio.us/">Mojolicious</a>는 웹프레임워크지만
의존 모듈이 없고 매우 가볍기 때문에 널리 사용되고 있습니다.</p>
<h2 id="mojo::useragent">Mojo::UserAgent</h2>
<p>사실 제 경우 간단한 HTTP 요청을 처리할때 대부분의 경우 <a href="https://metacpan.org/module/HTTP::Tiny">CPAN의 HTTP::Tiny 모듈</a>을 즐겨 사용합니다.
하지만 요청 후 받은 응답의 HTML을 체계적으로 처리해야 하는 경우라면
<a href="https://metacpan.org/module/Mojo::UserAgent">Mojo::UserAgent 모듈</a>과 <a href="https://metacpan.org/module/Mojo::DOM">Mojo::DOM 모듈</a>을 사용하죠.
<code>Mojo::UserAgent</code>와 <code>Mojo::DOM</code> 두 모듈 모두 <a href="https://metacpan.org/module/Mojolicious">Mojolicious 모듈</a>에 속해있는 하부 모듈입니다.
웹페이지를 단순히 긁어오는 것이 아니라 제목이나, 저자, 첫 문단 등을 명확하게 구분하려면 단순한 문자열 비교나
정규표현식 보다는 HTML의 <a href="http://en.wikipedia.org/wiki/Document_Object_Model">DOM 트리 구조</a>를 이용해 각각의 요소에 접근하는 편이 유리합니다.
<code>HTTP::Tiny</code> 모듈은 HTTP 프로토콜 처리 그 자체만 다루는데 비해 <code>Mojo::UserAgent</code> 모듈은
<code>Mojo::DOM</code> 모듈과 연동해 HTML 문서의 DOM 트리를 다룰 수 있기 때문에 이런 류의 작업에는 딱이죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: seoulpm-advent-calendar-digest.pl
#
use v5.16;
use utf8;
use strict;
use warnings;
use Mojo::UserAgent;
say
Mojo::UserAgent
->new
->get('http://advent.perl.kr/2013/2013-12-06.html')
->res
->text;
</pre>
<p>앞의 예제는 <a href="http://advent.perl.kr/2013/2013-12-06.html">여섯째 날 기사인 "알록달록 perldoc" 기사</a>의 HTML 코드를 화면에 출력합니다.</p>
<pre class="brush: bash;">
$ chmod 755 seoulpm-advent-calendar-digest.pl
$ ./seoulpm-advent-calendar-digest.pl
<!doctype html>
<html xmlns:fb="http://ogp.me/ns/fb#">
<head>
<title>여섯째 날: 알록달록 perldoc | R.I.P. @am0c - Seoul.pm 펄 크리스마스 달력 #2013</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="description" content="Seoul.pm Perl Advent Calendar 2013" />
...
</pre>
<h2 id="mojo::dom">Mojo::DOM</h2>
<p><code>Mojo::UserAgent</code> 모듈로 받아온 HTML에서 DOM 트리에 접근하는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
my $url = "http://advent.perl.kr/2013/2013-12-06.html";
my $dom = Mojo::UserAgent->new->get($url)->res->dom;
</pre>
<p>다행히(?) <em>Seoul.pm 펄 크리스마스 달력</em>의 기사는 전체적으로 비슷한 구조를 가집니다.
HTML 코드 내부를 살펴보면 다음과 같은 구조로 이루어져 있습니다.</p>
<pre class="brush: xml;">
<!doctype html>
<html xmlns:fb="http://ogp.me/ns/fb#">
<head>
<title>여섯째 날: 알록달록 perldoc | R.I.P. @am0c - Seoul.pm 펄 크리스마스 달력 #2013</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="description" content="Seoul.pm Perl Advent Calendar 2013" />
... 메타 태그
... 자바스크립트
... CSS
</head>
<body>
...
<div id="wrap">
<div class="nav top">
... 네비게이션 및 메뉴
... 메뉴
</div>
<div id="cont">
<h1>여섯째 날: 알록달록 perldoc</h1>
<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>
...
</div>
... 푸터
</div>
</body>
</html>
</pre>
<p>언듯 복잡해보이지만 우리에게 필요한 모든 정보는 <code><div id="cont">...</div></code> 사이에 있습니다.
우리가 사용할 HTML 요소는 모두 <code>div#cont</code> 하부에 있기 때문에
<code>at</code> 메소드를 사용해서 좀 더 하부의 DOM 트리를 선택해보죠.</p>
<pre class="brush: perl;">
my $url = "http://advent.perl.kr/2013/2013-12-06.html";
my $dom = Mojo::UserAgent->new->get($url)->res->dom->at('#cont');
</pre>
<h2>추출!</h2>
<p>정리할 목록을 정하면 어떤 항목들을 추출해야 할지 명확해지겠죠?
다음 항목을 추출한다고 생각해보죠.</p>
<ul>
<li>제목</li>
<li>저자</li>
<li>목차</li>
<li>요약</li>
</ul>
<p>우선 제목은 <code>div#cont</code> 항목 중 <code>h1</code> 요소에 해당하는 문자열입니다.
<code>$dom</code> 트리를 이용해서 해당 값을 가져오는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
$dom->at('h1')->all_text
</pre>
<p>저자는 <code>div#cont</code> 항목 중 첫 번째 <code>h2</code> 요소 바로 다음 단락의 문자열입니다.
<code>$dom</code> 트리를 이용해서 해당 값을 가져오는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
$dom->at('h2')->next->all_text
</pre>
<p>목차는 <code>div#cont</code> 항목 중 <code>h2</code> 요소에 해당하는 문자열 목록입니다.
사실 기사에서 <code>h3</code> 태그를 사용하는 경우도 있지만 목차 생성시에는 가장 큰 제목인 <code>h2</code> 항목만을
고려한다고 가정(<code>h3</code> 요소를 추출하는 것 역시 그리 어렵지는 않습니다. :)하죠.
<code>$dom</code> 트리를 이용해서 해당 값을 가져오는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
my @toc;
$dom->find('h2')->each(sub {
return if $_->all_text eq '저자';
push @toc, $_->all_text;
});
</pre>
<p>요약은 <code>div#cont</code> 항목 중 두 번째 <code>h2</code> 요소와 세 번째 <code>h2</code> 요소 사이의 모든 요소가 포함하는 문자열입니다.
<code>$dom</code> 트리를 이용해서 해당 값을 가져오는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
my @desc;
for ( my $e = $dom->find('h2')->[1]->next; $e; $e = $e->next ) {
last if $e->type eq 'h2';
push @desc, $e->all_text;
}
</pre>
<p>맙소사! 추출이 모두 끝났습니다. ;-)</p>
<h2>나머지 코드</h2>
<p>내친 김에 명령줄에서 사용자의 입력을 받아 지정한 기사를 요약할 수 있도록 수정해보죠.
전체 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: seoulpm-advent-calendar-digest.pl
#
use v5.16;
use utf8;
use strict;
use warnings;
use Mojo::UserAgent;
binmode STDOUT, ':utf8';
my $y = shift;
my $d = shift;
die "Usage: $0 <year> <day>\n" unless $y && $d;
my $url = sprintf "http://advent.perl.kr/%d/%d-12-%02d.html", $y, $y, $d;
my $dom = Mojo::UserAgent->new->get($url)->res->dom->at('#cont');
my $author = get_author($dom);
my $title = get_title($dom);
my $desc = get_desc($dom);
my @toc = get_toc($dom);
print <<"END_DIGEST";
주소: $url
제목: $title
저자: $author
목차:
@{[ join "\n ", @toc ]}
요약:
$desc
END_DIGEST
sub get_author { shift->at('h2')->next->all_text }
sub get_title { shift->at('h1')->all_text }
sub get_toc {
my $dom = shift;
my @toc;
$dom->find('h2')->each(sub {
return if $_->all_text eq '저자';
push @toc, $_->all_text;
});
return @toc;
}
sub get_desc {
my $dom = shift;
my @desc;
for ( my $e = $dom->find('h2')->[1]->next; $e; $e = $e->next ) {
last if $e->type eq 'h2';
push @desc, $e->all_text;
}
return join( "\n\n", @desc );
}
</pre>
<p>요약할 달력 기사를 지정할 수 있기 때문에 실행할 때 연도와 날짜를 명령줄 인자로 넘겨줍니다.</p>
<pre class="brush: bash;">
$ ./seoulpm-advent-calendar-digest.pl 2013 6
</pre>
<p><em>그림 1</em>은 터미널에서 실행한 결과를 갈무리한 것입니다.</p>
<p><img src="2013-12-07-1_r.png" alt="달력 기사 요약 결과" id="" />
<em>그림 1.</em> 달력 기사 요약 결과 (<a href="2013-12-07-1.png">원본</a>)</p>
<p>간단하죠? :)</p>
<h2>정리하며</h2>
<p>사실 <em>Seoul.pm 펄 크리스마스 달력</em>의 요약판이 존재하지 않은 덕분(?)에 <a href="http://twitter.com/#!/gypark">@gypark님</a>께서는
<a href="http://gypark.pe.kr/wiki/Perl/AdventCalendar">요약 페이지</a>를 이미 제공하고 있으며, 이에 그치지 않고
<a href="http://gypark.pe.kr/wiki/Perl/%ED%8E%84%ED%81%AC%EB%A6%AC%EC%8A%A4%EB%A7%88%EC%8A%A4%EB%8B%AC%EB%A0%A5%EC%9A%94%EC%95%BD%EC%A7%91%EB%A7%8C%EB%93%A4%EA%B8%B0">요약 페이지를 만드는 법</a>까지도 상세한 단계별 설명과 함께 소개하고 있습니다.
<a href="http://twitter.com/#!/luzluna">@luzluna님</a>께서는 <a href="http://doc.perl.kr">펄 문서화 위키</a>에 <a href="http://doc.perl.kr/Main/%ED%81%AC%EB%A6%AC%EC%8A%A4%EB%A7%88%EC%8A%A4%EB%8B%AC%EB%A0%A5">연도별 요약 페이지</a>를 만들어주시기도 하셨죠.
어떤 측면(?)에서는 Seoul.pm 크리스마스 달력 요약판이 없었던 것이 더 긍정적이지 않았나 싶기까지 하군요. :-)</p>
<p><a href="https://metacpan.org/module/Mojolicious">Mojolicious 모듈</a>이 제공하는 <a href="https://metacpan.org/module/Mojo::UserAgent">Mojo::UserAgent 모듈</a>과
<a href="https://metacpan.org/module/Mojo::DOM">Mojo::DOM 모듈</a>을 이용하면 HTML 문서에서 필요한 항목을 추출해 요약하는 작업이 무척 수월해집니다.
물론 두 모듈을 사용하기 위한 DOM 트리와 CSS 선택자에 대한 최소한의 이해(또는 공부)는 필수겠죠?</p>
<p>Enjoy Your Perl! ;-)</p>
<p><img src="2013-12-07-2.png" alt="EOT" id="eot" style="margin: 0" /></p>
<p><em>EOT</em></p>
2013-12-07T00:00:00+09:00keedi알록달록 perldochttp://advent.perl.kr/2013/2013-12-06.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>펄은 <a href="http://perldoc.perl.org/">perldoc</a>이라는 시스템을 통해 시스템에 제공되는
펄은 물론 관련 모듈까지 모두 문서를 제공하며 이 에코 시스템에 익숙해진
펄 개발자라면 누구나 자연스레 <em>perldoc</em> 문서를 읽고, 또 만들게 됩니다.
<a href="https://metacpan.org/module/Carp">Carp 모듈</a>의 내용이 궁금하다면 언제든지 작업하던 터미널에서
<code>perldoc Carp</code>라고 입력하면 <em>perldoc</em> 문서를 읽을 수 있습니다.
이렇게 편리한 <em>perldoc</em>이지만 한 가지 아쉬운 점이 있다면 밋밋하게 단색으로 보여
현란한 웹에 익숙해진 현대인으로서는 읽기가 그리 편하지 않다는 점입니다.</p>
<p>지금부터 터미널을 좋아하는 텍스트 덕ㅎ.. 아니 여러분을 위해
<code>perldoc</code> 매뉴얼에 알록달록 색깔을 입혀보죠!</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Pod::Text::Color::Delight">CPAN의 Pod::Text::Color::Delight</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Pod::Text::Color::Delight
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Pod::Text::Color::Delight
</pre>
<h2>사용 방법</h2>
<p>우선 <em>Pod::Text::Color::Delight</em> 모듈은 256색 이상을 지원하는 터미널을
사용하는 것을 전제로 색상 팔레트를 사용하고 있습니다.
따라서 여러분이 사용하는 터미널을 최소 256색 이상을 지원하도록 맞춰주어야겠죠.
자세한 내용은 여러분이 사용하는 터미널의 매뉴얼을 참고하세요.</p>
<p>데비안에서 그놈 터미널을 사용하고 있는 제 터미널의 설정은 다음과 같습니다.</p>
<pre class="brush: bash;">
$ echo $TERM
TERM=xterm
COLORTERM=gnome-terminal
</pre>
<p><code>tmux</code>를 사용할 때 터미널의 설정은 다음과 같습니다.</p>
<pre class="brush: bash;">
$ echo $TERM
TERM=screen-256color
COLORTERM=gnome-terminal
</pre>
<p>모듈 설치가 끝나면 기존 <code>perldoc</code> 명령을 실행할때 방금 설치한 모듈을 적재하도록 옵션을 명시해줍니다.</p>
<pre class="brush: bash;">
$ perldoc -MPod::Text::Color::Delight Carp
</pre>
<p>짠~!</p>
<p><img src="2013-12-06-1_r.png" alt="Pod::Text::Color::Delight를 적용한 perldoc 화면" id="pod::text::color::delightperldoc" />
<em>그림 1.</em> Pod::Text::Color::Delight를 적용한 perldoc 화면 (<a href="2013-12-06-1.png">원본</a>)</p>
<p>아! 아름답군요. ;-)</p>
<p>하지만 <em>perldoc</em>을 실행할 때 마다 항상 저렇게 길게 입력할 수는 없는 노릇이겠죠.
유닉스 계열의 운영체제를 사용한다면 자신이 사용하는 쉘의 <em>alias</em> 기능을 이용하세요.
Bash 쉘의 경우 <code>.bashrc</code> 파일 또는 그에 준하는 파일에 다음처럼 설정합니다.</p>
<pre class="brush: bash;">
alias perldoc='perldoc -MPod::Text::Color::Delight'
</pre>
<p>설정 후 새로운 터미널을 실행시켜 수정한 내용이 반영되게 하면
이제는 간단히 <code>perldoc</code> 명령만으로도 알록달록한 <em>perldoc</em> 화면을 볼 수 있습니다.</p>
<h2>정리하며...</h2>
<p>지금까지 <em>perldoc</em>에 알록달록 색깔을 입히는 방법을 알아보았습니다.</p>
<p>...?</p>
<p>...라고 하기에는 조금 아쉽죠? 조금만 더 파볼까요? ;)</p>
<h2>사용자 정의를 위한 지침서</h2>
<p><a href="https://metacpan.org/module/Pod::Text::Color::Delight">Pod::Text::Color::Delight 모듈</a>은
크게 다음 세 모듈을 사용해서 <em>perldoc</em>에 색을 칠합니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Pod::Text::Color">CPAN의 Pod::Text::Color 모듈</a></li>
<li><a href="https://metacpan.org/module/Term::ANSIColor">CPAN의 Term::ANSIColor 모듈</a></li>
<li><a href="https://metacpan.org/module/Syntax::Highlight::Perl::Improved">CPAN의 Syntax::Highlight::Perl::Improved 모듈</a></li>
</ul>
<p><code>Pod::Text::Color</code> 모듈은 펄 5.6 버전부터 이미 코어 모듈로 들어가 있는
기본적으로 제공되는 모듈입니다. 이 모듈이 대부분의 터미널에서 보여줄 수 있는
수준으로 굵게 또는 밑줄, 반전 등의 효과를 이용해 <em>perldoc</em>을 꾸며주고 있는 것입니다.
물론 이 모듈 덕분에 <em>man</em> 페이지와 거의 유사하게 문서를 읽을 수 있지만
여기서 만족 못하는 사람이 많은 덕분에 <code>Pod::Text::Color::Delight</code> 모듈이 나온 것이겠죠?
<code>Pod::Text::Color::Delight</code> 모듈은 <code>Pod::Text::Color</code> 모듈을 상속받기 때문에
문법 강조와 관련해 사용자 정의(커스터마이즈)하고 싶은 부분이 있다면
<code>Pod::Text::Color</code> 모듈과 이 모듈이 또 상속받는 <code>Pod::Text</code> 모듈을 살펴보면 됩니다.</p>
<p><code>Term::ANSIColor</code> 모듈은 터미널에 색을 간편하게 입힐 수 있게 도와줍니다.
대부분의 번거로운 작업은 이 모듈이 대신 해주기 때문에 여러분이 해야할 일은
자신의 터미널에서 지원하는 색상 목록을 확인하고 어떤 색상을 선택할지 고르는 일입니다.
역시 펄 5.6 버전부터 이미 코어 모듈로 들어가 있으므로 따로 설치할 필요가 없습니다.
다음 명령을 통해 여러분의 터미널에서 표현할 수 있는 색상 팔렛트를 확인해보죠.</p>
<pre class="brush: bash;">
$ wget http://api.metacpan.org/source/RRA/Term-ANSIColor-4.02/examples/generate-colors
$ chmod 755 generate-colors
$ ./generate-colors fg256
</pre>
<p><code>generate-colors</code>는 다음 6가지의 옵션을 제공합니다.
원하는 색상값을 고르기 위해서 각각의 옵션과 함께 실행해보고 적절한 색상을 확인해두세요.</p>
<ul>
<li>basic</li>
<li>bright</li>
<li>fg256</li>
<li>bg256</li>
<li>grey</li>
<li>ansi256</li>
</ul>
<p><code>Syntax::Highlight::Perl::Improved</code> 모듈은 <code>Pod::Text::Color::Delight</code> 모듈이 <em>perldoc</em>
내용에 색상을 입힐때 <a href="http://perldoc.perl.org/perlpod.html#Verbatim-Paragraph">글자 그대로의 문단(Verbatim Paragraph)</a>
영역을 처리하기 위해 사용합니다.
따라서 <a href="https://metacpan.org/pod/Syntax::Highlight::Perl::Improved#FORMAT-TYPES">공식 문서의 'FORMAT TYPES' 섹션</a>을
참고하면 원하는 색상을 입히는데 도움이 됩니다.</p>
<h2>크리스마스 선물 #1: 설정파일</h2>
<p><code>Pod::Text::Color::Delight</code> 모듈의 기본 설정 값은 다음과 같습니다.</p>
<pre class="brush: perl;">
use constant COLOR_TABLE => {
head1 => 'bright_cyan',
head2 => 'reset bold',
head3 => '',
head4 => '',
bold => 'reset bold',
file => 'bright_green',
italic => 'reset italic',
link => 'rgb045',
code => {
Character => 'cyan',
String => 'rgb542',
Quote => 'rgb542',
Label => 'rgb542',
Builtin_Function => 'bright_red',
Builtin_Operator => 'bright_red',
Keyword => 'bright_red',
Package => 'rgb345',
Subroutine => 'rgb454',
Bareword => 'white',
Symbol => 'white',
Operator => 'white',
Number => 'white',
Variable_Hash => 'rgb520',
Variable_Array => 'rgb520',
Variable_Scalar => 'rgb520',
Variable_Typeglob => 'rgb520',
Comment_Normal => 'grey10',
Comment_POD => 'grey10',
DATA => 'grey10',
Directive => 'bright_green',
},
};
</pre>
<p>하지만 <code>Pod::Text::Color::Delight</code> 모듈은 설정 파일을 지원하기 때문에
얼마든지 특정 키워드의 색을 원하는 색상으로 변경할 수 있습니다.
설정 파일의 경로는 <code>~/.pod_text_color_delight</code> 입니다.</p>
<p>제 경우 대부분의 경우 <a href="http://www.vim.org/">Vim</a>을 즐겨 사용하며
<em>Vim</em>의 desert256 색상 테마가 가독성이 높고 눈이 편안해 무척 좋아합니다.
여러분께 드릴 선물은 <code>Pod::Text::Color::Delight</code>의 유사 <em>desert256</em> 설정입니다. :)</p>
<pre class="brush: perl;">
#
# FILE: ~/.pod_text_color_delight
#
+{
head1 => 'rgb113 bold',
head2 => 'rgb113',
bold => 'rgb512 bold',
file => 'green',
italic => 'rgb512',
link => 'rgb530',
code => {
Character => 'rgb522',
String => 'rgb522',
Number => 'rgb522',
Quote => 'rgb522',
Label => 'rgb542 bold',
Builtin_Function => 'rgb542 bold',
Builtin_Operator => 'rgb542 bold',
Keyword => 'rgb542 bold',
Package => 'rgb220 bold',
Variable_Hash => 'rgb252',
Variable_Array => 'rgb252',
Variable_Scalar => 'rgb252',
Variable_Typeglob => 'rgb252',
Subroutine => 'rgb252',
Bareword => 'reset',
Symbol => 'reset',
Operator => 'reset',
Comment_Normal => 'rgb255',
Comment_POD => 'rgb255',
DATA => 'rgb255',
CodeTerm => 'rgb255',
Directive => 'bright_red',
},
};
</pre>
<p><img src="2013-12-06-2_r.png" alt="유사 <em>desert256</em> 테마를 적용한 perldoc 화면" id="desert256perldoc" />
<em>그림 2.</em> 유사 <em>desert256</em> 테마를 적용한 perldoc 화면 (<a href="2013-12-06-2.png">원본</a>)</p>
<p>마음에 드시나요? :)</p>
<h2>크리스마스 선물 #2: 패치</h2>
<p>현재 <code>Pod::Text::Color::Delight</code> 모듈에는 버그가 있습니다.
<a href="http://perldoc.perl.org/perlpod.html">POD 형식</a>중 목록 항목을 처리할때 특정 경우
몇몇 항목을 보여주지 못하는(지우는) 문제가 있습니다.
다행히 심각한 문제는 아니라서 수정하기 어렵지는 않습니다.</p>
<p>눈치채신 분이 계실지 모르겠는데 <code>Pod::Text::Color::Delight</code> 모듈의
설정 파일은 펄 코드입니다. 덕분에 패치 제작은 정말 간편합니다.
다음은 앞선 <em>유사 desert256</em> 색상 테마 설정 파일에 런-타임 패치를
적용한 멍키 패치(monkey-patch)입니다.</p>
<pre class="brush: perl;">
#
# FILE: ~/.pod_text_color_delight
#
sub Pod::Text::Color::Delight::cmd_item_text {
my ($self, $attrs, $text) = @_;
$self->SUPER::cmd_item_text($attrs, $text);
}
+{
head1 => 'rgb113 bold',
head2 => 'rgb113',
bold => 'rgb512 bold',
file => 'green',
italic => 'rgb512',
link => 'rgb530',
code => {
Character => 'rgb522',
String => 'rgb522',
Number => 'rgb522',
Quote => 'rgb522',
Label => 'rgb542 bold',
Builtin_Function => 'rgb542 bold',
Builtin_Operator => 'rgb542 bold',
Keyword => 'rgb542 bold',
Package => 'rgb220 bold',
Variable_Hash => 'rgb252',
Variable_Array => 'rgb252',
Variable_Scalar => 'rgb252',
Variable_Typeglob => 'rgb252',
Subroutine => 'rgb252',
Bareword => 'reset',
Symbol => 'reset',
Operator => 'reset',
Comment_Normal => 'rgb255',
Comment_POD => 'rgb255',
DATA => 'rgb255',
CodeTerm => 'rgb255',
Directive => 'bright_red',
},
};
</pre>
<p>와우! 원래 모듈의 소스 수정 없이 버그를 패치했네요.
펄에서는 흔히 사용하는 기교니 너무 놀라지는 마세요. :-)</p>
<h2>정리하며</h2>
<p>지금까지 터미널에서 <em>perldoc</em>을 알록달록하게 꾸미는 방법을 알아보았습니다.
어쩌면 웹페이지를 띄워서 보면 그만일 문제기는 하지만 터미널에 익숙한 개발자라면
<code>Ctrl-Shift-T perldoc Carp</code>를 키보드로 입력하는데 아마 1초도 걸리지 않겠죠.
이 작은 팁이 펄 개발을 더욱 즐겁게 만들어 주(었)길 바래봅니다.</p>
<p>예전에도 그랬고 지금도 여전히 <a href="http://www.cpan.org/">CPAN</a>은 개발자를 자극하는 흥미로운 것이 한 가득입니다.
항상 설치와 <em>SYNOPSIS</em>대로 따라하는 것으로 끝내지 말고 조금 더 찬찬히 문서를
읽어보고 소스를 확인해 어떤 기법을 사용했는지 참고해 여러분의 것으로 만드는 것도 잊지마세요!</p>
<p>Enjoy Your Perl! ;-)</p>
<p><img src="2013-12-06-3.png" alt="EOT" id="eot" style="margin: 0" /></p>
<p><em>EOT</em></p>
2013-12-06T00:00:00+09:00keedipcap을 이용한 네트워크 패킷 캡쳐http://advent.perl.kr/2013/2013-12-05.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/luzluna">@luzluna</a> - luz + luna</p>
<h2>시작하며</h2>
<p>네트워크상에서 작업을 하다보면 가끔씩은 정말 무엇이 어떻게 돌아가고 있는지
두 눈으로 확인하고 싶은 욕구가 (치?)밀어오를 때가 많곤합니다.
응용과 시스템 그리고 원격지 사이에서 그 누구도 잘못이 없어보일 때
범인을 찾으려면 역시 네트워크 패킷을 직접 확인하는 것보다 정확한 것은 없죠.
<a href="http://www.tcpdump.org/">tcpdump</a>나 <a href="http://www.wireshark.org/">와이어샤크</a> 같은 유명한 도구도 있지만 직접 펄 코드와 연동해서
디버깅하는 것이 훨씬 편리할 때가 많습니다.
<a href="http://www.tcpdump.org/">pcap 라이브러리</a>는 <em>tcpdump</em> 팀이 제작한 라이브러리로
<em>tcpdump</em>와 <em>와이어샤크</em>등의 패킷 캡쳐 프로그램의 핵심이라고 할 수 있습니다.
<em>pcap</em>을 사용하면 펄에서는 손쉽게 패킷 캡쳐를 펄 프로그램과 연동할 수 있습니다.</p>
<h2>준비물</h2>
<p><a href="http://www.tcpdump.org/">pcap</a> 관련 라이브러리를 컴파일하고 <a href="https://metacpan.org/module/NetPacket">NetPacket 모듈</a>에
의존성이 있는 <a href="https://metacpan.org/module/Net::Libdnet">Net::Libdnet 모듈</a>을 컴파일 하기 위해 설치해야 할 도구가 있습니다.
데비안 계열의 리눅스를 사용하고 있다면 다음 명령을 이용해서 패키지를 설치합니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install libpcap-dev libdumbnet-dev
</pre>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/Net::Pcap">CPAN의 Net::Pcap 모듈</a></li>
<li><a href="https://metacpan.org/module/NetPacket">CPAN의 NetPacket 모듈</a></li>
</ul>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan Net::Pcap NetPacket
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan Net::Pcap NetPacket
</pre>
<h2 id="netpacket">NetPacket 모듈 설치</h2>
<p>시스템에 따라 <code>NetPacket</code> 모듈 설치에 실패하는 경우도 있습니다.
<code>NetPacket</code> 모듈 설치시 실패하는 경우는 대부분 <code>Net::Libdnet</code> 모듈 설치 실패로 발생합니다.</p>
<pre class="brush: plain;">
...
/home/askdna/perl5/perlbrew/perls/perl-5.18.1/bin/perl /home/askdna/.perlbrew/libs/perl-5.18.1@advent/lib/perl5/ExtUtils/xsubpp -typemap /home/askdna/perl5/perlbrew/perls/perl-5.18.1/lib/5.18.1/ExtUtils/typemap -typemap typemap Libdnet.xs > Libdnet.xsc && mv Libdnet.xsc Libdnet.c
cc -c -I/usr/include -D_REENTRANT -D_GNU_SOURCE -fno-strict-aliasing -pipe -fstack-protector -I/usr/local/include -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -O2 -DVERSION=\"0.98\" -DXS_VERSION=\"0.98\" -fPIC "-I/home/askdna/perl5/perlbrew/perls/perl-5.18.1/lib/5.18.1/x86_64-linux-thread-multi/CORE" Libdnet.c
Libdnet.xs:37:18: fatal error: dnet.h: 그런 파일이나 디렉터리가 없습니다
#include <dnet.h>
^
compilation terminated.
make: *** [Libdnet.o] 오류 1
...
Installing /home/askdna/.perlbrew/libs/perl-5.18.1@advent/lib/perl5/x86_64-linux-thread-multi/.meta/Net-IPv6Addr-0.2/install.json
Installing /home/askdna/.perlbrew/libs/perl-5.18.1@advent/lib/perl5/x86_64-linux-thread-multi/.meta/Net-IPv6Addr-0.2/MYMETA.json
-> FAIL Installing the dependencies failed: Module 'Net::Libdnet' is not installed
-> FAIL Bailing out the installation for Net-Packet-3.27.
7 distributions installed
</pre>
<p>이는 <em>dnet 라이브러리</em>가 최근 <em>dumbnet</em>으로 이름을 변경하며 헤더 파일의 이름이 달라져 발생하는 문제입니다.
관련 시스템 개발 라이브러리를 설치했는데도 <code>dnet.h</code> 헤더 파일을 찾지 못한다면 모듈을 약간 수정해주어야 합니다.</p>
<pre class="brush: bash;">
$ wget http://cpan.metacpan.org/authors/id/G/GO/GOMOR/Net-Libdnet-0.98.tar.gz
$ tar xvzf Net-Libdnet-0.98.tar.gz
$ cd Net-Libdnet-0.98
$ perl -pi.bak -e 's/#include <dnet.h>/#include <dumbnet.h>/' Libdnet.xs
</pre>
<p>제대로 변경되었는지 확인해보죠.</p>
<pre class="brush: bash;">
$ diff -urN Libdnet.xs.bak Libdnet.xs
</pre>
<p>다음처럼 변경되었다면 성공입니다.</p>
<pre class="brush: diff;">
--- Libdnet.xs.bak 2013-12-05 10:34:56.100728050 +0900
+++ Libdnet.xs 2013-12-05 10:36:01.688559294 +0900
@@ -34,7 +34,7 @@
#include "XSUB.h"
#include <stdio.h>
-#include <dnet.h>
+#include <dumbnet.h>
#ifdef DNET_BLOB_H
typedef blob_t Blob;
</pre>
<p>빌드 및 설치를 진행합니다.
자신의 펄 환경에 따라 사용자 권한 또는 관리자 권한으로 모듈을 설치하세요.</p>
<pre class="brush: bash;">
$ perl Makefile.PL
$ make
$ make test
$ make install # 또는 sudo make install
</pre>
<h2 id="pcap">pcap과 관리자 권한</h2>
<p>다음은 패킷 캡쳐에 사용할 장치를 화면에 출력하는 간단한 프로그램입니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: pcap.pl
#
use v5.14;
use strict;
use warnings;
use Net::Pcap;
my $err = q{};
my $dev = pcap_lookupdev(\$err); # find a device
say $dev;
</pre>
<p>간단하죠? 실행해볼까요?</p>
<pre class="brush: bash;">
$ perl pcap.pl
$
</pre>
<p>어라! 명령줄에 아무 것도 출력되지 않는군요.</p>
<p><em>pcap 모듈</em>은 네트워크 장비에 직접 접근하기 때문에 <em>관리자 권한</em>이 있어야 제대로 동작합니다.
관리자 권한으로 실행해보죠.</p>
<pre class="brush: bash;">
$ sudo perl pcap.pl
eth0
$
</pre>
<p>잘 되는군요! :-)</p>
<h2>펄 경로와 관리자 권한</h2>
<p>앞서 모듈 설치시에 잠깐 언급했던 것처럼 여러분이 사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면 지금쯤이면 꽤나 혼란스러울 것입니다.
아마도 다음과 같은 오류를 출력하며 간단한 예제 프로그램이 여전히 동작하지 않을테니까요. :)</p>
<pre class="brush: bash;">
$ sudo perl pcal.pl
Can't locate Net/Pcap.pm in @INC (you may need to install the Net::Pcap module) (@INC contains: /etc/perl /usr/local/lib/perl/5.18.1 /usr/local/share/perl/5.18.1 /usr/lib/perl5 /usr/share/perl5 /usr/lib/perl/5.18 /usr/share/perl/5.18 /usr/local/lib/site_perl .) at pcap.pl line 1.
BEGIN failed--compilation aborted at pcap.pl line 1.
$
</pre>
<p>놀라지 마세요. 모든 문제와 문제의 해답은 경고나 오류 메시지에 담겨져 있습니다.
관리자 권한으로 실행하기 위해 관리자 계정으로 로그인 하거나 <code>sudo</code>를 이용해서
프로그램을 실행하는 순간 지금까지 <em>사용하던 펄</em>이 아닌 <em>관리자 계정의 펄</em>을 사용하게 되기 때문입니다.
여러분이 시스템의 펄과는 별개의 펄을 사용하고 있었다면 당연히 모듈도 별개의 펄을 위해 설치했을 것입니다.
어쩌면 필요한 모듈을 찾지 못하는 것은 당연한 결과죠.
지금까지 <em>사용하던 펄</em>이 아닌 <em>관리자 계정의 펄</em>이 어떻게 다른지 직접 확인해보죠.</p>
<pre class="brush: bash;">
$ which perl
/home/user/perl5/perlbrew/perls/perl-5.18/bin/perl
$ sudo which perl
/usr/bin/perl
$
</pre>
<p>문제를 해결하기 위한 몇 가지 방법이 있는데 가장 간단한 방법부터 가장 정확한 방법까지 하나씩 짚어보죠.</p>
<h3>관리자 권한으로 로그인, 펄 모듈 설치, 프로그램 실행</h3>
<p><em>DON'T DO THAT!</em></p>
<p>이 방법은 쉽고 간단해서 마약과 같습니다.
부득이한 경우가 아니라면 추천하지 않습니다.</p>
<h3 id="sudo"><code>sudo</code>로 실행시 펄의 절대 경로 지정</h3>
<p><code>sudo</code>로 실행 시 두 번째 인자로 <code>perl</code>을 입력하죠.</p>
<pre class="brush: bash;">
$ sudo perl pcal.pl
</pre>
<p>이 부분에 착안을 하면 관리자 권한으로 실행을 하되 자신의 계정에 설치했던 펄을
사용할 수 있도록 전체 경로를 지정하면 설치했던 모듈을 사용하는 데 지장이 없을 것입니다.</p>
<pre class="brush: bash;">
$ sudo /home/user/perl5/perlbrew/perls/perl-5.18/bin/perl pcap.pl
eth0
$
</pre>
<p>잘 되죠?</p>
<h3>쉬뱅라인에 펄의 절대 경로 지정</h3>
<p>그런데 매번 실행할때마다 저 복잡한 라인을 다 쓰려면 꽤나 번거롭습니다.
유닉스 계열에서 스크립트는 대부분 쉬뱅 라인을 지정합니다.
실행 권한이 있는 스크립트는 파일의 가장 첫 줄인 쉬뱅 라인을 읽어
자신을 실행시킬 프로그램을 찾습니다.
보통 확장성 있는 펄 프로그램을 작성하기 위해 쉬뱅 라인은 <code>#!/usr/bin/env perl</code>로
작성하는 것을 선호하지만 이 경우 펄의 정확한 절대 경로로 설정할 경우
<code>sudo</code>로 실행한다 하더라도 원하는 펄로 스크립트를 구동할 수 있습니다.</p>
<p>쉬뱅 라인을 바꿔보죠.</p>
<pre class="brush: perl;">
#!/home/user/perl5/perlbrew/perls/perl-5.18/bin/perl
#
# FILE: pcap.pl
#
use v5.14;
use strict;
use warnings;
use Net::Pcap;
my $err = q{};
my $dev = pcap_lookupdev(\$err); # find a device
say $dev;
</pre>
<p>그리고 스크립트에 실행 권한을 줍니다.</p>
<pre class="brush: bash;">
$ chmod +x pcap.pl
</pre>
<p>실행해볼까요?</p>
<pre class="brush: bash;">
$ sudo ./pcap.pl
eth0
$
</pre>
<p>역시 잘 동작합니다. :)</p>
<h3 id="pcap">pcap을 사용할 펄에 적절한 권한 추가</h3>
<p>이번 방법은 조금 어렵지만 리눅스를 사용한다면 제일 확실한 방법입니다.
단 시스템과 파일의 권한과 pcap에 대해서 어느 정도 이해할(하려 노력할) 필요가 있습니다. :)</p>
<p>모든 문제의 시작은 pcap 라이브러리가 네트워크 장치에 접근할 때 <em>관리자 권한</em>이 필요하기 때문에 발생합니다.
더 정확히 표현하면 <em>관리자 권한</em>은 모든 장치에 접근할 수 있기 때문에 관리자 권한을 획득해서
프로그램을 실행하는 것은 종종 가장 간단한 해결책이며 유일한 해결책이기도 합니다.
다행히 pcap은 선택의 여지가 있는데 pcap을 사용할 바이너리에 적절한 권한을 부여한다면
사용자 권한이 없이도 pcap 관련 도구를 사용할 수 있습니다.</p>
<p>현재 사용하는 펄의 경로를 우선 확인합니다.</p>
<pre class="brush: bash;">
$ which perl
/home/user/perl5/perlbrew/perls/perl-5.18/bin/perl
</pre>
<p>상기 경로의 펄에 적절한 권한을 추가합니다.</p>
<pre class="brush: bash;">
$ sudo setcap cap_net_raw,cap_net_admin=eip /home/user/perl5/perlbrew/perls/perl-5.18/bin/perl
</pre>
<p>권한이 제대로 추가되었는지 확인합니다.</p>
<pre class="brush: bash;">
$ sudo getcap /home/user/perl5/perlbrew/perls/perl-5.18/bin/perl
/home/user/perl5/perlbrew/perls/perl-5.18/bin/perl = cap_net_admin,cap_net_raw+eip
</pre>
<p><code>setcap</code>과 <code>getcap</code> 유틸리티가 없을 경우 데비안 계열의 리눅스를 사용하고 있다면
다음 명령을 이용해서 패키지를 설치합니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install libcap2-bin
</pre>
<p>파일 관련 권한 기능이 제대로 동작하려면 커널에 capability 기능이 포함되어 있어야 합니다.
리눅스 커널 2.6.33 이후의 경우 커널에 포함되어 있으며 이전 커널의 경우
<code>CONFIG_SECURITY_FILE_CAPABILITIES</code> 설정을 활성화 시키고 빌드되어 있어야 합니다.</p>
<p>해당 기능을 활성화하기 위해 부팅 커널의 매개변수 옵션에 <code>file_caps=1</code>을 추가합니다.
데비안의 경우 <code>/etc/default/grub</code> 파일에서 <code>GRUB_CMDLINE_LINUX_DEFAULT</code> 값을 조정합니다.</p>
<pre class="brush: diff;">
--- a/etc/default/grub
--- b/etc/default/grub
-GRUB_CMDLINE_LINUX_DEFAULT="quiet"
+GRUB_CMDLINE_LINUX_DEFAULT="quiet file_caps=1"
</pre>
<p>수정 후 다음 명령을 실행해서 <em>grub</em> 설정을 갱신합니다.</p>
<pre class="brush: bash;">
$ sudo update-grub
</pre>
<p>커널 매개변수 옵션을 변경했다면 재부팅 후 다시 프로그램을 실행합니다.</p>
<pre class="brush: bash;">
$ ./pcap.pl
eth0
$
</pre>
<p>관리자 권한이 없이도 pcap 관련 함수가 제대로 호출되어 결과를 반환합니다.
권한과 관련한 자세한 내용은 <code>man capabilities</code> 문서를 참조하세요. :)</p>
<h2>실제 패킷 캡쳐해보기</h2>
<p>먼 길을 돌아 <code>Net::Pcap</code> 모듈을 사용할 준비가 되었으니 이제 패킷을 캡쳐해봐야겠죠?</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: pcap.pl
#
use v5.14;
use strict;
use warnings;
use Net::Pcap;
my $err = q{};
my $dev = pcap_lookupdev(\$err); # find a device
say $dev;
my $pcap = pcap_open_live( $dev, 1024, 1, 0, \$err );
pcap_loop($pcap, 3, \&process_packet, "just for the demo");
pcap_close($pcap);
sub process_packet {
my ( $user_data, $header, $packet ) = @_;
printf "%d/%d %d\n",$header->{caplen}, $header->{len}, length($packet);
}
</pre>
<p>앞의 예제는 패킷을 3번 캡쳐한 후 캡쳐한 자료의 길이, 헤더에 기록된 길이, 그리고 캡쳐된 데이터의 실제 길이를 출력하는 예제입니다.
실행해 볼까요?</p>
<pre class="brush: bash;">
$ ./pcap.pl
eth0
66/66 66
1024/1514 1024
60/60 60
$
</pre>
<h2>필터 기능</h2>
<p>너무 많은 자료는 사실 없는 것이나 마찬가지입니다.
원하는 패킷 정보만 추려내려면 필터 기능을 이용하면 됩니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: pcap.pl
#
use v5.14;
use strict;
use warnings;
use Net::Pcap;
my $err = q{};
my $dev = pcap_lookupdev(\$err); # find a device
say $dev;
my $pcap = pcap_open_live( $dev, 1024, 1, 0, \$err );
my $filter;
my $filter_str = "host advent.perl.kr";
pcap_compile( $pcap, \$filter, $filter_str, 1, 0 );
pcap_setfilter( $pcap, $filter );
pcap_loop($pcap, 10, \&process_packet, "just for the demo");
pcap_close($pcap);
sub process_packet {
my ( $user_data, $header, $packet ) = @_;
printf "%d/%d %d\n",$header->{caplen}, $header->{len}, length($packet);
}
</pre>
<p>새로워진 예제의 핵심은 다음 두 줄입니다.</p>
<pre class="brush: perl;">
pcap_compile($pcap, \$filter, $filter_str, 1, 0);
pcap_setfilter($pcap, $filter);
</pre>
<p>먼저 필터를 컴파일(<code>pcap_compile</code>)한 뒤 필터를 적용(<code>pcap_setfilter</code>)합니다.
예제의 <code>host advent.perl.kr</code> 필터는 <code>advent.perl.kr</code> 호스트와 통신하는 패킷을 캡쳐하라는 의미입니다.</p>
<p>필터를 제대로 사용하려면 필터 문법을 제대로 알아야 합니다.
다음은 유용하게 쓰일만한 필터 몇 가지 입니다.</p>
<pre class="brush: plain;">
host advent.perl.kr # advent.perl.kr 과 통신하는 모든 패킷
dst host advent.perl.kr # destination 이 advent.perl.kr 인 패킷
src host advent.perl.kr # source 가 advent.perl.kr 인 패킷
port 80 # port가 80인 패킷
dst port 80 # destination port 가 80인 패킷
src port 80 # source port 가 80인 패킷
len <= 10 # 10 바이트 이하인 패킷
len >= 10 # 10 바이트 이상인 패킷
</pre>
<p><code>and</code>, <code>or</code>, <code>()</code>을 조합하여 다양하게 변형이 가능합니다.</p>
<pre class="brush: plain;">
#
# ip 패킷이면서 192.168.7.0/24대역과 통신하지 않는 패킷
#
ip and not net 192.168.7.0/24
#
# snup 게이트웨이를 통과하면서 ftp 포트를 사용하는 패킷
#
gateway snup and (port ftp or ftp-data)
</pre>
<p>좀 더 복잡한 조합으로 사용하는 것 역시 가능합니다.</p>
<pre class="brush: plain;">
#
# http 접속중 SYN패킷만 모아서 보기
#
tcp[tcpflags] = tcp-syn and port http
</pre>
<p>더 자세한 필터 문법은 <a href="http://www.tcpdump.org/#documentation">tcpdump 공식 문서</a>를 참조하세요.</p>
<h2>패킷 분석</h2>
<p><a href="https://metacpan.org/module/NetPacket">CPAN의 NetPacket 모듈</a>은 패킷 분석을 손쉽게 할 수 있게 도와줍니다.
패킷 분석 기능을 추가한 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: pcap.pl
#
use v5.14;
use strict;
use warnings;
use Net::Pcap;
use NetPacket::Ethernet qw(:strip);
use NetPacket::IP qw(:strip);
use NetPacket::TCP;
my $err = q{};
my $dev = pcap_lookupdev(\$err);
say $dev;
$dev = 'wlan0';
my $pcap = pcap_open_live( $dev, 1024, 1, 0, \$err );
my $filter;
my $filter_str = "host advent.perl.kr";
pcap_compile( $pcap, \$filter, $filter_str, 1, 0 );
pcap_setfilter( $pcap, $filter );
pcap_loop($pcap, 10, \&process_packet, "just for the demo");
pcap_close($pcap);
sub process_packet {
my ( $user_data, $header, $packet ) = @_;
my $ip_obj = NetPacket::IP->decode( eth_strip($packet) );
my $tcp_obj = NetPacket::TCP->decode( $ip_obj->{data} );
printf(
"%s:%d->%s:%d (%d) ",
$ip_obj->{src_ip}, $tcp_obj->{src_port},
$ip_obj->{dest_ip}, $tcp_obj->{dest_port},
length( $tcp_obj->{data} )
);
print "FIN " if ( $tcp_obj->{flags} & FIN );
print "SYN " if ( $tcp_obj->{flags} & SYN );
print "RST " if ( $tcp_obj->{flags} & RST );
print "PSH " if ( $tcp_obj->{flags} & PSH );
print "ACK " if ( $tcp_obj->{flags} & ACK );
print "URG " if ( $tcp_obj->{flags} & URG );
print "ECE " if ( $tcp_obj->{flags} & ECE );
print "CWR " if ( $tcp_obj->{flags} & CWR );
print "\n";
if ( length( $tcp_obj->{data} ) > 0 ) {
print $tcp_obj->{data}, "\n";
print "============\n";
}
}
</pre>
<p>실행 후 <code>advent.perl.kr</code> 호스트에 웹브라우저나 <code>curl</code>, <code>wget</code> 등으로 접속하면 다음과 같은 결과를 화면에 출력합니다.</p>
<pre class="brush: bash;">
$ perl pcap.pl
eth0
221.143.48.32:80->192.168.25.37:45902 (0) SYN ACK
192.168.25.37:45902->221.143.48.32:80 (0) ACK
192.168.25.37:45902->221.143.48.32:80 (685) PSH ACK
GET /2013/2013-12-04.html HTTP/1.1
Host: advent.perl.kr
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://advent.perl.kr/2013/
Cookie: __utma=97638351.475138252.1385891323.1386204394.1386208284.13; __utmz=97638351.1385891323.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _fjtads1=443; _fjtad6=3:4T2T5T6T1T3; _fjpvnum1=4; _fjpermvid1=1386195821039-8972276113869514; __utmc=97638351; __utmb=97638351.1.10.1386208284
Connection: keep-alive
If-Modified-Since: Tue, 03 Dec 2013 20:57:50 GMT
============
221.143.48.32:80->192.168.25.37:45902 (0) ACK
221.143.48.32:80->192.168.25.37:45902 (157) PSH ACK
HTTP/1.1 304 Not Modified
Server: nginx/1.2.4
Date: Thu, 05 Dec 2013 02:11:56 GMT
Connection: keep-alive
Last-Modified: Tue, 03 Dec 2013 20:57:50 GMT
============
192.168.25.37:45902->221.143.48.32:80 (0) ACK
192.168.25.37:45905->221.143.48.32:80 (0) SYN
192.168.25.37:45906->221.143.48.32:80 (0) SYN
192.168.25.37:45907->221.143.48.32:80 (0) SYN
192.168.25.37:45908->221.143.48.32:80 (0) SYN
</pre>
<h2>정리하며</h2>
<p>대부분의 패킷 캡쳐와 관련한 기능은 궁극의 <a href="http://www.tcpdump.org/">pcap 라이브러리</a>가 다 처리합니다.
펄에서는 이 <em>pcap 라이브러리</em>를 바인딩한 <a href="https://metacpan.org/module/Net::Pcap">Net::Pcap 모듈</a>이
<a href="http://www.cpan.org/">CPAN</a>을 통해 배포되고 있어 간단하게 <em>pcap</em>을 사용할 수 있습니다.
성능 역시 <em>pcap 라이브러리</em>에 의존하기 때문에 C로 작성하는 것과 비교해 별반 차이가 없죠.
아마 여러분에게 닥친 상황이 복잡하면 복잡할수록 여러분은 다른 패킷 캡쳐 도구보다 <code>Net::Pcap</code>을 더 선호하게 될 겁니다. :-)</p>
2013-12-05T00:00:00+09:00luzlunaEmacs에서의 Perl 개발 환경http://advent.perl.kr/2013/2013-12-04.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/aanoaa">@aanoaa</a> - 홍형석, 사당동 펠프스, <a href="https://github.com/aanoaa">github:aanoaa</a></p>
<h2>시작하며</h2>
<p><a href="http://www.gnu.org/software/emacs/">Emacs</a>에 펄 개발 환경을 설정하면 문서를 보기에도 좋고, 코딩하기에도 편리합니다.
Emacs를 사용해서 펄로 개발할때 생산성을 높여주는 몇 가지 유용한 팁을 소개합니다.</p>
<h2 id="perlbrewplenv">perlbrew / plenv</h2>
<p><a href="http://perlbrew.pl/">perlbrew</a>, <a href="https://github.com/tokuhirom/plenv">plenv</a> 등의 펄 설치 및 바이너리
관리 도구(Perl installation/binary management tool)을 사용한다면 자신의
쉘 설정과 Emacs의 환경 변수를 맞춰주어야 합니다.</p>
<ul>
<li><a href="https://github.com/dams/perlbrew-mini.el">perlbrew-mini.el</a></li>
<li><a href="https://github.com/karupanerura/plenv.el">plenv.el</a></li>
</ul>
<p><em>plenv</em>를 사용한다면 다음 코드가 도움이 될 것입니다.</p>
<pre class="brush: lisp;">
; in your .emacs
(setenv "PATH" (concat (getenv "HOME") "/.plenv/shims:" (getenv "HOME") "/.plenv/bin:" (getenv "PATH")))
(setq exec-path (cons (concat (getenv "HOME") "/.plenv/shims") (cons (concat (getenv "HOME") "/.plenv/bin") exec-path)))
(require 'plenv)
(plenv-global "5.18.1")
</pre>
<h2 id="perl5lib"><code>PERL5LIB</code> 환경 변수</h2>
<p>보통 작업을 하다보면 현재 디렉터리에 존재하는 <code>lib</code> 디렉터리에 펄 모듈을 둘 때가 많습니다.
<code>PERL5LIB</code> 환경 변수에 <code>./lib</code>을 추가하면 실행시 마다 <code>-I</code> 옵션을 주거나 환경 변수를
재정의해야 할 필요가 없기 때문에 유용합니다.</p>
<pre class="brush: lisp;">
(setenv "PERL5LIB" (concat "./lib:" (getenv "PERL5LIB")))
</pre>
<h2 id="cperl-mode"><code>cperl-mode</code></h2>
<p><code>perl-mode</code> 대신에 항상 <code>cperl-mode</code> 를 사용합니다.
Emacs가 파일 확장자에 따라 알아서 <code>cperl-mode</code>로 동작하도록 설정하면 편리합니다.</p>
<pre class="brush: lisp;">
(defalias 'perl-mode 'cperl-mode)
(add-to-list 'auto-mode-alist '("\\.\\([pP][Llm]\\||psgi\\|t\\)\\'" . cperl-mode))
</pre>
<p><code><C-j></code>(newline-and-indent)로 개행을 하는 습관을 가지면 줄바꿈 후에 적절한 들여쓰기가 됩니다.</p>
<p><code><M-x> cperl-perldoc</code>을 사용하면 펄의 내장 함수부터 확장 모듈의 문서까지 손쉽게 볼 수 있습니다.
화면을 분할해서 문서를 띄워놓고 작업할때 매우 유용합니다.
저는 자주 사용하는 기능이기 때문에 단축키 <code><C-h P></code>로 맵핑해 사용하고 있습니다.</p>
<h2 id="flymake">flymake 문법 검사</h2>
<p><a href="https://github.com/illusori/emacs-flymake">flymake</a>는 Emacs의 문법 검사기입니다.
버퍼에 구문 오류가 있다면 해당 부분을 강조해주므로 오타와 같은 오류를 줄일 수 있습니다.</p>
<p><img src="2013-12-04-1_r.png" alt="perldoc과 flymake" id="perldocflymake" />
<em>그림 1.</em> perldoc과 flymake (<a href="2013-12-04-1.png">원본</a>)</p>
<p><em>perlbrew</em> 사용자라면 <a href="https://metacpan.org/module/Project::Libs">CPAN의 Project::Libs 모듈</a>이 추가로 필요합니다.</p>
<pre class="brush: lisp;">
(add-hook 'cperl-mode-hook (lambda () (flymake-mode t)))
;; for plenv user
(defun flymake-perl-init ()
(let* ((temp-file (flymake-init-create-temp-buffer-copy
'flymake-create-temp-with-folder-structure))
(local-file (file-relative-name
temp-file
(file-name-directory buffer-file-name))))
(list (guess-plenv-perl-path) (list "-wc" local-file))))
;; for perlbrew user
(defun flymake-perl-init ()
(let* ((temp-file (flymake-init-create-temp-buffer-copy
'flymake-create-temp-inplace))
(local-file (file-relative-name
temp-file
(file-name-directory buffer-file-name))))
(list (perlbrew-mini-get-current-perl-path)
(list "-MProject::Libs" "-wc" local-file))))
</pre>
<h2>정리하며</h2>
<p><code>cperl-mode</code>와 <a href="https://github.com/illusori/emacs-flymake">flymake</a>, <code>perldoc</code> 정도의
도움만 받아도 꽤나 편리한 환경에서 펄 프로그램을 개발할 수 있습니다.
그 밖에 편리한 Emacs 팁이 있다면 공유해주세요.
(고수는 노트패드만 있어도 코딩한다던데... :)</p>
<p><a href="http://twitter.com/#!/am0c">@am0c</a>군은 하늘에서도 여전히 Emacs로 코딩하고 있겠죠? :)</p>
<p>'-']/</p>
2013-12-04T00:00:00+09:00aanoaa좋은 모듈을 고르는 좋은 방법http://advent.perl.kr/2013/2013-12-03.html<h2>저자</h2>
<p><a href="http://twitter.com/#!/JEEN_LEE">@JEEN_LEE</a> - 세작, 사쿠라, 쁘락치, <a href="https://github.com/jeen">github:jeen</a></p>
<h2>시작하며</h2>
<p><a href="http://stats.cpantesters.org/">CPAN Testers Statistics</a>에서 확인할 수 있는 것처럼 현재 CPAN 에 등록된 모듈은 3만여개에 가깝습니다.
많은 모듈이 있다는 것은 좋은 것입니다. 하지만 모든 모듈이 다 좋은 것은 아닙니다.
<em>A</em>라는 일을 하기위한 모듈이 수십여개인 경우 도대체 무엇을 골라야 할지 난감하기 마련입니다.
그럼 우리는 어떻게 좋은 모듈을 골라야 할까요? </p>
<h2>좋은 모듈이란?</h2>
<p>그러면 좋은 모듈이란 도대체 무엇일까요?</p>
<ul>
<li>보다 빠르게</li>
<li>보다 유연하게</li>
<li>보다 쉽게</li>
<li>잘 문서화 된</li>
<li>삽질을 덜 하게 해줄</li>
<li>잘 유지보수되고 있는</li>
</ul>
<p>제가 생각하는 몇가지를 나열해봤습니다만, 이 모든 것들을 충족시켜주는 것들은 손에 꼽을 정도가 아닐까요?
아니, 뭐 더 많을 수도 있구요. 좋은 모듈이라는 것은 결국은 개인 경험에 따른 주관이 개입하기 마련입니다.
그럼 100% 제 주관에 근거한 좋은 모듈을 찾는 방법을 알아보도록 하겠습니다.</p>
<h2 id="top100leaderboardmetacpanhome-metacpan-leaderboard">1. <a href="http://metacpan.org/favorite/leaderboard">Top 100 ++ Leaderboard @ MetaCPAN</a></h2>
<p>주관을 뒤로하고 객관적인 지표가 될 수 있는 것은 바로 다른 사람들의 어떤 모듈에 대한 평가입니다.
현재 CPAN 에서 가장 높은 평가를 받고 있는 모듈은 그럼 무엇일까요?
2013년 12월 3일을 기준으로 다음과 같습니다.</p>
<ol>
<li><a href="https://metacpan.org/module/Mojolicious">Mojolicious</a> (170++)</li>
<li><a href="https://metacpan.org/module/Moose">Moose</a> (168++)</li>
<li><a href="https://metacpan.org/module/perl">perl</a> (153++)</li>
<li><a href="https://metacpan.org/module/DBIx::Class">DBIx-Class</a> (143++)</li>
<li><a href="https://metacpan.org/module/App::cpanminus">App-cpanminus</a> (142++)</li>
<li><a href="https://metacpan.org/module/DBI">DBI</a> (129++)</li>
<li><a href="https://metacpan.org/module/Plack">Plack</a> (127++)</li>
<li><a href="https://metacpan.org/module/Moo">Moo</a> (118++)</li>
<li><a href="https://metacpan.org/module/App::perlbrew">App-perlbrew</a> (107++)</li>
<li><a href="https://metacpan.org/module/DateTime">DateTime</a> (103++)</li>
</ol>
<p>Top 100중 10위까지의 결과입니다.
1위는 요즘 인기를 누리고 있고, 저 역시 절찬리에 애용하고 있는 <a href="http://mojolicio.us/">Mojolicious</a> 입니다.
2위는 Meta Object Protocol 구현체인 <a href="http://moose.iinteractive.com/en/">Moose</a>, 3위는 두 말할 것도 없는 <a href="http://www.perl.org/">Perl</a>,
4위는 대표적인 ORM인 <a href="https://metacpan.org/module/DBIx::Class">DBIx::Class</a>입니다.</p>
<p>적어도 다른 사람들이 높게 평가하는 모듈을 사용하면 큰 지장은 없습니다.</p>
<h2 id="metacpan">2. MetaCPAN</h2>
<p>MetaCPAN 홈페이지에는 몇 가지 참고할 만한 사항들이 있습니다.</p>
<p><img src="2013-12-03-1_r.png" alt="Moo - MetaCPAN" id="moo-metacpan" />
<em>그림 1.</em> Moo - MetaCPAN (<a href="2013-12-03-1.png">원본</a>)</p>
<p><em>그림 1</em>의 화면은 MetaCPAN의 <code>Moo</code> 모듈의 페이지입니다.
여기서 다음 항목을 살펴볼까요.</p>
<ul>
<li><code>++</code> 수 (118++)</li>
<li>최근 릴리즈 (Sep 10, 2013)</li>
<li>Bugs(7)</li>
<li>Reviews</li>
<li>Repository</li>
<li>Test results</li>
<li>License</li>
</ul>
<p>이전에 언급한 대로 <code>++</code> 수는 참고할만한 가장 일반적인 수치입니다.</p>
<p><em>최근 릴리즈 날짜</em> 또한 좋은 선택기준이 됩니다. 가장 최근까지 이 모듈이 유지보수되고 있다는 것을 의미해주죠.</p>
<p><em>Bugs</em> 또한 살펴봐야 할 부분입니다. 경우에 따라서는 실행환경에 의존되는 내용이 주를 이루는 경우가 있어, 관련 내용도 유심히 살펴볼 필요가 있습니다.</p>
<p><em>Reviews</em>의 경우는 굉장히 주관적인 평이 주를 이루기 때문에 이 내용들 또한 잘 살펴봐야 됩니다.</p>
<p><em>Repository</em>는 GitHub 같은 공개된 소스 저장소를 통해 모듈이 잘 관리되고 있는지를 확인할 수 있습니다.
GitHub 같은 저장소 링크가 있다면 필요한 기능이나 패치를 직접 구현한 후 Pull Request를 손쉽게 보낼 수 있겠죠.</p>
<p><em>Test Results</em>도 매우 중요합니다. 테스트 케이스가 많다면 어느 정도의 신뢰성을 보장해주기 마련입니다.
더불어 모듈의 <em>SYNOPSIS</em>를 봐도 이해하기 어려운 모듈의 사용법은 테스트 코드를 예제 삼아 사용법을 확인할 수 있습니다.</p>
<p><em>License</em>는 <a href="http://dev.perl.org/licenses/">펄 라이센스</a>를 따르는 경우가 대부분이지만
경우에 따라 독자적인 라이센스를 제시하고 있는 경우가 있으니 항상 확인하는 것이 좋습니다.</p>
<h2 id="cpanhome-cpantesters-deps">3. <a href="http://deps.cpantesters.org/?module=Moo">CPAN 의존성</a></h2>
<p><a href="http://metacpan.org/favorite/leaderboard">Top 100 Leaderboard</a>에서 확인할 수 있는 <em>좋은 모듈</em> 또한 의존 모듈을 가집니다.
그런 <em>좋은 모듈</em>의 의존모듈은 기본적인 안정성을 보장하기 때문에 이 의존 관계에
자주 등장하는 모듈이라면 이 또한 분명 <em>좋은 모듈</em>이라고 할 수 있을 것입니다.</p>
<p><img src="2013-12-03-2_r.png" alt="Moo - MetaCPAN" id="moo-metacpan" />
<em>그림 2.</em> CPAN 의존성 - Moo (<a href="2013-12-03-2.png">원본</a>)</p>
<p>거꾸로 역 의존성(reverse dependency)을 확인해볼 수 있습니다.
<em>이 모듈을 얼마나 많은 모듈이 사용하고 있냐?</em>라는 것이 하나의 지표가 되겠죠?
<a href="https://metacpan.org/requires/distribution/Moo?sort=[[2,1]]">Moo 역 의존성</a> 링크에서 볼 수 있듯이, 수백개가 넘는 모듈이 <code>Moo</code>를 기반으로 만들어졌습니다.
대개 중추적인 기능을 하는 모듈은 이 역 의존성 페이지를 통해 확인할 수 있습니다.
<code>Moo</code>와 같은 OOP 구현체 또는 각종 파서, 각종 클라이언트, 로거 등의 범용적인 모듈을 역 의존성 페이지를 이용해 확인하면 좋습니다. </p>
<h2 id="task::belike::.search-cpan-task-belike">4. <a href="https://metacpan.org/search?q=Task%3A%3ABeLike">Task::BeLike::.+</a></h2>
<p><code>Task::BeLike::</code> 이름 공간으로 시작하는 모듈은 나름 스스로 잘나간다고 생각하는 CPAN Author마다 하나씩 가지고 있는 모듈입니다.
이런 모듈은 어떤 일들을 처리하기 위해서 어떤 모듈들을 사용하는 지에 대해서 나열합니다.
저자별로 수십가지 <code>Task::BeLike::</code> 모듈이 있으니 나름 이름있는 CPAN Author의 <code>Task::BeLike::</code> 페이지를 확인해보세요.</p>
<ul>
<li>RJBS <a href="https://metacpan.org/module/Task::BeLike::RJBS">Task::BeLike::RJBS</a></li>
<li>TOKUHIROM <a href="https://metacpan.org/module/Task::BeLike::TOKUHIROM">Task::BeLike::TOKUHIROM</a></li>
</ul>
<h2 id="module::advisorcpan-module-advisor">5. <a href="https://metacpan.org/pod/Module::Advisor">Module::Advisor</a></h2>
<p>모듈을 사용하다 보면 <em>이 모듈을 쓰면 문제가 생기지는 않을까?</em>하고 종종 걱정하기도 합니다.</p>
<blockquote>
<p>혹시나 어떤 문제가 있지는 않을까?
이것보다 더 좋은 것이 있지는 않을까?</p>
</blockquote>
<p>그럴때는 <a href="https://metacpan.org/pod/Module::Advisor">Module::Advisor</a>를 사용해보세요.
<code>Module::Advisor</code>를 사용하면 모듈의 특정 버젼이 가진 보안 이슈, 퍼포먼스 이슈, 버그를 확인할 수 있으며
코드안에서 특정 모듈을 사용할 경우 그것보다 더 나은 선택지가 있는지를 알려줍니다.</p>
<h2>6. 다른 사람에게 물어보기</h2>
<p><a href="http://webchat.freenode.net/?channels=perl-kr">프리노드 IRC 서버의 #perl-kr 채널</a>에는 당장 무엇을 하려할때
가장 적합한 모듈이 무엇인지 알려줄 수 있는 사람들이 있습니다.
IRC 에 들어오셔서 사람들과 호흡해보세요. :-)</p>
<p><a href="http://stackoverflow.com/">스택오버플로우</a> 역시나 큰 도움이 됩니다.
적절한 키워드를 사용해서 <em>스택오버플로우</em>를 탐험해보세요.</p>
<h2>정리하며</h2>
<p><em>좋다</em>라는 것은 극히 주관적인 감정의 표현입니다.
다른 사람들에게는 소중하고 아름다운 모듈이라도 내가 쓰기에 좋지 아니한 경우라면 어쩔 수가 없는 노릇입니다.
특히 HTTP 클라이언트 구현체가 그렇습니다.
정말로 많은 사람들이 저마다의 모듈을 내보이니, 두 손 두 발 다 써도 셀 수 없을 지경입니다.
그 중에 몇몇 모듈은 저마다의 목적에 맞게 널리 사용되고 있습니다.
대표적으로 <a href="https://metacpan.org/module/LWP::UserAgent">LWP::UserAgent</a>, <a href="https://metacpan.org/module/HTTP::Tiny">HTTP::Tiny</a>, <a href="https://metacpan.org/module/Furl">Furl</a>,
<a href="https://metacpan.org/module/Mojo::UserAgent">Mojo::UserAgent</a>, <a href="https://metacpan.org/module/LWP::Simple">LWP::Simple</a> 등이 있습니다.
결국은 꾸준하게 사용하면 <em>좋은 것</em>을 찾아내고 그러면서 <em>더 좋은 것</em>을 선택하게 되지 않을까요?</p>
2013-12-03T00:00:00+09:00JEEN_LEE유니코드 인코딩/디코딩http://advent.perl.kr/2013/2013-12-02.html<h2>저자</h2>
<p><a href="http://twitter.com/newbcode">@newbcode</a> - 사랑스러운 딸 바보 도치파파.
<a href="http://www.yes24.com/24/goods/11040637">리눅스의 모든것</a> 공동저자.</p>
<h2>시작하며</h2>
<p><a href="http://ko.wikipedia.org/wiki/%EB%B6%80%ED%98%B8%ED%99%94">위키백과</a>를 따르면 인코딩(encoding)은 정보의 형태나 형식을
표준화, 보안, 처리 속도 향상, 저장 공간 절약 등을 위해서 다른 형태나 형식으로
변환하는 처리 혹은 그 처리 방식을 말한다고 합니다.
인코딩/디코딩은 다루는 자료의 종류에 따라 의미하는 바가 조금씩 다르지만
지금은 유니코드 인코딩/디코딩으로 한정합니다.</p>
<pre class="brush: plain;">
I/O 스트림
+--------+ +-------------+ +------+
| | 표준 입력 | | 표준 출력 | |
| 키보드 +------------->| 펄 프로그램 |+------------>| 화면 |
| | | | | |
+--------+ +-------------+ +------+
</pre>
<p>앞의 그림은 가장 일반적인 입출력 스트림(input/output stream)을 보여줍니다.
입출력 스트림은 <em>바이트 스트림(byte stream)</em>과 <em>문자 스트림(character stream)</em> 두 종류로 나눌 수 있습니다.
입력 스트림을 통해 전달되는 자료는 프로그램, 즉 펄 프로그램을 만나기 전까지는 바이트 스트림으로 존재합니다.
펄 프로그램에서 바이트 스트림을 어떻게 처리하느냐가 펄에서의 인코딩/디코딩의 핵심입니다.
실제 웹 상에 있는 HTML 문서를 이용해서 직접 인코딩/디코딩을 살펴봅니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="http://metacpan.org/module/Encode">CPAN의 Encode 모듈</a></li>
<li><a href="http://metacpan.org/module/HTTP::Tiny">CPAN의 HTTP::Tiny 모듈</a></li>
<li><a href="http://metacpan.org/module/Data::Printer">CPAN의 Data::Printer 모듈</a></li>
</ul>
<p><code>Encode</code> 모듈의 경우 펄 코어 모듈이므로 별도로 설치할 필요는 없습니다.
<code>HTTP::Tiny</code>의 경우 펄 5.14 이후 버전(정확히는 v5.13.9)의 경우
코어 모듈로 지정되었으므로 역시 별도로 설치할 필요는 없습니다.
하지만 <code>HTTP::Tiny</code>는 지속적으로 버전이 갱신되고 있으므로 가능하면 최신 버전으로 설치하도록 합니다.
<code>Data::Printer</code> 모듈은 디버깅의 편의를 위해 필요한 모듈입니다.
<code>print</code>나 <code>say</code>로도 충분하지만 이런 도구를 잘 활용한다면 자료를 시각적으로
확인할 수 있기 때문에 문제가 생겼을때 빠른 시간에 문제를 해결할 수 있도록 도와줍니다.</p>
<p>직접 <a href="http://www.cpan.org/">CPAN</a>을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ sudo cpan HTTP::Tiny Data::Printer
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan HTTP::Tiny Data::Printer
</pre>
<h2>오늘의 날씨</h2>
<p>현실 세계의 인코딩/디코딩 문제를 다뤄보기 위해 웹상의 문서를 예로 들어보죠.
<a href="http://www.kma.go.kr">대한민국 기상청</a>에서는 시시각각 변하는 기상 정보를 홈페이지에서 제공합니다.
대한민국 곳곳의 날씨는 <a href="http://www.kma.go.kr/weather/observation/currentweather.jsp">도시별 현재 날씨</a> 페이지에서 확인할 수 있습니다.</p>
<p><img src="2013-12-02-1_r.png" alt="대한민국 기상청 도시별 현재 날씨" id="" />
<em>그림 1.</em> 대한민국 기상청 도시별 현재 날씨 (<a href="2013-12-02-1.png">원본</a>)</p>
<p>도시별 현재 날씨 페이지를 다운로드 받는 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: weather.pl
#
use strict;
use warnings;
use HTTP::Tiny;
my $url = 'http://www.kma.go.kr/weather/observation/currentweather.jsp';
my $res = HTTP::Tiny->new->get($url);
die "Failed!: $res->{status} - $res->{reason}\n" unless $res->{success};
print $res->{content};
</pre>
<p>웹 상의 HTML 문서를 내려받기 위해 <code>HTTP::Tiny</code>를 이용합니다.
<code>HTTP::Tiny</code> 객체는 GET/POST/PUT/DELETE 등의 HTTP 1.1 프로토콜은 완벽하게 지원하며
특히 요청후 반환 받은 해시는 다음과 같은 항목을 포함하고 있습니다.</p>
<ul>
<li><code>protocol</code></li>
<li><code>reason</code></li>
<li><code>status</code></li>
<li><code>success</code></li>
<li><code>headers</code></li>
<li><code>content</code></li>
</ul>
<p>자세한 내용은 <a href="http://metacpan.org/module/HTTP::Tiny">공식 문서</a>를 참조하세요.</p>
<h2>한글이 깨져요!</h2>
<p>앞의 코드를 실행하면 내려 받은 도시별 현재 날씨 페이지의 HTML을 터미널에 출력합니다.
그런데 자세히 보면 브라우저에서 정상적으로 보이던 한글이 모두 깨져 보입니다.</p>
<pre class="brush: bash;">
$ perl weather.pl
...
<option value=''>17û-----------------</option>
<option value="http://www.nts.go.kr/">����û</option>
<option value="http://www.customs.go.kr/">����û</option>
<option value="http://www.pps.go.kr/">����û</option>
<option value="http://kostat.go.kr/">����û</option>
...
</pre>
<p>한글이 정상적으로 보이지 않을 경우에는 항상 문자 인코딩을 확인해야 합니다.
코드를 약간 수정해서 기상청 페이지의 문자 인코딩을 확인해보죠.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: weather.pl
#
use strict;
use warnings;
use DDP;
use HTTP::Tiny;
my $url = 'http://www.kma.go.kr/weather/observation/currentweather.jsp';
my $res = HTTP::Tiny->new->get($url);
die "Failed!: $res->{status} - $res->{reason}\n" unless $res->{success};
p $res->{headers};
</pre>
<p><code>DDP</code> 모듈(<code>Data::Printer</code>가 제공하는 단축 모듈)을 적재하는 부분이 추가되고
<code>$res->{content}</code>를 출력하던 부분 대신 <code>DDP</code> 모듈이 제공하는 <code>p</code> 함수를 이용해서
<code>$res->{headers}</code>를 출력하는 부분으로 변경되었습니다.
실행 결과는 다음과 같습니다.</p>
<pre class="brush: bash;">
$ perl weather.pl
\ {
connection "close",
content-type "text/html;charset=euc-kr",
date "Mon, 02 Dec 2013 05:32:37 GMT",
server "Jeus WebContainer/JEUS 5.0 (fix #16)",
set-cookie "JSESSIONID=cJDvjEKVIp7F8AEd0aTJbEERec727KIZUeHXSFnHBt7GQGCk2aFJF6qq5GEkZ2Mv;Path=/",
transfer-encoding "chunked"
}
</pre>
<p>실제로 터미널에서 보면 색을 입혀 꽤나 알록달록하게 보이기 때문에 디버깅할때 큰 도움이 됩니다.
응답 헤더의 <code>content-type</code>가 <code>"text/html;charset=euc-kr"</code>임을 알 수 있습니다.
자료가 펄 프로그램을 만나기 전까지는 <code>EUC-KR</code>로 인코딩 되어 있는데
펄 프로그램 내부로 들어오게 될 때 적절하게 디코딩을 해주지 않으면
<code>EUC-KR</code> 자료를 <code>UTF-8</code>로 해석하려하기 때문에 깨져보이는 문자가 생깁니다.
즉 기상청이 제공하는 페이지의 문자 인코딩은 <code>EUC-KR</code>이며, 펄의 기본 인코딩은
<code>UTF-8</code>이기 때문에 한글이 정상적으로 보이지 않았던 것입니다.</p>
<pre class="brush: plain;">
+---------------+ +-----------------+ +-----------+
| html 문서 | --> | 펄 프로그램 | --> | 터미널 |
| EUC-KR 인코딩 | | 디코딩하지 않음 | | 문자 깨짐 |
+---------------+ +-----------------+ +-----------+
</pre>
<p>그러면 인코딩으로 인해 문자가 깨지는 현상을 방지하려면 어떻게 해야할까요?</p>
<pre class="brush: plain;">
+---------------+ +-----------------+ +-----------+
| html 문서 | --> | 펄 프로그램 | --> | 터미널 |
| EUC-KR 인코딩 | | 디코딩 | | 정상 출력 |
+---------------+ +-----------------+ +-----------+
</pre>
<p>즉 펄 프로그램 내부에서 사용하기 직전에 <code>EUC-KR</code> 인코딩을 펄 내부 인코딩에 맞도록 디코딩 과정을 거쳐야 합니다.
디코딩을 수행하기 위해 <code>Encode</code> 모듈의 <code>decode</code> 함수를 사용합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: weather.pl
#
use strict;
use warnings;
use Encode qw( decode );
use HTTP::Tiny;
my $url = 'http://www.kma.go.kr/weather/observation/currentweather.jsp';
my $res = HTTP::Tiny->new->get($url);
die "Failed!: $res->{status} - $res->{reason}\n" unless $res->{success};
print decode( 'euc-kr', $res->{content} );
</pre>
<p>이제는 실행해보면 한글이 정상적으로 보이는 것을 확인할 수 있습니다.</p>
<pre class="brush: bash;">
$ perl weather.pl
...
<option value=''>17청-----------------</option>
<option value="http://www.nts.go.kr/">국세청</option>
<option value="http://www.customs.go.kr/">관세청</option>
<option value="http://www.pps.go.kr/">조달청</option>
<option value="http://kostat.go.kr/">통계청</option>\
...
</pre>
<h2 id="widecharacterinprintat.">Wide character in print at.</h2>
<p>사실 출력이 길어서 눈치채치 못했을 수도 있지만 사실 프로그램은 계속 경고를 출력하고 있었습니다.</p>
<pre class="brush: bash;">
$ perl weather.pl > /dev/null
Wide character in print at weather.pl line 14.
$
</pre>
<p>펄로 한글을 비롯 영어가 아닌 문자를 출력하다보면 <code>Wide character ...</code> 경고를 종종 볼 수 있습니다.
이 경고는 인코딩을 하지 않았을때 펄이 친절하게(?) 경고해주는 것입니다.
펄의 내부 형식으로 존재하는 바이트를 그대로 보내게(출력을 한다던가) 되었을 때 발생합니다.
물론 펄의 내부 형식은 'UTF-8'이지만 이 내부 형식에 의존해서 출력하면(내보내면) 안됩니다.
펄은 이런 경우를 최대한 감지해서 사용자에게 <code>Wide character ...</code> 경고를 보여줍니다.
입력 받은 자료에 대해서 디코딩을 하듯 출력할 자료에 대해서도 인코딩을 명시적으로 해야 한다는 뜻입니다.
지금까지 보았던 그림을 더 정확하게 표현해보죠.</p>
<pre class="brush: plain;">
+---------------+ +------------------------------+ +----------------+
| html 문서 | | 펄 프로그램 | | 터미널 |
+---------------+ +------------------------------+ +----------------+
| | --> | (디코딩) | --> | |
| EUC-KR 인코딩 | | EUC-KR -> 내부 형식 | | |
| | | 내부 형식 -> UTF-8 | | UTF-8 (리눅스) |
| | | (인코딩) | | |
+---------------+ +------------------------------+ +----------------+
</pre>
<p>그림에서 <em>내부 형식</em>이라고 표기했지만 이 펄의 <em>내부 형식</em>은 언급했듯이 <em>UTF-8</em>입니다.
그럼에도 불구하고 <em>명시적</em>으로 <em>UTF-8</em>로 인코딩함을 유의하세요.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: weather.pl
#
use strict;
use warnings;
use Encode qw( decode encode );
use HTTP::Tiny;
my $url = 'http://www.kma.go.kr/weather/observation/currentweather.jsp';
my $res = HTTP::Tiny->new->get($url);
die "Failed!: $res->{status} - $res->{reason}\n" unless $res->{success};
print encode( 'utf-8', decode( 'euc-kr', $res->{content} ) );
</pre>
<p>이제는 <code>Wide character ...</code> 경고도 나타나지 않으며 한글 출력에도 전혀 문제가 없습니다. :-)</p>
<h2>정리하며</h2>
<p>인코딩/디코딩에 익숙하지 않다면 문자 인코딩 때문에 한 두번쯤은 골머리를 싸매게되곤 합니다.
이 문자열 인코딩의 문제는 비단 펄에서만의 이슈가 아니라 어떤 프로그래밍 언어를 사용하든
입출력 스트림에서 제공하거나 요구하는 형식에 따라 항상 발생하는 문제입니다.
하지만 펄은 이런 인코딩 문제를 코어 모듈을 이용해서 간단하게 해결 할 수 있는 방법을 제공합니다.
이것은 펄이 내부적으로 유니코드 기반으로 동작하기 때문이며 이런 이슈에 대해
세심하게 고려한 덕분(Thanks, Larry Wall! :)이기도 합니다.</p>
<h2>참고문서</h2>
<ul>
<li><a href="https://github.com/aero/perl_docs/wiki/Unicode-in-Perl">Unicode in Perl</a> - <a href="https://twitter.com/aer0">@aer0</a></li>
<li><a href="http://gypark.pe.kr/wiki/Perl/%ED%95%9C%EA%B8%80">Perl과 한글</a> - <a href="http://twitter.com/gypark">@gypark</a></li>
<li><a href="http://perldoc.perl.org/perlunicode.html">perldoc perlunicode</a></li>
<li><a href="http://perldoc.perl.org/perlunifaq.html">perldoc perlunifaq</a></li>
<li><a href="http://perldoc.perl.org/utf8.html">perldoc utf8</a></li>
</ul>
2013-12-02T00:00:00+09:00newbcodeuse Barcode;http://advent.perl.kr/2013/2013-12-01.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><a href="http://en.wikipedia.org/wiki/Barcode">바코드(barcode)</a>는 광학 기기가 판독할 수 있도록 고안된 굵기가 다른 흑백 막대로 조합시켜 만든 코드입니다.
전통적인 바코드는 서로 굵기가 다른 막대 모양의 이미지를 적절한 간격으로 배치해서 숫자나 문자를 표현합니다.
최근에는 단순한 막대 모양이 아닌 사각형의 배열의 점으로 자료를 표현하는 2차원 코드도 개발되어 많은 양의 정보를 담기도 합니다.
바코드 단순하면서도 판독율이 좋기 때문에 다양한 개체를 전산화하기 위한 용도로 널리 사용됩니다.
더불어 바코드는 종이 뿐만 아니라 의류 등 인쇄할 수 있는 위치의 제약이 거의 없고, 표준화되어 있으며
출력 비용을 제외하면 생성 비용이 거의 없어서 오래된 기술임에도 불구하고 여전히 많은 곳에서 사용하고 있습니다.</p>
<p>지금부터 <code>HTML::Barcode</code>를 이용해 다양한 종류의 바코드를 만들어 보고
<code>Mojolicious</code>와 연동해서 웹 기반의 바코드 생성기를 제작합니다.</p>
<h2>준비물</h2>
<p>필요한 모듈은 다음과 같습니다.</p>
<ul>
<li><a href="https://metacpan.org/module/HTML::Barcode">CPAN의 HTML::Barcode 모듈</a></li>
<li><a href="https://metacpan.org/module/HTML::Barcode::Code93">CPAN의 HTML::Barcode::Code93 모듈</a></li>
<li><a href="https://metacpan.org/module/HTML::Barcode::Code128">CPAN의 HTML::Barcode::Code128 모듈</a></li>
<li><a href="https://metacpan.org/module/HTML::Barcode::QRCode">CPAN의 HTML::Barcode::QRCode 모듈</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 \
HTML::Barcode \
HTML::Barcode::Code93 \
HTML::Barcode::Code128 \
HTML::Barcode::QRCode \
Mojolicious
</pre>
<p>사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
<a href="http://perlbrew.pl/">perlbrew</a>를 이용해서 자신만의 Perl을 사용하고 있다면
다음 명령을 이용해서 모듈을 설치합니다.</p>
<pre class="brush: bash;">
$ cpan \
HTML::Barcode \
HTML::Barcode::Code93 \
HTML::Barcode::Code128 \
HTML::Barcode::QRCode \
Mojolicious
</pre>
<h2 id="qrcode">QRCode 모듈 설치</h2>
<p>준비물 중 <code>HTML::Barcode::QRCode</code> 모듈은 <code>Text::QRCode</code> 모듈을 사용합니다.
<code>Text::QRCode</code> 모듈은 빌드시 <code>pkg-config</code> 명령줄 유틸리티를 사용하기 때문에
<code>pkg-config</code>가 없을 경우 다음 오류를 내며 설치에 실패합니다.</p>
<pre class="brush: plain;">
Cannot determine perl version info from lib/Text/QRCode.pm
Cannot find pkg-config command.
Specify it to PKG_CONFIG_PATH env variable if you have pkg-config cmd at Makefile.PL line 41.
</pre>
<p><code>pkg-config</code> 유틸리티는 데비안 계열 리눅스의 경우 다음 명령을 이용해 설치할 수 있습니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install pkg-config
</pre>
<p><code>Text::QRCode</code> 모듈은 <a href="http://fukuchi.org/works/qrencode/">libqrencode 라이브러리</a>(<a href="https://github.com/fukuchi/libqrencode">저장소</a>)를
사용하기 때문에 <code>libqrencode</code> 개발 라이브러리가 없을 경우 모듈 설치에 실패합니다.</p>
<pre class="brush: plain;">
Cannot determine perl version info from lib/Text/QRCode.pm
Package libqrencode was not found in the pkg-config search path.
Perhaps you should add the directory containing `libqrencode.pc'
to the PKG_CONFIG_PATH environment variable
No package 'libqrencode' found
*** You must install libqrencode.
*** See http://megaui.net/fukuchi/works/qrencode/index.en.html at Makefile.PL line 73.
</pre>
<p>데비안 계열 리눅스의 경우 다음 명령을 이용해 <code>libqrencode</code> 개발 라이브러리를 설치할 수 있습니다.</p>
<pre class="brush: bash;">
$ sudo apt-get install libqrencode-dev
</pre>
<p>필요한 의존 라이브러리와 유틸리티 설치가 완료되면 <code>HTML::Barcode::QRCode</code> 모듈 설치를 완료합니다.</p>
<h2>사용 방법</h2>
<p><code>HTML::Barcode</code> 모듈의 사용법은 무척 간단합니다.
<code>978-89-93827-28-6</code> 문자열을 <em>CODE_93</em> 규격 바코드로 변환하는 법은 다음과 같습니다.</p>
<pre class="brush: perl;">
use 5.010;
use strict;
use warnings;
use HTML::Barcode::Code93;
my $code = HTML::Barcode::Code93->new( text => '978-89-93827-28-6' );
say $code->render;
</pre>
<p>간단하죠? 실행하면 복잡한 문자열이 화면에 출력됩니다.
복잡해보이지만 이 문자열은 유효한 HTML로 웹브라우저에서 바코드 형태로 확인할 수 있습니다.
사람이 알아볼 수 있게 들여쓰기를 정리하면 대략 다음과 같은 내용을 가집니다.</p>
<pre class="brush: xml;">
<style type="text/css">
table.hbc {border-width:0;border-spacing:0;}
table.hbc {border-width:0;border-spacing:0;}
table.hbc tr, table.hbc td{border:0;margin:0;padding:0;}
table.hbc td{text-align:center;}
table.hbc td.hbc_on,table.hbc td.hbc_off {width:2px;height:100px;}
table.hbc td.hbc_on {background-color:#000;color:inherit;}
table.hbc td.hbc_off {background-color:#fff;color:inherit;}
</style>
<table class="hbc">
<tr>
<td class="hbc_on"></td>
<td class="hbc_off"></td>
<td class="hbc_on"></td>
<td class="hbc_off"></td>
...
<td class="hbc_on"></td>
<td class="hbc_on"></td>
<td class="hbc_off"></td>
<td class="hbc_on"></td>
</tr>
<tr>
<td colspan="190">978-89-93827-28-6</td>
</tr>
</table>
</pre>
<p>즉 전혀 이미지를 사용하지 않고 HTML의 스타일을 재정의한 테이블 태그만 사용해서
바코드를 표현합니다. 따라서 웹 기반의 프로그램을 작성한다면 매우 손쉽게
바코드를 생성하고 화면에 보여줄 수 있습니다.
<code>HTML::Barcode</code> 모듈은 다양한 바코드를 생성하기 위한 부모 클래스로
<code>HTML::Barcode::Code93</code> 모듈에서 상속받고 있습니다.
현재 지원하지 않는 바코드 형식이 있다면 <code>HTML::Barcode::FooBar</code> 모듈을 작성하면
다른 모듈과 동일한 방식으로 바코드를 생성할 수 있습니다.
자세한 내용은 <a href="https://metacpan.org/module/HTML::Barcode">공식 문서</a>를 참조하세요.</p>
<h2>웹 기반 바코드 생성기</h2>
<p>그럴듯한 바코드 생성기를 만들기 위해 간단한 웹앱을
만들어서 사용자의 요청을 받을 수 있도록 합니다.
새로 만드는 웹앱은 다음 세 가지 요청을 처리하도록 합니다.</p>
<ul>
<li><code>/code93</code>: CODE_93 바코드 생성기</li>
<li><code>/code128</code>: CODE_128 바코드 생성기</li>
<li><code>/qrcode</code>: QR_CODE 바코드 생성기</li>
</ul>
<p><a href="http://mojolicio.us/">Mojolicious</a>는 <a href="http://perldancer.org/">Dancer</a>와 더불어 펄의 대표적인 경량 웹 프레임워크입니다.
각각의 웹 프레임워크에 대한 자세한 설명은 공식 홈페이지 또는 CPAN의 공식 문서를 참조하세요.
<em>Mojolicious</em>를 사용해서 간단히 웹앱을 구현해 보죠.
우선 <code>Mojolicious::Lite</code> 모듈을 적재합니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
#
# FILE: barcode-generator-web.pl
#
use Mojolicious::Lite;
</pre>
<p>이제 <em>CODE_93</em> 형식의 바코드를 생성하는 <code>/code93</code> 컨트롤러를 만듭니다.
<code>code</code> 파라미터를 입력받아 <em>CODE_93</em>으로 변환한 HTML 결과를
자료 공유 영역인 stash의 <code>code</code> 항목으로 전달합니다.</p>
<pre class="brush: perl;">
get '/code93' => sub {
my $self = shift;
my $code = $self->param('code') || 'Enjoy Perl';
my $b = HTML::Barcode::Code93->new( text => $code );
$self->stash( code => $b->render($code) );
} => 'barcode';
</pre>
<p><code>/code93</code> 컨트롤러에 대한 템플릿 영역은 다음과 같습니다.</p>
<pre class="brush: perl;">
__DATA__
@@ barcode.html.ep
% layout 'default';
% title 'Barcode Generator';
<div class="center">
<%== $code %>
%= form_for '' => begin
%= label_for code => 'Code Text'
<br>
%= text_field 'code'
%= submit_button
% end
</div>
</pre>
<p><code>form_for</code>나 <code>label_for</code>, <code>text_field</code>, <code>submit_button</code>은
<a href="http://mojolicio.us/perldoc/Mojolicious/Plugin/TagHelpers">Mojolicious의 TagHelper 플러그인</a>의 기능입니다.
<code>Mojolicious::Plugin::TagHelper</code> 플러그인은 기본 플러그인이므로 따로 설치할 필요는 없습니다.
자세한 내용은 <a href="http://mojolicio.us/perldoc/Mojolicious/Plugin/TagHelpers">공식 문서</a>를 참고하세요.</p>
<p>여기까지 작업한 내용을 웹으로 확인해볼까요?
명령줄에서 다음 명령을 실행합니다.</p>
<pre class="brush: bash;">
$ morbo barcode-generator-web.pl
[Sun Dec 1 13:55:49 2013] [info] Listening at "http://*:3000".
Server available at http://127.0.0.1:3000.
</pre>
<p>이제 <code>http://localhost:3000/code93</code>으로 접속하면 바코드 예제와 함께
원하는 문자열을 바코드로 변환하기 위한 텍스트 입력창이 있습니다.</p>
<p><img src="2013-12-01-1_r.png" alt="CODE_93 바코드 생성기" id="code_93" />
<em>그림 1.</em> CODE_93 바코드 생성기 (<a href="2013-12-01-1.png">원본</a>)</p>
<p>최근 대부분의 휴대폰은 바코드 읽기 기능을 기본적으로 지원하고 있습니다.
생성한 바코드를 휴대폰으로 한번 읽어 볼까요?</p>
<p><img src="2013-12-01-2_r.png" alt="휴대폰으로 CODE_93 바코드 판독" id="code_93" />
<em>그림 2.</em> 휴대폰으로 CODE_93 바코드 판독 (<a href="2013-12-01-2.png">원본</a>)</p>
<h2>나머지 코드</h2>
<p>내친 김에 남은 <em>CODE_128</em> 형식과 <em>QR_CODE</em> 형식까지 구현해보죠.
전체 코드는 다음과 같습니다.</p>
<pre class="brush: perl;">
#!/usr/bin/env perl
use Mojolicious::Lite;
use HTML::Barcode::Code93;
use HTML::Barcode::Code128;
use HTML::Barcode::QRCode;
helper code93 => sub {
my ( $self, $text ) = @_;
my $code = HTML::Barcode::Code93->new( text => $text );
return $code->render;
};
helper code128 => sub {
my ( $self, $text ) = @_;
my $code = HTML::Barcode::Code128->new( text => $text );
return $code->render;
};
helper qrcode => sub {
my ( $self, $text ) = @_;
my $code = HTML::Barcode::QRCode->new( text => $text );
return $code->render;
};
get '/' => 'index';
get '/code93' => sub {
my $self = shift;
my $code = $self->param('code') || 'Enjoy Perl';
$self->stash( code => $self->code93($code) );
} => 'barcode';
get '/code128' => sub {
my $self = shift;
my $code = $self->param('code') || 'use Perl;';
$self->stash( code => $self->code128($code) );
} => 'barcode';
get '/qrcode' => sub {
my $self = shift;
my $code = $self->param('code') || 'http://advent.perl.kr';
$self->stash( code => $self->qrcode($code) );
} => 'barcode';
app->start;
__DATA__
@@ index.html.ep
% layout 'default';
% title 'Barcode Generator';
%= link_to CODE_93 => '/code93'
<br />
%= link_to CODE_128 => '/code128'
<br />
%= link_to QR_CODE => '/qrcode'
<br />
@@ barcode.html.ep
% layout 'default';
% title 'Barcode Generator';
<div class="center">
<%== $code %>
%= form_for '' => begin
%= label_for code => 'Code Text'
<br>
%= text_field 'code'
%= submit_button
% end
</div>
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<style type="text/css">
.center { text-align: center; }
.center table { margin-left: auto; margin-right: auto; }
</style>
</head>
<body>
<%= content %>
</body>
</html>
</pre>
<p><em>Mojolicious</em>에서 제공하는 <code>helper</code> 기능을 사용하고 <code>index</code> 컨트롤러를
추가해서 <code>/</code> 주소, 즉 <code>http://localhost:3000</code>으로 접속했을때
각각의 바코드 생성기 화면으로 이동할 수 있는 링크를 제공합니다.</p>
<p><em>겨우 90줄 미만의 코드</em>로 <em>웹 기반의 바코드 생성기</em>를 만들었습니다.
우리의 바코드 생성기가 제대로 동작하는지 테스트해보죠.
<code>http://localhost:3000/code128</code>로 접속해볼까요?</p>
<p><img src="2013-12-01-3_r.png" alt="휴대폰으로 CODE_128 바코드 판독" id="code_128" />
<em>그림 3.</em> 휴대폰으로 CODE_128 바코드 판독 (<a href="2013-12-01-3.png">원본</a>)</p>
<p>이번에는 <code>http://localhost:3000/qrcode</code>로 접속해보죠.</p>
<p><img src="2013-12-01-4_r.png" alt="휴대폰으로 QR_CODE 바코드 판독" id="qr_code" />
<em>그림 4.</em> 휴대폰으로 QR_CODE 바코드 판독 (<a href="2013-12-01-4.png">원본</a>)</p>
<p>멋지군요! ;-)</p>
<h2>정리하며</h2>
<p>바코드는 굉장히 고전적인 부호 체계입니다.
바코드는 인식 속도가 매우 빠르고 생산 단가가 거의 없다시피 하며,
바코드 리더기가 저렴하기 때문에 지난 10년 사이에는 조그마한 소매점은 물론
도서 대여점에서까지 소장하고 있는 품목을 전산화하는 용도로 널리 사용되고 있습니다.
그럼에도 불구하고 바코드 리더기를 들고 다니지 않는 이상 사람이
바코드만 보고 이해할 수 있는 내용은 거의 없기 때문에 사실 일반인들이
바코드를 직접 사용할 기회는 거의 없었습니다.
하지만 최근에는 고성능 스마트폰이 급속도로 보급화되면서
개개인이 휴대폰의 카메라를 이용한 바코드 리더기를 소지하고 다니게 된 덕분에
명함은 물론 이벤트 및 벽보 광고 등에서도 바코드와 QR코드를 활용하는 사례가 늘고 있습니다.</p>
<p>펄을 이용하면 손쉽게 다양한 종류의 바코드를 생성할 수 있습니다.
더불어 웹에서 필요에 따라 적절한 바코드를 표현하는 것도 어렵지 않습니다.
여러분은 바코드를 어디에 활용해보고 싶나요? :)</p>
<p>Enjoy Your Perl... and R.I.P. <a href="http://twitter.com/#!/am0c">@am0c</a>! ;-)</p>
<p><img src="2013-12-01-5.png" alt="EOT" id="eot" style="margin: 0" /></p>
<p><em>EOT</em></p>
2013-12-01T00:00:00+09:00keedi