적절한 서버측 스크립트 언어 선택하기
동일한 기본적인 작업을 다섯 가지 언어가 구현하는 방법 요약: Perl, PHP, Python, Tcl 및 Java 서블릿과 같은 인기 있는 다섯 가지 스크립트 툴(script tools)을 동일한 6개의 일반적인 서버 사이드(sever-side) 태스크에 적용시켜 비교해 본다. 각각의 문법(syntax)을 나란히 조사하고 어떻게 각 언어가 특정한 태스크를 처리하는지에 대해 평가 할 수 있다. 서버측 스크립트 언어를 사용해본 적이 없거나 이들 언어 중 일부만 사용해 보았다면 이들 언어들이 어떻게 닮았는지 확인할 수 있을 것이다. 이미 한 언어에 익숙해 있다고 하더라도 가용성, 기능성, 그리고 가독성에 있어서 다른 언어들이 어떤 특징을 갖는지 파악할 수 있을 것이다.
개인적으로 Java 서블릿, Perl, PHP, Python, Tcl등 5개 언어 선택에 있어서 나는 매우 과학적인 개념을 사용한다. 두 가지 유형으로 이 언어들을 분류할 수 있다. Common Gateway Interface(HTML을 리턴하는 외부 프로그램을 호출)와 일명 슈퍼 마크업(다른 언어 마크업 코드를 포함하는 HTML 페이지, 즉 HTML의 슈퍼셋(superset))이라고 하는 것이다. 여러분이 선호하는 서버 스크립팅 툴을 누락시켰다면 이해해주길 바란다. 모든 컴파일 가능한 툴을 가지고 있지 않기 때문이다.
각 언어들의 작동방법을 나란히 보여주기 위해, 각각의 언어로 다음과 같은 동일한 여섯 가지 태스크를 구현하였다.
- 태스크 1 : time/date 가져오고 포맷하기
- 태스크 2: 폼 필드의 데이터를 변수에 저장하기: 2개의 HTML 폼 필드
- 태스크 3: 검색 및 대체
- 태스크 4: 파일 쓰기: 트랜잭션 날짜 및 시간에 따라 컴마로 분리된 두개의 값을 CSV 텍스트 파일(CSV = comma-separated values, 예: 1,2,3)에 기록
- 태스크 5: 파일 읽기: 모든 CSV 파일의 기록을 읽고 표시한다.
- 태스크 6: 행을 컴마로 구분하여 변수에 할당: 앞의 파일의 내용을 컴마로 구분하여 다시 변수에 할당한다.
위의 기본 태스크를 위해서 각 언어 당 1 개씩, 모두 5개의 스크립트(참고자료)를 제공한다. 여기서 잠깐 시간을 내어 먼저 전반적으로 각 스크립트 언어를 검토한 후 상세한 분석에 들어 가도록 하겠다. 이 중 어떤 언어의 전문가가 이 코드를 본다면, 여기서 사용한 각 언어의 숙어가 매우 적음을 알게 될 것이다. 난 초보자의 편의를 위해 성능(performance)을 포기하고 가독성(readability)에만 신경을 썼다.
태스크별로 프로그램에 대해 상세히 알아보겠다.
태스크 1: time/date 불러오고 포맷하기
이것은 다른 어떤 언어보다도 스크립트 언어가 편리하다. 스크립트가 로그에 기록할 때 사람이 읽기 편한 포맷으로 time/date를 보여주길 원할 것이다. 이 기본 기능에 대한 두 가지 방법론적 입장(특히 Raw Date 와 Formatted Date)이 있다는 것은 매우 흥미로운 일이다.
Raw Date 방법은 Perl, Python, Tcl의 경우가 이에 포함되는데, 기준 시간 부터 초의 수를 리턴한 후 시간 및 날짜 포맷 함수를 거쳐 기술되는 방식이다. 날짜 데이터에 조작을 많이 가해야 하는 경우라면 Java 서블릿이나 PHP가 제공하는 완전히 포맷된 문자열로서의 날짜 데이터가 더 나을 것이다. 물론 예외 없는 규칙은 없으며 여기서 모든 것을 언급하지는 않겠다.
태스크2: 폼 필드의 데이터를 변수에 저장하기
각 언어는 환경에서 "name=value" 쌍을 불러오기 위해 자체 고유 방법과 숙어가 있다. 대부분 이 기본 트레이드 트릭은 각각 새로운 스크립트로 잘라서 붙여넣기의 단순한 방법이다. 또한 필요하면 수정도 하는데 많지는 않다. 웹 서버가 외부 프로그램을 호출할 경우는 클라이언트 브라우저에 의해 전송된 데이터는 그 프로그램 환경에 있는 특정 변수에 할당된다. 포스트 메소드를 사용하는 HTML 형식을 위해서 프로그램은 standard in(STDIN)에서 "Comment Length" 환경 변수에 있는 바이트 수를 읽어야 한다. STDIN에서 데이터를 보관하는 프로그램 변수는 지금 "name=value" 페어(pair) 문자열을 보관하고 URL 디코드 값, 단편으로 분리한 다음 프로세싱 변수로 입력할 필요가 있다.
다음이 그 예제이다(오류 검사가 없다는 것을 유의):
Python은 할당 변수 값에 직접 액세스를 허용하여 태스크를 단순화시키는 모듈을 사용한다 :
Python:
form = cgi.FieldStorage() data = form['data'].value data2 = form['data2'].value |
PPHP 및 Java 서블릿을 사용하는 경우 예상한 변수명을 받으면 첫번째 단계를 생략하고 바로 할당될 수 있다. 보다 더 일반적인 프로세스로 생성하여 처리하려면 접수된 변수 리스트 통해 프로세스를 반복하여 이 리스트를 동적 할당 시키는 것이 적합하다.
PHP:
$data = $HTTP_POST_VARS["data"]; $data2 = $HTTP_POST_VARS["data2"]; |
Java:
String data = request.getParameter("data"); String data2 = request.getParameter("data2"); |
Perl의 표준 코드 블록은 모듈에 숨겨지는 것이 아니다. 여기서 모든"name=value" 쌍은 문자열에서 발췌되어 각각 @pairs array에서 슬롯에 할당된다.
Perl:
read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'}); @pairs = split(/&/, $buffer); |
그 다음, 배열을 통해 이름 및 값의 변수 입력을 반복한다 :
foreach $pair (@pairs) { ($name, $value) = split(/=/, $pair); |
브라우저로 제출된 값은 서버로 전송하기 위해 "URL encoded"된 것이므로, 먼저 모든 +s를 공백으로 변경한 다음에 모든 escape 코드를 원래의 값으로 변환시켜 "URL encoded"된 값을 가져야 한다.
$name =~ tr/+/ /; $name =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; $value =~ tr/+/ /; $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; |
마지막으로 = (equal)의 우측에 있는 무엇이든지 = sign 좌측의 명명된 변수 값으로 할당한다.
$Form{$name} = $value; } |
Tcl 코드 방법도 이와 동일하다. 모든 프로그램에 이 논리를 적용시키게 되면 지루하면서 오류를 범할 수 있다. 다행히 어떤 CGI 프로그램에서 축어를 사용하여 이 블록을 잘라내어 붙여넣기(paste)할 수 있다.
태스크 3: 검색 및 대체
HTML 폼에 입력된 데이터는 빈번하게 수정이 필요하게 된다. 또한 사용자가 입력한 데이터를 이용하여 쉘 명령어를 실행하려면 절대적으로 필요 한 절차이다. 시스템의 무결성을 손상시키는 어떤 입력된 데이터라도 정리 작업이 필요하다.
Perl은 가장 강력한 정규식 엔진(regular expression engine)을 가지며 텍스트 조작에서 뛰어난 성능을 발휘한다. 여러분의 프로그램에 많은 텍스트 조작이 필요하다면 Perl을 사용하는 것이 좋을 것이다. Python, PHP, Tcl은 Perl의 화려한 구문(syntax)과 비교했을 때 각각의 인터페이스는 부자연스럽고 복잡해 보이긴 하나, 정규식의 검색 및 대체를 지원한다. 명확성을 위하여 각각 동일한 변수명 사용에 참조할 수 있는 수정된 스크립트 예제가 있다. 대소문자를 구별하지 않고 "cat"이라는 문자를 검색한 후 'data'변수의 값으로 할당한 후 다시 각각에 대해 "dog"단어로 대체된다 :
Perl:
$data =~ s/cat/dog/gi; } |
PHP:
$data = eregi_replace("cat","dog",$data); } |
Python:
data = regsub.gsub("cat", "dog", data) } |
Tcl:
regsub -all -nocase {cat} $data {dog} data } |
Java 언어는 정규식을 지원하지 않는다. 그러나 substring을 이용하여 쉽게 문자열 대체기능을 구현할 수 있다 :
Java:
String findString = new String("cat"); String replaceString = new String("dog"); int x = data.indexOf(findString); while(x != -1) { data = new String(data.substring(0,x) + replaceString + data.substring(x+findString.length())); x = sourceString.indexOf(findString); } |
별로 우아하게 보이지 않는다. 다른 언어로 같은 기능을 구현해보면 명확하고 간단 명료한 하나의 구문으로 표현할 수 있으므로 그것이 더 나아 보인다. 어쨌든 Sun은 표준 Java에 정규식 관련 기능을 넣지 않기로 결정했다.
태스크 4: 파일 쓰기
Java 서블릿을 제외한 나머지 언어의 경우 직관적이고 유사한 형태를 보인다. 각각의 언어에서 "file.txt"라는 파일을 추가(append) 모드로 여는 방법과 "out"이라는 변수에 파일 핸들로서 어떻게 할당하는지 비교해보라 :
Java:
PrintWriter out = new PrintWriter(new FileOutputStream("file.txt",true)); |
Perl:
open (OUT, ">>file.txt"); |
PHP:
$out = fopen("file.txt", "a"); |
Python:
out = open("file.txt", 'a') |
Tcl:
set out [open "file.txt" a+] |
PHP, Python, Tcl 코드의 단순성, 직관성 및 유사성을 주목하자. Perl 코드도 아주 비슷하면서 쉽고 직관적이다. Java에서의 "그것을 위한 클래스가 존재한다(There's a class for that)"라는 식의 접근법과 대조해 보라. 사실상 Java에는 이런 작업과 관련된 60개의 클래스가 존재한다. 언어 자체에서 알아서 수행하는 대신, 어떤 출력 스트림을 사용할 것인지, 그리고 모든 입출력 상황에서 어떤 특정한 프린트 라이터(print writer)를 사용할 것인지를 직접 결정해야 한다. 더구나 구문의 끝에 있는"true"가 추가 모드(append mode)의 의미로 받아들일 수 있을 만큼 직관적인가? PHP, Python, Tcl은 추가 모드의 경우 'a'를 사용한다. 그리고 파일 핸들링도 이해하기 쉬우며 몇 번의 타이핑으로 끝난다.
여기서 증명된 대로 파일 쓰기 태스크는 (파일 핸들 "out"에 변수 "joined"의 내용을 쓰는 것) 전체적으로 비슷하다 :
Java:
out.println(joined); |
Perl:
print OUT "$joined\n"; |
PHP:
fwrite($out, $joined); |
Python:
out.write (joined) |
Tcl:
puts $out $joined |
태스크 5: 파일 읽기
스크립트 언어에서 파일을 읽기 위해 여는 일은 매우 간단하지만, Java에서는 객체의 생성이 요구된다. 사실 파일 읽기는 각 언어마다 약간의 차이가 있고 언어 간의 원리적인 차이를 보여준다. Perl 및 Python은 각 행을 배열 또는 목록의 요소에 적절하게 할당하고 각 요소에 대해 이를 반복하여 처리함으로써 전체 파일 읽기를 용이하게 해준다. 다른 언어 툴은 한 개의 행을 읽고 이를 처리하고 나서 또 다른 행을 찾을 수 있게 되어 있다. 다음 사례는 가장 소수의 코드부터 최대수의 코드까지의 예이다. 각 사례에서 파일 핸들이 'in'이고 각 행은 STDOUT로 인쇄된다 :
Perl:
@lines = <IN>; foreach $line (@lines){ print $line; } |
Note: Python 프로그래머들은 Perl은 추가적인 문자를 사용한다는 내용의 메일을 나에게 보내기 전에, "foreach" 대신 "for" 또는 아예 전체를 for (<IN>){ print }로 사용할 수도 있었다는 것을 알기 바란다.
Python:
lines = in.readlines() for line in lines: print line |
PHP:
while (!feof($in)) { $line = fgets($in, 4096); print $line; } |
Tcl:
while {1} { gets $in line puts $line if {$line == ""} { break } } |
Java:
do { try { line = in.readLine(); if (line != null){ out.println(line); } } catch(Exception e) { e.printStackTrace(); } } |
태스크 6: 콤마 구분 행(comma-delimited)을 변수로 분리
스트립트는 CSV 파일에서 행을 읽는다. 각 필드를 쉽게 개별 변수로(이 경우 a,b,c,d) 분리하는 방법은 무엇일까? Perl, PHP, Python은 구분자를 인수로 하여 문자열을 분리하는데 편리한 'split' 함수를 가지고 있다. Java 서블릿과 Tcl는 각 필드를 개별적으로 설정해야 한다. 이 예제에서는 문제가 없으나 각 행이 많은 필드를 가지는 경우에는 반드시 주의해야 한다.
Perl:
($a,$b,$c,$d) = split /,/, $lines[@lines-1]; |
PHP:
list( $a,$b,$c,$d ) = split( ",", $last, 4 ); |
Python:
a,b,c,d = splitfields(lines[-1], ',') |
Java:
String a, b, c, d; StringTokenizer st = new StringTokenizer(last, ","); a = st.nextToken(); b = st.nextToken(); c = st.nextToken(); d = st.nextToken(); |
Tcl:
set fieldlist [split $last ,] set a [lindex $fieldlist 0] set b [lindex $fieldlist 1] set c [lindex $fieldlist 2] set d [lindex $fieldlist 3] |
데이터 손상 주의
모든 입력 자료가 손상될 가능성 있다고 가정하고 최악의 사태를 대비해야 한다. 시스템 명령어를 데이터에 입력하여 해커들이 시스템 침입할 수 있다. 예를 들면 서버 스트립팅을 통해 사용자가 시스템에서 원격 프로그램을 실행할 수 있다. 시스템 침입에 대해 설명하는 것은 절대 아니다. 디렉터리 리스트를 불러오기와 같은 단순한 요청에서 발생될 수 있다는 것이다.
사용자에게 디렉터리명으로 "~" 또는 ".."과 같은 예상되는 입력 나열을 요청할 수 있다. 그러면 이 요청은 "ls ~" 같은 ls 명령어와 함께 쉘로 전송된다. 악의는 없지만 해커가 만약 "~;rm *"을 입력한다면? 쉘은 먼저 "ls ~" 다음에 "rm *"을 실행하면서 기꺼이 명령을 실행할 것이다.
의도한 바가 아니라 해도 그러한 데이터 손상에 주의를 하지 않으면 충분히 가능성이 있는 일이다. 예를 들면, Perl에서 알파벳과 숫자 또는 언더라인/별표/틸드 이외의 모든 것을 아예 제거할 수도 있다. 그런 경우에는 "ls ~;rm *" 명령은 "ls ~rm *"이 된다. 입력을 잘못해도 시스템에 큰 손상을 입히는 것이 아니라 단순한 오류만을 발생시킬 뿐이다.
적절한 언어 선택하기
자, 이제 교체하여 사용할 만한, 또는 현재 사용하는 것에 만족을 느낄 수 있을 만한 최상의 언어는 어떤 것일까? 사용하기에 최상의 언어란 가장 친숙한 언어이다. 별로 소용없는 대답 또는 완전 초보자에게는 쓸모없는 잔소리처럼 들릴 수도 있다. 간단한 사실은 Perl을 모르는 Tcl의 베테랑이라 하더라도 "다른 모든 사람들이 Perl을 사용할 줄 안다"는 이유로 전체 사이트의 CGI를 Perl로 작성하지는 못한다는 것이다. 더구나 개발 시간 확장이 필요한 것 외에도 매우 현실적인 위험이 도사리고 있다(우연히 보안 허점을 남기는 것 등). 그러나 잘 모르는 언어라도 바꿀만한 충분한 이유가 있다면, 중대한 웹 애플리케이션을 곧바로 작성하지 말고, 이전에 써왔던 스크립트를 새로운 언어로 포팅하는 작업부터 권유하고 싶다.
한가지 짚고 넘어가고 싶은 것은, 일반적인 생각과는 달리 Java 서블릿이나 PHP가 다른 스크립트 언어들보다 별로 빠르지 않다는 것이다. 이 언어의 엔진은 웹 서버의 일부로서 실행된다. 따라서 CGI 스크립트에서처럼 리소스를 요청할 때마다 새로운 프로세스 시작을 요구하지는 않는다는, 즉 Java 서블릿과 PHP가 다른 스크립트 언어보다 속도가 빠르다는 주장이 가능한 것이다. 그러나 위의 비교는 프로그램을 "CGI 방식"으로만 실행하는, 즉 Perl, Python 또는 Tcl 엔진을 서버에 탑재하지 않을 경우에만 사실이다. 이 언어들의 사용자는 깊이 들여다 볼 필요가 있는 모듈들이 존재하기 때문이다.
CGI 게임을 처음 접하려는 사람이라면, 이 몇 가지의 가능성에 대해 호기심을 가질 수 있다. 처음에 어떤 언어를 선택할 지에 대해 모든 프로그램을 검토해 보고 가장 적당한 언어를 검토해 본다. 직관적 또는 콘텍스트에서 쉽게 진행상황을 파악할 수 있는가? 스크래치부터 구성까지의 시도에서 더욱 편안하게 느껴지는 것이 어떤 것인가? dream과 speech에서 눈에 거슬리는 것이 어떤 것인가? 모두 무료이기 때문에 비용은 논의 대상에서 제외한다. 시스템에 웹 서버를 탑재하고 시작하자.
마지막으로 내가 Java 서블릿을 server-side 솔루션으로 비난한 것처럼 느껴질 수 있는데 그럴 의도는 전혀 없다. 대부분 다른 언어로는 server-side 애플리케이션을 잘 만들지 않는다. 또한 Java의 객체 지향적 문법 및 패키징의 오버헤드는 항상 개발 시간 및 노력을 투자할 가치가 있는 것은 아니다. 기타 언어 보다 Java 서블릿을 사용하는 것은 다음과 같은 실질적인 두 가지 이유 때문이다. 첫째, 여러분 회사의 시스템이 Java기반으로 되어 있고 server-side 프로그램밍 하는데 Java 프로그래머가 필요한 경우이다. 둘째, 여러분의 server-side 프로그래밍이 대규모의 복잡한 프로그램이 요구됨에 따라 "Java의 힘"이 필요하다고 판단 될 경우이다. 아는 채만 할 줄 아는 여러분의 보스가 이 같은 사항을 결정한 것이라면, 다른 한 언어를 사용하여 구축하고, 몇 주 동안 인터넷 서핑을 즐기다가, Java로 완성한 것이라고 나중에 보고하라.
참고자료
5개 언어로 구성된 홈 페이지:
CGI 성능 향상:
필자소개
댓글 없음:
댓글 쓰기