2014-01-24

RAII는 무엇인가

 RAII는 C++에서 자주 쓰이는 idiom으로 자원의 안전한 사용을 위해 객체가 쓰이는 스코프를 벗어나면 자원을 해제해주는 기법이다. C++에서 heap에 할당된 자원은 명시적으로 해제하지 않으면 해제되지 않지만, stack에 할당된 자원은 자신의 scope가 끝나면 메모리가 해제되며 destructor가 불린다는 원리를 이용한 것이다.
 원래는 exception 등으로 control flow가 예상치 못하게 변경될 때를 대비하기 위해서 쓰이던 기법이다.

 아래의 예제를 보자.

 첫 번째 코드는 위험하다. thisFunctionCanThrowException() 함수에서 exception을 발생시킨다면 resource를 해제하지 못한다.
두 번째 코드는 일단은 resource를 해제하고 있다. 하지만 보기에 좋은 코드는 아니다. 그리고 유지하기도 어려워진다.
세 번째 코드는 RAII를 위해 c++ 11의 스마트 포인터인 unique_ptr을 이용하는 방법이다. unique_ptr은 소멸할 때 자신이 들고 있는 메모리를 해제시켜주기 때문에 함수 밖으로 나가면 resource가 해제되는 것이 보장된다.

 여기서 말하는 자원은 단순히 heap 메모리만을 말하는 것이 아니다. heap 메모리 이외에도 파일이나 db와 같은 것들도 전부 RAII를 이용해 안전하게 사용할 수 있다.
여기에 더 나아가서 특정 scope를 벗어나면 반드시 실행돼야 하는 코드들도 RAII를 이용해 처리할 수 있다. 즉, 다른 언어에서 finally에 해당하는 구문을 RAII를 이용해서 처리할 수 있다. 실제로 C++의 아버지이자 RAII라는 용어를 처음 만든 Bjarne Stroustrub는 c++에 finally를 집어넣지 않는 이유를 "RAII가 있는데 굳이 있을 필요가 없다."라고 말하고 있다.

2014-01-23

잘못된 assert는 사용하지 말자

 assert라는 함수는 인자로 받는 조건이 true가 아니면 예외를 발생시키는 함수로, 디버깅을 도와주거나, 코드의 가독성을 올리는 용도로 쓰인다. 게다가 릴리즈에서는 아무 일도 않기 때문에(c++의 경우 NDEBUG가 정의되었는가 아닌가로 동작이 달라진다.), 성능 저하 없이 검증 코드를 집어넣을 수 있다.
 근데 릴리즈에서는 아무 동작도 하지 않는다는 특성 때문인지, assert가 가지는 implicit 한 의미를 이해 못 했는지 assert를 잘못 사용하는 때도 있다.

 다음의 코드를 보고 이상한 점을 찾아보자.

 Connection이라는 class의 Send method에서 message를 받아서 platformConnection_(내부 구현이 어떻게 되었을지는 신경 쓰지 말자.)을 통해서 Send해주고 결과 값을 돌려준다. 그 전에 platformConnection_이 valid한지를 검사하여 valid하지 않다면 false를 돌려주는 평범한 코드다.
 문제는 함수의 맨 앞에서 valid한지 아닌지 assert로 체크를 하고 있다는 것이다.
 이 assert문이 가지는 의미는 절대로 valid하지 않을 때는 절대로 이 함수가 호출되지 않는다는 것이다. 그럼에도 그다음 문장에서 valid한지 않은지 검사하고 있다.

 이에 대해 assert는 릴리즈 빌드에는 포함되지 않기 때문에 릴리즈 빌드를 위해서 안전한 코드를 작성해야 한다고 주장하는 사람도 있다. 하지만 이런 경우라면 애초에 assert를 쓰지 않고 조건문만을 써야 한다.
 assert를 썼다면 assert에 걸릴만한 조건이 들어오지 않도록 코드를 작성했어야 한다.

2014-01-09

