스물한번째 날: 번역 - "Modification of a read-only value attempted" 오류가 나는 흔한 경우

저자

@gypark - Perl을 좋아합니다. GyparkWiki의 Perl페이지에서 Perl 관련 문서들을 정리해두고 있습니다.

시작하며

언제나 매개 변수나 반환값이 복사되어 전달되거나 모든 변수가 불변성을 갖는 다른 언어들과 달리, 펄에서는 변수가 단지 다른 값 또는 변수의 별칭(alias)으로 동작하는 경우가 있고, 이런 상황에서 그 변수의 값을 수정하려 시도할 때 예상하지 못한 결과를 낼 수 있습니다. 오늘 기사는 PerlMonks좋은 글 하나(Common Causes for "Modification of a read-only value attempted" by imp, 2006.9.1)를 번역하고 더불어 관련해 제가 겪은 문제에 대해서도 소개합니다.

"Modification of a read-only value attempted" 오류가 나는 흔한 경우

여러분이 직접적으로 또는 간접적으로 상수의 값을 수정하려고 할 때 이 오류가 발생합니다. 이런 형태의 오류는 발생된 지점이 멀리 떨어져 있어서 추적하기 어려울 때가 많습니다. 때로는 $_ 변수의 마술과 관련있지만 여기에 책임을 물을 수는 없습니다.

흔한 경우들을 언급해보면 다음과 같습니다.

루프 변수를 L값으로 다루는 경우

다음 예에서, $x는 상수 1의 별칭(alias)으로 연결되었기 때문에 반복문의 몸통에서 $x의 값을 증가시키려고 시도하면 오류가 납니다. 아래 상수값이 포함된 목록 절에서 더 자세한 내용을 참고하세요.

for my $x ( 1, 2 ) {
    $x++;
}

foreach, map, grep 안에서 $_ 변수를 수정하는 경우

다음 예제 모두 $_는 상수의 별칭이고, 루프의 몸통에서 $_의 값을 수정하려고 하면 오류가 발생합니다. 아래 상수값이 포함된 목록 절에서 더 자세한 내용을 참고하세요.

for ( 1, 2 ) {
    chomp;
}

for ( "foo", @list ) {
    s/foo/bar/;
}

@array = map  { $_++ } (1,2);
@array = grep { $_++ } (1,2);

@_의 원소를 직접 수정하는 경우

@_ 배열의 원소를 직접 수정하면 함수에 인자로 전달된 변수의 값을 수정할 수 있습니다. 예를 들어 위의 예문에서 $n의 값은 이제 2가 되었습니다. 그러나 두 번째 호출처럼 상수가 전달되었을 때는 오류가 날 것입니다.

sub incr {
    $_[0]++;
}

my $n = 1;
incr($n); # 좋음
incr(1);  # 나쁨

sort 안에서 $a, $b를 수정하는 경우

sort 내부에서 $a$b를 수정하는 것(문제의 소지는 있지만)이 허용됩니다. 그러나, $a$b가 상수의 별칭이 된 경우에는 역시 오류가 납니다.

@array = sort { $a++ } (1,2);

sort 안에서 $a,$b를 autovivify하는 경우

변수 $a$b는 정렬할 목록의 각 원소에 별칭으로 연결되며, 이와 같이 수정하는 것도 가능합니다. 그러나 현재 연결된 원소가 수정할 수 없는 원소라면 오류가 납니다.

my @bad;
$bad[0] = [1];
$bad[2] = [2];
@bad = sort {$a->[0] <=> $b->[0]} @bad;

이런 경우가 생기는 흔한 이유 중 하나는 레퍼런스의 배열을 정렬하는데 배열의 중간에 빠진 부분이 있는 경우입니다. 이런 경우 $aundef이 되고, 여기에 디레퍼런스를 수행하여 autovivify하려고 하면 오류가 발생합니다.

지역화되지 않은 $_를 수정하는 경우

다음 예제에서는 for 루프에서 $_가 상수 1의 별칭으로 연결되고 이어서 prompt_user를 호출했는데 이 함수는 STDIN에서 한 라인을 읽고 그 내용을 여전히 1의 별칭인 $_에 저장하려고 시도하면서 오류가 발생합니다.

for ( 1, 2 ) {
    my $data = prompt_user();
}

sub prompt_user {
    print "Enter a number\n";
    while (<STDIN>) {
        # Do stuff
    }
}

이 오류는 다음과 같이 더 간단하게 만든 시나리오에서도 나타납니다.

for ( 1, 2 ) {
    while (<STDIN>) {
    }
}

