[CoffeeScript] 왜 커피스크립트를 사용하지 않는가

아랫글은 2016년에 썼던 글인데 왜인지 모르게 아직 publish 안 하고 있었다. 그 사이에 ES2015 (ES6)의 변경을 추가 한 커피스크립트2 가 나왔다. 하지만 이미 ES2015를 넘어 ES2017 도 나왔고, 브라우저들도 ES2016 는 네이티브로 지원하고 있기 때문에 앞으로도 커피스크립트를 쓸 일은 없을 것 같아 발견한 김에 publish 한다. 커피스크립트 는 자바스크립트 코드를 간결하게 만드는 것을 목표로 만들어진 언어다. 2009년 첫 버젼을 릴리즈 하였고, 2010년 12월 1.0이 릴리즈 되었다. 내가 커피스크립트를 처음 썼던 것은 1.0이 릴리즈 된 지 조금 뒤인 2011년 경이었던 것 같다. 지금도 자바스크립트 코드가 다른 언어에 비해 간결하지는 않지만, 당시 자바스크립트 코드는 지금보다도 verbose 하였기 때문에 꽤 애용하였었다. 그러다가 웹 말고 다른 일을 하다 보니 자바스크립트를 사용하지 않게 되었고 자연스럽게 커피스크립트도 안 쓰게 되었다. 그러다가 2014년경 잠시 웹 개발을 하게 되었는데 이때 습관적으로 다시 커피스크립트를 사용하였었다. 하지만 그것도 잠시였고, 그 뒤로는 사용하지 않게 되었다. 더 이상 커피스크립트를 쓰지 않게 된 이유는 크게 2가지였다. 일단 커피스크립트의 문법은 너무 애매했다. 커피스크립트가 가장 중요하게 생각하는 요소 중 하나는 자바스크립트로 일대일로 매칭되는 것이다. 하지만 처음 커피 스크립트를 보면 자바스크립트 같은 느낌이 전혀 들지 않는다. 이는 커피스크립트 코드에서는 괄호를 거의 사용하지 않기 때문이다. 사실 일부 기능을 제외하고 대부분의 커피스크립트 코드는 적절한 위치에 괄호를 추가하는 것으로 자바스크립트 코드로 변환할 수 있다. 이는 커피스크립트의 설계자가 자바스크립트의 괄호가 자바스크립트 코드를 복잡하게 만든다고 생각했기 때문이다. 하지만 실제로 사용해보면 이는 딱히 편하지 않다. 물론 괄호가 없기 때문에 타이핑은 많이 줄어든다. 하지만 코드를 작성해본 사람은 알겠지

[C++] copy elision - 복사 생성자는 생략될 수 있다

위의 코드를 실행하면 무엇이 출력될까? A 의 기본 생성자 로 인스턴스를 생성하고 이것을 a 에 복사하는 복사 할당 이 한 번 불렸으므로 " 0 1 "이라고 생각할 수 있다. 하지만 위의 코드에서는 복사 할당이 불리지 않는다. 할당자는 이미 초기화돼 있는 값에 새 값을 할당하는 연산자이기 때문이다. 따라서 위의 코드는 사실 A a(A()); 와 같은 의미이고 복사 생성자 가 불리는 코드다. 이를 복사 할당자를 불리게 하고 싶으면 아래와 같이 a 가 초기화된 상태에서 값을 대입해야 한다. 그렇다면 처음 코드는 정말로 " 1 0 "을 출력할까? 사실 이건 C++ 버전에 따라 다르다. 우선 C++ 14 까지는 " 1 0 " 혹은 " 0 0 "이 출력된다. 이는 C++ 14까지는 어떤 레퍼런스에도 바인드 되지 않는 temporary object 를 인자로 받는 이동 생성자와 복사 생성자를 생략하는 copy elision 을 허용하기 때문이다. 보통 최적화는 실행 결과를 변경시키지 않기 위해서, side-effect가 없는 경우에 대해서만 허용하는 것이 보통이다. 위의 코드는 전역 변수를 수정하기 때문에 side-effect가 있는 함수고, 따라서 최적화되지 않을 거로 생각하기 쉽다. 하지만 copy elision은 복사 생성자와 소멸자가 side-effect가 있는 함수라도 허용된다. 즉, 어떻게 최적화했는지에 따라 코드의 실행 결과가 달라질 수 있다는 것이다. 따라서 위의 코드는 copy elision이 됐다면 " 0 0 "이 출력될 것이고, copy elision이 되지 않았다면 " 1 0 "이 출력될 것이다. C++ 17에서 위의 코드는 반드시 " 0 0 "를 출력한다. prvalue 를 인자로 받는 복사/이동 생성자를 없애는 copy elision을 반드시 수행하도록 스펙이 수정됐기 때문이다.