테스트 얼마나 만들어야 할까?

 에자일한 소프트웨어를 만드는 방법의 하나로 TDD에서는 test를 먼저 작성하는 것을 권장? 혹은 강요한다. 스펙에 맞춰 테스트를 하나하나 만들어가고, 그 테스트 케이스들을 초록색으로 만드는 것을 반복해 나가다 보면 에러 없는 코드가 완성된다는 것이다. 이 방법론에 대해 비난할 생각은 없다. 매우 좋은 방법이다. 근데 정말로 언제나 해야 하는 걸까? 부작용은 없을까?

 내가 이것들에 대해 고민하게 된 것은 TDD를 처음 배우고 Rails를 사용하고 있던 3년 전 일이다. 그전에 했던 프로젝트가 C++로 서버를 만드는 일이었고 사소한 예외처리 몇 개를 실수해서 몇 일 밤을 새웠던 직후라서 더욱더 테스트에 매달리게 되었다. 다행히도 rails는 유닛테스트에 특화된 framework이었고 (당시에 DB까지 unit test가 가능하도록 지원해주는 framework이 얼마 없었다.), TDD의 원칙에 따라서 많은 테스트와 함께 코드를 작성해 나갔다.
 그렇게 가끔 테스트가 없었으면 크게 삽질했을 버그를 잡아가면서, 그 프로젝는 내가 질릴 때까지 별문제 없이 진행되었다.

 여기까지만 보면 TDD의 긍정적인 사례에 추가될 수 있을 것 같지만, 실상은 전혀 아니었다. 문제는 두 가지가 있었다. 첫 번째는 테스트 작성에 심취해서 개발에 너무 많은 시간이 들었다는 것이고, 두 번째는 너무 빨리 질렸다는 것이다.
 당시에는 아직 TDD에 적응이 안 돼서라고 생각했는데, 정도의 차이가 있었지만 2번째 프로젝트에서도 비슷한 현상이 나타났다.
 왜 그렇게 된 것일까?

 이것에 대해 고민하다가 RubyOnRails의 제작자인 David Heinemeier Hansson의 글을 보게 되었다. 논쟁이 많이 되었던 글이지만 David가 말하고자 하는 바는 단순하다.
 테스트를 작성하는데 들어가는 비용도 기능 구현이나 디버깅에 들어가는 것과 같은 비용이니 overtest하지 않도록 노력해야 한다는 것이다.
 Unittest라는 도구를 처음 접해 아무 생각 없이 심취해버렸던 나는, test를 작성하는데 열정과 시간이라는 비용을 너무 많이 지불했던 것이다.

 아직 계속 발전시켜 나가는 중이지만 요새는 아래와 같은 기준을 정하고 테스트를 작성한다.
  1. 프로토타입을 만들 때는 절대 테스트를 만들지 않는다.
  2. 테스트를 먼저 만드는 것을 강제하지 않는다.
  3. 디버깅은 테스트 작성으로 하는 것을 원칙으로 한다.
  4. 단순히 다른 함수를 호출하기만 하는 래퍼 함수는 테스트를 만들지 않는다.
  5. 단, third party 라이브러리의 래퍼인 경우, 해당 함수가 하는 일이 직관적이지 않은 경우(문서를 찾아봐야 동작이 이해되는 경우)는 만든다.
  6. Refactoring 할 기능에 대한 test는 반드시 만든다.

프로토타입을 만들 때는 절대 테스트를 만들지 않는다.

 내 경험으로 봤을 때, 프로토타입은 엉성하게 만들어야 한다.
 다시 말하면 엄밀하게 짜인 프로토타입은 비용의 낭비다. 프로토타입을 만들다가 디버깅을 해야 하는 단계까지 갔다면, 그건 이미 프로토타입의 단계를 넘어갔다고 본다.
 그런 의미에서 프로토타입을 만드는 중에는 테스트를 만들지 않는다.

