C/C++ 같은 지극히 static한 컴파일 지향 언어에서는 상상도 못 할 일이겠지만, 아주 다이나믹하고 인터프리터를 지향하는 프로그래밍 언어들은 문자열에 담긴 자기 언어 코드를 지금의 변수/함수 context를 기준으로 실행해 주는... 엄청난 기능을 제공하기도 한다.
PHP와 자바스크립트 모두 eval이라는 함수가 이 일을 한다. 루비던가 펄이던가 문자열의 동적 처리가 강한 다른 언어에도 응당 동일 기능이 있다.
문자열 변수에 들어있는 값(가령 "abc")을 통해서 해당 이름의 변수에 접근하는 것(abc)도 가능하다. C/C++에서는 지역 변수는 그냥 함수 스택 프레임으로부터 고정된 오프셋 숫자를 나타내겠지만, 저런 언어에서 지역 변수는 힙 기반의 해시나 트리를 참조하는 유동적인 이름-값 key 명칭에 해당할 것이다.
그리고 또 다르게 비유하자면, static type 언어는 테이블의 각 필드별로 타입과 정보량이 매우 엄격하게 정해져야 하는 데이터베이스와 같고(액세스),
dynamic type 언어는 셀에 들어가는 값과 타입이 자유자재로 바뀌어도 되는 스프레드 시트와 같다(엑셀).
어마어마한 대용량의 데이터를 처리하는 성능은 까다롭고 전문적인 DB가 훨씬 더 뛰어나지만, 그래도 엑셀 정도만 돼도 성능이 굉장히 좋아지고 그러면서 일상생활에서는 더 편리하다. 세상엔 그 정도 tradeoff는 어디에나 있는 듯하다.
그나저나, 문자열 코드를 실행하는 기능은 편리한 건 그렇다 치더라도 보안을 매우 취약하게 만들 수 있다는 점을 감안하고 사용해야 한다. C 언어의 %d, %s 같은 포맷 문자열만 해도 이미 위험하기 때문에 입출력 포맷 문자열에다가는 사용자로부터 실시간으로 입력받은 문자열을 절대 넣지 말아야 한다는 것이 불문율이다. 그런데 하물며 저건 문자열에 명시된 대로 아예 프로그램이 실행되니 C의 포맷 문자열과는 차원이 다른 방식으로 위험할 수밖에 없다.
본인이 경험해 본 프로그래밍 언어 중에 코드를 실시간으로 생성하고 자기 자신을 변형할 수 있는 물건의 원조는 역시 GWBASIC이었다.
비록 얘는 문자열을 코드로 그대로 해석하고 실행하는 기능은 없지만, 그래도 RUN, CHAIN, MERGE처럼 파일 차원에서 임의의 코드를 즉석에서 실행하는 기능은 있었기 때문이다.
다음은 스택 기반의 복잡한 파싱 없이 15*(2+5), 256^2, 1/5 등의 수식을 쓱쓱 계산해 내는 계산기 프로그램이다. 내가 이걸 생각한 게 20여 년 전의 일이다. ㅎㅎ
10 INPUT "Expression? ", A$
20 IF A$="" THEN END
30 OPEN "TMP.BAS" FOR OUTPUT AS #1
40 PRINT #1, "10 ON ERROR GOTO 30"
50 PRINT #1, "20 PRINT "+A$+": GOTO 40"
60 PRINT #1, "30 PRINT "+CHR$(34)+"Error"+CHR$(34)
70 PRINT #1, "40 RUN "+CHR$(34)+"CALC.BAS"+CHR$(34)
80 CLOSE #1
90 RUN "TMP.BAS"
프로그램의 동작 원리를 알면 피식 웃음이 나올 것이다. 이 프로그램이 하는 일이라고는 입력받은 수식을 그대로 PRINT하는 프로그램을 생성한 뒤, 그걸 실행하는 것이 전부이기 때문이다. 즉, 실질적인 계산을 하는 부분은 내가 짠 코드가 아니라 GWBASIC 인터프리터 자체인 것이다.
그나마 수식에 오류가 있어도 뻗지 않게 하려고 ON ERROR GOTO를 쓰는 일말의 치밀함(?)을 보였다.
사실, RUN은 지금 내 프로그램을 지우고 실행 제어를 다른 프로그램으로 완전히 옮기는 명령이다. MERGE나 CHAIN을 쓰면 내 프로그램의 컨텍스트를 유지하면서 타 프로그램을 그대로 병합을 할 수가 있으며 이게 내가 의도한 형태에 더 가깝다. 하지만 본인은 저 두 명령은 어떻게 사용하는지 잘 모른다.
MERGE 같은 경우 QuickBasic이나 QBasic으로 넘어가면서 후대의 베이직 언어들도 지원하지 않게 되었으며, 후대의 언어들도 차라리 문자열 코드를 실행하는 eval은 지원해도 저런 무지막지한 명령을 지원하지는 않는다. 먼 옛날에 컴퓨터와 함께 제공되었던 두툼한 MS-DOS 겸 GWBASIC 매뉴얼에서나 그런 명령에 대한 자세한 설명을 볼 수 있었던 걸로 기억한다.
자, 그럼 다시 자바스크립트 얘기로 돌아온다. 얘는 HTML, CSS와 더불어 웹을 구성하는 한 축이며, HTML 문서가 MS 오피스의 Word, Excel 같은 일반 문서라면 자바스크립트는 그런 문서에 첨부된 매크로나 마찬가지이다. 다른 모든 언어들은 사용하기 위해서 해당 언어 구현체들 런타임이나 엔진을 설치해야 하지만 자바스크립트는 웹브라우저만 있는 운영체제라면 바로 돌려볼 수 있다는 차이가 존재한다.
자바스크립트에는 딱히 컴파일· 링크 같은 건 없지만 난독화 겸 간소화(compaction)라는 리팩터링 후처리를 거쳐서 서버에 올라가곤 한다. 굳이 IOCCC 같은 대회를 노려서는 아니다. 핵심 알고리즘의 누출을 막고(분석을 완전히 막지 못하더라도, 어렵게 만들거나 지연시키기 위해) 크기도 줄이기 위해서이다.
난독화 내지 간소화를 자동으로 해 주는 도구가 당연히 존재한다. 모든 주석이나 공란은 제거되며 지역 변수는 전부 a~z, aa ... 처럼 식별만 가능하지 최대한 짤막하고 암호 같은 명칭으로 바뀐다. 상수 명칭의 조합으로 좀 더 알아보기 쉽게 숫자를 표현하던 것도 당연히 다 실제 숫자로 치환된다.
그런데 자바스크립트를 리팩터링해 주는 툴 중에는 그런 명칭 치환 수준을 넘어서 코드를 암호화에 가까운 완전히 다른 형태로 바꿔 버리는 것도 있다. 그런 덩어리를 보면 예전 코드는 다 이상한 문자열로 바뀌고 코드의 가장 바깥은 eval을 호출하는 형태가 돼 있다. 그 문자열을 원래의 자바스크립트 코드로 해독하는 건 다른 치환과 디코딩 함수들이고 말이다.
이건 개념적으로 실행 파일 압축과 동일한 기법이다. 거기도 압축을 푸는 데 필요한 최소한의 코드만 들어있고 나머지는 데이터를 압축을 풀어서 코드를 생성함으로써 실행되니까 말이다. 그게 기계어 코드가 아닌 인터프리터 스크립트형 언어에도 저런 식으로 존재할 수 있다는 게 신기하게 느껴진다. 자바스크립트의 JIT는 저런 것까지 다 감안해서 동작해야 할 테니 런타임이 안 그래도 거의 VM 수준의 스케일로 만들어져야 할 듯하다.
이념적으로 C/C++의 완전 정반대편에 있는 동적 언어를 익혀서 나쁠 건 없는 것 같다. JS는 그렇다 치더라도 파이썬은 슬라이싱 기능이 완전 마음에 든다. 늘 하는 생각이지만, 메모리 관리가 자동으로 되고 마음대로 new를 남발해도 되는 언어로 코딩을 하는 기분은 운전으로 치면 정말 클러치 걱정을 할 필요 없는 자동변속기 차를 모는 것과도 같아 보인다.
그리고 저런 동적 언어들은 아무래도 문자열 처리가 강세이며, 언어의 설계자가 확실히 정규 표현식 덕후라는 생각이 들었다.
메모리 관리를 할 필요가 없는 건 편하지만 C/C++의 포인터처럼 아주 저수준 직관적으로 문자열에 접근을 할 수 없는 건 좀 불편하다. 새로운 언어를 익히면 그 언어의 String 클래스가 제공하는 멤버 함수(메소드)부터 새로 익혀야 하니까.
또한 자바스크립트, 정규 표현식, URL, 그리고 HTML 내부에 제각각으로 돌아가는 탈출문자들 때문에 신물이 났다.
문자열을 검색· 치환하는 함수들이 다 있는 그대로 찾아 바꾸는 게 아니라 기본적으로 정규 표현식 기반이다. 역슬래시나 따옴표 같은 크리티컬한 문자 그 자체를 찾거나 그걸로 바꿔야 할 때 프로그램이 곧이곧대로 동작하지 않아서 그것도 좀 애로사항이었다.
Posted by 사무엘