클로저가 다른 리습 계열 언어들(Common Lisp, Scheme, Racket, …​)에 비해 갖는 장점들 중의 하나는 풍부한 라이브러리들을 이용할 수 있다는 것이다. 리습 계열 언어가 자체적으로는 아무리 뛰어나다 하더라도 이용할 수 있는 라이브러리가 제한적이어서 그 활용 가능성은 현저히 떨어질 수 밖에 없었다. 하지만 클로저의 경우에는, 자바(더 정확히는, JVM 상)의 모든 방대한 라이브러리들을 그대로 아주 쉽게 가져다 쓸 수 있다는 점에서 다른 리습 계열 언어들과는 차별화된다.

이 장에서는 클로저에서 자바 코드를 어떻게 불러다 쓸 수 있는지 살펴본다.

1. 자바의 클래스, 메소드, 필드 사용하기

클로저는 자바 코드를 쉽게 호출할 수 있는 다양한 방법을 제공하는데, 다음의 표에 간결하게 정리했다.

Clojure forms Java equivalents
 클래스의 인스턴스 생성
 (ClassName.)
(ClassName. arg1 arg2 ...)
 new ClassName()
new ClassName(arg1, arg2, ...)
 객체의 인스턴스 메소드 호출
 (.methodName object)
(.methodName object arg1 arg2 ...)
 object.methodName()
object.methodName(arg1, arg2, ...)
 객체의 인스턴스 필드 읽기
 (.-field object)
(.field object)
 object.field
 객체의 인스턴스 필드 쓰기
 (set! (.-fieldName object) 5)
(set! (.fieldName object) 5)
 object.fieldName = 5
 정적 메소드 호출
 (ClassName/staticMethod)
(ClassName/staticMethod arg1 arg2 ...)
 ClassName.staticMethod()
ClassName.staticMethod(arg1, arg2, ...)
 정적 필드 읽기
 ClassName/FIELD
 ClassName.FIELD

위의 '객체의 인스턴스 필드 읽기' 항목에서 .field.-field의 두 가지 형태가 제공되는데, 가급적이면 .-field의 형태를 쓰기를 권한다. 그 이유는 (.field object)의 호출 형태가 객체의 인스턴스 메소드 호출 형태인 (.methodName object)와 겉모습이 동일해서, 실제 사용될 떄 인스턴스 필드 읽기인지 아니면 인스턴스 메소드 호출인지 구분이 가지 않기 때문이다. .- 형태는 클로저스크립트에서 먼저 도입되었는데 나중에 클로저에서도 이 표기법을 수용한 결과로 현재는 두 가지 표기법이 혼재한다.

위의 여러가지 Java Interp 형태는 일종의 syntactic sugar(구문 단축형)로, 내부적으로는 모두 클로저의 특수 형식(special form)인.new 두 개로 구현되어 있다. 이 구문 단축형은 특수 형식을 이용하는 것보다 짧고 편리하므로 굳이 특수 형식을 이용할 필요는 없겠지만, 참고로 이 두 형태를 서로 비교해 보면 아래와 같다.

 Clojure Syntactic Sugar
 Clojure Special Form
 클래스의 인스턴스 생성
 (ClassName.)
(ClassName. arg1 arg2 ...)
 (new ClassName)
(new ClassName arg1 arg2 ...)
 객체의 인스턴스 메소드 호출
 (.methodName object)
(.methodName object arg1 arg2 ...)
 (. object methodName)
(. object methodName arg1, arg2, ...)
 객체의 인스턴스 필드 읽기
 (.-field object)
(.field object)
 (. object field)
 객체의 인스턴스 필드 쓰기
 (set! (.-fieldName object) 5)
(set! (.fieldName object) 5)
 (set! (. object fieldName) 5)
 정적 메소드 호출
 (ClassName/staticMethod)
(ClassName/staticMethod arg1 arg2 ...)
 (. ClassName staticMethod)
(. ClassName staticMethod arg1 arg2 ...)
 정적 필드 읽기
 ClassName/FIELD
 (. ClassName FIELD)

