이명헌 경영 스쿨
[펄] 펄의 구문: 루프와 루프 조절
루프와 루프 탈출
이명헌 [ 2003-1-17 ]

이 글은 래리 월 씨가 직접 쓴 펄의 교과서, "Programming Perl"(캐멀북) 4장, "Statement and Declaration" 을 바탕으로 펄의 루프 및 관련 구문을 정리한 것입니다.

단문(Simple Statements)

단문, 복문.
흡사 영어 문법책에서나 봤음직한 단어들입니다.
단문은 세미콜론;으로 끝나며 그 문장 자체가 직접 어떤 기능을 하는 표현(expression)입니다. 예를 들면,
$a++;
$string = 'abc';
print "$myname is cool !";

이러한 단문은 그 뒤에 modifier가 붙여서 의미를 바꿀 수 있습니다. 단문 뒤에 붙일 수 있는 modifier로는,
if EXPR
unless EXPR
while EXPR
until EXPR
foreach LIST
for LIST

입니다. EXPR는 expression의 약자입니다. 예를 들어 봅시다.

++$num while <MYFILE>;
push @even, $number if ($number % 2 == 0);
print $_ , "\n" foreach (@items);

모두 단문입니다. 그런데, 잘 보세요, 두 번째, 세 번째 코드는 이렇게 풀어 쓸 수도 있을 것입니다.

if ($number % 2 == 0) {
push @even, $number;
}

foreach (@items) {
print $_ , "\n";
}

바로 이런 형태가 C나 자바 등의 일반적인 언어에서 주로 쓰는 방식입니다.(그리고 이것이 뒤에 나올 복문의 형태입니다.) 그런데 펄은 이것을 한 줄로 줄일 수 있는 것입니다. 또 그렇게 쓰는 것이 펄 스타일입니다. 어순을 바꾸며 간결하게 쓰는 것이죠. print something foreach(things)와 같은 펄 스타일에 빨리 익숙해지는 게 코딩을 하는 데나 다른 사람의 코드를 읽는 데 큰 도움이 됩니다.

위의 예를 부연설명하자면, 첫 번째 코드는 MYFILE이라는 파일핸들로 연 파일이 몇 줄인지를 세는 코드입니다. 두 번째 예는 $number를 2로 나눠서 나머지가 0이면 @even이라는 배열에 $number를 넣는 것입니다. 세 번째 예는 @items라는 배열에 담긴 각 원소들을 줄을 바꿔 가며 출력하는 코드입니다.

다른 예를 조금 더 봅시다.
s/perl/java/ for @resumes;
$count-- until $fire;

첫 번째 코드는 @resumes라는 배열에 담긴 항목 각각에 대해서 "perl"을 "java"로 바꿔 주는 코드입니다. s/somepattern/replacement/는 레귤라 익스프레션(regular expression)에서 자세하게 다룰 것입니다만 우선 somepattern을 replacement로 바꾸는 기능을 하는 것으로 알면 됩니다. 두 번째 코드는 $fire가 참이 될 때까지 $count 값을 하나씩 줄여 가는 코드입니다. 첫 번째 코드는 수행하는 기능에 비해 코드가 굉장히 간소해 보이죠? 위와 같은 것이 펄 스타일입니다. for 루프에 대해서는 밑에서 설명이 나오므로 눈에 쉽게 들어오지 않더라도 우선은 넘어 가세요.

이제 복문입니다.

복문(Compound Statements)

중괄호 {}로 묶인 것을 BLOCK이라고 합니다. 중괄호는 변수의 scope을 정하는 역할도 합니다. 펄의 복문이라는 것은 이런 BLOCK이 있는 구문을 뜻합니다. 다음과 같은 것입니다.

if (EXPR) BLOCK
if (EXPR) BLOCK else BLOCK
if (EXPR) BLOCK elsif BLOCK ... else BLOCK

