제목은 거창하지만, 조엘 온 소프트웨어의 실질적인 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. 오늘따라 함수형 언어가 검사가 잘되서 좋다! 라고 강조하는 사람이 있어서 -_-. 함수형 언어가 정적 검사들에 좀 더 적합한 것은 사실이다. 그렇지만 프로그램의 정확성은 결국 실행해보는 테스트들만이 보장해준다. 그것도 모든 것을 포함하지는 못하고…
Design by contract에 대해서는 어떻게 생각하세요?
Design by contract는 컴퍼넌트 설계에 관련된 문제 아닐까요.
물론 DbC가 사용되면 개별 계약(?)단위마다 테스트를 지정하면 되긴하죠. 그렇지만 이 글과 아주 직접적인 연관은 없는듯 합니다.
이글에서 쓰고 싶었던 것은 “정적분석보다 실제 테스트를 믿고 써줘”라는 거라서(…)
(3/15에 추가)
+ 만들어놓은 테스트는 그 자체가 “contract의 일부”를 표현하기도 할 것이고,
+ 이후의 변경이 contract를 깼는지 여부를 확인하는 도구가 될 수도 있겠군요
제 느낌에도 DbC의 contract가 TDD의 test와 사실상 거의 같은 개념인거 같더라고요. 그리고 동시에 실행할때도 항상 체크하게 되고.
타입이 맞는 프로그램이 제대로 동작하는 프로그램이다! 라는 것 자체가 우리의 기준에 맞지 않는건 맞지만, 한 발자국 나간 느낌이라고 생각해요. 프로그램의 의미 구조를 formal하게 정의할 수 있게 된다면 그걸 기반으로 의미 구조가 성립하는지를 확인하는 녀석을 만들 수 있을지도 모르죠. nML수업을 듣고 생각한건 타입 추론은 큰 목적을 위해 쌓아가는 중- 정도의 의미라고 생각해요.
DbC의 contract 가 TDD의 테스트랑 유사한 개념이라는데는 나도 동의.
한발자국 나아간 것인 것 같긴하지만, 그걸로 채워지지 않는 부분이 너무 많다고 생각해. 타입 추론이 의미론적인 빌딩블럭인지는 조금 의문. 함수형 언어에서는 각종 reduction을 수행하기 위해서 어쩔 수 없이 그런 검사가 필요한 것일 뿐이지 않나하는 생각이 좀 드는데;
코딩을 제대로 안 한지 좀 됐지만, 예쩐에 그래도 좀 긴 프로그래밍 짤 때, 유닛테스트를 썼었다. 그런데 계속 테스트 실패하길래, 왜 그럴까 했는데, 유닛테스트 코드에 버그가 있었다는. 물론 내가 테스트 코드 짜는데 익숙해서 생긴 문제겠지만 ;;; 하긴 옛날(=군대 가기전) XP 세미나에서 듣기로 테스트 코드가 복잡해지만 그걸 테스트하는 코드를 짜는 경우도 있다고 듣긴 한 것 같다.
테스트에 대한 테스트! 메타테스트!(…)
익숙해서 -> 익숙하지 않아서 (…)
이건 별 관계 없는 얘기지만 …
어제 convmv를 해보고 발견한건데,
실제 mv를 수행하기 전에 이름 문자열만 먼저 바꿔보고 실패하는 것들을 쭉 뽑아내 주더라구요. 왠지 TDD를 보는 느낌(…) 알아서 해결할 방법을 제공해 주지 않는건 좀 불만이었지만요.(escaping이라거나..)
그건 내가 생각하는 TDD랑은 좀 다른듯. 내가 생각하는건,
* 자동화되어있고
* (그래서 인간간섭없이) 실행되는 테스트가 있는
방법론임.
일단 프로그래머 대부분이 생각 -> 프로그래밍 -> 테스트 하는 싸이클은 기본적으로 따라가거든. 테스트 자체가 자동화되고 (가능하면 표준화된) 결과를 보고받는 방식이어야 TDD 가 아닐까 함.
근데 뭐 네 말대로 테스트(?)먼저 해보는 방식은 약간 TDD스럽긴 하네. 사실 *nix의 많은 툴들이 일명 dry-run (전과정을 실제 사이드이펙트 없이 순서대로 밟아만 보는 것) 기능을 제공하지…