이 문서는 http://gihyo.jp/dev/serial/01/perl-hackers-hub/001601 이것을 번역한 문서입니다.

Perl의 내부 구조에 관한 문서입니다.

내부 구조를 들여다보다

Perl에서 개발을하고 있으면 가끔 이해하기 어려운 현상이 발생 할 수 있습니다. 예를 들어, 데이터를 JSON ( JavaScript Object Notation )으로 변환하려고 할 때 숫자로 취급해 주었으면 값이 문자열로 시리얼라이즈되어 버립니다. 또는 인코딩이 올바른 것인데 깨져서 나옵니다. 이런 때는 과감하게 Perl의 내부 구조를 들여다 보는 것으로, 무엇이 일어나고 있는지를 알 수 있습니다.

본 문서에서는 Perl의 내부 구조에 대해 설명합니다. 사용할 버전은 perl 버전 5.16.0 (2012 년 5 월 21 일 출시)입니다. 또한 터미널의 인코딩은 UTF-8을 설정하고 있습니다. 또한 Perl 처리 시스템은 C 언어로 쓰여져 있지만 본문서에서는 C 언어의 지식은 필요하지는 않습니다.

perl ── Perl 구현체의 구현에 관하여

본론으로 넘어가기 전에 Perl 구현체에 대해 조금 설명하겠습니다. Perl5 언어의 구현체는 사실상 하나 밖에없고,이 구현은 모두 소문자로 "perl"라고 씁니다. 본고에서도 이후 perl과 쓸 때이 구현을 말합니다. perl은 전술 한 바와 같이 C로 작성된 소프트웨어입니다. 대량의 매크로에 의해 익숙해지지 않으면 매우 알기 어렵지만, 하나하나 읽고 쓰면 이해는 갑니다.

무엇보다, 본문서에서는 소스 코드를 거의 참조하지 않습니다(실제 디버깅에서도 소스 코드를 참조해야한다 케이스는 드물다.) 그럼에도 불구하고 지식으로 내부 구조를 알고, 내부 구조를 조사 도구를 이용하여 문제를 쫓아 갈 수 있습니다.

SV ( Scalar Value ) 구조체와 Devel :: Peek의 읽는 법

그런데 첫 번째 테마는 SV 구조체 ( 주 1 )입니다. SV 구조는 Perl 값의 실체입니다. Perl의 값은 대부분의 경우 내용이 수치이다 든가 문자열이라는 것을 의식하지 않고 사용할 수 있지만 그것은 SV 구조가 그렇게 정의되어 있기 때문입니다. 즉 SV 구조를 살펴보면 perl 값의 거동을 모두 다 알 수 있다고 할 수 있습니다

주 1) 구조체는 C의 데이터 형의 일종으로, Perl의 해시 참조처럼 이름 값 쌍 여러 이루어진 데이터입니다.

JSON 직렬화 문제

SV 구조를 살펴보기 전에 먼저 SV 구조체 구현에 기인 한 문제를 따라가 보겠습니다.

목록 1 의 코드를 실행 해 보겠습니다. 이것은 JSON :: PP 모듈 ( 주 2 )를 사용하여 Perl 데이터 구조를 JSON으로 직렬화하는 코드입니다.

*목록 1* buggy-json-serialize.pl

use strict;
use warnings;
use feature 'say';
use JSON :: PP qw (encode_json);
$| = 1; # autoflush

my % data = (
    answer => 42,
);

say "the answer : $ data {answer}"; # (a)

say encode_json (\ % data);
# 실행 결과 :
# The answer : 42
# { "answer": "42"}

※ $| = 1 은 STDOUT의 autofush를 사용하는 것입니다. 이것이 없으면, STDOUT에 출력 say () 와 STDERR에 출력하는 Dump() 가 올바른 순서로 표시되지 않을 수 있기때문에 사용하였습니다.

