Clojure Var와 전역 환경(Vars and the Global Environment)

Clojure Var와 전역 환경(Vars and the Global Environment)

Clojure는 바뀌는 값에 대한 지속적인 참조(persistent reference)를 유지해야 할 때가 종종 있어요. 그리고 그걸 통제된 방식으로 다루는 네 가지 장치를 제공하는데, 바로 Var, Refs, Agents, Atoms예요. 이 중 Var는 스레드 단위로 동적으로 다시 바인딩(rebind)할 수 있는 가변 저장 위치를 참조하는 메커니즘이에요. 이 문서에서 Var가 무엇이고 어떤 규칙으로 동작하는지 하나씩 살펴볼게요.

출처: Clojure 공식문서

본문

Var 만들기: def와 루트 바인딩(root binding)

특별 폼(special form) def는 Var를 만들고 인턴(intern)해요. Var에 접근하는 값을 살펴볼게요.

user=> (def x)
#'user/x
user=> x
#object[clojure.lang.Var$Unbound 0x14008db3 "Unbound: #'user/x"]

def가 Var를 만들 때 초기 값이 없으면 그 Var는 바인딩되지 않은(unbound) 상태가 돼요. 그래서 x를 평가하면 Unbound 객체가 나오죠.

user=> (def x 1)
#'user/x

user=> x
1

이번엔 def x 1처럼 초기 값을 주면 루트를 바인딩해요. 이때 이미 바인딩된 Var라도 새 값으로 다시 바인딩돼요.

루트 바인딩과 스레드 로컬 바인딩

모든 Var는 (꼭 그래야 하는 건 아니지만) 루트 바인딩(root binding) 을 가질 수 있어요. 루트 바인딩은 스레드 로컬 바인딩이 없는 모든 스레드가 공유하는 바인딩이에요. 그래서 Var의 값은 다음과 같이 정해져요.

  • 그 스레드의 스레드 로컬 바인딩이 있으면 그 값,
  • 없으면 루트 바인딩 값(루트 바인딩이 있다면).

동적 Var(dynamic Var)와 binding

기본적으로 Var는 정적(static) 이에요. 하지만 Var에 ^:dynamic 메타데이터를 붙이면 동적(dynamic) 으로 표시할 수 있고, 그러면 binding 매크로로 스레드 단위 바인딩을 만들 수 있어요. 각 스레드 안에서 바인딩은 스택 규칙(stack discipline) 을 따라요.

user=> (def ^:dynamic x 1)
user=> (def ^:dynamic y 1)
user=> (+ x y)
2

user=> (binding [x 2 y 3]
         (+ x y))
5

user=> (+ x y)
2

binding으로 만든 바인딩은 binding 블록 안에서만 적용되고, 블록을 벗어나면 원래 값으로 되돌아와요. 그래서 (+ x y)는 처음에 2, binding 안에서는 5, 다시 밖에서는 2가 나오는 거예요.

여기서 두 가지 중요한 점이 있어요.

  • binding으로 만든 바인딩은 다른 스레드에서는 볼 수 없어요.
  • 하지만 binding 바인딩은 값을 다시 할당할 수 있어서, 중첩된 컨텍스트가 콜 스택(call stack)에 올라가기 전에 앞선 코드와 소통하는 수단이 될 수 있어요.

이 기능은 ^:dynamic 메타데이터로 명시적으로 옵트인(opt-in)해야만 쓸 수 있어요. 한편 정적 Var를 특정 컨텍스트 안에서 다시 정의하고 싶은 상황도 있는데, Clojure는 (버전 1.3부터) 이를 위한 함수인 with-redefswith-redefs-fn을 제공해요.

함수도 Var에 저장돼요

defn으로 정의한 함수는 Var에 저장돼요. 그 덕분에 실행 중인 프로그램에서 함수를 다시 정의할 수 있고, 여러 가지 관점 지향 프로그래밍(aspect-oriented programming)이나 컨텍스트 지향 프로그래밍(context-oriented programming) 도 가능해져요. 예를 들어 특정 호출 컨텍스트나 특정 스레드에서만 로깅(logging) 동작을 함수에 감싸는(wrap) 식의 처리를 할 수 있죠.

바인딩 전달(Binding conveyance)

Clojure의 일부 동시성(concurrency) 함수(예: future, agent)는 바인딩 전달 을 제공해요. 이는 현재 스레드의 동적 바인딩 집합을 다른 스레드로 전달해서, 같은 환경을 유지한 채 비동기로 작업을 계속할 수 있게 해주는 기능이에요. 이 기능은 future, send, send-off, pmap이 제공해요.

(def ^:dynamic *num* 1)
(binding [*num* 2] (future (println *num*)))
;; prints "2", not "1"

여기서 주목할 건 future 블록이 실행되는 시점의 바인딩이 전달된다는 점이에요. *num*binding으로 2를 묶은 상태에서 future를 만들었으니, future 안에서 println은 루트 값인 1이 아니라 2를 출력해요.

set! — 할당 특별 폼

(*set!* var-symbol expr)은 할당(assignment)을 위한 특별 폼이에요.

  • 첫 피연산자가 심볼(symbol) 이면 전역 Var로 해석돼야 해요.
  • 그 Var의 현재 스레드 바인딩 값expr의 값으로 설정돼요.
  • 현재는 set!로 Var의 루트 바인딩을 설정하는 것은 오류예요. 즉 Var 할당은 스레드 로컬이에요.
  • 어느 경우든 expr의 값이 반환돼요.

