devthewild

번역어의 사회성

책상은 책상이다라는 책이 있다. 중고등학교 때 언어의 많은 특징에 대해 배우는데, 그중에서 언어의 사회성이라는 것을 배운다. 언어란 사람들 간의 의사소통을 위한 도구이며, 그래서 추상적인 생각이든 구체적인 물건이든 어떤 의미에 대해서 어떻게 부르자는 사람들 간의 약속을 언어의 사회성이라고 한다. 저 책에서는 그 사회성을 무시했을 때 발생할 수 있는 문제들에 대한 이야기를 다루고 있다.


슬랙이라는 메시징 앱이 있는데 이 앱이 유행하면서 아직 채팅 공간이 없는 커뮤니티들은 IRC 대신 슬랙을 사용하고 있고, 그래서 이것저것 추가를 하다 보니 10개를 넘어갔다가 다시 추려서 7개만 남았는데, 그중에서 가장 오랜 시간을 보고 있고, 계속 1번에 위치했던 이상한 모임 팀에 질문이 올라왔다. (슬랙은 업무용 툴이라 하나의 단위가 팀이다. 사실 이 글이 중복 게시되는 메타블로그를 통해 보는 사람이 대다수라 부연설명이 필요 없을 수도 있지만, 혹시 모르는 분들이 있을까봐 남겨봤다. )

"Lazy evaluation를 뭐라고 번역해야 좋을까요?"

한국어 위키에 "느긋한 계산법"이라고 되어있다며 질문이 올라왔고 질문한 분은 "이따 평가"라는 제안을 했고 나는 "지연평가가 가장 흔한 번역같습니다."라고 대답했다.

필요해질 때까지 평가를 미뤄둔다는 의미에서 Lazy의 번역으로 '지연'이라는 표현을 많이 사용하는데, 지연이라는 말에 타의적 혹은 일정 시간이라는 정적인 뉘앙스가 있다고 생각해서 원래 의미가 살지 못해서 좋은 번역은 아니라고 생각한다. 다만, 그 번역을 들었을만한 사람(특히 개발자)들은 대부분 "Lazy Evaluation"이라는 원문과 동시에 그 원문의 뜻을 같이 떠올릴 수 있을 것이다. 좋은 번역은 아니지만, 대부분은 뉘앙스를 알 수 있다는 의미로 나는 "합의된 번역"이라고 표현한다.


도입에서 말한 언어의 사회성이라는 것은 번역어에도 적용된다. 하지만 언어끼리의 1:1 매칭이 되는 표현은 거의 존재하지 않는다는 특수성 때문에 번역어에서의 사회성은 언어의 사회성과 다소 차이가 있다. 자연스럽게 시간이 흐르면서 생기는 사회적 합의 없이, 이미 존재하는 언어의 이미 존재하는 표현을 갑자기 다른 언어의 다른 표현으로 옮기는 것에는 사회성이 존재하기 어렵다. 그래서 많은 번역어가 사회적 합의 없이 생겨나고 생긴 뒤에서야 사람들이 그 표현에 익숙해지는 후불제 합의에 가깝다. 그래서 어떤 분야를 맨 처음 번역하는 사람들의 역할이 중요하다. 처음 시도했던 번역어가 후세에도 많은 영향을 끼치기 마련이고, 그 영향을 벗어나 새로운 합의를 만들어내기까지 오랜 시간과 노력이 필요하기 때문이다. 그 시간과 노력을 투자하는 사람은 언어 자체를 공부하는 사람이 될 수도 있고, 또 다른 번역자일 수도 있다. 하지만 누군가의 주도 하에 결정되기보다 사회적 합의를 이루기 위해 커뮤니티의 주도로 이루어졌으면 좋겠다는 생각을 한다. 페이스북의 개발자영어라는 그룹에서 종종 그런 글들이 올라온다. 이런 말은 보통 이렇게 번역하는데 더 좋은 표현이 없을까 하는 질문글. 저 그룹이 어떤 권위나 영향력이 있지는 않지만, 합의를 위해 토론이 이루어지는 (내가 아는 한에서)유일한 커뮤니티이다.

이 생각을 맨 처음 했던 것은 얼마 전에 읽었던 책 때문이다. 유명한 역자분이 번역했고, 역자 서문에서부터 현재의 (합의된)번역어보다 더 어울리는 말을 찾기 위해 많이 고민하고 노력했다고 적혀있고 책을 읽으면서 생소한 단어 때문에 읽는 데 방해도 많이 되긴 했지만, 무슨 뜻일까 거꾸로 영어로 영작해보기도 하고 원문을 찾아보고 한자어의 경우 한자 뜻을 찾아보고 나서야 어떤 의도로 그런 번역어를 만들었다고 이해하기도 했고 혹은 아무 문제 없다고 생각했던 번역어를 굳이 쓰지 않고 곡해하기 좋은 새로운 번역어라고 생각되는 경우도 있었다.

책을 읽으며 아쉬웠던 점은 합의없이 새로운 표현을 만들어냈다는 것이 아니라, 생소한 표현을 처음 사용할 때는 차라리 원어 병기를 통해 유추라도 가능하도록 도와줬으면 좋았을 텐데 그렇지 못한 경우가 너무 많았다는 점 정도다. 번역어에 대한 합의는 현재 그럴만한 적당한 공간이 없기 때문에 그걸 생략했다고 누구에게 뭐라고 할 상황은 아니라고 생각한다. 그래서 더더욱 그럴만한 적당한 공간이 없다는데 더 안타깝다.


마무리로 광고를 남기자면 이상한 모임의 슬랙에 들어와 보시면 이 공간의 아이덴티티를 이해하는 데 도움이 된다. 위에서 말한 번역에 대한 이야기도 간혹 하고, 개발자가 많다보니 개발에 관한 채널이 많지만 그 이외에도 각종 가젯(gadget)/SW 지름, 음악, 책, 디자인, 운동 등 아주 잡다한 분야에 대한 이야기가 항상 오고가서 어떤 사람들이 어떤 이야기를 한다고 특별히 정의내려서 설명하기 어렵다. 그냥 직접 와서 겪어보는게 이해하는데 제일 좋다. 굳이 공통점을 꼽자면 재미있는 것에 목마른 사람들이라고 표현하고 싶다.

Callback에서 Future로(그리고 Functor, Monad)

Translation of "From callback to (Future -> Functor -> Monad)" into Korean, under the same license as the original.

동기

함수형 프로그래밍에서 기본개념은 조합(composition) 이다. 간단히 설명해서, 단순한 것들을 엮어서 더 복잡한 것을 만들 수 있고 그 결과를 다시 엮어서 더 복잡한 것을 만들 수도 있다. 함수의 의미나 리턴값이 무엇인지만 알고 있으면 조합으로 무엇이든 만들어낼 수 있다.

Node.js를 써봤으면 아래와 같은코드를 본 적이 있을 것이다.

1
2
3
4
fs.readFile('...', function (err, data) {
if (err) throw err;
....
});

위의 코드는 전형적인 CPS(continuation-passing style) 함수이다. fs.readFile이라는 CPS 함수는 계속(continuation) 진행될 콜백을 추가 파라미터로 받는다. 이 CPS가 끝나면 호출한 곳에 값을 반환하는게 아니라 계속 함수, 콜백에 계산 결과를 넘겨준다.

나는 콜백을 쓰는걸 꺼리진 않는다. 사실 콜백은 부수효과를 표현하거나 이벤트 알림같은 것을 다룰 때 훌륭하다. 그렇지만 그걸로 흐름을 관리하기 시작하면 함정에 빠진 것이다. 왜냐면 조합할 수 없기 때문에.