실행 결과를 보면 JSON으로 시리얼라이즈된 "answer" 값이 문자열 "42" 로되어 있습니다. JSON은 숫자와 문자열을 구분하는 형식이기 때문에, 숫자로 넣은 것은 숫자로 직렬화되어야하지만 의도한 대로되지 않습니다. (a) 행을 주석 처리하면 의도대로 숫자로 직렬화됩니다. 이 문제는 Web API의 응답으로 JSON을 반환하는 응용 프로그램에서 일어나기 쉽습니다. 일반적으로 숫자로 출력해야할 것은 0 + $ data {answer} 등으로 확실하게 숫자로하고 데이터 구조를 만들고 그 즉시 시리얼라이즈하여 해결할 수 있다고 되어 있습니다.

왜 이런 문제가 일어나는 것입니까? "확실하게 숫자로 한다"라는 것은 무슨 뜻입니까? 원래 Perl 값에 숫자 나 문자열 같은 구별은있는 것입니까? 있다면 어떻게 구별되는 것입니까? 이 질문에 대답하기 위해 SV 구조를 살펴볼 필요가 있습니다.

주 2) JSON : PP 모듈은 Perl v5.14.0에서 표준 모듈입니다.

SV 구조의 조사 방법

perl의 기본 데이터 구조인 SV를 Perl에서 보는 방법에는 두 가지가 있습니다. Devel :: Peek 모듈 검사 방법과 B 모듈에서 확인하는 방법입니다.

Devel :: Peek 모듈 검사

우선 Devel :: Peek를 다음 명령으로 사용해 보자.

$ perl-MDevel :: Peek-e 'Dump "foo"'

출력 결과는 그림 1 과 같이됩니다. 복잡해 보이기는 하지만 모든 행에 의미가 있습니다.

*그림 1* Devel::Peek::Dump("foo")의 실행 결과

001 : SV = PV (0x7fc474001ea0) at 0x7fc47402ac40
002 : REFCNT = 1
003 : FLAGS = (PADTMP, POK, READONLY, pPOK)
004 : PV = 0x10c702af0 "foo"\ 0
005 : CUR = 3
006 : LEN = 16

첫 번째 행 SV = PV (0x. .. a0) at 0x ... 40 은 SV가 PV임을 보여줍니다. PV는 포인터 값 ( Pointer Value ) ( 주 3 )의 약자로 텍스트 문자열이나 바이트를위한 SV 형입니다. 0x ... 로 시작하는 16 진수 값은 C 수준의 주소로 각각 PV (0x. .. a0) 가 SV 바디의 주소 at 0x ... 40 이 SV 헤더의 주소입니다. SV 바디는 SV 별 데이터를 소유하고있는 구조입니다. SV 헤더는 참조 횟수와 SV마다 공통되는 플래그를 소유하는 구조입니다. SV 헤더는 SV 바디의 포인터 ( 주 4 )도 소유하고 있습니다.

두 번째 행의 REFCNT는 SV의 레퍼런스 카운터에서 SV의 참조가 1임을 보여줍니다. SV는 레퍼런스 카운팅 GC ( Garbage Collection )라는 구조로 관리되고 있으며, 예를 들어 Perl의 개체 값이 참조되지 않게되었을 때 즉시 소멸자가 호출되는 것은 GC의 일 때문입니다. 이 레퍼런스 카운터는 모든 SV에 필요하기 때문에, SV 헤더가 소유하고 있습니다.

세 번째 줄의 FLAGS도 모든 SV에 필요한 데이터이기때문에 SV 헤더가 소유하고 있습니다. FLAGS에는 SV 의 형식과 다양한 속성이 들어 있습니다. 예를 들어 PADTEMP는 값이 리터럴이다, POK와 pPOK 문자열로 참조 가능한 상태에있는 것, READONLY 값이 불변임을 의미합니다 ( 주 5 ).

4 행째 이후의 필드, 즉 PV, CUR, LEN은 PV 시스템 관련 데이터에서 각 문자열 버퍼, 문자열의 이진 표현의 크기, 확보하고있는 메모리 영역의 크기를 나타냅니다.

B 모듈 검사

이 문자열 "foo" 를 나타내는 SV를 만일 Perl로 표현하면 목록 2 처럼 될 것입니다. BODY 부분은 SV의 형태에 따라서 다양한 데이터가 들어갑니다.

