|
CGI 란?
cgi는 "common gateway interface"의 약자 입니다. 해석을 해보면 공통된 게이트웨이 인터페이스 입니다. 게이트웨이란 써버 컴퓨터에는 존재하지만 웹써버 프로그램을 통해서는 접근할 수 없는 다른 프로그램으로의 입구를 뜻합니다. 예를들어, 일반적인 웹브라우져로 웹써버에 접속해서 웹써버가 깔려있는 컴퓨터에 있는 mysql db에는 절대로 직접 접근할 수가 없습니다. 하지만 mysql 데이타를 다룰 수 있는 cgi프로그램을 통해서 사용자는 웹브라우져로 mysql 데이타에 접근할 수 있습니다. 즉, cgi 프로그램이 mysql db로의 'gateway'를 제공하는 것입니다. 그러므로 "common gateway interface"는 써버 컴퓨터에 존재하는 여러가지 데이타, 프로그램으로의 공통된(common) 게이트웨이를 만들어주는 인터페이스를 의미하는 것입니다.
그러면 그 게이트웨이를 통해서 무엇이 들어가고 무엇이 나올까요. 쉬운 예로 웹페이지 회원등록 폼(form)에 개인 정보를 입력하는 경우를 생각해봅시다. 회원등록 페이지는 이름, 주소, id, 패쓰워드 등의 여러가지 정보를 입력할 수 있는 html 폼으로 이뤄져 있습니다. 사용자는 이 폼에 자신의 이름, 주소등의 정보를 입력하고 "보내기"(submit) 버튼을 클릭하게 됩니다. 이때 입력된 사용자 데이타, 바로 이것이 어떤 방식을 거쳐서 웹써버에 도착하고, 웹써버에서는 이들 데이타들을 '처리'하게 됩니다. '처리'는 이들 데이타들을 단순히 웹써버에 저장하는 것일 수도 있고, 이들 데이타를 웹써버에 존재하는 다른 프로그램으로 프로세싱해서 어떤 출력물을 만들어 내는 것일 수도 있습니다. 어쨌든 사용자가 입력한 데이타가 들어가고, 이들 데이타들은 cgi라는 인터페이스를 통해서 웹써버 또는 웹써버와 연결된 다른 컴퓨터에 있는 프로그램을 만나고, 이들 프로그램에 의해 처리된 결과는 다시 cgi라는 인터페이스를 통해서 주 사용자쪽(웹브라우져)으로 보내지는 것입니다.
(위 그림은 O'Reily 책에서 발췌했습니다) |
그러면 먼저 웹써버로 들어가는 정보에는 어떤 것이 있는지부터 자세하게 알아봅시다. 크게 3 가지가 들어가는데요.
- 클라이언트 정보, 써버 정보, 사용자 정보
- 사용자가 폼에 입력한 정보
- 부가적인 패쓰(path) 정보
마지막 부가적 패쓰정보는 별로 중요한 것이 아니구요.위의 2 가지가 중요합니다. 첫번째에 나오는 클라이언트, 써버, 사용자 정보는 현재 사용자가 어떤 브라우져를 사용하는지, 어떤 싸이트를 거쳐서 이 웹써버에 접속하고 있는지, 사용자의 ip 주소는 무엇인지, 또 써버쪽에선 어떤 웹써버 프로그램을 쓰고 있는지, DocumentRoot의 path는 어떻게 되는지 등등을 의미합니다. 이런 정보들은 사용자가 폼에 직접 입력한 데이타와 함께 http header에 담겨서 써버에 전달됩니다. (http로 나누는 대화를 읽어 보세요) 이를테면 게시판에 글을 올리는 경우를 생각해 봅시다. 게시판 글쓰기 창(폼)에 사용자가 입력한 글과 함께, 현재 사용자가 어떤 웹브라우져를 쓰고 있고, ip 주소가 무엇인지 등의 정보가 함께 웹써버에 전달되는 것입니다. 그러면 이 전달된 정보는 어디에 저장될까요.
환경 변수 (Environment Variables)
사용자가 폼등을 통해 어떤 데이타를 입력한 경우, 입력한 그 데이타 뿐만 아니라 여러가지 부가적인 정보들이 웹써버로 즉시 전달된다고 했는데요, 이들 정보는 웹써버의 환경변수(Environment Variable) 라는 것에 저장 됩니다. CGI는 바로 이 환경변수에 접근해서 클라이언트 컴퓨터의 정보나 사용자가 입력한 데이타를 가져오는 것입니다. 환경변수에는 다음과 같은 것이 있습니다. 대략 이런것이 있다는 정도로만 우선 파악하시면 됩니다.
SERVER_NAME | 써버의 호스트네임과 ip 주소 |
SERVER_SOFTWARE | 써버 소프퉤어의 버전과 이름 |
SERVER_PROTOCOL | 사용자 요청을 처리하는 프로토콜의 버전과 이름 |
SERVER_PORT | 써버가 동작하고 있는 포트 넘버 (포트에 관한 설명은 TCP/IP 및 네트웍 프로토콜 설명 클릭) |
REQUEST_METHOD | 요청 방식 |
PATH_INFO | cgi 프로그램에 전달된 부가적 패쓰 정보 |
DOCUMENT_ROOT | 웹문서의 루트 디렉토리 |
QUERY_STRING | 사용자가 폼에 입력한 정보 |
REMOTE_HOST | 사용자의 호스트네임 |
REMOTE_ADDR | 사용자의 IP 주소 |
CONTENT_TYPE | 마임 (MIME) 타입 |
CONTENT_LENGTH | 표준입력을 통해 CGI 프로그램에 전달되는 데이타의 길이 |
HTTP_REFERER | 사용자가 CGI 프로그램에 접근하기 전에 머물던 문서의 주소 |
HTTP_USER_AGENT | 사용자가 사용하는 브라우져의 종류 |
이와 같은 정보들이 웹브라우져로 cgi 프로그램에 접근하는 순간 웹써버의 환경변수에 담기게 되는 것입니다. 그러면 펄을 이용해서 이들 정보에 접근해보죠. 펄에서는 %ENV
해쉬를 통해서 이들 환경변수에 접근할 수 있습니다. 즉, 각각의 환경변수를 $ENV{'환경변수'}
의 형태로 간편하게 접근할 수 있는 것이죠. 이야기를 더 진행시키기 전에 우선 HTTP 헤더(header)에 관해서 알고 있어야 합니다. 아직까지 http로 나누는 대화를 읽지 않으셨다면 꼭 읽고 오시기 바랍니다.
링크 해드린 글에 보시면 웹써버가 사용자 웹브라우져로 요청된 문서를 보내줄때 http 리스판스 헤더와 함께 전달한다고 했고, 그 헤더에는 다음과 같은 정보가 담겨고 했습니다.
HTTP/1.0 200 Found
Date: Mon, 10 Feb 1997 23:48:22 GMT
Server: Apache/1.2
Content-type: text/html
Last-Modified: Tues, 11 Feb 2000 22:45:55 GMT
바로 위와 같은것이 http 리스판스 헤더 입니다. 헤더의 각 줄에 대한 설명은 링크 해드린 글을 읽어봤다면 대략 이해를 하고 계실거구요. 써버에서 요청된 데이타를 보내주는 http 프로토콜 버전, status code (200 Found; 요청한 문서가 있더라), 보내주는 날짜, 웹써버의 버전, 보내주는 파일의 마임타입, 마지막 변형된 날짜등이 웹써버로부터 클라이언트쪽으로 전달되는 것을 알 수 있습니다.
그런데 이 헤더와 요청된 데이타 사이의 구분은 어떻게 할까요.
즉, 지금 써버에서 클라이언트에 http라는 프로토콜을 통해 보내주는 데이타 중 과연 어디까지가 http 헤더고, 어디서부터 요청된 데이타인지를 구분하는 방법이 있어야 하지 않겠습니까?
http 프로토콜의 경우 '빈 줄 하나가 띄워진 다음 부터가 요청된 데이타'로 약속되어 있습니다. 즉 위와 같은 헤더 정보가 쭉 나온다음, 빈 줄 한 줄이 나오면 헤더는 끝나고 데이타가 시작된다고 웹브라우져에서 인식하는 것입니다. 이 http 리스판스 헤더중 대부분은 웹써버에서 알아서 설정해주므로 (물론 일일이 직접 설정할 수도 있습니다) 써버 버전이나 http의 버전, status 코드 등은 굳이 우리가 작성하지 않아도 됩니다. 우리는 전달되는 데이타의 마임타입을 가르키는 Content-type
만 지정해주면 됩니다. 다음의 코드를 보시죠.
#!/usr/bin/perl
print "Content-type: text/html\n\n";
print "<html><head></head><body>";
print "서버이름: $ENV{'SERVER_NAME'}<br />";
print "서버 포트: $ENV{'SERVER_PORT'}<br />";
print "프로토콜: $ENV{'SERVER_PROTOCOL'}<br />";
print "사용자 웹브라우져: $ENV{'HTTP_USER_AGENT'}<br />";
print "사용자 IP 주소: $ENV{'REMOTE_ADDR'}";
print "</body></html>";
제일 중요한 부분은 첫 줄 입니다. 보시면 Content-type: text/html
다음에 빈 줄 한 줄을 만들고 있는 것을 볼 수있습니다. (\n\n
) Content-type
은 보내줄 문서의 마임타입을 지정한 것입니다. 어쨌든 빈 줄이 나왔으므로 이제 웹브라우져는 http 헤더는 끝났다고 보고, 그 다음 나오는 내용부터 웹브라우져에 띄워주게 되는 겁니다. (또는 마임타입에 따라서 연결된 프로그램을 돌려서 띄워주게 됩니다. 예를들어 mp3 파일이라면 윈앰프가 뜨면서 mp3 파일이 실행됩니다) 위의 코드를 보면, $ENV{'SERVER_NAME'}
, $ENV{'REMOTE_ADDR'}
처럼 %ENV
해쉬를 이용해서 환경변수에 접근하고 있는 것을 볼 수 있습니다.
이 코드를 적당한 이름으로 저장하고 (예: env.pl) 퍼미션을 755 로 준다음 (chmod 755 env.pl
) 웹브라우져에서 띄워보시면 (http://써버이름/env.pl) 다음과 비슷한 결과가 출력될겁니다.
서버이름: abc.com
서버 포트: 80
프로토콜: HTTP/1.1
사용자 웹브라우져: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)
사용자 IP 주소: 61.77.173.220
물론 위의 ip 주소는 가짜 입니다. ^_^
퍼미션이 755인 문서는 그 문서를 웹브라우져로 '보내 주는' 것이 아니라 웹써버에서 실행(execution)하게 됩니다. 즉, cgi 프로그램이 웹브라우져에 의해 요청되는 경우, 일반 html 문서처럼 웹브라우져로 보내서 그대로 보여주는것이 아니라 웹써버에서 바로 실행을 해야 하기 때문에 퍼미션을 755 로 주는 것이죠. (Permission 755 는 -rwxr-xr-x
죠? r 은 read, w 는 write, x 는 execute)
참고로, 모든 환경변수들을 다 알아보고 싶다면 다음과 같이 하시면 됩니다. 한번 해보세요.
#!/usr/bin/perl
print "Content-type: text/html\n\n";
print "<html><head></head><body>";
foreach $name (sort keys %ENV) {
print "<b>$name : </b> $ENV{$name} <br />"
}
print "</body></html>";
적당한 이름으로 저장하신 다음 퍼미션을 755 로 해주고 웹브라우져에서 불러보시면 모든 환경변수를 다 보실 수 있습니다.
같은 기능을 하는, 보다 더 '펄스타일'에 가까운 코드는 이렇습니다.
#!/usr/bin/perl
print "Content-type: text/html\n\n";
print "<html><head></head><body>";
print qq($_ : $ENV{$_}<br />) foreach sort keys %ENV;
print "</body></html>";
펄의 default variable 인 $_
을 활용해서 foreach
구문을 한 줄로 줄여놓은 것을 볼 수 있습니다. cool 하죠? qq( )
는 큰 따옴표로 괄호안을 묶어준다는 의미이구요. 큰 따옴표 안에서 또 큰 따옴표를 사용할때도 탈출 \"
을 할 필요가 없기 때문에 자주 쓰이는 것입니다. 예를들어,
print "이것은 \"펄\" 프로그램 입니다";
print qq(이것은 "펄" 프로그램 입니다);
둘은 똑같은 것입니다. qq( )
가 아주 편리하죠?
위의 환경 변수 출력 프로그램은 터미널에서 한 줄로 입력할 수도 있습니다.
perl -e 'print qq($_ : $ENV{$_}\n) foreach sort keys %ENV'
perl -e ' '
는 터미널에서 한 줄짜리 펄 코드를 바로 실행하게 하는 것입니다. 이렇게 다른 프로그램으로 코딩하면 여러 줄이 될 수 있는 것을 축약해서 한 줄로 만드는데 펄의 묘미가 있는 것이구요. 특히 유닉스 긱스들은 이러한 '한 줄'에 집착이 약간 있는 편이라고들 하죠. 영어로는 "one-liner"라고 하는데요. 유닉스의 파이프를 이용해서 복잡한 기능을 한 줄로 멋지게 만들어 내는 것을 '단순하고 아름답다' 라고 생각하는 사람들이 상당수 있답니다. 펄이 유닉스 긱스들에게 어필한 여러가지 이유가 있겠지만 펄의 "simple but elegant"한 코딩 스타일이 유닉스 스타일과 잘 맞아 떨어졌다는 점도 큰 이유중 하나입니다.
cgi 프로그램은 퍼미션만 755로 설정했다면 굳이 웹브라우져를 띄우지 않고도 실행해볼 수 있습니다. 예를들어 위의 env.pl의 경우 프로그램이 있는 디렉토리로 cd
해서 이동한 다음, ./env.pl
라고 입력하면 터미널 창에서 실행을 할 수 있습니다. 왜 그렇게 하느냐. cgi의 디버깅을 할 때 웹브라우져 상에서 하기는 조금 곤란합니다. 에러를 브라우져로 띄워줄 수 있게 코딩할 수 있지만 번거롭구요. cgi 프로그램에 큰 버그가 없는지 가장 간단하면서도 효과적으로 확인하는 방법이 바로 터미널 창에서 바로 실행하는 것입니다.
어쨌든 환경변수에 어떤 정보들이 담기는지 또 이 정보를 펄에서는 어떻게 접근할 수 있는지 이제 아셨을 겁니다.
사용자가 폼에 입력한 정보에 접근하는 방법은 훨씬 더 복잡하므로 밑의 "웹 기반 복리 계산 프로그램 만들기"에서 자세하게 설명 드리겠습니다.
그러면 이번엔 웹써버에서 cgi를 거쳐 밖으로 나갈 수 있는 것은 뭐가 있는지를 알아봅시다. 다음과 같은 것이 나가게 됩니다.
- 여러가지 바이너리 데이타들 (그래픽 파일, ..)
- 보내주는 문서를 캐쉬에 저장할 것인가를 지정
- 특별한 http status code
첫번째 것을 볼까요? '바이너리 파일'은 단순한 그래픽 파일일 수도 있고 웹써버상의 다른 프로그램 (예를들면 데이타베이스 프로그램)에서 처리한 결과물일 수도 있습니다. 그런 바이너리 파일이 cgi를 거쳐서 사용자 컴퓨터쪽으로 갈 수 있습니다. 그러므로 글 서두에 말씀드린 것처럼 써버(또는 써버와 연결된 다른 컴퓨터)에 있는 DB등의 프로그램으로 처리한 데이타가 사용자쪽으로 건너갈 수 있는 것입니다.
두번째, 세번째에 관해서는 조금 있다가 말씀드리구요. 우선 http 헤더에 대해서 조금 더 얘기를 해봅시다.
사용자가 링크를 클릭하는 순간 만들어지는 http 리퀘스트 헤더에는 Accept
가 있습니다. 나는 이런 이런 마임타입의 파일들을 처리할 수 있다는 것을 웹써버에 알려주는 것이죠. 보통은 */*
, 즉 임의의 모든 마임타입을 다 처리할 수 있게 되어 있습니다. 그리고, 웹브라우져에 의해 보내진 Accept 정보는 웹써버 환경변수 중 $ENV{'HTTP_ACCEPT'}
에 저장 됩니다.
한편 웹써버에서 어떤 문서 (또는 그래픽, 싸운드..)를 보내줄때도 '다음에 보내주는 데이타는 이런 마임타입이다.' 라고 알려주는 부분이 있습니다. 그것이 바로 http 리스판스 헤더중 Content-type
입니다. 위에서 본 예의 경우, Content-type
을 text/html
로 지정해서 "빈 줄 한 줄이 나온 다음의 내용은 html 텍스트 이므로 그걸로 처리해라"고 얘기하고 있는 것입니다. 따라서 이 헤더를 전달 받은 웹브라우져는 운영체계에서 text/html
을 처리하도록 지정된 프로그램을 (이 경우는 웹브라우져 그 자신) 띄워서 전달받은 데이타를 처리하게 됩니다. (마임 타입(Mime Type)을 읽어보세요)
이와 비슷한 것으로 Content-length
도 있습니다. 이것은 나가는 데이타가 어느 정도의 길이라는 것을 알려주는 부분 입니다. 이처럼 웹써버에서 클라이언트로 보내주는 리스판스 헤더를 cgi프로그램을 이용해서 하나하나 다 지정 해줄 수 있습니다. 위에서 Content-type
을 지정한 것처럼 말이죠.
그러한 헤더 정보중에 캐쉬를 하게 할 것인가를 지정하는 부분이 있습니다. 웹브라우져는 웹써버로부터 받은 문서나 그래픽등을 하드디스크 내에 캐쉬라는 형태로 저장을 합니다. 그래서 브라우징 속도를 빠르게 하죠. 바로 이 캐쉬에 지금 보내주는 내용을 저장하게 할것인가 말것인가를 http 리스판스 헤더의 Expires
, Pragma
를 통해 지정할 수 있습니다. 예를들어 text/html
문서를 보내면서 사용자 컴퓨터에 캐쉬로 저장되기를 원치 않는 경우에는,
#!/usr/bin/perl
print "Content-type: text/html\n";
print "Pragma: no-cache\n\n";
print "<html> .. .";
.. .. .
Content-type : text/html
다음에 빈 줄을 만들지 않고 그냥 줄바꿈만 하고 있는것을 아실 수 있습니다. (\n
한개) 따라서 아직은 헤더가 끝나지 않았다는 것을 얘기하고, 그 다음 줄 Pragma
에서 캐쉬에 저장하지 않는 것으로 지정한 다음 빈 줄을 하나 만들고 (\n
두개), 이제 헤더는 끝, 그 다음에는 전달될 데이타가 나오고 있습니다.
마찬가지로 다음과 같이 날짜를 지정할 수도 있습니다.
#!/usr/bin/perl
print "Content-type: text/html\n";
print "Expires: Wednesday, 27-Dec-99 05:13:10 GMT\n\n";
.. .. .
웹 기반 복리 계산 프로그램 만들기
위의 내용을 이용해서 만들어 볼 cgi프로그램은 원금과 연이율, 그리고 돈을 맡기는 햇수를 주면 이를 복리로 계산해서 원리합계표를 만들어 주는 프로그램 입니다. 이 cgi 는 단순해 보이지만 사실상 cgi 프로그램의 기본 원리는 모두 다 담고 있으며, 이 프로그램을 이해하고 나시면 게시판이나 방명록 또는 채팅 (채팅 프로그램은 사실상 실시간 방명록에 다름 아닙니다) 프로그램의 기본적 골격에 대해서도 쉽게 이해를 할 수 있으실 겁니다.
먼저, 앞에서 얘기한대로 사용자가 폼을 통해 입력한 정보가 cgi를 통해 웹써버로 전달된다고 했습니다. 예를들어 위 cgi 프로그램의 경우 원금란이나 이율란에 입력한 숫자들이 바로 사용자가 입력한 정보인 것입니다. 이들 정보는 "보내기" 버튼을 통해서 웹써버로 전달되는데, 과연 이 정보들은 어디로 가는 것일까요.
여기에 대해 자세히 알아보기 전에 알아두셔야 할 내용은, 폼을 통해 입력된 정보가 웹써버로 전달되는 방식에는 크게 2 가지가 있다는 점입니다. 2가지란 "GET"과 "POST"를 얘기합니다.
- 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'}
에 저장됩니다. - 이와달리 웹써버의 표준입력(STDIN) 으로 전달되는 방식이 POST method 입니다. 따라서 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
Accept
부분이나 User-Agent
는 이제 잘 이해되시죠? 어쨌든 두 방식이 전혀 다르게 전달된다는 것을 볼 수 있습니다.
GET 방식, POST 방식은 각각 장단점이 있습니다. GET 방식의 경우엔 url을 통해서 사용자가 입력한 정보가 노출된다는 점이 단점입니다. 노출될 뿐만 아니라, 웹폼을 통하지 않고도 주소창에서 cgi 주소 끝에 aaa=bbb&ccc=ddd
를 붙여서 cgi 프로그램을 동작시킬 수 있다는 보안상의 약점이 있습니다. 또, 클라이언트나 써버에 따라서 url을 잘라내어 버리는 경우(truncate) 데이타가 제대로 전달되지 않을 수도 있습니다.
그런데 폼에 입력된 정보가 어떤 방식으로 전달되는가는 HTML 쪽에서 결정됩니다.이런 식이죠.
<form action="compound.pl" method="GET">...</form>
<form action="compound.pl" method="POST">...</form>
그리고 폼입력 정보를 추출하는 방식은 위에서 말씀드린대로 각각 전혀 다른식으로 이뤄집니다. 따라서 혼자서 html 디자인과 cgi 프로그래밍을 다하는 경우라면 몰라도 웹디자이너와 웹프로그래머가 각기 작업을 하는 경우 웹디자이너가 어떤 방식으로 html 을 만들었느냐를 일일이 확인을 해야하는 번거로움이 생기게 됩니다.
그러므로 cgi를 만들때는 디자이너가 어떤 방식으로 해놓았든 사용자 입력 정보를 잘 처리해낼 수 있도록 코딩을 하는것이 좋습니다. GET 이든 POST 이든 다 처리할 수 있게 코딩할 필요가 있다는 것이죠.
그러면 실제 어떻게 써버에 전달된 폼입력 정보를 처리하는지를 자세히 알아봅시다.
위에서 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 이면 $buffer
에 $ENV{'QUERY_STRING'}
에 담겨있는 것을 담는다 라고 되어 있습니다. 쉽게 이해가 되시죠? 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
에 담습니다. 그다음 나오는 2 줄은 잠시 건너뛰세요. 마지막줄에서는 $value
를 $FORM{$name}
에 할당함으로써 %FORM
해쉬를 만드는것을 알 수 있습니다.
건너뛴 2 줄을 제외하고는 평이한 펄코드 입니다.
이제 문제의 2 줄에 대해서 설명을 드릴께요.
첫번째 $value =~ tr/+/ /;
은 $value
에 들어있는 모든 플러스 표시를 공백으로 바꾸는 것입니다. 갑자기 웬 플러스 표시? 우리가 폼에 입력한 값에는 공백이 있는 경우도 있을 겁니다. 예를들어 이름을 입력하면 "이 명헌" 이라고 입력할 수도 있는 것입니다. 이런 공백문자는 http를 통해 써버에 전달될때 자동으로 플러스 표시로 바뀌어서 (인코딩encoding 되어서) 전달 됩니다. 따라서, 우리는 전달된 값에 있는 플러스 표시를 다시 공백으로 바꿔줘야 원래 사용자가 입력한대로 되돌려 놓을 수 있는 겁니다.
그 다음 줄 $value =~ s/%([a-fA-F0-9]{2})/pack("C",hex($1))/eg;
괜히 긴장되시죠? ㅡ_ㅡ;
자세히 보시면 아무것도 아닙니다.s///eg;
는 레귤라익스프레션을 공부하셨다면 잘 알고 계실거구요. 플랙으로 붙은 g는 global, 즉 해당 패턴을 나오는대로 다 찾는다는 의미이고, e는 expression , 즉 대체될 부분에 pack 구문과 같은 expression이 담길때 쓰는 플랙이죠. 그러면 첫번째 슬래쉬 사이에 있는 레귤라 익스플레션을 자세히 살펴보죠. 보시면 %
기호가 나오고 a-f 또는 A-F 또는 0-9 중에 한글자씩 2글자로 이뤄진 패턴을 가르킵니다. 즉 이런것들을 가르키는거죠. %3f, %07, %2a, %ba, %7a , . . . .
이게 뭡니까..? 어디서 많이 보신거죠? 바로 16진수 값들이라는 것을 대충 느끼셨을겁니다. 이들은 바로 아스키코드 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><head><title>복리 계산기</title>
<meta http-equiv="content-type" content="text/html; charset=euc-kr" /></head>);
print qq(<body><br /><br />
<table border="0" width="60%" bgcolor="#ccccff">
<tr><td>복리의 놀라운 마술을 보시기 바랍니다</td>
</tr></table><br />);
}
# HTML footer 출력
sub htmlFooter {
print qq(</body></html>);
}
# 사용자 입력 폼 출력
sub inputForm {
print qq|<form action="./compound.pl">|;
print qq|<table border="0" width="60%" cellpading="10" cellspacing="10">|;
print qq|<tr><td bgcolor="#eeeeee">원금을 입력하세요 (만원단위)
<input type="text" name="principal" /> 만원</td></tr>|;
print qq|<tr><td bgcolor="#eeeeee">연이율을 입력하세요 (퍼센트)
<input type="text" name="rate" /> %</td></tr>|;
print qq|<tr><td bgcolor="#eeeeee">맡기는 햇수를 입력하세요
<input type="text" name="years" /> 년</td></tr>|;
print qq|<tr><td bgcolor="#cccccc">
<input type="submit" value="계산하기" size="30" />
</td></tr></table>|;
print qq(</form><br /><br /><br />);
}
# 원리합계를 계산 합니다
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 {
print qq|<br />원금 $first 만원을 이율 $rate % (복리) 로 $years 년 동안
예금하시면,<br /><br /><br />|;
print qq|<table border="0" width="800" cellpadding="5" cellspacing="1"><tr>|;
for my $j (1..$years) {
print qq|<td bgcolor="#cccccc">$j년</td>|;
}
print qq|</tr><tr>|;
for my $k (1..$years) {
print qq|<td bgcolor="#ffffff">$sum[$k]만원</td>|;
}
print qq|</tr></table>|;
}
위와 같이 됩니다. 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를 만드는 내용에서는 CGI.pm을 사용해서 설명을 드리겠습니다. CGI.pm을 사용하면 사용자 입력 내용을 파싱 하는 코드가 필요 없이 $value = param('name');
라는 코드로 곧바로 입력된 값에 접근할 수 있다는 점만 잘 외워두시면 됩니다.
위의 코드는 사실 cgi의 기본적인 기능만을 극히 간단하게 보여주는 정도 입니다. 본격적인 cgi 개발은 다음 글들에서 자세하게 다룰 것입니다. 보안의 문제라든지, 기타 실제 코딩 과정에서 부딪히게 되는 몇 가지 문제들과 함께 자세하게 공부하게 됩니다.
복리는 정말 놀랍죠??