ASDF으로 간단한 의존성 연결과 로딩하기

Posted on Nov 3, 2018

System? 이런게 왜 필요하지? require있잖아

대부분의 유명한 프로그래밍언어들이 커먼리습의 System 에 대응하는 기능이 명확하게 없으니까, 다른 프로그래밍 하거나 하는 방법으로 이야기를 하지는 않아야겠다.1

https://lispcookbook.github.io/cl-cookbook/systems.html 에서 마음에 드는 설명을 찾을수 있다.

A system is a collection of Lisp files that together constitute an application or a library, and that should therefore be managed as a whole. A system definition describes which source files make up the system, what the dependencies among them are, and the order they should be compiled and loaded in.

즉,

  1. 커먼리습의 네임스페이스, Packages 은 그냥 심볼들을 묶는 데이터타입이고, 함수나 변수 같은 심볼들이 특정한 타입에 따라 정리되어있을뿐이다.2
    1. 다른 말로, 어떤 패키지 X은 한개의 소스파일에 대응하지 않을수도 있고, 심지어 한개의 소스파일 xyz.lispX, Y, Z 3개의 패키지를 선언할수도 있는거니까. (역으로 하나의 패키지를 여러개의 소스파일으로 구성도 할수있고.)
  2. 커먼리습은 특성상 현재 로드된 함수 등의 원래 소스코드와 직접적인 연관을 갖지 않는다.3
  3. 결국 다른 언어들처럼 단순하게 패키지 이름등으로 import, require한다고 그걸 적절한 매칭하는 파일/경로이름으로 변환하는 과정이 없고4, 그렇게 하기도 난감하다.5

그런데 다음과 같이 의아할 수 있겠다.

  1. 그러면 CLHS에 명시된 *modules*, require, provide은 뭔가?
  2. load은 그렇다치고, 컴파일은 그냥 Makefile 쓰면 안되나?
  3. 커먼리습의 패키지(Packages) 이랑은 다른건가?

A-1. *modules*, require, provide

결론부터 말하면, 이젠 *modules*, require, provide은 CLHS에는 있지만 이젠 쓸일이 없다.6

*modules*, require은 현대에는 그냥 ASDF인것처럼 동작한다. 하지만 편의를 위해 제공하는것뿐이지 딱히 정확하지도 않고, 계속해서 이것들을 사용할 이유도, 그래서도 안될거 같다.

이런 기능들 자체가 커먼리습에 시스템 이란 개념이 제대로 정립되기 이전에 나온것들일거고, 굳이 사용할 이유가 없어진거 같아서 그런거 같다.

그렇지만, 여전히 load이나 *feature* 같은 System Construction 관련 항목들은 여전히 유용하다.

A-2. load을 그대로 써도 된다만… Makefile만은…

위의 패키지 구성하기 에 따라서 제대로 구성하려면,

load을 이용해서 패키지를 선언하는 소스파일부터 패키지의 내용을 채워넣는 파일들을 의존관계의 순서에 따라서 하나씩 로딩하는 스크립트를 짜서 제대로 로딩하기만 하면 된다.

그런데, 이걸 단순히 내 손으로 하기도 어렵겠고(귀찮고), 어떤 시스템 하나를 로딩할 때, 그것에 의존하는 또 다른 시스템에 대해서도 이런 로딩 절차를 선행해주기는 끔찍할거 같다.7

A-3. 패키지랑은 다른것.

이미 위에 이야기를 했지만, 시스템은 패키지들의 묶음로 생각할 수 있다.

일반적으로, zxc이란 시스템을 만든다면, 아마 zxc이란 패키지는 적어도 하나 있을거고, zxc의 하위 패키지들도 이 시스템에 넣을것 같다.

그래서, asdf 같은 것들이 생기고 필요해짐.

뭐, 위와 같은 이유들로 시스템이란 개념을 만들어서 프로젝트 단위의 빌드나 의존성들을 커먼리습에서는 관리한다.8

이 포스팅에서는 ASDF 에 대해서만 이야기한다.

하지만, 어차피 소스코드를 조직하고 빌드하고 이미지로 로딩하는 방식이고, 표준에서 정한것이 아니고 커뮤니티의 de facto인거니, 다른 대안들도 있다.


