Perl 객체 참조

Perl 객체 참조

이 문서는 Perl의 객체 지향(object orientation) 기능에 대한 레퍼런스예요. Perl에서 객체 지향 프로그래밍의 입문을 찾고 있다면 perlootut을 먼저 보세요.

Perl 객체를 이해하려면 먼저 Perl의 참조(reference)를 이해해야 해요. 자세한 내용은 perlreftut을 참고하세요.

이 문서는 Perl의 모든 객체 지향(OO) 기능을 기초부터 설명해요. 그냥 자기만의 객체 지향 코드를 좀 작성하려는 거라면, perlootut에서 설명하는 CPAN의 객체 시스템 중 하나를 쓰는 편이 더 낫죠.

만약 자기만의 객체 시스템을 작성하려 하거나, 처음부터 객체를 구현하는 코드를 유지보수해야 한다면, 이 문서가 Perl이 정확히 어떻게 객체 지향을 하는지 이해하는 데 도움을 줄 거예요.

객체 지향 Perl을 규정하는 몇 가지 기본 원칙이 있어요:

  1. 객체는 그저 자신이 어떤 클래스에 속하는지 아는 데이터 구조다.

  2. 클래스는 그저 패키지다. 클래스는 객체에 대해 동작할 것으로 기대되는 메서드를 제공한다.

  3. 메서드는 그저 객체에 대한 참조(클래스 메서드라면 패키지 이름)를 첫 번째 인자로 받을 것으로 기대되는 서브루틴이다.

이 각각의 원칙을 깊이 있게 살펴볼게요.

출처: Perl 공식 문서 - perlobj

본문

객체는 그저 데이터 구조다 (An Object is Simply a Data Structure)

객체 지향을 지원하는 많은 다른 언어들과 달리, Perl은 객체를 구성하기 위한 어떤 특별한 문법도 제공하지 않아요. 객체는 그저 특정 클래스와 명시적으로 연관된 Perl 데이터 구조(해시, 배열, 스칼라, 파일핸들 등)일 뿐이에요.

그 명시적 연관은 내장 bless 함수로 만들어지는데, 보통 클래스의 생성자(constructor) 서브루틴 안에서 사용돼요.

간단한 생성자는 이래요.

package File;

sub new {
    my $class = shift;

    return bless {}, $class;
}

new라는 이름은 특별하지 않아요. 생성자 이름을 다른 걸로 지어도 돼요.

package File;

sub load {
    my $class = shift;

    return bless {}, $class;
}

OO 모듈의 현대적 관례는 항상 생성자 이름으로 new를 쓰는 거지만, 그렇게 할 의무는 없어요. 데이터 구조를 클래스로 bless하는 어떤 서브루틴이든 Perl에서 유효한 생성자예요.

앞선 예들에서 {} 코드는 빈 익명 해시에 대한 참조를 만들어요. 그러면 bless 함수가 그 참조를 받아 $class의 클래스와 해시를 연관시켜요. 가장 단순한 경우, $class 변수는 결국 "File"이라는 문자열을 담게 돼요.

우리 객체로 bless되고 있는 데이터 구조에 대한 참조를 저장하는 데 변수를 쓸 수도 있어요.

sub new {
    my $class = shift;

    my $self = {};
    bless $self, $class;

    return $self;
}

$self가 가리키는 해시를 bless하고 나면 그 위에서 메서드를 호출할 수 있어요. 객체 초기화를 별도의 자체 메서드에 넣고 싶을 때 유용해요.

sub new {
    my $class = shift;

    my $self = {};
    bless $self, $class;

    $self->_initialize();

    return $self;
}

객체는 해시이기도 하므로 해시처럼 취급해서, 객체와 연관된 데이터를 저장하는 데 쓸 수 있어요. 보통 클래스 내부 코드는 해시를 접근 가능한 데이터 구조로 취급하고, 클래스 외부 코드는 항상 객체를 불투명하게 취급해야 해요. 이것을 캡슐화(encapsulation) 라고 해요. 캡슐화는 객체의 사용자가 그것이 어떻게 구현됐는지 알 필요가 없다는 뜻이에요. 사용자는 그냥 객체에 문서화된 메서드를 호출할 뿐이죠.

단, (대부분의 다른 OO 언어와 달리) Perl은 캡슐화를 어떤 식으로도 보장하거나 강제하지 않는다는 점을 명심하세요. 객체가 실제로 불투명 하길 원한다면 직접 그렇게 마련해야 해요. "Inside-Out 객체"나 CPAN의 모듈을 쓰는 것을 포함해 여러 방법으로 그럴 수 있어요.

객체는 bless된다; 변수는 아니다 (Objects Are Blessed; Variables Are Not)

무언가를 bless할 때, 우리는 그것에 대한 참조를 담고 있는 변수를 bless하는 것도, 변수가 저장하는 참조를 bless하는 것도 아니에요. 우리는 변수가 가리키는 것(때로는 _referent_라고 불리는)을 bless하는 거예요. 이 코드가 제일 잘 보여 줘요.

use Scalar::Util 'blessed';

my $foo = {};
my $bar = $foo;

bless $foo, 'Class';
print blessed( $bar ) // 'not blessed';    # prints "Class"

$bar = "some other value";
print blessed( $bar ) // 'not blessed';    # prints "not blessed"