[CppCoreGuidelines] not_null - null이 될 수 없는 값 구분하기

Null pointer dereferencing 은 C++을 사용하다 보면 자주 발생하는 문제다. 값이 없을 수 있는 객체를 지칭할 때 포인터를 사용하고 값이 없는 상태를 null로 표현하는 C++에서 이를 근본적으로 회피할 방법은 없다. 따라서 null일 수 있는 포인터는 사용하기 전에 항상 체크하고 사용해야 한다. 하지만 모든 포인터가 null이 될 가능성을 가지고 있는 건 아니다. 로직 상으로 일부 포인터들은 null이 될 수 없다. 반드시 존재하는 객체의 주소를 가리키고 있을 수도 있고, 이미 null인지 체크한 포인터일 수도 있다. 이런 포인터까지 사용하기 전에 null인지 체크하고 사용하는 건 귀찮고, 추가 비용만 들어간다. 이런 경우 과거에는 레퍼런스 를 이용했다. 레퍼런스는 선언 시 반드시 초기화해야 하므로 레퍼런스가 가리키는 객체는 null이 아닐 것이라는 생각에서였다. 하지만 사실 레퍼런스도 null pointer dereferencing에 대해서 그다지 안전하지 않다. 위와 같은 함수를 아래처럼 포인터를 받아서 부르는 경우를 생각해보자. 위의 코드는 여전히 null pointer dereferencing 문제를 가진다. f5 가 인자로 받은 t 의 null 체크를 하지 않고 f4 로 넘겼기 때문이다. 게다가 레퍼런스로 부르는 방식은 modern c++에 스마트 포인터가 들어오면서 일반적으로 사용할 수 있는 방법은 아니게 됐다. shared_ptr 과 unique_ptr 은 포인터의 semantic을 그대로 따르기 때문에 null이 될 수 있다. 하지만 null이 될 수 없는 shared_ptr 과 unique_ptr 를 reference로 표현할 수 없다. 따라서 포인터 시멘틱을 따르는 타입이지만, null이 될 수 없는 객체를 표현할 일반적인 방법이 필요하다. C++ Core Guidelines 는 null이 될 수 없는 포인터 계열의 변수는 not_null<T> 이라는 클래스를 사용하기를 권장한다. no

[CppCoreGuidelines] 포인터 구분해서 쓰기 - span, owner