ASDF? Quicklisp?

ASDF은 시스템을 defsystem 정의하고, 이렇게 작성한 .asd 파일을 찾고 load, compile, test등의 작업을 적용할 수 있게 해주는 operation들을 적용해주는 부분이 있다.

그리고 조금 헷갈리지만, 현대에는 오픈소스 리습 라이브러리를 다운로드까지 받아 한번에 로딩할 수 있게 해주는 Quicklisp도 있는데, ASDF와 유사해보이지만9 엄밀하게는 ASDF을 지원해 이런 기능을 구현한것이고 아예 별개이므로 혼동하지말자.

그리고 이 글에서는 Quicklisp을 이용하는 방법이 아니라, ASDF으로 시스템을 구성하고 로딩하고 하는 기초를 이야기하려고 한다.

그리고 ASDF에 대한 더 좋은 문서로 https://lisp-lang.org/learn/writing-libraries 을 읽어보기도 권한다.


간단한 프로젝트 만들어서 준비

프로젝트를 처음 만드는 방법은 몇가지 있지만, 나는 선호하는 툴인 quickproject을 Quicklisp을 통해 로딩해서 프로젝트를 생성한다.10

CL-USER> (ql:quickload :quickproject)
CL-USER> (quickproject:make-project #P"~/P/cl-adder")

위와 같이 실행하면, ~/P/cl-adder 디렉토리에 cl-adder 이란 이름으로 새로운 빈 프로젝트를 만든다.

대강의 프로젝트가 생성되면 다음과 같은 형태를 갖는다.11

https://github.com/ageldama/cl-adder

기본 규칙들

생성한 골격을 설명하면:

  1. cl-adder.asd
    • defsystem으로 내가 새로 만든 프로젝트의 시스템을 선언하고있다.
    • 다른 시스템에 대한 의존성을 지정할수도 있는데, 여기엔 의존할게 없으니 없다.
    • 더 중요한건 로딩할 리습 소스코드 파일들의 목록을 :components 으로 나열하고 있다.12
  2. package.lisp
    • defpackage으로 시스템에서 정의하는 패키지들을 선언.
    • (defpackage와 커먼리습의 Packages 대해서는 http://clhs.lisp.se/Body/11_.htm 이나 커먼리습 서적들을 참고하면 좋을거 같다.)
    • 각 패키지별로 public으로 노출할 심볼들이나 패키지 내에서 참조할 다른 패키지를 나열.
    • 시스템에서 정의할 패키지들을 defpackage으로 계속 나열해나가기.
  3. cl-adder.lisp
    • 실제 변수나 함수 같은 패키지에 속할 심볼들, 그리고 내부 구현들을 여기에 코딩.
    • 패키지 선언은 이미 package.lisp에서 다 된걸로 생각하고, 여기서는 어떤 패키지에 속할 심볼인지에 따라서, in-package을 이용해서, 그 이후에 따라오는 소스코드 라인에서 정의하는 defvar, defun등은 해당 패키지에 속하도록.
    • 다른말로, 여러개의 패키지가 있다면 in-package을 번갈아가며 지정해서, 하나의 .lisp 소스코드에서 여러 패키지에 속하는 내용들을 다 정의할 수 있다.

사용하기

위 소스에서는 cl-adder:add이란 함수를 구현하고 export한다.

이젠 이 시스템을 의존성으로 사용하는 다른 시스템을 만들어보겠다.13

다른 중간 과정을 모두 설명을 생략하고, 참조하는 한 라인만이 중요한거 같다. https://github.com/ageldama/cl-use-adder/blob/master/cl-use-adder.asd

여기에서,

(asdf:defsystem #:cl-use-adder
  ...
  :depends-on ("cl-adder")
  ...

depends-on으로 참조하는 시스템의 이름을 적어준게 전부다.

나머지는, cl-use-adder 시스템의 package.lisp에서 (defpackage ... :use)을 이용해서 패키지의 심볼들을 import할수있겠지만, 이건 그냥 코드에서 cl-adder:add으로 참조하느냐 add으로 참조하느냐의 차이만을 만들고, 실제 의존관계를 표현하는것은 아니다.


그런데, 이걸 어떻게 로딩한다고?

이렇게 2개의 cl-adder, cl-use-adder 시스템을 만들고 의존관계를 만들어봤다. 그런데, 이 2개의 시스템을 커먼리습 이미지에 어떻게 불러오는지 설명하지않았다.

어쩌면 누군가는 시도를 해보고 실패하고 짜증이났을지도 모르겠다. 미안하다

예를 들어, 다음을 실행하면 그냥 실패할것이다.

(asdf:load-system :cl-use-adder) 혹은 (asdf:load-system :cl-adder)

가장 간단한 방법은 symbolic link을 이용하는것이다.

다음을 실행해보자.

  1. mkdir ~/common-lisp
  2. ln -s $SOME_PATH/cl-adder.asd ~/common-lisp
  3. ln -s $SOME_ANOTHER_PATH/cl-use-adder.asd ~/common-lisp

그리고 실행하던 커먼리습 이미지를 (quit)하거나 새로 시작해서,

(asdf:load-system cl-use-adder)을 실행하면, 의존성인 cl-adder을 포함해서 로딩할것이다.

더 자세한 내용들은 https://common-lisp.net/project/asdf/asdf.html#Controlling-where-ASDF-searches-for-systems 여기에서 설명하고 있으며, 설정 파일 등을 이용하여 하위 디렉토리를 검색하여 자동으로 .asd 파일을 검색하도록 하는 방법도 있다.

그냥 legacy, asdf:*central-registry* 쓰기

여기서 설명하는건 예전의 방식이고, 현재의 ASDF3은 위에 설명한 설정파일에 명시하는걸 권장합니다.

단순하기는한데, 가능하면 위에 설명한 심볼릭링크나 설정파일을 이용하는걸 권장한다. ASDF2 버젼에서도 이미 앞으로 deprecated할 방법이라고 설명했었던거 같다. 가능하면 사용하지말자.

하지만, 몇몇 편의를 위해서 이렇게 사용하는 경우도 있긴한데, 나쁜짓인거 같긴하다.

마무리 생각

ASDF은 단순히 로딩만 하는데 사용하는것이 아니라, 다음과 같은 일들을 하는데 사용하는게 보통인거 같다.

  1. 시스템의 테스트suites을 실행하기
  2. 시스템의 컴파일
    • 뭐 로딩하려면 필요하기도 하고.
  3. 시스템을 로딩한 이미지/실행파일의 생성14

그리고 커먼리습 오픈소스 커뮤니티의 나름대로 표준이기 때문에 잘 이해하고 사용하는게 좋을거 같다.

또한, 다른 커먼리습의 부분들과 마찬가지로 ASDF자체도 단순하게 커먼리습 이미지에서 실행하는 함수들일뿐이고 모든 데이터에 접근할수있다. 예를 들어, 다음과 같이 로딩된 시스템의 정보를 Inspect해볼수있다.

(inspect (asdf:find-system :asdf))15

그리고 이런 시스템 모델 이외에, 내 프로젝트의 특성에 따라서 더 적절한 방식으로 .asd 파일을 찾아서 로딩하게 만들수도 있다.

asdf:*system-definition-search-functions* 리스트에 string -> pathname-OR-nil이 타입인 함수를 만들어서 추가하면, 내가 원하는곳에서 주어진 이름의 시스템을 로딩하도록 할수있을것 같다.

다른 커먼리습과 마찬가지로 뭔가 내 마음대로 할수있는 유연한 도구들, 툴킷을 제공하는 느낌의 프레임웍인것 같다. 그러면서도 세세하고, 매뉴얼을 읽고 이해하는 재미와 이점도 충분히 있는.



  1. 사실 Java 9의 Jigsaw 같은 개념이 비슷하다고 할수도 있겠지만, 솔직히 둘은 다른거 같다. 그리고 파이썬, 루비, 자바스크립트 등등에서 수많은 프로그래밍 언어들이 이야기하는 패키지 가 이에 해당하는거지만, 커먼리습의 시스템 와의 가장 큰 차이는 명확하게 실행시간에 first-class 객체로 시스템 자체에 접근하고 설명할 수 있는점이 커먼리습과 나머지와의 차이인거 같다. 물론 나머지도 확장 패키지를 불러서 그렇게 접근이 가능하긴하다만 커먼리습은 자체의 특성 때문에 이렇게 될수밖에 없었겠지. ↩︎

  2. 다른 패키지를 상속해 사용하거나, 해당 패키지 내부에서만 참조가 가능한 심볼이나, 외부에서 사용할 수 있도록 노출된 심볼이라거나 하는 등으로. ↩︎

  3. 물론 실제로는 (커먼리습 구현체에 따라 다르겠지만) 함수나 심볼에 대해서, 원래의 소스코드 pathname을 property등으로 설정해놓고 디버깅이나 소스코드 navigation을 위해 제공하지만. ↩︎

  4. 우리가 흔히 알고 있는 파이썬의 site-packages 같은 규칙이나(https://docs.python.org/3/reference/import.html, https://nodejs.org/api/modules.html), 자바의 기본 클래스로더의 클래스파일 이름, 경로명 등으로 적절히 로딩하는 방식 같이 동작하지 않는다. ASDF 같은것 없이 기본적으로는. ↩︎

  5. 왜냐하면, 파일을 로딩하는것뿐이고, 실제로 모든 커먼리습 이미지 안에서의 의존은 이미지 안에 이미 존재하는 심볼들이라는 가정이니까. 굳이 복잡하게 참조시에 뭐가 일어나고 하는걸 생각하지 않는다. https://llvm.org/docs/LinkTimeOptimization.html 같은게 구현되기 어려운 이유일거 같긴하다. ↩︎

  6. 혹여 다른 언어나 Scheme 등에서 사용하던 관성과 친숙함으로 이들을 사용하고 싶다고 하더라도 정말 말리고 싶다. 그냥 asdf 을 쓰시라. ↩︎

  7. 왜냐하면, 내가 작성했거나 조금 이해하기 좋은 시스템이 의존이라면 로딩 순서를 지키기 좋겠지만, 그냥 내가 잘 모르고 소스코드가 수십개 거나 그런 의존성이 한두개가 아닌 상황이 되면 결국 몇백개의 파일들을 손으로 그 순서를 지정해줘야할테니까. 현대에 조금 많은 의존성을 가질 웹프레임웍이나 그런 시스템을 이렇게 로딩하는걸 상상해보자. ↩︎

  8. 그리고 이런 시스템들을 로딩한 “내 이미지“와 “World“이란 더 큰 단위로 부르기도함. ↩︎

  9. https://www.cliki.net/asdf-install 예전에는 Quicklisp와 유사한 이런 ASDF extension이 있었지만 지금은 별로 효용이 없다. ↩︎

  10. Quicklisp을 세팅해놓는게 필요. 자세한 방법은 https://www.quicklisp.org/ ↩︎

  11. cl-adder.lisp, package.lisp을 제외하고는, 그것도 1~2줄을 제외하고 처음 생성한 기본 그대로 커밋해놓았다. ↩︎

  12. 그리고 :serial t이 지정되어 있으므로, 파일들은 나열한 순서대로 로딩될것이다. https://common-lisp.net/project/asdf/asdf.html#Serial-dependencies ↩︎

  13. 테스팅을 추가하고 싶다면, cl-adder 시스템에 cl-adder.tests 같은 테스팅 시스템을 추가해서, 커먼리습용 테스팅 프레임웍을 이용해 테스트를 작성하기는 하겠지만 여기서는 생략했다. ↩︎

  14. 실행파일을 만들거나 이미지를 저장하는건 커먼리습 컴파일러마다 방식이 다르고, 이런걸 정리해주는 다양한 도구들이 있는데, 내게 편안한건 그냥 ASDF을 이용하는것 같다. ↩︎

  15. SLIME이나 Sly같은 IDE을 사용한다면, (asdf:find-system :asdf)한 다음에 C-c I을 입력해서 Interactive Inspector을 사용할 수 있다. ↩︎