Atoms
Atoms (원자)
여러 스레드가 같은 가변값을 안전하게 공유하면서도, 동기적으로 즉시 반영되는 상태가 필요할 때가 있어요. Clojure의 Atoms(원자)는 그러한 공유되고, 동기적이며, 독립적인 상태를 관리하는 방법이에요. Refs나 Vars처럼 레퍼런스 타입(reference type)의 하나이죠. 간단한 카운터, 캐시, 플래그 같은 '다른 것과 조율할 필요가 없는' 상태를 다룰 때 자연스럽게 쓰여요.
출처: Clojure 공식문서
본문
기본 사용법
Atoms는 atom으로 만들고, deref/@로 그 값을 읽어요. Refs나 Agents처럼 Atoms도 검증자(validator)를 지원하고요. 값을 바꿀 때는 swap!을 쓰고, 더 저수준의 compare-and-set!도 제공돼요. Atoms에 가해지는 변경은 언제나 경쟁 상태(race condition)에서 자유로워요.
무슨 일이 벌어지나요
다른 레퍼런스 타입과 마찬가지로, Atoms는 Clojure의 불변 데이터 구조 하나를 담는 용도로 설계됐어요. 그리고 Refs의 alter나 Agents의 send처럼, 값을 바꾸는 방법도 같아요. 예전 값에 함수 하나를 적용해 새 값을 만드는 거죠.
이 적용은 swap!에 의해 원자적으로 일어나요. 내부적으로 swap!는 현재 값을 읽고, 함수를 적용하고, 그 결과를 compare-and-set!로 넣으려고 해요. 그 사이 다른 스레드가 값을 바꿨을 수도 있으니, 필요하면 스핀 루프(spin loop)로 재시도하죠. 그 결과값은 항상 '어떤 시점의 현재 값에 함수를 적용한 결과'가 원자적으로 반영되어요. 다만 함수가 여러 번 호출될 수 있으므로, swap!에 넘기는 함수는 부수 효과(side effect)가 없어야 해요.
언제 쓰나요
Atoms는 다른 어떤 상태와도 조율할 필요가 없고, 동기적으로 즉시 바꾸고 싶은 상태를 나타내는 효율적인 방법이에요. (비슷하게 독립적이지만 비동기적으로 바꾸는 Agents와 대비되죠.) 전형적인 용례가 바로 메모이제이션(memoization)이에요.
memoize를 Atoms로 직접 구현해 볼게요. 함수가 받은 인자들을 assoc해서 mem이라는 map에 저장해 두는 방식이에요.
(defn memoize [f]
(let [mem (atom {})]
(fn [& args]
(if-let [e (find @mem args)]
(val e)
(let [ret (apply f args)]
(swap! mem assoc args ret)
ret)))))
먼저 순수 재귀로 피보나치를 만들고, 그 앞에 memoize를 입혀 보죠.
(defn fib [n]
(if (<= n 1)
n
(+ (fib (dec n)) (fib (- n 2)))))
(time (fib 35))
user=> "Elapsed time: 941.445 msecs"
(def fib (memoize fib))
(time (fib 35))
user=> "Elapsed time: 0.044 msecs"
swap! 덕분에 여러 스레드가 동시에 같은 mem에 값을 더해도 안전해요. 같은 인자로 다시 호출되면 계산 없이 캐시된 값을 바로 돌려주죠. 그래서 두 번째 (fib 35)는 941ms에서 0.044ms로 확 줄어든 걸 볼 수 있어요. 그 대신 swap!가 함수를 여러 번 부를 수 있으니, 여기처럼 순수한 함수만 넘기는 게 핵심이에요.
관련 함수
- Atoms 생성: atom
- Atoms 조회: deref (함께 보기:
@리더 매크로) - Atoms 상태 변경: swap!, reset!, swap-vals!, reset-vals!
- 검증자: set-validator!, get-validator
- 감시자(watcher): add-watch, remove-watch
더 알아보기
- Refs와 트랜잭션 — 동기적·조율이 필요한 공유 상태 (STM)
- Vars와 환경 (Vars and Environments) — 스레드 격리를 통한 가변 저장
- Agents — 비동기적 상태 변경