프로그램이 동작한다고 믿으려면 무엇이 필요한가?

제목은 거창하지만, 조엘 온 소프트웨어의 실질적인 2기라고 생각하는 조엘의 “소프트웨어 블로그 베스트 29선”을 다시 읽으면서 되새김질 하는 중에 쓴 글.

11번째로 실린 글은 Thinking in Java/C++ 의 저자이기도 한 브루스 에켈(Bruce Eckel)의 “타입검사와 테스트(Strong-type and Strong-testing)”다. 여기에서 주장하는 것은,

  • 동적 타입 검사인지 ((Python이나 ruby 같은 요즘의 프로그래밍 언어가 그렇듯이, 컴파일 타임(?)에는 타입을 확인하지 않고, 실행 시간에 타입이 맞지 않으면(특정 함수가 없다거나) 런타임 예외를 던지거나 하는 언어들))  정적 타입 검사인지 ((C++이나 Java, nML 처럼 컴파일 시간에 타입(형)을 검사해주는 언어)) 는 중요하지 않다
  • 프로그램의 동작에서 타입 검사가 차지하는 부분은 매우 작다
  • 결국 프로그램의 신뢰성은 테스트로 보장해야 한다

라는 것들. C++도 그렇고, 타입검사가 그렇게 강점이라는 nML도 그렇고 타입 검사는

이 연산이 맞는 결과 타입을 보장하는가

만을 검사해준다. 결국 프로그램이 동작하는지 파악할 수 있는 것은 타입검사가 아니라 좀 더 광범위한 구현층, 기능층, 표현층 등 각 레벨에서의 테스트 뿐이라는 것. ((필연적으로 각종 테스트들과 테스트 프레임웍, 자동화된 테스트가 필요하게된다))

예를 들어 다음과 같은 factorial 코드가 있다고 치자. ((편의상 아주 단순한 함수인 factorial 을 역시 쉬운 언어인 python으로 구현해보겠다))

def fact( n ):
    if n != 1 : n * fact(n – 1) # 약간의 의도 가 담긴 구문
   else: return 1

이 코드가 동작하는가? 일단 타입 검사나 문법 형식 검사는 통과 한다. 그리고 보통은 잘 동작하는 것 처럼 보인다. 그러나 1보다 작은 값이 들어오면(…) 스택이 넘쳐날 때 까지 함수를 실행하게 된다.

즉, 타입 검사는 부분적인 보장일 뿐, 무엇이 정확하게 동작하는지는 보장해주지 않는다는 것.