자, 생각해보자. "위에 나온 함수의 **표기(denotation)**나 리턴값은 무슨 의미일까?" 답은 undefined이다. undefined라는건 테스트할 때 실제로 undefined인지 확인하는 용도 이외에는 쓸 데가 없다.

콜백 안에서 다른 실행 흐름으로 넘어갈 때 일방통행이라는게 문제다.

물리학에서 유명한 블랙홀처럼 콜백을 생각해보자:

블랙홀은 수학적으로 정의된 지역이다. 강한 인력이 작용하거나, 어떤 티끌이나 전자기적 파장조차 빠져나갈 수 없는

그 중에서도

어떤 빛도 반사하지 않는 등 블랙홀은 많은 부분에서 이상적인 검은 물체처럼 작용한다.

콜백 함수 또한 흐름에서의 어떤 것도 반사하지 못한다.

나중에 첫번째 콜백에 들어갈 때 다른 콜백 스타일 함수를 쓸 수 있는데, 그때는 두번째 흐름을 잃게 되고 다른 구멍에 빠지게 된다. 콜백을 쓰면 쓸 수록 지옥에 빠지게 된다.

그럼 블랙홀에 빠지지 않고 코드를 진행할 수는 없을까?

답은 조합이다. 하지만 조합을 사용하려면 일단 CPS 함수가 어디로도 돌아갈 수 없다는 사실을 알아야하고, 함수로부터 뭔가를 받아와야한다. 그러니 어떻게든 함수가 뭔가를 반환하게 만들어야한다. 어떤 값이 반환될까? 이게 이 글의 동기이다.

이미 자바스크립트에서의 해답을 알고 있을 수 있다. 하지만 계속 이 글을 읽도록, 강하게, 추천한다. 지시적인(즉 함수형) 생각의 힘을 보게 될 것이고, 깔끔하고 간결한 해답을 어떻게 사용할지 보게 될 것이다.

future로 입문

파일 읽기, 네트워크 요청, DOM 이벤트, 이런 함수들의 공통점은 뭘까?

이 함수들은 즉시 완료되지 않는 것들이다. 즉, (보통 함수들을 다루는 식으로는) 현재 프로그램 흐름에서 저 함수들이 완료될 때까지 기다릴 수 없다는 뜻이다. 그래서 _future_를 설명할 것이다.

그래서 특별한 반환 타입, 나중에 결과를 만들어준다고 명시하는 Future를 만들어보자. 요점은 다른 함수들로 넘길 수 있는 1등급 클래스 값을 사용하는 것이다.

Future는 무슨 의미일까? 특정 시간(0이 될 수도 있다) 후에 발생할 것이라고 명시해놓은 값이다. 그 시간은 우리가 x초 후라고 말하는 것처럼 명시적인 시간이 될 수도 있지만, Future 2개가 완료된 후 혹은 Future 하나가 완료된 뒤 다른 Future 완료될 때처럼 상대적인 개념일 수도 있다.

여기서 중요한 점은: Future의 결과는 항상 불변값이다.

즉, 완료 값을 어떤 방법으로든 변경할 수 없다. 이 제약으로 구현 뿐만 아니라 의미론에 대한 추론도 간단해진다.

Future는 일회용의 간단한 상태머신처럼 구현될 수 있다. 이 머신은 대기 로 시작했다가 완료 가 된 후에 멈춘다. 한번 완료되면 계속 완료상태에 고정된다.

내부적으로 Future는 콜백에 여전히 의존하고 있지만, 그 콜백들이 컨트롤 흐름 매커니즘을 방해하지는 않는다. 대신 올바른 목적으로만 사용된다, 이벤트 알림.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Future() {
// 대기중인 구독들을 저장하는 리스트
this.slots = [];
}

// 완료를 알린다
Future.prototype.ready = function(slot) {
if(this.completed) slot(this.value);
else this.slots.push(slot);
}

// 간단한 로그 유틸리티
function logF(f) {
f.ready( v => console.log(v) );
}

Future를 완료시키는 외부 인터페이스로 메소드가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Future.prototype.complete = function(val) {
// 불변성 보장
if(this.completed)
throw "이미 완료된 Future는 완료시킬 수 없다."

this.value = val;
this.completed = true;

// 구독들에게 알림
for(var i=0, len=this.slots.length; i< len; i++) {
this.slots[i](val);
}

// 모두 실행되면 이제 필요없다.
this.slots = null;
}

Future의 가장 간단한 예제로 어떤 값으로 즉시 완료시켜보자. 그 역할을 unit이란 메소드를 만들어보자.

1
2
3
4
5
6
7
8
// unit: Value -> Future<Value>
Future.unit = function(val) {
var fut = new Future();
fut.complete(val);
return fut;
}

logF( Future.unit('hi now') );

코드에 대해 간단히 설명하기 위해 타입 표기(type annotation) 를 사용했다.

unit: Value -> Future<Value>를 풀어보면 1- unit은 함수고, 2- 제네릭 타입 Value를 입력으로 받으며, 3- 제너릭 타입을 가진 Future 인스턴스를 리턴한다. 여기서 타입 정보는 중요하지 않으므로 Value라는 제너릭은 신경쓰지 않아도 된다.

다음 예제는 특정 시간이 지나고 완료되는 값이다.

1
2
3
4
5
6
7
8
9
10
// delay: (Value, Number) -> Future<Value>
Future.delay = function(v, millis) {
var f = new Future();
setTimeout(function() {
f.complete(v);
}, millis);
return f;
}

logF( Future.delay('안녕, 이건 5초 걸린다', 5000) );

delay의 결과는 주어진 값만큼의 시간이 지난 뒤에 완료되는 Future다.

readFile 예제로 돌아가서, 이제 CPS 함수 대신에 Future를 리턴하는 함수를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var fs = require('fs');

// readFileF: (String, Object) -> Future<String>
function readFileF(file, options) {
var f = new Future();
fs.readFile(file, options, function(err, data) {
// 에러는 잠시 후에 다루겠다

if(err) throw err;
f.complete(data);
});
return f;
}

logF( readFileF('test.txt', {encoding: 'utf8'}) );

readFileF의 결과는 인자로 받은 파일 이름의 내용을 잡고 있는 Future가 된다.

Future 다루기: 첫번째 게스트

Future를 결과적으로 함수의 결과를 잡고 있는 마법 상자처럼 생각할 수도 있다.

뭔가 쓸모있는 것을 하려면 Future 타입에서 쓸모있는 연산들을 제공해야한다. 그러지 않는다면 필요없이 또 다른 undefined를 만든 것과 다를 바 없다.

그러면 어떤 연산을 Future에서 제공해야할까?

Future 상자에서 잡고 있는 값에 어떤 연산을 하고 싶을 때 (function map을 줄인)fmap을 호출할 것이다.

fmap의 예제를 보자. 여기서 Future는 텍스트 파일의 내용을 잡고 있고, 이 내용의 길이를 계산하려고 한다.

1
2
3
4
5
var textF = readFileF('test.txt', {encoding: 'utf8'});

// fmap: (Future<String, (String -> Number)> -> Future<Number>)
var lengthF = textF.fmap( text => text.length );
logF( lengthF );

lengthF의 뜻은 인자로 받은 Future가 잡고 있는 파일 내용의 길이를 잡고 있는 Future다.

일반화를 해보자면, fmap은 인자를 둘 받는데, 하나는 값을 잡고 있는 Future고 하나는 일반값을 다루는 매핑 함수다. 입력으로 받은 Future의 결과물에 매핑 함수를 적용한 결과를 잡고 이는 Future가 결과로 나온다. 받은 Future와 결과 Future는 둘 다 동시에 완료된다.

정확하진 않지만, 이렇게 표현할 수 있다

1
fmap( Future<value>, func ) = Future< func(value) >

fmap은 몇줄만으로 구현할 수 있다.