*목록 2* Perl로 표현하는 SV 구조

my $ sv = bless {
    REFCNT => 1,
    FLAGS => [qw (PADTMP POK READONLY pPOK)
    BODY => {
        PV => "foo"
        CUR => 3,
        LEN => 16,
    }
} "SV :: PV";

Devel :: Peek는 SV의 내용을 텍스트로 성형하여 콘솔에 출력 할뿐입니다. 하지만 B 모듈에서는 이러한 인터페이스 SV에 액세스 할 수 있습니다. 목록 3 은 B 모듈로 SV의 구조를 조사하는 프로그램입니다.

FLAGS가 정수인 것을 제외하고 Devel :: Peek과 거의 동일한 정보를 얻을 수 있습니다. 프로그램에서 작업을 하려면 Devel :: Peek보다 B가 좋지만, FLAGS을 이름으로 표시 해주는 디버깅에서는 Devel :: Peek를 사용하는 것이 더 쉽습니다. 본 문서에서도 이후 Devel :: Peek를 사용합니다.

*목록 3* use-B.pl
use strict;
use warnings;
use feature 'say';
use B qw (svref_2object);

my $ sv = svref_2object (\ "foo");
say ref ($ sv); # B : PV
say $ sv-> FLAGS; # 134235140
say $ sv-> PV; # foo
say $ sv-> CUR; # 3
say $ sv-> LEN; # 16

주 3) C의 포인터에는 다양한 의미가 있습니다 만, PV의 포인터는 단순히 문자열을위한 버퍼라는 의미입니다. 주 4) SV 바디의 포인터와 Perl 참조와 마찬가지로, 다른 데이터 구조에 대한 참조입니다. 주 5) 문자열 리터럴은 불변이지만, 이것을 변수에 복사하여 READONLY 플래그를 피해서 변경 가능합니다.

다시 JSON 시리얼라이즈 문제를 쫓는

이제 JSON 시리얼라이즈 문제를 추적 할 수 있습니다. 문제의 부분을 Devel :: Peek에서 살펴 봅시다. 목록 4목록 1에서 2 개의 Dump () 를 더한 것입니다.

*목록 4* buggy-json-serialize-with-devel-peek.pl
use strict;
use warnings;
use feature 'say';
use JSON :: PP qw (encode_json);
use Devel :: Peek;
$ | = 1; # autoflush

my % data = (
    answer => 42,
);

Dump $ data {answer}; # (a)
say "the answer : $ data {answer}"; # (b)
Dump $ data {answer}; # (c)

say encode_json (\ % data);

결과는 그림 2 와 같이됩니다. $data {answer} 을 만지기 전에, 즉 (a) 의 지점에서이 SV의 형태는 IV이었습니다 (그림 2의 첫 번째 줄). IV는 정수 ( Integer Value )입니다. 그런데 목록 4 (b) 에서 $ data {answer}에 닿은 후 목록 4 (c) 의 지점에서 SV가 PVIV되어 있습니다 (그림 2의 6 번째 줄). PVIV는 PV와 IV 쌍방의 성질을 갖는 값입니다. 즉, 값에 액세스 한 것만으로 SV 바디의 내용이 바뀌고 있습니다. SV 헤더는 모두 at0x7fccc3003ce8이며 변화는 없습니다.

*그림 2* Listing 4의 결과
001 : SV = IV (0x7fccc3003cd8) at 0x7fccc3003ce8
002 : REFCNT = 1
003 : FLAGS = (IOK, pIOK)
004 : IV = 42
005 : the answer : 42
006 : SV = PVIV (0x7fccc3038238) at 0x7fccc3003ce8
007 : REFCNT = 1
008 : FLAGS = (IOK, POK, pIOK, pPOK)
009 : IV = 42
010 : PV = 0x10c5b2ca0 "42"\ 0
011 : CUR = 2
012 : LEN = 16
013 { "answer": "42"}

이 SV 바디의 변화가 JSON 직렬화의 결과에 영향을주고있는 것입니까?