C++을 쓰는 사람들이 가장 어려워하는 것 중 하나가 포인터다. 그중에서도 함수 포인터 를 읽고 해석하는 것이 가장 어렵다고 한다. 하지만 실제 코드에서는 함수 포인터를 볼 일은 거의 없다. 특히 modern c++에서는 가능하면 std::function 를 쓰는 걸 권장하기 때문에 몇몇 특수한 목적을 가진 코드를 제외하고는 함수 포인터를 볼 일은 거의 없다. 그다음으로 어려운 것은 메모리 관리다. C++에서 전통적으로 많이 발생하던 문제가 double free와 memory leak이다. 이는 C++에서 포인터로 가리키는 객체의 소유권이 명확하지 않기 때문이었다. 이를 해결하기 위해 C++11에서는 소유권을 혼자 차지하고 있는 std::unique_ptr 과 소유권을 공유하는 std::shared_ptr 을 만들었다. std::unique_ptr 과 std::shared_ptr 을 잘 활용하면 dobule free와 memory leak은 예방할 수 있다. 하지만 C++11 이후에도 여전히 포인터는 다양한 역할을 가지고 있다. 현재 C++의 포인터에 남은 역할은 다음과 같다. std::unique_ptr 을 사용하지 않지만, 소유권을 넘길 때 함수의 인자로 배열을 넘길 때 문자열을 가리킬 때 소유권을 넘기지 않고 하나의 객체를 가리킬 때 스마트 포인터를 사용하지 않지만, 소유권을 넘길 때 앞에서 말했듯이 C++11은 std::unique_ptr 과 std::shared_ptr 를 도입하여 소유권을 관리할 수 있도록 하였다. 하지만 스마트 포인터를 사용하지 못하는 경우도 있다. 이 경우 적절한 지점에서 객체를 소멸시켜줘야 한다. 하지만 이를 지칭하는 것이 단순히 포인터이기 때문에, 메모리를 소멸시켰는지, 한 번만 소멸시켰는지 알기 어렵다. 그래서 C++ Core Guidelines에서는 이 경우 owner<T> 라는 클래스를 사용하는 것을 권장한다. owner<T> 클래스는 아무런 일도 하지 않는 클래스다. 사실 클래

RLP encoding

RLP( Recursive Length Prefix )는 임의의 깊이와 개수로 중첩된 배열을 binary data로 표현하는 인코딩 방식이다. 인코딩할 데이터 앞에 binary data의 길이를 추가하는 방식으로 동작하기 때문에 Length Prefix라는 이름이 붙었다. 현재 RLP는 이더리움이 patricia tree 를 만드는 데만 이용되고 있지만, 스펙 자체는 일반적으로 사용할 수 있도록 정의돼 있다. RLP의 input은 binary data이다. 그 값의 이름이 무엇이고, 어떤 타입이고, 어떤 representation을 가지는지는 RLP에서 정의하지 않는다. 이는 별도의 규약을 정하여 RLP 인코딩을 하기 전에 binary data로 변경해야 한다. RLP가 인코딩하는 방법은 인코딩할 입력이 무엇인지에 따라 달라진다. ASCII 우선 입력이 한 바이트의 일반 ASCII 캐릭터. 즉, 0x00에서 0x7F에 해당하는 값이라면 별도의 length prefix 없이 바로 사용한다. 문자열 입력이 한 바이트 ASCII 캐릭터가 아닌 바이트의 배열. 즉, 문자열이라면, 앞에 배열의 길이를 prefix로 붙이고, 그 뒤에 바이트의 배열을 그대로 사용한다. 다만 여기서 말하는 문자열은 일반적인 프로그래밍에서 말하는 문자열이 아닌, 바이트의 배열로 봐야 한다. RLP는 배열이 아닌 다른 타입은 구분 없이 문자열로 받는다. 예를 들어 문자에는 ASCII를 쓰고, 정수는 zero padding 없는 big endian을 사용한다고 했을 때, 문자열 'ab'도 [0x61, 0x62] 로 표현되고, 정수 24,930도 [0x61, 0x62] 로 표현된다.실제 이 값을 정수로 해석해야 하는지, 문자로 해석해야 하는지는 RLP보다 상위 레이어에서 결정하여야 한다. 여러 바이트의 문자열은 몇 바이트가 인코딩됐는지 length prefix를 붙여 인코딩하는데, length prefix를 어떻게 붙일지는 문자열의 길이에 따라 달라진다. 만약

Raft - electionTimeout