테스트를 먼저 만드는 것을 강제하지 않는다.

 아마 많은 사람이 동의 못 할 부분이 이 부분이라고 생각한다.
 나도 예전에는 이런 주장을 하는 사람을 보고 TDD를 이해 못 해서 그러는 거로 생각했다. 그런데 TDD라는 방법론을 쓰는 이유는 무엇일까를 생각해보자.
 옆 사람한테 잘 보이기 위해서일 리는 없다. 테스트를 잘 만들기 위해서라면 주객이 전도된 것이다. TDD라는 방법론을 쓰는 이유는 한 문장으로 말하면 "삽질하는 시간을 줄이고, 코드의 퀄리티를 보장하자." 정도가 될 것이다.
 그렇다면 이제 가장 최근에 작업했던 파일을 열어보자.
 모든 함수가 테스트가 필요할 만큼 복잡한 코드인가? 정말로 모두 다 그렇게 중요한가? 모든 함수가 잠재적 버그의 위험성을 가지고 있다고 보는가?
 내 경험상으로는 아니었고, 테스트 작성에도 80/20 법칙이 적용된다고 본다. 실제로 테스트를 작성하면서까지 엄밀하게 보장해줘야 하는 코드는 20% 정도밖에 안 된다고 본다.
 그 외의 부분에도 테스트를 먼저 작성할 것을 강제하면, 결국 뻔한 테스트 코드밖에 안 나온다.

디버깅은 테스트 작성으로 하는 것을 원칙으로 한다.

 그렇다면 테스트는 언제 작성되어야 할까?
 난 디버깅 중에 작성한다. 상황에 따라서 printf를 찍어가며 디버깅 하는 경우도 있지만, 난 보통 다음과 같은 과정을 따라 디버깅한다.
  1. 해당 증상을 보고 의심되는 함수를 추측한다.
  2. 의심되는 함수들에 대해 테스트 케이스들을 만들어 돌린다.
  3. 실패하는 테스트 케이스가 있으면 성공하도록 바꾼다.
  4. 버그가 없어졌는지 본다.
  5. 안 없어졌으면 1번부터 다시 한다.
 이런 과정을 거쳐서 테스트를 만들면, 쓸모 없는 테스트 때문에 코드의 크기를 늘리는 일 없이 테스트를 만들 수 있다. 난 이걸 테스트의 밀도를 높인다고 표현한다.

단순히 다른 함수를 호출하기만 하는 래퍼 함수는 테스트를 만들지 않는다.

 그렇다면 절대 테스트를 만들지 말아야 하는 경우도 있을까?
 난 특별히 예외처리나 validation이 추가되지 않는 한 래퍼 함수에 대해서는 테스트를 만들지 않는다.
 래퍼 함수가 부르는 함수는 이미 테스트 된 코드일 것이다.
 래퍼 함수에 대해 다시 테스트를 만드는 것은 테스트의 밀도를 낮춘다고 생각한다.

단, third party 라이브러리의 래퍼인 경우, 해당 함수가 하는 일이 직관적이지 않은 경우(문서를 찾아봐야 동작이 이해되는 경우)는 만든다.

 래퍼 함수라도 서드파티 라이브러리의 래퍼 함수인 경우에는 어떻게 해야 할까?
 보통 TDD를 하는 사람들은 "서드파티의 무결성은 서드파티 내에서 테스트해서 보장했을 것이니 믿고 써야 한다. (만약 못 믿을 거면 써서는 안 된다.)"라고 주장한다.
 하지만 나는 라이브러리가 예외처리를 어떻게 했는지를 대해 스펙 문서를 읽어봐야 했을 때 테스트를 작성한다. 이때 작성하는 테스트는 무결성을 보장하기 위한 테스트와는 목적이 다르다.
 문서를 찾아봐야 한다는 것은 함수 이름이 직관적이지 않았다는 것이다, 이런 함수는 다음에 봤을 때, 또 헷갈릴 여지가 있다. 다음번에 문서 찾는 비용을 줄이기 위해서 테스트를 작성해둔다.

Refactoring 할 기능에 대한 test는 반드시 만든다. 

 그렇다면 반드시 테스트를 만들어야 할 경우는 뭐가 있을까?
 많은 사람이 말하듯이 리팩토링 하기 전에도 반드시 테스트를 만들어야 한다.
 리팩토링이라는 것 자체가 test가 선행되지 않았다면, 그냥 놔두는 게 좋다고 생각한다. 그렇지 않으면 토발즈가 일부 커널개발자들에게 불평했듯이 "일단 고치고 될 때까지 뜯어고치는" 서부 영화에 나오는 총잡이 같은 개발이 될 것이다.