Skip to main content

Programming By Contract vs. Defensive Programming

Head First Object-Oriented Analysis & Design이라는 책을 읽다 보니 Programming By Contract와 Defensive Programming 방법을 비교한 부분이 있습니다. ((Head First 시리즈는 Head First Design Pattern 다음으로 두번째인데 너무 맘에 드는 시리즈입니다. Design Pattern이 뭔지 OOA&D가 뭔지 다 아시는 분들에게도 강추!!)) Programming By Contract는 Design By Contract(DBC)라는 말로 더 잘 알려져 있죠.

간단히 비교하자면 DBC에서는 client가 service를 사용하기 전에 service를 제공하는 supplier와의 contract에 따라 precondition을 만족시키는 입력만을 제공해야 하고, Defensive Programming에서는 client가 어떤 입력을 주더라도 supplier는 안전하게 동작해야 한다 정도가 됩니다.

일반적으로는 입력이 올바르지 않을 경우 DBC나 Defensive Programming 모두 supplier가 이를 처리해야 하므로 DBC와 같이 확실히 contract을 가지는 것이 supplier 입장에서 구현이 쉬워집니다. 또한 DBC를 사용하는 쪽이 코드도 간결해지죠. 코드끼리 서로 믿지 않으면서 작업하려면 체크해야 할것들이 많아지니까요.

이 부분을 읽으면서 제가 지금 하고 있는 프로젝트에 대해 생각을 해봤습니다.

지금 작업하고 있는 소프트웨어는 덩치가 엄청 큽니다. 이 하나의 소프트웨어에 4~5개의 랩들이 포함되어 있으며 각 랩은 2~3개의 파트로 나누어져 있습니다. 각 파트의 개인들이 담당하고 있는 block도 모두 다르죠.

이런 상황에서 core dump 같은 문제가 발생하면 문제의 원인이 누구인지가 모두의 관심사가 됩니다. :-)

보통 pstack을 확인하면 나오는 부분은 supplier쪽 코드들이죠. 예를 들어 null pointer access로 에러가 발생했다고 하면 null을 넘겨준 client가 문제인지 null check를 하지 않은 supplier가 문제인지 결정해야 하는데 대부분 null check를 하지 않은 supplier쪽 잘못으로 결정됩니다. (이렇게 범인(?)이 결정되면 범인은 주변의 따가운 시선을 받아가며 코드 수정하고 시험하고 문서 작성까지 해야 합니다.)

그리고 어느 랩에서 문제를 일으켰는지 저 위에 계신 분들에게도 보고가 됩니다. 문제가 발생한 랩의 장들은 주간 회의같은 곳에 들어가면 씁쓸한(?) 기분으로 나오게 됩니다. 물론 랩 분위기도 싸늘해지죠. :-)

이러다보니 코드는 갈수록 지저분해집니다. 체크하고 또 체크하고, 체크하는 코드를 체크하고 필요없는 체크가 들어가고... 심한 경우는 포인터를 쓸때마다 null check를 하는 분도 나옵니다. (too defensive? over defensive?) 왈, 포인터를 쓰고 있는데 중간에 다른 쓰레드가 해당 메모리를 null로 만들면 어떡하냐 입니다. 이런 경우 보통 pstack에 나온 코드의 owner가 진범을 찾아야 하므로 이렇게 무식하게 체크를 넣는 편이 낫다는 것이죠. 이렇게 되는 또 한가지 이유는 문제가 발생하더라도 이렇게 해 놓으면 높은 분들이 보시기에 여기는 할일을 다 했구나라고 생각하여 비난의 강도가 낮아진다는 점입니다. 결국 책임 회피용이죠.

이런 환경에서 위의 글을 보니 우리도 처음부터 DBC를 했더라면 어땠을까 하는 생각이 들더군요. 일단 책임 소재가 분명해집니다. precondition 체크를 안하면 supplier에 있는 assert와 같은 DBC 체크에 걸릴 것이고 원인은 client로 결정됩니다. supplier의 precondition 체크에 걸리지 않고 발생한 문제들은 물론 supplier의 책임이 되겠죠?

이렇게 하면 supplier 코드는 수행전에 문제 발생시 잘못이 자신에게 없음을 확인하기 위해 precondition 체크를 자발적으로(?) 하게 될것입니다. 마지막으로 client는 supplier가 제공하는 contract를 준수하기 위해 supplier가 제공하는 기능과 입/출력에 대해 좀 더 잘 이해하게 될것입니다.

