2월, 2018의 게시물 표시

좋은 코드를 많이 봐야 한다

얼마 전 트위터에서 재밌는 이야기를 봤다. 은행에 입사하면 위조지폐를 가리는 훈련으로 진짜 돈을 계속 만지게 한다는 말을 들었다. 진짜에 익숙해지면 가짜를 접했을 때 바로 알게 된다고. 가짜를 가리기 위해 왜 가짜인지를 공부할 필요는 없다고. — 연 (@b__5k) 2017년 2월 22일 가짜를 알기 위해서 가짜를 공부할 필요가 없다는 글인데, 이 트윗을 보니 어렸을 때 봤던 갓핸드 테루라는 의료 만화가 떠올랐다. 갓핸드 테루는 신입 의사인 마히가시 테루가 수련을 받으며 명의가 돼가는 과정을 그린 의료만화인데, 그중에서 다음과 같은 에피소드가 나온다.
 주인공 테루가 슬럼프에 빠져 엑스레이 판독을 못 하게 되자 선배 의사가 테루에게 과제를 하나 내준다. 어느 환자의 엑스레이 사진을 주면서 이 환자의 문제가 무엇인지 찾아오라는 것이었다. 테루는 열심히 고민해보지만 결국 문제를 찾지 못하고 문제를 냈던 선배에게 물어보는데, 그 사진은 사실 정상인의 엑스레이 사진이었다. 테루는 슬럼프에 빠진 자신을 놀린 거냐며 시간 낭비했다고 화냈지만, 실제 환자의 엑스레이를 보면서 선배의 의도를 알게 된다. 환자의 엑스레이를 통해 공부하면, 병의 종류에 따라서 다른 엑스레이를 보며 공부해야 하고, 엑스레이 판독을 할 때도 가능한 모든 병을 고려해봐야 한다. 하지만 정상인의 엑스레이에 한 번 익숙해 지면 익숙하지 않은 부분이 문제가 있는 부분이라고 금방 눈치챌 수 있다는 것이다.
 어렸을 때는 이 장면을 그저 만화적 과장이라고 생각했다. 하지만 프로그래머로 일하다 보니 딱히 과장이 아닐 수 있다고 생각하게 됐다. 흔히들 코딩할 때 정답은 없다고 말한다. 같은 결과를 낼 수 있는 수많은 방법이 있기 때문이다. 하지만 코딩에 오답은 있다. 이는 버그가 있는 코드를 말하는 건 아니다. 버그가 있는 코드는 논할 가치도 없다. 오답은 코드를 수정했을 때 버그가 발생할 확률이 높은 코드다. 수정에 민감한 코드는 아무리 지금 버그가 없어도 오답이다.
 근데 프로젝트를 진행하다 보면 이…

Cloudflare의 Flexible SSL을 쓰면 안 되는 이유

이미지
Cloudflare의 서비스 중 Flexible SSL이라는 것이 있다. SSL 인증서가 없는 서버에 있는 웹페이지도 https를 이용해 접근할 수 있도록 해주는 서비스다. 자신이 인증서를 설치할 수 없는 서버나 서비스를 사용할 때도 https를 사용할 수 있게 해주기 때문에 blogger처럼 커스텀 도메인에 https를 지원 안 하는 서비스를 이용하는 사람들이 많이 사용한다. 이 블로그도 blogger에서 custom domain을 이용하고 있기 때문에 https를 지원하려면 Cloudflare의 Flexible SSL이 사실상 유일한 옵션이다. 하지만 https를 포기하고 Flexible SSL을 사용하지 않고 있다. 왜냐하면, Flexible SSL이 아무런 이득이 없는, 오히려 위험하기만 한, 존재해서는 안 되는 서비스이기 때문이다. Flexible SSL을 엄청 디스한 것 같은데 어째서 그런지 Flexible SSL이 동작하는 방식을 보면 쉽게 이해할 수 있다.  위의 도표는 Flexible SSL이 어떻게 동작하는지를 그림으로 표현한 것이다. Cloudflare의 DNS는 요청된 도메인에 대해서 원래 서버의 주소를 주지 않고, Cloudflare 서버의 주소를 준다. 그러면 클라이언트의 브라우저는 원래 서버가 아닌 Cloudflare의 서버로 접속한다. 그러면 Cloudflare의 서버는 원래 서버로 다시 요청을 보내고, 받은 결과를 클라이언트에게 돌려준다. 이때 클라이언트와 Cloudflare 사이의 통신은 암호화된 https로 이루어지고, Cloudflare와 원래 서버 사이의 통신은 암호화되지 않은 http로 이루어진다. 이를 보고 "최소한 Cloudflare와 클라이언트 사이에는 https를 사용하기 때문에 안전하지 않은가"라고 생각하는 사람도 있다. 하지만 아니다. 보안에서 자주 사용되는 격언에 "A chain is only as strong as its weakest link."라는 말이 있다. 어떤 시…

Diffie-Hellman Key Exchange - 공개된 정보만으로 secret key 만들기