변수에 bless를 호출하면, 우리는 사실 변수가 가리키는 밑바탕 데이터 구조를 bless하는 거예요. 참조 자체도, 그 참조를 담고 있는 변수도 bless하지 않아요. 그래서 두 번째 blessed( $bar ) 호출이 false를 반환하는 거예요. 그 시점에 $bar는 더 이상 객체에 대한 참조를 저장하고 있지 않으니까요.

오래된 책이나 문서에서 "참조를 bless한다"거나 객체를 "bless된 참조"로 설명하는 걸 보게 될 텐데, 그건 틀렸어요. 객체로 bless되는 건 참조가 아니라, 참조가 가리키는 것(, 참조 대상)이에요.

클래스는 그저 패키지다 (A Class is Simply a Package)

Perl은 클래스 정의를 위한 어떤 특별한 문법도 제공하지 않아요. 패키지는 변수와 서브루틴을 담는 이름공간일 뿐이에요. 유일한 차이는, 클래스에서는 서브루틴이 첫 번째 인자로 객체에 대한 참조나 클래스의 이름을 받을 것으로 기대할 수 있다는 거예요. 이것은 순전히 관례의 문제라서, 클래스는 객체나 클래스에 대해 동작하지 않는 메서드와 서브루틴을 모두 담을 수 있어요.

각 패키지는 @ISA라는 특별한 배열을 담고 있어요. @ISA 배열은 그 클래스의 부모 클래스들의 목록을 담아요(있으면). 이 배열은 Perl이 메서드 해석(method resolution)을 할 때 검사되는데, 이건 나중에 다룰게요.

패키지에서 메서드를 호출하는 건 그 패키지가 로드되어 있어야 하므로, 모듈을 로드하면서 동시에 @ISA에 추가하고 싶을 때가 많을 거예요. parent 프래그마를 쓰면 한 단계로 그럴 수 있어요. (오래된 코드에서는 base 프래그마를 만나게 될 수도 있는데, 요즘은 똑같이 권장되지 않는 fields 프래그마와 작업해야 할 때를 제외하면 권장되지 않아요.)

부모 클래스가 어떻게 설정되든, 패키지의 @ISA 변수는 그 부모들의 목록을 담게 돼요. 이것은 그저 각각 패키지 이름에 대응하는 문자열인 스칼라들의 리스트예요.

모든 클래스는 UNIVERSAL 클래스로부터 암시적으로 상속받아요. UNIVERSAL 클래스는 Perl 코어가 구현하며, isa(), can(), VERSION() 같은 여러 기본 메서드를 제공해요. UNIVERSAL 클래스는 패키지의 @ISA 변수에 결코 나타나지 않아요.

Perl은 내장 기능으로 오직 메서드 상속만 제공해요. 속성(attribute) 상속은 클래스가 직접 구현하도록 맡겨져 있어요. 자세한 내용은 "Accessor 작성" 절을 보세요.

메서드는 그저 서브루틴이다 (A Method is Simply a Subroutine)

Perl은 메서드 정의를 위한 어떤 특별한 문법도 제공하지 않아요. 메서드는 그저 일반 서브루틴이며 sub로 선언돼요. 메서드를 특별하게 만드는 것은 첫 번째 인자로 객체 또는 클래스 이름을 받을 것으로 기대한다는 점이에요.

Perl은 메서드 호출을 위한 특별한 문법, -> 연산자는 제공해요. 이것은 나중에 더 자세히 다룰게요.

여러분이 작성하는 대부분의 메서드는 객체에 대해 동작할 것으로 기대돼요.

sub save {
    my $self = shift;

    open my $fh, '>', $self->path() or die $!;
    print {$fh} $self->data()       or die $!;
    close $fh                       or die $!;
}

메서드 호출 (Method Invocation)

객체에 메서드를 호출하는 것은 $object->method라고 써요.

메서드 호출(또는 화살표) 연산자의 왼쪽은 객체(또는 클래스 이름)이고, 오른쪽은 메서드 이름이에요.

my $pod = File->new( 'perlobj.pod', $data );
$pod->save();

-> 문법은 참조를 역참조할 때도 사용돼요. 같은 연산자처럼 보이지만, 이 둘은 서로 다른 두 연산이에요.

메서드를 호출하면, 화살표 왼쪽의 것이 메서드의 첫 번째 인자로 전달돼요. 즉 Critter->new()를 호출하면, new() 메서드는 문자열 "Critter"를 첫 번째 인자로 받아요. $fred->speak()를 호출하면, $fred 변수가 speak()의 첫 번째 인자로 전달되죠.

마찬가지로 @_로 전달된 모든 인자는 원래 인자에 대한 별칭(alias)이에요. 이것은 객체 자체도 포함해요. $_[0]에 직접 할당하면 객체에 대한 참조를 담고 있는 변수의 내용을 바꾸게 돼요. 정확히 뭘 하고 있는지 알지 못한다면 이렇게 하지 않는 걸 권장해요.

Perl은 화살표 왼쪽을 보고 메서드가 어느 패키지에 있는지 알아요. 왼쪽이 패키지 이름이면 그 패키지에서 메서드를 찾아요. 왼쪽이 객체면, Perl은 그 객체가 bless된 패키지에서 메서드를 찾아요.

