(번역) 클로저, 지금 바로 시작합시다! - 1부

저자와 freshcodeit의 허락을 받은 번역입니다. 원글과 차이가 있을 수 있습니다.

클로저는 리습(Lisp)을 기반으로 한 함수형 언어입니다. 번역한 글에서는 클로저의 탄생배경과 특징, 학습 방법이 구체적으로 잘 소개되어 있습니다. 그린랩스의 주 언어 중 하나인 클로저의 매력을 체험해보시죠!

1. 클로저의 역사

1.1 리치 히키

클로저는 리치 히키 (Rich Hickey)가 만들었습니다. 클로저 커뮤니티에선 이미 숭배받는 괴짜 프로그래머로, 뉴욕 대학에서 C++를 가르쳤고, 이후 현실 프로세스와 데이터를 정형화하는 다양한 시스템을 개발했습니다. 2005년 안식년을 가지며 개인 프로젝트를 시작했고, 2년 뒤에 클로저의 첫 번째 버전이 탄생했습니다. 오늘날 리치 히키와 그가 CTO로 있는 회사 코그니텍(Cognitect)은 클로저를 위한 상업적 지원을 제공하고 있습니다.

1.2 클로저를 쓰는 이유

리치 히키는 클로저를 만든 이유에 대해 다음과 같이 대답했습니다.

"왜 굳이 또 언어를 만들었냐고요? 저는 기본적으로

  • 함수형 프로그래밍을 위한
  • 리습(Lisp)이
  • 이미 갖춰진 플랫폼과 상생하면서
  • 동시성 문제를 잘 해결할 수 있기를

원했기 때문입니다."

더욱 자세한 스토리는 언어의 창시자가 직접 작성한 "클로저의 역사"라는 기사에서 확인할 수 있습니다.

클로저는 어쩌면 당신의 기대만큼 새롭고 독창적인 기능들이 많이 있지는 않을 것입니다. 당연히 자바와 비교하면 꽤나 실험적인 언어지만, 애초에 리치 히키는 이미 많이 검증된 아이디어와 컨셉을 기반으로 언어를 설계했습니다.

클로저는 리습의 방언이고, 람다 대수(Lambda calculus)는 일급, 고차함수를 다루는 리습의 핵심 개념입니다. 다른 말로, 함수가 값처럼 취급되어 인자로도, 리턴 값으로도 쓰일 수 있다는 얘깁니다.

리습은 '코드는 데이터다'라는 아이디어에서 출발했습니다. 리습의 기본 코드 단위는 아래와 같은 s-표현(symbolic expression)입니다.

(A (B 3) (C D) (()))

S-표현은 리스트와 값들로 구성되어 있는 리스트입니다. 특징으로는 전위 표현(prefix notation)을 사용하기 때문에 첫번째 인자는 함수 혹은 연산자이며, 그 뒤에 오는 값들은 인자로 취급됩니다. (+ 1 2 3) 은 + 연산자를 1 2 3 에 적용하게 되어 6 이라는 결과가 나옵니다.

리습으로 짠 프로그램은 그 자신의 추상 구문 트리(Abstract syntax tree)와 동일합니다. 이 특징은 다른 말로 동형성(homoiconicity)이라고 하는데, 이런 특징은 프로그램이 실행 중일 때도 쉽게 수정이 가능하게 하는 (modifying on the fly) 메타 프로그래밍의 새로운 장을 열어줍니다.

클로저는 리습을 현실 세계의 문제를 더 잘 다룰 수 있도록 혁신한 버전입니다. 예를 들어서, 리스트만 있던 리습에 비해 클로저는 맵, 벡터, 셋 과 같은 데이터 자료형이 추가되었습니다.

첫 릴리즈 후 얼마 지나지 않아, 리습에 매크로라는 개념이 추가됐습니다. 리습 프로그램을 해석하는 과정에 매크로 확장(macro-expansion)이 추가되어, 프로그래머들이 리습을 확장해 사용하는 것이 전보다 훨씬 용이해졌습니다. 하지만, 이러한 용이함은 동시에 발목을 잡기도 했습니다. (리습의 저주) 시간이 흐름에 따라 리습 커뮤니티는 마구 확장됨과 동시에 파편화되었고, 잉여스러운 구현들이 넘쳤으며, 통일된 표준을 찾기 힘들었습니다.

