2018-02-23

좋은 코드를 많이 봐야 한다

 얼마 전 트위터에서 재밌는 이야기를 봤다.

 가짜를 알기 위해서 가짜를 공부할 필요가 없다는 글인데, 이 트윗을 보니 어렸을 때 봤던 갓핸드 테루라는 의료 만화가 떠올랐다. 갓핸드 테루는 신입 의사인 마히가시 테루가 수련을 받으며 명의가 돼가는 과정을 그린 의료만화인데, 그중에서 다음과 같은 에피소드가 나온다.
 주인공 테루가 슬럼프에 빠져 엑스레이 판독을 못 하게 되자 선배 의사가 테루에게 과제를 하나 내준다. 어느 환자의 엑스레이 사진을 주면서 이 환자의 문제가 무엇인지 찾아오라는 것이었다. 테루는 열심히 고민해보지만 결국 문제를 찾지 못하고 문제를 냈던 선배에게 물어보는데, 그 사진은 사실 정상인의 엑스레이 사진이었다. 테루는 슬럼프에 빠진 자신을 놀린 거냐며 시간 낭비했다고 화냈지만, 실제 환자의 엑스레이를 보면서 선배의 의도를 알게 된다. 환자의 엑스레이를 통해 공부하면, 병의 종류에 따라서 다른 엑스레이를 보며 공부해야 하고, 엑스레이 판독을 할 때도 가능한 모든 병을 고려해봐야 한다. 하지만 정상인의 엑스레이에 한 번 익숙해 지면 익숙하지 않은 부분이 문제가 있는 부분이라고 금방 눈치챌 수 있다는 것이다.
 어렸을 때는 이 장면을 그저 만화적 과장이라고 생각했다. 하지만 프로그래머로 일하다 보니 딱히 과장이 아닐 수 있다고 생각하게 됐다.

 흔히들 코딩할 때 정답은 없다고 말한다. 같은 결과를 낼 수 있는 수많은 방법이 있기 때문이다. 하지만 코딩에 오답은 있다. 이는 버그가 있는 코드를 말하는 건 아니다. 버그가 있는 코드는 논할 가치도 없다. 오답은 코드를 수정했을 때 버그가 발생할 확률이 높은 코드다. 수정에 민감한 코드는 아무리 지금 버그가 없어도 오답이다.
 근데 프로젝트를 진행하다 보면 이런 오답 같은 코드들이 많이 보인다. 이런 오답 코드를 작성하는 이유는 보통 제대로 된 방법으로 코딩하면 시간이 많이 들고 귀찮기 때문이다. 그래서 약간의 편법을 쓰면 일을 빠르게 진행할 수 있을 거로 생각하기 때문이다. 뭐 가끔은 프로젝트가 끝날 때까지 아무 문제가 없을 수도 있다. 하지만 어떤 이유로든 코드를 수정하게 되면 큰일이 발생한다.

 그렇다면 오답과 오답이 아닌 코드를 어떻게 구분할 수 있을까? 문제가 됐던 프로젝트를 열심히 분석하면 어떻게 수정할지 알 수 있을까? 문제가 된 프로젝트를 분석하면 무엇이 문제였는지는 알 수 있을 것이다. 하지만 어떻게 수정할지는 알기 어렵다. 아마 다른 오답 코드를 작성할 것이다. 오답이 아닌 코드를 만들기 위해서는 잘 짜여있는 코드를 많이 봐두는 것이 중요하다. 좋은 코드에 충분히 익숙해져 있으면, 오답 코드를 봤을 때 어색함이 느껴진다. 그래서 프로그래머로서 성장하기를 원한다면 참여하고 있는 프로젝트 외에 자신보다 잘하는 사람들이 진행하는 프로젝트의 코드를 꾸준히 보는 것이 가장 중요하다고 생각한다.

 아마 그 어색함이 무엇인지 모르는 사람은 이해하기 어려울 것이다. 사실 나도 그랬다. 한 5년쯤 전이었을 것이다. 같은 회사에서 일했던 뛰어난 프로그래머에게 어떻게 하면 그렇게 할 수 있는지 물어봤던 적이 있다. 그때 그분이 했던 정확한 단어는 기억이 안 나지만 비슷한 느낌의 말을 했었다. 그리고 그때 나의 반응은 정말 '그게 무슨 소리인가요?'였다. 그때 들은 조언이 그냥 코드를 보다 보면 알게 된다는 것이었다. 그리고 지금 생각해보면 그 조언을 더 빨리 따랐으면 좋았을 것으로 생각한다.
 사실 프로그래밍 공부를 할 때, 다른 사람이 짠 코드를 분석하는 것은 가장 재미없는 일이다. 읽을 가치가 있는 좋은 프로젝트는 대부분 많은 코드를 가지고 있고, 읽고 분석하는데 시간이 많이 들기 때문에 귀찮고 하기 싫은 일일 때가 많다. 특히 많은 프로그래머는 다른 사람이 짠 코드를 읽는 것보다 자신의 코드를 작성하고 실제 돌아가는 것을 보는 걸 좋아한다. 하지만 만약 더 이상 실력이 늘지 않는다고 생각한다면, 슬럼프에 빠진 것이 아닌지 걱정된다면, 한 번 유명한 다른 프로젝트를 분석해보는 것을 추천한다. 지금까지 보이지 않았던 다른 것이 보이게 될 것이다.

