요즘 많이 쓰이는 Perl 기반 웹프레임웍인
Catalyst 같은 코드들을 보면
sub some_sub : Private {
....
}
같은 형식으로 attribute라는 것(여기서는 : Private )을 쓰는 걸 종종 볼 수 있다.
이것은 Perl을 제법 공부했다는 사람에게도 생소한 경우가 많으며, 예제나 튜토리얼을 따라서 그대로 쓰고 있기는 하지만 그 내부 동작을 제대로 이해하지 못하고 기계적으로 쓰는 경우도 많다. 그 이유는 attribute의 동작을 이해하기 위해서는 Perl을 배우는 데 있어 고급주제에 속하는 모듈과 심볼테이블의 동작방식을 속속들이 알아야 하기 때문이다.
Perl에서는
perlsub 문서를 보면 알겠지만 lvalue 같은 표준적으로 제공하는 attribute가 이미 존재한다.
my $val;
sub canmod : lvalue {
# return $val; this doesn't work, don't say "return"
$val;
}
sub nomod {
$val;
}
이렇게 lvalue attribute를 정의한 서브루틴은 그 자체로 lvalue로 사용하여 값을 할당할 수 있다.
canmod() = 5; # assigns to $val
nomod() = 5; # ERROR
Perl 5.6 부터는 여기에 사용자가 나름대로의 attribute를 정의할 수 있도록 확장되었다.(문서상에는 아직 실험적인 기능이라고 하지만 이미 많은 모듈과 프레임웍에서 널리 사용되고 있어 거의 정착된 기능으로 봐도 무방할 것 같다.)
그럼 처음으로 돌아가서 attribute는 무엇일까? attribute는 어떤 변수나 서브루틴에 별도의 의미를 부여하는 것이라고 보면 된다. 예를 들어 서브루틴이 실행될 때 서브루틴이 시작하고 끝나는 것을 추적하는 어떤 로그를 남기고 싶다고 하면 모든 서브루틴내에서 그 루틴을 포함하는 것이 아니라 다음처럼 기존의 서브루틴들에 _log라는 attribute만 붙여주면 서브루틴이 실행될 때 _log attribute 속성을 가지고 있으면 자동으로 로그를 찍도록 하겠다는 것이다.
sub mysub : _log {
....
}
sub yoursub : _log {
....
}
그러면 일단 나름대로의 attribute를 정의하고 코드를 실행해보자.
sub mysub : Loud {
print "Hello, World!\n";
}
그러면 다음과 같은 에러가 난다.
Invalid CODE attribute: Loud at -e line 1
BEGIN failed--compilation aborted at -e line 1.
이것은 서브루틴에 정의한 Loud attribute가 유효하지 않다는 것이다. (서브루틴에 attribute를 붙였기 때문에 CODE attribute 라는 에러가 났다. )
그러면 : Loud 같은 attribute가 붙었을 때 Perl 내부적으로 어떻게 동작하는지 살펴보자.
Perl은 attribute 형식의 문법이 나오면 내부적으로 다음과 같은 코드로 실행된다.
sub mysub : Loud { ... 형식으로 나왔다면 다음과 같은 코드가 내부적으로 실행된다.
use attributes ( 'main', \&mysub, 'Loud');
뒤에 따르는 LIST는 차례대로 호출한 측의 패키지 이름, attribute가 사용된 것의 레퍼런스(여기서는 mysub 서브루틴의 레퍼런스), attribute의 목록(여러 개면 여러 개가 차례로)이 된다.
use는 모듈을 로딩하여 사용할 때 쓰는 명령어임은 잘 알고 있을 것이다. 하지만 use가 내부적으로 어떻게 동작하는지는 잘 모르는 사람이 많다. 다시 한 번 정리하고 넘어가자면
use SomeModule LIST; 는 (예: use List::Util qw/shuffle sum/; 이것은 use List::Util ('shuffle','sum'); 과 같은 의미)
BEGIN {
require SomeModule;
SomeModule->import LIST;
}
와 같은 의미이다.
그러면 use attributes ( 'main', \&mysub, 'Loud'); 는 어떻게 동작할까? 그렇다
BEGIN {
require attributes;
attributes->import('main',\&mysub,'Loud');
}
처럼 동작한다. 여기서 import는 모듈에서 모듈의 심볼과 호출 측 네임스페이스 내의 심볼을 연결시키는 심볼 export작업에 주로 사용되는 기본 서브루틴(따로 정의하지 않으면 비어 있음)으로 이것을 나름대로 정의 해서 다양한 용도로 쓸 수 있다. Perl의 고급 테크닉의 시작은 이것을 어떻게 잘 이해하고 이용하느냐가 첫 걸음이 된다.
그러면 이제 그다음 동작은 어떻게 진행되는지 보려면 어디로 추적해 들어가야 할까?
그렇다. attributes 모듈의 import 서브루틴이 어떻게 만들어져 있는지 보면 된다.
<attributes 모듈의 import 서브루틴>
sub import {
@_ > 2 && ref $_[2] or do {
require Exporter;
goto &Exporter::import;
};
my (undef,$home_stash,$svref,@attrs) = @_;
my $svtype = uc reftype($svref);
my $pkgmeth;
$pkgmeth = UNIVERSAL::can($home_stash, "MODIFY_${svtype}_ATTRIBUTES")
if defined $home_stash && $home_stash ne '';
my @badattrs;
if ($pkgmeth) {
my @pkgattrs = _modify_attrs($svref, @attrs);
@badattrs = $pkgmeth->($home_stash, $svref, @pkgattrs);
...
...
이 소스를 보고
attributes->import('main',\&mysub,'Loud'); 가 호출 되었을 때 어떻게 되는지 분석해보자
->연산자를 통해서 모듈 내의 서브루틴을 호출하면 -> 왼쪽의 것이 첫 번째 인자로 넘어간다. 따라서
import('attributes','main',\&mysub,'Loud'); 로 호출이 된다.
그러면 $home_stash는 'main', $svref은 \&mysub, @attrs은 'Loud' 가 될 것이다.
그다음 $svtype은 $svref의 레퍼런스 타입이 되므로 'CODE' 가 된다.
그다음 UNIVERSAL::can('main',"MODIFY_CODE_ATTRIBUTES") 가 실행되어 MODIFY_CODE_ATTRIBUTES라는 서브루틴이 실행가능한지를 채크하고 그 결과를 $pkgmeth 변수(성공이면 MODIFY_CODE_ATTRIBUTES 서브루틴에 대한 레퍼런스) 에 넣고 attribute들의 적합성을 채크한 다음 해당 $pkgmeth->('main',\&mysub, ('Loud')) 으로 해당 서브루틴을 호출한다.
그러면 attribute를 원하는대로 처리해주려면 attribute를 정의해준 변수나 서브루틴등 타입에 따라 MODIFY_[TYPE]_ATTRIBUTES( 서브루틴이면 MODIFY_CODE_ATTRIBUTES, 스칼라면 MODIFY_SCALAR_ATTRIBUTES 등) 라는 서브루틴을 정의해주면 될 것이라는 걸 눈치 챌 수 있을 것이다.
<테스트코드>
#!/usr/bin/perl
use strict;
use warnings;
sub MODIFY_CODE_ATTRIBUTES {
print "MODIFY_CODE_ATTRIBUTES\n";
print "@_\n";
return;
}
sub mysub : Loud {
print "Hello, World!\n";
}
<결과>
MODIFY_CODE_ATTRIBUTES
main CODE(0x663d20) Loud
여기서 넘어온 인자(@_)가 $pkgmeth->('main',\&mysub, ('Loud')) 호출에서 넘겨준 것과 같음을 알 수 있다.
attributes 모듈은 특정한 타입에 대해서 attribute들을 저장하고 가져오기 위한 FETCH_[TYPE]_ATTRIBUTES 서브루틴도 지원한다. 이것은 attributes 모듈로 넘어가는 인자가 변수나 서브루틴(코드)의 레퍼런스이기 때문에(레퍼런스는 메모리상에 위치한 주소로 unique한 값이다.) 필요하다면 그 레퍼런스를 해시의 키등으로 사용하여 어떤 attribute를 저장해놨다가 나중에 다시 꺼내어 볼 수 있는 것이다.
이 모든 것을 하나의 샘플코드로 정리해 보면 다음과 같다.
<샘플코드>
#!/usr/bin/perl
use strict;
use warnings;
my %attrs;
sub MODIFY_SCALAR_ATTRIBUTES {
print "MODIFY_SCALAR_ATTRIBUTES\n";
print "@_\n";
my($package,$scalar_ref,@attrs)=@_;
$attrs{$scalar_ref}=\@attrs; # %attrs 해시에 넘어온 attributes를 저장
return;
#return 'attr4'; # 지원하지 않는 속성을 return을 통해서 넘겨주면 에러가 발생되므로 예외 처리에 사용가능
}
sub FETCH_SCALAR_ATTRIBUTES {
print "FETCH_SCALAR_ATTRIBUTES\n";
print "@_\n";
my($package,$scalar_ref)=@_;
return $attrs{$scalar_ref}; # 저장했던 attributes를 넘겨준다.
}
sub MODIFY_CODE_ATTRIBUTES {
print "MODIFY_CODE_ATTRIBUTES\n";
print "@_\n";
my($package,$code_ref,@attrs)=@_;
$attrs{$code_ref}=\@attrs; # %attrs 해시에 넘어온 atttributes를 저장
return;
}
sub FETCH_CODE_ATTRIBUTES {
print "FETCH_CODE_ATTRIBUTES\n";
print "@_\n";
my($package,$code_ref)=@_;
return $attrs{$code_ref}; # 저장했던 attributes를 넘겨준다.
}
my $var : attr1 attr2 attr3;
# 위처럼 attribute를 사용하면 실제로는 아래 처럼 동작한다.
#use attributes ('main', \$var, 'attr1', 'attr2', 'attr3');
# attributes 모듈의 import가 두번째 인자의 TYPE에 따라 FETCH_[TYPE]_ATTRIBUTES가
# 정의되어 있으면 호출한다.
print "@{ attributes::get(\$var) }\n";
# FETCH_SCALAR_ATTRIBUTES 가 호출된다. 인자는 패키지이름과 레퍼런스 2개
sub test : attr1 attr2 attr3 {
print "test\n";
}
#use attributes ('main', \&test, 'attr1', 'attr2', 'attr3');
#위와 마찬가지로 뒤의 리스트들은 attributes 모듈의 import의 인자로 들어간다.
print "@{ attributes::get(\&test) }\n";
# FETCH_CODE_ATTRIBUTES 가 호출된다
<결과>
MODIFY_CODE_ATTRIBUTES
main CODE(0x65e0c0) attr1 attr2 attr3
MODIFY_SCALAR_ATTRIBUTES
main SCALAR(0x604fa0) attr1 attr2 attr3
FETCH_SCALAR_ATTRIBUTES
main SCALAR(0x604fa0)
attr1 attr2 attr3
FETCH_CODE_ATTRIBUTES
main CODE(0x65e0c0)
attr1 attr2 attr3
위 예제는 attribute의 동작방식을 알아보기 위한 것이지 실제 어떻게 사용하는지의 예제로는 적합하지 않다.
다음은 어떤 서브루틴에 Quiet라는 attribute를 주면 해당 서브루틴이 표준출력으로 출력하는 모든 글자를 소문자로 만드는 예제이다.
<Quiet.pm>
package Quiet;
use strict;
use warnings;
use IO::Capture::Stdout;
my @cache;
CHECK {
# attributes 모듈은 BEGIN 단계에서 로딩되므로 그 시점에서는 서브루틴이 완전히 로딩되지 않아서
# 서브루틴의 코드에 대한 조작을 할 수 없다. 따라서 MODIFY_CODE_ATTRIBUTES 에서 cache에 던져놓고
# 서브루틴이 로딩이 끝난 CHECK 단계에서 조작하기 위해 CHECK 서브루틴에 그 작업을 정의한다.
no strict 'refs';
my %code_cache;
foreach (@cache) {
my ($pkg, $ref, @attrs) = @$_;
unless ($code_cache{$pkg}) {
$code_cache{$pkg} = {};
foreach my $sym ( values %{$pkg.'::'} ) {
next unless *{$sym}{CODE};
$code_cache{$pkg}->{*{$sym}{CODE}} = *{$sym}{NAME};
}
}
my $sym = $pkg . '::' . $code_cache{$pkg}->{$ref};
no warnings 'redefine';
*{$sym} = sub {
my $capture = IO::Capture::Stdout->new();
$capture->start();
# call the original subroutine
my @rets = $ref->(@_);
$capture->stop();
foreach ( $capture->read ) { print "\L$_" }
return @rets;
};
}
}
sub MODIFY_CODE_ATTRIBUTES {
my ($package,$code_ref,@attrs) = @_;
push @cache, [$package, $code_ref, @attrs];
return grep { $_ !~ /^Quiet$/ } @attrs; # Quiet attribute가 아니면 에러발생
}
1;
<Quiet_test.pl>
#!/usr/bin/perl
use strict;
use warnings;
use base qw/Quiet/;
# We want this function to be loud
sub foo : Quiet {
print "Purple is a nice color.\n";
print "Hello, World !\n";
}
foo();
<결과>
purple is a nice color.
hello, world !
이 코드가 이해된다면 고수들이 만든 예전에는 암호 같아만 보였던 모듈 및 코드들이 이해되기 시작하면서 Perl을 공부하는 데 있어 새로운 재미를 느낄 수 있는 길이 열릴 것이라고 본다.
하지만 뭐가 이리 복잡해? 라고 생각하는 사람도 있을 텐데 다행히 이런 attribute를 다루는 dirty한 작업을 손쉽게 해줄 수 있는 모듈들이 존재한다. 다음번에는 그러한 모듈인
Attribute::Handlers 라는 모듈을 소개해보도록 하겠다.
참고:
http://search.cpan.org/perldoc?attributeshttp://www.myemy.com/article/class-component/