본문 바로가기

Clojure

Programming Clojure 읽기 - Introduction

본 내용은 할로웨이 스튜어트의 프로그래밍 클로저를 읽고 정리한 내용입니다. 잘못된 내용이 있다면 댓글로 편하게 지적해주시면 감사하겠습니다.

클로저의 특징

 

함수형 프로그래밍과 병렬처리를 위한 소프트웨어 트랜잭션 메모리(STM)는 멀티코어 하드웨어에 적합한 프로그래밍 패러다임입니다. 그 중 클로저는, Lisp에서는 오랜 프로그래밍 역사에서 오는 지혜를, 자바에서는 주류 플랫폼다운 안정성과 수많은 라이브러리를 지원합니다. 이 두 가지 강력한 조합이 바로 클로저입니다. 다음부터는 클로저의 특징에 대해서 조금 더 자세히 살펴보겠습니다.

 

  • 우아하고 표현력이 좋은 코드
  • ‘코드와 데이터의 형태가 같다’는 Lisp의 특성
  • 재사용성을 높이고 오류를 줄이는 함수형 프로그래밍
  • lock 기반이 아닌, 쉬운 병행 프로그래밍 → STM, agent, atom, dynamic var
  • 자바를 포함하여, 별도 계층 없이 바로 호출 가능하다.
  • 속도가 느린 많은 동적 언어와 달리 빠르다.

→ JVM에서 동작하며 성능을 희생하지 않으면서, 함수형 언어로 병행 프로그래밍에 유리합니다.

 

클로저는 우아하다.

고차함수를 이용하기 때문에 상태 변화, 분기 없이 간결하고 우아한 코드를 작성할 수 있습니다.

Immutable 자료구조이기 때문에 스레드 프로그래밍 시 안전하다. 그럼에도 다른 더 순수한 함수형 언어와 달리, 상태 변경이 필요한 경우, 클로저의 ref, agent, atom을 이용해 새로운 인스턴스로 치환 가능합니다.

 

클로저는 Lisp의 부활이다.

클로저는 Lisp의 방언입니다. Lisp가 어떤 장점을 가지고 있는지 살펴보면 클로저의 장점도 이해할 수 있습니다.

Lisp은 macro라는 강력한 기능을 가지고 있습니다. macro는 Lisp을 개발자 목적대로 변형해 사용할 수 있게 하는 도구입니다.

다음과 같은 자바 코드를 생각해봅시다.