broadcastTime ≪ electionTimeout ≪ MTBF Raft가 정상적으로 동작하기 위해서는 반드시 위의 조건을 만족해야 한다. electionTimeout은 leader election에서 설명한 random 한 timeout의 최대치를 의미한다. 사실 broadcastTime, electionTimeout, MTBF 중에서 사용자가 설정할 수 있는 것은 electionTimeout 뿐이다. 따라서 위의 조건을 만족시킨다는 것은 적절한 electionTimeout을 선택한다는 것이다. MTBF 는 평균 무고장 시간(Mean time between failures)의 약자로, 한 서버가 시작한 뒤 죽기 전까지의 평균 시간을 의미한다. 보통 MTBF가 길면 availability가 높지만, availability와 일치하지는 않는다. MTBF가 길더라도 MTTR (Mean time to repair)가 길면 availability은 떨어질 수 있다. MTBF는 시스템의 고유한 속성이다. MTBF는 보통 노드가 하는 일의 종류와 개발자의 숙련도, 얼마나 비싼 하드웨어를 사용하는지에 따라 결정된다. Raft paper 는 electionTimeout을 MTBF보다 작게 할 것을 권장한다. 실질적으로 MTBF는 최소 몇 주에서 몇 개월 정도 되기 때문에 electionTimeout을 이보다 더 크게 설정하는 것은 쉽지 않다. 이는 그냥 electionTimeout을 너무 크게 설정하지 말라는 정도로 받아들여도 된다. 리더가 죽은 뒤 electionTimeout 동안 client의 요청을 전혀 처리 못 하니 네트워크 전체의 availability를 올리기 위해서 가능하면 작은 electionTimeout을 설정해야 한다. 하지만 electionTimeout을 너무 작게 설정하면 안 된다. electionTimeout은 아무리 작아도 broadcastTime보다 커야 한다. broadcastTime이란, 한 노드에서 네트워크 안의 다른 모든

Raft - consistency

Raft는 모든 결정을 leader가 맡아서 한다. 따라서 term이 변경되기 전에는 leader의 결정을 따르면 된다. 문제는 leader에 문제가 생기거나 네트워크 파티션으로 인해 leader가 변경되고 다음 term으로 진행된 경우다. Consistency를 위해 가장 이상적인 것은 모든 노드가 하나의 leader만 따르도록 하는 것이다. 하지만 이는 사실상 불가능하다. 이게 가능하면 애초에 합의에 도달한 것이다. 그래서 Raft에서는 특정 시간에 2개 이상의 리더가 존재할 수 있다. 단, state를 변경시킬 수 있는 리더는 1개 밖에 있을 수 없다. 이 두 말은 별 차이 없는 것 같지만, 이 차이가 분산 환경에서 구현 가능한 시스템이 되도록 만들어준다. Raft에서는 leader에 커밋 된 로그만이 state를 변경시킨다. Leader가 커밋하기 위해서는 네트워크에 참여하는 노드 과반의 동의가 필요하다. 새 leader가 선출되면 과거의 leader는 절반 이상의 지지를 받지 못한다. 모든 요청에 요청하는 노드의 term이 담겨있고, 요청받은 쪽은 자신의 term보다 작은 term인 노드가 보낸 요청은 모두 거절한다. 새 leader가 선출됐다는 것은 이미 절반 이상의 노드가 다음 term으로 넘어갔다는 것이고 과거의 leader를 지지하는 노드는 절반이 되지 않기 때문에 과거 leader는 더 이상 상태를 변경시킬 수 없다. 따라서 같은 시간에 두 개의 노드가 상태를 변경시키는 것은 불가능하다. 물론 leader가 아닌 노드들이 가지고 있는 상태는 consistent 하지 않다. 새 RequestVote를 받기 전에 과거의 leader가 보낸 AppendEntries 메시지를 받고 자신의 상태를 변경시킬 수 있기 때문이다. 하지만 네트워크의 상태는 리더에 커밋 된 로그를 기준으로 만들어지기 때문에 각 노드의 inconsitecy는 클라이언트가 보는 네트워크 상태에 영향을 주지 않는다. 그렇다면 leader에 커밋 된 로그를 가지지 않은

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

RAII는 무엇인가

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API