이명헌 경영 스쿨
[펄] 펄을 이용한 웹 기반 복리계산기
GET 또는 POST
이명헌 [ 2002-6-20 ]
* 복리계산기는 여기를 클릭하세요.

이 글은 CGI의 의미 및 펄을 이용한 CGI 개발에서 설명한 내용을 바탕으로 웹 기반 복리계산기 만드는 과정을 알아 보는 글입니다. 참고할 내용은 다음과 같습니다.

  1. 웹 기반 복리계산기: 복리계산기
  2. 복리 수학 공식이 나오게 된 배경: 화폐의 시간가치

만들어 볼 cgi 프로그램은 원금과 연이율, 그리고 햇수를 입력하면 이를 복리로 계산해서 원리합계표를 만들어 주는 프로그램입니다. 이 cgi는 단순해 보이지만 사실상 cgi 프로그램의 기본 원리는 모두 다 담고 있습니다.

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

여기에 대해 자세히 알아 보기 전에 알고 있어야 할 내용이 있습니다. 폼을 통해 입력된 정보가 웹 써버로 전달되는 방식에는 크게 두 가지가 있습니다. "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. 이와 달리 웹 써버의 표준입력(STDIN)으로 전달되는 방식이 POST 방식입니다. POST 방식으로 전달된 데이타는 표준입력(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 방식, POST 방식은 각각 장단점이 있습니다. GET 방식은 url을 통해서 사용자가 입력한 정보가 노출된다는 점이 단점입니다. 노출될 뿐만 아니라, 웹 폼을 통하지 않고도 주소창의 cgi 주소 끝에 aaa=bbb&ccc=ddd 형태로 데이타를 붙여서 cgi 프로그램을 동작시킬 수 있다는 보안상의 약점이 있습니다. 또, 클라이언트나 써버에 따라서 url을 잘라내 버리는 경우(truncate) 데이타가 제대로 전달되지 않을 수도 있습니다.

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

폼에 입력된 정보가 어떤 방식으로 전달되는가는 써버에서 결정하는 게 아니라 HTML 쪽에서 결정됩니다.이런 식이죠.
<form action="compound.pl" method="GET">...</form>
<form action="compound.pl" method="POST">...</form>

그런데 폼 입력 정보를 추출하는 방식은 각각 전혀 다른식으로 이뤄지므로, 혼자서 html 디자인과 cgi 프로그래밍을 다하는 경우라면 몰라도 웹 디자이너와 웹 프로그래머가 각기 작업을 하는 경우 웹 디자이너가 어떤 방식으로 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로 선언했군요. 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 쌍을 해쉬로 전환합니다. foreach 구문은 쉽게 이해가 되죠? 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)을 알고 있다면 어렵지 않은 것입니다. 플랙으로 붙은 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은 앞의 regex의 괄호 부분을 가리키므로 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 = parma('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을 사용해서 코딩하세요. CGI.pm을 사용하면 사용자 입력 내용을 파싱하는 부분 없이 $value = param('name');라는 코드로 곧바로 입력된 값을 사용할 수 있습니다.

지금 계신 곳은: TECH > [펄] 펄을 이용한 웹 기반 복리계산기