물론 가장 좋은 길은 모든 랩이 하나의 목적을 위한 하나의 팀으로 기능하고 동작하는 것이겠지만 현재 제가 처한 분위기가 그렇질 못하다는 것이 부끄럽네요. 분위기가 이렇다보니 나올 수 있는 방법들은 주로 또 하나의 관리로 귀결되는 것 같습니다.

Comments

  1. 저도 고민을 많이 한 부분이네요. 그렇지만 custumer(client)를 신뢰할수 없다면 계약 자체가 의미가 없어집니다. 저 같은 경우 게임 서버를 만들고 있는데 온갖 핵이 난무하기때문에 클라이언트와의 계약을 절대로 신뢰하지 않습니다. 그렇기때문에 대단히 방어적인 프로그래밍을 하죠.

    물론 전체를 다 그렇게 검사하는게 불가능하기때문에 청정 구역을 두고 그 앞단까지만 데이터의 유효성을 검사합니다. (Code Complete의 방어적 프로그래밍 챕터에 나오는 내용이죠) 연구실의 살균실 같은 공간을 두는 건데 괜찮은 방법 같습니다. 현재는 그렇게 하고 있네요.

    항상 눈팅만 하고 있다가 저도 많이 고민을 한 부분이라서 답글 남깁니다.

    ReplyDelete
  2. 게임 클라이언트들은 게임 서버와 한 시스템이라 보기 어려울 것 같습니다. 답글을 읽으니 오히려 서버의 빈틈을 노리는 적들일것 같네요. :-) 따라서 말씀하신대로 대단한 방어적 프로그래밍이 필요할 듯 합니다.

    저희 경우는 한 시스템인데도 불구하고 다른 랩들에서 나누어 개발하다보니 서로의 책임을 면하기 위해 불필요한 방어적 코딩이 많아지는 경향이 있습니다. 이런 식의 방어적 코딩들은 대개 readability를 떨어뜨리는 경우가 많아 더 문제가 되고 있답니다. :-|

    ReplyDelete
  3. (어라 커멘트 수정이 안되네요;;)
    그러니까, contract를 적어주면 이것을 언어가 체크한다면 좋을거란 생각이 듭니다.

    ReplyDelete
  4. DBC를 언어적으로 체크할 수 있다면 더 좋겠다는 생각이 듭니다.

    ReplyDelete
  5. Bertrand Meyer가 Eiffel이라는 언어를 디자인하면서 나온 말이라니까 Eiffel이라는 언어는 DBC를 native하게 지원하겠죠? ^^ 자바에는 DBC 툴들이 많이 있는것 같은데 C++은 아직 assert밖에 없는 듯 하네요. :-|

    그런데 이 assert라는게 양날의 검인지라 이를 run-time시에도 빼지 않고 넣기에는 상당히 부담되는 것도 사실입니다. 함수들 사이에 계약 위반했다고 client 앞에서 core dump를 내버리면 곤란하니까요. assert에서 exception을 던지도록 하는 것이 괜찮을 것 같기는 한데 현재 저희 코드들의 exception-safety 레벨이 매우 낮다는 문제가 있습니다.

    얼마전 회사내에서 왜 S/W의 품질이 이렇게 떨어지나? 어떤 정책을 사용해야 하나? 개발자들에겐 어떤 교육을 시켜야 하나?등을 논의하는 자리가 있었는데 여기서 나온 얘기중 제가 뽑은 최고의 정답은... 애초에 잘하는 사람을 뽑았어야지... 였다죠? :-)

    ReplyDelete
  6. 트랙백을 걸수 없어서 뎃글 남깁니다.

    좋은글 잘 봤습니다. ^^

    http://freesearch.pe.kr/685

    ReplyDelete

Post a Comment

Popular posts from this blog

1의 개수 세기 - 해답