네트워크상의 두 노드가 암호화된 통신을 하기 위해선 먼저 두 노드가 어떤 암호화 방식으로 어떤 키를 이용해서 암호화할지 합의해야 한다. 보통 암호화 방식은 사용하는 애플리케이션에 따라 고정된 방식을 사용하거나 두 노드가 처음 통신을 시작할 때 암호화하지 않은 패킷을 이용해 합의하거나 한다. 이후 패킷은 양쪽 노드밖에 모르는 암호키를 이용해 암호화할 것이기 때문에 암호화 방식은 암호화되지 않은 방식으로 합의를 해도 안전하다. 하지만 어떤 키를 사용할지는 암호화되지 않은 방식으로 합의해선 안 된다. 키가 공개되면, 이 비밀키를 이용해서 제삼자가 패킷을 위조할 수 있기 때문이다. 그렇다면 이 비밀키는 어떻게 안전하게 교환할 수 있을까?
 이에 대한 해답으로 나온 것 중 하나가 Diffie-Hellman key exchange(a.k.a. DH)다. 사실 이외에도 다른 방법들이 많이 있지만, 개인적으로 생각하기에 가장 범용적으로 안전하게 사용할 수 있는 것은 DH라고 생각한다. 또한, 이후 이것에 대해 많은 변종이 나왔지만, DH만 이해하면 나머지는 이해하는 데 별문제 되지 않는다. 그렇다면 DH는 어떻게 동작할까? 우선 DH가 성립하기 위해서는 특별한 수학적 성질을 만족하는 generator가 필요하다. 이 generator는 하나의 입력을 받아 하나의 출력을 내뱉는다. 이 generator가 g라고 하고, 입력 x에 대해서 출력 Y를 내뱉는 Y=g(x)가 있을 때, x로부터 Y를 가지고 오는 것은 빠르고 쉽게 계산할 수 있지만, Y로부터 x를 가지고 오는 것은 어려운 일이어야 한다. 즉, 수학적으로는 역함수가 없는 함수여야 하고, 결괏값의 스페이스가 매우 커서 brute-force로 찾는 것이 매우 힘들어야 한다. 사실 이외에도 만족해야 할 수학적 성질이 여러 개 있지만 이번 포스팅에서는 그에 대한 설명은 생략하고 넘어가겠다. DH가 처음으로 제시한 방법은 generator로 modular exponentiation을 사용했다. modular expone…

[C] tagged pointer - 포인터에 정보 담기

Tagged pointer는 메모리 크기를 줄이기 위한 고전적인 테크닉이다. 기본적인 아이디어는 포인터의 모든 값이 의미 있는 값은 아니라는 것이다. 예를 들어 4 byte 단위로 align 되는 객체의 32-bit 포인터를 생각해보자. 그렇다면 이 객체의 주소는 4로 나누어 떨어지는 값이 돼야 하니 LSB(Least Significant Bit)으로 부터 2 bit은 언제나 0b00으로 고정될 것이다. 그렇다면 이 2 bit을 다른 정보를 담는 데 써도 아무 문제가 없다. 조금 더 구체적으로 경우 포인터의 값이 0x5678FFF0, 0x5678FFF1, 0x5678FFF2, 0x5678FFF3인 경우 모두 0x5678FFF0에 있는 객체를 가리키도록 하고, 0x5678FFF4, 0x5678FFF5, 0x5678FFF6, 0x5678FFF7인 경우 모두 0x5678FFF4를 가리키는 포인터로 해석하는 것이다. Tagged pointer를 만드는데 LSB 만 쓸 수 있는 건 아니다. 보통 user space에서 쓸 수 있는 최대 메모리가 제한돼 있다. 예를 들어 32-bit 윈도우에서 user space는 최대 3GB 까지 늘릴 수 있지만, 기본적으로 2GB이다. 즉, MSB(Most Significant Bit) 1 bit를 tag에 쓸 수 있다. 64-bit 리눅스라면, 프로세스당 최대 메모리 스페이스는 256 TB까지 이므로 48 bit만 사용된다. 즉, MSB로부터 16 bit를 tag에 사용할 수 있다. 하지만 위의 두 예시에서 보았듯이 tag에 이용할 수 있는 MSB의 크기는 시스템별로 다르다. 따라서 MSB를 tagged pointer로 사용하는 경우 portable 한 코드를 만들기 어려워진다. Tagged pointer를 모든 포인터에 일반적으로 적용하지 않아도 된다. 그보다는 테이블같은 것에 저장할 포인터에만 사용하거나 포인터를 리턴하는 함수에 대해서만 사용하는 것이 일반적이다. 특히 포인터와 추가 정보를 리턴하는 함수를 tagged poi…

[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_ptrunique_ptr은 포인터의 semantic을 그대로 따르기 때문에 null이 될 수 있다. 하지만 null이 될 수 없는 shared_ptr과 unique_ptr를 reference로 표현할 수 없다. 따라서 포인터 시멘틱을 따르는 타입이지만, null이 될 수 없는 객체를 표현할 일반적인 방법이 필요하다.C++ Core Guidelinesnull이 될 수 없는 포인터 계열의 변수는 not_null<T>이라는 클래스를 사용하기를 권장한다. not_null<T>은 …

[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> 클래스는 아무런 일도 하지 않는 클래스다. 사실 클래스일 필요도 없기에 GSL에서는 포인터의 al…