1
2
3
4
5
6
7
Future.prototype.fmap = function(fn) {
var fut = new Future();
this.ready(function(val) {
fut.complete( fn(val) );
});
return fut;
}

현재 Future가 완료되었을 때 결과로 나온 Future도 완료된다. 그 때 매핑 함수를 적용시킨다.

위의 예제에서는, 파일 내용을 잡고 있는 Future를 내용 길이를 잡고 있는 다른 Future로 변이시켰다.

어디서 들어본 말 같지 않은가? 잘 알고 있는 자바스크립트 Array의 map 메소드와 꽤 비슷하다. 실제로 정확히 같은 개념이다.

  • Array 타입은 여러 값들을 잡고 있는 박스다
  • Future 타입은 완료될 값을 잡고 있는 박스다
  • Array.map(...) 은 Array 박스 안의 값들을 변이시켜서, 변이된 값들을 잡고 있는 다른 Array 박스를 돌려준다
  • Future.fmap(...)은 Future 박스 안의 값을 변이시켜서, 변이된 값을 잡고 있는 다른 Future 박스를 돌려준다

Array와 Future 타입 둘 모두 포함되는 Functor라는 첫번째 게스트가 등장했다. 일반 함수를 하나 받아서 안에 무엇을 가지고 있든 그것이 변이된 결과를 표현하는 다른 인스턴스를 만들어내는 타입이다.

  • 다른 타입을 감싸는 컨텍스트처럼 작동할 수 있는 타입이고
  • 내부에 있는 것을 일반 함수에 적용시킬 수 있다면

Array와 Future가 아니더라도 그게 무엇이든간에 그 타입을 Functor라고 부를 수 있다.

이제 Future를 다른 Future로 매핑할 수 있다. 이제 일반값을 다루듯이 Future를 직접적으로 다루는 함수를 만들 수 있다는 뜻이다. textF.fmap( c => c.length )처럼 호출하는 대신에 Future를 직접 다루는 lengthF라는 특별한 종류의 함수를 만들 수도 있다.

1
2
3
4
// lengthF: Future<String> -> Future<Number>
function lengthF(strF) {
return strF.fmap( s => s.length )
}

파일 길이를 읽는 예제를 흔히 보던 방법처럼 다시 작성할 수 있게 되었다.

1
nbCharsF = lengthF( readFileF('...') )

lengthFlift된 함수라고 부른다. Functor같은 _박스 타입_을 다루는 함수를 lift한다는 것은 일반값을 다루는 함수를 박스 타입을 다루는 함수로 만든다는 뜻이다. 여기에서는 문자열을 다루는 함수 length(String)를 lift해서 Future를 다루는 함수lengthF( Future<String> )로 lift했다.

일반화된 lift1(인자를 하나만 받아서 lift하는 함수)를 정의해보자.

1
2
3
Future.lift1 = function(fn) {
return fut => fut.fmap(fn);
}

비동기 실행을 일반 함수 실행처럼 만들어주는 간단한 추상 함수다. 위에서 lengthF( readFileF('...') )readFileFlengthF를 조합해서 비동기 연산을 현재의 흐름을 떠나지 않고 실행할 수 있다.

인자를 여러개 받는 함수는 어떻게? (두번째 게스트?)

질문에 대답하기 전에 잠시 기초지식에 대해 생각해보자: Future 박스가 잡을 수 있는 타입에는 뭐가 있을까? Future는 모든 타입에 대해 같은 의미를 가질까?

Future<String>의 뜻은 명확하다: 시간이 지난 뒤에 문자열 타입의 값이 발생한다는 뜻이다. 다른 타입들에 이 의미를 확장할 수 있을까? 숫자, 객체, 배열? 그럴듯... 그럼 Future 자체에 대해서는 어떨까? Future<Future>는 무슨 뜻일까? 그러니까 Future의 Future는?

보고 바로 이해할 수 있도록, 디렉토리를 보고 첫번째 파일의 내용을 읽는 간단한 예제를 만들어보자 (간단히 생각하기 위해 내부에 다른 디렉토리가 없다고 가정한다).

Node.js에서는 비동기 함수 fs.readdir을 통해 디렉토리 속 파일들 이름의 배열을 가져올 수 있다. 먼저 이걸 Future식 함수로 만들어보자.

1
2
3
4
5
6
7
8
9
10
// readDirF: String -> Future< Array<String> >
function readDirF(path) {
var f = new Future();
fs.readdir(path, (err, files) => {
// 기다리면 곧 실행된다
if(err) throw err;
f.complete(files);
});
return f;
}

readDirF는 디렉토리 내 파일 이름들의 배열을 기다리는 Future를 뜻한다.

위에서 말한걸 구현하려면 필요한 나머지는

  1. Future가 잡고 있는 파일 이름들의 배열을 기다린다.
  2. 첫번째 파일명을 가져온다.

여기서 fmap을 사용할 수 있을까? Node에서 이걸 실행해보자

1
2
var resultF = readDirF("testdir").fmap( files => readFileF( files[0]) )
logF( resultF )

기다리면... 아차

1
{ slots: [] }

확실히 뭔가 잘못됐다. 콘솔에서 파일 내용이 나오는게 아니라 Future 인스턴스 객체의 내용이 나왔다.

왜냐면 fmap은 매핑 함수의 결과가 무엇이든 받아서 그걸 Future로 잡아 돌려주기 때문이다. 위에서의 매핑 함수는 또다른 Future(readFileF의 결과)를 fmap은 그 Future를 잡는 Future를 만들어 resultF에 보내기만 한다.

하지만 Future는 잡고 있는 Future와 함께 끝나는지 않으므로, 속에 있는 Future가 완료될 때까지 계속 기다릴 뿐이다.

그래서 이럴 때 필요한 함수를 만들어보자. Future를 리턴하고 끝내는 대신에 속에 있는 Future가 끝날 때까지 기다리는 함수다.

(이중 Future)Future를 그냥 Future로 만들어주는 flatten를 만들어보자.

1
2
3
4
5
6
7
8
9
10
// flatten: Future< Future<Value> > -> Future<Value>
Future.prototype.flatten = function() {
var fut = new Future();
this.ready(function(fut2) {
fut2.ready( function(val) {
fut.complete(val);
} );
});
return fut;
}

이렇게 하면 원하는 결과를 얻을 수 있다.

1
2
3
var result = readDirF("testdir")
.fmap( files => readFileF(files[0], {encoding: 'utf8'}) )
logF( result.flatten() )

fmapflatten을 따로 부르는 대신에 한번에 부를 수 있게 합쳐보자: 매핑 함수에서 나온 2중 Future를 압축(flatten)하는 두가지 일을 한다. 하는 일 그대로 flatMap이라고 하자 (좀 이상한건 나도 안다).

1
2
3
Future.prototype.flatMap = function( fn ) {
return this.fmap(fn).flatten();
}

개념상으로는 위에서 독립적인 두 연산을 _이어서_하는 것인데, readDirF에서 나오는 파일 이름의 배열을 readFileF에 넘겨준다.

여기에서 두번째 게스트가 등장하는데, Future를 Functor라고 부를 수 있는 것처럼 Monad라고 부를 수도 있다. 순서대로 연산할 수 있는 방법에 대한 개념이다. 위에서 flatMap에서처럼, 이전 단계에서의 결과를 다음 단계로 넘겨서 여러 함수를 연이어 연산할 수 있다.

Functor처럼 Monad도 많은 사용법이 있는데, 기술적으로 모든 모나드는 다음을 만족한다.

  • 일반값을 Monad식(Monoadic) 값 으로 lift하는 방법: 예를 들어, Future.unit은 일반값을 Future로 만든다.
  • 연이은 연산 2개를 이어서 실행하는 방법: Monad는 연산을 이어서 실행하게 해주는 방법이 포함된다. 위에서 flatMap은 그냥 Future 하나만 만들고 다음으로 넘어가는게 아니라, 앞의 Future가 끝날 때까지 기다렸다가 넘어가는 방법이 들어있다.