벌써 어제 말한 내일이 되었는데 답을 주신 분이 아무도 없어서 좀 뻘쭘하네요. :-P 그리고 어제 문제에 O(1)이라고 적었는데 엄밀히 얘기하자면 O(log 10 n)이라고 적었어야 했네요. 죄송합니다. ... 문제를 잠시 생각해보면 1~n까지의 수들 중 1의 개수를 얻기 위해서는 해당 숫자 n의 각 자리의 1의 개수가 모두 몇개나 될지를 구해서 더하면 된다는 사실을 알 수 있습니다. 예를 들어 13이라는 수를 생각해 보면 1~13까지의 수에서 1의 자리에는 1이 모두 몇개나 되는지와 10의 자리에는 모두 몇개나 되는지를 구해 이 값을 더하면 됩니다. 먼저 1의 자리를 생각해 보면 1, 11의 두 개가 있으며 10의 자리의 경우, 10, 11, 12, 13의 네 개가 있습니다. 따라서 2+4=6이라는 값을 구할 수 있습니다. 이번엔 234라는 수에서 10의 자리를 예로 들어 살펴 보겠습니다. 1~234라는 수들 중 10의 자리에 1이 들어가는 수는 10, 11, ..., 19, 110, 111, ... 119, 210, 211, ..., 219들로 모두 30개가 있음을 알 수 있습니다. 이 규칙들을 보면 해당 자리수의 1의 개수를 구하는 공식을 만들 수 있습니다. 234의 10의 자리에 해당하는 1의 개수는 ((234/100)+1)*10이 됩니다. 여기서 +1은 해당 자리수의 수가 0이 아닌 경우에만 더해집니다. 예를 들어 204라면 ((204/100)+0)*10으로 30개가 아닌 20개가 됩니다. 이런 방식으로 234의 각 자리수의 1의 개수를 구하면 1의 자리에 해당하는 1의 개수는 ((234/10)+1)*1=24개가 되고 100의 자리에 해당하는 개수는 ((234/1000)+1)*100=100이 됩니다. 이들 세 수를 모두 합하면 24+30+100=154개가 됩니다. 한가지 추가로 생각해야 할 점은 제일 큰 자리의 수가 1인 경우 위의 공식이 아닌 다른 공식이 필요하다는 점입니다. 예를 들어 123에서 100의 자리에 해당하는 1의 개수는 ((123/1

std::map에 insert하기

얼마전 회사 동료가 refactoring한 코드를 열심히 revert하고 있어서 물어보니 다음과 같은 문제였습니다. 원래 코드와 refactoring한 코드는 다음과 같더군요. nvp[name] = value; // original code nvp.insert(make_pair(name, value)); // refactored 아시겠지만 위의 두 라인은 전혀 다른 기능을 하죠. C++03에 보면 각각 다음과 같이 설명되어 있습니다. 23.1.2/7 Associative containers a_uniq.insert(t): pair<iterator, bool> inserts t if and only if there is no element in the container with key equivalent to the key of t. The bool component of the returned pair indicates whether the insertion takes place and the iterator component of the pair points to the element with key equivalent to the key of t. 23.3.1.2/1 map element access [lib.map.access] T& operator[](const key_type& x); Returns: (*((insert(make_pair(x, T()))).first)).second. 원래 코드는 매번 새 값으로 이전 값을 overwrite했지만 새 코드는 이전에 키가 존재하면 새값으로 overwrite하지 않습니다. 따라서 원래 기능이 제대로 동작하지 않게 된것이죠. 그래서 물어봤죠. "왜 이렇게 했어?" "insert가 성능이 더 좋다 그래서 했지." :-? 사실 Fowler 아저씨는 Refactoring 책에서 refactoring은 성능을 optimizing하기 위한 것이 아니다라

C++ of the Day #9 - Boost.Python 사용하기 #1

Python 은 가장 인기있는 interpret 언어중의 하나입니다. Python의 장점 중 하나는 C/C++ 모듈과 쉽게 연동할 수 있다는 점입니다. 물론 손으로 일일히 wrapper를 만드는 것은 손이 많이 가고 에러를 만들수 있는 작업이나 SWIG 등과 같은 도구를 사용하면 쉽게 python 모듈을 만들 수 있습니다. Boost.Python 은 이런 SWIG와 같이 python 모듈을 쉽게 만들 수 있도록 도와주는 라이브러리로 순수 C++만을 사용한다는 점이 SWIG와 다른 점입니다. 그리고 개인적으로는 Boost 라이브러리에 포함되어 있는 것들이 왠지 좀 더 믿음직스러워서... :-) 이번 글에서는 Boost.Python 문서에 나와 있는 예제 를 가지고 간단하게 python 모듈을 만드는 방법에 대해서 알아보겠습니다. Requirements 리눅스 이 글에서는 리눅스 환경에서의 사용 방법을 설명한다. Boost.Python 라이브러리 (1.33.1) Boost 라이브러리를 다운로드받아 아래와 유사한 명령으로 라이브러리를 빌드한다. bjam -sTOOLS=gcc -with-python install bjam의 --prefix 옵션으로 라이브러리가 설치될 위치를 변경할 수 있다. Python 라이브러리 (2.4.3) Python을 다운로드 받아 빌드하여 설치한다. 위의 경우와 유사하게 configure의 --prefix 옵션으로 설치될 위치를 변경할 수 있다. Write C++ Code 다음과 같이 코드를 작성한다. // greet.cpp #include <stdexcept> char const* greet(unsigned x) { static char const* const msgs[] = { "hello", "Boost.Python", "world!" }; if (x > 2) throw std::range_error("