REPL과 main 진입점

REPL과 main 진입점

Clojure를 실제로 실행해 보려면 크게 두 경로가 있어요. 명령줄에서 코드 조각을 하나씩 입력하며 바로 결과를 확인하는 대화형 REPL, 그리고 파일에 모아둔 코드를 스크립트처럼 통째로 돌리는 방식이죠. 이 문서는 이 두 실행 방식이 Java의 프로그램 실행 도구인 java와 어떻게 이어지는지, 그 중심에 있는 clojure.main 네임스페이스와 관련 함수들을 설명하는 글이에요.

출처: Clojure 공식문서

본문

clojure.main 네임스페이스

clojure.main 네임스페이스는 Clojure 프로그램과 대화형 세션을 Java의 애플리케이션 실행 도구인 java를 통해 시작할 수 있게 해 주는 함수들을 제공해요.

clojure.main --help

clojure.main/main 진입점은 여러 인자와 플래그를 받아들여요.

  • 옵션이나 인자가 없으면 대화형 Read-Eval-Print Loop(REPL)를 실행해요.
  • init 옵션:
    • -i, --init path — 파일이나 리소스를 로드해요.
    • -e, --eval string — 문자열 안의 표현식을 평가하고, nil이 아닌 값은 출력해요.
    • --report target — 잡히지 않는 예외를 "file"(기본값), "stderr", "none" 중 어디로 보고할지 지정해요. clojure.main.report 시스템 프로퍼티를 덮어써요(1.10.1부터 추가).
  • main 옵션:
    • -r, --repl — REPL을 실행해요.
    • path — 파일이나 리소스에서 스크립트를 실행해요.
    • - — 표준 입력에서 스크립트를 실행해요.
    • -m, --main — 실행할 -main 함수를 가진 네임스페이스를 지정해요.
    • -h, -?, --help — 도움말 메시지를 출력하고 종료해요.
  • 동작(operation):
    • 흔히 set!할 수 있는 vars에 스레드 로컬 바인딩을 설정해요.
    • user 네임스페이스로 들어가요.
    • 어떤 main 옵션 뒤에 오는 명령줄 인자들을 담은 문자열 시퀀스를 *command-line-args*에 바인딩해요.
    • 모든 init 옵션을 순서대로 실행해요.
    • 요청했다면 REPL이나 스크립트를 실행해요.

init 옵션은 반복해서 쓰고 자유롭게 섞을 수 있지만, 반드시 어떤 main 옵션보다 앞에 있어야 해요. REPL을 실행하기 전에 어떤 eval 옵션이라도 등장하면, 평소 나오는 REPL 인사말("Clojure ~(clojure-version)")은 표시되지 않아요.

경로는 파일시스템 기준의 절대 경로나 상대 경로일 수도 있고, 클래스패스 기준일 수도 있어요. 클래스패스 기준 경로에는 @ 또는 @/ 접두어가 붙어요.

REPL 시작하기

Clojure REPL을 시작하는 가장 간단한 방법은 clojure.main을 호출하는 clj 명령 도구를 쓰는 거예요.

$ clj
Clojure 1.12.0
user=>

REPL 프롬프트는 현재 네임스페이스(기본값은 user)의 이름을 보여줘요.

REPL을 쓸 때 사용할 수 있는 특별한 vars가 몇 개 있어요.

  • *1, *2, *3 — 마지막으로 평가된 세 개의 표현식 결과를 담아요.
  • *e — 마지막 예외의 결과를 담아요.

clojure.repl 네임스페이스에는 사용 가능한 함수의 소스와 문서를 살펴보는 데 유용한 함수가 몇 가지 있어요.

  • doc — 이름이 주어지면 그 var의 docstring을 출력해요.
  • find-doc — doc이나 이름이 패턴과 일치하는 var의 docstring을 출력해요.
  • apropos — 정규식과 일치하는 정의들의 시퀀스를 반환해요.
  • source — 심볼의 소스를 출력해요.
  • pst — 주어진 예외 또는 기본적으로 *e에 대한 스택 트레이스를 력해요(print stack trace).

스크립트 실행하기

Clojure 코드가 담긴 파일을 스크립트로 실행하려면, clojure.main에 인자로 그 스크립트 경로를 전달하면 돼요.

clj -M /path/to/myscript.clj

스크립트에 인자 전달하기

스크립트에 인자를 넘기려면 clojure.main을 시작할 때 뒤에 인자를 더 붙이면 돼요.

clj -M /path/to/myscript.clj arg1 arg2 arg3

이 인자들은 문자열의 시퀀스로 제공되어 *command-line-args* var에 바인딩돼요.

*command-line-args* => ("arg1" "arg2" "arg3")

오류 출력

REPL에서

