클로저의 이름 공간(namespace)은 자바의 패키지(package)에 해당하는 개념으로, 기본적으로 라이브러리들 사이의 이름 충돌(name clash)을 방지해 주고, 다른 클로저 또는 자바 라이브러리들의 사용을 원활하게 해주는 역할을 수행한다.

클로저에서 REPL을 실행하면 다음의 예에서 볼 수 있듯이 기본적으로 user 이름 공간 안에서 실행된다. def 문을 평가한 결과로 var가 반환되었는데 이때 이름공간 `user`가 앞에 붙어 있는 것에 주목할 필요가 있다. 이 var가 소속된 이름 공간이 `user`임을 명시적으로 표시하고 있다.

user> (def a 10)
#'user/a

실제 이름 공간 관련 작업을 할 때는 주로 ns 매크로를 사용하지만 이에 대한 설명은 나중으로 미루고, require, use, import 같은 함수들을 먼저 다루겠다. 그 이유는 `ns`가 배후에서 이 함수들을 이용하는 매크로에 불과하기 때문이다.

1. require

먼저 예제에서 사용할 라이브러리 clj-time`을 `project.clj 파일의 :dependencies 부분에 다음과 같이 추가한다.

[clj-time "0.8.0"]

이 상태에서 다음과 같이 실행하면 클래스를 찾을 수 없다는 예외가 발생하는데, 그 이유는 clj-time.core 모듈이 메모리에 아직 로드되지 않았기 때문이다.

(clj-time.core/date-time 2015 9 10 3 25 28 456)
; >> ClassNotFoundException clj-time.core  java.net.URLClassLoader.findClass

require 함수는 인수로 주어진 모듈이 이미 메모리에 로드되어 있지 않은 경우에만 해당 모듈을 로드하고, 이미 로드되어 있으면 다시 로드를 시도하지 않는다.