왼쪽이 패키지 이름도 객체도 아니면, 메서드 호출은 오류를 일으켜요. 더 미묘한 내용은 "메서드 호출 변형" 절을 보세요.

상속 (Inheritance)

우리는 이미 특별한 @ISA 배열과 parent 프래그마에 대해 이야기했어요.

클래스가 다른 클래스로부터 상속받으면, 부모 클래스에 정의된 어떤 메서드든 자식 클래스에서 사용할 수 있어요. 자기 클래스에 정의되지 않은 메서드를 객체에 호출하려 하면, Perl은 가질 수 있는 부모 클래스들에서도 그 메서드를 찾아봐요.

package File::MP3;
use parent 'File';    # sets @File::MP3::ISA = ('File');

my $mp3 = File::MP3->new( 'Andvari.mp3', $data );
$mp3->save();

File::MP3 클래스에서 save() 메서드를 정의하지 않았으므로, Perl은 File::MP3 클래스의 부모 클래스들을 뒤져 save() 메서드를 찾아요. 상속 계층 어디에서도 save() 메서드를 찾지 못하면 Perl은 죽어요(die).

이 경우 Perl은 File 클래스에서 save() 메서드를 찾아요. 여기서 주의할 점: 이 경우 save()에 전달된 객체는 메서드가 File 클래스에서 발견됐음에도 여전히 File::MP3 객체라는 거예요.

자식 클래스에서 부모의 메서드를 재정의(override)할 수 있어요. 그럴 때도 SUPER 사이비 클래스로 부모 클래스의 메서드를 여전히 호출할 수 있어요.

sub save {
    my $self = shift;

    say 'Prepare to rock';
    $self->SUPER::save();
}

SUPER 수식어는 메서드 호출에 쓸 수 있어요. 일반 서브루틴 호출이나 클래스 메서드에는 쓸 수 없어요.

SUPER::save($thing);     # FAIL: looks for save() sub in package SUPER

SUPER->save($thing);     # FAIL: looks for save() method in class
                         #       SUPER

$thing->SUPER::save();   # Okay: looks for save() method in parent
                         #       classes

SUPER가 어떻게 해석되는가 (How SUPER is Resolved)

SUPER 사이비 클래스는 호출이 이루어진 패키지에서 해석돼요. 객체의 클래스에 기반해서 해석되지 않아요. 이것은 중요해요. 깊은 상속 계층의 서로 다른 레벨에 있는 메서드들이 각각 자기 부모 메서드를 올바르게 호출할 수 있게 해 주니까요.

package A;

sub new {
    return bless {}, shift;
}

sub speak {
    my $self = shift;

    say 'A';
}

package B;

use parent -norequire, 'A';

sub speak {
    my $self = shift;

    $self->SUPER::speak();

    say 'B';
}

package C;

use parent -norequire, 'B';

sub speak {
    my $self = shift;

    $self->SUPER::speak();

    say 'C';
}

my $c = C->new();
$c->speak();

이 예에서 우리는 다음 출력을 얻어요.

A
B
C

이것은 SUPER가 어떻게 해석되는지 보여 줘요. 객체가 C 클래스로 bless됐음에도 B 클래스의 speak() 메서드는 SUPER::speak()를 호출해서 (객체가 속한 클래스인) C의 부모가 아니라, (메서드 호출이 일어난 클래스인) B의 부모 클래스에서 올바르게 찾는 것을 기대할 수 있어요.

이 패키지 기반 해석이 문제가 될 수 있는 드문 경우가 있어요. 서브루틴을 한 패키지에서 다른 패키지로 복사하면, SUPER 해석은 원래 패키지에 기반해 이루어져요.

다중 상속 (Multiple Inheritance)

다중 상속은 종종 설계 문제를 나타내지만, Perl은 요청하면 항상 스스로 목을 맬 수 있을 만큼 충분한 밧줄을 줘요.

여러 부모를 선언하려면, 그냥 use parent에 여러 클래스 이름을 전달하면 돼요.

package MultiChild;

use parent 'Parent1', 'Parent2';

메서드 해석 순서 (Method Resolution Order)

메서드 해석 순서는 오직 다중 상속의 경우에만 중요해요. 단일 상속의 경우 Perl은 그냥 상속 체인을 따라 올라가며 메서드를 찾아요.

Grandparent
  |
Parent
  |
Child

Child 객체에 메서드를 호출했는데 그 메서드가 Child 클래스에 정의되어 있지 않으면, Perl은 Parent 클래스에서, 필요하면 그다음 Grandparent 클래스에서 그 메서드를 찾아요.

Perl이 이 클래스들 어디에서도 메서드를 찾지 못하면 오류 메시지와 함께 죽어요.

클래스에 부모가 여러 개 있으면 메서드 조회 순서가 더 복잡해져요.

기본적으로 Perl은 메서드를 깊이 우선(depth-first), 왼쪽에서 오른쪽으로 검색해요. 즉 @ISA 배열의 첫 번째 부모에서 시작해서, 그 부모의 모든 부모, 조부모 등을 모두 검색해요. 메서드를 찾지 못하면 원래 클래스의 @ISA 배열의 다음 부모로 가서 거기부터 검색해요.

          SharedGreatGrandParent
          /                    \
PaternalGrandparent       MaternalGrandparent
          \                    /
           Father        Mother
                 \      /
                  Child

