정적 타입과 삽
(carefully.understood.systems)- 2000년대부터 2010년대 초까지 정적 타입의 인기가 줄었다가 2010년대 중후반 다시 늘어난 이유는 정적 타입 시스템의 품질 향상으로 설명됨
- 동적 타입 시스템은 변수와 필드의 상태·내용을 사람이 직접 판단해야 하며, 컴퓨터가 돕지도 방해하지도 않는 맨손으로 땅 파기에 비유됨
- 초기 Java나 C++98 같은 과거의 정적 타입 시스템은 nullable과 non-nullable 포인터 구분도 돕지 못하고, 타입 이름을 반복 작성하게 만드는 종이 삽에 비유됨
- TypeScript, Haskell, MyPy, Swift, Rust 같은 현대 타입 시스템은 null 처리, 합 타입·유니언 타입, 타입 추론을 통해 프로그램 오류 확인과 상태 표현을 더 잘 지원함
- IDE의 메서드 이름 자동완성 같은 기능이 널리 퍼지면서, 정적 타입 시스템에 넣은 정보가 오류 검사 외에도 생산성 이점으로 이어짐
핵심 주장
- 정적 타입의 인기는 단순한 유행이 아니라, 널리 사용할 수 있는 정적 타입 시스템의 품질이 좋아지면서 다시 높아졌다는 관점임
- 좋은 삽이 있으면 맨손보다 삽으로 땅을 파는 것이 낫지만, 종이로 만든 삽만 있다면 맨손이 더 낫다는 비유가 사용됨
- 동적 타입 시스템은 프로그램의 변수와 필드가 어떤 상태와 내용을 갖는지 사람이 직접 생각해야 하며, 컴퓨터가 그 판단을 돕지 않음
- 나쁜 정적 타입 시스템은 도움보다 부담이 커질 수 있으며, 이는 종이 삽으로 땅을 파는 상황에 비유됨
과거 정적 타입 시스템의 한계
- 90년대와 2000년대 초에 널리 쓰인 초기 Java나 C++98의 정적 타입 시스템은 nullable 포인터와 non-nullable 포인터를 구분하는 단순한 작업도 제대로 돕지 못함
- 과거의 정적 타입 시스템은 합 타입이 없고 곱 타입만 있는 구조로 설명됨
- 과거의 정적 타입 시스템은 타입 이름을 곳곳에 수동으로 작성하게 만들었음
BufferedReader bufferedReader = new BufferedReader(new FileReader(filename));같은 코드는 작은 재난으로 표현됨
현대 타입 시스템의 개선점
- TypeScript, Haskell, MyPy, Swift, Rust 같은 현대 타입 시스템은 nullable 타입과 non-nullable 타입을 구분하는 방법을 제공함
- Haskell은
Maybe t, TypeScript는T | null, Swift는T?, Rust는Optional<T>를 예로 들며, 타입 시스템이 null 검사가 필요한 위치와 누락 여부를 알려줄 수 있음 - 실제로 런타임에서 null pointer 오류를 거의 보지 않게 된다고 설명됨
- 현대 타입 시스템은 합 타입이나 유니언 타입 중 하나 이상을 제공하며, "Make invalid states unrepresentable" 실천을 가능하게 함
- 이 방식은 상태 머신을 나타내는 객체에서 각 필드가 관련 상태일 때만 존재하도록 표현할 수 있게 함
- 현대 타입 시스템은 타입 추론을 제공하며, 컴파일러가
let x = 5;를 숫자로 판단할 수 있으면let x: number = 5;를 작성할 필요가 없음
IDE 기능과 결론
- 메서드 이름 자동완성 같은 IDE 기능이 널리 퍼지면서 정적 타입 시스템의 유용성이 더 커짐
- 90년대에는 Intellisense가 Visual Studio의 핵심 기능이었지만, 2020년대에는 비슷한 기능이 거의 모든 IDE와 에디터에서 제공됨
- 정적 타입 시스템에 넣은 정보는 프로그램 오류 검사뿐 아니라 추가적인 생산성 이점을 만들어냄
- 좋은 동적 타입 시스템은 나쁜 정적 타입 시스템보다 낫지만, 지금은 과거보다 훨씬 더 나은 정적 타입 시스템을 사용할 수 있음
댓글과 토론
Lobste.rs 의견들
-
이 글은 좋지만 완전히 동의하진 않음. 2000년대 초의 정적 타입 시스템이 훌륭하진 않았어도, 정적 타입이 전혀 없는 것보다는 훨씬 나았다고 봄
닫힌 합 타입은 없었지만 하위 타입으로 상당 부분을 모델링할 수 있었고, null 불가 타입은 없었지만 C++의 참조와 비포인터 타입, Java의 기본 타입이 일부를 담당했음. Ruby나 JavaScript에서는 모든 타입이 null 가능할 뿐 아니라 문자열처럼도, 정수처럼도, 프로그램 안의 다른 모든 타입처럼도 취급될 수 있어서 더 나쁜 상황이었음
정적 타입에 대한 흐름이 바뀐 큰 이유는 Web 2.0 소셜 네트워크 붐 때 선점 효과가 모든 것보다 중요했기 때문이라고 봄. Ruby나 Python으로 기술 부채를 쌓더라도 빨리 출시하고 반복하는 편이 Friendster나 Digg처럼 밀려나는 것보다 나았고, 느리면 당시 쉽게 구할 수 있던 저금리 자금으로 서버를 더 사면 됐음
이후 모바일 붐에서는 통제할 수 없는 제한된 사용자 기기에서 소프트웨어가 돌아가게 됐고, 느린 동적 타입 앱은 그냥 실제로 느렸으며 타입 오류가 나면 서버처럼 최상위 응답 핸들러로 우아하게 복구할 수도 없었음. 그 환경에서는 정적 타입의 안전성과 성능이 훨씬 설득력 있어짐- Java와 90년대식 C++을 동적 타입 언어 코드베이스와 비교해 버그율이 비슷하다는 논문들이 꽤 있고, 동적 언어 선호자들이 이를 정적 타입이 유용하지 않다는 근거로 자주 듦
2000년대 초에는 나도 동의했는데, 당시 타입 시스템은 거의 틀리지 않는 속성만 강제하면서 코드 구조화에는 도움이 안 되는 제약을 부과하곤 했음. 특히 하위 타입과 구현 상속이 결합된 방식은 유연하지 않았음
더 현대적인 타입 시스템을 쓰면서 생각이 바뀜. snmalloc에서는 C++ 타입 시스템으로 메모리 소유권 상태 기계를 강제하고, 다른 코드베이스에서는 링 버퍼 카운터의 올바른 오버플로 동작을 검사함. 둘 다 틀리면 디버깅이 귀찮고 흔한 오류 원인인데, 실제로 맞다고 생각한 코드에서 컴파일 실패를 내서 버그가 트리에 들어가지 못하게 막아줬음 - 동적 타입 언어로 개발하는 게 정적 타입 언어보다 느리다고 봄. 반대 주장을 계속 보지만 이해가 안 됨
IDE에서.을 누르고 메서드 이름을 조금 치다가 맞는 후보에서 Enter를 치면 몇 초마다 2초씩 아끼고, 어떤 메서드가 있는지 모를 때 클래스 정의를 찾아보는 30초도 아낌. 이 원리는 https://grugbrain.dev/#grug-on-type-systems 에도 잘 남아 있음
함수 매개변수 타입을 쓰는 일보다 메서드를 호출하는 코드 줄을 훨씬 자주 쓰기 때문에, 거래는 동적 타입에 압도적으로 불리함. 가치 있던 건 런타임에 실패할 말도 안 되는 코드를 허용하는 게 아니라, 지역 변수 타입을 생략하는 것이었고 정적 타입 언어가 애초에 그걸 금지할 필요는 없었음 - 2000년대 초의 인기 타입 시스템은 그저 “엄청나진 않았다” 수준이 아니라 별로였고, 매우 장황했음
타입 시스템을 진지하게 쓰는 드문 코드베이스는 아무 말도 하지 않는 코드가 페이지 단위로 쌓였고, 그래도 런타임 조건문이 산더미였으며, Java라면 타입 계층이 늘어날수록 프로그램도 실질적으로 느려졌음. 대부분의 코드베이스는 타입을 듬성듬성 쓰면서 런타임 조건문을 많이 넣었고, 필요한 테스트 범위 면에서 동적 타입 시스템 대비 크게 절약되지 않았음
동적 타입 언어는 정적 보상은 없었지만, 간결해서 읽고 검토하기 쉬웠고 테스트도 쉬웠음. 특히 90년대 말~2000년대 초의 의존성 주입 프레임워크처럼 새 서비스를 추가할 때마다 XML 파일 여러 개를 고쳐야 하는 환경에서는 더 그랬음. RAM 절반을 먹는 IDE 없이도 작업할 수 있었음
내 초기 커리어가 딱 이랬기 때문에 글에 완전히 동의함. Java 1.4부터 Java 6까지의 비용 대비 효용이 너무 나빠서 정적 타입 언어를 거의 포기하게 됐고, 몇 년 뒤 취미로 Haskell을 만져보고 나서야 정적 타입도 합리적인 비용 대비 효용을 가질 수 있으며 문제는 Java였다는 걸 알게 됨. “python is not java” 에세이도 그 어두운 시절을 잘 보여줌 - 상속 기반 하위 타입은 더 빈약했음. 완전성 검사가 되는 패턴 매칭의 사용성을 얻지 못했고, 구현이 여러 곳으로 쪼개졌음
- 경쟁자보다 먼저 사이트를 띄워 사용자 앞에 내놓고 규모의 경제를 잠그는 게 중요했다는 설명은, 요즘 상황과도 꽤 익숙하게 들림
- Java와 90년대식 C++을 동적 타입 언어 코드베이스와 비교해 버그율이 비슷하다는 논문들이 꽤 있고, 동적 언어 선호자들이 이를 정적 타입이 유용하지 않다는 근거로 자주 듦
-
정적 타입이 시대정신이 된 뒤 우리 소프트웨어의 신뢰성 이득을 실제로 봤는지 의문임
정적 타입의 장점은 즉각적인 개발 피드백과 치명적인 런타임 실패 감소에 훨씬 더 있다고 생각했는데, 이론상 그런 실패는 늘 가능해도 실제로 그렇게 자주 발생하는 것 같지는 않았음- 봤음. 사소하지 않은 TypeScript 코드베이스에서 TypeScript 오류 0개를 목표로 삼기 시작하자
undefined와null에 메서드를 호출하려는 시도가 급격히 줄었음
주니어와 일부 시니어는 처음엔 여기저기@ts-ignore가 생길 거라고 회의적이었지만, 실제로는 의존성의 깨진 타입 때문에 생긴 것까지 포함해 세 개쯤뿐임. 예전에는 개발 브랜치에서 타입 혼동 때문에 일주일에 한 번쯤 앱이 죽어 내 작업을 막았는데, 지금은 마지막으로 그런 일을 겪은 게 언제인지도 기억 안 남
tsc를 만족시키는 것만으로도 내가 직접 코드를 쓰지 않은 경우까지 타입 관련 버그가 줄어듦. 반면 요즘 린터는 지나치게 열성적이고, Sonar 같은 도구를 만족시키려다 실제 리팩터링 파손을 봄. 경고 95%는 가짜였고, 3%는 도구 쪽 버그였으며, 도움이 된 2%도 실제 버그 원인은 아니었음. 코드베이스를 맞추느라 1주일을 쓰고 버그 하나를 잡는 대신 그 과정에서 두 개를 더 넣었음
tsc를 만족시키는 작업은 대략 하루에 순수 버그 수정 2개와 회귀 1개 정도를 만들었지만, 회귀는 보통 전체 크래시가 아니라 잘못된 동작 수준으로 더 낮은 심각도였음
여기에 속성 기반 테스트를 추가하면 평균 2~4시간 걸렸고 항상 최소 하나의 버그를 드러냈음. 코드가 속성 기반 테스트 가능하다면 해야 함
저렴한 모델인 DeepSeek V4 Flash로 테스트 범위를 늘리되 쓰레기 테스트가 생기지 않게 주의하니 하루에 논리 버그 2~3개 정도를 고쳤고, 크래시는 없었음. 다만 테스트 묶음은 간신히 유지보수 가능한 수준임
주니어가 Sonnet과 Opus 4.5, 4.6 계열로 대충 테스트를 만들게 했을 때는 모델들이 “현재 동작을 문서화”하는 테스트만 만들어 수정 효과가 작았고, 테스트 묶음은 유지보수 불가능해서 폐기해야 했음
모델 기반 테스트는 버그를 잡는 데 매우 좋지만 설정이 복잡하고, 표면 기능에 사이클을 태우지 않고 구석구석 파고들게 유도하는 일이 매우 번거로움. 프로파일 기반 모델 기반 퍼저 같은 게 있으면 흥미로울 것 같음
요약하면 타입 검사기는 치명적 실패와 여러 혼동을 잘 잡고, 속성 기반 테스트는 훌륭함. 일반 테스트는 꾸준히 보상을 얻으려면 많은 규율이 필요함 - 개인적으로는 그렇다고 봄. 내가 쓰는 JavaScript에서 null 포인터 버그는 TypeScript로 옮긴 뒤 거의 무시할 만한 수준이 됐고, 동료들도 비슷했음
- 봤음. 사소하지 않은 TypeScript 코드베이스에서 TypeScript 오류 0개를 목표로 삼기 시작하자
-
여기서 TypeScript를 좋은 타입 시스템과 한데 묶는 데 가장 동의하기 어려움
- 맞음. TypeScript는 건전하지 않고, 특히
await을 가로질러 타입을 좁히는 방식이 여러 번 나를 물었음. 그래도 상황을 극적으로 개선한 건 사실임
솔직히 구조적 타입 지정도 결국 받아들이게 됐고, 앞으로의 언어 설계에 긍정적 영향을 줄 거라고 봄
- 맞음. TypeScript는 건전하지 않고, 특히
-
이 주장은 설득력이 약함. 대수적 자료형과 타입 추론을 갖춘 괜찮은 프로그래밍 언어는 90년대 중반부터 있었음
Java와 C++의 타입 시스템은 매우 빈약했지만 SML, OCaml, Haskell은 이미 있었고 오늘날과 크게 비슷한 느낌이었음. 사람들이 그 언어들을 쓰지 않았다면 그건 문화, 채택, 말로 드러나지 않은 요구사항의 문제지 “사용 가능한 타입 시스템이 충분히 좋지 않았다”만으로 설명할 수 없음
혹은 “당시 인기 언어의 타입 시스템은 나빴고, 오늘날 인기 언어의 타입 시스템은 더 좋아져서 타입 시스템이 더 인기를 얻었다”는 주장이라면 순환 논법처럼 들림
타입 시스템과 함께 설계된 언어와, 원래 타입 없이 설계된 뒤 나중에 타입 시스템이 얹힌 언어의 차이에 대해서도 많은 뉘앙스가 있음 -
원래 동적 타입을 선호해 온 입장에서도 이 글은 꽤 공정하다고 봄. 요즘은 C#으로 일하고, 취미로는 Lisp을 쓰며 예전에는 Python도 썼음
Java 5를 써야 했을 때는 대개 라이브러리 개발자의 나쁜 결정 때문에 타입 시스템과 계속 싸웠음. 2010년쯤 C#으로 옮긴 뒤에는 타입 시스템이 적극적으로 해롭지는 않았지만 대체로 중복적이었고, Python에서 가장 흔한 타입 혼동이던 null 포인터 예외도 막아주지 못했음
C#의 타입 시스템이 실제로 도움이 되기 시작한 건 2020년쯤 null 불가 참조 타입이 들어오면서부터임. 올해는 네이티브 유니온 타입도 들어오지만, 완전성을 강제하는 유니온 타입 라이브러리는 적어도 2016년부터 가능했고 나는 2020년부터 쓰기 시작했음
유행도 여전히 역할을 한다고 보지만, 그중 일부는 나쁘지 않음. 더 표현력 있는 타입 시스템을 가진 유행 언어들이 우리가 돈 받고 쓰는 보통 언어들에도 개선을 가져왔음 -
Haskell과 그 타입 시스템은 이미 2000년대에도 있었음. 지금처럼 널리 쓰이진 않았지만 분명 존재했으니, 이 주장은 그 부분을 보완해야 함
개인적으로는 TypeScript가 주류 언어 사용자들에게 더 나은 타입 시스템을 익숙하게 만든 큰 요인이었다고 봄. 품질과 Microsoft의 지원 외에도 JavaScript에 적용된다는 장점이 있었고, JavaScript는 Python보다 타입이 더 절실했음. “Undefined is not a function.”과 “The good parts.” 때문임- 최신 JavaScript 판에 맞춘 “good parts” 책이 간결함을 유지한 채 나오면 좋겠음
“Real World Haskell”은 2008년에 나왔고, 주류 프로그래머에게 Haskell을 더 매력적으로 보이게 하려는 목표였음. 좋은 소식을 퍼뜨리는 데 얼마나 도움이 됐는지는 모르겠음
Java 세계에는 Scala가 2004년에 멋진 타입을 가져왔고, .NET에는 F#이 2005년에 나왔음. Scala는 Twitter 같은 눈에 띄는 사용자를 가장 많이 얻었을지도 모르지만, TypeScript처럼 해당 플랫폼 사용자의 큰 비중을 흡수할 위치에 있지는 않았고 Rust나 Go처럼 다른 언어 사용자들을 대거 끌어들일 만큼 매력적이지도 않았음 - 글은 이 문제를 이미 다루고 있음. 90년대와 2000년대 초에 인기 있던 초기 Java나 C++98 같은 빈약한 정적 타입 시스템을 종이 삽에 비유했음
바로 다음 문단에서 Haskell을 “현대적 타입 시스템”으로 언급하지만, 90년대 말과 2000년대 초에 Haskell 경험이 있는 사람은 개인적으로 만져본 수준까지 포함해도 사실상 0%에 가까웠음. 글은 당시 대다수 개발자가 정적 타입 언어를 어떻게 경험했고, 왜 그 대다수가 정적 타입 언어를 집단적으로 피했는지를 말하는 것임 - Haskell과 OCaml은 어느 정도 도구 생태계가 약해서 고생한다고 봄. 언어 자체는 훌륭하지만, 도구 쪽의 수많은 작은 종이 베임 때문에 채택을 잃음
예를 들어 OCaml에서dune을 쓰려면opam파일,dune파일,ocaml module문법,ocaml문법을 이해해야 함. Haskell의 선택적 컴파일러 확장도 똑같이 무섭게 느껴짐
cargo에서는toml과 Rust만 알면 되는 것과 대비됨
- 최신 JavaScript 판에 맞춘 “good parts” 책이 간결함을 유지한 채 나오면 좋겠음