위에서 2개의 다른 연산(fmapflatten)으로 두번째 인터페이스(flatMap)를 만들 수 있다는 것을 확인했다. fmap 함수를 정의하는 Functor라면 이중 구조를 단순화시켜서 합치는(flatten) 연산이 필요해진다.

이제 처음의 질문으로 돌아가보자, Future들 여러개를 받는 함수를 어떻게 lift할 수 있을까?

다시 파일 예제로 돌아가서, 디렉토리의 모든 파일 내용을 합치려면 이런 코드가 될 것이다.

1
2
// concatF: (Future<String, ...) -> Future<String>
var resultF = concatF( text1F, text2F, ...)

이건 무슨 뜻일까? 입력받은 Future들이 잡고 있는 각 문자열들을 합친 것을 다시 잡고 있는 Future를 만들어준다. concatF는 입력받은 모든 Future들이 순서대로 처리되도록 기다려야하므로 결과로 나온 Future는 입력받은 모든 Future가 완료될 때 완료된다.

인자 2개를 받는 경우부터 시작해보자.

1
2
3
4
5
6
7
8
9
10
11
// fn: (Value, Value) -> Value
// lift2: ( (Value, Value) -> Value ) -> ( (Future, Future) -> Future )
Future.lift2 = function(fn) {
return (fut1, fut2) => {
fut1.flatMap( value1 =>
fut2.flatMap( value2 =>
Future.unit( fn(value1, value2) );
)
)
};
}

보이는 것과는 별개로 코드의 로직은 꽤 간단하다. 한줄씩 읽어보자면:

  • Future.lift2는 "일반값 2개를 다루는 함수"를 받아서 "Future 2개를 다루는 함수"를 리턴한다.
  • 리턴된 (lift된) 함수가 실제로 하는 일은
    • (중첩되어 실행되는) 2개의 연산을 순서대로 flatMap에 넣고
    • 첫번째 연산은 그 자체로 하는게 없지만 value1을 바인딩해서 스코프에 묶어두는 역할을 하고
    • 두번째로 중첩된 연산은 value1value2fn에 넘긴다.
    • fn은 일반값을 리턴하는데 flatMap은 받은 함수가 Future를 리턴해야하므로 Future.unit을 통해 일반값을 Future로 lift한다.

이게 트릭이다: 모든 Future에서 순차적으로 flatMap을 실행해서 모두 끝나길 기다린 다음에 모든 완료값이 한 스코프에 모였을 때 함수를 실행한다.

readDir 내부에서 readFile를 실행하는 것처럼 순차 연산으로 설명되는 Monadic 값과는 다르게 여러 인자를 한번에 lift하도록 마지막에 Future.unit를 사용했다.

파일 2개의 내용을 합치기 위한 예제다.

1
2
3
4
5
6
var concat2 = Future.lift2( (str1, str2) => str1+' '+str2 );

var text1 = readFileF('test1.txt', {encoding: 'utf8'});
var text2 = readFileF('test2.txt', {encoding: 'utf8'});

logF( concat2(text1, text2) );

두번째 Future text2가 첫번째의 text1보다 먼저 끝나더라도 text1을 기다리게 되고, text1이 끝나면 text2는 이미 끝났으므로 바로 함수를 실행한다.

여러 인자를 받는 함수는, 입력들이 언제 끝나는지나 의존성과는 관련없다는 것을 알 수 있다. 이걸 정리하면 다음과 같다.

  • fmap이 하나의 연산을 실행하고
  • flatMap은 순차 연산을 실행하지만
  • 여러 인자를 lift하는 함수는 병렬 실행이다.

위에서 봤듯이, Future들을 한번에 실행하고 연산이 진행되기 전에 이미 그 결과를 기다리고 있다.

lift2의 패턴을 lift3이나 lift4로 쉽게 확장할 수 있지만, 인자의 갯수와 관계없이 위에서 나온 중첩과 스코프를 통해 일반화를 구현해볼 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function toArray(args) {
return Array.prototype.slice.call(args);
}

Future.lift = function(fn) {
return function() {
var futArgs = toArray(arguments), // Future 인자들
ctx = this; // 컨텍스트(`this`)를 저장

return bindArg(0, []);

function bindArg(index, valArgs) {
// 현재 Future 인자를 기다린다
return futArgs[index].flatMap(function(val) {
valArgs = valArgs.concat(val); // 완료값들을 모은다.

return (idnex < futArgs.length - 1) ? // 아직 마지막 Future 인자가 아니라면
bindArg(index+1, valArgs) : // 다음 인자를 flatMap에 넘기고 기다린다
Future.unit( fn.apply(ctx, valArgs) ); // 끝까지 오면 모은 완료값들을 함수에 넘긴다
});
}
}
}

lift에서는 lift2의 패턴을 재활용했다. 인자가 몇개 들어올지 정확히 모르니 재귀를 통해 전체를 순회(iterate)하고, 완료를 기다렸다가 결과를 계속 넘겨서 모은다.(index번째의 Future를 기다렸다가 완료값을 저장하고, 모든 입력이 완료될 때까지 다음 Future 입력에 이 연산을 반복한다.) 마지막 Future까지 오면 함수를 실행하고 결과를 lift해서 리턴한다.

노트: 'Applicative Functor'라는 자료구조를 통해 n개 인자를 lift하도록 구현할 수 있지만, 그러려면 람다나 커리에 대한 설명을 해야하므로 오늘은 일단 생략하자.

에러 처리

위에서 fs.readFile의 에러값을 어떻게 뒀는지 다시 확인해보자.

