cv.cm

코딩 · 프로그래밍 언어

초보자를 위한 Clojure 기초 튜토리얼

9분 분량

핵심 답변

Clojure는 Java 가상 머신(JVM)에서 실행되는 동적 함수형 Lisp입니다(JavaScript용으로는 ClojureScript가 있습니다). 코드는 괄호로 묶은 리스트로 작성하고, 데이터는 기본적으로 불변이며, 벡터, 맵, 집합에 작은 함수를 조합해 프로그램을 만듭니다. Clojure CLI를 설치하고 clj로 REPL을 시작하며, clojure -M hello.clj로 파일을 실행합니다.

정규식 테스트 열기

Clojure란 무엇이며 누가 쓰는가

Clojure는 Rich Hickey가 만들어 2007년에 처음 공개했습니다. Lisp 방언이며 모든 표현식이 첫 요소가 함수나 연산자인 리스트입니다. 예를 들면 (+ 1 2)입니다. JVM 바이트코드로 컴파일되므로 모든 Java 라이브러리를 직접 호출할 수 있습니다. ClojureScript는 같은 언어를 브라우저와 Node.js용 JavaScript로 컴파일합니다.

Clojure는 백엔드 웹 서비스, 데이터 처리 파이프라인, 사내 업무 시스템에 쓰이며, 특히 불변 데이터와 대화형 개발을 중시하는 팀에서 많이 씁니다. 개발자는 보통 편집기와 연결된 REPL(read-eval-print loop) 안에서 프로그램이 실행되는 동안 표현식을 하나씩 평가하며 작업합니다.

Clojure 설치와 Hello, world 실행

Java 런타임(JDK 17 이상이 무난합니다)을 설치한 다음 clojure.org에서 Clojure CLI 도구를 설치합니다(macOS는 Homebrew, Linux는 설치 스크립트, Windows는 설치 프로그램). CLI는 두 가지 명령을 제공합니다. clj(줄 편집이 되는 REPL)와 clojure(프로그램 실행용)입니다.

;; hello.clj
(println "Hello, world!")
clojure -M hello.clj
# Hello, world!
clj
# user=> (+ 1 2)
# 3

실제 프로젝트에서는 의존성을 deps.edn 파일에, 코드를 src/ 아래에 두며 파일 하나에 네임스페이스 하나를 둡니다. Leiningen(lein)은 project.clj 파일을 쓰는 더 오래되었지만 여전히 인기 있는 빌드 도구입니다.

값, 타입, 이름

Clojure는 동적 타입 언어입니다. def는 전역 이름(var)을 바인딩하고 let은 지역 이름을 바인딩합니다. 값은 생성된 뒤 바뀌지 않으며 대입문은 없습니다.

타입예시설명
Integer4264비트 long. 42N은 BigInt
Floating point3.14Java double
Ratio(/ 1 3)0.333이 아니라 정확한 분수 1/3
String"hello"Java String이며 큰따옴표만 사용
Character\a문자 하나
Keyword:name빠르고 자기 자신으로 평가되는 레이블로, 맵의 키로 자주 사용
Boolean / niltrue, false, nilfalse와 nil만 거짓
(def pi-approx 3.14159)
(let [r 2
      area (* pi-approx r r)]
  (println "Area:" area))
;; Area: 12.56636
(str "a" 1 :b)   ;; => "a1:b"

제어 흐름

Clojure에서는 if를 포함해 모든 것이 값을 반환하는 표현식입니다. if는 정확히 하나의 then과 하나의 else 형식을 받습니다. else 없이 부수 효과 여러 개를 실행하려면 when을, 여러 조건에는 cond를 사용하세요.

(defn grade [score]
  (cond
    (>= score 90) "A"
    (>= score 80) "B"
    :else         "C"))
(println (grade 85))            ;; B
(when (pos? 5) (println "positive") :done)
(doseq [x [1 2 3]] (println (* x x)))   ;; 1 4 9
(for [x (range 5) :when (odd? x)] (* x 10))   ;; => (10 30)
(loop [i 0 acc 0]
  (if (< i 5) (recur (inc i) (+ acc i)) acc))   ;; => 10

값을 바꾸는 카운터를 쓰는 for 반복문은 없습니다. doseq는 각 요소에 대해 부수 효과를 실행하고, for는 지연 시퀀스(리스트 컴프리헨션)를 만들며, loop/recur는 호출 스택을 늘리지 않는 반복을 표현합니다.

함수

이름 있는 함수는 defn으로 정의합니다. 함수는 여러 arity(인자 개수)를 가질 수 있으며, #(...)는 %를 인자로 쓰는 작은 익명 함수의 줄임 표기입니다.

(defn greet
  ([] (greet "world"))
  ([name] (str "Hello, " name "!")))