위 다이어그램이 주어지면 Perl은 Child, Father, PaternalGrandparent, SharedGreatGrandParent, Mother, 마지막으로 MaternalGrandparent를 검색해요. 이것은 문제가 될 수 있는데, 이제 SharedGreatGrandParent를 그것의 모든 파생 클래스(, MotherMaternalGrandparent를 시도하기 전에)를 확인하기 전에 보기 때문이에요.

mro 프래그마로 다른 메서드 해석 순서를 요청할 수 있어요.

package Child;

use mro 'c3';
use parent 'Father', 'Mother';

이 프래그마는 "C3" 해석 순서로 전환하게 해 줘요. 간단히 말하면 "C3" 순서는 공유 부모 클래스가 절대 자식 클래스보다 먼저 검색되지 않도록 보장해요. 그래서 Perl은 이제 Child, Father, PaternalGrandparent, Mother, MaternalGrandparent, 마지막으로 SharedGreatGrandParent를 검색해요. 단 이것이 "너비 우선(breadth-first)" 검색은 아니라는 점을 명심하세요: 모든 Father 조상들(공통 조상 제외)은 어떤 Mother 조상도 고려되기 전에 검색돼요.

C3 순서는 next 사이비 클래스로 형제(sibling) 클래스의 메서드를 호출할 수도 있게 해 줘요. 이 기능에 대한 자세한 내용은 mro 문서를 보세요.

메서드 해석 캐싱 (Method Resolution Caching)

Perl이 메서드를 검색하면, 이후 호출에서 다시 검색할 필요가 없도록 조회를 캐시해요. 클래스의 부모 클래스를 바꾸거나 클래스에 서브루틴을 추가하면 그 클래스의 캐시가 무효화돼요.

mro 프래그마는 메서드 캐시를 직접 조작하는 몇 가지 함수를 제공해요.

생성자 작성 (Writing Constructors)

앞서 언급했듯이 Perl은 특별한 생성자 문법을 제공하지 않아요. 즉 클래스는 자기 자신의 생성자를 구현해야 해요. 생성자는 그저 새 객체에 대한 참조를 반환하는 클래스 메서드예요.

생성자는 객체를 규정하는 추가 매개변수도 받을 수 있어요. 앞서 사용한 File 클래스의 실제 생성자를 작성해 볼게요.

package File;

sub new {
    my $class = shift;
    my ( $path, $data ) = @_;

    my $self = bless {
        path => $path,
        data => $data,
    }, $class;

    return $self;
}

보시다시피 우리는 path와 파일 데이터를 객체 자체에 저장했어요. 기억하세요, 내부적으로 이 객체는 여전히 그냥 해시예요. 나중에 이 데이터를 조작하기 위해 accessor를 작성할 거예요.

File::MP3 클래스의 경우, 우리가 받은 path가 ".mp3"로 끝나는지 확인할 수 있어요.

package File::MP3;

sub new {
    my $class = shift;
    my ( $path, $data ) = @_;

    die "You cannot create a File::MP3 without an mp3 extension\n"
        unless $path =~ /\.mp3\z/;

    return $class->SUPER::new(@_);
}

이 생성자는 실제 객체 구성을 부모 클래스에 맡겨요.

속성 (Attributes)

속성(attribute)은 특정 객체에 속하는 데이터 조각이에요. 대부분의 객체 지향 언어와 달리, Perl은 속성의 선언과 조작을 위한 특별한 문법이나 지원을 제공하지 않아요.

속성은 종종 객체 자체에 저장돼요. 예를 들어 객체가 익명 해시라면, 속성 이름을 키로 사용해 속성 값을 해시에 저장할 수 있어요.

클래스 밖에서 이러한 해시 키를 직접 참조하는 것이 가능하긴 하지만, 모든 속성 접근을 accessor 메서드로 감싸는 것이 모범 사례로 여겨져요.

이것은 여러 장점이 있어요. accessor는 원래 API를 유지하면서 나중에 객체의 구현을 바꾸기 더 쉽게 만들어 줘요.

accessor는 속성 접근 주변에 추가 코드를 넣을 수 있게 해 줘요. 예를 들어 생성자에서 설정되지 않은 속성에 기본값을 적용하거나, 속성의 새 값이 받아들일 수 있는지 검증할 수 있어요.

마지막으로, accessor를 쓰면 상속이 훨씬 간단해져요. 서브클래스는 부모 클래스가 내부적으로 어떻게 구현되는지 알 필요 없이 accessor를 쓰면 돼요.

Accessor 작성 (Writing Accessors)

생성자와 마찬가지로 Perl은 특별한 accessor 선언 문법을 제공하지 않으므로, 클래스는 명시적으로 작성된 accessor 메서드를 제공해야 해요. 두 가지 흔한 accessor 타입이 있는데, 읽기 전용과 읽기-쓰기예요.

간단한 읽기 전용 accessor는 그저 단일 속성의 값을 가져와요.

sub path {
    my $self = shift;

    return $self->{path};
}

읽기-쓰기 accessor는 호출자가 값을 가져올 뿐 아니라 설정할 수 있게 해 줘요.

sub path {
    my $self = shift;

    if (@_) {
        $self->{path} = shift;
    }

    return $self->{path};
}

더 똑똑하고 안전한 코드에 관한 잠깐의 여담 (An Aside About Smarter and Safer Code)

