Perl의 Attribute (1)

| | Comments (0) | TrackBacks (0)
요즘 많이 쓰이는 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?attributes
http://www.myemy.com/article/class-component/