Clojure 1.10부터 Clojure 오류는 여러 단계(phase) 중 하나로 분류돼요.

  • :read-source — REPL이나 소스 파일에서 문자를 읽는 동안 발생한 오류.
  • :macro-syntax-check — 매크로 호출의 문법에서 찾은 오류. spec 또는 매크로가 IllegalArgumentException, IllegalStateException, ExceptionInfo를 던진 경우.
  • :macroexpansion — 매크로 평가 중에 발생한 그 외의 모든 오류는 매크로 확장 오류로 분류돼요.
  • :compile-syntax-check — 컴파일 중에 잡힌 문법 오류.
  • :compilation — 컴파일 중에 잡힌 비문법 오류.
  • :execution — 실행 시점에 발생한 모든 오류.
  • :read-eval-result — 실행 결과를 읽는 동안 발생한 오류(결과를 읽는 REPL에만 해당).
  • :print-eval-result — 실행 결과를 출력하는 동안 발생한 오류.

모든 단계에서 던져진 예외(:execution 제외)에는 다음 키 중 하나 이상이 담긴 ex-data가 붙어요.

  • :clojure.error/phase — 단계 표시.
  • :clojure.error/source — 파일 이름(경로 제외).
  • :clojure.error/line — 정수 줄 번호.
  • :clojure.error/column — 정수 열 번호.
  • :clojure.error/symbol — 확장/컴파일/호출 중인 심볼.
  • :clojure.error/class — 원인 예외 클래스 심볼.
  • :clojure.error/cause — 원인 예외 메시지.
  • :clojure.error/spec — spec 오류에 대한 explain-data.

clojure.main REPL은 이 오류 분류와 출력 기능을 기본으로 포함하지만, 이 과정의 개별 단계도 다른 REPL이 쓸 수 있도록 노출되어 있어요. 구체적으로는 이런 함수들이에요.

  • Throwable->map — 예외 체인을 Clojure 데이터로 변환해요.
  • ex-triage — Clojure 예외 데이터를 분석해 예외 체인의 위아래에서 관련 정보를 끌어내, 예외 문자열을 만들기 위해 필요한 데이터만 담은 맵을 만들어요.
  • ex-str — 예외 데이터 집합을 받아 해당 단계에 맞는 메시지를 만들어요.

clojure.main REPL은 이 함수들을 파이프라인으로 연결해 출력되는 예외 메시지를 만들며, 그 조합은 (-> ex Throwable->map clojure.main/ex-triage clojure.main/ex-str)와 같아요. 다른 REPL은 예외 출력을 만들거나 커스터마이즈할 때 이 파이프라인의 일부를 필요에 따라 쓸 수 있어요.

실행 도구(launcher)로서

Clojure 1.10.0까지는 clojure.main을 프로그램 실행 도구(-m, -e 또는 스크립트)로 쓸 때, 잡히지 않는 예외가 전체 중첩 스택 트레이스와 함께 자동으로 출력됐고, 위의 오류 분류·출력 과정은 적용되지 않았어요.

Clojure 1.10.1부터는 잡히지 않는 예외를 Clojure REPL과 같은 오류 분류·출력 기능으로 잡아 출력해요. 전체 스택 트레이스, ex-info 등 정보는 설정으로 지정한 대상(target)에 출력돼요.

세 가지 오류 대상이 가능해요.

  • file — 임시 파일에 써요(기본값, stderr로 폴백).
  • stderr — stderr 스트림에 써요.
  • none — 쓰지 않아요.

이 오류 대상은 clojure.main의 옵션으로 지정하거나 Java 시스템 프로퍼티로 지정할 수 있는데, 플래그가 우선해요. clojure.main을 호출할 때(또는 clj 도구를 쓸 때)는 --report <target>을 쓰고, Java 시스템 프로퍼티로는 -Dclojure.main.report=<target>을 써요.

다른 프로그램도 이 기능을 활용하고 싶을 수 있는데, report-error에 제공되어 있어요. Throwable과 선택적으로 :target을 받아요.

user 네임스페이스

기본적으로 Clojure REPL은 user 네임스페이스에서 시작하며, 이 네임스페이스는 주로 탐색적인 실험 작업에 쓰여요.

Clojure REPL은 다음 네임스페이스들을 자동으로 로드하고 다음 함수들을 refer해요.

in-nsns로 다른 네임스페이스로 전환하면, 그 네임스페이스에서 명시적으로 refer하지 않는 한 이 함수들을 쓸 수 없어요.

user.clj 로딩

Clojure 런타임은 시작 시 클래스패스에서 user.clj를 찾으면 로드해요. 이는 개발 시점 편의 기능을 제공하기 위한 장치라서, 일반적으로 운영(production)에서는 권장되지 않아요.

user.clj 파일은 Clojure 런타임이 초기화될 때 로드되므로 보통 애플리케이션의 main 네임스페이스보다 먼저 실행돼요. 그래서 user.clj가 로드하는 네임스페이스나 리소스는 애플리케이션의 시작 시간에 영향을 줘요.

대화형 사용을 위한 라이브러리 추가

