2P by GN⁺ | ★ favorite | 댓글 1개
  • Clojure 1.12.0은 Java 8 바이트코드를 유지하면서도, 이후 릴리스에서 최소 Java 호환성과 바이트코드 기준을 최신 Java LTS로 옮기기 전 마지막 Java 8 기준 릴리스로 예고됨
  • JDK 21의 가상 스레드 환경에서 lazy-seqdelaysynchronized 대신 락을 사용해, 블로킹 I/O가 실제 스레드를 고정하는 상황을 줄임
  • REPL에서는 JVM 재시작 없이 add-lib, add-libs, sync-deps로 라이브러리를 추가할 수 있지만, 이 기능은 개발 중 대화형 사용에 한정됨
  • Java 상호운용성이 넓어져 메서드 값, :param-tags, 배열 클래스 문법, 함수형 인터페이스 변환, Supplier, Java Stream 처리 함수가 추가됨
  • 성능과 호환성 측면에서는 PersistentVector spliterator, 효율적인 drop/파티션 처리, Var interning 정책 강화, CVE-2024-22871 수정, Java 직렬화 식별자 정리가 포함됨

Java 8 호환성, 보안, 직렬화 정리

  • Clojure 1.12.0 다운로드와 사용 정보는 Downloads 페이지에서 확인할 수 있음
  • Java 8 기준은 이번 릴리스에서도 유지됨
    • Clojure 1.12는 Clojure 1.10, 1.11과 동일하게 Java 8 바이트코드를 생성함
    • 이후 릴리스는 바이트코드와 최소 Java 호환성을 더 최신 Java LTS 릴리스로 옮길 예정임
  • JDK 21의 가상 스레드 고정 문제가 완화됨
    • 1.12 이전에는 lazy-seqdelay가 한 번만 실행되는 동작을 보장하기 위해 사용자 코드를 synchronized 블록 안에서 실행함
    • JDK 21 기준 synchronized는 아직 협력적 블로킹에 참여하지 않아, 해당 코드가 블로킹 I/O를 수행하면 실제 스레드를 고정할 수 있음
    • -Djdk.tracePinnedThreads=full 사용 시 JDK 21이 이 상황에 대한 경고를 낼 수 있음
    • 1.12에서는 lazy-seqdelaysynchronized 블록 대신 락을 사용함
  • 보안 수정으로 CVE-2024-22871이 반영됐으며, 관련 권고는 GHSA-vr64-r9qj-h27f에 있음
  • Java 직렬화 관련 클래스의 serialVersionUID가 명시적으로 설정됨
    • Clojure 데이터 타입은 Clojure 1.0부터 Java 직렬화 인터페이스를 구현해왔음
    • Java 직렬화는 클래스 이름, 타입 계층, 직렬화 필드를 바탕으로 생성되는 식별자가 역직렬화 시 일치해야 동작함
    • Clojure는 버전 간 직렬화 일관성을 보장하지 않지만, 필요 이상으로 호환성을 깨지 않도록 향후 제어력을 높이는 변경을 적용함
  • 의존성도 함께 갱신됨
    • spec.alpha는 0.5.238로 업데이트됨
    • core.specs.alpha는 0.4.74로 업데이트됨