unless (EXPR) BLOCK
unless (EXPR) BLOCK else BLOCK
unless (EXPR) BLOCK elsif BLOCK ... else BLOCK

LABLE while (EXPR) BLOCK
LABLE while (EXPR) BLOCK continue BLOCK
LABLE until (EXPR) BLOCK
LABLE until (EXPR) BLOCK continue BLOCK

LABLE for (EXPR; EXPR; EXPR) BLOCK
LABLE foreach VAR (LIST) BLOCK
LABLE foreach VAR (LIST) BLOCK continue BLOCK

LABLE BLOCK
LABLE BLOCK continue BLOCK

복잡해 보이더라도 신경쓰지 말고 가볍게 읽어 보세요.
우선 모두 다 BLOCK, 즉 { }로 묶인 부분이 있다는 것을 알 수 있습니다. 복문입니다.
LABLE은 어떤 BLOCK을 문자 그대로 '표시하는' 기능을 하는 것으로, 있어도 되고 없어도 됩니다.

하나하나 봅시다. ifunless의 경우는 EXPR이라는 스케일라 구문이 나오고 { }이 나옵니다만 foreach의 경우는 LIST가 따라 나오죠? 이런 것을 컨텍스트라 한다고 했습니다. foreach 앞의 VAR(변수) 부분은 생략할 수 있다는 얘기도 이미 했습니다. 생략하는 경우, 펄의 디폴트 변수인 $_foreach의 아이템 각각이 담깁니다. 맨 마지막 2개는 for, if, until 등이 나오지 않는, 순수한 { } 블락입니다. 이런 것을 벌거벗은 블락(bare block)이라고 합니다. 역시 자세한 것은 차차 나오므로 우선 형태만 보세요.

재미있는 점은, 위의 if 문이나 until 문, for, foreach 문들 모두 앞의 단문에서 살펴본 것처럼 한 줄로 표현할 수 있다는 사실입니다. 물론 { } 안에 담기는 구문이 하나뿐일때만 가능한 것이지만 어쨌든 같은 기능을 하는 코드를 단문으로도 또 복문으로도 자유롭게 쓸 수 있다는 펄의 놀라운 유연성에 주목하세요. 구문이 한 줄인 경우엔 단문 형태로 쓰는 것이 훨씬 더 펄다운 코딩 스타일입니다. 실제 실행 속도면에서도 다른 차이가 없다면 BLOCK이 없는 경우가 있는 경우보다 더 빠릅니다. 즉, 효율성을 생각한다면 복문보다는 단문형태로 쓰는 것이 더 좋습니다.

이상의 내용을 각각 자세히 알아 봅시다.

if , unless 구문

다른 프로그래밍 언어에도 다 있는 것이므로 별 설명이 필요 없습니다. 예를 들어,

if ($weather eq 'rain') {
$soju++;
}
elsif ($weather eq 'sunshine') {
$beer++;
}
else {
$water++;
}

아주 주의해야 할 점은 elsif에 'e'가 없다는 사실니다. 주의하세요. 에러는 사소한 타이핑 실수 때문에 생기는 경우가 많습니다. 그리고 또 하나 주의할 점은 if .. elsif .. else 중 참인 것 하나만 실행된다는 것입니다. 위의 예의 경우, 만약 $weather가 'rain'이면 $soju를 1 증가 시킨 뒤 if..elsif..else 복문은 종료합니다.

여기서 조금 더 나가 볼까요? 캐멀북의 24장에 나오는 내용인데 아주 재밌습니다.
if ($a) { $foo = $a; }
elsif ($b) { $foo = $b; }
elsif ($c) { $foo = $c; }

이런 형태는 흔히 나올 수 있는 것이죠? 펄에서는 위와 같은 복문을 다음과 같은 단문으로 확 줄일 수 있습니다.

$foo = $a || $b || $c;

놀랍죠? 비슷한 것으로 많이 사용되는 코드가 있습니다. 어떤 변수의 기본값(default value)을 설정하는 코드 입니다.