2018-02-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."라는 말이 있다. 어떤 시스템이 해킹당할 위험도는 그 시스템을 구성하는 요소 중에서 가장 취약한 요소가 해킹당할 위험도와 같다는 말이다. 설령 클라이언트와 Cloudflare 사이의 통신이 암호화됐다고 해도, Cloudflare 서버와 원본 서버 사이의 통신이 암호화됐지 않았기에 Man-in-the-middle attack을 당해 조작된 페이지를 받거나 요청된 데이터가 해킹당할 위험도는 Cloudflare 서버를 통하지 않고 원본 서버와 바로 http로 통신했을 때와 같은 위험도를 가진다.

 사실 Flexible SSL은 사용 안 하는 것보다 더 위험하다. Flexible SSL을 사용하지 않으면, 기본적인 지식이 있는 사용자라면 지금 페이지가 안전한 페이지인지 판단하여 민감한 정보를 보낼지 말지 결정할 수 있다. 하지만 Flexible SSL을 이용하면 모든 페이지가 암호화된 것처럼 보이기 때문에 사용자가 안전한 페이지인지 판단할 수 없다. 게다가 최신 브라우저의 경우 보안을 위해서 안전하지 않은 페이지와 안전한 페이지를 자동으로 구분해서 https 페이지에서 http 페이지로 데이터를 보내거나 http 페이지에서 콘텐츠를 가지고 오는 것을 막는다. 하지만 Flexible SSL을 사용하면 이런 기능을 전혀 이용할 수 없다.
 따라서 Flexible SSL을 사용하면 해킹 위험도는 그대로 가져가면서, 오히려 브라우저의 보안 기능을 못 사용하게 될 수 있기 때문에 더 위험해질 수 있다. 특히나 사용자에게 위험도를 속인다는 점에서 Flexible SSL을 사용하면 사용자를 기만하게 된다고 말할 수 있다.


이 글을 쓸 때는 몰랐는데 작년 12월에 이미 blogger도 custom domain에 대한 https 지원을 시작했었다. 올해 계속 바빠서 확인을 제대로 못 했던 것은 있는데, 이 정도 변화면 레딧이든 해커 뉴스든 어디선가 관련된 글이 올라왔을 것 같은데 왜 못 보고 넘어갔는지 모르겠다.

2018-02-21

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 exponentiation에서는 입력을 x라고 했을 때, 출력 Y를 다음과 같이 계산한다. Y:=bxmodm. 이때 bm은 적절한 수학적 성질을 만족하는 값으로, bm의 pair가 generator g가 된다.
 이후 여러 변종이 나왔고, 요새 많이 사용되는 것은 elliptic curve를 이용하는 Elliptic-curve Diffie–Hellman(a.k.a. ECDH)이다. 이 경우 사용하는 곡선의 종류가 generator g가 된다.

 어떤 generator를 사용할지 결정되고 나면 DH는 다음의 과정을 거쳐서 진행된다.