REPL에서 라이브러리와 도구를 다루는 기능

  • 개발 중에는 JVM을 재시작하지 않고 라이브러리를 추가해야 하는 상황이 있음
    • 실험적 평가
    • 프로젝트에 알려진 의존성 추가
    • 특정 작업을 위한 라이브러리 추가
  • Clojure 1.12는 REPL 상태를 잃지 않고 라이브러리를 추가하는 새 함수들을 제공함
    • add-lib: 클래스패스에 없는 lib를 다운로드하고 classloader에 추가함
      • 이미 클래스패스에 있는 lib는 업데이트하지 않음
      • 좌표가 없으면 최신 Maven 버전 또는 git 저장소 이름을 추론할 수 있는 경우 최신 git 버전이나 태그를 사용함
    • add-libs: 여러 새 라이브러리와 버전을 함께 해석함
    • sync-deps: deps.edn에는 있지만 아직 클래스패스에 없는 lib에 대해 add-libs를 호출함
  • 이 함수들은 개발 중 REPL 사용만을 의도함
    • 프로덕션 코드를 빌드하고 유지하는 올바른 방식은 여전히 deps.edn 사용임
    • 세 함수는 모두 *repl*이 true로 바인딩되어 있는지 확인함
    • clojure.main/repl은 이 플래그를 자동으로 바인딩함
    • clojure.main REPL에서는 새 함수들이 user 네임스페이스에 자동으로 refer됨
    • 다른 REPL에서는 (require '[clojure.repl.deps :refer :all])가 필요할 수 있음
  • 라이브러리 해석과 다운로드는 tools.deps가 담당함
    • 개발 중 프로젝트 클래스패스에 tools.deps와 그 의존성을 넣지 않도록, Clojure CLI를 통해 함수를 별도 프로세스에서 호출하는 새 API도 추가됨
  • clojure.tools.deps.interop/invoke-tool은 도구 함수를 별도 프로세스에서 호출함
    • 도구의 클래스패스는 deps.edn에 정의됨
    • 도구 의존성을 프로젝트 클래스패스에 추가할 필요가 없음
    • add-lib 기능은 invoke-tool을 사용해 만들어졌으며, 사용자 도구를 대화형으로 빌드하거나 호출하는 데도 사용할 수 있음
    • 함수 실행 프로토콜은 CLI reference에서 확인 가능함

외부 프로세스 실행 API

  • 기존 clojure.java.shell 네임스페이스에 더해, Java의 새 프로세스 정보·제어·I/O 리다이렉션 API를 활용하는 경로가 생김
  • Clojure 1.12는 새 네임스페이스 clojure.java.process를 추가함
    • Java의 새 프로세스 관련 API를 활용함
    • 기존 방식보다 사용하기 쉽게 설계됨
  • 주요 함수는 다음과 같음
    • start: 스트림을 완전히 제어하고, 고급 사용을 위해 기반 Java 객체에 접근할 수 있음
    • exec: 외부 프로세스를 실행하고 완료 시 stdout을 반환하는 일반적인 경우를 다룸

Java 상호운용성 확장

  • 메서드 값이 추가되어 Java 메서드를 고차 함수에서 더 직접적으로 사용할 수 있음
    • 이전에는 Java 메서드를 map 등에 넘기려면 직접 함수로 감싸야 했음
    • 수동 래핑은 장황하고, 오버로드 구분을 위한 힌트가 필요하거나 부수적인 리플렉션·박싱이 생길 수 있었음
    • 이제 qualified methods를 값 위치에서 일반 함수처럼 사용할 수 있으며, 컴파일러가 래핑 함수를 자동 생성함
    • qualified method가 오버로드 때문에 해석되지 않으면 컴파일러는 리플렉션 호출을 생성함
    • 개발자는 :param-tags 메타데이터로 원하는 단일 메서드 시그니처를 지정할 수 있음
  • Qualified method 문법은 클래스와 메서드를 명시함
    • Classname/method: 정적 메서드를 호출하는 Clojure 함수 값
    • Classname/.method: 인스턴스 메서드를 호출하는 Clojure 함수 값
    • Classname/new: 생성자를 호출하는 Clojure 함수 값
    • 정적 메서드와 인스턴스 메서드를 구분하려면 Classname/methodClassname/.method 문법을 사용해야 함
  • :param-tags 메타데이터는 오버로드된 메서드 해석에 사용됨
    • 값으로 쓰이는 qualified method는 클래스와 메서드 이름만 제공하므로 오버로드 메서드를 해석할 수 없음
    • :param-tags[tag …] 형태의 벡터이며, 각 태그는 원하는 시그니처의 파라미터에 대응함
    • 오버로드되지 않은 타입의 파라미터에는 _ 플레이스홀더를 사용할 수 있음
    • :param-tags를 제공하면 컴파일러가 컴파일 시점에 단일 메서드로 해석할 수 있어야 함
    • 새 메타데이터 리더 문법 ^[tag …]는 멤버 심볼에 :param-tags 메타데이터를 붙임
  • 배열 클래스 문법이 추가됨
    • Clojure는 클래스 이름 심볼을 클래스 객체 값과 타입 힌트로 지원했지만, 문자열 외에는 배열 클래스 문법을 제공하지 않았음
    • 이제 ComponentClass/#dimensions 형태의 심볼로 배열 클래스를 참조할 수 있음
    • 예: String/1, java.lang.String/1, long/2
    • 컴포넌트 클래스는 완전한 클래스명, import된 클래스, primitive가 될 수 있음
    • 배열 클래스 문법은 타입 힌트와 값으로 모두 사용할 수 있음
  • Java 함수형 인터페이스 상호운용이 개선됨
    • Java 함수형 인터페이스는 @FunctionalInterface가 붙고 단일 메서드를 가짐
    • Clojure 함수는 arity가 맞으면 함수형 인터페이스를 받는 Java 메서드 호출에 전달될 수 있음
    • Clojure 컴파일러는 람다 어댑터를 만들어 Clojure 함수를 필요한 함수형 인터페이스로 암묵 변환함
    • 루프에서 반복적인 어댑터 생성을 피하려면 let 바인딩 이름에 힌트를 붙여 명시적으로 강제할 수 있음
  • Supplier 상호운용도 개선됨
    • 값을 공급하는 Supplier를 받는 메서드를 호출하려면 이전에는 reify로 어댑터를 작성해야 했음
    • Clojure의 IDeref 구현체인 delay, future, atom 등은 이제 Supplier 인터페이스를 직접 구현함

Stream 처리와 컬렉션 성능 개선

  • Java API가 점점 더 많이 반환하는 Stream을 Clojure 방식으로 소비하기 위한 함수들이 추가됨
    • Clojure 1.12의 함수형 인터페이스 지원과 함께 Stream 상호운용 함수가 제공됨
    • (stream-seq! stream) ⇒ seq
    • (stream-reduce! f [init-val] stream) ⇒ val
    • (stream-transduce! xf f [init-val] stream) ⇒ val
    • (stream-into! to-coll [xf] stream) ⇒ to-coll
    • 모든 함수는 터미널 스트림 연산이며 스트림을 소비함
  • PersistentVector는 Java 컬렉션의 stream 구현에 쓰이는 spliterator를 직접 제공함
    • spliterator는 더 빠른 병렬 순회를 위해 나눌 수 있는 iterator임
    • PersistentVector의 새 커스텀 spliterator는 병렬성을 지원하며 성능이 크게 개선됨
  • drop, nthrest, nthnext와 파티션 처리 효율이 개선됨
    • CLJ-2713은 컬렉션이 순차 순회보다 더 효율적으로 drop할 수 있음을 나타내는 내부 인터페이스 IDrop을 추가함
    • 이 인터페이스는 persistent collection과 range, repeat 같은 알고리듬 컬렉션에 구현됨
    • 새 함수 partitionv, partitionv-all, splitv-at은 기존 대응 함수보다 효율적이며, realized seq 파티션 대신 벡터 파티션을 생성함

Var interning 정책 강화

  • 네임스페이스에서 var를 interning하는 것은 aliasing과 달리, 모든 참조가 같은 객체를 얻도록 안정적 참조를 만드는 일임
  • 기존에는 interned var가 교체될 수 있는 일부 사례가 있었고, 1.12.0-alpha1에서 정책이 더 엄격해짐
    • 이런 상황이 발생하면 "REJECTED: attempt to replace interned var #'some-ns/foo with #'other-ns/foo in some-ns, you must ns-unmap first" 형태의 경고가 나타남
  • 이 정책은 Clojure 1.11.0에서 clojure.core에 새 함수, 특히 abs가 추가되며 드러난 문제의 근본 원인을 다룸
    • 이전 Clojure 버전에서 컴파일된 코드가 clojure.core에 새로 추가된 함수명과 같은 var 이름을 갖고 있으면, 1.11.0 런타임에서 로드될 때 unbound가 될 수 있었음
    • CLJ-2711 외에도 이 영역의 이전 수정인 CLJ-1604가 롤백됨

전체 변경 목록

댓글과 토론

Hacker News 의견들
  • 정말 대형 릴리스이고 멋진 새 기능이 많음
    개인적으로는 add-libs가 가장 마음에 듦. 이제 이슈 재현용 단일 파일 데모나 최소 예제를 만들 수 있어서, 실행 가능한 작은 코드 조각을 공유하는 장벽이 크게 낮아짐
    Java 보일러플레이트 없이 Java 라이브러리도 데모할 수 있음. REPL에서 이것저것 만져본 뒤 HN 댓글 같은 곳에 코드를 붙여 넣으면, 누구나 같은 “설정”을 그대로 재현해 실행할 수 있음. 저장소를 클론할 필요도 없음

    • Groovy를 기억하는 사람이 있는지 모르겠음. Groovy에는 설명한 add-libs와 사실상 같은 일을 하는 @Grab 어노테이션이 있고, 스크립트 작성에 매우 편리함
    • Java에도 이제 REPL과 스크립팅 지원이 있지만, add-libs 같은 기능은 아직 사용 가능한 메타 명령에 없음
  • Clojure/conj 2024까지 이 릴리스를 미룰 줄 알았음. 딱히 근거가 있던 건 아니지만, Clojure 1.10이 Clojure/conj 2021 즈음에 나왔고 Datomic 무료화 발표도 Clojure/conj 2023 초반에 있었기 때문임
    그래도 아직 spec2를 기다리는 중임. 지금은 spec의 경직성을 Malli로 우회하고 있지만, Clojure에서 일급 시민은 아님. 주로 매크로를 검사할 수 없기 때문이고, 이는 Clojure 컴파일러 설계상 의도된 부분임. 다만 Malli 스키마를 데이터로 조작하면 schema/select의 아이디어는 흉내 낼 수 있음
    함수형 인터페이스 변경 덕분에 이제는 (defmacro ->Consumer [f] ...) 같은 유틸리티 매크로를 유지할 필요 없이 함수를 바로 넘기면 됨

    • “maybe not”을 보고 schema/select가 정말 필요해져서 Malli용 라이브러리를 만들었음: https://github.com/eval/malli-select
    • “매크로를 검사할 수 없다”는 게 무슨 뜻인지 궁금함. spec으로 매크로에 s/fdef를 붙일 수 있고, 컴파일 시점에 호출을 검사함
      실제로 Clojure core도 spec으로 매크로 호출을 검사해서, 잘못 호출하면 스택 추적에서 그 흔적을 볼 때가 있음. 다른 의미로 말한 건지 궁금함
  • 새 기능이 잔뜩 들어왔는데도 기존 코드가 그대로 돌아가는 게 정말 좋음. 호환성 깨짐을 피하려는 꾸준한 노력이 빛남

  • Clojure를 더 알고 싶다면 10월 23~25일 버지니아 Alexandria에서 열리는 Clojure/conj 콘퍼런스를 보면 좋음: https://2024.clojure-conj.org

  • add-libssync-deps가 들어온 게 반가움. 이제 세션을 굳이 닫아야 할 이유가 거의 없거나 아예 없어 보임
    이번 릴리스는 이전 릴리스들과 범위가 꽤 달라 보이고, 들어간 내용이 많아서 흥미롭다. 다만 속도가 빨라진 탓에 몇 릴리스 뒤에 얽히고설킨 덩어리처럼 되지는 않았으면 함

    • Rich Hickey와 Clojure Team은 매우 신중한 설계자들이라, 너무 걱정하지 않아도 된다고 봄
    • 순전한 추측이지만, Rich Hickey가 nubank를 떠난 뒤 첫 릴리스라서 그가 더 많은 관심을 쏟을 수 있었을지도 모름
  • 함수형 인터페이스 변경은 엄청 큼. Clojure는 신중한 상호운용을 통해 Java와 가깝게 있을 때 가장 좋고, 이번 변경이 큰 빈틈 하나를 메워줌

  • spec은 어떻게 된 건지 궁금함. 버려진 건가? 기대할 만한 소식이 있는지 궁금함

    • spec은 계속 존재하고 사용 중임. 후속 작업도 많이 진행됐지만, 여러 사안에서 무엇을 할지 검토하는 동안 멈춰 있는 상태임
  • 꽤 탄탄한 릴리스로 보이고, Clojure가 여전히 잘 나가고 있어서 기쁨

    • 정말 잘 나가고 있는지 궁금함. 새 프로젝트에 쓸지 평가 중이고, Clara[0]와 함께 고려하고 있음. 다만 예전만큼 주류는 아닌 듯하고 생태계도 전보다 듬성듬성해진 느낌이 듦
      트롤링하려는 건 아님. 선택하고 싶고, 공학적으로 좋은 결정처럼 보임. 하지만 인기와 기여자가 급락 중이라면 가까운 미래에 발목을 잡을 수도 있음
      [0] https://www.clara-rules.org/
  • 기존 개발자를 Clojure 개발자로 전환시키기가 그 어느 때보다 쉬워졌음
    처음에 겪는 큰 문제[0]는 코드를 읽는 것인데, ChatGPT나 Claude 같은 AI가 기존 Clojure 코드를 설명하는 데 매우 뛰어남. 그 결과 개발자 온보딩이 훨씬 빨라질 수 있음
    [0] 몇 주 지나면 Clojure 읽기가 자연스러워지고, 예전에는 읽지 못했다는 사실조차 잊게 됨

  • 멋진 개선이 많음. 보통 손이 가는 주된 Lisp 계열 언어