우리의 생성자와 accessor는 그리 똑똑하지 않아요. $path가 정의됐는지도 확인하지 않고, $path가 유효한 파일시스템 경로인지도 확인하지 않아요.

이런 검사를 손으로 하다 보면 금방 지루해져요. accessor를 여럿 손으로 쓰는 것도 엄청나게 지루하고요. CPAN에는 더 안전하고 간결한 코드를 쓰는 데 도움을 주는 모듈이 많아요. perlootut에서 추천하는 모듈들도 포함해서요.

메서드 호출 변형 (Method Call Variations)

Perl은 지금까지 본 $object->method() 사용 외에도 메서드를 호출하는 다른 몇 가지 방법을 지원해요.

정규화된 이름을 가진 메서드 이름 (Method Names with a Fully Qualified Name)

Perl은 정규화된 이름(패키지와 메서드 이름)으로 메서드를 호출할 수 있게 해 줘요.

my $mp3 = File::MP3->new( 'Regin.mp3', $data );
$mp3->File::save();

File::save 같은 정규화된 메서드 이름을 호출하면, save 메서드의 메서드 해석 검색이 File 클래스에서 시작돼요. File::MP3 클래스가 정의한 어떤 save 메서드든 건너뛰고요. 필요하면 여전히 File 클래스의 부모들을 검색해요.

이 기능은 조상 클래스로부터 상속된 메서드를 명시적으로 호출하는 데 가장 흔히 쓰이지만, 이것을 강제하는 기술적 제약은 없어요.

my $obj = Tree->new();
$obj->Dog::bark();

이것은 Tree 클래스의 객체에 Dog 클래스의 bark 메서드를 호출하는데, 두 클래스가 완전히 무관하더라도 그래요. 매우 주의해서 쓰세요.

앞서 설명한 SUPER 사이비 클래스는 정규화된 이름으로 메서드를 호출하는 것과 같지 않아요. 자세한 내용은 앞의 "상속" 절을 보세요.

문자열을 메서드 이름으로 (Method Names as Strings)

Perl은 문자열을 담은 스칼라 변수를 메서드 이름으로 쓸 수 있게 해 줘요.

my $file = File->new( $path, $data );

my $method = 'save';
$file->$method();

이것은 $file->save()를 호출하는 것과 정확히 똑같이 동작해요. 동적 코드를 작성할 때 매우 유용할 수 있어요. 예를 들어, 호출할 메서드 이름을 다른 메서드에 매개변수로 전달할 수 있게 해 주죠.

문자열을 클래스 이름으로 (Class Names as Strings)

Perl은 문자열을 담은 스칼라를 클래스 이름으로도 쓸 수 있게 해 줘요.

my $class = 'File';

my $file = $class->new( $path, $data );

다시 말해, 이것은 매우 동적인 코드를 가능케 해요.

서브루틴 참조를 메서드로 (Subroutine References as Methods)

서브루틴 참조를 메서드로 쓸 수도 있어요.

my $sub = sub {
    my $self = shift;

    $self->save();
};

$file->$sub();

이것은 $sub->($file)이라고 쓰는 것과 정확히 동등해요. 실제 코드에서 이것이 can 호출과 결합된 관용구를 볼 수 있을 거예요.

if ( my $meth = $object->can('foo') ) {
    $object->$meth();
}

역참조 메서드 호출 (Dereferencing Method Call)

Perl은 메서드 호출에서 역참조된 스칼라 참조를 쓸 수도 있게 해 줘요. 입에 달라붙는 말이라, 코드를 좀 보자면:

$file->${ \'save' };
$file->${ returns_scalar_ref() };
$file->${ \( returns_scalar() ) };
$file->${ returns_ref_to_sub_ref() };

이것은 역참조가 문자열 또는 서브루틴 참조를 만들어내면 동작해요.

Filehandle에 대한 메서드 호출 (Method Calls on Filehandles)

내부적으로 Perl filehandle은 IO::Handle 또는 IO::File 클래스의 인스턴스예요. 열려 있는 filehandle을 가지게 되면 그 위에서 메서드를 호출할 수 있어요. 추가로 STDIN, STDOUT, STDERR filehandle에서도 메서드를 호출할 수 있어요.

open my $fh, '>', 'path/to/file';
$fh->autoflush();
$fh->print('content');

STDOUT->autoflush();

클래스 메서드 호출 (Invoking Class Methods)

Perl이 패키지 이름과 서브루틴 이름에 bareword를 쓰는 것을 허용하기 때문에, 때로는 bareword의 의미를 잘못 해석하기도 해요. 예를 들어 Class->new() 구조는 'Class'->new() 또는 Class()->new()로 해석될 수 있어요. 말로 하면 두 번째 해석은 "Class()라는 서브루틴을 호출한 다음, Class()의 반환 값에 대해 new()를 메서드로 호출하라"는 뜻이에요. 현재 이름공간에 Class()라는 서브루틴이 있으면, Perl은 항상 Class->new()를 두 번째 대안으로 해석해요: Class() 호출이 반환한 객체에 대한 new() 호출.

Perl이 첫 번째 해석(즉 "Class"라는 이름의 클래스에 대한 메서드 호출)을 쓰도록 강제하는 방법은 두 가지가 있어요. 첫째, 클래스 이름에 ::를 덧붙이는 거예요.

Class::->new()