리습의 탄생 이후, 커먼 리습을 포함한 여러 방언이 나왔습니다. 하지만 현대 스크립트 언어인 파이썬이나 루비만큼의 인기를 얻진 못했죠. 그래서 클로저의 목표중 하나는 리습의 대중화였습니다. 이 목표는 JVM 플랫폼과 커뮤니티의 도움으로 잘 현실화 되고 있습니다.

1.3 통계자료

클로저 2020의 설문 조사에 따르면, 작년 한 해 기업용, 상업용 애플리케이션 개발에 점점 더 많이 사용되고 있음을 알 수 있습니다.

클로저는 꾸준히 새로운 사용자들을 유입하고 있습니다. 2020년에는 응답자 중 15.78%가 처음 사용하기 시작했다고 했습니다. 지금 클로저를 배우는 것이 시기적절하고 유망한 트렌드로 보입니다.

더 자세한 내용과 인사이트는 클로저에 관한 지난 기사에서 찾아보실 수 있습니다.

2. 클로저를 배우는 세 가지 단계

루스 올슨(Russ Olsen)가 쓴 "Clojure Applied"의 서문을 보면, 대부분의 사람들은 클로저를 배울 때 아래의 세 단계를 거칩니다.

첫번째 단계에선 기본적인 문법과 원칙을 학습합니다. 예를 들어 괄호와 꺾쇠 괄호는 언제 어떻게 사용하며 리스트와 벡터의 차이는 무엇인가 등입니다.

중간 단계에선 이제 조각을 맞춰가기 시작합니다. 불변 자료 구조를 적극적으로 사용하게 되고, 고차 함수를 실제 코드에 적용해보기도 합니다.

언어에 대한 이해를 마친 마지막 단계에선, 클로저 생태계를 탐험하기 시작합니다. 다른 사람들이 만든 방대한 라이브러리와 애플리케이션을 새로운 지식과 접목해 프로그램을 만들어나갑니다.

진짜 재미의 시작이죠!

2.1 클로저 기초: 시작해보기

스크립트성 태스크

저는 2013년 텔레콤 부문에서 자바EE 프로젝트를 하며 클로저를 처음 접했습니다. 당시의 메인 웹 애플리케이션은 이제 막 Java7으로 마이그레이션 된 상태였습니다. try-with-resources와 NIO는 있었지만 arrow 함수, 자바 스트림 API나 jshell 같은건 없었습니다.

자바가 꽤 장황한 프로그래밍 언어로 인식된다는건 더 이상 비밀이 아닙니다. IDE, 최신 자바 문법과 API가 이 장황함을 어느 정도 성공적으로 가려줄 수는 있지만 말이죠.

자바16이 현재 최신 버전임에도 불구하고, 젯브레인(JetBrain)의 조사에 따르면 75%의 자바 개발자들이 2020년에도 Java8을 주로 사용하고 있고, 뉴렐릭(New Relic)은 80%가 넘는 상용 애플리케이션이 Java8을 사용하고 있다고 합니다.

자바는 스크립트성 태스크엔 적합하진 않습니다. 이것이 셀레늄 테스트에서 클로저를 고려하게 된 이유 중 하나입니다. 다른 이유는

  • 안정적이며 JVM 친화적이다.
  • 코드 재사용성과 중복 방지의 측면에서 자바와 호환성이 좋다.
  • 재컴파일 필요 없이 REPL을 통해 인터랙티브 테스팅이 가능하다.
  • (자바나 스칼라보다) 문법이 간단해서 QA 엔지니어들이 테스트 케이스를 스스로 수정할 수 있다.

가장 마지막 포인트에 대해선 의견이 분분했습니다. 함수형 언어로의 패러다임 전환은 숙련된 엔지니어들에게도 어려울 것이라는 걱정도 있었습니다. 하지만 팀은 당시 열정으로 가득 차 있었고, 혁신을 수용하는 분위기 속에서 다소 위험을 감수하고 클로저를 시도해보기로 정했습니다.

