개발 시작 시간(Startup Time) 줄이기

개발 시작 시간(Startup Time) 줄이기

Clojure 라이브러리는 대부분 소스 상태로 배포돼요. 그래서 개발을 시작할 때마다 그 소스들을 처음부터 다시 컴파일하게 되죠. 의존하는 라이브러리가 늘어날수록 이 초기 로딩 단계가 점점 더 길어지는데요. 바로 이 지점에서 compile 함수가 개발 시작 시간을 크게 줄이는 데 도움을 줘요.

여기서 짚고 갈 핵심 사실들을 먼저 정리할게요.

  • 대부분의 라이브러리는 소스로 배포되기 때문에, 우리는 그걸 계속 반복해서 컴파일하게 돼요
  • 네임스페이스를 명시적으로 컴파일할 수 있는 compile 함수가 있어요
  • 네임스페이스 컴파일은 전이적(transitive) 으로 일어나요 — 즉 그 네임스페이스가 require 하는 모든 네임스페이스까지 함께 컴파일돼요
  • compile은 이 결과를 디스크에 파일로 써 주고, 이후 require는 그 파일을 사용해요
  • compile을 개발 시작 때 로드하는 네임스페이스나 user.clj, 또는 서버로 실행하는 main 네임스페이스에 적용하면 시작 시간을 줄일 수 있어요

출처: Clojure 공식문서

본문

compile이 하는 일

compile 함수는 네임스페이스 심볼을 받아서, 그 네임스페이스와 그 네임스페이스가 require 하는 모든 네임스페이스를 *compile-path*으로 컴파일해요. *compile-path*은 기본값이 classes인데요, 이 디렉터리가 반드시 존재하고 클래스패스에 포함돼 있어야 해요.

(compile 'my.namespace)    ;; .class 파일을 *compile-path*에 쓴다

이렇게 컴파일해 두면, 이후에 그 네임스페이스들이 require 될 때 원래 .clj 파일 대신 컴파일된 class 파일이 로드돼요. 다만 소스 파일이 수정돼서 더 새로워지면 그 소스가 대신 로드되죠. 그래서 새 의존성이 생기거나 코드가 바뀔 때는 가끔 다시 컴파일해 주는 게 필요해요.

컴파일은 로딩의 부수 효과

주의할 점 하나 — 컴파일은 로딩(loading)의 부수 효과라서, 이미 로드된 네임스페이스에는 영향을 주지 못해요. 여기서 특히 특별한 경우가 user.clj 파일인데요, Clojure 런타임이 이 파일을 자동으로 로드하거든요. 개발 중에 user.clj를 쓰고 있거나, 이미 컴파일된 네임스페이스를 다시 컴파일해야 한다면, 컴파일하면서 재로드를 강제로 실행하면 돼요.

(binding [*compile-files* true] ;; 로드하면서 컴파일한다
  (require 'user :reload-all))  ;; 이 네임스페이스와 전이적으로 로드된 모든 네임스페이스를 재로드한다

정말 이게 전부예요! 이 기법을 쓰면 개발 중 시작 시간을 꽤 줄일 수 있어요. 특히 의존성이 늘어날수록 그 효과가 커져요.

개발용 alias 만들기

deps.edn 프로젝트에서는 classes 디렉터리를 포함하는 개발용 alias(예: :dev)를 만들어 두는 게 좋아요.

{:deps { ... }
 :aliases
 {:dev {:extra-paths ["classes"]}}}

그리고 classes 디렉터리가 존재하도록 해야 해요. 빈 classes 디렉터리를 버전 관리되는 프로젝트 구조에 포함해 두는 것도 쓸만한데요(단, 컴파일된 .class 파일은 무시하고 커밋하지 않도록 주의하세요).

그다음 alias로 REPL을 시작하고 compile을 호출하면 classes 디렉터리가 채워지면서 이점을 보기 시작해요.

$ clj -A:dev
Clojure 1.10.1
user=> (compile 'my.namespace)

개발 alias에 user.clj 통합하기

자동으로 로드되는 user.clj를 쓰고 싶다면, dev alias에 dev 소스 경로를 추가해서 통합하면 돼요.

{:deps { ... }
 :aliases
 {:dev {:extra-paths ["dev" "classes"]}}}

그리고 dev/user.clj를 만들고요.

(ns user
  (:require ... ))

;; dev 전용 함수 등

user.clj에 대해서는 앞서 본 수정된 컴파일 과정을 꼭 사용해야 해요.

$ clj -A:dev
Clojure 1.10.1
user=> (binding [*compile-files* true] (require 'user :reload-all))

더 알아보기