AliceBobEve
1GGG
2ab
3A=G(a)B=G(b)
4BAA, B
5s=B(a)s=A(b)

 우선 위의 표를 설명하자면, AliceBob은 서로 통신하기 원하는 사용자고, EveAliceBob 사이에 무슨 내용을 통신하는지 알기 원하는 도청자다. AliceBob은 공개된 공간에 있기 때문에 둘 사이의 통신하는 내용은 제삼자인 Eve도 알 수 있다. 그리고 위의 표에서 푸른색으로 표시된 문자는 자신이 생성한 데이터고, 붉은색으로 표시된 문자는 다른 사람이 생성하여 통신을 통해서 듣게 된 데이터다. 또한, 대문자와 소문자도 다른 의미를 가지는데, 대문자는 외부에 공개해도 되는 public 한 데이터고, 소문자는 자신만 알고 있어야 하는 private 한 데이터다. 따라서 붉은색 데이터는 모두 Eve도 알게 되고, 붉은색 데이터와 푸른색 데이터를 조합하여 AliceBob은 알지만, Eve는 모르는 데이터를 만드는 것이 DH의 목적이다.

1GGG

 우선 첫 단계는 어떤 generator를 사용할지 결정하는 단계다. generator를 결정하는 단계도 여러 방법으로 구현할 수 있지만 그다지 중요한 내용은 없으니 이번 포스트에서는 결정됐다고 가정하고 넘어가도록 하겠다.

2ab

 다음 단계에서 AliceBob은 각자 private key를 만든다. 이 private key를 어떻게 만드는가는 DH에서 중요한 요소가 아니지만, replay 공격을 막기 위해 private key는 랜덤하게 생성하는 것이 좋다.

3A=G(a)B=G(b)

 그리고 AliceBob은 generator G를 이용해서 각자 public key를 생성한다. 여기까지 진행되면 AliceBob은 자신의 private key와 public key만 알고 있고, Eve는 아무 키도 모르고 있는 상태가 된다.

4BAA, B

 다음 단계는 AliceBob이 각자 자신의 public key를 말하는 단계다. 이 단계를 지나면 AliceBob의 public key 두 개는 Eve를 포함한 모든 사람이 알고 있는 정보가 된다.

5s=B(a)s=A(b)

 다음 단계에서 AliceBob의 public key를 기반으로 generator를 만들어 자신의 private key를 사용하여 secret key를 만들고, BobAlice의 public key를 기반으로 generator를 만들어 자신의 private key를 사용하여 secret key를 만든다. 이때 public key를 사용해서 generator를 만드는 것은 어떤 generator를 사용하는가에 따라 다르다. modular exponentiation을 사용하는 경우 m는 기존의 generator의 m을 그대로 사용하고, 새로 받은 public key를 b로 사용한다. elliptic curve를 사용하는 경우 타원 곡선은 그대로 사용하고, public key를 시작점으로 계산한다.
 어쨌든 전달받은 public key를 기반으로 generator를 만들어 자신의 private key를 input으로 만들면, 그 결과 나온 secret key는 놀랍게도 AliceBob이 같은 값을 가지게 된다. 이는 아까 설명을 생략했던 generator의 몇 가지 수학적 성질 중 하나 덕분에 가능한 것이다. 이 secret key s를 알기 위해서는 최소 한 개의 private key가 필요하므로 Eve는 secret key s를 알 수 없다. 이제 AliceBob은 공유하지만, Eve는 모르는 값이 생겼으니, 이후 메시지는 전부 secret key s를 이용해서 암호화하면 된다.

 이상이 Diffie-Hellman Key Exchange를 이용하여 두 노드가 안전하게 비밀키를 만들어 공유하는 방법을 설명하였다. 사실 DH를 직접 구현해야 할 경우는 거의 없다. 하지만 네트워크 통신의 기본이 되는 내용이고, 많은 프로토콜이 컨넥션을 시작하는 단계에서 DH를 사용하기 때문에 서버 프로그래머라면 알아 두는 것이 좋다.

2018-02-09