public class Person {
	private String firstName;
	public String getFirstName() { // 이하 생략.

 

클래스 내에서 메소드는 프로그래머 필요에 따라 자유롭게 정의하거나, 다형성을 가지도록 할 수 있습니다. 그러나, 다른 키워드에 대해서는 이미 프로그래밍 언어에 의해 정의되어 있기 때문에 변경할 수 없습니다. 만약 다음과 같이 기존의 단어에 새로운 의미를 주고 싶은 경우에는 어떻게해야할까요?

  • private의 의미를‘단위 테스트와 직렬화의 경우에는 public이지만 배포 시에 는 private’으로 바꾸고 싶다.
  • 특별한 지시가 없는 한, class 키워드가 private field에 대한 getter와 setter를 자동으로 생성하도록 재정의한다.
  • 인스턴스의 생명주기 동안 생기는 이벤트에 대해 콜백을 제공하는 클래스의 서브클래스를 만들고 싶다. 예를 들어, 클래스의 인스턴스가 생겨날 때마다 이벤트를 발생시키는 클래스를 만들려 한다.

기존 언어에 새로운 언어를 정의해 내는 것이 Lisp의 고유 능력입니다. 이를 가능하게 하는 Lisp의 강력한 특성 2가지를 소개합니다.

 

1. Lisp은 코드와 데이터의 형태가 같습니다. 

클로저의 코드는 클로저에서 쓰이는 자료구조로 구성되어 있습니다. 클로저 프로그램을 실행하면‘리더(reader)’라는 부분이 프로그램의 텍스트를 읽어 클로저의 데이터로 변형합니다.

 

2. Lisp 문법에는 연산자의 우선순위, 상관관계를 위한 규칙이 전혀 없습니다.

Lisp은 모든 표현이 괄호로 둘러쌓여 있으므로 모호함이 생길 여지가 없습니다. 다만, 다른 언어와 대비해 괄호가 많기 때문에 읽기 어렵다는 의견이 많습니다. 클로저는 그럼에도 좀 더 읽기 쉬운 코드를 쓰기 위한 장치들이 있는데요, 바로 threading macro입니다. 이와 더불어 

Lisp의 주요 개념인 Arity, S-expression, 평가 우선순위에 대해 잘 설명된 글이 있어 소개합니다.

https://green-labs.github.io/is-clojure-hard-to-read

 

클로저(Clojure) 코드는 정말 읽기 어려운가?

클로저에는 슬픈 전설이 있어. 무슨... 전설인데요?...혹시 코드가 읽기 어렵다는 그 전설인가요? 난 전설같은 건 믿지 않아.

green-labs.github.io

 

'클로저가 부활한 Lisp이다'라고 표현한 이유는, 클로저가 Lisp의 특징을 물려받으면서도 기존의 단점들을 보완하였기 때문입니다. 클로저의 Lisp 방언으로서의 장점을 살펴봅시다.

  • 클로저는 Lisp의 리스트를 ‘시퀀스’라는 좀 더 일반화된 데이터 형태로 다룬다.
  • JVM 라이브러리를 그대로 활용, 배포가 가능하다.
  • 매크로 작성을 쉽게하는 여러 편의 장치를 제공한다.
  • 리스트 외에도 정규식, 맵, 집합, 벡터, 메타데이터 등 다양한 자료 구조를 표현하기 위한 문법 제공
  • 쉼표를 공백으로 인식하여, 쉼표를 추가해 자료 구조를 쉽게 읽을 수 있게 한다.
  • 클로저에서는 꼭 필요한 곳에만 괄호를 사용한다.
  • 상태의 변경이 필요한 경우, 상태를 체계적으로 관리하기 위한 여러 메커니즘 제공한다. STM, ref, agent, atom, dynamic binding.
  • 동적 타입을 사용하여 함수형 프로그래밍을 배우고 접근하기 쉽다.
  • 자바 호출은 함수형 접근 방식이 아닌, 변경 가능한 세계로 돌아가므로 함수형 방식 적용이 어려운 경우 실용적 대안이 될 수 있다.

 

쉬운 병행 프로그래밍

함수형 프로그래밍은 자료구조의 데이터를 변경하는 것이 불가능하기 때문에, 다른 스레드에 의해 데이터가 변경되어 오류가 생길 염려가 없습니다. 그러나 실제 비즈니스에서는 변경 가능한 데이터에 대한 레퍼런스가 필요한 경우가 생깁니다.

 

병행 프로그래밍의 고전적이며 가장 일반적인 모델은 락, 컨디션 변수를 이용한 멀티스레드 프로그래밍입니다. 자바에서는 monitor(락과 컨디션 변수)를 언어에 포함시키며 대중적으로 사용되게 되었습니다. 그러나, 락을 이용한 멀티스레드 프로그래밍 기술은 다음과 같은 단점이 있습니다.

  • 비결정적인(non-deterministic) 프로그램 특성 때문에 테스트가 어렵고, 재현이 어렵다.
  • 동기화의 구간 단위가 커질 수록 scalability가 떨어지며, 락을 잘게 쪼개게 되면 deadlock과 race condition의 병폐에 부딪힐 확률이 커진다.

 

클로저는 변경 가능한 데이터에 대한 레퍼런스가 필요할 때, STM(Software Transaction Memory)을 이용해 레퍼런스를 보호하여 해결합니다. STM은 락에 의한 자바의 메커니즘보다 상위 수준에서 스레드 안전성을 보장합니다. 이 방식은 락 방식보다 훨씬 생산성이 높습니다.

 

트랜잭셔널 메모리로 구현된 원자적 작업은 여러 개를 조합하여 더 큰 원자적 작업으로 만들 수 있습니다. 그러나, Lock 기반 병렬 코드는 이런 일을 할 수 없습니다.

 

예를 들어, 다음 코드는 스레드 세이프한 계좌 데이터베이스를 메모리에 만듭니다.

(def accounts (ref #{}))
(defstruct account :id :balance)

;; dosync로 갱신
(dosync (alter accounts conj (struct account “CLJ“ 1000.00)))

 

dosync는 내부적으로 트랜잭션을 실행하여 마치 데이터베이스에 트랜잭션을 통해 질의하는 것처럼 계좌에 갱신을 일으키게 할 수 있습니다.

 

이상으로 클로저의 주요 특징을 살펴보았고, 다음 장부터는 각 특징에 대해 상세히 다루므로 이를 정리해보겠습니다.