User Tools

Site Tools


lecture:nrepl:docs

nREPL

최신 클로저용 REPL로 활용되고 있는 nREPL입니다. 소스 분석으로서의 가치는 소켓 통신과 프로토콜 구현, 클로저의 평가 과정에 이해입니다.

원격으로 클로저 코드를 평가할 필요가 있는 툴이나 IDE를 위한 공통 API와, REPL 서버와 클라이언트를 제공하는 클로저 network REPL이다.

설치


nREPL은 메이븐으로 되어 있다. Leiningen 프로젝트의 project.clj 파일에 아래의 :dependency를 추가하라.

[org.clojure/tools.nrepl "0.2.1"]

혹은 메이븐 프로젝트의 pom.xml 파일에 다음을 추가하라.

<dependency>
  <groupId>org.clojure</groupId>
  <artifactId>tools.nrepl</artifactId>
  <version>0.2.1</version>
</dependency>

nREPL은 클로저 1.2.0 - 1.5.0에 호환된다.

nREPL 클라이언트


nREPL 서버에 접속하는 nREPL 클라이언트가 필요할 것이다. 아래는 nREPL 클라이언트를 지원하는 툴이다.

대개의 클로저 개발 환경은 nREPL을 지원하기 때문에 딱히 프로그래머가 할 일은 없다. 그것을 이용하거나 nREPL endpoint에 직접 접속해야 할 것이다.

nREPL endpoint에 프로그램적으로 말하기


기본 트랜스포트로 nREPL서버에 접속하려면 다음과 같이 한다.

