Dynamic Typing과 Test Driven Development

Posted 2006. 10. 8. 19:40
Dynamic Typing과 관련된 글을 블로그에 Static Type Checking이란 제목으로 쓴 적이 있는데 여기서 다시 한 번 정리해 보고자 한다.

블로그를 돌아다니다보면 많은 사람들이 TDD가 Dynamic Typing하는 언어의 특징이라고 생각하고 있음을 알 수 있다. 특히 일부 파이썬/루비 팬들이 운영하는 블로그에서 TDD가 언급되면 어김없이 동적 타이핑 언어의 테스트 편리성(혹은 크게는 생산성)에 대한 이야기가 뒤따라 온다.

하지만 C/C++처럼 수십 년도 더 된 구세대 언어와, 소프트웨어 개발 방법론과 테스팅에 대한 어느정도 이해가 쌓인 후에 나온 파이썬/루비와 같은 언어를 정적/동적 타이핑 면에서만 놓고 비교하는 것은 공정하지 못한다. 사실 너무 불공평하다.

파이썬/루비와 정반대 선상에서 정적 타입핑의 극상을 달리는 친구들은 ML과 Haskell 같은 함수형 언어(functional)들인데, 이 놈들은 C/C++ 보다 더 엄격하게 컴파일 타임에 타입 검사를 한다. 하지만 이쪽 언어 사용자들의 주장에 따르면 ML과 Haskell은 절대 테스트하기 불편한 언어도 아니고, 코드 길이가 길거나 생산성이 떨어지는 언어도 아니다.

순수하게 타이핑 측면에서만 바라본다면, 동적 타이핑은 컴파일 타임에 해주는 검사가 적은만큼(상당히 귀납적이다) 테스트를 통해 더 많은 버그를 찾아내야 하므로, 테스트를 훨씬 더 철저하게 많이 해야 한다. 잘못된 타입 연산 등의 간단한 버그 조차도 테스트를 통해서만 발견할 수 있다는 것은 재앙이다(타입 검사는 연역적이다)

테스트의 부담이 더 큰 파이썬/루비 같은 동적 타이핑 언어가 테스트를 강조하는 것은 당연한 일이다. 같은 기능을 하는 다른 프로그램 보다 더 많은 테스트를 해야 프로그램이 제대로 동작하기 때문이다. 즉 원래 언어적인 측면에서 태생적인 약점이 있으므로, 후천적인 노력으로 개선을 한 셈이다.

파이썬과 루비 같은 동적 타이핑하는 스크립트 언어의 테스트 편리성은 동적 타이핑이라는 속성에 기인한 것이 아니다. 이는 언어의 추상화(abstraction) 수준과 관계가 있다. 추상화 수준이 높을수록 코드는 사람이 생각한 바(스펙)에 가까워지고, 알아보기 쉬워지며, 금방 만들 수 있다.

C 언어와 파이썬/루비의 추상화 수준은 어떻게 다를까? 간단한 통계로 C 언어 construct 하나가 보통 1-10개의 머신 인스트럭션으로 컴파일 되는 반면에, 파이썬과 루비의 construct 하나는 100-1000개 이상의 머신 인스트럭션으로 실행된다.

즉, 파이썬이나 루비 개발자가 그토록 강조하는 "가독성", "짧은 코드"는 절대로 동적 타이핑(흔히 스크립트 언어)의 특성이 아닌 셈이다. 강력한 정적 타이핑 언어인 헤스켈(Haskell)의 퀵 소트 코드를 본 적이 있다면, 타이핑은 생산성이나 테스트 편의와는 직접적인 상관 관계가 없다는 데 동의할 것이다.

qsort [] = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++ qsort (filter (>= x) xs)

위 Haskell 코드는 polymorphic하다. qsort에 넘길 수 있는 타입은 >= 연산자가 정의된 list이기만 하면 된다. Haskell이 이처럼 간결한 코드를 보여줄 수 있는 이유는 파이썬/루비와 같은 수준의 추상화 레벨이면서, type inference를 통해 불필요한 타입 어노테이션을 필요를 상당히 줄였기 때문이다.

이처럼 같은 추상화 수준이라면 당연히 동적 타이핑보다는 정적 타이핑이 유리하다. 비유하자면, 타입 이론은 '사람은 모두 죽는다' '그러므로 소크라테스도 죽는다'라고 말하는 것이고, 테스트는 'A도 죽었고, B도 죽었고, C도 죽었으니 아마 소크라테스도 죽을 것이다'라고 말하는 셈이다.

ML 공부를 위한 참고 자료

Posted 2006. 10. 7. 20:13
ML 참고 자료입니다. 이 글을 앞으로 계속 갱신할 예정입니다.

ML 참고 문서 Version 0.1 (2006-10-07)

[1] Programming in Standard ML,Robert Harper Carnegie Mellon University Spring Semester, 2005
http://www.cs.cmu.edu/~rwh/smlbook/

카네기 멜론 대학(CMU)에서 나온 SML 입문서입니다. CMU 학부 학생들이 SML을 처음 공부할 때 보는 책이라고 나와 있습니다. 총 293 페이지로 SML의 기본적인 내용을 아주 자세히 설명하고 있어서 함수형 언어를 처음 배우는 (혹은 프로그래밍을 처음 접하는) 입문자에게 좋은 책입니다.

Parametric Polymorphism과 Dynamic Typing

Posted 2006. 10. 5. 23:18
동적 타이핑에 대한 여러 가지 오해가 있는데, 그 중 하나가 동적 타이핑을 써야만  Polymorphism이 간단해진다고 데 믿는 것이다. 일반적으로 많은 프로그래머들이 C++의 Template이나, Java/C#의 Geneircs을 쓰다가 파이썬/루비의 Parametric Polymorphism만  접해보고 나서 성급하게 내리는 결론이 아닐까 쉽다.