1
2
3
4
5
function readFileF(file, options) {
var f = new Future();
fs.readFile(file, options, function(err, data) {
if(err) throw err;
...

실제로 작동하지 않는 코드다. 프로그램 흐름에서 떨어져서 실행중이므로 발생하는 에러를 잡을 방법이 없다. 위의 상황에서 에러는 상위로 전파되며 잡는 핸들러가 없어서 Node.js 전체 프로그램을 중단시킨다.

에러를 잡아 흐름을 고치려고 한다거나 의미있는 메시지를 사용자에게 전달하는게 필요할 수도 있다.

가능한 방법으로는 Future실패 의 개념을 붙여서 의미를 확장하는 것이 있다. 아직까지는 Future의 결과에 어떤 의미를 붙이지는 않았지만, 가능한 2가지 결과(완료 혹은 실패)로 Future를 생각해볼 수도 있다. 실패에 대한 경우가 포함되었는지 확인해보자.

먼저, 완료를 알리는 ready 메소드가 있으니 실패를 알리는 메소드를 정의해보자.

1
2
3
4
5
6
7
8
9
function Future() {
this.slots = [];
this.failslots = [];
}

Future.prototype.failed = function(slot) {
if(this.hasFailed) slot(this.error);
else this.failslots.push(slot);
}

Future가 실패할 때의 메소드도 정의해보자.

1
2
3
4
5
6
7
8
9
Future.prototype.fail = function(err) {
if(this.completed || this.hasFailed)
throw "이미 끝난 Future를 실패할 수는 없다!"
this.hasFailed = true;
this.error = err;
for(var i=0, len=this.failslots.length ; i<len ; i++) {
this.failslots[i](err);
}
}

이제 fmap를 다시 생각해보자.

readFileF(...).fmap( s => s.length) 예제에서 파일이 없을 때에 대한 처리가 없다. 제대로 읽었을 때에 대해서만 변환하기 때문에 아닐 때는 에러와 함께 실패할 것이다. 혹시 변환 중 실패할 경우에도 실패해야한다.

1
2
3
4
5
6
7
8
9
10
Future.prototype.fmap = function(fn) {
var fut = new Future();
this.ready( val => {
try { fut.complete( fn(val) ); }
catch(err) { fut.fail(err); }
});
this.failed( err => fut.fail(err) );

return fut;
}

flatten은 약간 복잡하다. 안쪽과 바깥쪽의 Future 2개가 있고, 각각 완료될 수도 실패할 수도 있다. 그래서 4가지(2x2) 경우를 다뤄야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Future.prototype.flatten = function() {
var fut = new Future();

// 1- 밖깥 실패 안쪽 실패 => 결과 실패
// 2- 바깥 실패 안쪽 완료 => 결과 실패
this.failed( _ => fut.fail(err) );

// 3- 바깥 완료 안쪽 실패 => 결과 실패
this.ready( fut2 =>
fut2.failed( err => fut.fail(err) );
);

// 4- 바깥 완료 안쪽 완료 => 결과 완료
this.ready( fut2 =>
fut2.ready( val => fut.complete(val) );
);

return fut;
}

flatten에서 안쪽과 바깥쪽 모두 완료되었을 때만 결과가 완료된다.

flatMaplift는 수정할 필요가 없다. 이미 fmapflatten의 의미를 가져오는 것이기 때문에 자동으로 에러에 대한 의미가 추가된다.

자, 이제 실패한 Future들은 연산에서 제외하게 만들었다. 그럼 실패한 Future들을 어떻게 다뤄야할까?

Future 에러를 잡아서 고치면 된다. 어떻게? 실패한 Future를 완료값으로 변이시켜서 원래의 연산에 포함시키면된다.

fmap과 비슷하지만 좌우반전같은 fmapError 함수를 만들 것이다.

1
2
3
4
5
6
7
8
9
Future.prototype.fmapError = funciton(fn) {
var fut = new Future();
this.ready( val => fut.complete(val) );
this.failed( err => {
try { fut.complete( fn(err) ); }
catch(err1) { fut.fail(err1); }
});
return fut;
}

fmapErrorcatch문의 비동기 버전처럼 작동하며, 정상적으로 완료되면 그냥 값을 넘기고 에러가 발생했을 때는 매핑 함수에 적용시켜서 완료값으로 넘긴다.

간단히 예제를 만들어보자

1
readFileF('unknown file').fmapError( err => 'alternate content')

그럼 에러를 Monad식으로 파이프라인처럼 다음 연산으로 넘기려면?

flatMap의 좌우반전같은 flatMapError를 만들어보자

1
2
3
Future.prototype.flatMapError = funciton( fn ){
return this.fmapError(fn).flatten();
}

예를 들어서 어떤 주소(URL)에서 내용을 가져오려고 할 때 요청이 실패한다면 다른 주소에서 가져오도록 시도를 하려고 하는데, flatMapError을 사용해서 앞의 실패를 잡아서 다른 요청을 만들 수 있다.

1
resultF = requestF('/url1').flatMapError( err => requestF('/url2') )

resultF는 첫번째 요청이 성공할 때 'url1'의 내용을 잡고 있고, 실패할 때는 'url2'를 요청해서 그 결과를 잡고 있다는 뜻이다.

부수효과

합성해서 연산할 수 있는 방법에 대해 필요한 것들을 모두 다뤄보았다. 지금까지 다뤘던 함수들을 통해서 Future를 동기 연산을 할 때처럼 일반값으로 넘겨서 비동기 처리를 하게 해봈다.

하지만 연산들은 끝까지 도달해야 결과가 나온다. 부수효과가 필요한 연산들을 다뤄 볼 시간이다. UI를 업데이트한다거나 콘솔에 로그를 찍는다거나 데이터베이스에 저장을 한다거나.

readyfailed 이벤트를 사용할 수도 있지만 좋은 방법은 아니라고 생각한다.

실제 어플리케이션에서 한 Future가 여러 자식 Future들을 가지고 그 Future들은 또 자식 Future들을 갖게 되는 트리같은 구조가 된다. Future하나가 완료돌 때 매핑된 Future들이 연쇄적으로 완료된다.

Future의 ready알림을 통해서 부수효과를 실행하려고 한다면 트리 내부에 있는 Future들 전체에 영향을 끼치게 된다. 의미적으로나 구현상으로나 업데이트가 끝날 때까지 부수효과 연산을 미뤄두는 것이 좋다. 예를 들어 DOM을 업데이트할 때는 requestAnimationFrame같은 스케쥴러에 맡기는게 더 좋을 수도 있다.

위에서 말한 이유로 do라는 메소드를 하나 만들텐데 부수효과 연산을 명시하는 것이다. fmap처럼 부수효과 함수를 받겠지만, 내부의 알림들(readyfailed)이 완료될 때까지 지연될 것이다.

예를 들어

1
requestF('/url').do( val => /* update ... */ )

이번에도 do의 의미와 리턴값이 무엇인지 생각해보자.

변이없이 그냥 Future를 리턴한다면 future.fmap( Id )(여기에서 Idx => x 같은 항등함수) 와 같은 형태이다. fmap과 다른 점은, 먼저 do에서 부수효과가 발생한다는 점이고 두번째는 다른 컨텍스트에서 실행된다는 점이다.(fmap은 즉시, do는 나중에). 가장 다른건 _의미_다.

정정: 2015년 4월 6일. Action이라는 새로운 타입을 통해 do를 적용했는데, 굳이 Monad(Future) 안에 다른 Monad(Action)을 넣어 복잡하게 만들 필요가 없었다. 서버에 데이터를 넘기거나 응답을 기다리는 등의 상황에서 리턴값이 필요할 수도 있는데, 다음 글에 이걸 개발해 볼 수도 있다.

빠르게 대충 구현해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Future.prototype.do = funciton(action) {
var fut = new Future();
if(this.completed) {
action(this.value);
fut.complete(this.value);
} else {
this.actions.push( val => {
action(val);
fut.complete(val);
});
}

return fut;
}

Future.prototype.complete = function(val) {
...
var me = this;
setTimeout( () => {
for(var i=0, len=me.actions.length ; i<len; i++)
me.actions[i](val);
});
}

덧붙여서, 비동기실행을 제대로 구현하려면 process.nextTick이나 MessageChannel 등을 사용해야 하지만 여기서는 간단히 구현하고 넘어가자. 비슷하게, 부수효과의 실패에 대응해 doError도 만들어야 하는데, do와 비슷하므로 각자 알아서 구현해보자. (Gist에 코드 전체가 있다)


역주 1: Promise와의 비교는 Future/Functor/Monad 개념을 이해하는데 관계없다고 생각해서 생략했다.

역주 2: 그림으로 설명하는 Functor, Applicative, Monad(번역)과 함께 읽으면 이해하는데 도움이 될 것이다.

번역, 스칼라로 전환

Translation of "Transitioning to Scala" into Korean, under the same license as the original.

2011년 말부터 2014년 초까지 전자상거래 솔루션 전문 에이전시, Nurun Toronto에서 리드개발자로 일했다. 자바와 스프링만으로 새로운 프로젝트들을 계속하다보니 대체제를 찾아야할 때라는걸 알게 되었다.

에이전시에서의 업무를 장기적인 관점에서 생각해봤다. "자바가 구리다"를 이해하지 못하는 고객들 때문에 마감에 시달렸다. 2004년도에 어플리케이션을 만들던 툴과 테크닉들은 2014년에는 별 도움이 되질 않았다. 2004년에는 코드 한줄을 테스트하기 위해 서버를 재시작하는게 당연했다 - 웹스피어는 재시작할 때 정체불명의 1200줄 XML 설정파일을 읽어오는데, 평균 걸리는 시간이 커피 한잔 마시고 오기 딱 좋은 120초 정도다. 요새는 이렇게 하면 에이전시나 개발자나 망한다.

우리가 만드는 어플리케이션들은 단순히 데이터베이스의 뷰어가 아니라, 매일매일 똥을 치워주는 소중한 도구다.

1. 왜 타입세이프의 스택을?

지난 프로젝트를 루비온레일즈로 진행해보고 뭘 싫은지를 알았다. 다른 동적 타입 언어도 인터프리트 언어도 상태기반 웹 프레임워크(stateful web framework)도 쓰기 싫었다. 자바 바이트코드로 컴파일되는 정적 타입 언어나 활발한 생태계를 가진 툴들, 그리고 확장성을 위한 무상태 웹 프레임워크가 더 괜찮았다. 또한 우리 고객사들은 믿을만한 회사를 통해 전문적인 기술지원을 받을 수 있어야 마음의 위안을 가졌다.

그래서 몇가지 선택지를 꼽아봤는데, 가장 먼저 타입세이프가 떠올랐다. 우리가 원하는 모든 것들이 있었다.

컨셉증명을 위해 초소형 전자상거래 사이트를 내부적으로 만들어서 스칼라의 단순함과 플레이의 개발 생산성에 대해서 시연했고 충분히 관심을 끌어서 결국 월마트 캐나다의 새로운 전자상거래 플랫폼의 토대가 되었다.

왜 스칼라가 복잡하다고 느낄까?

스칼라는 유연하다. 유연하다는 것은 단순하다는 것을 포기해야하는 일이지만, 다른 면으로 스칼라는 단순히 "자바보다 나은" 정도가 아니라 그보다 더 좋으며 매우 우아한 언어이다. 스칼라나 새로운 어떤 언어로의 전환이라는 큰 도전은 단지 기술적인 일만이 아니다. 능력있는 개발자라면 새로운 문법, 새로운 개념, 새로운 IDE를 배울 수 있다. 변화는 기술보다는 그 과정이나 문화같은 다른 면에서 어렵다.

짧게 말해서, 모든 것은 사람에 달려있다.

이 글의 뒷부분은 스칼라 프로그래밍 튜토리얼이 아니다. 이미 많은 글들이 있고, 고급 스칼라의 깊은 부분에 대한 최신 트릭을 가르칠 만큼 나는 인정받은 스칼라 개발자도 아니다. 이 뒤로는 스칼라로의 전환을 생각하고 있는 개발자들, 팀장 혹은 매니저들에게 전하는 조언들이다. 이 조언들은 기업용 스칼라 프로젝트를 이끌 때 개인적으로 한 경험을 토대로한 것들이다.

스칼라로의 전환을 생각 중인 매니저와 개발자들에게 하는 조언

스칼라, 커피 한잔보다 좋다!

1. 언어의 기능들을 이해해라

모든 스칼라 개발자, 팀장, 매니저는 마틴 오더스키의 스칼라 레벨 가이드를 읽어야한다.

전업 스칼라 개발자로 경력 1년반이 지나고 엔터프라이즈 스칼라 프로젝트도 진행했지만, 마틴의 가이드에서 스칼라 개발자 등급 A2.5/L1.5라고 생각한다. A3/L3에 있는 테크닉들을 사용하지만, 웹 어플리케이션을 쭉 개발해오면서 대부분은 써본 적이 없다. 케이크 패턴을 써본 적도 없고, 고계도 타입(high-kinded type, 역주: 스칼라 학교에서는 상류 타입이라고 번역했는데, 하나의 Layer 위에 있다는 생각으로 고계高階를 생각해봤다)을 써본 적이 아직 없다. 그렇다고 나쁜 개발자도 아니고, 가면현상의 증상도 아니고, 단지 내 시간은 한정되어있고 가장 돈이 되는 것에 집중하려고 한다. 게다가 드럼도 치고 기타도 치고 일주일에 두번 댄스 레슨도 다니고 커피도 많이 마시고 데이트하러 나가야한다. 시간은 소중하니까.

Walmart.ca 프로젝트에서는 콤비네이터 파서와 폴드를 사용하지도 않고 레벨 가이드의 얇은 부분만을 썼다. "얇은" 스칼라로도 이전 플랫폼보다 훨씬 좋은 생산성을 보여줬다. 구현하는데 골치아픈 일도 없었다. 그렇게 짠 코드들은 이전보다 더 관리하기에도 좋고 생산성도 더 좋았다. 블랙 프라이데이나 박싱 데이(역주, 북미지역 등지에서 추수감사절 시즌/크리스마스 시즌에 대부분의 쇼핑몰들이 매년하는 대량할인 이벤트 기간들)에서의 확장도 완벽하게 돌아갔고, 많은 자바기반의 전자상거래 플랫폼은 하지 못했던 것들이다.

그래서 중요하지 않다는건가?

단순하게 스칼라를 쓴다는 것을 스칼라가 부족하다는 것으로 착각하지 마시길. A1과 A2 등급에서 익힐 수 있는 것들을 보자.

  • 간단한 클로저
  • map, filter 등의 콜렉션
  • 패턴 매칭
  • trait 합성
  • 재귀
  • XML 표현식

(역주: 코멘트에서 XML 표현식은 SWIFT에서 사용중이라고 한다.)

자바에 몇개 더 추가한 것과 비슷하다. 서술하듯이 개발하고 소프트웨어를 관리하는 새로운 방법이다. A3에 있는 몇개도 익히기 꽤 쉽고 - Akka나 다른 병렬 처리 라이브러리들을 사용하기 위해 꽤나 중요한 것들인데 - 그에 비해 크게 어렵지 않고 수학 학위가 필요할 정도는 아니다.

  • fold
  • stream, 혹은 지연평가 자료구조
  • actor
  • combinator parser

이런 테크닉들을 익혔을 때의 좋은 부가효과는, 사용하는 모든 언어에서 더 좋은 개발자가 된다는 것이다. 나는 스칼라와 자바스크립트 모두에서 클로저나 다른 테크닉들을 익히는데 정말 도움이 됐고 더 좋은 자바스크립트 프로그래머가 되었다.

2. 시간을 써라

자바에서 건너온 많은 스칼라 개발자들은 바로 적응하고 싶어하지만 스칼라는 완전히 다른 언어다. 새로운걸 익힌다는 것은 연습을 필요로 한다. 스칼라도 예외는 아니다.

좋은 소식은 A2/L1 등급만으로도 충분히 스칼라 어플리케이션을 만들만한 자격이 있다는 것이다. 모든 스칼라 개발자가 고급 순수함수 자료구조, 타입 이론, 고계도 타입을 이해하고 있을 필요는 없다. 하지만 스칼라는 차세대 어플리케이션을 만드는 전문 개발자를 위한 프로그래밍언어라는 것이다. 그래서 배우고 체득하는데 많은 시간이 걸릴 것이다.

3. 배우는걸 두려워말라

자바 개발자라면 다음과 같은 자료들을 통해 스칼라를 배우길 강력히 추천한다.

  • Scala for the Impatient를 읽어라. 특히 예제가 필요한 성격급한 개발자들에게 좋은 시작점이다. (역주, 번역판 쉽게 배워서 빨리 써먹는 스칼라 프로그래밍, 2013 비제이퍼블릭)
  • 마틴 오더스키, 렉스 스푼, 빌 베너스의 Programming in Scala를 읽어라. Scala for the Impatient보다 더 자세한 책이라 언어의 기능들에 대해 폭넓은 시야를 익히기에도 좋다. (역주, 번역판 Programming in Scala, 2014 에이콘)
  • 가능하면 Coursera의 Functional Programming Principles in Scala 코스를 들어라. Coursera가 Scala로 만들어졌다.
  • Typesafe Activator 템플릿들을 살펴봐라. 다른 언어들에 비해 온라인 문서나 학습자료가 부족하기도 하지만, 다른 사람 코드를 분석하는게 제일 좋은 방법이기도 하다. 특히 James Ward같은 능력있는 개발자들이 짠 코드라면.
  • 가능하면 Coursera의 Principles of Reactive Programming 코스를 들어라. 스칼라와 대량 데이터 처리를 위한 Akka를 사용하려는 개발자들에게 좋은 자료다.
  • 스칼라 모임에 참석하고 스칼라를 사용하는 실무자들이 어디에 쓰는지 배워라.
  • Functional Programming Patterns in Scala and Clojure를 읽어라. 예전의 명령형 스타일 코드와 더 읽을만해진 함수형 스타일 코드를 비교해보고 더 함수형의 언어를 배우고 싶어졌다. 하스켈은 좀 과하고, 그래서 Clojure를 배우기 시작했다. 그리고나서 내 스칼라 코드는 더 간략해지고 더 의미있어졌다.

4. 온라인에서 읽는 것들은 적당히 감안해서

Scalaz를 만든 토니 모리스처럼 다른 세계에서 온 똑똑한 개발자들이 많이 있다. 하스켈 세계나, 함수형 프로그래밍, 그리고 수학 분야.

토니는 다음과 같은 함수 선언에 반대를 한다.

1
def reverse[A]: List[A] => List[A]

그리고 이런 선언을 더 선호한다.

1
def <-:[A, B](f: A => B): List[A] => List[B]

다음에 <-:라는 이름의 함수를 보면 이렇게 생각해라, "으악, 읽기도 구리고 내가 뭘 하고 있는거야?", (아마 친근보다는 좀 강력한) 다른 툴은 없는지? 이거 타입은 뭐지? 대수적인 속성은 뭐지? 드러난 속성들이 또 뭐가 있지?

Sticks, stones, but names are not useful to me by Tony Morris

이게 스칼라의 미학이다. 토니도 맞다. 틀린건 없고, 그냥 개인과 팀의 선택에 대한 문제다. 나는 <-:보다 reverse를 더 선호하지만, 내가 가독성과 단순성을 선호하는만큼 토니같은 개발자들은 수학적인 순수성과 사실성을 선호한다. 이 스타일들은 항상 다른 것 같다. 토니는 라이브러리들을 개발했고, 나는 라이브러리들로 어플리케이션을 만들고, 우리 둘 다 스칼라로 개발한다. 나는 가끔 var를 쓰고 나중에 걷어내지만, 누군가는 그걸 질색한다.

그런데 팀에서 사람들이 영어, 불어, 독어 같이 한 언어가 아닌 여러 언어를 쓴다고 해보자. 이럴 때 함수 이름을 영어 동사로 써도 그러려니 한다. 내가 같이 일했던 캐나다 사람들은 영어와 불어를 쓰는 사람들이 많았고, 헝크러진 머리나 좁쌀만한 눈(역주, 미국인이 캐나다인을 놀릴 때 주로 쓰는 표현)보다는 보기 괜찮다고 확신할 수 있다. 젠장(Sacré bleu!)

스칼라처럼 독선적이지 않은 언어는 그래서 아름답다. 자기 스타일을 자유롭게 적용할 수 있고, 언어가 그걸 방해하지 않는다. <-:도 쓸 수 있다는게 마음에 든다.

5. 주머니가 허락한다면 기술지원을 받아라

나는 운좋게도 타입세이프의 Nilanjan Raychaudhuri와 Roland Kuhn같은 진짜 고수들에게 배워서 Nurun에 설계 리뷰, 코드 리뷰, 페어 프로그래밍을 도입할 수 있었다. 새로운 프로그래밍 스타일을 배운 덕분에 신뢰도를 월등히 높인 프로젝트를 진행하면서 다방면으로 값으로 따질 수 없는 도움을 받았다.

단순히 스칼라의 함수형 스타일뿐만 배운게 아니라, 리액티브 프로그래밍 컨셉도 배웠다. Play와 Akka도. 새로운 테크닉들과 프로젝트 전반에 걸쳐 타입세이프의 도움을 많이 받았다. 우리가 항상 제대로 된 길을 가고 있는지에 대해 확신을 받았다.

타입세이프의 이메일 지원 역시 훌륭하다. 지원 구독은 주머니가 허락한다면 지출할 가치가 충분히 있다.

6. 다양성을 포용하라

스칼라 커뮤니티에는 전세계의 다양한 프로그래머들이 모여있다. 나처럼 전직 자바 개발자도 있고, 학계에서 온 사람들도 있다. 독학한 개발자도 있고 박사학위자들도 있다. 빠듯한 예산으로 사업문제를 해결하려 하는 사람들도 있고, 관심분야를 넓히려고 하는 사람들도 있다. 어플리케이션을 만드는 사람도 있고, 라이브러리를 만드는 사람도 있다.

커뮤니티의 모든 사람을 존중하고 이해하는게 좋은 개발자가 되는데 중요하다. StackOverflow에 단순한 질문을 올렸는데, 이해하려면 카테고리 이론을 몇년 배워야하는 난해한 답변이 달릴지도 모른다. 하지만 스칼라는 아직 새로운 언어이고 커뮤니티는 자기 색깔을 찾아가고 있다는 것을 염두에 둬라. 학계 출신이 아닌 개발자들이 스칼라 세계에 더 많아지고 더 많이 답변하다보면 토론은 조금 더 이론 개념보다는 어플리케이션 개념으로 옮겨갈 것이다.

답변에 실망했다면, 트위터의 Finagle이나 mDialog의 Smoke같은 라이브러리들의 소스코드를 봐라. 두 프로젝트는 제품 레벨에서도 많이 쓰이는 구현체로 훌륭한 스칼라 예제이다. 모든 스칼라가 어마어마하게 복잡하지는 않다.

7. 현실적인 목표를 설정하라

자바에서 온 신입 스칼라 개발자들은 하룻밤만에 고급 스칼라, 함수형 스칼라를 배울 수 없다는 사실을 깨달아야한다. 전형적인 비지니스 어플리케이션을 성공적으로 개발하는데 고급 함수형 스칼라가 필요한건 아니다.

함수형 프로그래밍을 접해본 개발자라면 스칼라 스타일로 익히데 시간이 덜 걸릴 것이다. 그렇지만 팀원들 대부분이 명령형 언어 개발자들이라면 스타일을 맞춰야할 것이다.

그리고 팀 밸런스에 대한 것인데, 유지보수해야할 사람이 이해할 수 있는 코드를 짜야할 것이다. 아무리 전세계에서 가장 우아한 코드라도 관리할 수 없으면 쓸모없다.

8. 짝코딩과 코드리뷰는 의무

짝코딩을 하면 팀 전체 스타일과 기술 평균에서 너무 멀어지지 않게 해주는데 효과적이다. 마지막으로 바라는게 필멸자들이 감히 범접할 수 없는 어려운 코드를 짜거나, 팀원들이 다들 준비가 될 때까지 다시 완벽하게 작동하는 명령형 코드로 짜는 것이다. 실험은 중요하지만, git은 두었다 무엇하는가. 포크해라, 두번해라.

팀원들이 이렇게 된다

스칼라의 유연성 덕분에 복잡함의 칼날을 피하는 것도, 언어의 새 기능이나 스칼라의 표현력을 익히기 쉽다. 문화는 언제나 개발팀에게 중요하지만, 더 중요한건 스칼라를 처음 배울 때 다같이 페달을 밟아 나가야한다는 것이다.

9. 간결함을 유지하라

스칼라는 새로운 것이고, 사람들은 무엇을 써야하고 무엇을 피해야할지 여전히 배우는 중이다. 더글라스 크록포드가 스칼라를 마스터하고 Scala: The Good Parts를 쓰기 전까지는, 언어의 각 부분에 대한 가치를 알아서 확인해야한다. 옳고 그름은 없고, 단지 시도와 실패만 있을 뿐이다. 뭐가 더 맞는지에 대해 얼마든 질문해라.

Reflection API가 처음 자바에 도입되었을 때, 모든 자바 개발자들이 자신들의 지적 능력을 동원해 모든 것에 리플렉션을 사용하려고 했다. 당시 내가 개발하던 코드들은 관리하기가 더럽게 복잡해졌고, 한 개발자가 미쳐날뛰어서 이해하지 못하는 기능을 남용했다는 것 말고 다른 이유는 없었다. 모든 생소한 기능은 발을 담그기 전에 천천히 깨끗하고 심플한 코드를 짜는게 더 낫다. 고급 테크닉을 이상하게 뒤죽박죽으로 구현한 것보다 깔끔하게 명령형 스타일로 스칼라 코딩을 하는게 차라리 낫다.

준비가 되기 전에 깊게 들어가지마라. 천천히 가자.

좋은 음악처럼 좋은 코드도 우아하고 드물다. 좋은 음식에 꼭 좋은 재료가 들어가는건 아니다. 상상할 수 있는 모든 향신료가 들어간 음식을 먹고 싶어할 사람이 있을까? 코드를 쓰는 것도 그렇다. A1급 개발자가 쓴 신뢰할만한 코드는 자기가 뭘 하고 있는지 왜 하는지도 모르며 제멋대로 짠 A3/L3급 개발자의 코드보다 더 관리하기 쉽다.

10. 구린 코드를 살펴봐라

심각하게 구린 스칼라 코드를 짤 수도 있고, 자바, 펄, 그리고 영어도 마찬가지다.

하지만 구린 자바코드와 구린 스칼라코드의 중요한 차이가 있다.

구린 스칼라 코드는 좀 다른 방식으로 구리다. 명령형으로 구리거나, 함수형으로 구리거나, 혹은 두가지가 섞인 채로 구리거나. 이해할 수 없을 정도로 구리다면 익숙하지 않은 스타일이라서 그럴 수도 있다. 스칼라는 새로운 언어라, 자바같은 성숙된 언어처럼 바로 안티패턴을 발견해내는게 아직은 어렵다. 그래서 개발팀들이 아름다운 코드를 구리다고 착각할 수도 있고, 구린 코드를 아름답다고 착각하게 될 수도 있다. 개발자들은 배운대로 구린 패턴을 짜기 시작하면 나중에는 더 구린 코드가 나온다. 그렇게 악순환이 된다.

나중에 고치려고 하는 것보다 처음부터 피하는게 더 좋다.

똑똑한 개발자가 스칼라로 이상하게 코딩한다면 불러봐라. 질문해라. 익히지 못한 언어에 대해 추측하지마라. 최악의 경우는, 잘못짰으면서 우아한 코드라고 생각은 하는데, 우아한지 이해하지 못하는 경우다.

11. 스칼라는 단지 퍼즐의 일부분

웹 어플리케이션을 개발하는 방법은 10년전에 비하면 매우 다양하다. 요새는 스칼라, 플레이, AngularJS, MongoDB 앱을 개발한다. 내가 짜는 코드 대부분은 클라이언트단이다. 몇년간은 스칼라보다 Angular를 더 많이 짰는데, 나쁘다는게 아니라 그냥 현실이 그렇다.

스칼라의 미학은 자바처럼 쓸데없는 밑바닥을 만들어야하거나 루비같은 동적 언어의 불안함을 걱정할 필요없이 깔끔하고 안정적이고 성능좋은 서버단 코드를 짤 수 있게 해준다는 것이다. 스칼라로 짠 서버쪽 로직은 견고하기에 클라이언트쪽 코드를 안정적으로 짜는데 시간을 투자할 수 있다.

스칼라의 모든 쪽에서 마스터가 되고 싶어하는만큼 파고들어야할 기술들이 너무 많다. HTML5, SASS, AngularJS, RequireJS, SQL, MongoDB, 또, 또, 또.

한 언어의 모든 면을 마스터할 시간은 없겠지만, 스칼라는 맛을 보기만 해도 괜찮은 기술이다. Reactive Programming은 다음 세대의 대세가 될 것이며 그 패러다임 전환의 선두에 스칼라가 있을 것이라 믿는다. 성능과 안정성을 모두 얻을 수 있는 리엑티브 어플리케이션을 무시하기는 힘들다.

요즘엔 대부분 묵직한 XML 대신 JSON을 쓴다. SOA 패턴 대신 REST를 쓴다. 데스크탑 대신 모바일을 쓴다. 어마어마한 크기의 데이터에서 필요한 정보를 뽑아낼 때, Akka의 성능이라면 막대한 하드웨어를 투자하지 않고서도 가능하다.

스칼라는 퍼즐의 한 부분일 뿐이지만, 새로운 종류의 개발을 위해 필요한 다른 많은 부분들의 심장과도 같다.

12. 스칼라를 배우면 더 좋은 프로그래머가 된다

직장인 개발자가 자기 영역을 넓히는건 정말 드문 일이다. 소프트웨어 개발에서 완전히 다른 접근법을 배워본게 마지막으로 언제인가?

마지막 전환(그리고 내 경력에서 겪었던 유일한 전환)은 절차지향 언어에서 객체지향 언어로의 변환이었다. CIBC에서 인턴하던 1998년에 운좋게도 첫 자바 어플리케이션 개발자 중 하나가 될 수 있었다. 대부분의 개발자들은 전직 COBOL이나 C였다가 전환하는 시점이었다. 요새는 뭐든 다 자바를 쓰지만, 당시에 윈도우와 OS/2에 모두 배포해야하는 상황에서 자바는 매우 실용적이었다.

2-30년 경력의 개발자들(몇명은 실제로 펀치카드로 프로그래밍을 해봤던)과 일하면서 좋은 경험을 쌓았고, 한가지 스타일에 매이면 안된다는 것을 깨달았다. 자바를 배울 때 JCL도 관리해야했다. 바로 다시 복귀하기 몇달전까지는 old COBOL과 360 어셈블리도 파고들었다. 넓게 보자. JCL과 COBOL이 섹시한 언어는 아니지만 필요한 분야에서는 괜찮은 언어다. 인턴시절 스몰토크에서 엑셀까지 모든 것을 겪어볼 수 있었다. 엑셀은 처음 접한 함수형 프로그래밍이다. (역주: 엑셀이 함수형 프로그래밍을 지원하는지에 대한 StackExchange의 글이 댓글에 있다.)

스칼라는 부당한 평을 많이 받았다. 어떤 개발자들은 생각을 깊게 하기보다는 잠깐 시도해보고 익숙한 언어로 도망친다. 문제는 그 사람들이 인터넷에 남긴 불평들 때문에 관심있어하는 개발자들에게 언어의 가치가 잘못 전달될 수도 있다.

스칼라에 대한 불만을 읽는다면 누가 썼는지 찾아보기를. 다른걸 원한 사람일 수도 있다. 쉽게 흔들리지 말고, 불평하는 사람들에게 얽매이지 마라. 점점 널리 퍼져가는 스칼라의 성공사례들을 찾아보기를 바란다.

결론

스칼라는 기술뿐만이 아니라 문화적인 투자다. 투자할만한 가치가 있는 보상이 있는데, 어쨋든 해봐야하지 않을까? 확장가능하고 믿을만하고 관리하기 편한 프로젝트를 진행중이라면, 혹은 프로그래머로서 사고를 확장하고 싶다면 단언컨데 스칼라는 할만 하다. 기본만 있으면 보이는 것만큼 어렵지도 않다.

스칼라는 실무에서도 쓸만하다는걸 기억해라, 아니 이미 쓰이고 있다.