결국 필요한 것은 이런 코드? ((Java나 C# 사용자라면 익숙한 모습이다. Python 각 파일들은 main을 가질 수 있고, 해당 python 파일을 실행하면 main() 이 호출된다. 코드는 factorial 값들을 체크하는 문장들. 그러나 비슷한 논리로 이 역시도 테스트해준 영역에서나 코드의 정확성을 보장해준다…))

def main():
    assert( 1 == fact( 0 ) )
    assert( 1 == fact( -69573 ) )
    assert( 24 == fact( 4 ) )
    assert( 120 == fact( 5 ) )
    assert( 1307674368000 == fact(15) )
    print “Test passed”

프로그래밍 작업을 통해 무언가를 만들었다는 것은, 그거에 대한 테스트 ((물론 axiomatic proof를 통한 연역적인 보장도 있겠지만, 실제로 사용될 수준의 코드들에서는 사실 상 무의미한 짓이라고 생각한다)) 가 보여주는 영역에서 성공적으로 동작하는 것일 뿐이다라는 것.

뭔가를 만들고 나면 근거없는 찝찝함(특히 간만에 예전 코드를 손댔을 때)을 만족스럽게 제거할 방법은 결국 테스트밖에 없는 것 같다. 요즘 다시 읽고 있는 “프로그램은 왜 실패하는가?”에서도 말하고 있지만, 수 많은 SW 엔지니어나 교수들이 강조해온 것 — 테스트로 프로그래밍 정확성을 보장하라라는 것 — 을 실제로 따라하기 시작한지 얼마나 되는걸까 -_-;; 좀 더 반성하고 내 코드의 질을 보장해줄 수 있는 테스트를 작성할 수 있게 노력해야; 그런 의미에서 Jolt Productivity Award인 xUnit test-pattern이 무지 땡기는데…

ps. 오늘따라 함수형 언어가 검사가 잘되서 좋다! 라고 강조하는 사람이 있어서 -_-. 함수형 언어가 정적 검사들에 좀 더 적합한 것은 사실이다. 그렇지만 프로그램의 정확성은 결국 실행해보는 테스트들만이 보장해준다. 그것도 모든 것을 포함하지는 못하고…

Jinuk Kim
Jinuk Kim

SW Engineer / gamer / bookworm / atheist / feminist

Articles: 935

9 Comments

  1. Design by contract는 컴퍼넌트 설계에 관련된 문제 아닐까요.

    물론 DbC가 사용되면 개별 계약(?)단위마다 테스트를 지정하면 되긴하죠. 그렇지만 이 글과 아주 직접적인 연관은 없는듯 합니다.
    이글에서 쓰고 싶었던 것은 “정적분석보다 실제 테스트를 믿고 써줘”라는 거라서(…)

    (3/15에 추가)
    + 만들어놓은 테스트는 그 자체가 “contract의 일부”를 표현하기도 할 것이고,
    + 이후의 변경이 contract를 깼는지 여부를 확인하는 도구가 될 수도 있겠군요

  2. 제 느낌에도 DbC의 contract가 TDD의 test와 사실상 거의 같은 개념인거 같더라고요. 그리고 동시에 실행할때도 항상 체크하게 되고.

    타입이 맞는 프로그램이 제대로 동작하는 프로그램이다! 라는 것 자체가 우리의 기준에 맞지 않는건 맞지만, 한 발자국 나간 느낌이라고 생각해요. 프로그램의 의미 구조를 formal하게 정의할 수 있게 된다면 그걸 기반으로 의미 구조가 성립하는지를 확인하는 녀석을 만들 수 있을지도 모르죠. nML수업을 듣고 생각한건 타입 추론은 큰 목적을 위해 쌓아가는 중- 정도의 의미라고 생각해요.

  3. DbC의 contract 가 TDD의 테스트랑 유사한 개념이라는데는 나도 동의.

    한발자국 나아간 것인 것 같긴하지만, 그걸로 채워지지 않는 부분이 너무 많다고 생각해. 타입 추론이 의미론적인 빌딩블럭인지는 조금 의문. 함수형 언어에서는 각종 reduction을 수행하기 위해서 어쩔 수 없이 그런 검사가 필요한 것일 뿐이지 않나하는 생각이 좀 드는데;

  4. 코딩을 제대로 안 한지 좀 됐지만, 예쩐에 그래도 좀 긴 프로그래밍 짤 때, 유닛테스트를 썼었다. 그런데 계속 테스트 실패하길래, 왜 그럴까 했는데, 유닛테스트 코드에 버그가 있었다는. 물론 내가 테스트 코드 짜는데 익숙해서 생긴 문제겠지만 ;;; 하긴 옛날(=군대 가기전) XP 세미나에서 듣기로 테스트 코드가 복잡해지만 그걸 테스트하는 코드를 짜는 경우도 있다고 듣긴 한 것 같다.

  5. 이건 별 관계 없는 얘기지만 …

    어제 convmv를 해보고 발견한건데,
    실제 mv를 수행하기 전에 이름 문자열만 먼저 바꿔보고 실패하는 것들을 쭉 뽑아내 주더라구요. 왠지 TDD를 보는 느낌(…) 알아서 해결할 방법을 제공해 주지 않는건 좀 불만이었지만요.(escaping이라거나..)

  6. 그건 내가 생각하는 TDD랑은 좀 다른듯. 내가 생각하는건,
    * 자동화되어있고
    * (그래서 인간간섭없이) 실행되는 테스트가 있는
    방법론임.

    일단 프로그래머 대부분이 생각 -> 프로그래밍 -> 테스트 하는 싸이클은 기본적으로 따라가거든. 테스트 자체가 자동화되고 (가능하면 표준화된) 결과를 보고받는 방식이어야 TDD 가 아닐까 함.

    근데 뭐 네 말대로 테스트(?)먼저 해보는 방식은 약간 TDD스럽긴 하네. 사실 *nix의 많은 툴들이 일명 dry-run (전과정을 실제 사이드이펙트 없이 순서대로 밟아만 보는 것) 기능을 제공하지…

Leave a Reply