(greet)          ;; => "Hello, world!"
(greet "Ana")    ;; => "Hello, Ana!"
(defn sum-all [& nums] (reduce + nums))
(sum-all 1 2 3)  ;; => 6
(map #(* % %) [1 2 3])        ;; => (1 4 9)
(filter even? (range 10))     ;; => (0 2 4 6 8)

함수는 값입니다. map, filter, reduce에 넘기고, 다른 함수에서 반환하고, 맵에 저장할 수 있습니다.

컬렉션: 리스트, 벡터, 맵, 집합

Clojure의 핵심 컬렉션 네 가지는 불변이며 영속적입니다. 하나를 변경하면 기존 것과 구조 대부분을 공유하는 새 버전이 반환되므로 갱신 비용이 적습니다.

컬렉션리터럴주된 용도
List'(1 2 3)코드와, 앞쪽에 추가하는 순차 데이터
Vector[1 2 3]인덱스로 접근하는 시퀀스이며 끝에 추가
Map{:name "Ana" :age 30}키-값 레코드
Set#{:red :green}고유한 값, 포함 여부 검사
(def user {:name "Ana" :age 30})
(:name user)                   ;; => "Ana"
(assoc user :email "[email protected]")   ;; new map with :email
(update user :age inc)         ;; => {:name "Ana", :age 31}
(conj [1 2] 3)                 ;; => [1 2 3]
(contains? #{:red :green} :red) ;; => true
(get-in {:a {:b 1}} [:a :b])   ;; => 1
user                           ;; unchanged: {:name "Ana", :age 30}

대표 기능: 스레딩 매크로, 아톰, 매크로

스레딩 매크로를 쓰면 데이터 파이프라인을 위에서 아래로 읽을 수 있습니다. ->는 이전 결과를 다음 형식의 첫 번째 인자로, ->>는 마지막 인자로 넣습니다.

(->> (range 1 11)
     (filter odd?)
     (map #(* % %))
     (reduce +))   ;; => 165
(def counter (atom 0))
(swap! counter inc)
(swap! counter + 10)
@counter           ;; => 11
(defmacro unless [test & body]
  `(if ~test nil (do ~@body)))
(unless false (println "runs"))

아톰은 바뀔 수 있는 상태를 담습니다. swap!은 함수를 원자적으로 적용하므로 여러 스레드에서 안전하게 쓸 수 있습니다. ref와 agent는 다른 동시성 상황을 다룹니다. 코드가 데이터(리스트)이기 때문에 매크로가 컴파일 전에 코드를 변환할 수 있으며, when, cond, ->도 매크로입니다.

오류 처리

Clojure는 try, catch, finally로 JVM 예외를 사용합니다. 오류에 데이터를 붙이려면 ex-info를 던지고 ex-data로 데이터를 다시 읽습니다.

(defn parse-age [s]
  (let [n (Long/parseLong s)]
    (when (neg? n)
      (throw (ex-info "Age must not be negative" {:value n})))
    n))
(try
  (parse-age "-5")
  (catch clojure.lang.ExceptionInfo e
    (println (ex-message e) (ex-data e)))
  (catch NumberFormatException _
    (println "not a number")))
;; Age must not be negative {:value -5}

작은 완성 프로그램: 단어 빈도

(ns wordcount
  (:require [clojure.string :as str]))
(defn word-counts [text]
  (->> (str/split (str/lower-case text) #"\W+")
       (remove str/blank?)
       frequencies
       (sort-by val >)))
(doseq [[word n] (take 2 (word-counts "The cat and the hat and the bat"))]
  (println word n))
;; the 3
;; and 2

이 프로그램은 텍스트를 소문자로 바꾸고, 정규식 리터럴 #"\W+"로 단어가 아닌 문자 기준으로 나누고, frequencies(맵을 반환)로 각 단어를 센 다음, 항목을 횟수의 내림차순으로 정렬합니다.

다음 단계

  • clojure.org의 공식 가이드: Getting Started, Learn Clojure, deps.edn 가이드.
  • 리더 문법, 특수 형식, 자료구조에 대한 Clojure 레퍼런스 페이지.
  • REPL과 연결되는 편집기를 설정하세요(예: VS Code용 Calva, Emacs용 CIDER, IntelliJ용 Cursive).
  • 브라우저를 대상으로 한다면 ClojureScript를 사용해 보세요.

자주 묻는 질문

Clojure는 함수형 언어인가요?

네. Clojure는 순수 함수와 불변 데이터를 선호하지만 실용적이어서, 부수 효과를 허용하고 아톰, ref, agent로 관리되는 가변 상태도 제공합니다.

Clojure를 배우려면 Java를 알아야 하나요?

아니요. Java 없이도 언어를 배울 수 있지만 Java 런타임이 설치되어 있어야 하고, JVM을 조금 알면 스택 트레이스를 읽거나 Java 라이브러리를 쓸 때 도움이 됩니다.

Clojure에서 리스트와 벡터의 차이는 무엇인가요?

리스트 '(1 2 3)는 앞쪽에 요소를 추가하며 주로 코드에 쓰이고, 벡터 [1 2 3]는 끝에 추가하며 인덱스로 빠르게 접근할 수 있습니다. Clojure 프로그램의 데이터 대부분은 벡터와 맵에 저장됩니다.

Clojure는 왜 괄호를 그렇게 많이 쓰나요?

Clojure 코드는 Lisp 자료구조로 작성되므로 모든 호출이 리스트입니다. 이 균일한 문법 덕분에 매크로가 가능하고, 구조 편집을 지원하는 편집기를 쓰면 괄호를 쉽게 다룰 수 있습니다.

Clojure와 ClojureScript의 차이는 무엇인가요?

같은 언어가 다른 호스트에서 동작하는 것입니다. Clojure는 JVM에서 실행되고 ClojureScript는 JavaScript로 컴파일됩니다. 호스트 상호 운용은 다르지만 핵심 함수와 자료구조는 같습니다.

코딩 튜토리얼 전체