- Clojure 1.12.0은 Java 8 바이트코드를 유지하면서도, 이후 릴리스에서 최소 Java 호환성과 바이트코드 기준을 최신 Java LTS로 옮기기 전 마지막 Java 8 기준 릴리스로 예고됨
- JDK 21의 가상 스레드 환경에서
lazy-seq와delay가synchronized대신 락을 사용해, 블로킹 I/O가 실제 스레드를 고정하는 상황을 줄임 - REPL에서는 JVM 재시작 없이
add-lib,add-libs,sync-deps로 라이브러리를 추가할 수 있지만, 이 기능은 개발 중 대화형 사용에 한정됨 - Java 상호운용성이 넓어져 메서드 값,
:param-tags, 배열 클래스 문법, 함수형 인터페이스 변환,Supplier, JavaStream처리 함수가 추가됨 - 성능과 호환성 측면에서는
PersistentVectorspliterator, 효율적인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-seq와delay가 한 번만 실행되는 동작을 보장하기 위해 사용자 코드를synchronized블록 안에서 실행함 - JDK 21 기준
synchronized는 아직 협력적 블로킹에 참여하지 않아, 해당 코드가 블로킹 I/O를 수행하면 실제 스레드를 고정할 수 있음 -Djdk.tracePinnedThreads=full사용 시 JDK 21이 이 상황에 대한 경고를 낼 수 있음- 1.12에서는
lazy-seq와delay가synchronized블록 대신 락을 사용함
- 1.12 이전에는
- 보안 수정으로 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 상태를 잃지 않고 라이브러리를 추가하는 새 함수들을 제공함
- 이 함수들은 개발 중 REPL 사용만을 의도함
- 프로덕션 코드를 빌드하고 유지하는 올바른 방식은 여전히
deps.edn사용임 - 세 함수는 모두
*repl*이 true로 바인딩되어 있는지 확인함 clojure.main/repl은 이 플래그를 자동으로 바인딩함clojure.mainREPL에서는 새 함수들이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를 활용함
- 기존 방식보다 사용하기 쉽게 설계됨
- 주요 함수는 다음과 같음
Java 상호운용성 확장
- 메서드 값이 추가되어 Java 메서드를 고차 함수에서 더 직접적으로 사용할 수 있음
- 이전에는 Java 메서드를
map등에 넘기려면 직접 함수로 감싸야 했음 - 수동 래핑은 장황하고, 오버로드 구분을 위한 힌트가 필요하거나 부수적인 리플렉션·박싱이 생길 수 있었음
- 이제 qualified methods를 값 위치에서 일반 함수처럼 사용할 수 있으며, 컴파일러가 래핑 함수를 자동 생성함
- qualified method가 오버로드 때문에 해석되지 않으면 컴파일러는 리플렉션 호출을 생성함
- 개발자는
:param-tags메타데이터로 원하는 단일 메서드 시그니처를 지정할 수 있음
- 이전에는 Java 메서드를
- Qualified method 문법은 클래스와 메서드를 명시함
Classname/method: 정적 메서드를 호출하는 Clojure 함수 값Classname/.method: 인스턴스 메서드를 호출하는 Clojure 함수 값Classname/new: 생성자를 호출하는 Clojure 함수 값- 정적 메서드와 인스턴스 메서드를 구분하려면
Classname/method와Classname/.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바인딩 이름에 힌트를 붙여 명시적으로 강제할 수 있음
- Java 함수형 인터페이스는
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 파티션 대신 벡터 파티션을 생성함
- CLJ-2713은 컬렉션이 순차 순회보다 더 효율적으로 drop할 수 있음을 나타내는 내부 인터페이스
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 1.12.0의 전체 변경 목록은 공식 changelog에 있음