Perl은 항상 이것을 메서드 호출로 해석해요.

또는 클래스 이름을 인용할 수 있어요.

'Class'->new()

물론 클래스 이름이 스칼라에 있으면 Perl도 올바르게 동작해요.

my $class = 'Class';
$class->new();

간접 객체 문법 (Indirect Object Syntax)

파일 핸들 경우를 제외하면, 이 문법은 Perl 인터프리터를 헷갈리게 할 수 있으므로 사용이 권장되지 않아요. 아래에서 자세한 내용을 보세요.

Perl은 "간접 객체(indirect object)" 표기법이라 불리는 또 다른 메서드 호출 문법을 지원해요. 메서드가 호출되는 객체보다 앞에 오기 때문에 "간접적"이라고 불러요.

이 문법은 어떤 클래스나 객체 메서드와 함께 쓸 수 있어요.

my $file = new File $path, $data;
save $file;

여러 이유로 이 문법을 피할 것을 권장해요.

첫째, 읽기에 헷갈릴 수 있어요. 위 예에서 saveFile 클래스가 제공하는 메서드인지, 그저 파일 객체를 첫 번째 인자로 기대하는 서브루틴인지 명확하지 않아요.

클래스 메서드와 함께 쓰면 문제는 더 심각해요. Perl이 서브루틴 이름을 bareword로 쓰는 것을 허용하기 때문에, Perl은 메서드 다음의 bareword가 클래스 이름인지 서브루틴 이름인지 추측해야 해요. 다시 말해 Perl은 그 문법을 File->new( $path, $data ) 또는 new( File( $path, $data ) )로 해석할 수 있어요.

이 코드를 파싱하기 위해 Perl은 본 적 있는 패키지 이름, 현재 패키지에 존재하는 서브루틴, 이전에 본 bareword, 그 외 입력에 기반한 휴리스틱을 사용해요. 말할 필요도 없이, 휴리스틱은 매우 놀라운 결과를 낼 수 있어요!

오래된 문서(와 일부 CPAN 모듈)는 특히 생성자에서 이 문법을 권장했으므로, 여전히 실제 코드에서 찾을 수 있을 거예요. 하지만 새 코드에서는 사용을 피할 것을 권장해요.

앞서 본 것처럼 bareword에 ::를 덧붙여 Perl이 클래스 이름으로 해석하도록 강제할 수 있어요.

my $file = new File:: $path, $data;

간접 객체 문법은 "indirect" 이름 기능이 활성화돼 있을 때만 사용할 수 있어요. 이것은 기본적으로 활성화되어 있지만, 요청하면 비활성화할 수 있어요. 이 기능은 오래된 기능 버전 번들에 존재했지만 :5.36 번들에서 제거됐어요. 그래서 v5.36 이상의 use VERSION 선언도 기능을 비활성화해요.

use v5.36;
# indirect object syntax is no longer available

bless, blessed, ref

앞서 봤듯이 객체는 bless 함수를 통해 클래스로 bless된 데이터 구조일 뿐이에요. bless 함수는 인자를 하나 또는 둘 받을 수 있어요.

my $object = bless {}, $class;
my $object = bless {};

첫 번째 형태에서는 익명 해시가 $class의 클래스로 bless돼요. 두 번째 형태에서는 익명 해시가 현재 패키지로 bless돼요.

두 번째 형태는 서브클래스가 부모의 생성자를 재사용하는 능력을 깨뜨리기 때문에 강력히 권장되지 않지만, 기존 코드에서 마주칠 수는 있어요.

특정 스칼라가 객체를 참조하는지 알고 싶다면, Perl 코어와 함께 제공되는 Scalar::Util이 내보내는 blessed 함수를 쓸 수 있어요.

use Scalar::Util 'blessed';

if ( defined blessed($thing) ) { ... }

$thing이 객체를 참조하면, 이 함수는 객체가 bless된 패키지의 이름을 반환해요. $thing이 bless된 객체에 대한 참조를 담고 있지 않으면 blessed 함수는 undef를 반환해요.

blessed($thing)$thing이 "0"이라는 이름의 클래스로 bless된 경우에도 false를 반환한다는 점을 유의하세요. 이것은 가능하지만 꽤 병리적이에요. 뭘 하고 있는지 알지 못한다면 "0"이라는 이름의 클래스를 만들지 마세요.

비슷하게 Perl 내장 ref 함수는 bless된 객체에 대한 참조를 특별히 취급해요. ref($thing)을 호출했는데 $thing이 객체에 대한 참조를 담고 있으면, 객체가 bless된 클래스의 이름을 반환해요.

변수가 객체 참조를 담고 있는지 간단히 확인하려면 defined blessed($object)를 쓰는 걸 권장해요. ref는 객체뿐 아니라 모든 참조에 대해 참 값을 반환하니까요.

UNIVERSAL 클래스 (The UNIVERSAL Class)

모든 클래스는 Perl 코어에 내장된 UNIVERSAL 클래스로부터 자동으로 상속받아요. 이 클래스는 여러 메서드를 제공하는데, 모두 클래스나 객체에서 호출할 수 있어요. 여러분의 클래스에서 이 메서드들 중 일부를 재정의할 수도 있어요. 그럴 때는 아래에 설명된 내장 의미론을 따르는 걸 권장해요.

isa($class)