SV 업그레이드

이 SV 바디의 변화는 SV의 업그레이드라고하고, 다른 스크립트 언어로 볼 수없는 perl (또는 Perl)에 특유의 현상입니다. Perl의 값은 문자열로 처리하면 문자열로 동작하고 숫자로 취급하면 숫자로 행동하는 성질을 가지고 있습니다. 즉, 값이 문자열인지 숫자인지는 프로그래머의 판단에 맡길 수 있습니다.

그러나 perl을 구현하는 C 언어에서는 문자열과 숫자는 전혀 다른 데이터입니다. 따라서 SV 형의 내용이 C 수치였다해도, Perl에서 문자열로 참조 할 때 자동으로 C 수치에서 C 문자열을 생성합니다. 이때 생성 결과를 재사용하기 위해 숫자 나 문자열 등 여러 값이 들어가는 SV의 바디를 할당합니다. 이 작업이 SV의 업그레이드입니다. 예를 들어, 목록 4 (b) 는 IV에서 PVIV로 업그레이드가 일어나고 있습니다.

SV 업그레이드가 발생하면 이전의 형식이 무엇 이었는가를 확인할 방법은 없습니다. 목록 5 를 보면 IV에서 PVIV로 업그레이드도 PV에서 PVIV로 업그레이드도 결과 PVIV만을 보면 각종 주소 값 이외는 동일합니다.

*목록 5* sv-upgrade.pl
use strict;
use warnings;
use Devel :: Peek;
{
    my $ sv = 10; # IV
    my $ dummy = "$ sv"; # upgrade to PVIV

    Dump $ sv;
    # 실행 결과 (발췌) :
    # SV = PVIV (0x7fe1bc0381f0) at 0x7fe1bc02abb0
    # FLAGS = (PADMY, IOK, POK, pIOK, pPOK)
}
{
    my $ sv = "10"; # PV
    my $ dummy = int $ sv; # upgrade to PVIV

    Dump $ sv;
    # 실행 결과 (발췌) :
    # SV = PVIV (0x7fe1bc038208) at 0x7fe1bc02ac10
    # FLAGS = (PADMY, IOK, POK, pIOK, pPOK)
}

SV 업그레이드가 일어나는 조건은 법칙이없는 것처럼 보입니다. 예를 들어, say "the answer : $ data {answer}" 는 SV 업그레이드가 일어나고 있지만 say "the answer :", $ data {answer} 는 SV 업그레이드는 발생하지 않습니다. 사용하실때에는 항상 값에 액세스 할 때마다 SV 업그레이드가 일어날 가능성이 있다고 생각해도 좋을 것입니다.

업그레이드의 반대 작업

그런데 업그레이드 한 것은 돌이킬 수없는 것입니까? 예를 들어 PVIV을 IV와 PV에 되돌릴 수없는 것일까요? 이것은 새로운 값을 다시 만드는 것으로 그 때 필요한 데이터 만 저장되는 가장 "작은" SV 형을 얻을 수 있습니다. 예를 들어, 0 + $value는 IV없고 NV ( Number Value )를 "" . $value는 PV를 얻는 작업입니다.

SV 구조 정리와 JSON 직렬화 문제의 진상

그런데이 근처에서 한 번 SV 대해 정리합시다.

* Perl의 값의 구현인 SV는 C의 데이터 구조이다 * SV에서는 문자열과 숫자는 다른 자료이다 * SV는 필요에 따라 자동으로 업그레이드  되고* SV 업그레이드는 값을 사용하면 언제든지 발생할 수있다 (읽기만 해도) * 일단 SV 업그레이드가 발생하면 업그레이드 이전 형식의 정보가 손실된다 * SV를 다시 생성하여 특정 내부 데이터 형식의 SV를 얻을 수있다

이제 JSON 시리얼라이즈 문제의 원인이 보이기 시작했습니다. 아무래도이 문제는 "한 번 업그레이드가 발생하면 업그레이드 이전 형식의 정보가 손실 된다"라는 것이 관련 있다고합니다. 즉, 예를 들어 serializer가 PVIV 값을 볼 때이를 문자열로 직렬화하거나 숫자로 직렬화하거나 결정해야합니다. 그러나 SV 사양에있는 PVIV이 원래 IV인지 PV인지 알 수 없습니다.