[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 pointer를 리턴하는 함수로 만들면, 리턴값이 하나의 레지스터에 들어가게 돼서 더 빨라지기도 한다.

 Tagged pointer는 사용하기 불편한 테크닉이지만 놀랍게도 아직 종종 사용된다. 하지만 사용하기 전에 정말로 필요한지 고민해봐야 한다. Tagged pointer는 사용하는 메모리 크기를 줄이지만, 구현이 쓸데없이 복잡해진다. 구현이 복잡하다는 것은 버그가 발생할 확률이 높다는 것이고, 게다가 디버거는 tagged pointer의 의미를 알지 못 하므로 디버깅도 많이 귀찮아진다. 이 개발 비용을 감수하면서도 tagged pointer가 주는 이득이 충분한지 따져보고 사용해야 한다.

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

 아랫글은 2016년에 썼던 글인데 왜인지 모르게 아직 publish 안 하고 있었다.
 그 사이에 ES2015(ES6)의 변경을 추가 한 커피스크립트2가 나왔다. 하지만 이미 ES2015를 넘어 ES2017도 나왔고, 브라우저들도 ES2016는 네이티브로 지원하고 있기 때문에 앞으로도 커피스크립트를 쓸 일은 없을 것 같아 발견한 김에 publish 한다.


 커피스크립트는 자바스크립트 코드를 간결하게 만드는 것을 목표로 만들어진 언어다. 2009년 첫 버젼을 릴리즈 하였고, 2010년 12월 1.0이 릴리즈 되었다.
 내가 커피스크립트를 처음 썼던 것은 1.0이 릴리즈 된 지 조금 뒤인 2011년 경이었던 것 같다. 지금도 자바스크립트 코드가 다른 언어에 비해 간결하지는 않지만, 당시 자바스크립트 코드는 지금보다도 verbose 하였기 때문에 꽤 애용하였었다. 그러다가 웹 말고 다른 일을 하다 보니 자바스크립트를 사용하지 않게 되었고 자연스럽게 커피스크립트도 안 쓰게 되었다. 그러다가 2014년경 잠시 웹 개발을 하게 되었는데 이때 습관적으로 다시 커피스크립트를 사용하였었다. 하지만 그것도 잠시였고, 그 뒤로는 사용하지 않게 되었다. 더 이상 커피스크립트를 쓰지 않게 된 이유는 크게 2가지였다.

 일단 커피스크립트의 문법은 너무 애매했다. 커피스크립트가 가장 중요하게 생각하는 요소 중 하나는 자바스크립트로 일대일로 매칭되는 것이다. 하지만 처음 커피 스크립트를 보면 자바스크립트 같은 느낌이 전혀 들지 않는다. 이는 커피스크립트 코드에서는 괄호를 거의 사용하지 않기 때문이다. 사실 일부 기능을 제외하고 대부분의 커피스크립트 코드는 적절한 위치에 괄호를 추가하는 것으로 자바스크립트 코드로 변환할 수 있다. 이는 커피스크립트의 설계자가 자바스크립트의 괄호가 자바스크립트 코드를 복잡하게 만든다고 생각했기 때문이다.
 하지만 실제로 사용해보면 이는 딱히 편하지 않다. 물론 괄호가 없기 때문에 타이핑은 많이 줄어든다. 하지만 코드를 작성해본 사람은 알겠지만 적은 타이핑은 딱히 간결한 코드를 만드는데 중요한 요소가 아니다. 게다가 이 정도 타이핑은 사실 좋은 에디터를 쓰면 해결될 문제다. 실제로 커피스크립트를 사용하면 타이핑이 줄어서 드는 이득보다 코드를 읽기 힘들어져서 생기는 문제가 더 크다. 물론 익숙해지면 어느 정도 해결되는 문제지만, 엣지 케이스에 대해서는 꾸준히 밟는 지뢰가 생긴다. 게다가 문제가 발생했을 때 컴파일 에러가 발생하는 것이 아니라 예상치 못했던 방식으로 컴파일돼서 실행되는 것이 더 큰 문제다.

 또한 당시에는 ECMAScript 6(a.k.a. ES6)를 ECMAScript 5로 변환시켜주는 babel이라는 기술이 나오고 있었다. ES6를 사용하면 커피스크립트만큼은 아니지만, 어느 정도 간결한 코드를 사용할 수 있었다. 그리고 ES6에는 이미 커피스크립트의 arrow 문법(-> 대신 =>를 사용하기는 한다)과 class가 추가됐다. 게다가 변수 선언이 암시적으로 이루어지는 커피스크립트의 특성상 ES6에 추가 된 let이나 const같은 장점을 전혀 사용할 수 없다.

 뭐 이런저런 이유로 더 이상 커피스크립트를 사용하지는 않을 것 같다. 하지만 커피스크립트의 노력이 헛되다고 생각하지는 않는다. arrow를 함수로 사용하기 시작한 것도, 자바스크립트에 class를 도입한 것도 내가 알기로는 커피스크립트가 처음이었고, 자바스크립트를 조금 더 쓰기 좋은 언어로 만드는데 커피스크립트가 기여한 바는 결코 작지 않다. 다만, 이미 커피스크립트가 해결하고자 하는 문제는 거의 다 자바스크립트에 반영됐고, 자바스크립트는 다음 문제로 넘어가고 있다. 따라서 앞으로 커피스크립트를 쓸 일은 없을 것 같다.

[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을 반드시 수행하도록 스펙이 수정됐기 때문이다.

2018-02-08

[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_ptrunique_ptr를 reference로 표현할 수 없다. 따라서 포인터 시멘틱을 따르는 타입이지만, null이 될 수 없는 객체를 표현할 일반적인 방법이 필요하다.

 C++ Core Guidelinesnull이 될 수 없는 포인터 계열의 변수는 not_null<T>이라는 클래스를 사용하기를 권장한다. not_null<T>은 간단하게 T의 alias일 수도 있다. 하지만 GSL에서는 null 체크를 대신 해주면서 T의 포인터 문법을 그대로 따르는 객체로 구현했다. 이는 실수로 null을 대입하였을 때 빠르게 알기 위해서다. 어쨌든 중요한 점은 not_null<T>이 명시적으로 기존의 T와는 다른 이름으로 보인다는 것이다. 어떤 변수의 타입이 not_null<T>이면, 이 변수에 값을 할당하는 측에서 null 체크를 해야 하고, 이미 할당된 변수에 대해서는 더 이상 null 체크를 안 해도 된다.  간단하게 어떤 함수의 인자가 not_null<T>이라면, 이 인자의 null 체크는 caller가 담당하고, 어떤 함수의 결과 타입이 not_null<T>라면, 이 반환 값의 null 체크는 callee가 담당한다고 말할 수 있겠다.

2018-02-07

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

 C++을 쓰는 사람들이 가장 어려워하는 것 중 하나가 포인터다. 그중에서도 함수 포인터를 읽고 해석하는 것이 가장 어렵다고 한다. 하지만 실제 코드에서는 함수 포인터를 볼 일은 거의 없다. 특히 modern c++에서는 가능하면 std::function를 쓰는 걸 권장하기 때문에 몇몇 특수한 목적을 가진 코드를 제외하고는 함수 포인터를 볼 일은 거의 없다.

 그다음으로 어려운 것은 메모리 관리다. C++에서 전통적으로 많이 발생하던 문제가 double free와 memory leak이다. 이는 C++에서 포인터로 가리키는 객체의 소유권이 명확하지 않기 때문이었다. 이를 해결하기 위해 C++11에서는 소유권을 혼자 차지하고 있는 std::unique_ptr과 소유권을 공유하는 std::shared_ptr을 만들었다. std::unique_ptrstd::shared_ptr을 잘 활용하면 dobule free와 memory leak은 예방할 수 있다.

 하지만 C++11 이후에도 여전히 포인터는 다양한 역할을 가지고 있다. 현재 C++의 포인터에 남은 역할은 다음과 같다.

  1. std::unique_ptr을 사용하지 않지만, 소유권을 넘길 때
  2. 함수의 인자로 배열을 넘길 때
  3. 문자열을 가리킬 때
  4. 소유권을 넘기지 않고 하나의 객체를 가리킬 때

스마트 포인터를 사용하지 않지만, 소유권을 넘길 때

 앞에서 말했듯이 C++11은 std::unique_ptrstd::shared_ptr를 도입하여 소유권을 관리할 수 있도록 하였다. 하지만 스마트 포인터를 사용하지 못하는 경우도 있다. 이 경우 적절한 지점에서 객체를 소멸시켜줘야 한다. 하지만 이를 지칭하는 것이 단순히 포인터이기 때문에, 메모리를 소멸시켰는지, 한 번만 소멸시켰는지 알기 어렵다. 그래서 C++ Core Guidelines에서는 이 경우 owner<T>라는 클래스를 사용하는 것을 권장한다. owner<T> 클래스는 아무런 일도 하지 않는 클래스다. 사실 클래스일 필요도 없기에 GSL에서는 포인터의 alias로 구현하고 있다.
 owner<T> 자체는 아무것도 하지 않지만, 포인터와는 구분되기 때문에 코드를 읽는 사람이 소유권을 명확히 알 수 있다는 장점이 생긴다. owner<T>를 인자로 받은 함수가 이 값의 소유권을 넘기지 않았다면 이 함수에서 객체를 소멸시킬 책임을 진다. 다시 말해 이 값을 다른 함수에 넘기거나, 새 스마트 포인터를 만들거나, 리턴하거나, 전역 변수에 할당한 것이 아니라면, 이 함수에서 소멸시켜야 한다.

함수의 인자로 배열을 넘길 때

 C++ 11에는 std::array가 들어왔지만, 여전히 배열을 함수의 인자로 사용할 때는 첫 번째 원소의 포인터와 배열의 길이를 따로 넘기는 방법이 많이 사용된다.
 이유는 크게 2가지 있다. 일단 아래와 같이 배열이나 std::array를 쓰면, 이 함수는 배열의 길이별로 전부 다른 함수로 instantiation 된다.  또한, 이 함수는 컴파일 타임에 크기가 정해지는 배열에 대해서만 사용할 수 있으므로, 동적 크기 배열에 대해서는 함수를 처음의 방식으로 재정의해야 한다.
 이런 문제를 해결하기 위해 C++ Core Guidelines에서는 span<T>이라는 클래스를 정의해서 사용하기를 권장한다. span의 생성자가 배열, std::array, 동적 크기의 배열과 길이를 받는 생성자를 전부 정의하고 있으면, 배열을 받아야 하는 함수는 아래와 같이 span<T>을 이용하면 된다.  span의 여러 가지 장점을 가진다. 우선, 동적 배열과 정적 배열 양쪽에 모두 사용할 수 있다. 또한, 포인터와 길이를 묶기 때문에, 포인터의 길이를 잃어버릴 걱정도 안 해도 된다. 마지막으로 포인터가 지칭하고 있는 것이 배열의 첫 번째 원소인지 단일 객체인지 고민하지 않아도 된다. 실제로 C++ Core Guidelines를 따르는 코드에서는 포인터가 보이면 무조건 단일 객체라고 생각해도 된다.

C-style의 문자열을 가리킬 때

 C++에서 포인터를 사용하는 또 다른 용도는 null 캐릭터로 끝나는 C 스타일 문자열을 지칭할 때다. 이 경우 zstring을 이용하면, 이것이 단일 캐릭터인지, 길이를 가지는 문자열인지, null 캐릭터로 끝나는 문자열인지 고민하지 않아도 된다.
 C 스타일의 문자열은 zstring을 이용하면 되지만, non-ascii 캐릭터를 사용할 경우 null 캐릭터가 문자열의 마지막을 의미하지 않을 수도 있다. 이 경우 문자열의 길이가 필요하다. 이런 종류의 문자열을 명시하기 위해서 위에서 설명한 span<T>을 사용할 수도 있다. 하지만 C++ Core Guidelines에서는 문자열이라는 것을 더욱 명확히 명시하기 위해서 string_span라는 이름을 사용할 것을 권장한다.

소유권을 넘기지 않고 하나의 객체를 가리킬 때

 사실 일반적인 C++ 코드에서 볼 수 있는 포인터는 대부분 소유권을 가지지 않는 하나의 객체를 가리크는 경우이다. 그리고 C++ Core Guidelines를 완벽하게 따르는 코드에서는 포인터를 사용하는 것을 사용하는 유일한 경우다. 따라서 C++ Core Guidelines를 잘 따르는 코드에서 포인터가 보이면 이는 소유권을 가지지 않는 단일 객체를 지칭하는 것으로 생각할 수 있다.

 이상으로 C++ Core Guidelines에서 포인터를 어떻게 구분해서 사용하는지 보았다. 보면 알겠지만, owner<T>zstring은 타입에 새 이름을 붙인 것이고, span<T>string_span은 관련 있는 데이터를 하나의 구조체로 묶은 것이다. 매우 기초적이고 단순한 것이지만, 무시해서는 안 된다. 실제로 위의 네 타입만 잘 써도 포인터를 사용하면서 발생하는 많은 문제을 쉽게 해결, 혹은 발견할 수 있다.