가장 먼저, 웹사이트와 컨트롤 misc UI 컴포넌트를 탐색하기 위한 헬퍼함수들(helper functions)을 clj-webdriver를 사용해 개발했습니다. 그리고 실제 테스트 케이스들을 작성했습니다.

REPL의 장점은 이제 누구나 아는 것이지만, 당시엔 정말 큰 반향을 일으켰습니다. 테스트 케이스들이 콘솔에서 바로(on-the-fly), 컴파일이나 스크립트를 재시작 할 필요 없이 작성되었고, 이 모든 것들이 JVM 위에서 돌아갔습니다. 믿을 수 없었죠!

여기 실제 테스트 케이스의 예시입니다.

1(ns project.tests
2 (:use project.utils))
3
4(defcase standart-create-qos "Create QoS policy"
5 (select-main-menu "SLA management" "QoS policy")
6 (press-button "Create new QoS policy")
7 (input "Name" "test_qos_2")
8 (select-option "Service class" "Class G")
9 (select-option "Profile type" "test_profile")
10 (set-quality-rule "For 15 minutes" "packet loss rate" "in the forward direction" "is less than" "10")
11 (wait-response (press-button "Save"))
12 (check-row-exists "test_qos_2"))

클로저로 작성된 마지막 테스트 케이스들은 읽기 쉽고, 친숙해 보였으며.. 명령형이었습니다! DSL이 QA 엔지니어들이 작성한 케이스의 3/4를 커버했습니다. 다른 케이스들은 개발자들이 투입되어야 했는데, 주로 QA를 도와 헬퍼 함수들을 짜는 역할을 보조했습니다.