=> (require '[clojure.tools.nrepl :as repl])
nil
=> (with-open [conn (repl/connect :port 59258)]
     (-> (repl/client conn 1000)    ; message receive timeout required
       (repl/message {:op "eval" :code "(+ 2 3)"})
       repl/response-values))
[5]

response-values는 read함수를 통해 그들의 pr 인코드된 재현으로부터 읽어들인 후, 평가된 문구의 값만을 리턴한다. 응답 메세지를 다 찍어 볼 수 있다.

=> (with-open [conn (repl/connect :port 59258)]
     (-> (repl/client conn 1000)
       (repl/message {:op :eval :code "(time (reduce + (range 1e6)))"})
       doall      ;; `message` and `client-session` all return lazy seqs
       pprint))
nil
({:out "\"Elapsed time: 68.032 msecs\"\n",
  :session "2ba81681-5093-4262-81c5-edddad573201",
  :id "3124d886-7a5d-4c1e-9fc3-2946b1b3cfaa"}
 {:ns "user",
  :value "499999500000",
  :session "2ba81681-5093-4262-81c5-edddad573201",
  :id "3124d886-7a5d-4c1e-9fc3-2946b1b3cfaa"}
 {:status ["done"],
  :session "2ba81681-5093-4262-81c5-edddad573201",
  :id "3124d886-7a5d-4c1e-9fc3-2946b1b3cfaa"})

각 메시지는 수행될 연산의 종률를 특정하는 적어도 하나의 :op 혹은 (“op”) slot을 담고 있다. nREPL endpoint에 의해 지원되는 연산들은 그 endpoint가 시작될 때 사용되는 핸들러와 미들웨어 스택에 의해 결정된다. 기본 미들웨어 스택은 특정 연산을 제공한다. 자세한 사항은 다음 링크 참조.

nREPL 포함하기, 서버 시작하기


Leiningen으로 프로젝트를 한다면, lein repl(만약 Reply 터미널 기반 클라이언트가 필요없다면 lein repl :headless)로 서버에 접근할 수 있다.

그렇지 않다면 당신의 어플리케이션에 nREPL 서버를 포함하는 것이 아주 유용할 것이다. 그러면 디버깅, 정상 동작 확인, 코드 패칭 등이 아주 쉬워진다.

nREPL은 소켓 기반 서버를 제공하기 때문에 당신의 어플리케이션에서 간단하게 구동시킬 수 있다. 다음과 같은 코드를 삽입하라.

=> (use '[clojure.tools.nrepl.server :only (start-server stop-server)])
nil
=> (defonce server (start-server :port 7888))
#'user/server
=> (with-open [conn (repl/connect :port 7888)]
     (-> (repl/client conn 1000)
       (repl/message {:op :eval :code "(+ 1 1)"})
       repl/response-values))
[2]

서버를 멈추려면 (stop-server server)하면 된다.

서버 옵션

nREPL은 기본 메시징 프로토콜이나 기본적인 소켓 사용에 제한되어 있지 않다. nREPL은 전송 추상을 제공하기 때문에, 다른 프로토콜이나 접속 방식을 구현할 수 있다. 다른 전송 구현이 이용가능하며, 구현하는 것은 어렵지 않다. 아래에서 설명한다.

nREPL 빌딩

릴리즈는 메이븐 중앙 저장소에서 이용가능하고, 마스터 HEAD의 SNAPSHOT 빌드는 Sonatype의 OSS 레포지토리에서 자동으로 배포된다. 그래서 nREPL을 따로 빌드할 필요는 없지만, 꼭 하고자 한다면 :

  1. 저장소에서 소스를 다운로드한다.
  2. 메이븐을 설치한다.
  3. 메이븐 빌드를 실행한다.
    1. mvn package : nREPL jar 파일을 target 디렉토리에 만든다. 클로저 1.2.0으로 테스트를 돌린다.
    2. mvn verify : 위와 똑같지만, 다른 클로저 프로파일(다른 클로저 버전)로 테스트를 수행한다.

왜 nREPL인가?

nREPL은 어플리케이션 개발자 (인터랙티브 원격 디버깅, 개발환경에서의 실험, 배포된 어플리케이션 업데이트 같은 보다 발전된 유즈케이스등의 다양한 활동들에 대한 지원)와 툴 제작자 (표준 인터랙티브 REPL과 텍스트 기반 REPL을 포함한 모든 종류의 사용자 인터페이스에 알리는 방법으로서 작동하는 환경에 접속하여 내부를 들여다 보는 표준 방법의 제공) 둘 다의 요구에 부응할 목적으로 디자인되었다.

기본 네트웍 프로토콜은 JVM이나 클로저에 의존하지 않기 때문에 클로저가 아닌 REPL 클라이언트 개발이 가능하다. REPL 연상 동작 특성은 그런 식으로 되어 있어서, 어떤 비 JVM 클로저 클라이언트도 프로토콜 구현이 가능하며, 호스트는 비동기 평가나 인터럽트를 허용하지 않을 수도 있다.

보다 자세한 내용은 여기, 노트, 토론을 보라.

디자인

nREPL들은 크게 3가지 추상이 있다: 핸들러, 미들웨어, 전송. 이것은 몇가지 의미론적 차이가 있기는 하지만, Ring의 핸들러, 미들웨어, 어댑터와 각 각 같다. nREPL은 기본적으로 메시지 지향이며 비동기적이다 (다른 REPL들이 터미널이 제공하는 스트림상에 기반하고 있는 것과는 대조적이다).

메세지

nREPL의 메시지는 맵이다. 메시지의 키와 값은 전송에 따라 다르다. 전송이 다르면 메시지 인코딩도 다르며, 어떤 데이타 타입은 표현되지 않을 수도 있다.

nREPL endpoint에 보내진 메시지는 “op”로 지정된 연산을 수행하라는 요청을 구성한다. 각 연산은 다른 필요한 데이타를 담은 메시지를 요구한다. 연산이 어떤 데이타를 요구하는지 혹은 받을 수 있는지는 각 각 다르다. 다음은 메세지의 예이다.

{"op" "eval" "code" "(+ 1 2 3)"}

연산을 수행한 결과는 하나 이상의 응답 메시지로 nREPL 클라이언트로 보내진다.

전송

전송은 Ring의 어댑터와 비슷하다. 전송은 공통 프로토콜의 구현(clojure.tools.nrepl.transport.Transport)을 제공하는데, nREPL 클라이언트와 서버가 채널이나 메시지 인코딩과는 상관없이 메시지를 보내고 받는 것을 가능하게 한다.

nREPL은 2개의 전송을 제공하는데, 둘 다 소켓 기반이다 : tty 전송은 telnet(가장 단순하고 인터랙티브한 문구 평가를 지원)등을 이용하여 nREPL에 접속한다. 다른 하나는 bencode를 이용하여 소켓상의 메시지를 인코딩한다. clojure.tools.nrepl.server/start-server 과 clojure.tools.nrepl/connect이 이용하는 기본은 후자이다.

그 밖에 다른 전송은 여기를 보라.

핸들러

핸들러는 하나의 메시지를 인자로 받는 함수이다. nREPL 서버는 하나의 핸들러로 시작하는데, 이 핸들러는 서버가 살아있는 동안 계속 메시지를 처리한다. 핸들어의 리턴값은 무시되며, 수행된 연산의 결과만이 전송을 통해 클라이언트에 보내진다. 이것은 매우 특이한데, 2가지 요인이 있다.

  • 다연산(Many of operation) - 코드 평가처럼 단순한 것을 포함하여 - 은 본질적으로 nREPL서버의 관점에서는 비동기적이다.
  • 다연산(Many of operation)은 복수의 결과를 낳는다 : “(+ 1 2) (def a 6)” 과 같은 경우처럼.

그래서 nREPL 핸들어에 제공된 메시지는 자신의 모든 응답들을 보내기 위해 사용되어야 하는 전송을 담는 :transport 엔트리를 담는다.(이 엔트리는 nREPL 서버에 의해 추가되는데, 만약 클라이언트가 “transport” 엔트리를 담은 메시지를 보낸다면, 메시지의 소스인 Transport에 의해 무시된다) 더욱이 nREPL 핸들어에 보내진 모든 메시지는 키워드 키(clojure.walk/keywordize-keys)를 갖는다.

:op에 따라, 메시지는 다른 슬롯(slot)을 가질 수도 있다. 일반적으로 메시지들은 전역의 :id를 갖는다. 모든 요청 메시지는 적어도 하나 이상의 응답 메시지를 야기하는데, 이 응답 메시지들은 요청 메시지의 :id를 슬롯(slot)으로 갖는다.

핸들러가 메시지를 완전히 처리하면, :status와 :done를 담는 메시지가 반드시 전송된다. 어떤 연산들은 :status와 :done을 보낸 후 추가적인 응답을 필요로 한다 (예를 들어 future를 시작한 코드가 평가되어 *out*으로 쓰여진 내용들). :op의 의미에 따라 다른 상태들이 보내질 수도 있다 : 특히 메시지가 특정 :op에 대해 잘못되었거나 불완전할 때는 :error와 :status를 담은 메시지가 문제의 원인에 대한 추가적인 정보와 함께 보내져야 한다.

서버가 요청없이 클라이언트에 메시지를 보낼 수도 있다 (System/out에 쓰여진 스트리밍 컨텐츠는 요청에 의해 시작되고 멈춰지는데, 그 컨텐츠를 담은 메시지는 그 요청에 대한 응답이라고 생각되지는 않는다).

nREPL 서버의 핸들러가 요청 메시지의 :op 연산을 모른다거나 수행할 수 없다면, “unknown-op” 라는 :status를 담은 메시지로 응답해야 한다.

현재는 clojure.tools.nrepl.server/start-server에 :handler로 제공된 핸들러는 여러 미들웨어 조각들로 구성된 결과로 만들어 진 것이다.

미들웨어

미들웨어는 함수를 인자로 받고 다른 함수를 리턴하면서 원래의 것에 추가적인 기능을 구성하는 고계함수이다. 예를 들어 다음은 서버의 로컬 타임을 묻는 :op “time?”을 처리하는 미들웨어이다.

(require '[clojure.tools.nrepl.transport :as t]) (use '[clojure.tools.nrepl.misc :only (response-for)])

(defn current-time
  [h]
  (fn [{:keys [op transport] :as msg}]
    (if (= "time?" op)
      (t/send transport (response-for msg :status :done :time (System/currentTimeMillis)))
      (h msg))))

전에 Ring 미들웨어를 구현해보았다면, 이 코드 패턴에 익숙할 것이다.

nREPL의 모든 기본 기능은 미들웨어로 구현된다. clojure.tools.nrepl.server/default-middlewares에서 제공되는 이 모든 기능들은 표준 클로저 REPL의 기능에 준하고 또 넘어서기 위한 것이다. clojure.tools.nrepl.server/default-handler에 제공되는 유저 특화 미들웨어도 포함될 수 있다. 미들웨어 '기술자'를 알아볼 필요가 있다.

미들웨어 기술자와 nREPL 서버 구성

nREPL 유저는 최소한의 REPL 기능이 항상 사용가능하기를 기대한다 : 평가(그리고 평가 인터럽트), 세션, 파일 로딩 등등. 하지만 모든 미들웨어들에 관해 말하자면, nREPL 미들웨어가 기본 핸들러에 적용되는 순서가 중요하다; 즉, 세션 미들웨어 핸들러는 사용자 세션을 찾고 자신이 포함한 핸들어에 위임하기 전에 매시지 맵에 추가한다 (그래서 평가 미들웨어는 그 세션 데이타를 이용하여 유저의 동적인 평가 환경을 세울 수 있다). 만약 미들웨어가 단지 함수라면 nREPL의 미들웨어 nREPL 미들웨어 스택의 커스터마이제이션은 기본 스택의 앞뒤로 붙는 경우를 제외하고 모든 기본 사항을 명백하게 반복할 필요가 있다.

이런 경우를 피하기 위해 nREPL 미들웨어 함수를 담지한 Var가 해당 미들웨어가 어떻게 적용되어야 하는지에 대한 제약사항을 정하는 기술자를 가질 수 있다. 예를 들어 clojure.tools.nrepl.middleware.session/add-stdin의 기술자는 다음과 같다.

(set-descriptor! #'add-stdin
  {:requires #{#'session}
   :expects #{"eval"}
   :handles {"stdin"
             {:doc "Add content from the value of \"stdin\" to *in* in the current session."
              :requires {"stdin" "Content to add to *in*."}
              :optional {}
              :returns {"status" "A status of \"need-input\" will be sent if a session's *in* requires content in order to satisfy an attempted read operation."}}}})

미들웨어 기술자는 :clojure.tools.nrepl.middleware/descriptor 하의 var 메타데이타에서 맵으로 구현된다.

각 기술자는 다음 3가지를 담는다.

  • :requires, 해당 미들웨어보다 높은 수준에서 적용되어야 하는 다른 미들웨어를 나타내는 스트링이나 Var 집합. Var는 구체적인 세부 의존성을 지시하고, 스트링은 지정된 :op를 처리하는 미들웨어에 대한 의존성을 지시한다.
  • :expects, 참조 미들웨어가 해당 미들웨어보다 낮은 수준에서 마지막 스택에서 존재해야 한다는 것을 제외하고 위와 같다.
  • :handles, 미들웨어에 의해 구현된 연산들을 문서화하는 맵. 맵의 각 엔트리는 그 키로 처리되는 :op의 스트링과 다음 4개의 엔트리를 값으로 갖는다.
    • :doc, 미들웨어를 설명하는 docstring.
    • :requires, 지시된 :op로 요청 메시지에서 처리 연산이 찾아야 하는 슬롯의 맵.
    • :optional, 지시된 :op로 요청 메시지로부터 처리 연산이 사용할 수 있는 슬롯의 맵.
    • :returns, 지시된 :op를 처리를 응답으로 보낸 메시지에서 찾을 수 있는 슬롯의 맵.

:handles 맵의 값들은 “describe” 연산을 지원하는데 사요되는데, 이것은 기계와 사람이 읽을 수 있는 형태의 디렉토리 구조와 nREPL endpoint에 의해 지원되는 연산에 대하 문서화를 제공한다 (clojure.tools.nrepl.middleware/describe-markdown과 “describe”의 결과, 그리고 describe-markdown을 보라.)

:requires와 :expects 엔트리는 미들웨어가 기본 핸들어에 적용되는 순서를 조정한다. 위의 add-stdin 예제에서 이 미들웨어는 “eval” 연산을 처리하는 미들웨어 이후에, clojure.tools.nrepl.middleware.session/session 미들웨어 이전에 적용된다. add-stdin의 경우, 진입 메시지가 add-stdin의 핸들러가 보기 전에 세션 미들웨어를 먼저 도달하는데 (그래서 사용자의 동적 스코프- *in* 을 포함한-가 메시지에 추가된다.) 이렇게 해서 제공된 stdin의 내용이 *in* 이하의 버퍼에 추가된다. 게다가 add-stdin은 eval 미들웨어 위에 있어야 한는데, 왜냐하면 각 평가에 앞서 *in* 에 대해 clojure.main/skip-if-eol를 호출하는 책임을 지기 때문이다 (클로저의 기본 스트림 REPL 함수 패러티를 보장하기 위해)

lecture/nrepl/docs.txt · Last modified: 2019/02/04 14:26 (external edit)