$pi ||= 3.14;idiom

위 코드는 $pi라는 변수에 특별한 값이 들어있지 않으면 3.14를 할당한다는 의미 입니다. 자세히 풀어서 보면 A || B라는 것은 'A 또는 B'입니다. A가 참이면 B는 실행되지 않습니다. A가 거짓이면 B가 실행됩니다. 따라서 $pi가 참이면, 즉 $pi라는 변수에 어떤 값이 담겨있으면 그대로 넘어 가고 $pi에 아무 값도 담겨있지 않아서 거짓이 되면 $pi = 3.14를 실행합니다. 재밌죠? 꼭 외워 두세요. 변수 기본값 설정 코드입니다.

루프 구문(Loop Statements)

루프(loop)는 어떤 작업을 반복적으로 수행하는 것입니다. 프로그래밍에 조금이라도 관심을 갖고 있는 분이라면 누구나 for , while과 같은 루프 구문에 대해 들어 보았을 것입니다. 펄에도 for, while 루프가 있고, 그 외에 몇 가지 새로운 것이 있습니다.

그런데 루프는 반복적인 작업을 하는 것이기 때문에 '어떠 어떠한 경우에는 루프를 벗어난다', '어떠 어떠한 경우에는 진행하던 것을 멈추고 다음 번 루프를 다시 시작한다'와 같은 흐름 조절이 필요하게 됩니다. 그런 것을 "loop control"이라 하고, 흐름 조절을 더 정밀하게 하기 위해서는 루프에 이름을 붙여줄 필요가 생깁니다.

예를 들면 A라는 큰 루프 안에 B라는 작은 루프가 있는데 B 루프를 돌다가 어떤 조건이 만족되면 다시 A 루프 처음으로 되돌아 가야하는 경우에 어떻게 해야 할까요? A 루프와 B 루프를 구분지어줄 어떤 이름표 같은 것이 필요하지 않겠습니까? 바로 그런 이름표를 레이블(lable)이라 합니다. 레이블은 모든 루프에 다 붙일 수 있습니다.(물론 붙일 필요가 없으면 안 붙여도 됩니다.)

그러면 대표적인 루프인 whileuntil 구문에 대해 알아 봅시다.

while , until 루프

while 구문은 어떤 조건이 참인 한(while something is true..) 계속 루프를 돌게 됩니다. while 구문은 until 구문으로 바꿀 수 있는데 until 역시 문자 그대로 어떤 조건이 참이 될 때까지(until something is true..) 루프를 돕니다.

while 루프는 주로 어떤 파일을 읽어들일 때 많이 쓰입니다. myfile.txt 라는 파일을 열어서 각 줄의 끝에 html 줄바꿈 태그 <br>을 붙이는 코드는,
open MYFILE, "myfile.txt" or die ("can't open myfile.txt");
while (<MYFILE>) {
$_ .= "<br>"
}

위의 while 복문은 다음과 같이 쓸 수 있습니다. 펄 스타일입니다
$_ .= "<br>" while (<MYFILE>);

파일 읽는 것은 사용자 입력 처리에서 다루었습니다.

for 루프

for 루프는 for (A;B;C)라는 형태로 괄호 안에 3개의 expression을 넣어서 사용합니다. A에 해당하는 것은 초기화(initialization), B는 조건(condition), C는 재초기화 (reinitialization)입니다.
for (my $i = 1; $i <= 10; $i++) {
...
}

$i에 초기화 값으로 1을 넣고 루프를 시작해서, $i가 10보다 작거나 같은 지를 테스트하고 그렇지 않으면 $i를 1 증가시켜서 다시 초기화, 또 다시 루프를 시작합니다. 그 다음에는 $i에 2가 할당되어서 다음 루프를 도는 것입니다.

for 루프 괄호 내의 세미콜론 사이에는 아무 것도 넣지 않아도 됩니다. 그 경우 각각은 참으로 간주됩니다.
for(;;) {
print "-_-", "\n";
}