여기서 기억할 점이 하나 있어요. 함수의 파라미터나 로컬 바인딩에는 할당할 수 없어요. Clojure에서 가변(mutable)인 것은 Java 필드, Var, Refs, Agents뿐이에요. 한편 set!를 Java 필드에 쓰는 방법은 Java Interop 문서에서 다뤄요.

인턴(Interning)

네임스페이스 시스템은 심볼 → Var 객체의 전역 맵을 유지해요. def 표현식이 현재 네임스페이스에서 해당 심볼에 대한 인턴된 항목을 찾지 못하면 새로 만들고, 이미 있다면 기존 Var를 사용해요. 이 '찾거나 만들거나(find-or-create)' 과정을 인터닝(interning) 이라고 불러요.

이 때문에 두 가지가 성립해요.

  • unmap되지 않는 한 Var 객체는 안정적인 참조라서 매번 다시 찾을 필요가 없어요.
  • 네임스페이스는 일종의 전역 환경을 이루는데, 컴파일러는 그 안에서 모든 자유 심볼(free symbol)을 Var로 해석하려 해요.

Var 객체 자체를 (현재 값 대신) 얻고 싶다면, var 특별 폼이나 #' 리더 매크로를 쓸 수 있어요.

인턴되지 않은 Var(Non-interned Vars)

with-local-vars를 쓰면 인턴되지 않은 Var를 만들 수도 있어요. 이런 Var는 자유 심볼 해석 중에 발견되지 않고, 값을 수동으로 접근해야 해요. 대신 유용한 스레드 로컬 가변 셀(thread-local mutable cell) 로 쓸 수 있죠.

Var 메타데이터

def, defn, defmacro 같은 Var를 만드는 폼들은 표준적인 Var 메타데이터 집합을 사용해요. 일부 폼은 메타데이터에 저장된 값을 받기 위해 명시적 구문을 쓰기도 하지만, 일반적으로 Var 심볼에 맵으로 메타데이터를 제공할 수 있어요.

흔히 쓰는 Var 메타데이터 키(모두 Var 정의 시 선택 사항)를 정리할게요.

  • :doc — Var를 설명하는 문자열. 보통 docstring 파라미터로 설정돼요.
  • :added — 이 Var가 추가된 버전을 기록하는 문자열.
  • :private — 불리언 플래그. 보통 defn-으로 설정되고, 이 Var가 구현 세부 사항이라는 의도를 나타내요. private Var는 전역적으로 접근 가능하지만, non-private Var만 걸러내는 ns-... 함수들에서는 참조되거나 나열되지 않아요.
  • :arglists — arglist들의 컬렉션. 제공되지 않으면 자동으로 생성되고, 주로 매크로 구문을 문서화할 때 써요.
  • :macrodefmacro가 자동으로 추가하는 불리언 플래그 (보통 직접 쓰지 않아요).
  • :tag — Var에 담긴 값의 타입이나 함수의 반환 타입을 나타내는 타입 식별자(보통 클래스). 주의할 점은 Var 메타데이터는 평가(evaluate)된다는 거예요. 그래서 ^long 같은 타입 힌트는 원시 long 타입 힌트가 아니라 long 함수로 평가돼요. 일반적으로는 defn Var의 arglist에 타입 힌트를 다는 게 더 권장돼요.
  • :testclojure.test 프레임워크가 단위 테스트를 Var에 붙일 때 쓰는 키 (보통 직접 쓰지 않아요).
  • :dynamic — Var가 스레드 컨텍스트에서 동적으로 재바인딩될 수 있음을 나타내요. 다이렉트 링킹(direct linking)으로 컴파일할 때 동적 Var는 다이렉트 링크되지 않아요.
  • :redef — 다이렉트 링킹으로 컴파일할 때 이 Var를 다이렉트 링크하지 말라는 표시 (그래서 재정의를 허용해요).
  • :static — 이제는 쓰이지 않아요. (원래는 Var가 기본적으로 동적이었는데, 지금은 기본이 정적이에요.)
  • :const — Var가 컴파일 타임 상수임을 나타내는 키. 컴파일러는 이 값을 쓰는 코드에 인라인할 수 있어요. 이건 거의 필요하지 않고, (평가되지 않고 읽히는) 숫자·문자열 같은 상수에서만 동작해요. 클래스, 함수, 참조 타입(ref types) 등은 안 돼요. const Var를 재정의하거나 동적으로 바인딩해도, 이미 컴파일되고 로드된 런타임의 소비 코드에는 영향이 없어요.

다이렉트 링킹과 컴파일 중 메타데이터 생략(elision)에 대한 더 자세한 내용은 Compiler Options 문서도 참고해요.

더 알아보기

  • 관련 문서: Libs, Refs and Transactions, Namespaces, Evaluation, Reader, Special Forms, Metadata, Java Interop
  • def의 변형들: defn, defn-, definline, defmacro, defmethod, defmulti, defonce, defstruct
  • 인턴된 Var 다루기: declare, intern, binding, find-var, var
  • Var 객체 다루기: with-local-vars, var-get, var-set, alter-var-root, var?, with-redefs, with-redefs-fn
  • Var 검증자(validator): set-validator!, get-validator
  • Var 메타데이터 사용: doc, find-doc, test