isa 메서드는 객체가 $class의 클래스나 $class의 서브클래스의 구성원이면 을 반환해요.

이 메서드를 재정의한다면 절대 예외를 던지면 안 돼요.

DOES($role)

DOES 메서드는 객체가 역할(role) $role을 수행한다고 주장하면 을 반환해요. 기본적으로 이것은 isa와 동등해요. 이 메서드는 MooseRole::Tiny 같은 역할을 구현하는 객체 시스템 확장이 사용하도록 제공돼요.

여러분의 클래스에서 DOES를 직접 재정의할 수도 있어요. 재정의한다면 절대 예외를 던지면 안 돼요.

can($method)

can 메서드는 그것이 호출된 클래스나 객체에 $method라는 메서드가 있는지 확인해요. 이것은 클래스와 모든 부모들에서 메서드를 확인해요. 메서드가 존재하면 서브루틴에 대한 참조가 반환돼요. 존재하지 않으면 undef가 반환돼요.

클래스가 AUTOLOAD로 메서드 호출에 응답한다면, can을 오버로드해서 AUTOLOAD 메서드가 처리하는 메서드들에 대해 서브루틴 참조를 반환하고 싶을 거예요.

이 메서드를 재정의한다면 절대 예외를 던지면 안 돼요.

VERSION($need)

VERSION 메서드는 클래스(패키지)의 버전 번호를 반환해요.

$need 인자가 주어지면 현재 버전(패키지의 $VERSION 변수로 정의된)이 $need보다 크거나 같은지 확인하고, 그렇지 않으면 죽어요. 이 메서드는 useVERSION 형태가 자동으로 호출해요.

use Package 1.2 qw(some imported subs);
# implies:
Package->VERSION(1.2);

다른 패키지의 버전에 접근할 때는 $Package::VERSION을 직접 보는 대신 이 메서드를 쓰는 걸 권장해요. 보고 있는 패키지가 VERSION 메서드를 재정의했을 수 있거든요.

또한 모듈이 충분한 버전을 갖고 있는지 확인할 때도 이 메서드를 쓰는 걸 권장해요. 내부 구현은 version 모듈을 사용해 서로 다른 타입의 버전 번호가 올바르게 비교되도록 보장해요.

AUTOLOAD

클래스에 존재하지 않는 메서드를 호출하면 Perl은 오류를 던져요. 하지만 그 클래스나 그 부모 클래스 중 하나가 AUTOLOAD 메서드를 정의하면, 그 AUTOLOAD 메서드가 대신 호출돼요.

AUTOLOAD는 일반 메서드로 호출되며, 호출자는 차이를 알지 못해요. AUTOLOAD 메서드가 반환하는 값이 무엇이든 호출자에게 반환돼요.

호출된 정규화된 메서드 이름은 여러분 클래스의 $AUTOLOAD 패키지 전역에서 사용할 수 있어요. 이것은 전역이므로, strict 'vars' 아래에서 패키지 이름 접두사 없이 참조하려면 선언해야 해요.

# XXX - this is a terrible way to implement accessors, but it makes
# for a simple example.
our $AUTOLOAD;
sub AUTOLOAD {
    my $self = shift;

    # Remove qualifier from original method name...
    my $called =  $AUTOLOAD =~ s/.*:://r;

    # Is there an attribute of that name?
    die "No such attribute: $called"
        unless exists $self->{$called};

    # If so, return it...
    return $self->{$called};
}

sub DESTROY { } # see below

our $AUTOLOAD 선언이 없으면 이 코드는 strict 프래그마 아래에서 컴파일되지 않아요.

주석이 말했듯이 이것은 accessor를 구현하는 좋은 방법이 아니에요. 느리고 너무 영리하기까지 해요. 하지만 오래된 Perl 코드에서 accessor를 제공하는 방법으로 이것을 볼 수 있을 거예요. Perl에서 OO 코딩에 대한 권장 사항은 perlootut을 보세요.

클래스에 AUTOLOAD 메서드가 있다면, 클래스에서 can도 재정의할 것을 강력히 권장해요. 재정의된 can 메서드는 AUTOLOAD가 응답하는 어떤 메서드에 대해서도 서브루틴 참조를 반환해야 해요.

소멸자 (Destructors)

객체에 대한 마지막 참조가 사라지면 객체는 파괴돼요. 어휘 스칼라에 저장된 객체에 대한 참조가 하나만 있다면, 그 스칼라가 스코프를 벗어날 때 객체가 파괴돼요. 객체를 패키지 전역에 저장하면, 그 객체는 프로그램이 끝날 때까지 스코프를 벗어나지 않을 수 있어요.

객체가 파괴될 때 뭔가를 하고 싶다면 클래스에서 DESTROY 메서드를 정의할 수 있어요. 이 메서드는 메서드가 비어 있지 않으면 Perl이 항상 적절한 때에 호출해요.

이것은 다른 메서드처럼 객체를 첫 번째 인자로 받아 호출돼요. 추가 인자는 받지 않아요. 하지만 $_[0] 변수는 소멸자에서 읽기 전용이므로 값을 할당할 수 없어요.

DESTROY 메서드가 예외를 던지면, 메서드를 벗어나는 것 이상의 제어 이동(control transfer)은 일어나지 않아요. 예외는 "(in cleanup)"으로 표시된 경고로 STDERR에 보고되고, Perl은 전에 하고 있던 일을 계속해요.

