티스토리 뷰

2002-6-20

 

이 글은 펄 CGI 프로그램을 이용해서 아래 링크에 있는 것과 같은 간단한 웹 기반 복리 계산기를 만들어 보는 과정을 설명하는 글입니다.

 

복리 계산기는 원금과 연이율 그리고 투자기간(연 단위)을 입력하면 복리로 계산해서 원리합계 표를 만들어 보여주는 프로그램입니다. 간단한 프로그램이지만 펄 CGI 프로그램의 기본 원리를 이해하는 데는 충분할 것 같습니다.

 

 

 

사용자가 폼을 통해 입력한 정보는 CGI를 통해 웹 서버로 전달된다고 했습니다. 원금 입력 란이나 이율 입력 란에 입력한 숫자들이 사용자가 입력한 정보입니다. 이 정보는 "보내기(SUBMIT)" 버튼을 누르면 웹 서버로 전달됩니다. 그렇다면 그 데이타는 어디로 가는 것일까요?

GET 메쏘드와 POST 메쏘드

우선, 폼을 통해 입력한 정보가 웹 서버로 전달되는 방식에 대해서 알고 있어야 합니다. 두 가지 메쏘드는 "GET"과 "POST"입니다.

 

 (1) GET 메쏘드(GET method)는 폼 입력 란의 이름(name)과 값(value)이 CGI 프로그램의 URL에 붙어서 전달되는 방식입니다. 위 복리계산기를 실행한 다음 주소창에 보면, compound.pl?principal=10000&rate=6&years=10처럼 되어 있습니다. 그렇게 CGI 프로그램 URL 뒤에 물음표 ?가 나오고 name1=value1&name2=value2&... 형태로 이름과 값이 붙어서 전달되는 방식을 GET method라 합니다. GET 메쏘드에 의해 전달된 정보는 환경변수 중 $ENV{'QUERY_STRING'}에 저장됩니다.

 

 (2) POST 메쏘드(POST method)웹 서버의 표준입력(STDIN)으로 전달되고, 전달된 데이타는 표준입력(STDIN)으로부터 데이타를 읽어 들입니다. 이 때 사용자가 입력한 데이타의 길이는 $ENV{'CONTENT_LENGTH'}라는 환경변수에 담깁니다. 그러므로 POST 방식으로 보내진 정보는 표준입력에서 CONTENT_LENGTH 길이만큼 가져와라는 식으로 읽어 오게 됩니다.

 

각각의 경우 서버에 전달되는 요청의 실제 내용을 보면 더 이해가 쉽습니다.

 

GET 방식의 경우 써버에 전달되는 요청:

GET /cgi-bin/compound.pl?principal=10000&rate=6&years=10

Accept: www/source

Accept: text/html

Accept: text/plain

User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)

 

POST 방식의 경우 써버에 전달되는 요청:

POST /cgi-bin/compound.pl HTTP/1.1

Accept: www/source

Accept: text/html

Accept: text/plain

User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)

Content-type: application/x-www-form-urlencoded

Content-length: 31

principal=10000&rate=6&years=10

 

두 방식이 전혀 다르게 전달된다는 것을 볼 수 있습니다.

 

이들 두 메쏘드는 각각 장단점이 있습니다. GET 방식은 URL을 통해서 사용자가 입력한 정보가 노출된다는 점이 단점입니다. 노출될 뿐만 아니라 웹 폼을 통하지 않고도 CGI 주소 끝에 aaa=bbb&ccc=ddd 형태로 데이타를 붙여서 CGI 프로그램을 동작시킬 수 있다는 보안상의 약점이 있습니다. 또 클라이언트나 써버에 따라서 URL을 잘라내는(truncate) 경우 데이타가 제대로 전달되지 않을 수도 있습니다.

 

하지만 GET 방식은 URL에 입력한 값이 같이 담겨 있으므로 사용자 쪽에서는 북마크하기에 용이하다는 장점이 있습니다. 예컨데 검색엔진의 특정 검색어에 대한 결과를 북마크하고 싶은 경우 search.pl?q=검색어로 북마크가 되므로 편리합니다. 

 

폼에 입력한 정보가 어떤 방식으로 전달되는가는 HTML에서 결정합니다.


<form action="compound.pl" method="GET">...</form>
<form action="compound.pl" method="POST">...</form>

 

그런데 폼 입력 정보를 추출하는 방식은 각각 전혀 다른식으로 이뤄지므로, 한 사람이 HTML 디자인과 CGI 프로그래밍을 다하는 경우가 아니라면 메쏘드 종류를 어떻게 해놓아도 잘 처리할 수 있게 코딩하는 것이 좋습니다.

 