이것은 -_-를 줄을 바꿔가며 계속 프린트합니다. for () 안이 계속 참이므로 무한루프입니다. 위 코드는 while 구문으로 바꿀 수도 있습니다. while 괄호 안의 조건이 항상 참이 되도록 하면 됩니다.
while(1) {
print "-_-", "\n";
}

입니다.

펄의 유연성은 for 구문에서도 예외가 아닙니다. 여러 개의 변수를 동시에 변경해 가면서 루프를 돌릴 수도 있습니다.
for ($i=65, $j=97; $i<=90, $j<=122; $i++, $j++) {
print chr($i), chr($j), "\n";
}

위 코드는 십진수 아스키값 65(대문자 A)와 십진수 아스키값 96(소문자 a)부터 Z, z까지 줄을 바꿔 가며 출력하는 코드입니다. chr() 함수는 아스키 값을 주면 문자로 바꿔주는 함수입니다.

foreach 루프

foreach 루프는 for 루프와 항상 바꿔 쓸 수 있는 거의 같은 것입니다. 둘 다 어떤 리스트(list)의 아이템 각각을 갖고 루프를 돕니다. 차이점이 있다면 foreach는 현재 몇 번째 아이템을 가지고 작업하는지를 알 수가 없다는 게 다릅니다. 그것을 알 필요가 있는 경우에는 for를 써야 합니다. 또 그 점 때문에 foreach 루프가 for 루프보다 더 속도가 빠릅니다. foreach는 리스트 아이템 각각에 직접 접근하지만 for는 순서를 나타내는 숫자를 통해서 접근하기 때문입니다. 실행 속도가 중요한 경우에는 foreach를 쓰는게 더 유리합니다. foreach는 이런 식으로 사용합니다.

$data = 'Perl:Python:PHP:ColdFusion:ASP';
foreach $value (split /:/, $data) {
print "$value", "\n";
}

위의 코드 중 split /:/, $data$data에 담긴 값을 콜론 : 별로 나누어서 그 값들을 리스트 형태로 되돌려 줍니다. foreach 루프는 그렇게 반환된 리스트의 아이템 각각을 $value에 담아서 루프를 돌게 됩니다.

위 코드를 실행해 보면 Perl, Python,...이 줄을 바꿔 가면서 출력되는 것을 볼 수 있습니다. 위 코드에서 foreachfor로 바꿔도 됩니다. forforeach 모두 list context를 끌고 다니므로 for VAR (LIST), foreach VAR (LIST) 모두 가능합니다.

그런데 펄에는 디폴트 변수 $_가 있습니다. 변수를 특별히 명시하지 않으면 $_에 담깁니다. 따라서 위 foreach 루프는 다음과 같이 바꿀 수 있습니다.

foreach (split /:/, $data) {
print $_, "\n";
}

이 코드 역시 foreach대신 for를 써도 됩니다. forforeach 모두 루프 변수를 명시하지 않으면 펄의 디폴트 변수인 $_를 사용합니다. 위 코드는 블락 내부가 한 줄이므로 한 번 더 줄일 수 있습니다.

print $_, "\n" foreach (split /:/, $data);

코드가 훨씬 더 펄다워졌습니다. 이렇게 코딩하는 게 펄 스타일입니다.
물론 C, Java 스타일로 차곡차곡 코딩하는 것을 굳이 말리지는 않겠습니다. : )

위의 루프 변수 $value$_는 리스트 아이템 각각을 가리키는 일종의 alias(가상본, 바로가기)입니다. 즉, 루프 변수를 수정하면 그건 리스트내 아이템을 직접 수정하는 것과 같습니다. 예를 들어 위의 위 랭귀지 이름 뒤에 " language" 라는 단어를 모두 붙이고 싶다면 어떻게 하면 될까요?
print $_ . " language", "\n" foreach (split /:/, $data);