DESTROY 메서드는 언제든 호출될 수 있으므로, DESTROY 메서드에서 하는 어떤 일이 설정할 수 있는 전역 상태 변수는 모두 지역화(localize)해야 해요. 특정 상태 변수에 대해 확신이 없으면 지역화해도 나쁠 건 없어요. 다섯 개의 전역 상태 변수가 있고, 가장 안전한 방법은 다섯 개 모두를 지역화하는 거예요.

sub DESTROY {
    local($., $@, $!, $^E, $?);
    my $self = shift;
    ...;
}

클래스에 AUTOLOAD를 정의하면, Perl은 DESTROY 메서드를 처리하기 위해 AUTOLOAD를 호출해요. autoload 예에서 했던 것처럼 빈 DESTROY를 정의하면 막을 수 있어요. $AUTOLOAD의 값을 확인하고 DESTROY를 처리하라는 호출일 때는 아무것도 하지 않고 반환할 수도 있어요.

전역 파괴 (Global Destruction)

프로그램이 끝나기 전의 전역 파괴 중에 객체가 파괴되는 순서는 예측할 수 없어요. 즉 여러분의 객체가 담고 있는 어떤 객체든 이미 파괴되었을 수 있어요. 담긴 객체에 메서드를 호출하기 전에 정의됐는지 확인해야 해요.

sub DESTROY {
    my $self = shift;

    $self->{handle}->close() if $self->{handle};
}

${^GLOBAL_PHASE} 변수로 현재 전역 파괴 단계에 있는지 감지할 수 있어요.

sub DESTROY {
    my $self = shift;

    return if ${^GLOBAL_PHASE} eq 'DESTRUCT';

    $self->{handle}->close();
}

이 변수는 Perl 5.14.0에서 추가됐다는 점을 유의하세요. 이전 Perl 버전에서 전역 파괴 단계를 감지하고 싶다면 CPAN의 Devel::GlobalDestruction 모듈을 쓰면 돼요.

DESTROY 메서드가 전역 파괴 중에 경고를 내면, Perl 인터프리터는 경고에 " during global destruction" 문자열을 덧붙여요.

전역 파괴 중에 Perl은 항상 bless되지 않은 참조보다 객체를 먼저 가비지 컬렉션해요. 전역 파괴에 대한 더 자세한 내용은 perlhacktips의 "PERL_DESTRUCT_LEVEL"을 보세요.

비-해시 객체 (Non-Hash Objects)

지금까지의 모든 예제는 bless된 해시에 기반한 객체를 보여 줬어요. 하지만 스칼라, glob, 서브루틴을 포함한 어떤 타입의 데이터 구조나 참조 대상도 bless하는 것이 가능해요. 실제 코드를 볼 때 이런 종류의 것을 보게 될 수도 있어요.

bless된 스칼라로서의 모듈 예제를 볼게요.

package Time;

use v5.36;

sub new {
    my $class = shift;

    my $time = time;
    return bless \$time, $class;
}

sub epoch {
    my $self = shift;
    return $$self;
}

my $time = Time->new();
print $time->epoch();

Inside-Out 객체 (Inside-Out objects)

과거에 Perl 커뮤니티는 "inside-out 객체"라는 기법을 실험했어요. inside-out 객체는 객체 참조의 바깥, 객체 자체가 아니라 객체의 메모리 주소 같은 고유한 속성으로 인덱스된 곳에 데이터를 저장해요. 이것은 객체 속성의 캡슐화를 강제한다는 장점이 있는데, 데이터가 객체 자체에 저장되지 않으니까요.

이 기법은 한동안 인기를 끌었지만(데이미언 콘웨이의 _Perl Best Practices_에서 권장되기도 했고), 보편적으로 채택되지는 못했어요. CPAN의 Object::InsideOut 모듈이 이 기법의 포괄적인 구현을 제공하며, 실제 코드에서 이것이나 다른 inside-out 모듈을 볼 수 있을 거예요.

코어 모듈 Hash::Util::FieldHash를 사용한 간단한 예제를 볼게요. 이 모듈은 inside-out 객체 구현을 지원하기 위해 코어에 추가됐어요.

package Time;

use v5.36;

use Hash::Util::FieldHash 'fieldhash';

fieldhash my %time_for;

sub new {
    my $class = shift;

    my $self = bless \( my $object ), $class;

    $time_for{$self} = time;

    return $self;
}

sub epoch {
    my $self = shift;

    return $time_for{$self};
}

my $time = Time->new;
print $time->epoch;

사이비 해시 (Pseudo-hashes)

사이비 해시 기능은 이전 Perl 버전에서 도입된 실험적 기능이었고 5.10.0에서 제거됐어요. 사이비 해시는 해시처럼 명명된 키로 접근할 수 있는 배열 참조예요. 실제 코드에서 이것을 사용하는 코드를 마주칠 수도 있어요. 더 자세한 정보는 fields 프래그마를 보세요.

더 알아보기 (SEE ALSO)

Perl에서 객체 지향 프로그래밍에 대한 더 친절하고 온화한 튜토리얼은 perlootut에서 찾을 수 있어요. 모듈과 클래스 둘 다 구축하기 위한 몇 가지 스타일 가이드는 perlmodlib도 확인해 보세요.

더 알아보기