바로 이러한 사양을 위해, JSON :: PP 모듈은 특정 값이 비록 숫자로 만들어진 것이었다해도 문자열로 해석 가능하면 항상 문자열로 직렬화하는 것입니다. 그리고 이것은 JSON::PP가 원래 JSON::XS는 XS 모듈, 즉 C에서 구현되는 perl의 API를 사용하여 SV에 액세스하는 모듈이며, 따라서 perl의 구현 형편이 표면에 나와 버리고 있다고 말할 수 있겠지요.

이 문제가 일어나는 것은, 앞서 언급 한 바와 같이 데이터 구조를 구축하고 나서 더 값에 액세스 할 때입니다. 0+$value는 숙어는 확실히 IV또는 NV를 얻기 위한 것이었다는 것입니다. 그리고 일단 시리얼라이즈에 대한 데이터 구조를 구축 한 후 그 값에 액세스하지 말라 것도 이해할 수 있었다고 생각합니다.

SV 형과 FLAGS의 관계

그런데 지금까지 SV에 PV 및 IV, PVIV 등으로 언급하고 이러한 형식은 SV가 가지고있는 정보를 표현하고있는 것처럼 설명했습니다. 설명을 간단하게하기 위해 세부 사항을 생략하고있었습니다 만, 실은 SV의 형태와 SV의 소유 데이터는 반드시 일치하지 않습니다. 즉, SV의 형태가 PV 다라고고해서 반드시 유효한 문자열이 아닐수도 있으며 PVIV가 유효한 문자열과 정수를 모두 포함하고 있다고도 할 수 없습니다. SV 형식이 PVIV이라는 것은 문자열을 정수도 저장할 수 있다는 가능성을 보여뿐입니다.

즉, SV 관해서보아야 정보는 SV의 형식이 아닌 FLAGS입니다. 그림 2를 다시 봐. 세 번째 줄은 FLAGS = (IOK, pIOK) 이 있습니다. 이 SV는 SV의 형태가 IV 그래서 정수인 것이 아니라 IOK 플래그가 있기 때문에 정수로 유효한 것입니다. SV 유형을 IV로하면서 정수로 잘못된 값을 가질 수 있습니다. 목록 6 에서는 그러한 값을 만들고 있습니다.

*목록 6* iv-but-undef.pl
use strict;
use warnings;
use Devel :: Peek;

my $ s = 10;
$ s = undef;
Dump $ s;
# 실행 결과 (발췌) :
# SV = IV (0x7ff4ca02abe8) at 0x7ff4ca02abf8
# FLAGS = (PADMY)
# IV = 10

SV 형은 IV의 상태에서 IV 필드에 10이 들어가 있습니다 만, FLAGS에서 IOK이 꺼져 있습니다. Perl 코드를 보면 알 수 있듯이,이 변수의 값은 undef입니다. SV의 내부 에까지 들어서 디버깅 할 때, SV 형식에 현혹되어 올바른 해석을 할 수 없다는 것은 일어나기 쉬운 상황입니다. 그러나, 의미 있는 것은 SV의 형식이 아닌 FLAGS입니다.

그러나 SV가 단순한 스칼라가 아닌 경우는 SV 형식이 의미가 있습니다. 배열의 실체 인 AV ( Array Value )는 항상 배열을 의미하고, 해시의 실체 인 HV ( Hash Value )는 FLAGS의 값에 관계없이 항상 해시를 의미합니다. 타입 글로브의 GV ( Glob Value )이나 서브 루틴의 CV ( Code Value )도 마찬가지입니다. 이들을 식별하기 위해서는 SV 의 형식을 보는 것이 옳습니다.

* TO BE Continued ....

-- LuzLuna - 2013-05-16
Topic revision: r4 - 2013-05-16, LuzLuna
 
This site is powered by FoswikiCopyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding Foswiki? Send feedback