이렇게 하면 됩니다. 점 하나는 concatenation operator로 문자열을 합쳐주는 연산자입니다.

기왕 '펄다운' 코드 얘기가 나온 김에 관용적으로 많이 쓰이는 것을 하나 더 알아 봅시다. 예를 들어 1에서 100까지 숫자 중 홀수만 모으는 코드는 이렇게 할 수 있습니다.

foreach (1..100) {
push @odd, $_ if ($_ % 2 == 1);
}

위의 코드도 사실 if (..) 구문을 단문 형태로 줄인 것입니다만, 펄은 여기서 멈추지 않습니다. 유닉스의 커맨드에도 있는 grep이 펄에도 있습니다. 위의 코드는 다음과 같이 한 줄로 줄일 수 있습니다.

@odd = grep { $_ % 2 } 1..100 ; idiom

아주 펄다운 코드입니다. 이 코드가 왜 위의 복문과 같은지 알아 봅시다.

grep { .. } LIST { } 뒤의 리스트에 있는 아이템 각각을 $_ 에 담아서 { } 안에 들어 있는 연산을 수행합니다. 그러므로 위의 코드는 1에서 100까지를 하나씩 2로 나눠서 나머지가 '참'이면, 즉 1이면 @odd에 결과를 담는 것이 됩니다. 결국 @odd 내에는 1에서 100까지 중 홀수들만 담기게 됩니다. grep { } LIST 패턴은 좋은 펄 코드에서 자주 나오는 것이므로 잘 기억해 두세요.

루프 컨트롤(Loop Control)

루프는 무한히 반복해서 도는 것이기 때문에 '어떤 조건이 충족되면 루프를 탈출해라.'든지 '어떤 값이 뭐가 되면 어디어디로 가라.' 등의 조절이 필요합니다. 이런 것을 담당하는 것이 루프 컨트롤이며, '어디어디'에 해당하는 것이 레이블입니다. 루프 컨트롤에는 크게 세 가지가 있습니다. next, last, redo.

next는 하던 작업을 멈추고 다음 번 루프를 돌아라는 의미입니다.
last 역시 루프의 맨 끝 부분으로 가라는 의미입니다.
redo 는 처음부터 다시 하라는 의미입니다.
실제 예를 봅시다.