JVM을 재시작하지 않고 라이브러리를 대화형으로 추가하고 싶은 개발 시점 상황이 여럿 있어요. 추측성 평가(speculative evaluation), 프로젝트에 알려진 의존성을 추가하거나, 특정 작업을 하기 위한 라이브러리를 더하는 경우가 그렇죠.

Clojure CLI로 REPL 시작 시점에 로드될 의존성을 선언할 수 있어요. Clojure 1.12부터는 REPL에서 라이브러리를 동적으로 로드해 대화형으로 쓸 수도 있어요. 이 함수들은 clojure.repl.deps 네임스페이스에 있어요.

  • add-lib — 클래스패스에 없는 lib를 받아서, 필요하면 다운로드하고 클래스로더에 추가해 사용 가능하게 만들어요. 이미 클래스패스에 있는 lib는 갱신하지 않아요. 좌표(coordinate)를 주지 않으면 가장 최신 Maven 버전이나 git 태그(라이브러리에 추론된 git 저장소 이름이 있을 때)를 사용해요.
  • add-libsadd-lib과 비슷하지만, 새 라이브러리와 버전들의 집합을 함께 해석해요.
  • sync-depsdeps.edn에 있지만 아직 클래스패스에 없는 lib들로 add-libs를 호출해요.

이 새 함수들은 REPL에서의 대화형 사용만을 위한 거예요. 코드를 만들고 유지하는 올바른 방법은 여전히 deps.edn을 쓰는 거죠. 그래서 이 함수들은 모두 *repl*true로 바인딩되어 있는지 확인해요. clojure.main REPL에서는 이 새 함수들이 user 네임스페이스에 자동으로 refer되어 있고, 다른 REPL에서는 사용 전에 (require '[clojure.repl.deps :refer :all])이 필요할 수 있어요.

tap

tap은 일련의 정보·진단 값을 (아마도 부수 효과를 가진) 핸들러 함수 집합에 분배하는, 공유되고 전역적으로 접근 가능한 시스템이에요. 더 나은 디버그용 prn으로 쓰거나 로깅 같은 기능에 활용할 수 있어요.

tap>은 tap 집합에 값을 보내요. tap은 add-tap으로 추가할 수 있고, tap>에 보내진 어떤 값으로도 호출돼요. tap 함수는 (스트림 같은 경우처럼) 잠시 블록할 수 있지만, tap> 호출을 막지는 않아요. 다만 무한정 블록하면 tap 값이 버려질 수 있어요. 등록된 tap이 없으면 tap>은 값을 버려요. tap 제거는 remove-tap로 해요.

소켓 서버 시작하기

Clojure 런타임은 이제 시스템 프로퍼티에 기반해 초기화 시점에 소켓 서버를 시작할 수 있어요. 대표적인 용도로는 소켓 기반 REPL을 제공하는 일이지만, 코드를 바꾸지 않고 기존 프로그램에 서버 기능을 동적으로 더하는 용도로도 쓸 수 있어요.

clojure.server.<server-name> 같은 각 JVM 시스템 프로퍼티마다 소켓 서버 하나가 시작돼요. 이 프로퍼티의 값은 소켓 서버의 설정을 나타내는 edn 맵이며, 이러한 프로퍼티들을 가져요.

  • server-daemon — 기본값 true, 소켓 서버 스레드가 종료를 막지 않아요.
  • address — 호스트나 주소, 기본값은 loopback이에요.
  • port — 양의 정수, 필수예요.
  • accept — 소켓 접수 시 호출할 함수의 네임스페이스가 붙은 심볼, 필수예요.
  • args — accept에 전달할 인자들의 순차 컬렉션.
  • bind-err — 기본값 true, *err*을 소켓 출력 스트림에 바인딩해요.
  • client-daemon — 기본값 true, 소켓 클라이언트 스레드가 종료를 막지 않아요.

추가로, 소켓 서버와 함께 쓰도록 약간 커스터마이즈된 repl 함수가 clojure.core.server/repl에 제공되어 있어요.

다음은 repl 리스너로 소켓 서버를 시작하는 예시예요. 이걸 기존 Clojure 프로그램에 추가하면 5555 포트의 로컬 연결로 외부 REPL 클라이언트를 받아들일 수 있어요.

-Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}"

Clojure CLI에서는 -J 플래그로 이 옵션을 JVM에 전달할 수 있어요(소켓 REPL과 함께 로컬 REPL도 함께 시작한다는 점에 주의).

clj -J-Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}"

이 repl에 원격으로 접속하는 예시 클라이언트로는 telnet이 있어요(netcat도 쓸 수 있어요).

$ telnet 127.0.0.1 5555
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
user=> (println "hello")
hello

특별한 명령 :repl/quit으로 서버에게 클라이언트 repl 세션을 닫으라고 지시할 수 있어요.

user=> :repl/quit
Connection closed by foreign host.

관련 자료:

관련 함수

더 알아보기