지난 한 달간 커먼 Lisp(Common Lisp, 이하 CL)에 대해 많이 익혔는지? 이번 글부터는 KMP 시스템 개발 중에 사용했던 예를 중심으로 CL 프로그래밍을 해보자. 독자들은 CL 프로그래밍에 필수적인 개발 환경과 기본적인 CL 프로그래밍에 대해 익숙해졌으리라 가정한다.KMP 시스템의 웹 프레임워크간단히 KMP 시스템의 구성을 떠올려 보자. KMP 시스템은 IT 분야 지식 관리 및 매매 시스템이며, UI 클라이언트로 웹 브라우저를 사용한다. KMP 시스템은 사용자가 시스템에 접근하기 위해 필요한 웹 UI 층, 시스템의 로직 부분인 객체 층, 객체에 영속성을 제공하는 ORDB 층 등으로 구성되어 있다.KMP 시스템용으로 개발된 웹 프레임워크는 내부적으로 CLWF(Common LISP Web Framework)라고 부르는데, 이 글에서 소개할 세 번의 변경에 대해 차례로 CLWF1, CLWF2, CLWF3라고 부르기로 하자. CLWF1은 필요한 라이브러리 설치와 연결 등을 포함하여 두 사람이 2주 정도 걸려서 완성했다. 이후 개발자 셋이 4주 정도에 걸쳐 ORDB 층, 객체 층, CLWF1을 이용한 UI 층의 세 부분을 서로 연결하여 동작하는 상태를 유지하면서 개발했다.처음 CLWF1의 제약이 발견된 것은 총 개발 기간 중 약 1.5개월 정도 되었을 때였다. 발견된 제약을 제거하기 위해 하루 동안 두 사람이 수정하여 CLWF2를 얻었고, 하루를 더 소비하여 그동안 작성했던 UI 층을 다시 작성했다. 이후 1개월 정도 지난 다음에 CLWF2에서 큰 결점이 발견됐다. 이 때는 이미 많은 UI 층이 작성되어 있었기 때문에 수정을 하는 것에 대해 부정적인 생각이 많았다.그럼에도 불구하고 웹 프레임워크를 다시 한번 보완했는데, 재설계는 두 사람이 약 2시간 정도, 수정은 한 사람이 약 반나절 정도 걸려서 CLWF3를 완성했다. 그동안 작성했던 UI 층 수정은 두 사람이 약 1.5일 정도 걸렸으니, 결국 2일이 걸려서 KMP 시스템을 본래대로 작동시킬 수 있었다.짝 프로그래밍과 코드 리뷰CLWF에 대한 몇 차례의 수정이 필자로써는 꽤 만족스러운데 이런 빠른 변경은 모두 짝 프로그래밍과 코드 리뷰 덕분이다. 만일 필자 혼자만 했다면 결코 빠르게 원하는 코드를 만들어내지 못했으리라고 생각한다. 짝 프로그래밍을 하면 코드 리뷰의 효과가 있어서 혼자 하는 것보다 빠른 시간에 질 좋은 코드를 작성해 낼 수 있다.짝 프로그래밍이 불가능하다면 시간을 희생해서 1주 정도 후에 자신이 이미 작성한 코드를 다시 리뷰해 보는 것만으로도 더 나은 코드를 작성할 수 있다. 프로그래밍을 하다보면 문제 영역에 대해 점점 더 잘 알게 되기 때문에 언젠가 수정이 필요한 것은 필연적이다. 즉 최초에 알고 있던 문제 영역에 대한 지식을 모두 만족하는 프로그램을 작성했다 하더라도 시간 흐름에 따라 그것은 낡은 지식을 토대로 한, 현 시점에서는 틀린 프로그램이 되기 마련이다. 축적된 지식을 바탕으로 이전에 작성한 코드를 리뷰하다 보면 틀린 부분을 발견하거나 더 효과적인 코드가 떠오르는 것이 보통이다. 개발자로써 처음부터 문제 영역을 100% 이해하는 능력보다는 수정이 필요할 때 겁내지 않고 효과적으로 빠르게 기존 코드를 변경할 수 있는 능력이 더 필요하다. 때문에 필자는 도메인 지식을 더 많이 갖고 있는 개발자보다는 필연적인 변경을 효과적으로 처리할 줄 아는 개발자를 선호한다.개발자로써 코드 리뷰를 게을리 하지 말아야 하고, 가능하면 짝 프로그래밍을 시도해 볼 것을 권한다. 특정 프로그래밍 언어들을 사용할 때 변경을 두려워하는 개발자들이 적지 않다. 필자 역시 CL이 아닌 흔히 볼 수 있는 대중적인 프로그래밍 언어를 사용해야 할 때는 변경을 매우 두려워한다. LISP 류의 프로그래밍 언어를 사용하면 높은 수준에서 변경을 시도할 수 있기 때문에 두려움 없이 새로운 지식 축적에 따른 변경의 희열을 만끽할 수 있다.CL 웹 프로그래밍 준비KMP 시스템의 웹 인터페이스를 개발하기 위해서 하위 층부터 살펴보면서 필요한 프로그램이나 라이브러리를 파악해 보자. 웹 프로그래밍하기 위해서는 클라이언트인 웹 브라우저로부터 오는 요청(request)을 CL 시스템에서 사용할 수 있어야 한다. 이 부분은 아파치(Apache)+mod_lisp을 사용하여 클라이언트로부터의 요청이 아파치와 mod_lisp을 차례로 거쳐서 스트링 형태로 CL 시스템에 전달되도록 하였다. 이렇게 전달된 스트링 형태의 요청 파라미터들을 직접 처리하기 보다는 한 층을 더 추가하여 추상화함으로써 애플리케이션 개발자들이 문제를 해결하는 데만 집중할 수 있도록 TBNL을 사용했다. TBNL은 mod_lisp으로부터 받은 요청에 대한 제너릭(generic)한 핸들링 메커니즘과 멀티 프로세스 등을 제공하므로 웹 애플리케이션 개발자는 디스패처(dispatcher)만 작성하고 몇 개의 파라미터만 변경하면 된다. 또 동적 HTML 작성을 위해 CL-WHO, HTMLGEN 등 HTML을 CL에서 작성할 수 있도록 해주는 라이브러리를 사용했다. TBNL을 설치할 때는 TBNL이 의존하고 있는 부수적인 라이브러리들을 설치해야 하고, 아파치의 구성 파일을 수정해야 한다. 이제 CLWF1, CLWF2, CLWF3를 차례로 살펴보자.초보적인 웹 프레임워크, CLWF1탑다운 설계프로그래밍을 하기 위한 설계 방법에는 탑다운(Top-down)과 보텀업(Bottom-up) 방법이 있다. 최근 마소에 많이 연재됐던 TDD(Test Driven Development)의 경우 주로 보텀업 설계에 가까운데, 이는 세부적으로는 비교적 명확하고 큰 그림에 대해서는 추후 발전 가능성이 있을 때 효과적이다.이와는 반대로 탑다운 설계는 문제 영역에 대해서는 비교적 명확하지만 세부적으로 어떻게 구현해야 할지 잘 모를 때 주로 사용된다. 보텀업 설계를 할 것이냐 탑다운 설계를 할 것이냐는 주어진 문제, 개발자의 경험, 문제 영역의 특성 등에 따라 결정해야 한다. 대부분의 경우 설계는 이 두 가지 방법을 조화롭게 혼합하여 이루어진다. 즉 큰 그림을 보면서 적절한 추상화를 찾아내어 밑으로 내려가면서 생각을 정제해 나가고, 필요한 하위 층이 명확해지면 그 부분에 대해서 다시 위로 올라가면서 이미 존재하는 상위 층과 연결하는 식이다.필자가 속한 팀은 웹 기반 소프트웨어를 개발해 본 경험이 있어서 탑다운 설계를 선택해 상위 층에서부터 적절한 추상화를 유지하면서 하위 층에 대한 세부 구현은 뒤로 미루는 방법을 택했다. CL은 다른 프로그래밍 언어와는 달리 강력한 매크로를 지원하기 때문에 탑다운 설계시 추상화를 희생시키는 일 없이 개발자의 생각을 거의 그대로 기술할 수 있게 해준다. 오직 개발자의 상상력만이 한계점으로 작용한다.구체적으로 정확히 2주가 소요된 KMP 시스템 최초의 웹 프레임워크인 CLWF1을 살펴보자. CLWF1를 개발하기 전 이미 알고 있는 것들을 바탕으로 찾아낸 설계 요구사항은 다음과 같다.
◆ CLWF1은 동적 HTML을 지원한다.◆ 웹 사이트는 웹 페이지들의 집합으로, 웹 사이트와 각 웹 페이지들에 대한 정의로 이루어진다.◆ 웹 사이트는 특정 URL로 시작하는 웹 페이지들로 이루어진다.◆ 웹 페이지는 이름, 웹 페이지 이름, HTML을 만들어내는 데 사용할 제목, 헤더(header), 풋터(footer), 액세스 권한 등을 위한 보조적인 정보와 웹 브라우저로부터의 입력을 처리하는 사이드 이펙트 함수, 사이드 이펙트 결과를 바탕으로 화면에 HTML을 보여주는 뷰 함수(View function), 뷰 함수를 찾아내기 위한 이동 정보 등으로 이루어진다.
앞의 요구사항들은 다음과 같은 가상의 CL 코드로 대략 설계할 수 있다.
;; 사이트 정의 예(define-html-site kmp ; 사이트 이름은 kmp이다. ((:url-prefix "/kmp/"))) ; URL이 "/kmp/"로 시작되면 이 사이트에 대한 요청이다.;; 페이지 정의 예(define-html-page login (kmp) ; 페이지 이름은 login이며, kmp 사이트의 페이지이다. ((:title "Login") ; 페이지 제목, 스트링 혹은 스트링을 리턴하는 함수 (:header "login-header") ; 페이지 헤더, 스트링 혹은 스트링을 리턴하는 함수 (:footer "login-footer") ; 페이지 풋터, 스티링 혹은 스트링을 리턴하는 함수 (:credential user) ; 접근 권한 (:view-function login-view) ; HTML 페이지 생성 함수 (:action-function login-action) ; 사이드 이펙트 함수 (:transitions ((:success . front) (:fail . login))))) ; 이동 정보
앞의 정의들은 모두 객체를 만들어내기 위한 정의들로, 모든 정보들이 객체의 슬롯 - CLOS(Common LISP Object System)의 객체 지역 변수에 해당하는 것 - 에 초기 값을 지정하는 것들이다. 각 슬롯에 들어갈 함수나 스트링 등의 값들은 차차 살펴보기로 하자. 앞의 정의들이 객체가 되기 위해서는 클래스 정의가 필요하다. 마찬가지로 가상의 언어로 클래스들을 정의하자.
(define-class web-site () ;; 슬롯들의 정의 (url-prefix (html-pages :initform ())))(define-class html-page () ;; 슬롯들의 정의. action-function 슬롯은 초기 값으로 널 값인 NIL을 갖는다. (site-symbol name page-name title header footer credential view-function (action-function :initform nil) transitions))
CLOS를 이용한 OOPCLOS에 대한 많은 내용을 연재에서 다루기는 힘드므로 다른 프로그래밍 언어와 구별되는 부분에 집중하여 살펴보기로 하자. CLOS에 대해 이해하기 위해서는 지난 호에서 소개한 Sonya E. Keene의 「Object Oriented Programming in Common Lisp : A Programmer‘s Guide to CLOS」가 필독서이다. 앞에서 예로 든 web-site와 home-page 클래스 정의는 사실 CLOS의 defclass로, define-class는 키 입력을 줄이고 가독성을 높이기 위해 만든 매크로에 불과하다.
(defclass web-site () ((url-prefix :initarg :url-prefix :accessor url-prefix) (html-pages :initarg :html-pages :accessor html-pages :initform ())))
앞 정의를 글로 표현해 보자. 첫 번째 줄에서 web-site 클래스를 정의하는데 상속받는 수퍼 클래스는 없다. CLOS에서는 특정 클래스에 대해 상속 관계에 있는 상위 클래스들을 ‘수퍼 클래스’, 하위 클래스들을 서브 클래스라고 부른다.두 번째와 세 번째 줄에서 클래스의 슬롯은 url-prefix와 html-pages, 초기화에 사용할 키워드 이름은 각각 :url-prefix와 :html-pages, 액세서(accessor)는 url-prefix와 html-pages, 그리고 html-pages 슬롯은 디폴트로 ()로 초기화된다. 객체 속성을 나타낼 변수들은 ‘슬롯’이라고 부르며, 그것을 정의하는 폼을 ‘슬롯 정의’ 폼이라고 부른다.슬롯 정의 폼은 대개의 경우 슬롯 이름(예 - url-prefix), 초기화 키워드(예 - :initarg :url-prefix), 디폴트 값(예 - :initform ()), 액세서(예 - url-prefix) 등으로 이루어진다. 슬롯 값은 make-instance 함수를 통해 객체가 만들어지는 순간에 결정되거나 후에 라이터(writer)에 의해 결정된다. 객체 생성 이후 슬롯이 아무 값도 갖고 있지 않다면 ‘슬롯이 바운드되어 있지 않다’ 혹은 ‘슬롯이 언바운드다’라고 하는데, 만일 이런 슬롯을 참조하려고 하면 ‘슬롯 언바운드 에러’가 발생한다.
(make-instance 'web-site :url-prefix "/kmp/")
앞과 같은 make-instance 후에 얻는 객체의 url-prefix 슬롯과 html-pages 슬롯의 값들은 각각 “/kmp/”와 ()이다. 객체가 만들어지면 그 객체에 대한 슬롯들은 리더(reader), 라이터, 액세서 등을 통해 값을 읽어오거나 바꿀 수 있다. 액세서는 리더와 라이터를 함께 지칭하는 말이다. 다음의 두 슬롯 정의는 동일하다.
(html-pages :accessor html-pages)(html-pages :reader html-pages :writer (setf html-pages))
앞과 같이 정의하면 슬롯으로부터 값을 가져오는 리더 ‘html-pages’와 라이터 ‘(setf html-pages)’가 생긴다. 리더는 객체를 인자로 받으며, 라이터는 새로운 값과 객체를 인자로 받아서 객체의 슬롯을 새로운 값으로 바꾼다. 각각은 다음과 같이 메쏘드를 프로그래머가 정의하는 것과 같다.
(defmethod html-pages ((site WEB-SITE)) (slot-value site 'html-pages))(defmethod (setf html-pages (new-value (site WEB-SITE)) (setf (slot-value site 'html-pages) new-value))
함수의 인자 리스트는 ‘람다 리스트’라고 부르는데, 람다 리스트에 주의를 기울이며 앞의 정의를 살펴보기 바란다. 리더의 경우 web-site 클래스의 객체 site를 인자로 하고, 라이터의 경우에는 new-value와 web-site 클래스의 객체 site를 인자로 한다. 라이터의 경우 ‘setf'를 이용하여 정의할 때는 첫 번째 인자는 슬롯 값이 위치해야 한다.CLOS에서 슬롯에 접근하는 프로토콜은 메쏘드를 사용하는 것을 원칙으로 하지만, 앞의 defmethod 예처럼 프로그래머가 원한다면 프로토콜을 우회하여 언제든지 ‘slot-value’로 슬롯을 액세스할 수 있다. 다음의 코드를 참조해 슬롯에 대해 종합해 보자.
(let ((kmp-site (make-instance 'web-site))) (assert (not (slot-boundp kmp-site 'url-prefix))) (setf (url-prefix kmp-site) "/kmp/") (assert (string= (url-prefix kmp-site) "/kmp/")) (assert (null (html-pages kmp-site))) (setf (html-pages kmp-site) (list 'ex-page1 'ex-page2 'ex-page3)) (assert (= (length (html-pages kmp-site)) 3)))
CLWF1 디스패처상위 층에서 필요한 정의들이 대략 완성되었으므로, CLWF1의 최하위 층인 디스패처 부분으로 가서 어떻게 앞에서 정의한 정보들을 이용하여 웹 페이지들을 보여줄지 살펴보자.
(defun kmp-dispatcher (request) ;; request는 웹 서버로부터 받은 객체. 웹 브라우저로부터 받은 정보들을 갖고 있다. (let ((html-page (find-html-page-from-request request))) ; request로부터 페이지 객체를 찾는다. (cond ((and html-page (valid-credential? (session-value :current-user) html-page)) ;; 세션에서 얻은 사용자 객체가 접근할 수 있는 페이지이면 그 페이지를 처리하는 함수를 호출하는 ;; 람다 식(lambda expression)을 리턴한다. (lambda () (process-html-page html-page))) (html-page ;; 접근 권한이 없음을 알리는 함수를 호출하는 람다 식을 리턴한다. (lambda () (wrong-credential html-page (session-value :current-user)))) ;; 원하는 페이지가 없음을 알리는 함수를 호출하는 람다 식을 리턴한다. (t (lambda () (unknown-html-page html-page))))))
디스패처는 함수처럼 호출 가능한 람다 식을 리턴하는데, 이보다 더 하위 층인 TBNL에서는 해당 람다 식을 호출하여 HTML 스트링을 얻은 후 클라이언트인 웹 브라우저에 그 HTML 스트링을 보내는 일을 한다. 디스패처의 예를 통해 CL의 객체들에 대해 살펴보자. CLHS를 살펴보면 lambda 폼의 lambda는 매크로로, 결국 함수 형태인 #‘(lambda () ...)로 치환된다. 디스패처에서 호출하는 process-html-page는 실제 HTML을 리턴하는 함수이다. 그밖에 사용되고 있는 함수들은 잠시 무시하기로 하자.
(defun process-html-page (html-page) (handler-case (let* ((action (action-function html-page)) (next-html-page (find-next-html-page html-page (if action (funcall action) nil)))) (funcall (view-function next-html-page))) ;; FIXME: 나중에 더 자세한 에러 핸들링 필요 (condition (cond) (format nil "kmp web-command-processor error: ~A" cond))))
앞의 함수는 html-page 객체를 받아서(첫째 줄), 사이드 이펙트를 일으킬 함수를 지역 변수 action에 바인딩한다(둘째 줄). 보여줄 HTML 페이지 객체는 find-next-html-page 함수로부터 얻어서 next-html-page에 바인딩하는데(셋째 줄), action이 값이 있으면 함수 호출한 결과를 find-next-html-page의 인자로 주고(다섯째 줄), 아니면 NIL을 인자로 준다(여섯째 줄). 이렇게 얻은 next-html-page 객체로부터 HTML을 보여주는 함수를 얻어내서 호출 결과를 리턴한다(일곱째 줄). 이 모든 과정은 handler-case에 둘러쌓여 있다.CLHS를 참고하면 알겠지만, handler-case는 첫 번째 오는 폼을 실행하다가 에러가 발생하면 그 이후 오는 핸들러 폼에 따라 에러를 처리한다. 마지막 줄에서 보는 바와 같이 예에서는 ‘condition’ 클래스의 객체 ‘cond’에 대해서만 에러 처리하는데, 단순히 에러가 발생했다는 스트링을 리턴한다. CLHS를 참고하면 condition, error, signal 등의 에러 관련 클래스들의 상속 관계를 알 수 있을 것이다.브라우저의 요청으로부터 다음 페이지를 찾아내는 부분은 TBNL에 의존성이 있는 하위 층이다. 다음 함수 정의에서 ‘script-name’이 바로 TBNL의 request 객체로부터 얻어내야 할 정보이다. 비록 하는 일은 적지만, 하위 층과의 의존성을 명확히 알리고 추후 수정시 문제 발생을 최소화하기 위해 별도의 함수로 빼냈다.
(defun find-html-page-from-request (request) (get-matching-html-page (script-name request)))
필요한 정보인 스크립트 이름을 얻었다면, 그것을 키로 해서 CLWF의 웹 페이지 객체를 찾아내야 한다.
(defun get-matching-html-page (script-name) "Return page-object with given SCRIPT-NAME (url-prefix + page-name e.g: '/kmp/login.html')" (let* ((last-/ (position #/ script-name :test 'char= :from-end t)) (prefix (subseq script-name 0 (1+ last-/)))) (find-html-page (subseq script-name (1+ last-/)) (find-html-site prefix))))
앞 함수는 position 함수를 이용하여 script-name의 뒤부터 ‘/’의 위치를 찾은 후 함수 subseq로 script-name의 처음부터 찾은 ‘/’까지를 prefix에 바인딩한다. 예를 들면, ‘/kmp/login.html’로부터 ‘/kmp/’를 잘라내어 prefix로 바인딩한다. 마지막으로 find-html-page는 웹 페이지 스트링(예를 들면, ‘login.html’)과 find-html-site 호출 후 얻은 사이트 객체를 인자로 호출하여 웹 페이지 객체를 리턴한다.사이트 객체를 찾아내는 함수 find-html-site는 해시 테이블에서 객체를 찾아내며, find-html-page 함수는 이렇게 얻은 사이트 객체로부터 ‘find’ 함수를 이용하여 해당 웹 페이지 객체를 찾아낸다.
(defun find-html-site (site-key) "Return site object with given SITE-KEY which can be a symbol(site name) or string(url-prefix)" (gethash site-key *html-site-table*))(defun find-html-page (page-name site) "Return page-object with given PAGE-NAME(string) from its owner(SITE)" (find page-name (html-pages site) :key 'page-name :test 'string=))
CL 객체CL에서 객체(object)라고 하면 앞에서 살펴본 CLOS의 객체 외에 또 다른 의미를 갖고 있다. CL의 모든 데이터들은 다른 프로그래밍 언어와 달리 자신이 어떤 종류의 데이터인지 알 수 있는 정보도 메모리에 보관한다. 즉 메모리에 존재하는 모든 데이터들은 자신이 어떤 타입인지 알고 있으며, 메모리에 존재하는 각각의 요소들을 객체라고 부른다. CL에서 변수는 단지 객체를 가리키는 포인터나 마찬가지이며, 객체 스스로 타입 정보를 갖고 있기 때문에 변수에 대한 타입 정의가 꼭 필요한 것은 아니다. 함수 역시 데이터로 사용될 수 있으며, 함수를 정의하면 메모리 어디엔가 함수 객체가 만들어진다. 앞의 디스패처 예에서 리턴 값은 람다 식이였는데, 이것은 함수를 데이터로써 주고받을 때 사용하는 한 방법이다.람다 식을 이용하면 “funcall”, “apply” 등을 이용하여 호출할 수 있는 함수를 만들어낼 수 있다. CL에서는 함수 객체에 대해서도 다른 객체들처럼 데이터로 취급하여 필요한 연산을 할 수 있으며, 이러한 특징을 ‘함수는 1급 객체(first class object)’라고 한다. CL에서 객체라고 하면 ‘CLOS 객체’를 칭하는 것인지 아니면 LISP의 객체를 칭하는 것인지 문맥에 맞게 잘 해석해야 한다.CLWF1의 가정으로는 여러 개의 사이트 정의가 있을 수 있다. 즉 브라우저로부터 얻어온 URL 스트링을 통해 사이트 객체를 찾고 그 사이트 객체의 웹 페이지 객체들을 갖고 HTML을 만들어 낼 것이다. 각 사이트 객체 저장소로 해시 테이블(hash table)을 이용하자.
(defvar *html-site-table* (make-hash-table :test 'equal) "사이트 심볼과 객체를 위한 해시 테이블“)
앞의 해시 테이블에서 심볼 혹은 스트링에 대해 비교 가능한 함수 “equal”이 사용됐다. CL에는 이밖에도 eq, eql, equalp 등 많은 비교 함수가 있다. CLHS를 통해 서로 무엇이 다른지 익혀두고, make-hash-table과 같이 :test 키워드를 이용하여 비교 함수를 받아들일 수 있는 함수들이 디폴트로 어떤 비교 함수를 사용하는지 찾아보기 바란다. 간단히 설명하면 eq, eql, equal, equalp 순으로 비교 정도를 완화시켜간다. SLIME의 리스너에서 여러 가지 테스트를 해보기 바란다.한편 데이터로써 함수를 표시할 때는 심볼을 이용한 ‘equal 뿐 아니라, #‘equal 등과 같이 할 수도 있다. 앞의 예에서는 심볼을 사용했는데, 둘 사이의 차이점은 #’equal는 함수 객체 자체이고, 심볼은 이름으로 함수를 지칭하는 것이다. 필자의 경우 CL 라이브러리 함수들은 #‘ 스타일을, 그렇지 않은 함수들은 심볼 스타일을 선호한다. 그 이유는 CL 시스템을 다시 시작하지 않고 함수만을 재정의했을 때 얻는 효과가 다를 수 있기 때문이다. 다음 코드 예를 실행해 보고, example-test를 다르게 재정의해 보면서 왜 그런지 생각해 보기 바란다.
(defun example-test (string char) (char= (aref string 0) char))(defun start-with-vowel1? (string) (member string '(#a #e #i #o #u) :test #'example-test))(defun start-with-vowel2? (string) (member string '(#a #e #i #o #u) :test 'example-test))
CL 매크로앞에서 가상 언어를 이용해 기술한 정의들에 숨을 불어넣어 실제 코드가 되게 할 차례이다. 이를 위해서는 CL의 매크로를 이해하고 사용할 줄 알아야 한다. CL의 매크로는 코드 변형(transformation)으로, 매크로가 익스팬드(expand)될 때 폼의 일부를 평가하여 결과를 반영하는 코드 생성이라고 할 수 있다. 비교적 간단한 define-class는 입력을 줄이기 위해 만든 매크로인데, CLOS의 defclass를 이용하도록 매크로 익스팬드(macroexpand)되며 그 정의는 다음과 같다.
(defmacro define-class (class-name superclasses slots &rest slot-options) "DEFCLASS generation macro. Save keystrokes when slot name is same with initarg and accessors." (let ((slot-definition-list (mapcar #'make-slot-definition slots))) `(defclass ,class-name ,superclasses ,slot-definition-list ,@slot-options)))
앞의 정의를 한 줄씩 살펴보자. 첫째 줄에서 볼 수 있듯이 define-class의 인자는 defclass의 인자와 동일하다. 즉 클래스 이름(class-name), 수퍼 클래스들(superclasses), 슬롯들(slots) 그리고 슬롯 옵션들(slot-options)로 이루어진다. &rest 이후에 인자로 받는 모든 것들은 리스트 형태로 slot-options에 바인딩된다. 둘째 줄은 도큐먼트, 즉 매크로에 대한 설명이다. 셋째 줄은 실제 슬롯 정의들을 만들어낸 후 변수 slot-definition-list에 결과를 바인딩한다.백쿼트(back-quote)로 시작하는 마지막 줄에서 비로소 defclass를 이용한다. 백쿼트는 매크로에서 흔히 사용하며, 이 예에서의 의미는 “defclass 폼을 만드는데 class-name, superclasses, slot-definition-list, slot-options는 익스팬드할 때(expand time) 평가하여 그 결과를 이용하라”는 것이다. 백쿼트 이후 평가를 원하는 곳에는 콤마(,)를 이용하며, ,@은 리스트인 경우 평가 결과에서 리스트를 없애라는 것이다. 마지막 줄은 다음과 동등(equivalent)하다.
(list* 'defclass class-name superclasses slot-definition-list slot-options)
define-class에서 mapcar를 이용해 슬롯 정의를 만드는 함수를 호출하는데, mapcar는 리스트의 각 원소들에 대해 어떤 함수를 적용하고 그 결과들을 모은 리스트를 리턴한다. 다음의 간단한 예를 테스트해 보기 바란다.
(mapcar #'1+ '(1 2 3 4 5)) => (2 3 4 5 6)
슬롯 정의를 만드는 함수 make-slot-definition은 define-class에서 받은 슬롯들 각각에 대해 defclass에서 사용할 수 있는 슬롯 정의를 리턴하는 함수이다.
(defun make-slot-definition (slot-def) "Generate slot definitions from SLOT-DEF." (let ((key-package (find-package :keyword))) (if (atom slot-def) (list slot-def :initarg (intern (string-upcase slot-def) key-package) :accessor slot-def) (let ((slot-name (first slot-def))) `(,slot-name ,@(cdr slot-def) :initarg ,(intern (string-upcase slot-name) key-package) :accessor ,slot-name)))))
이 함수는 “:”로 시작하는 키워드를 만들기 위해 intern을 호출한다. 함수 intern은 스트링과 네임스페이스인 패키지(이 경우 keyword 패키지)를 인자로 해서 그 패키지에 새로운 심볼 혹은 이미 존재하는 심볼을 리턴한다. 심볼을 만들 경우 대소문자가 구분되므로 string-upcase를 이용했다.만일 slot-def가 아톰이면 슬롯 이름과 동일한 초기화 키워드와 액세서를 갖는 슬롯 정의를 만들어서 리턴하고, 리스트이면 그 슬롯 정의에 초기화 키워드와 액세서를 추가한 후 결과를 리턴한다. 사이트와 페이지 정의는(define-html-site와 define-html-page) 이보다 더 복잡하다.
(defun make-key-value-list (key-value-list) (mapcan #'(lambda (key-val) (let ((key (first key-val)) (val (second key-val))) (list key (cond ((or (symbolp val) (listp val)) (quote val) (t val))))) key-value-list))(defmacro define-html-site (name args) (let ((gsite-obj (gensym "site")) (gsite-old-obj (gensym "old-site"))) `(let ((,gsite-obj (make-instance 'web-site ,@(make-key-value-list args))) (,gsite-old-obj (find-html-site ',name))) ;; 이미 존재하는 객체는 새로운 객체로 대치한다. (when ,gsite-old-obj (setf (html-pages ,gsite-obj) (html-pages ,gsite-old-obj)) (remhash (url-prefix ,gsite-old-obj) *html-site-table*)) ;; 해시 키를 두 번 - 한번은 심볼에 대해 한번은 url-prefix 스트링에 대해 - 셋팅한다. (setf (gethash ',name *html-site-table*) ,gsite-obj) (setf (gethash (url-prefix ,gsite-obj) *html-site-table*) ,gsite-obj))))
make-key-value-list 함수는 키워드와 값들의 리스트를 받아서 값이 심볼인 경우 쿼트를 추가하여 매크로 define-html-site에서 web-site 객체를 만들 때 필요한 인자들을 계산해 준다. 매크로 define-html-site는 먼저 객체를 만들고, 이전에 존재했던 같은 이름의 객체를 찾아서 있으면 새로운 객체의 슬롯 값들을 예전 객체 슬롯 값들로 대치한다. 또 전역 변수(실제로는 스페셜 혹은 다이나믹 변수) *html-site-table*에 객체를 등록한다. CLWF1의 첫머리에 소개된 KMP 사이트는 다음과 같이 매크로익스팬드될 것이다.