클로저를 막 시작한 단계에서는 언어의 최소화된 문법 체계의 덕을 많이 봤습니다. (https://learnxinyminutes.com/docs/clojure/) 괄호에 대한 초보자들의 두려움은 정말 과장된 거라는 생각이 들었어요.

메이저 에디터와 IDE들은 paredit (혹은 parinfer) 플러그인을 지원합니다. 괄호의 짝을 맞춰주거나 실수로 삭제하는 걸 막아주고, 구조 수정을 쉽게 해주는 단축키를 제공해주는 등, 정말 단비와 같은 플러그인입니다. 아래는 구조 수정(structural editing)의 예시입니다.

https://static.tildacdn.com/tild3265-3938-4033-a230-336433333766/clojure_code.gif

아래는 히컵 스타일(Hiccup-style)의 마크업입니다.

https://static.tildacdn.com/tild6661-3532-4265-b830-393034366431/hiccup_clojure.gif

자바 인터롭은 굉장히 직관적입니다. 몇 가지 문법과 함수 호출이 리스트의 첫번째에 있다는 것만 기억하면 간단합니다.

예를 들어 아래의 자바 코드는,

1public ByteArrayInputStream toInputStream (String s, String charset)
2 throws UnsupportedEncodingException {
3 return new ByteArrayInputStream(s.getBytes(charset));
4}

클로저에서는 아래와 같이 적힙니다.

1(defn string->stream [s charset]
2 (-> s
3 (.getBytes charset)
4 (ByteArrayInputStream.)))

또한 클로저는 대부분의 자바 라이브러리와 모듈에 대한 래퍼(wrapper)를 제공합니다. 따라서 같은 코드를 자바를 호출하지 않고 바이트 스트림만을 이용해서 재작성할 수 있습니다.

1(defn string->stream [s encoding]
2 (to-input-stream s {:encoding encoding}))

통합(integration)의 측면에선, JVM 툴킷이 전부 그대로 클로저에도 적용될 수 있답니다. 예를 들어, 당신이 직접 만든 YourKit이 클로저 앱을 프로파일링 하는데 그대로 사용될 수 있죠.

이렇게 첫 번째 스테이지를 통과하면, 문법과 핵심 구조에 대해서 배우게 됩니다. 명심하세요. 한번 클로저를 시작하게 되면 멈추기가 참 힘듭니다. 계속 계속 파고들게 되니까요!

2.2 중급자 과정

다양한 문제들

보통 두 번째 단계가 가장 어렵습니다. 명령형 자바 스타일에서 리습의 스타일로 전환하는 것은 상당히 도전적인 과제입니다. 아무리 최소의 문법만 있다고 해도, 처음엔 큰 도움이 되기 힘들었죠. 이 시점에 풍부한 자료들과 책, 커뮤니티의 도움을 많이 받을 수 있습니다.

우아함과 단순성을 기반으로 한 클로저는, 어쩌면 루빅스 큐브같은 퍼즐 같은 느낌을 줍니다. 복잡하게 느껴질 수도 있지만, 어떻게 동작하는지 파악하는 것은 큰 기쁨이죠.

Euler 프로젝트, 4 clojure, advent of code 등의 사이트를 방문하며 이 단계를 넘어가는 데 큰 도움을 받았습니다. 좀 더 어려운 게임화 과제 (gamification), 실전 테스크들, 그리고 진짜 훌륭한 프로젝트들을 접해볼 수도 있었지만, 저에게 있어 진짜 동기는 클로저로 일을 하는 것이었습니다. 그래서 답을 찾아보고 분석하는데 초점을 맞췄습니다.


함수형 접근

클로저로 데이터 모델링을 하며 마주치는 첫번째 특징은, 친숙한 OOP적 요소가 없다는 것입니다.

자바는 클래스를 정의하고, 그 안에 필드와 메소드를 만들어 객체의 특성과 행동을 정의합니다. 클로저의 함수는 특정 클래스 데이터의 '일부'가 아니라, 그 자체로 데이터를 처리하는 역할을 합니다. 클로저 함수는 네임스페이스라는 기본 단위로 분류됩니다.

클로저는 순수하지 않은 함수의 작성도 허용하지만, 주로 map, reduce, filter, remove 등의 고차 함수와 함수의 컴포지션(partial, comp, juxt)을 활용할 것을 권장합니다.


데이터 지향 접근

<쉽게 이해하는 클로저>(https://www.youtube.com/watch?v=aSEQfqNYNAc)에서 리치 히키는 데이터 지향 접근의 장점에 관해 이야기합니다.

자바 데이터는 보통 접근자, 변형자 (혹은 쉽게 getter/ setter)로 접근합니다. 큰 POJO 객체의 경우, 데이터 접근을 위해 많은 보일러 플레이팅이 필요하게 되지요. 물론 IDE를 통해 제공되는 롬복과 같은 자동 완성 툴들이 도움을 줍니다. 비슷한 접근으로 자바 14부터는 레코드(Records)를 사용할 수 있습니다.

하지만 아직까지 사용되는 필수 라이브러리를 살펴보면 문제는 여전합니다. 예를 들어 javax.servlet.http.HttpServletRequest 클래스를 한번 들여다보겠습니다.

https://thumb.tildacdn.com/tild6339-3165-4135-b466-653863346537/-/resize/397x/-/format/webp/HttpServletRequest.png

여기 서로 다른 세 가지의 색으로 표기된 필드는 각각 서로 다른 인터페이스들이 필요합니다. getParameterMap 과 getHeader, remove와 set은 attribute에 사용되고 parameter에는 사용되지 못하는 등, 서로 다른 이름 규칙을 갖고 있습니다.

불편한 인터페이스 외에도 데이터 조작 로직을 재사용할 수 없다는 것과 테스트를 생성하기 복잡하다는 문제도 있습니다.

클로저는 데이터 지향 관점에서 생기는 문제를 그냥 객체를 맵에 맵핑함으로써 해결해버립니다.

https://thumb.tildacdn.com/tild3665-6261-4536-b061-376331363330/-/resize/560x/-/format/webp/Clojure__data-orient.png

어떤 데이터든 일반 시퀀스를 조작하듯 다룰 수 있다는 것이 핵심입니다. 그냥 맵이든, 헤더(header)들의 맵이든 어떤 형태의 컬렉션도 쉽게 다룰 수 있습니다. 컬렉션을 다루는 여러 함수는 범용적이기 때문에 일반적으로 훨씬 더 가치가 있습니다.

쓰레딩 매크로 (threading macro) - >> 는 함수 호출을 훨씬 더 편리한 방법으로 작성할 수 있게 해줍니다. 자바의 Stream API 와 비슷하다고 할 수 있습니다.

1(->> request
2 :headers
3 (filter #(str/starts-with? % "my-header")))

적은 수의 데이터 구조에 함수형 접근이 합쳐지면, 데이터 조작 프로세스를 크게 개선할 수 있습니다. 궁극적으로는 프로그래밍 프로세스를 향상하고 가독성을 증대시킵니다.


불변 자료 구조

클로저 데이터 구조들은 불변(immutable)하며 영속적(persistent)입니다. 이 말은 '수정'하는 행위는 새로운 데이터를 반환한다는 얘기입니다.

그걸 염두할 때 놀라운 점은, 연산에 걸리는 시간이 알고리즘 복잡도에 크게 영향을 받지 않는다는 것입니다. (벡터나 해시맵의 값에 접근하는 시간복잡도는 O(log32(N)입니다))

한 데이터 구조는 과거 버전의 데이터 구조와 같은 소스를 공유하고 있기 때문에, 메모리 소모 측면에서도 큰 변동이 없습니다.

불변 자료구조에서, 객체가 동등하다는 개념은 다른 의미를 갖습니다. 만약 두 객체가 같다면, 그 둘은 한 시점에서만 같은 것이 아니라, 영원히 같습니다.

이 접근은 멀티쓰레딩 측면에서 굉장히 유용합니다. 컬렉션을 사용할 때 매번 쓰레드 안전을 확인할 필요가 없기 때문이죠.

애플리케이션 상태를 저장하기 위한 실질적인 방법은 쓰레드 안전한 뮤터블 컨테이너 (Var, Atom, Agent, Ref)를 사용하는 것입니다.

클로저는 소프트웨어 트랜잭션 메모리(software transactional memory, STM)를 통해 Ref의 트랜잭션 변경을 지원합니다. 이 주제는 다른 글에서 다뤄보겠습니다.


지연 평가 (lazy evaluations)

클로저는 지연 평가 시퀀스를 지원합니다. 이는 시퀀스에 대한 평가가 미리 이뤄지는 것 (ahead of time)이 아니라 연산의 결과로 그 때 그 때 이뤄짐을 뜻합니다. 지연 함수 (Lazy function)은 지연 시퀀스를 반환하는 함수를 말합니다.

지연 시퀀스로 무한을 표현할 수 있습니다. 만약 2백만 까지의 모든 소수(prime)을 더하는 알고리즘(프로젝트 오일러의 문제)을 짜야한다고 할 때, 다음과 같은 표현이 가능합니다.

1(def primes
2 (filter prime? (iterate inc 1)))
3
4(defn solve []
5 (->> primes
6 (take-while #(< % 2E6))
7 (reduce +)))

소수를 판별하는 알고리즘인 prime은 생략하겠습니다. 여기서 primes(s가 추가로 붙었습니다) 함수는, 사실상 존재하는 모든 소수들을 반환할 수 있는 함수입니다. 이를 가능하게 하는 것이 바로 지연 평가의 요소인 iterate 와 filter 덕분인데요, primes 시퀀스는 실제로 컬렉션 값에 접근 하기 전까지 평가되지 않습니다.

take-while 도 또한 지연 함수인데요, 따라서 solve의 reduce까지 와서야 컬렉션이 평가되어 하나의 답을 내놓게 됩니다. 물론 위의 문제는 지연 무한 시퀀스 (lazy inifinite sequence)를 사용하지 않고서도 풀 수 있습니다. for를 사용 할 수도 있고요. 그래도 위의 방법이 훨씬 더 우아하지 않나요!

2.3 고급단계

클로저를 이용한 웹 개발

시간이 지나 클로저로 웹 개발을 할 기회가 생겼고, 클로저의 생태계를 탐험하는 세 번째 단계에 오게 되었습니다.

링(Ring)은 클로저의 웹 개발 표준입니다. 파이썬과 루비에 각각 WSGI와 Rack이 있는것과 비슷합니다.

링 저장소는 다음을 포함합니다.

  • request, response, middleware에 대한 스펙과 베이스 코드
  • jetty 웹 서버에 대한 기본 미들웨어 어댑터
  • 문서 (documentation)

모든 현대 클로저 웹 프레임워크는 링 표준을 따르고 있습니다. 이는 아무 변화 없이 다른 플랫폼(Jetty, http-kit, Immutant 등등)에서 애플리케이션을 실행 할 수 있다는 말입니다.

링이 주는 간편함은 저에겐 거의 혁명이었습니다. 링이 사용한 함수에 기반한 미들웨어의 개념에 대한 관용적 접근은 오늘날 많은 언어와 프레임워크에 사용되고 있습니다. (NodeJS의 Express도 그 중 하나입니다.)

당시 저는 자바 서플릿과 스프링으로 몇년간 개발을 하고 있던터라, 자연스레 이 미들웨어적 접근을 스프링 부트의 경험과 비교하게 되었습니다. 여기 스프링으로 만든 기본적인 REST 서비스를 동일한 기능을 하는 클로저 코드를 비교한 코드입니다.

저의 첫 번째 클로저 기반의 REST API는 거의 위의 예제처럼 생겼습니다. 아, 당시는 더 최신이었던 compojure-api 대신 compojure 라이브러리를 사용하긴 했어요. REPL과 결합하여 링은 정말 엄청난 개발 생산성을 보여줬습니다.

Compojure는 핸들러를 읽기 쉬운 routes의 리스트로 표현하는 라우팅 라이브러리입니다. 아래는 예제코드입니다.

1(def my-routes
2 (routes
3 (GET "/foo" [] "Hello Foo")
4 (GET "/bar/:id" [id] (str "Hello " id)))))

템플릿을 지원하는 라이브러리인 Hiccup은 HTML을 표현하는 편리한 DSL을 만들어줍니다.

1(html [:span {:class "foo"} "bar"]) ;; => <span class="foo">bar</span>

Hiccup의 최대 장점은 평범한 클로저 데이터 구조(벡터와 맵)과 함수들로 HTML에 대한 표현이 가능하다는 겁니다. <>와 같은 특수 부호들을 굳이 사용할 필요가 업습니다.

1(html [:ul (for [x (range 1 4)] [:li x])])
2;; => <ul><li>1</li><li>2</li><li>3</li></ul>

같은 포맷이 클로저스크립트의 ReactJS은 Reagent에서도 역시 사용됩니다.


확장성

클로저의 확장성은 소프트웨어 개발에 중요한 역할을 합니다. 다른 언어들에 있는 강력한 기능도 매크로를 통한 외부 라이브러리로 쉽게 구현이 될 수 있기 때문이죠. core.match 라이브러리의 패턴 매칭 (pattern matching)을 예로 들어보겠습니다.

1(let [x {:a 1 :b 1}]
2 (match [x]
3 [{:a _ :b 2}] :a0
4 [{:a 1 :b 1}] :a1
5 [{:c 3 :d _ :e 4}] :a2
6 :else nil))
7;=> :a1

다른 패턴과 데이터형을 매치시킬 수 있도록 확장도 가능합니다.

1(matchm [(java.util.Date. 2010 10 1 12 30)]
2 [{:year 2009 :month a}] a
3 [{:year (:or 2010 2011) :month b}] b
4 :else :no-match)

놀랍지 않나요?

다음은 core.async에 대해서 알아보겠습니다.

core.async는 Queue의 역할을 하는 채널과 쓰레드 풀(Thread pool), 제어역전(inversion of control)등의 디자인 패턴을 이용하여 비동기 통신을 하는 라이브러리입니다. Golang의 채널과 비슷한데요, 클로저의 구현은 언어자체에 탑재되어 있는 것이 아닌 라이브러리를 통해 이뤄진 점이 다릅니다.

1(defonce log-chan (chan))
2
3(defn loop-worker [msg]
4 (println (str "hello, " msg "!")))
5
6(go-loop []
7 (let [msg (<! log-chan)]
8 (loop-worker msg)
9 (recur)))
10
11(>!! log-chan "world") ; => hello, world!
12(>!! log-chan "core.async") ; => hello, core.async!

gogo-loop 블록은 바로 제어를 리턴하여 다른 쓰레드에서 비동기적으로 처리되는 코드 블록을 정의합니다.

<! 함수는 쓰레드를 go-loop 블록에 위치시킵니다. 메시지가 들어오면 loop-worker 핸들러가 호출되고, 블록이 다음 메시지를 멈춰 기다리게 됩니다.

블록 함수 >!!는 스트링 값들을 log-chan 채널로 전달하며, loop-worker는 그 메시지들을 출력합니다.

core.async는 디폴트로 8개의 쓰레드를 go 블록에서 지원합니다.

core.async는 클로저스크립트에서도 활발히 사용되고 있습니다. 클로저스크립트는 블로킹 없는 접근을 통해서 콜백 지옥 문제를 해결하고 있습니다!


스펙(clojure.spec)과 동적 타이핑

클로저는 리습(Lisp)에서 동적 타입의 레거시를 물려받았습니다. 언어의 창시자의 리치 히키도 동적 타입을 기본으로 사용하는 것을 지지하고 있습니다.

수년간 타이핑에 대한 길고 복잡한 논의들이 있어왔기에 제 사견만 조금 덧붙여보겠습니다. 동적 타이핑이 정말 발등 찍히기 쉬운 건 맞습니다. 하지만 한편으로는 더 실용적이고 결과 지향적 접근이기도 합니다.

정적 타입의 부재는 독립적인 컴포넌트나 심지어 전체 시스템을 "바로바로" 테스트해볼 수 있는 강력한 REPL과, 데이터의 명세를 표현할 수 있는 core.spec으로 상쇄합니다.

core.spec은 데이터 스키마를 클로저 데이터 구조로 표현하여 더 '데이터 지향적 접근'을 가능하게 합니다. 대담하게도, 클로저는 함수 체인의 처음부터 끝까지 전부 타입으로 추적할 필요가 없다고 얘기합니다. 대신, spec을 잘 활용하면 되는데 spec은 API를 설명하고, 웹 형식을 검증하며, 심지어 테스트 데이터 생성까지 전부 도맡아 할 수 있기 때문입니다.

아래는 spec이 request/response 검증과 타입 변환, swagger 문서 생성에까지 어떻게 사용되고 있는지에 대한 예시입니다.

1(s/def ::id int?)
2(s/def ::name string?)
3(s/def ::user (s/keys ::req-un [::id ::name]))
4
5(def app
6 (api
7 {:coercion :spec}
8 (GET "/users/:id" []
9 :path-params [id :- ::id]
10 :return ::user
11 (ok (users-by-id id)))))

수년간 정적 타입 언어인 자바를 써오다보니, 처음에는 클로저의 동적 타입 모델을 쓰면서 불안해했던것도 사실입니다. 물론 타입 체크를 통한 에러 색출과 인텔리센스는 정적 타이핑의 장점입니다만, clojure.spec을 써보니 그 생각이 잘 나지 않게 되었습니다.

복잡한 알고리즘이나 구체적인 모듈을 사용할 때는 정적 타입의 도움을 받는것이 합리적입니다. 여기서 클로저의 확장성이 또 한번 빛을 발하는데요, core.typed 라이브러리를 봅시다.

core.typed 라이브러리는 클로저 프로그램 전체 혹은 부분에 걸쳐 선택적으로 타입을 적용할 수 있도록하는 선택적 타입 시스템을 제공합니다.

2부에서 이어집니다.





Gravatar for tlonist.sang@gmail.com
김상현백엔드 개발자
2021. 05. 30.

추천 콘텐츠