데이터타입: deftype, defrecord, reify
데이터타입: deftype, defrecord, reify (Datatypes)
동기 (Motivation)
Clojure는 추상화 단위로 작성돼요. 시퀀스, 컬렉션, 호출 가능성(callability) 등에 대한 추상화가 있어요. 또한 Clojure는 이러한 추상화의 많은 구현을 공급해요. 추상화는 호스트 인터페이스로, 구현은 호스트 클래스로 명시돼요. 이것이 언어를 부트스트랩하기엔 충분했지만, Clojure에는 유사한 추상화와 저수준 구현 기능이 부족하게 남았어요. 프로토콜과 이 데이터타입 기능은 호스트 플랫폼의 기능에 비해 어떤 타협도 없이 추상화와 데이터 구조 정의를 위한 강력하고 유연한 메커니즘을 추가해요.
본문
기초 (Basics)
데이터타입 기능 — deftype, defrecord, reify — 은 추상화의 구현을 정의하는 메커니즘을 제공하고, reify의 경우 그 구현의 인스턴스들을 제공해요. 추상화 자체는 프로토콜이나 인터페이스로 정의돼요. 데이터타입은 호스트 타입(deftype와 defrecord는 이름이 있고, reify는 익명)을, 어떤 구조(deftype와 defrecord는 명시적 필드, reify는 암시적 클로저)와 함께, 그리고 추상화 메서드의 선택적인 인-타입 구현과 함께 제공해요. 상대적으로 깨끗한 방식으로, 호스트의 최고 성능 원시 표현과 다형성 메커니즘에 대한 접근을 지원해요. 주의할 점은, 이것들이 단지 괄호 안의 호스트 구성소가 아니라는 거예요. 호스트 기능의 제한된 부분집합만 지원하며, 종종 호스트 자체보다 더 많은 동적성을 가져요. 의도는, 상호운용이 제한된 범위를 넘어서도록 강제하지 않는 한, 플랫폼에서 가능한 최고 성능 데이터 구조를 얻기 위해 Clojure를 떠날 필요가 없다는 거예요.
deftype와 defrecord
deftype와 defrecord는 주어진 필드 집합과, 선택적으로 하나 이상의 프로토콜 및/또는 인터페이스에 대한 메서드를 가진 이름 붙은 클래스에 대해 컴파일된 바이트코드를 동적으로 생성해요. 동적이고 인터랙티브한 개발에 적합하고, AOT 컴파일할 필요가 없으며, 단일 세션 중에 다시 평가할 수 있어요. 이름 붙은 필드를 가진 데이터 구조를 생성한다는 점에서 defstruct와 유사하지만, 다음과 같은 점에서 defstruct와 달라요:
- 주어진 이름들에 대응하는 필드를 가진 고유한 클래스를 생성해요.
- 결과 클래스는 struct의 타입을 메타데이터로 인코딩하는 관례와 달리 적절한 타입을 가져요
- 이름 붙은 클래스를 생성하므로 접근 가능한 생성자가 있어요
- 필드는 타입 힌트를 가질 수 있고, 원시(primitive)일 수 있어요 ** 현재 원시가 아닌 타입의 타입 힌트는 필드 타입이나 생성자 인자를 제약하는 데 사용되지 않지만, 클래스 메서드에서의 사용을 최적화하는 데는 사용된다는 점에 주의하세요 ** 필드 타입과 생성자 인자를 제약하는 것은 계획 중이에요
- deftype/defrecord는 하나 이상의 프로토콜 및/또는 인터페이스를 구현할 수 있어요
- deftype/defrecord는 특수 리더 문법 #my.thing[1 2 3]으로 작성될 수 있는데, 여기서: ** 벡터 폼의 각 요소는 평가되지 않은 채로 deftype/defrecord의 생성자에 전달돼요 ** deftype/defrecord 이름은 완전히 한정되어야 해요 ** 1.3 이후 버전의 Clojure에서만 사용 가능해요
- deftype/defrecord Foo가 정의되면 대응 함수
pass:[->Foo]가 정의돼, 그 인자들을 생성자에 전달해요(1.3 이상 버전만)
deftype와 defrecord는 다음 방식으로 달라요:
- deftype는 생성자 외에 사용자가 지정하지 않은 기능을 제공하지 않아요
- defrecord는 영속 맵의 완전한 구현을 제공해요. 포함: ** 값 기반 동등성과 hashCode ** 메타데이터 지원 ** 연관(associative) 지원 ** 필드에 대한 키워드 접근자 ** 확장 가능한 필드(defrecord 정의에 제공되지 않은 키도 assoc할 수 있어요) ** 등
- deftype는 변경 가능한(mutable) 필드를 지원하지만 defrecord는 지원하지 않아요
- defrecord는 #my.record{:a 1, :b 2}의 추가 리더 폼을 지원하는데, defrecord를 초기화하는 맵을 받아요: ** defrecord 이름은 완전히 한정되어야 해요 ** 맵의 요소들은 평가되지 않아요 ** 기존 defrecord 필드는 키가 있는 값을 가져요 ** 리터럴 맵에 키가 있는 값이 없으면 defrecord 필드는 nil로 초기화돼요 ** 추가 키 값이 허용되고 defrecord에 추가돼요 ** 1.3 이후 버전의 Clojure에서만 사용 가능해요
- defrecord Bar가 정의되면 대응 함수
pass:[map->Bar]가 정의돼, 맵을 받아 그 내용으로 새 레코드 인스턴스를 초기화해요(1.3 이상 버전만)
왜 deftype과 defrecord를 둘 다 가질까? (Why have both deftype and defrecord?)
대부분의 OO 프로그램에서 클래스는 두 가지 별개의 범주로 나뉜다는 결론이 나요: 구현/프로그래밍 도메인의 산물인 클래스(예: String이나 컬렉션 클래스, 또는 Clojure의 레퍼런스 타입), 그리고 애플리케이션 도메인 정보를 나타내는 클래스(예: Employee, PurchaseOrder 등). 애플리케이션 도메인 정보에 클래스를 사용하는 것은, 그 결과 정보가 클래스 특유의 마이크로 언어 뒤에 숨겨진다는 불행한 특성이 항상 있었어요. 겉보기에 무해한 employee.getName()조차도 데이터에 대한 사용자 지정 인터페이스인 셈이죠. 정보를 그런 클래스에 넣는 것은 문제예요, 마치 모든 책이 서로 다른 언어로 쓰여진다면 문제인 것과 같아요. 더 이상 정보 처리에 일반적인(generic) 접근을 취할 수 없어요. 이는 불필요한 특수성의 폭발과 재사용의 부족을 가져와요.
이것이 Clojure가 항상 그런 정보를 맵에 넣도록 권장해 온 이유이고, 그 조언은 데이터타입에서도 변하지 않아요. defrecord를 사용하면 일반적으로 조작 가능한 정보에, 타입 주도 다형성의 추가 이점과, 필드의 구조적 효율성을 얻을 수 있어요. 반면에 vector 같은 컬렉션을 정의하는 데이터타입이 map의 기본 구현을 갖는 것은 말이 안 되므로, deftype은 그런 프로그래밍 구성소를 정의하는 데 적합해요.
전반적으로 레코드는 정보를 담는 모든 목적에서 structmap보다 나을 것이고, 그런 structmap을 defrecord로 옮겨야 해요. structmap을 프로그래밍 구성소로 사용하려던 코드는 거의 없을 것이지만, 그렇다면 deftype이 훨씬 더 적합하다는 것을 알게 될 거예요.
AOT 컴파일된 deftype/defrecord는 그 제약이 금지적이지 않은 gen-class의 일부 사용 사례에 적합할 수 있어요. 그런 경우에는 gen-class보다 더 나은 성능을 가질 거예요.
데이터타입과 프로토콜은 독선적이다 (Datatypes and protocols are opinionated)
데이터타입과 프로토콜이 호스트 구성소와 잘 정의된 관계를 가지고 있고, Clojure 기능을 Java 프로그램에 노출하는 훌륭한 방법이기는 하지만, 그것들은 주로 상호운용 구성소는 아니에요. 즉, 호스트의 모든 OO 메커니즘을 완전히 모방하거나 적응시키려 하지 않아요. 특히 다음의 독선(opinion)을 반영해요:
- 구체적 파생(concrete derivation)은 나쁘다 ** 데이터타입을 구체 클래스에서 파생시킬 수 없고, 인터페이스에서만 가능해요
- 항상 프로토콜이나 인터페이스로 프로그래밍해야 한다 ** 데이터타입은 그것들의 프로토콜이나 인터페이스에 없는 메서드를 노출할 수 없어요
- 불변성(immutability)이 기본이어야 한다 ** 그리고 레코드에게는 유일한 선택이에요
- 정보의 캡슐화는 어리석다 ** 필드는 public이고, 의존성을 피하려면 프로토콜/인터페이스를 사용하세요
- 다형성을 상속에 묶는 것은 나쁘다 ** 프로토콜이 그것으로부터 당신을 해방시켜요
데이터타입과 프로토콜을 사용하면 Java 소비자에게 제공할 깨끗하고 인터페이스 기반의 API를 갖게 될 거예요. 깨끗하고 인터페이스 기반의 Java API를 다룬다면, 데이터타입과 프로토콜을 사용해 상호운용하고 확장할 수 있어요. '나쁜' Java API를 다룬다면, gen-class를 사용해야 해요. 오직 이렇게 해야만 Clojure 프로그램을 설계·구현하는 데 쓰는 프로그래밍 구성소들이 OO의 우발적 복잡성에서 자유로울 수 있어요.
reify
deftype과 defrecord가 이름 붙은 타입을 정의하는 반면, reify는 익명 타입을 정의하고 그 타입의 인스턴스를 만든답니다. 사용 사례는 하나 이상의 프로토콜이나 인터페이스의 일회성(one-off) 구현이 필요하고 지역 컨텍스트를 활용하고 싶을 때예요. 이 점에서 사용 사례는 Java의 proxy나 익명 내부 클래스와 유사해요.
reify의 메서드 본문은 어휘적 클로저이며, 둘러싼 지역 범위를 참조할 수 있어요. reify는 proxy와 다음 점에서 달라요:
- 프로토콜이나 인터페이스만 지원되고, 구체적 슈퍼클래스는 없어요.
- 메서드 본문은 결과 클래스의 진짜 메서드이지, 외부 fn이 아니에요.
- 인스턴스의 메서드 호출은 직접적이며, 맵 조회를 사용하지 않아요.
- 메서드 맵에서 메서드를 동적으로 교체하는 것을 지원하지 않아요.
결과는 proxy보다 더 나은 성능이며, 구성과 호출 모두에서 그래요. reify는 제약이 금지적이지 않은 모든 경우에 proxy보다 선호돼요.
Java 애노테이션 지원 (Java annotation support)
deftype, defrecord, definterface로 만든 타입은 Java 상호운용을 위한 Java 애노테이션을 포함하는 클래스를 발산(emit)할 수 있어요. 애노테이션은 다음에 대한 메타로 서술돼요:
- 타입 이름(deftype/record/interface) - 클래스 애노테이션
- 필드 이름(deftype/record) - 필드 애노테이션
- 메서드 이름(deftype/record) - 메서드 애노테이션
예시:
(import [java.lang.annotation Retention RetentionPolicy Target ElementType]
[javax.xml.ws WebServiceRef WebServiceRefs])
(definterface Foo (foo []))
;; annotation on type
(deftype ^{Deprecated true
Retention RetentionPolicy/RUNTIME
javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
javax.xml.ws.soap.Addressing {:enabled false :required true}
WebServiceRefs [(WebServiceRef {:name "fred" :type String})
(WebServiceRef {:name "ethel" :mappedName "lucy"})]}
Bar [^int a
;; on field
^{:tag int
Deprecated true
Retention RetentionPolicy/RUNTIME
javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
javax.xml.ws.soap.Addressing {:enabled false :required true}
WebServiceRefs [(WebServiceRef {:name "fred" :type String})
(WebServiceRef {:name "ethel" :mappedName "lucy"})]}
b]
;; on method
Foo (^{Deprecated true
Retention RetentionPolicy/RUNTIME
javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]
javax.xml.ws.soap.Addressing {:enabled false :required true}
WebServiceRefs [(WebServiceRef {:name "fred" :type String})
(WebServiceRef {:name "ethel" :mappedName "lucy"})]}
foo [this] 42))
(seq (.getAnnotations Bar))
(seq (.getAnnotations (.getField Bar "b")))
(seq (.getAnnotations (.getMethod Bar "foo" nil)))