모듈 이름 앞에 인용(') 기호가 붙어야 한다는 점에 주의한다. 다시 말해, 이 함수의 인수는 반드시 *심볼형*이어야 한다.
(require 'clj-time.core)

(clj-time.core/date-time 2015 9 10 3 25 28 456)
; => #object[org.joda.time.DateTime 0x56c4a13 "2015-09-10T03:25:28.456Z"]

require 함수는 모듈 이름을 여러 개 나열할 수도 있다.

(require 'clj-time.core 'clojure.string)

1.1. 벡터 형태의 인수

제대로 실행되었지만 함수를 실행할 때마다 이름 공간인 clj-time.core`를 함수명 앞에 매번 붙여 주어야 한다면 상당히 짜증날 것이다. 이름 공간을 벡터 안에 넣은 후 `:as 키워드 뒤에 별칭(alias)을 부여해 대신 사용할 수 있다. 이 경우에도 벡터 앞에 인용(') 기호 붙이는 것을 잊지 않도록 한다.

(require '[clj-time.core :as time])

(time/date-time 2015 10 10 3 25 28 456)
; => #object[org.joda.time.DateTime 0x1c27e3aa "2015-10-10T03:25:28.456Z"]

require 함수의 벡터 형태의 인수에는 :refer 키워드도 올 수 있는데, 이 키워드 뒤에 오는 벡터에 나열된 함수명들에 한해 이름 공간을 붙이지 않고 직접 호출하는 것이 허용된다.

(require '[clojure.string :as str :refer [upper-case capitalize]])

(upper-case "korean")
; => "KOREAN"

(capitalize "korean")
; => "Korean"

(str/lower-case "USA")
; => "usa"

(lower-case "USA")
; >> CompilerException java.lang.RuntimeException:
;      Unable to resolve symbol: lower-case in this context,

:refer 키워드 뒤에는 :all 키워드도 올 수 있는데, 해당 모듈의 모든 함수를 이름 공간 없이 호출할 수 있다. 이 경우는 뒤에 설명할 use 함수를 사용한 것과 같은 효과를 내는데, `:refer :all`의 형태를 사용하는 것이 권장되고 있다.

(require '[clojure.string :refer :all])

(lower-case "USA")
; => "usa"

1.2. 리스트 형태의 인수

require 함수는 리스트 형태의 인수도 받을 수 있다. 이 경우 리스트의 첫 번째 요소가 공통 인수 역할을 한다. 예를 들어 clj-time 라이브러리는 core 모듈 이외에도 foramt, coerce 모듈도 함께 제공하는데, 이 세 모듈을 모두 require하고자 할 때 다음의 두 표현은 같은 결과를 낳는다.

(require 'clj-time.core 'clj-time.format 'clj-time.coerce)

(require '(clj-time core format coerce))

앞에서 배운 벡터 형태의 인수와 결합해 사용할 수도 있다. 다음의 두 표현도 같은 결과를 낳는다.

(require 'clojure.zip
         '[clojure.set :as set])

(require '(clojure zip [set :as set]))

1.3. flags

require 함수는 :reload, :reload-all, :verbose 3 종류의 플래그(flag)를 인수로 취할 수 있다. 이 플래그들은 라이브러리를 개발할 떄 매우 유용하다.

앞에서 require 함수는 이미 로드된 모듈은 다시 로드하지 않는다고 했다. 하지만 라이브러리 개발 도중에는 이미 로드된 모듈도 다시 강제로 require할 필요가 생긴다. 이름 공간 `example.a`가 `example.b`를, `example.b`가 다시 `example.c`를 require하는 경우를 예로 들어 보자.

            require               require
example.a  -------->  example.b  -------->  example.c

다음과 같이 :reload 플래그를 주면, example.a 모듈이 평가될 때 example.b 모듈이 이미 로드되어 있어도 example.b 모듈이 다시 강제로 로드된다.

:reload 플래그
;; example/a.clj
(ns example.a
  (:require [example.b :as b] :reload))   (1)

;; example/b.clj
(ns example.b
  (:require [example.c :as c]))

;; example/c.clj
(ns example.c)

다음 같이 :reload-all 플래그를 주면, example.a 모듈이 평가될 때 example.b 모듈뿐만 아니라, example.b 모듈이 다시 require하고 있는 example.c 모듈까지 줄줄이 다시 강제로 로드된다.

:reload-all 플래그
;; example/a.clj
(ns example.a
  (:require [example.b :as b] :reload-all))   (2)

;; example/b.clj
(ns example.b
  (:require [example.c :as c]))

;; example/c.clj
(ns example.c)

:verbose 플래그를 주면, 이름 공간이 require될 때 일어나는 과정과 관련되는 정보를 상세히 출력해 준다. 따라서 이름공간과 관련된 작업이 실제 어떻게 일어아는지 직접 확인하고 싶을 떄 사용하면 유용하다.

:verbose 플래그
(require '[clojure.string :as str] :verbose)   (3)
; >> (clojure.core/in-ns 'user)
;    (clojure.core/alias 'str 'clojure.string)
; => nil

2. refer

refer 함수는 require 함수의 인수에 :refer 옵션이 있거나 use 함수가 호출될 때 이 함수들의 내부에서 이용되는 함수로, 개발자가 `require`나 `use`같은 일을 하는 함수를 직접 개발할 일이 없는 한 호출할 일은 거의 없는 함수이지만, 그 작동 원리는 이해할 필요가 있다.

클로저는 이름 공간마다 다음과 같은 형태(실제로는 map 자료형의 키/값 쌍)로 심볼 테이블의 항목들을 유지한다.

symbol --> var

예를 들어 user 이름 공간에서 다음과 같이 upper-case 함수를 정의(자세한 구현은 생략)하고 실행해 보면 예상한 대로 결과가 나온다.

user> (defn upper-case [s]
        (str "My upper-case function: arg = " s))
#'user/upper-case

user> (upper-case "hello")
"My upper-case function: arg = hello"

이때`user` 이름 공간의 심볼 테이블에는 다음의 항목이 새로 추가된다.

user 심볼 테이블
upper-case --> #'user/upper-case

이제 user 이름 공간에서 clojure.string 이름 공간을 refer한 후 upper-case 함수를 호출해 보자.

refer 함수는 require 함수와는 달리, 한 개의 이름 공간만을 인수로 지정할 수 있음에 유의한다.
user> (refer 'clojure.string)

user> (upper-case "hello")
"HELLO"

방금 전에 user 이름 공간에서 정의한 upper-case 함수는 사라지고, clojure.string 이름 공간의 upper-case 함수가 실행되었다. 그 이유는 user 이름 공간의 심볼 테이블의 upper-case 항목이 다음과 같이 바뀌었기 때문이다.

user 심볼 테이블
upper-case --> #'clojure.string/upper-case

다시 말해 refer 함수를 호출하면, 인수로 지정된 이름 공간의 모든 public var와 관련된 심볼 테이블 항목들이 현재의 이름 공간의 심볼 테이블에 복사하는 방식으로 추가되면서, 이미 정의되어 있는 항목들은 덮어 쓰게 된다. 따라서 위에서 본 것 처럼, 예기치 않은 위험한 상황이 초래될 수 있으므로 특별한 상황이 아니면, 다음과 같이 필터를 주어 사용하는 것이 좋다.

2.1. filters

refer 함수는 :only, :exclude, :reanme 필터를 사용할 수 있다.

:only 키워드 뒤애 사용하고 싶은 심볼들을 나열하면, clojure.string 이름 공간에서 그 심볼들만을 현재의 이름 공간의 심볼 테이블에 추가한다.

(refer 'clojure.string
       :only '[upper-case trim])

(upper-case "world")
; => "WORLD"

(lower-case "UNESCO")
; >> CompilerException java.lang.RuntimeException:
;      Unable to resolve symbol: lower-case in this context

(clojure.string/lower-case "NASA")
; => "nasa"

반대로 :exclude 키워드 뒤에 심볼들을 나열하면, 그 심볼들을 제외한 나머지 모든 심볼들을 현재의 이름 공간의 심볼 테이블에 추가한다.

(refer 'clojure.string
       :exclude '[lower-case trim])

(lower-case "UFO")
; >> CompilerException java.lang.RuntimeException:
;      Unable to resolve symbol: lower-case in this context

(upper-case "love")
; => "LOVE"

:rename 키워드 뒤에 맵의 형태로, 사용하고자 하는 심볼들의 이름을 자신이 원하는 이름으로 변경할 수 있다.

(refer 'clojure.string
       :rename '{upper-case upcase capitalize cap})

(upcase "people")
; => "PEOPLE"

;; upper-case는 더 이상 사용할 수 없다.
(upper-case "ruby")
; >> CompilerException java.lang.RuntimeException:
;      Unable to resolve symbol: upper-case in this context

;; 사용하려면 clojure.string 이름 공간을 붙여 주여야 한다.
(clojure.string/upper-case "ruby")
; => "RUBY"

3. use

use 함수는 refer 함수를 확장한 것으로 보면 좋다. 그래서 refer 함수에서 사용한 모든 키워드 옵션을 그대로 사용할 수 있다. 다른 점이라면, require 함수처럼 여러 개의 이름 공간을 지정할 수 있고, 벡터 형태리스트 형태의 인수도 require 함수에서처럼 허용된다. 심지어는 require 함수에서 사용한 플래그들도 그대로 사용할 수 있다. 그래서 다음과 같이 use 함수를 require`와 `refer 함수를 하나로 합쳐 놓은 것으로 흔히들 많이 설명한다.

use = require + refer

다음은 use 함수에 다양한 형태의 인수들을 사용한 예이다.

(use 'clojure.test
     '[clojure.string :rename {capitalize cap reverse rev}
                      :only [capitalize trim]]
     '(clojure.java io shell)
     :reload
     :verbose)
; >> (clojure.core/load "/clojure/test")
;    (clojure.core/in-ns 'user)
;    (clojure.core/refer 'clojure.test)
;    (clojure.core/load "/clojure/string")
;    (clojure.core/in-ns 'user)
;    (clojure.core/refer 'clojure.string :rename '{capitalize cap, reverse rev}
;                                        :only '[capitalize trim])
;    (clojure.core/load "/clojure/java/io")
;    (clojure.core/in-ns 'user)
;    (clojure.core/refer 'clojure.java.io)
;    (clojure.core/load "/clojure/java/shell")
;    (clojure.core/in-ns 'user)
;    (clojure.core/refer 'clojure.java.shell)
; => nil

use 함수가 refer 함수와 다른 점은 :as 키워드를 붙여 별칭(alias)을 사용할 수 있다는 것이다.

(use '[clojure.string :as str :only [split]])

;; 별칭 str을 앞에 붙여 clojure.string의 모든 함수를 호출할 수 있다.
(str/replace "foobar" "bar" "baz")
; => "foobaz"

;; split 함수의 경우에는 이름 공간 없이 사용할 수 있다.
(split "hello world" #" ")
; => ["hello" "world"]

사실 다음 두 표현은 같은 일을 한다.

(use '[clojure.string :as str :only [split]])

(require '[clojure.string :as str :refer [split]])

따라서 이런 경우에 굳이 use 함수를 사용할 필요는 없을 것이다. 하지만 다음과 같이 use 함수에서만 제공하는 키워드 옵션을 사용해야 하는데, require 함수에서처럼 별칭을 사용하고 싶을 때에는 user 함수에서 :as 키워드를 사용하는 것이 불가피해진다.

(use '[clojure.string :as str
                      :rename {capitalize cap reverse rev}
                      :only [upper-case]])

(lower-case "ASCII")
; >> CompilerException java.lang.RuntimeException:
;      Unable to resolve symbol: lower-case in this context

(str/lower-case "ASCII")
; => "ascii"

(upper-case "physcs")
; => "PHYSICS"

(cap "math")
; => "Math"

4. import

기본적으로 자바 클래스들은, 클로저 라이브러리들의 경우와는 달리, import 함수를 사용하지 않고도 직접 호출할 수 있다.

(def date (java.util.Date.))

date
; => #inst "2015-09-10T07:49:28.622-00:00"

하지만, 클래스명 앞에 패키지 경로명을 매번 일일이 붙여주어야 하므로, 반복해서 사용해야 하는 경우에는 불편하다. 이런 경우에 import 함수를 사용한다.

(import java.util.Date)

(def date (Date.))

date
; => #inst "2015-09-10T07:52:54.847-00:00"

import 함수에서 자바 클래스를 개별적으로 나열할 때에 한해, 인용 기호를 붙이지 않아도 무방하다. 다음의 두 형태 모두 허용된다.

(import java.util.Date)

(import 'java.util.Date)

require 함수의 경우처럼 리스트 형태의 인수를 취할 수 있다. 이 경우에는 인용 기호를 반드시 앞에 붙여 주어야 한다.

(import java.sql.DriverManager
        '(java.util Date Calendar)
        '(java.net URI ServerSocket))

5. refer-clojure

예를 들어, 다음과 같은 코드를 작성하고 컴파일을 하면

(ns my-namespace)

(defn inc []
  "my new inc function")

my-namespace 이름 공간에 새로 정의한 inc 함수가 clojure.core`에 이미 정의되어 있는 `inc 함수를 덮어 쓴다는 경고 메시지를 컴파일러가 내보낸다.

WARNING: inc already refers to: #'clojure.core/inc in namespace: my-namespace,
  being replaced by: #'my-namespace/inc

하지만 다음과 같이 `refer-clojure`를 추가하면, 위와 같은 메시지가 뜨지 않도록 컴파일러에게 미리 알려주는 역할을 수행한다.

(ns my-namespace
  (:refer-clojure :exclude [inc]))

(defn inc []
  "my new inc function")

결과적으로 다음 두 함수는 같은 일을 수행한다.

(refer-clojure :exclude [inc])

(refer 'clojure.core :exclude [inc])

참고로 이 함수는 ns 매크로 안에서 실행해야만 효과가 있다. 다음과 같이 실행하면 (1)의 단계에서 기본적으로 `(refer-clojure)`가 이미 실행되어서, (2)를 실행한다 해도 그 효과가 발생하기에는 때가 너무 늦기 때문이다.

(ns my-namespace)                ;; (1)

(refer-clojure :exclude [inc])   ;; (2)

(defn inc []
  "my new inc function")

따라서 refer-clojure`는 `refer 함수에서 사용할 수 있는 필터들을 모두 사용할 수 있다.

(:refer-clojure :exclude [print])

(:refer-clojure :only [print])

(:refer-clojure :rename {print core-print})

6. ns

ns 매크로는 지금까지 설명한 함수들(단, refer 함수는 제외)을 모두 사용할 수 있도록 감싸 만든 매크로이다. 차이점이 있다면 각 함수 이름 앞에 콜론(:) 기호를 붙여 주어야 하고, 뒤따르는 이름 공간 앞에 인용(') 기호를 붙일 필요가 없다는 정도이다.

ns 매크로 안에서 인용 기호를 붙이면 오히려 에러가 발생한다.
(ns foo.bar
  (:refer-clojure :exclude [find])
  (:require [clojure.string :as string]
            [clojure.set :refer [difference intersect]]
            :verbose)
  (:use clojure.test :reload)
  (:import java.util.Date
           [java.util.concurrent Executors TimeUnit]))