어떤 펄 파일을 열어서 주석문(#로 시작하는)과 비어 있는 줄을 제외한, 실제 내용이 있는 줄이 몇 줄인가를 세는 코드는 이런 형태가 됩니다.
open PERL, "myfile.pl" or die "can't open perl file";
while (<PERL>) {
# next 를 만나면 여기로 오게 됩니다
next if /^#/;
next if /^$/;
$count++;
}
print "총 $count 줄입니다";

PERL이라는 파일핸들로 펄 파일을 연 다음, 각각의 줄을 이용해서 루프를 도는데,
/^#/, 즉 # 기호로 시작하는 줄이면 next, 다시 다음 줄을 읽으러 갑니다. 주석문을 스킵하는 것입니다. next if /^$/도 마찬가지입니다. 비어 있는 줄이면 다시 다음 줄을 읽으러 가라는 의미입니다. next는 하던 작업을 멈추고 다음 번 루프를 돌아라는 뜻입니다.(/^#/ 등이 생소한 분은 나중에 레귤라 익스프레션에서 자세히 다루므로 여기서는 #으로 시작하는지 여부를 매칭하는 코드라는 정도로만 알고 있으면 됩니다.)

next if .., 일상 영어 그대로 읽히죠? 펄의 특징입니다.

그런데 모든 BLOCK (중괄호로 묶여있는 코드) 은 이름표(레이블)를 붙일 수 있다고 했습니다. 즉, 위의 코드는 이렇게 쓸 수도 있습니다.
LINE: while (<PERL>) {
next LINE if /^#/;
next LINE if /^$/;
$count++;
}

"만약 # 로 시작하면 다음 줄" (next LINE if ..)
아주 자연스럽습니다. 위의 코드에서는 굳이 레이블을 붙일 필요가 없습니다. BLOCK이 한 단계 뿐이기 때문입니다. nextlast 뒤에 레이블을 생략하면 가장 가까운 BLOCK을 그 경계로 간주합니다. 위에서처럼 써 줘도 상관없습니다. 그리고 레이블은 관례적으로 대문자로 씁니다. 파일핸들처럼요.

이번에는 last 예를 들어 봅시다. last는 루프를 끝내고 마지막으로 가라는 의미이므로 C나 Java에서의 case 구문 같은 것을 만들 수 있습니다.(사실 펄에 switch, case 구문이 없는 이유가 last 를 이용해서 표현가능하기 때문입니다. 펄 버전 6에서는 case 구문이 추가될 예정입니다.)
SWITCH: {
if (/^abc/) { $abc = 1; last SWITCH }
if (/^def/) { $def = 1; last SWITCH }
$nothing = 1;
# last 를 만나면 여기로 오게 됩니다
}

쉽게 이해되죠?

주의할 점은 last, next 등은 BLOCK 내에서만 유효하다는 것입니다. 즉, for, foreach, while과 같이 BLOCK을 이끄는 구문이 아닌 경우에는 따로 중괄호로 묶어서 BLOCK을 만들어 줘야 합니다. 이렇게 의도적으로 중괄호를 앞뒤로 묶어서 일종의 루프처럼 만든 것을 벌거벗은 블락, bare block이라 합니다. 이것이 특히 많이 쓰이는 부분은 사용자 정의 함수 sub { }에서입니다. sub { }는 루프가 아니므로 그 내부에서 흐름조절이 필요한 경우 bare block 으로 묶어 줘야 하는 경우가 많습니다.

sub add {
$url = shift;
{
last if ($url eq 'localhost');
last if ($url =~ /yahoo/i);
last if ($url =~ /google/i);
$url_list = $url . "\n";
# last 를 만나면 여기로 오게 됩니다
}
return $url_list;
}

위 코드는 add라는 함수를 호출한 곳으로부터 전달 받은 $url이 localhost거나 yahoo, google 이라는 단어가 들어가는 경우가 아닐 때 그 값을 되돌려주는 코드입니다. 루프가 아닌 곳에서 last if .. 를 사용하기 위해서 bare block을 사용했음을 알 수 있습니다.

또 하나 재미있는 것이 ? : 입니다. 이건 다른 언어에도 대부분 있는 것입니다.

$grade =
($point < 10) ? "poor":
($point < 50) ? "room for improvement":
($point < 80) ? "great":
"excellent"; # 위에 해당되는 경우가 없으면

이 코드는 $point의 크기에 따라서 $grade에 "poor", "room for improvement", "great", "excellent" 를 담는 코드입니다. (조건1)? a : (조건2)? b : (조건3)? c : d; 와 같은 형태로 사용합니다. 조건 1을 만족하면 a, 조건 2를 만족하면 b, .. 앞에 만족하는 조건이 없으면 d라는 의미 입니다. 이 코드 역시 아주 많이 사용되므로 잘 기억하고 있어야 합니다.

마지막으로 한 가지. BLOCK, 즉, 중괄호로 감싸져 있는 코드 중 마지막 줄에는 세미콜론을 사용하지 않아도 됩니다. 하지만 마지막 줄에도 세미콜론을 붙이는 습관을 들이시는 게 좋습니다. 나중에 그 BLOCK 내에 새로운 코드를 첨가할 일이 생겼을 때, 그 전에 마지막 줄 끝에 세미콜론을 붙이지 않았다는 사실을 잊고 있기가 쉽기 때문입니다. 세미콜론 하나 때문에 몇 시간 보내는 일이 드물지 않습니다.

지금 계신 곳은: TECH > [펄] 펄의 구문: 루프와 루프 조절