실제로 2 개의 인자를 받아서 큰 값을 리턴하는 max 함수를 예로 들어보자.

<파이썬의 예>
>>> def max(a, b):
...     if (a >= b): return a
...     else: return b
...
>>> max(3,4)
4
>>> max(3.5,4.2)
4.2000000000000002


<자바의 예>
public class Foo {
    public static <T extends Comparable > T max(T a, T b) {
        if (a.compareTo(b) >= 0) return a;
        else return b;
    }

    public static void main(String... args) {
        System.out.println(max(3,4));
        System.out.println(max(3.5,4.2));
    }
}


위의 예를 보면 똑같은 일을 하기 위해서 파이썬은 간단히 변수 a, b만 있으면 되는 반면에 자바는 복잡한 문법으로 타입 변수  T에 대한 정보를 줘야 한다. 여기까지만 살펴보면, 파이썬이 훨씬 좋아 보인다.

한 가지 차이가 있다면 파이썬은 동적 타입 검사를 수행하기 때문에 a, b의 인자가 실제로 비교할 수 있는 연산자(>)가 정의되어 있지 않더라도 실행이 된다(실제로 실행할 때 런타임 에러가 발생한다)는 것이고, 자바의 경우는 Comparable 인터페이스를 구현하지 않은 객체를 넘기면 바로 컴파일 에러가 뜬다는 점이다.

그럼에도 불구하고, 여기까지만 살펴보면, 파이썬과 같은 동적 타입 언어가 Parametric Polymorphism을 훨씬 손쉽게 사용할 수 있는 것처럼 보인다. 자바의 Generics는 한 번 보고 직관적으로 사용할 수 있는 수준은 아니기 때문이다. 파이썬 입문자가 엄청난 생산성 향상을 맞보고 동적 타입 언어야 말로 개발자를 위한 언어라고 생각하는 게 무리가 아니다.

그렇지만 정적 타입 언어는 반드시 C++의 Template, Java/C#의 Generics처럼 저렇게 복잡한 타입 정보를 일일이 적어줘야만 하는 운명에 놓인 것인가? 정답은 그렇지 않다. 강력한 정적 타입 언어로 유명한 ML의 예를 살펴보자.

- fun umax (x, y, gt) = if gt(x,y) then x else y;
- umax (2, 3, op >);
val it = 3 : int
- umax (2.3, 3.2, op >);
val it = 3.2 : real

코드만 놓고 보면 파이썬과 별로 차이가 없다. (op >를 직접 코드에 쓰지 않고 인자로 넘긴 이유는 ML에서 > 연산자가 오버로딩 되었다는 점과 ML의 기본 타인 추론 규칙 때문인데, 자세한 설명을 생략한다.) 어쨌거나 x, y 두 개의 인자를 넘기는데, 타입 정보는 따로 준 적이 없다.

혹시ML은 파이썬과 마찬가지로 동적 타입 언어인데, 우리가 잘못 알았던 것일까? 그렇진 않다. ML은 무척 강력한 정적 타입 체킹을 하는 언어이다. 다만 타입 추론(type inference)를 통해서 불필요한 타입 정보를 생략할 수 있게 만들었다. 위 정보를통해서 ML은 x, y 변수가 >= 연산자가 정의되어 있는 임의의 타입임을 안다.

그럼 이 ML 코드가 파이썬 코드랑 뭐가 다른 것일까? 하는 일이 똑같다면 정적 타입 언어와 동적 타입 언어의 구분이 무의미할 테니깐 말이다. 개발자가 실수로 서로 비교할 수 없는 타입인 정수와 문자열을 비교했다고 하자.

ML의 경우
- umax (2.3, "hello", op >);
stdIn:1.1-17.7 Error: operator and operand don't agree [tycon mismatch]
operator domain: real * real * (real * real -> bool)
operand: real * string * ('Z * 'Z -> bool)
in expression:
umax (2.3,"hello",>)

바로 타입 에러가 나는 반면의 파이썬의 경우 int를 문자열로 바꿔서 비교한 후에 아무렇게나 리턴해 버린다. 물론 파이썬의 경우도 정말 정의가 안 된 연산(예를 들어 정수 + 문자열)에 대해서는 런타임에 타입 오류를 내는데, ML은 컴파일 타임에 알 수 있는 버그를 파이썬은 해당 코드를 실행시켜 보아야만 알 수 있다.

요약하면, 동적 타이핑 언어를 사용하면 Polymorphism 구현이 간단해지는 것은 사실이지만, 이 역시 컴파일 타임에 오류를 알 수 없다는 동적 타이핑 특유의 단점을 그대로 가지고 있다. 반대로 정적 타입 언어는 약간의 타입 정보만을 주면 이런 오류를 컴파일 타임에 모두 잡아낼 수 있다. C++, Java/C#처럼 뒤 늦게 Polymorphism을 추가한 경우 사용의 불편함이 있지만, ML처럼 처음부터 언어에 Polymorphism을 녹여넣고 타입 추론을 적극 활용하면 불편함을 최소한으로 줄이는 것이 가능하다.

똑같은 버그를 두고 1) 실행도 해보기 전에 컴파일하면서 잡는 방식, 2) 일일이 테스트하고 QA를 거쳐서 잡는 방식이 있다면 어느 쪽을 선택해야 할지는 자명하다. 소프트웨어 결함의 제거 비용은 시간에 대한 x^2 이상의 함수임을 잊지 말자. 현대의 스크립트 언어는 여러 가지 훌륭한 점이 많지만, 그 훌륭함이 동적 타입 체킹을 하기 때문은 아닐 것이다.