그러면 실제 웹 서버에 전달된 폼 입력 정보를 어떻게 처리하는지 자세히 알아봅시다. GET 메쏘드로 전달된 정보는 $ENV{'QUERY_STRING'}에 담기고 POST 메쏘드로 전달된 정보는 STDIN(표준입력)에 저장된다고 했고, 어떤 방식으로 전달되었는지는 $ENV{'REQUEST_METHOD'}라는 환경변수에 담깁니다. 

 


sub parseArgument {
	local ($buffer, $data, $name, $value);
	my @pair;

	if($ENV{'REQUEST_METHOD'} eq "GET") {
		$buffer = $ENV{'QUERY_STRING'};
	}
	else {
		read (STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
	}

	@pair = split /&/, $buffer;

	foreach $data (@pair) {
		($name,$value) = split(/=/,$data);
		$value =~ tr/+/ /;
		$value =~ s/%([a-fA-F0-9]{2})/pack("C",hex($1))/eg;
		$FORM{$name} = $value;
	}
}

 

위 코드는 과거 CGI 프로그램이라면 거의 그대로 사용했던 라이브러리격인 코드입니다. HTML 에서 POST 방식으로 전달을 하든 GET 방식으로 전달하든 위 코드는 각각의 name=value 쌍을 %FORM이라는 해쉬에 저장합니다.

 

각 줄을 자세히 알아 봅시다. 첫 두 줄은 변수를 선언한 것입니다. local{ } 내부에서는 지역변수처럼 사용되다가 { }를 벗어나면 전역변수로 사용되는 변수형입니다. 비슷한 것으로 { } 내부에서만 의미를 갖는 지역변수를 만드는 my가 있습니다. local로 선언한 것은 $name이나 $value를 위 함수 외부에서 불러야 할 경우를 대비한 것입니다. 여기서는 my로 해도 큰 상관은 없습니다.

 

그 다음 줄을 봅시다. $ENV{'REQUEST_METHOD'}가 GET이면 $ENV{'QUERY_STRING'}에 담겨있는 것을 $buffer에 담는다고 되어 있습니다.  GET 방식의 경우 QUERY_STRING에 저장되므로 거기 있는 것을 끄집어 내는 것입니다. 그 다음 else, 즉 GET 메쏘드가 아니면 read 구문이 실행됩니다.


read 구문은 read (A, B, C) 형태로 쓰며, A에 있는 것을 C만큼 읽어 들여서 B에 저장해라는 의미입니다. 따라서 위의 read 구문은 표준입력(STDIN)에 있는 데이타를 $ENV{'CONTENT_LENGTH'} 길이만큼 읽어 들여서 $buffer에 저장해라는 것이 됩니다. POST 메쏘드는 표준입력으로 정보가 전달된다고 했습니다. 이렇게 하면, GET 이든 POST든 $buffer에는 name1=value1&name2=value2&name3=value3&...가 저장됩니다.

 

이번에는 이것을 하나하나 나눠야 할 차례입니다. 우선 $buffer 내에 들어 있는 값을 name1=value1, name2=value2, name3=value3,...로 나눠야 하므로 split 구문을 사용합니다. split /&/, $buffer;$buffer에 있는 것을 &를 기준으로 나눈 다음 각각을 배열로 되돌려 줍니다. 위에서는 @pair라는 배열에 &를 기준으로 나누어진 name1=value1, name2=value2, name3=value3 등이 각각의 원소로 담기게 됩니다.

 

마지막 부분에서는 각각의 name=value 쌍을 해시로 전환합니다.split 구문은 $data=를 기준으로 나누어서 $name$value에 담습니다. 그 다음 나오는 두 줄은 밑에서 설명합니다. 마지막 줄에서는 $value$FORM{$name}에 할당함으로써 %FORM 해시를 만드는 것을 알 수 있습니다. 

 

이제 조금 복잡해 보이는 두 줄을 알아 보겠습니다.

 

먼저, $value =~ tr/+/ /;$value에 들어있는 모든 플러스 표시를 공백으로 바꾸는 것입니다. 갑자기 웬 플러스 표시일까요? 우리가 폼에 입력한 값에는 공백이 있는 경우도 있습니다. 예를 들어 이름을 "이 명헌"이라고 입력할 수 있습니다. 이런 공백 문자는 HTTP를 통해 서버에 전달될 때 자동으로 플러스 표시로 바뀌어서(=인코딩(encoding)되어서) 전달됩니다. 그러므로 전달된 값에 있는 플러스 문자가 있다면 다시 공백으로 바꿔 줘야 원래 사용자가 입력한 대로 되돌려 놓을 수 있는 것입니다.

 

그 다음 줄 $value =~ s/%([a-fA-F0-9]{2})/pack("C",hex($1))/eg;

엄청 복잡해 보입니다만 내용을 알면 별 것 아닙니다. s///eg;는 정규표현식(regular expression)입니다. 플랙(flag)으로 붙은 g는 global, 즉, 해당 패턴이 나오는 대로 다 찾는다는 의미입니다. e는 expression으로, 대체될 부분에 pack 구문 같은 expression을 넣을 때 쓰는 플랙입니다.

 

첫 번째 슬래쉬 사이에 있는 정규표현식을 자세히 보면, 먼저 % 기호가 나오고 a-f 또는 A-F 또는 0-9 중 한 문자씩, 총 두 개의 문자로 된 패턴을 찾는 것입니다. 즉, 이런 형태를 찾습니다.

 

%3f, %07, %2a, %ba, %7a, ...

 

어디서 많이 본 형태죠? 바로 16진수 값입니다. 이들은 아스키 코드(ASCII Code) 16진수 값입니다. 앞에 %가 붙은 것은 공백이 플러스 표시로 바뀌는 것처럼 일반 문자가 POST 방식으로 전달될 때 %로 시작되는 16진수 값으로 자동 변환되기 때문입니다. 16진수 값은 각 문자의 아스키 코드 값입니다.

 

따라서 이제는 역으로 %로 시작되는 16진수 값을 다시 원래 문자로 되돌려 놓아야 합니다. $1은 앞의 정규표현식의 괄호 부분을 가리키므로 hex($1)은 그 %를 제외한 16진수 부분만을 10진수로 바꾸는 것입니다. 바꾼 10진수 값을 "C"(character)로 pack해 넣음으로써 다시 원래 문자를 얻어낼 수 있는 것입니다. 즉, decoding하는 것입니다. 같은 기능을 하는 다른 코드도 있습니다.

 

$value =~ s/%([\da-f][\da-f])/chr(hex($1))/egi;

 

\d는 0-9 중의 하나를 가리키는 것입니다. 그러므로 첫 두 슬래시 사이에 담긴 코드는 위에서 숫자로 직접 쓴 것과 마찬가지로 %로 시작하는 16진수 값입니다. 바꿀 부분의 코드에 나오는 chr() 함수는 16진수 값을 문자열로 변환하는 함수입니다. 훨씬 깔끔합니다.

 

이상의 코드를 통해 GET 방식이든 POST 방식이든 전달된 name=value 쌍을 %FORM 해쉬로 바꿔 놓았습니다. 이제 이 해쉬에 접근해서 사용자가 입력한 데이타를 처리하면 됩니다. 예를 들어 name1이라는 이름의 입력 폼에 입력된 값은 $FORM{$name1}이 됩니다. 사용자 입력 값들을 입력란 별로 처리할 수 있게 된 것입니다.

 

위 코드는 펄 CGI 프로그램이라면 대부분 사용하는 것이므로 COPY-PASTE해서 많이 사용했습니다. 그런데 펄 모듈(Perl Module) 중 CGI.pm을 사용하면 위와 같이 복잡한 코드를 쓰지 않고 간단하게 똑같은 기능을 구현할 수 있습니다.

 

먼저 펄 모듈을 사용하는 방법은 use입니다. 예를 들어 CGI.pm이라는 펄 모듈을 사용한다면 use CGI;를 프로그램의 첫 머리에 써 주면 됩니다. 대개의 CGI 프로그램은 펄 프로그램 첫 줄에 use CGI qw(:standard);처럼 쓴다고 일단 외워두시기 바랍니다. 이것은 CGI.pm 모듈 중의 standard 함수들을 가져 오겠다는(import) 뜻입니다. 이 한 줄로 위에서 복잡하게 설명한 사용자 입력 처리가 허탈할 정도로 간단해집니다. CGI.pm 펄 모듈에는 사용자가 입력한 데이타를 쉽게 가져올 수 있게 하기 위해 param()이란 것이 있습니다. $value = param('name')이라고 하면 name이라는 이름의 입력 란에 입력된 값이 $value라는 스칼라 변수에  담깁니다. 즉, 위에서 복잡하게 설명한 parseArgument()라는 함수는 $value = param('name') 한 줄로 바꿀 수 있습니다. 실제 코드를 보면 더 쉽게 이해됩니다.

 

위에서 설명한 내용을 바탕으로 웹 기반 복리계산기를 만들어 보도록 하겠습니다. 먼저 전통적이 방식으로 코딩하면,


#!/usr/bin/perl

&parseNumbers;
&htmlHeader;

if ($FORM{"principal"} && $FORM{"rate"} && $FORM{"years"}) { &calculate; }
else { &inputForm; }

&htmlFooter;
 
# 입력된 숫자를 넘겨받습니다
sub parseNumbers {	
	local ($buffer, $data, $name, $value);
	local (@pair);

	if ($ENV{'REQUEST_METHOD'} eq "GET") {
	$buffer = $ENV{'QUERY_STRING'};
 }
else {
	read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
 }

@pair = split(/&/, $buffer);

foreach $data (@pair) {
	($name, $value) = split(/=/, $data);
	$value =~ tr/+/ /;
	$value =~ s/%([a-fA-F0-9]{2})/pack("C", hex($1))/eg;
	$FORM{$name} = $value;
 }
}


# HTML header 출력
sub htmlHeader {	
	print qq|Content-type: text/html\n\n|;
	print qq(html은 생략합니다.);
}

# HTML footer 출력
sub htmlFooter { 
	print qq(html은 생략합니다.);
}

# 사용자 입력 폼 출력
sub inputForm {
	폼 태그는 생략합니다.
}


# 원리합계를 계산 합니다
sub calculate {
	# 사용자 입력한 값 할당
	($principal, $rate, $years) = ($FORM{'principal'}, $FORM{'rate'}, $FORM{'years'});
	$first = $principal;
	for my $i (1..$years) {
		my $interest = int(($rate/100) * $principal); # 이자계산
		$sum[$i] = $principal + $interest; # 원리합계 계산
		$principal = $sum[$i]; # 원리합계를 다시 다음루프 원금으로
	}
	&printNumbers;
}

# 최종결과를 출력합니다
sub printNumbers {	
 디스플레이하는 html 은 생략합니다.
}

위와 같이 됩니다. principal, rate 등 입력 란에 담긴 값들이 parseNumbers()라는 함수를 통해 서버로 전달되어서 $FORM{'principal'}, $FORM{'rate'} 등의 해시 값에 담긴다는 것이 쉽게 이해됩니다. 나머지 복리 계산하는 부분은 간단한 것이므로 설명을 생략합니다.

 

print qq| ... |;print " ... "와 똑같습니다만, 큰 따옴표 안에 또 다시 큰 따옴표를 쓸 때도 탈출(\")할 필요가 없기 때문에 아주 유용합니다. qq|...|는 ...를 큰 따옴표로 묶은 것과 똑같고 q|...|;는 작은 따옴표로 묶은 것과 같고, qw|a b c|("a", "b", "c")와 같습니다. qw는 단어(word)별로 따옴표로 묶는다는 의미입니다.

 

대개 html을 출력할때 태그와 함께 큰 따옴표가 많이 쓰이므로 가급적이면 qq|..|를 활용하는 것이 print "..."를 사용하는 것보다 더 좋습니다. 그리고 또 한 가지, qq(...), qq#...#처럼 열고 닫는 문자만 맞춰주면 |, ()등을 사용해도 됩니다.

 

위의 코드는 펄 모듈 CGI.pm을 사용하면 parseNumbers()를 전혀 사용할 필요 없이, $principal = param('principal');이라고 하면 곧바로 principal 입력란에 입력한 값이 $principal이라는 스칼라 변수에 담깁니다. CGI.pm을 사용하면 대략 이런 식으로 됩니다.

 


#!/usr/bin/perl

use CGI qw(:standard);

# 입력된 숫자를 넘겨받습니다
$principal = param('principal');
$rate = param('rate');
$years = param('years');
# parseNumbers() 없이 위와 같이 간단하게 됩니다

&htmlHeader;
if ($FORM{"principal"} && $FORM{"rate"} && $FORM{"years"}) { &calculate; }
else { &inputForm; }

&htmlFooter;

# HTML header 출력
...
# HTML footer 출력
...
# 사용자 입력 폼 출력
...

sub valid {
	if ($principal && $rate && $years) {
		&calculate;
	}
}

# 원리합계를 계산 합니다
sub calculate {
	$first = $principal;
	for my $i (1..$years) {
		$interest = int(($rate/100) * $principal); # 이자계산
		$sum[$i] = $principal + $interest; # 원리합계 계산
		$principal = $sum[$i]; # 원리합계를 다시 다음 루프 원금으로
	}
	&printNumbers;
}

# 최종결과를 출력합니다
...

코드가 훨씬 더 간소해지고 깔끔합니다. CGI.pm을 사용하면 사용자 입력 내용을 파싱하는 부분 없이 $value = param('name');라는 코드로 곧바로 입력된 값을 사용할 수 있습니다.

반응형
댓글