read-only 오류를 피하기 위한 지침서

read-only 오류를 피하려면 다음 지침을 따르세요.

참고 - 상수값이 포함된 목록

이 문서의 내용을 따라가는 동안 수정 가능한 대상을 만드는 표현식이 어떤 것인지 이해하는 것이 중요합니다.

다음 표현식에는 상수가 들어 있습니다.

$_++ for ( 1, 2 );
$_++ for ( 1, @array );
@array = map { $_++ } ( 1, @array );

반면 다음 표현식은 안전합니다.

my @array = (1,2);
for (@array) {
    $_++;
}

my ( $x, $y ) = ( 1, 2 );
for ( $x, $y ) {
    $_++;
}

목록(list)와 배열(array)의 차이에 관해서는 다음 글을 읽어보세요.

실제 사례

오늘 제가 겪은 사례를 간단히 소개하겠습니다.

모듈을 .pm 파일로 작성한 후 샘플 코드를 만들어 use로 해당 모듈을 불러와 제대로 동작하는 것을 확인했는데 정작 이 모듈을 설치하려 하니 Build test 과정에서 오류가 발생하며 설치에 실패하더군요.

#   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)

메세지를 보면 여러 모듈이 동시다발적으로 오류가 난 것 같이 보입니다. 그래서 당황했지요. 무엇보다도, 어째서 use로 불러와서 사용하는 것은 성공했으면서 정작 단지 use만 하고 끝내는 테스트는 실패했는지 영문을 알 수가 없었습니다. 그래서 오류 메시지를 천천히 따라가보았습니다.

t/00_compile.t 파일은 모듈 개발툴인 Minilla가 자동으로 생성한 테스트 파일입니다.

use strict;
use Test::More;

# 여기를 주목
use_ok $_ for qw(
    TestModule
);

done_testing;

보다시피, 평범하게 use_ok TestModule을 수행하여 테스트하는 것이 아니라 한 개 이상의 모듈 이름으로 구성된 리스트를 순회하면서 리스트의 원소를 $_ 변수에 담아 use_ok의 인자로 넘겨줍니다.

그런데 제가 만든 TestModule.pm 파일에는 다음과 같은 구문이 있었습니다.

use Spreadsheet::XLSX;

Spreadsheet::XLSX 모듈은 다음과 같이 Spreadsheet::ParseExcel 모듈을 불러옵니다.

use Spreadsheet::ParseExcel;

Spreadsheet::ParseExcel 모듈은 또 Digest::Perl::MD5 모듈을 불러옵니다.

use Digest::Perl::MD5; 

Digest::Perl::MD5 모듈의 64행의 소스는 다음과 같습니다.

while(<DATA>) { # 64행
    chomp;
    ...
}

DATA 파일 핸들에서 각 줄을을 읽어 $_ 변수에 저장하고 반복문 내부를 수행하도록 되어 있습니다. 그런데 테스트 코드에서 $_ 변수는 문자열 "TestModule"의 별칭이고, 이 문자열은 상수이며, 따라서 상수의 별칭인 변수의 값을 수정하려 시도하면서 오류가 납니다.

하나의 코드가, use를 따라서 네 차례나 거슬러 올라가야 도달하는 지점의 코드와 만나면서 서로 반응을 하여 펑!하고 터진 것이죠!

이 문제를 해결하기 위해서 테스트 코드를 다음과 같이 수정했습니다.

for my $m ( qw/TestModule/ ) {  # 렉시컬 변수 $m
    use_ok $m;
}

이제는 아무런 문제가 없이 테스트를 통과합니다.

그리고 뒤늦게야 알았습니다만 Digest::Perl::MD5 모듈의 이 부분에 대해서는 2012년에 버그 리포트에 패치를 권장하는 의견이 이미 올라와 있었고 불과 일주일 전인 2013년 12월 14일에 이 패치가 적용된 새 버전이 업로드된 상태더군요.

while ( defined( my $data = <DATA> ) ) {  # 렉시컬 변수 $data
    chomp $data;                          # 여기도 $_ 대신 $data
    ...
}    

정리하며

앞으로 "Modification of a read-only value attempted..." 오류가 발생하면 당황하지 말고 이 기사에서 언급하고 있는 오류를 저지르지 않았는지 확인해 보면 좋을 것입니다. 물론 처음 코드를 작성하는 시점에 주의할 수 있다면 더욱 좋겠죠. 더불어 자신이 만들지 않은 다른 모듈에서 오류가 발생한다면 우선 해당 모듈이 최신 버전인지 확인부터 해보세요! :-)

blog comments powered by Disqus