1.1. 자바 클래스 import하기

자바 클래스를 사용할 때는, clojure.core/import 함수를 이용해 먼저 import해 주면, 짧은 클래스 이름만을 이용해 다음과 같이 간편하게 호출할 수 있다.

(import java.util.Date)

(Date.)   ;=> #inst "2015-12-07T08:07:00.231-00:00"

하지만 import 함수를 이용하지 않고, 다음과 같이 완전한 패키지명을 붙여 호출할 수도 있다.

(java.util.Date.)   ;=> #inst "2015-12-07T08:08:23.918-00:00"

자바 클래스 import와 관련된 자세한 내용은, Namespaces and Libraries: import 부분을 참조하기 바란다.

1.2. 인스턴스 생성 및 인스턴스 메소드 호출

자바 클래스의 인스턴스를 생성하려먼, 클래스 이름 바로 뒤에 .을 붙인다. 생성자의 인수들은 그 뒤에 나열하면 된다.

(import 'java.net.URL)

(def cnn (URL. "http://cnn.com"))

그리고 해당 인스턴스의 메소드를 호출하려면, 메소드 이름 바로 앞에 .을 붙이고, 그 뒤에 인스턴스 객체가 온다. 호출하려는 메소드에 추가로 인수가 있는 경우에는 이 인스턴스 객체 뒤에 차례로 나열해 준다.

(.getHost cnn)   ;=> "cnn.com"

(slurp cnn)
;=> "<html lang=\"en\"><head><title>CNN.com........."

1.3. 정적 필드 및 정적 메소드 호출

정적 필드는 클래스명 뒤에 슬래시(/) 기호를 붙인 후 필드명을 써 준다.

Double/MAX_VALUE   ;=> 1.7976931348623157E308

정적 메소드 역시 클래스명 뒤에 슬래시 기호를 붙인 후 메소드명을 써 준다. 정적 메소드의 인수들은 메소드명 뒤에 차례대로 나열해 준다.

(Double/parseDouble "3.141592653589793")   ;=> 3.141592653589793

1.4. 객체의 필드 읽고 쓰기

자바에서 필드를 public으로 노출하는 경우는 드물고, 그 값의 변경을 허용하는 예는 더욱 드물지만, 클로저에서는 객체의 public 필드를 읽고 쓰는 방법을 제공한다. 객체의 필드에 값을 설정할 때는 클로저의 특수 형식(special form)인 set!를 이용해야 한다.

(import 'java.awt.Point)

(def pt (Point. 5 10))

(.-x pt)   ;=> 5

(set! (.-x pt) -42)

(.-x pt)   ;=> -42

물론, 위에서 .-x 대신 .x를 써도 된다.

2. 유용한 Interop 관련 함수들

2.1. class

class 함수는 인수로 주어진 객체의 class를 반환한다.

(class "foo")   ;=> java.lang.String

2.2. instance?

instance? 함수는 두 번째 인수가 첫 번째 인수로 주어진 클래스의 인스턴스인지 여부를 반환한다.

(instance? String "foo")   ;=> true

2.3. ..

다음과 같은 연속 호출(chained calls) 형태의 자바 코드를, 클로저에서는 .. 매크로를 이용해 표현할 수 있다.

import java.util.Date;

Date date = new Date();

date.getTime().toString();
(import 'java.util.Date)

(.. (Date.) getTime toString)   ;=> "1449477417080"

호출하고자 하는 메소드가 인수를 필요로 하지 않으면 괄호로 둘러 싸지 않아도 된다. 즉, 다음의 두 코드는 같은 결과를 반환한다. 하지만, 코드의 간결성을 위해서는 괄호로 둘러싸지 않는 것이 바람직하다.

(.. "fooBAR" (toLowerCase) (contains "ooba"))   ;=> true

(.. "fooBAR" toLowerCase (contains "ooba"))     ;=> true

위의 코드는 -> 매크로를 통해서도 표현할 수 있다. 차이점은 -> 매크로를 사용할 떄에는, 자바 메소드 호출시 매번 .을 붙여 주어야 한다는 것이다. 그래서 -> 매크로는 자바 메소드와 클로저 함수를 혼합해 사용해마야만 하는 경우에 유용하다(참고로, .. 매크로 안에서는 클로저 함수를 호출할 수 없다). 개인적인 취향의 문제이겠지만, 본인은 자바 메소드 호출만으로 이루어진 경우에는 코드의 간결성을 위해 .. 매크로를, 그 이외의 경우에는 -> 매크로를 사용한다.

;; 자바 메소드만을 호출한 경우
(-> "fooBAR" .toLowerCase (.contains "ooba"))     ;=> true

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

;; 클로저 함수와 자바 메소드를 혼합해 호출한 경우
(-> "fooBAR" str/lower-case (.contains "ooba"))   ;=> true

2.4. doto

doto 매크로는 자바의 '동일한' 인스턴스 객체를 대상으로 여러 번의 설정 작업을 반복적으로 수행할 때 이용하면 편리하다.

예를 들면, 다음과 같은 자바 코드가 있을 때

ArrayList list = new ArrayList();

list.add(1);
list.add(2);
list.add(3);

이것을 클로저 코드로 변환하면 다음과 같다.

(import 'java.util.ArrayList)

(let [alist (ArrayList.)]
  (.add alist 1)
  (.add alist 2)
  (.add alist 3)
  alist)
;=> [1 2 3]

하지만 doto 매크로를 이용하면 다음과 같이 간결하게 표현할 수 있다. doto는 설정을 마친 인스턴스 객체를 반환한다.

(import 'java.util.ArrayList)

(doto (ArrayList.)
  (.add 1)
  (.add 2)
  (.add 3))
;=> [1 2 3]

예를 들어, 다음의 graphicsjava.awt.Graphics2D의 객체일 때, 다음과 같은 연속적인 작업을 doto 매크로를 이용해 수월하게 처리할 수 있다.

(doto graphics
  (.setBackground Color/white)
  (.setColor Color/black)
  (.scale 2 2)
  (.clearRect 0 0 500 500)
  (.drawRect 100 100 300 300))

3. Exceptions and Error Handling

클로저의 예외 처리는 자바의 예외 처리 방식을 그대로 이용한다. catch 절은 여러 개 나열될 수 있고, finally 절은 선택적으로 올 수 있다.

자바의 예외 처리
public static Integer asInt (String s) {
  try {
    return Integer.parseInt(s);
  } catch (NumberFormatException e) {
    e.printStackTrace();
    return null;
  } finally {
    System.out.println("Attempted to parse as integer: " + s);
  }
}
클로저의 예외 처리
(defn as-int
  [s]
  (try
   (Integer/parseInt s)
   (catch NumberFormatException e
     (.printStackTrace e))
   (finally
    (println "Attempted to parse as integer: " s))))
자바에서는 catchfinally 절이 try 절과 병렬로 배치되어 있는 반면에, 클로저에서는 catchfinally 절이 try 절의 내부에 속해 있다는 차이점에 주의하자.

예외를 던질 때에는 자바에서와 마찬가지로 throw를 이용한다. 이때 throw의 인수는 반드시 예외 클래스의 인스턴스이어야 한다.

(throw (IllegalStateException. "I don't know what to do!"))
;>> IllegalStateException I don't know what to do!

자바에서는 다음과 같이 메소드를 정의할 때, throws 뒤에 그 메소드가 던질 예외를 미리 선언할 수 있는데, 이런 예외를 checked exception[1]이라 부른다.

public static int parseInt(String s) throws NumberFormatException
{
   ...
}

자바에서는, 이와 같은 메소드를 '호출’하는 코드에서 try/catch/finally 구문을 통해 이 예외를 반드시 명시적으로 처리해 주어야 한다. 그렇지 않으면 컴파일러시 에러가 발생한다.

하지만 클로저에서는 그럴 필요가 없다. 그 이유는 checked exception을 강제하는 것은 자바 컴파일러이지 JVM 자체의 요구 사양은 아니기 때문이다. 클로저 소스 코드는 클로저의 자체 컴파일러가 직접 컴파일을 수행하므로 자바 컴파일러의 요구 사항을 무시할 수 있다.

4. Type Hinting for Performance

클로저에서는 ^ClassName 형식으로 type hinting 정보를 줄 수 있다.

(defn length-of
  [^String text]
  (.length text))

위와 같이 타입 힌팅 정보를 주면 ^{:tag String} text의 형식으로 text 인수의 metadata 맵의 :tag 키에 type 정보가 들어간다.

그런데 타입 힌팅 정보를 주더라도, Java Interop 호출을 하지 않으면 그 정보는 쓰이지 않고 컴파일러에 의해 무시된다.

(ns clj-prog.java-interop)

(defn accepts-anything-hint
  [^java.util.List x]
  x)

(defn accepts-anything-no-hint
  [x]
  x)

위의 두 함수를 컴파일 한 후에, 컴파일된 .class 파일을 다시 decompile해 보면, 다음과 같이 그 결과에 차이가 전혀 없다는 것을 확인할 수 있다.

accepts_anything_hint 함수의 decompile 결과
import clojure.lang.AFunction;

public final class java_interop$accepts_anything_hint extends AFunction {
    public java_interop$accepts_anything_hint() {
    }

    public static Object invokeStatic(Object x) {
        Object var10000 = x;
        x = null;
        return var10000;
    }

    public Object invoke(Object var1) {
        Object var10000 = var1;
        var1 = null;
        return invokeStatic(var10000);
    }
}
accepts_anything_no_hint 함수 decompile 결과
import clojure.lang.AFunction;

public final class java_interop$accepts_anything_no_hint extends AFunction {
    public java_interop$accepts_anything_no_hint() {
    }

    public static Object invokeStatic(Object x) {
        Object var10000 = x;
        x = null;
        return var10000;
    }

    public Object invoke(Object var1) {
        Object var10000 = var1;
        var1 = null;
        return invokeStatic(var10000);
    }
}

반면에 Java Interop 호출이 있는 경우에는, type hinting 정보가 있으면 컴파일시 그 정보가 반영되어, 런타임시 reflection으로 인한 실행 시간 지연을 막을 수 있다.

(defn length-of-hint
  [^String text]
  (.length text))

(defn length-of-no-hint
  [text]
  (.length text))
length_of_hint 함수 decompile 결과
import clojure.lang.AFunction;

public final class java_interop$length_of_hint extends AFunction {
    public java_interop$length_of_hint() {
    }

    public static Object invokeStatic(Object text) {
        Object var10000 = text;
        text = null;
        return Integer.valueOf(((String)var10000).length()); (1)
    }

    public Object invoke(Object var1) {
        Object var10000 = var1;
        var1 = null;
        return invokeStatic(var10000);
    }
}
1 (String)으로 타입 캐스팅되어 있다.
length_of_no_hint 함수 decompile 결과
import clojure.lang.AFunction;
import clojure.lang.Reflector;

public final class java_interop$length_of_no_hint extends AFunction {
    public java_interop$length_of_no_hint() {
    }

    public static Object invokeStatic(Object text) {
        Object var10000 = text;
        text = null;
        return Reflector.invokeNoArgInstanceMember(var10000, "length", false); (1)
    }

    public Object invoke(Object var1) {
        Object var10000 = var1;
        var1 = null;
        return invokeStatic(var10000);
    }
}
1 런타임에 reflection이 행해져 실행 시간의 지연을 초래하고 있다.

타입 힌팅 정보를 주면, 실행 속도를 향상시킬 수 있다.

(defn capitalize
  [s]
  (-> s
      (.charAt 0)
      Character/toUpperCase
      (str (.substring s 1))))

(time (doseq [s (repeat 100000 "foo")]
        (capitalize s)))
;>> "Elapsed time: 5040.218 msecs"

(defn fast-capitalize
  [^String s]
  (-> s
      (.charAt 0)
      Character/toUpperCase
      (str (.substring s 1))))

(time (doseq [s (repeat 100000 "foo")]
        (fast-capitalize s)))
;>> "Elapsed time: 154.889 msecs"

위의 실행 결과를 보면, 타입 힌팅 정보가 주어졌을 때 실행 시간이 단축되는 것을 확인할 수 있다. 하지만, 실행 속도를 향상시킬 수 있다고 해서 타입 힌팅 정보를 남발하는 것은 바람직하지 못하다. 프로파일링(profiling)을 실시해서 병목 지점을 확인한 후, 그 부분을 최적화할 때 타입 힌팅 정보를 주는 것이 바람직하다.

그런데 클로저 컴파일러가 코드의 어느 부분에서 reflection 기능을 호출하고 있는지 확인할 수 있으면 코드의 어느 부분에 타입 힌팅 정보를 주어야 할지 판단하는 데 도움이 될 것이다. 이런 경우에 *warn-on-reflection* 값을 true로 설정해 주면, 컴파일시 클로저 컴파일러가 코드의 어느 부분에서 reflection 기능을 호출하고 있는지 출력해 준다.

(set! *warn-on-reflection* true)

(defn capitalize
  [s]
  (-> s
      (.charAt 0)
      Character/toUpperCase
      (str (.substring s 1))))
;>> Reflection warning, NO_SOURCE_PATH:27 - call to charAt can't be resolved.
;>> Reflection warning, NO_SOURCE_PATH:29 - call to toUpperCase can't be resolved.
;>> Reflection warning, NO_SOURCE_PATH:29 - call to substring can't be resolved.

project.clj 파일에서 :warn-on-reflection true로 설정해도 같은 결과를 얻을 수 있다.

타입 힌팅 정보는 어느 식(expression)에나 붙일 수 있다. 다음과 같이 함수의 반환값에도 표시할 수 있다.

(defn split-name
  [user]
  (zipmap [:first :last]
          (.split ^String (:name user) " ")))

함수를 정의할 때 반환값에도 표시할 수 있다.

(defn file-extension
  ^String [^java.io.File f]   ; (1)
  (-> (re-seq #"\.(.+)" (.getName f))
      first
      second))

(.toUpperCase (file-extension (java.io.File. "image.png")))
1 ^String 부분이 함수의 반환값이 String형임을 표시한다.

var에도 표시할 수 있다.

(def a "image.png")

(java.io.File. a)
;>> Reflection warning, NO_SOURCE_PATH:1 - call to java.io.File ctor can't be resolved.

(def ^String a "image.png")

(java.io.File. a)
;=> #<File image.png>

5. Arrays

클로저에서는 자바의 기본(primitive) 자료형의 배열도 직접 다룰 수 있는 방법을 제공한다. 그래서 필요한 경우 처리 속도를 높이는 데 사용할 수 있다. 자세한 내용은 Numerics and Mathematics에서 다룬다.

Operation Clojure expression Java equivalent
 컬렉션으로부터 배열 생성하기
 (into-array ["a" "b" "c"])
 (String[]) coll.toArray(new String[list.size()]);
 빈 배열 생성하기
 (make-array Integer 10 100)
 new Integer[10][100]
 long형의 빈 배열 생성하기
 (long-array 10)
(make-array Long/TYPE 10)
 new long[10]
 배열의 값 읽기
 (aget some-array 4)
 some_array[4]
 배열에 값 쓰기
 (aset some-array 4 "foo")
(aset ^ints int-array 4 5)
 some_array[4] = 5.6

6. Defining Classes and Implementing Interfaces

proxy gen-class reify deftype defrecord

무명 클래스의 인스턴스 반환

O

O

이름 있는 클래스

O

O

O

부모 클래스 확장

O

O

implicit this

O

새 필드 정의

O

O

AOT compile

O

Object.equals, Object.hashcode 및 여러가지 클로저 인터페이스 default 제공

O

용도별 분류
  • reify, defrecord, deftype: Clojure 내부에서 사용

  • proxy, gen-class: Java Interop을 위해 사용

6.1. 무명 클래스의 인스턴스 만들기: proxy

reifyproxy 둘 다 무명 클래스의 인스턴스를 생성한다. 그리고 둘 다 top level form으로 쓰여서는 안된다. 이 무명 클래스는 컴파일할 때 한 번만 생성되고, reifyproxy를 감싸고 있는 함수가 호출될 때마다 이 무명 클래스의 인스턴스가 매번 생성된다.

  • reify는 자바의 인터페이스나 클로저의 프로토콜을 구현(implement)하고, 자바의 클래스들 중 오직 java.lang.Object 클래스만을 확장(extend)할 수 있다. 그런데 실제로 java.lang.Object 클래스를 확장하는 일은 거의 없으므로, 자바의 인터페이스나 클로저의 프로토콜을 구현(implement)할 때 주로 사용된다고 보면 된다.

  • proxyreify가 할 수 있는 일에 더해, 모든 자바의 클래스를 확장할 수 있다. 따라서 상위 클래스의 메소드를 재정의(overriding)할 필요가 있을 때 주로 사용된다. 하지만 상위 클래스에는 없는 새로운 메소드를 정의할 수는 없다. 이 일을 하려면 gen-class를 사용해야 한다.

상위 클래스를 subclassing할 일이 없으면, reify를 사용하는 것이 좋다.
proxy 형식
(proxy [super-class? interface*] [super-class-constructor-argument*]
  fun*)

super-class := 상속할 상위 클래스를 지정하며, 맨 처음에 와야 한다. 상위 클래스가
               지정되지 않으면 java.lang.Objcet를 상속하게 된다.
interface := 구현하고자 하는 자바의 인터페이스 또는 클로저의 프로토콜
super-class-constructor-argument := 상위 클래스 생성자의 인수
fun := 상위 클래스의 재정의하고자 하는 함수 또는 구현하고자 하는 인터페이스의 함수
       (name [params*] body) |
       (name ([params*] body) ([params+] body) ...)

fun은 closure를 형성할 수 있다.

다음은 java.lang.ObjecttoString 메소드를 재정의하고, 클로저의 프로토콜 clojure.lang.IDeref을 구현한 예이다.

(defn make-some-example
  [msg]
  (let [state (atom msg)]
    (proxy [clojure.lang.IDeref] []   ; (1)
      (toString [] @state)            ; (2)
      (deref [] state))))

(def o (make-some-example "Hello, there!"))

(.toString o)   ;=> "Hello, there!"

(reset! @o "Hi, everyone!")

(.toString o)   ;=> "Hi, everyone!"
1 clojure.lang.IDeref 프로토콜을 구현하므로, deref(또는 @)를 사용할 수 있다.
2 proxy 내부에서 지역 심볼 state를 closure로 참조할 수 있다.
proxy 내에서 overloading하는 함수들 안에는 (toString [this] …​)와 같이 this를 명시적으로 넣어줄 필요가 없다. 반면에, proxy를 제외한 다른 reify, defrecord, deftype, gen-class의 경우에는 모두 메소드의 첫 번째 인수에 this[2]를 명시적으로 넣어 주어야 한다.

6.2. 이름 있는 클래스(Named Classes) 정의하기: gen-class

proxy는 컴파일 타임에 컴파일한 후, 그 결과를 클래스 파일(*.class)에 저장하지는 않는다. 반면에 gen-class는 컴파일 타임에 클래스 파일을 디스크 상에 직접 생성해 준다[3]. 따라서 클로저로 작성된 함수를 자바 언어에서 직접 호출해 사용할 필요가 있을 때 유용하다. 예를 들어, 핵심 로직은 클로저로 작성하고 클래스 파일의 형태로 컴파일한 후 자바 프로그래머에게 건네주면, 그 자바 프로그래머는 클로저에 대한 지식이 없어도 원하는 작업을 수행할 수 있게 된다.

gen-class로 할 수 있는 일은 proxy가 할 수 있는 일에 더해 다음과 같은 일을 할 수 있다.

  • 상위 클래스의 protected 필드에 접근할 수 있다.

  • 여러 개의 생성자를 정의할 수 있다.

  • 상위 클래스에는 없는 새로운 정적(static) 메소드와 인스턴스 메소드를 정의할 수 있다.

  • static main 메소드를 정의할 수 있다.

하지만 gen-class는 가교 역할만을 담당하고 실제 일은 클로저 함수가 하게 된다는 점에서, 일반적인 자바 클래스와는 차이가 있다. 그리고 반드시 AOT(Ahead of Time) 컴파일을 수행한 후, 해당 클래스를 import해야만 그 효력이 발생한다는 점도 기억하자.

gen-class 형식
(gen-class
  :name class-name
  :prefix prefix
  :extends super-class
  :implements [interface+]
  :constructors {[param-type*] [super-param-type*] ...}
  :state state-name
  :init init-name
  :methods [[method-name [method-argument-type*] return-type]+]
  :main boolean
  ...)

class-name := 생성할 클래스명을 지정해 준다.
prefix := 메소드명 앞에 붙일 문자열 또는 심볼을 지정한다. default는 "-"이다.
super-class := 상속할 상위 클래스를 지정한다. 지정하지 않으면 java.lang.Object를 상속한다.
interface := 자바의 인터페이스 또는 클로저의 프로토콜
param-type := 이 클래스의 생성자 인수들의 타입을 지정한다.
super-param-type := 상속할 상위 클래스의 생성자 인수들의 타입을 지정한다.
state-name := public final 속성을 가진 멤버 변수 한 개만 지정 가능하다.
              여기에 atom을 지정하면 해당 값을 변경할 수 있다.
init-name := [[superclass-constructor-argument*] state]
             constructor가 호출될 때 이 함수를 호출한다.
:methods := [[method-name [arguemnt-type*] return-type]+]
             새로 추가하고자 하는 메소드를 선언한다.
:main := true이면 static public main 함수가 생성된다.

6.2.1. gen-class 이용 절차

먼저 간단한 예제로 시작해 보자.

  1. 다음 예에서는 :extends를 지정하지 않았으므로 java.lang.Object 클래스를 상속한다. 그리고 :prefix가 지정되지 않았으므로, 메소드 이름에는 default prefix인 "-"이 붙어야 한다.

    java-interop.example1.clj
    (ns java-interop.example1)
    
    (gen-class
      :name java_interop.example1.MyClass)
    
    (defn -toString
      [this]
      "Hello, World!")
  2. project.clj 파일의 :aot 옵션에 gen-class가 정의되어 있는 이름공간을 다음과 같이 추가해 준다.

    project.clj
    (defproject java-interop "0.1.0-SNAPSHOT"
      :dependencies [[org.clojure/clojure "1.7.0"]]
      :aot [java-interop.example1])
  3. 프로젝트의 루트 디렉토리에서 다음과 같이 컴파일해 준다.

    lein compile 실행
    $ lein compile
    Compiling java-interop.example1
  4. 그러면 target/classes/java_interop/example1 디렉토리 아래에 MyClass.class로 컴파일되어 있는 것을 확인할 수 있다.

  5. 다음은 이 클래스 파일을 다른 이름공간에서 읽어들여 실행한 결과이다. 여기에서는 클로저에서 읽어 들여 실행했지만, 실제로는 자바 언어로 읽어 들이게 될 것이다.

    java-interop.test1.clj
    (ns java-interop.test1
      (:import java_interop.example1.MyClass))
    
    (.toString (MyClass.))   ;=> "Hello, World!"

6.2.2. :prefix 옵션 지정하기

:prefix 옵션에 다음과 같이 자신이 원하는 prefix를 지정할 수도 있다. 그리고 하나의 이름 공간에 gen-class를 여러 번 정의할 수도 있다.

java_interop.example2.clj
(ns java-interop.example2)

(gen-class
  :name   java_interop.example1.MyClassA
  :prefix classA-)

(gen-class
  :name   java_interop.example1.MyClassB
  :prefix classB-)

(defn classA-toString
  [this]
  "I'm an A.")
java-interop.test2.clj
(ns java-interop.test2
  (:import (java_interop.example1 MyClassA MyClassB)))

(.toString (MyClassA.))   ;=> "I'm an A."
(.toString (MyClassB.))   ;=> "I'm an B."

6.2.3. 인터페이스 구현하기

gen-classns 안에서도 정의할 수 있다. 이 때 :name 옵션을 지정해 주지 않으면, ns 이름공간이 곧 클래스명이 된다.

java_interop.Example3.clj
(ns java-interop.Example3
  (:gen-class
   :implements [clojure.lang.IDeref]))

(defn -deref
  [this]
  "Hello, World!")
java-interop.test3.clj
(ns java-interop.test3
  (:import (java_interop.Example3)))

@(Example3.)   ;=> "Hello, World!"

6.2.4. 클래스 확장하기

:extends 옵션에 확장하고자 하는 상위 클래스를 지정할 수 있다. 앞에서 정의한 Example3 클래스를 그대로 상속해 보자.

java_interop.Example4.clj
(ns java-interop.Example4
  (:gen-class
   :extends java_interop.Example3))
java-interop.test4.clj
(ns java-interop.test4
  (:import java_interop.Example4))

@(Example4.)   ;=> "Hello, World!"

6.2.5. :state 옵션 지정하기

:constructors 옵션을 지정하게 되면 :init 옵션과 :state 옵션도 함께 지정해 주어야 한다. 이때 :state 옵션에는 public final 속성을 가진 멤버 변수 한 개만 지정 가능하지만, 여기에 atom을 지정하면 해당 값을 변경할 수도 있다.

java_interop.Example5.clj
(ns java-interop.Example5
  (:gen-class
   :implements   [clojure.lang.IDeref]
   :state        state
   :init         init
   :constructors {[String] []}))

(defn -init
  [message]
  [[] (atom message)])   ; (1)

(defn -deref
  [this]
  @(.state this))
1 -init 함수 안에서 :state 옵션에 지정된 state 필드를 (atom message)으로 초기화하고 있다. 따라서 이 필드의 값을 다음과 같이 변경할 수 있게 된다.
java-interop.test5.clj
(ns java-interop.test5
  (:import java_interop.Example5))

(def o (Example5. "Hello, there!"))

@o   ;=> "Hallo, there!"

(reset! (.state o) "Good morning, everybody!")

@o   ;=> "Good morning, everybody!"

6.2.6. 새로운 메소드 추가하기

gen-class는 상위 클래스에는 없는 정적 메소드나 인스턴스 메소드를 추가할 수 있다.

java_interop.Example6.clj
(ns java-interop.Example6
  (:gen-class
   :methods [^:static [greet [] String]   ; (1)
                      [greetMessage [String] String]]))

(defn -greet
  []
  "Hello, World!")

(defn -greetMessage
  [this msg]
  msg)
1 metadata ^:static를 추가하면 정적 메소드가 된다.
java-interop.test6.clj
(ns java-interop.test6
  (:import java_interop.Example6))

(Example6/greet)   ;=> "Hello, World!"

(.greetMessage (Example6.) "Good night!")   ;=>  "Good night!"

1. 컴파일 타임에 check하는 데서 이런 이름이 붙었다. 이에 대비되는 용어로 unchecked exception이 있는데, 이 예외들은 컴파일 타임에 check되지 않고 런 타임에 예외가 체크된다. 대부분의 예외는 unchecked exception이다.
2. 반드시 this일 필요는 없다.
3. project.clj 파일의 :aot 부분에 해당 이름 공간을 명시해 주어야 비로소 클래스 파일이 생성된다.