강력한 정적 타이핑, 나의 죽음을 걸고 싶은 언덕
(svix.com)- 20년 이상 소프트웨어를 작성한 경험에서 강력한 정적 타이핑은 REPL이나 일회성 스크립트 같은 예외를 제외하면 거의 항상 선택할 가치가 있음
- 타입은 호출자와 피호출자 사이의 계약을 코드에 남겨, 잘못된 매개변수나 반환값을 컴파일·타입 검사 시점에 걸러냄
- HTML 입력에서 온 문자열
"20"이 숫자처럼 쓰여"201"이 되는 예시는, 런타임 전에 잡히는 오류와 고객에게 노출되는 오류의 차이를 보여줌 - Svix는 Redis 키, 캐시 값,
PersonId·PetId같은 식별자, API 입력 검증을 타입 시스템에 넣어 오타와 잘못된 ID 전달을 줄이려 함 - 타입을 생략하면 초기 구현은 빨라질 수 있지만 문서화·테스트·디버깅 비용이 커지고, 타입 추론과 IDE 지원을 쓰면 리팩터링과 온보딩이 쉬워짐
정적 타입을 고집하는 이유
- 강력한 정적 타이핑은 좋은 아이디어를 넘어, 대부분의 소프트웨어에서 올바른 기본값에 가까움
- 타입이 없는 언어나 변형도 쓸모가 있음
- REPL 사용
- 이미 타입이 거의 없는 환경의 일회성 스크립트, 예를 들어 셸
- 그 외 대부분의 경우에는 강한 타입을 선호함
- 타입을 쓰지 않으면 당장 개발 속도는 빨라질 수 있지만, “절벽을 향해 전속력으로 가는 것”에 가깝다고 봄
- 선택지는 결국 둘 중 하나임
- 더 일해서 불변 조건을 컴파일 또는 타입 검사 시점에 확인함
- 덜 일하고 런타임에 확인하거나, 아예 런타임에서도 확인하지 않음
- 런타임 오류는 개발 중 항상 잡히지 않고, 잡히더라도 고객에게 드러나는 방식으로 발생할 수 있음
- 테스트는 도움이 되지만 가능한 모든 잘못된 함수 매개변수 타입을 테스트하기는 어렵고, 타입으로 잘못된 타입을 막는 편이 쉽다고 봄
타입은 코드 계약과 버그 감소에 직접 연결됨
- 타입은 사람과 도구 모두에게 유용한 코드 주석이자, 코드 조각 사이의 계약을 더 엄격하게 만드는 장치임
- 같은 생일 축하 함수라도 계약의 명확성이 크게 달라짐
birthdayGreeting1(...params)는 매개변수 개수조차 드러나지 않아 문서를 읽지 않으면 동작을 알기 어려움birthdayGreeting2(name, age)는 이름과 나이가 있다는 힌트는 있지만 타입은 없음birthdayGreeting3(name: string, age: number): string는 입력과 반환 타입까지 계약에 포함함
- 함수가
age + 1을 사용하도록 바뀌면 타입 없는 버전은 문자열 입력에서 문제가 생김- HTML 입력에서 온 값은 항상 문자열일 수 있음
birthdayGreeting2("John", "20")는"John will turn 201 next year!"를 반환함- 타입 있는 버전은
age가 숫자여야 하므로 잘못된 호출이 컴파일에 실패함
- 호출자와 피호출자 사이의 계약은 코드베이스가 커질수록 중요해짐
- 피호출자가 바뀔 때 호출자가 받는 영향을 알 수 있음
- 오픈소스 라이브러리처럼 서로 다른 사람이 호출자와 피호출자를 작성하는 경우 특히 중요함
- 이런 계약이 없으면 변경이 어디까지 영향을 미치는지 파악하기 어려움
개발 경험, 리팩터링, 온보딩에서의 이점
- 타입 정보는 IDE와 개발 도구가 활용해 개발 경험을 크게 개선함
- 코드를 작성하는 동안 기대가 틀렸을 때 바로 알 수 있어 인지 부하가 줄어듦
- 개발자는 현재 문맥의 모든 변수와 함수 타입을 기억하지 않아도 되고, 컴파일러가 어긋난 부분을 알려줌
- 리팩터링도 쉬워짐
- 함수 구현을 바꿀 때 다른 곳의 가정을 깨는지 컴파일러가 알려줄 수 있음
- 새 엔지니어가 코드베이스나 라이브러리에 적응하기도 쉬움
- 타입 정의를 따라가며 어디에서 사용되는지 이해할 수 있음
- 변경하면 컴파일 오류가 발생하므로 실험하기 쉬움
Person타입을 받는 함수 예제에서 차이가 드러남birthdayGreeting3(person: Person)은Person사용 지점을 IDE로 찾기 쉬움- 타입 없는
birthdayGreeting2(person)이 실제로Person을 기대한다는 점은 코드베이스 전체를 읽어야 알 수 있음
- 문서로 일부를 보완할 수는 있지만, 문서는 오래되기 쉽고 타입은 코드 자체에 남는 문서가 됨
- 타입은 유용한 변수명을 더 강하게 만든 형태에 가깝다고 봄
Svix가 타입 시스템에 정보를 넣는 방식
- Svix는 가능한 많은 정보를 타입 시스템에 넣어 컴파일 시점에 잡을 수 있는 오류를 줄이고, 개발 경험도 개선하려 함
- Redis는 본질적으로 문자열 기반 프로토콜이고 내장 타입이 없어서, Redis 계층에서 타입의 장점이 사라질 수 있음
- 단순 캐시 예제에는 두 가지 버그가 있음
person-{id}와preson-{id}처럼 키 이름 오타가 있음- 사람 데이터를
Pet타입으로 로드하려 함
- Svix는 이런 문제를 피하기 위해 두 가지를 적용함
- 키를 일반 문자열이 아니라 특정 타입으로 요구함
- 키와 값을 강제로 짝지음
- 예를 들어
PersonCacheKey::new(id)로 만든 키를 사용하면,cache.get(PersonCacheKey::new(id))결과를Pet으로 받으려는 코드는 컴파일에 실패함 - 단순
StringID도 실수를 부르기 쉬움do_something(id: String)은 어떤 ID를 받아야 하는지 명확하지 않음pet.id를 넘겨야 할 곳에 실제로는pet.owner를 넘겨야 하는 실수를 할 수 있음
- Svix는 각 ID에 별도 타입을 둠
PersonId(String)PetId(String)Pet의owner는PersonId
- API에서 받은 ID의 유효성도 타입 생성과 연결함
- 예를 들어 pet ID는
pet_접두사 뒤에 Ksuid가 붙는 형식임 PetId는 검증 없이 생성될 수 없게 함- 이 방식이면 데이터베이스에서 pet을 찾지 못해
404 Not Found를 반환할 때, ID 형식 자체는 유효했다고 확신할 수 있음 - 유효하지 않은 ID는 API 핸들러에서 이미
422또는400으로 처리됨
- 예를 들어 pet ID는
반대 논점과 도구의 역할
- 타입에 대한 주요 반대 논점은 개발 속도, 학습 곡선과 타입 복잡성, 노력과 보일러플레이트임
- 타입 없이 프로토타이핑하면 확실히 더 빠를 수 있음
- 컴파일러 불평 없이 코드를 주석 처리할 수 있음
- 올바른 값을 정하기 전까지 필드에 잘못된 값을 넣을 수 있음
- 하지만 이는 공격적이고 불필요한 기술 부채이며, 로컬·테스트 스위트·프로덕션에서 디버깅할 때 여러 번 비용을 치르게 된다고 봄
- 학습 곡선은 존재하지만 대부분의 사람은 타입 전문가가 될 필요가 없음
- 단순한 타입 표현만으로도 충분히 작업할 수 있음
- 막히는 경우에 도움을 받으면 됨
- 개발자는 이미 코딩, React, Axum 같은 프레임워크 등 많은 것을 배워야 하므로 타입 학습 부담이 과장되어 있다고 봄
- 타입 학습은 한 번의 비용이고, 특정 코드베이스에 온보딩할 때 타입의 도움으로 얻는 이점이 더 큼
- 타입을 쓰지 않으면 기본적인 안정성을 얻기 위해 상당한 문서화와 테스트가 필요함
- 문서와 테스트는 오래될 수 있음
- 올바른 타입을 추가하는 편이 더 적은 노력이라고 봄
- 타입 추론이 없는 언어에서는 타이핑이 번거로울 수 있음
- Java 예제는
Person person1 = newPerson();처럼 반복이 생김 - 글에는 이후 Java에 타입 추론이 있다는 정정이 추가됨
- Java 예제는
- Rust처럼 타입 추론이 있는 언어는
let person1 = new_person();처럼 더 간결함 - 타입의 이점을 얻으려면 언어를 이해하는 최신 코드 완성 기능을 지원하는 코드 편집기나 IDE가 필요함
vim대emacs, 탭 대 스페이스 같은 취향 논쟁과 달리, 타입은 비용 대비 이점이 커서 쓰지 않을 이유를 이해하기 어렵다는 입장임- 후속 글로 using the type system effectively가 있음
댓글과 토론
Hacker News 의견들
-
이 논의에서 가장 답답한 건 전부 사람들이 어떻게 느끼는지에 대한 이야기이고, 실증 근거가 부족하다는 점임
기존 연구로는 두 방식 사이에 의미 있는 차이가 없다고 봤고, 새 연구가 있지 않은 한 각자 선호하는 쪽이 확실히 맞다고 단정하기는 어려움
개인적으로는 타입 있는 언어를 좋아하지만 TypeScript 같은 타입 시스템은 부족함. 런타임에서 타입을 실제로 쓸 수 없어서 런타임 버그가 남고, 많은 런타임 로직을 타입 시스템에 인코딩할 수 없어 불가능한 경우를 여전히 직접 검사하게 됨
타입 시스템이 런타임 버그를 생각할 필요를 거의 없애준다면 압도적인 장점이겠지만, 대부분의 언어는 그 수준이 아니라 오버헤드와 일부 이점 사이의 애매한 중간 지점에 머묾
버그 수나 속도에서 큰 차이가 안 나는 이유는 결국 상쇄되기 때문 같음. 타입 안전망이 없으면 테스트를 더 많이 하게 되고, 반대로 타입 시스템을 과신하면 결국 비슷한 양의 런타임 버그가 남음. 이 주제에 대한 단단한 연구가 있었으면 좋겠지만 어려운 문제임- 대부분은 타입이 많은 버그를 막는다는 데 동의할 것 같고, 이 스레드에도 그런 연구가 올라왔음
핵심은 그보다 타입에 투자할 가치가 없다고 판단하는 주관적 이유가 무엇인지에 가까움 - 결국 판단 문제로 만족해야 할 것 같음
몇 년 전 개발자 생산성 연구를 살펴봤는데 거의 다 형편없거나 주니어에게만 제대로 적용되는 내용이었음. 예를 들어 초보자는 정적 오류에 대한 빠른 피드백에서 큰 이득을 봄
좋은 실험 설계를 대학생이 아닌 전문가에게 적용하기가 거의 불가능하고, 개인차·개발 종류·관리 방식 등 수많은 변수를 분리해야 해서 신호를 뽑아내기 어렵다. 슬프지만 삶의 많은 것들은 효과적으로 측정하기 힘듦 - 내 생각도 거의 같음
글과 많은 댓글이 프로그래머 편의성, 생산성, “정확성”을 이야기하지만, 현재 연구로는 정적 타입이 이런 것들을 개선하거나 악화한다는 의미 있는 결과가 없음. 사실상 주관적임
다만 정적 타입이 실제로 주는 효과 하나는 사소하게 증명 가능함. 더 효율적인 코드를 쓸 수 있게 해준다는 점임. 타입 규율 논의에서는 이게 중심이어야 하고, 나머지는 지금 단계에선 뜬구름에 가까움
글에서 쓴 TypeScript는 실제로 강한 타입이 아니고, 정적 타입이지만 약한 타입임. 타입이 주석에 가까워 성능이나 메모리 배치 보장도 없음. 그래서 문서화 외에는 정적 타입의 비용을 내면서도 실질적 이점은 거의 못 얻음
기술 커뮤니티가 실제 근거를 무시하고 문화적·개인적 선호를 사실처럼 받아들이는 게 놀라움 - 정적 타입은 더 신뢰할 수 있는 소프트웨어를 위한 스위스 치즈 한 장일 뿐임
다른 기법처럼 구멍이 있으니 최대 신뢰성을 위해 여러 기법을 섞어야 함. 모든 것을 잡지 못한다고 정적 타입을 버리는 건 도둑이 창문을 깰 수 있으니 문을 잠그지 않겠다는 것과 비슷함. 보안이 정말 중요하면 문도 잠그고 창문에도 쇠창살을 달아야지, 둘 중 하나만 고르는 게 아님 - 타입을 표현하는 언어로 프로그램을 쓰면 “런타임 버그”가 마법처럼 사라진다고 생각하는 것처럼 보임
언어가 일반 프로그램을 쓸 만큼 강력하다면 버그도 만들 수 있을 만큼 강력함
정적 타입은 특정 종류의 버그를 잡는 데 효과적일 수 있지만 전부는 아님. 때로는 정적 단위 테스트나 실행 가능한 문서용 도메인 특화 언어처럼 가독성을 높여줌
일반적으로 동적 언어는 더 민첩하고 테스트를 더 쉽게 많이 쓸 수 있음. 정적 타입 언어였다면 쓰지 않아도 되는 테스트도 있으니 타입은 여전히 유용하지만, 흔히 믿는 만큼 보편적으로 강력하지는 않음
- 대부분은 타입이 많은 버그를 막는다는 데 동의할 것 같고, 이 스레드에도 그런 연구가 올라왔음
-
정적 타입을 좋아해야 한다는 사회적 압박과 별개로, 결국 정적 타입에서 멀어진 이유는 항상 그 주변에 상아탑이 세워졌기 때문임
두 패러다임 모두에서 각각 10년씩 소프트웨어를 만들었고, 이제는 타입 시스템을 쓰지 않는 편을 선호함
동적 타입은 단위 테스트가 조합 가능한 코드를 강제하듯, 단순한 코드를 쓰게 만드는 강제력으로 느껴짐. 읽기 쉽고 이해하기 쉬운 코드 말임
초보 개발자가 코드베이스에 접근하기 쉬워진다는 주장도 별로라고 봄. 이해 없이 빨간 표시만 없애려는 반복 루프를 장려하기 쉽기 때문임. 타입 시스템은 각 프로젝트마다 언어 위에 또 하나의 아주 도메인 특화된 언어를 얹어 배우게 만들어, 실제 동작 이해를 방해할 때가 많음
본문에서 제시한 문제들은 타입만큼 견고하면서도 이해하기 쉬운 방식으로 풀 수 있음. 타입을 단순하게 쓸 수는 있겠지만, 내 경험상 실제로는 거의 그렇지 않았음. 자동완성도 좋아하지 않으니 그렇게 받아들이면 됨
“코드가 곧 문서”라고 외치는 늙은 개발자일 뿐일 수도 있지만, 요즘 업계에 넘쳐나는 “ChatGPT가 맞다 했고 월급은 크게 받는” 개발자들에 대한 깊은 불만에서 나온 생각일 수도 있음- 동적 타입이 대부분의 개발자에게 실제로 단순한 코드를 쓰게 만든다면 매우 강력한 주장일 것임
하지만 일반적으로는 반대 증거가 강해 보임. 실제 동적 코드를 문서화하려고 나중에 만든 타입은 처음부터 정적 타입으로 구현한 같은 기능보다 훨씬 복잡한 경우가 많음. TypeScript 생태계의 DefinitelyTyped가 수많은 사례를 제공함
그 타입들이 “단순하게 사용”된다고 보기 어렵지만, 그 복잡성은 타입 시스템 자체나 타입 정의 제공 방식에서 나온 게 아니라, 그것이 설명하는 동적 코드의 복잡성에서 나옴
처음부터 정적 타입으로 만든 동등한 패키지는 대개 인터페이스가 더 단순함. 타입을 나중에 기존 API에 끼워 맞추는 게 아니라 앞에서 정의하기 때문임
인터페이스를 명시하지 않고는 그 인터페이스가 단순한지 복잡한지 알 수 없다고까지 봄. “코드가 문서”라는 이상에는 동의하지만, 인터페이스를 명시하는 코드가 없다면 그 인터페이스는 정의상 문서화가 부족함 - “타입 시스템이 초보 개발자의 이해를 막고 빨간 표시만 없애게 만든다”는 말은 처음 듣기엔 정반대로 느껴짐
실제로 그런 장면을 옆에서 보고 싶을 정도임. 내 분야에서는 도메인 특화 로직이 동적 타입 코드베이스에서는 거의 이해 불가능한 반면, 정적 타입 코드는 개발자에게 비즈니스 로직을 가르쳐줌
“코드가 문서”라는 말도 오히려 헷갈림. 내 경험으로는 정적 타입이 있어야 코드가 문서가 됨. 없으면 객체에 어떤 속성이 있는지, 존재하지 않는다고 생각한 속성을 왜 검사하는지 알 방법이 없음. 주석이 있긴 하지만 의미 있는 주석을 남기는 사람을 거의 못 봄 - 이런 논리가 거의 전부 거꾸로 느껴져서 이해하기 어려움
내 경험은 반대임. 매우 동적인 패턴은 제대로 타입을 붙이기 어렵고, 좋은 타입 시스템은 더 단순한 패턴을 장려해서 타입도 단순해짐
빨간 표시를 없애는 건 중요함. 빨간 표시는 문제가 있다는 뜻이고, 다른 방식으로 오류를 발견하는 것보다 훨씬 쉬움. 왜 그 오류를 나중에 발견하고 싶어 하는지 모르겠음
자동완성도 싫다는 말 때문에 정적 타입 반대자를 못 믿겠다는 쪽에 서게 됨. 컴퓨터가 프로그래밍을 도와주는 걸 원하지 않는 프로그래머는 매우 의심스러움 - 상아탑과 사회적 압박이 특정 기술 도입을 꺼리게 만든다는 점은 좋은 지적임
다만 그것이 기술적 장점을 깎아내리지는 않음. 어떤 기술은 훌륭하면서도 그 주변 사람들이 허세 가득할 수 있음
동적 타입이 단순한 코드를 쓰게 만든다는 주장은 “눈가리개를 쓰고 운전하면 천천히 운전하게 돼서 좋다”는 말처럼 들림. 그런 목표라면 줄 길이나 매개변수 수 제한을 거는 린터를 쓰면 되고, 제한을 우회적으로 만들 필요는 없음
타입이 유일한 해법은 아니지만 투자 대비 효과가 매우 커서 먼저 꺼낼 도구라고 봄. 투자는 거의 없고 이득은 큼
“코드가 문서”라는 말에는 동의하지만, 타입도 코드의 일부임. 그래서 “코드가 문서이고, 타입은 코드의 일부”라고 표현하고 싶음 - 거의 같은 답을 쓰려던 참이었음
우리가 있는 영역은 공학이고, 단 하나의 정답은 없으며 모든 것은 절충임. 오히려 그래서 우리의 일이 곧바로 자동화되어 사라지지 않는 것임
이 스레드에서 다른 엔지니어의 의견이나 경험 때문에 “낮게 본다”는 분위기는 정말 불쾌함
- 동적 타입이 대부분의 개발자에게 실제로 단순한 코드를 쓰게 만든다면 매우 강력한 주장일 것임
-
대부분의 데이터가 JSON으로 네트워크를 오가는 상황에서 강한 정적 타입을 적용하는 싸움은 대체로 일관성 없이 벌어짐
가능한 도구는 모두 써야 하지만, 대부분의 “데이터”는 생각보다 훨씬 흐물흐물함. 사람들이 전화번호를 문자열로 두는 건 게을러서가 아니라, 한때 더 강한 타입으로 만들 수 있다고 착각했다가 너무 많이 당했기 때문임. 이름, 주소, 우편번호도 마찬가지임
이런 값들은 사용자에게서 받아야 하고, 사실상 텍스트를 파싱하는 방식밖에 없음. 파싱 전 원문 텍스트를 저장하지 않도록 시스템을 만들면 언젠가 거의 반드시 후회함
원래 입력 텍스트를 보관하면서 백엔드 사용자에게 타입 있는 데이터 집합으로 제공하는 계층이 최선이라고 보지만, 각자의 작은 영역에서 그만한 투자 대비 효과가 있는지는 따져야 함
무거운 평가를 한다면 SAT나 다른 수치 모델로 변환하는 계층이 필요할 가능성이 큼. 그 세계에서는 숫자가 추상화임. 다른 식으로 하려 하면 거의 반드시 고통을 겪음. 문제를 정식화로, 해 공간을 도메인으로 번역하는 계층을 두는 게 좋고 타입이 여기에 도움을 줄 수 있지만, 실제로 주목받는 “타입”은 이런 타입이 아닌 경우가 너무 많음- 글쓴이임. Svix에서 하는 방식 중 한 문단에서만 살짝 말하고 더 설명했어야 했던 게 있음
Serde와 Pydantic 같은 라이브러리 덕분에 역직렬화가 곧 검증이라는 방식을 따름. JSON 데이터를 코드의 구조체로 만들기 전 전부 검증함
Redis 예시와 비슷한데, 네트워크로 JSON을 받아도 완전히 검증한 뒤 코드에 도달하면 잘 형식화된 타입이라고 안심할 수 있음. 그래서 코드에서는 이메일 타입이 유효한 이메일이고, ID 타입이 유효한 ID라고 가정 가능함 - 이름과 주소가 깊이 들여다보면 둘 다 문자열이더라도 여전히 문자열이 아니라 별도 타입으로 써야 함
이름 필드와 주소 필드를 섞는 건 거의 항상 오류이고, 타입 시스템이 그걸 강제할 수 있음 - 대부분의 데이터가 JSON으로 오간다고 해서 정말 그렇게 혼란스러운가? 특정 경우에는 Map도 완전히 합리적인 타입임
- 글쓴이임. Svix에서 하는 방식 중 한 문단에서만 살짝 말하고 더 설명했어야 했던 게 있음
-
오타가 런타임 오류가 된다면 “더 빨리 움직이는” 게 아니고, 함수 시그니처를 바꿀 때 코드베이스를 grep해서 모든 호출 지점을 찾고 다 고쳤기를 기도해야 한다면 “더 생산적”인 것도 아님
타입은 좋지만 무엇이든 지나치면 문제가 됨. 모든 비즈니스 로직을 타입 시스템에 인코딩하는 것을 인생 목표로 삼으면 타입이 전혀 없는 것보다 더 이해 불가능한 혼란이 생김. 오류 메시지에서 타입 이름이 한 줄에 안 들어가면 너무 멀리 간 것임- 미친 듯한 TypeScript 타입 정의를 본 적이 있음
공정하게 말하면 오래된 순수 JS 코드와 맞추기 위한 것이었고, 그 불쌍한 변수에는 온갖 값이 들어갈 수 있었음
TypeScript에는 영원히 고맙지만, 미래에 “타입이 너무 멀리 갔던 시대”라는 논문에 그런 코드가 실려도 놀라지 않을 것 같음 - 예전에는
from pdb import set_trace: set_trace()로 대화형 편집을 할 수 있으니 컴파일러보다 실행 중인 프로그램과 상호작용하는 게 낫다고 생각했음
하지만 프로그램이 조금만 복잡해져도 상황이 달라짐. 큐로 시스템 간 데이터를 넘기고, 비동기·스레드·멀티프로세싱을 쓰고, 성능 핵심부에 컴파일된 바이너리 라이브러리를 쓰게 되면 결국 전부 Erlang으로 썼으면 좋았겠다고 느끼게 됨 - 함수 시그니처 변경을 실제로 그렇게 작업하는 사람들이 어떻게 일하는지 정말 궁금함
100% 코드 커버리지의 매우 엄격한 테스트 같은 전반적 방법론이 있는 건가? - 정적 타입에 대해 이런 불평을 볼 때마다, 타입 정의와 아키텍처를 도대체 어떻게 만들었길래 이게 문제가 되는지 궁금함
빌드나 컴파일 시점에 호출 지점을 못 잡아준다면 정적 타입을 쓰고 있는 게 아님 - 두 가지가 타입 논의와 어떻게 연결되는지 잘 모르겠음
오타는 가장 강한 정적 타입 언어에서도 잘못된 코드를 만들 수 있음. 아니면 코드를 쓴다는 게 대체 무슨 의미겠음? 런타임 오류와, 오류 없이 틀린 결과를 내는 것 중 뭐가 더 나쁜가?
Python 프로젝트 실행이 C++ 컴파일보다 빠를 수도 있고, 동적 타입 언어도 함수 호출을 찾는 데 grep보다 나은 방법을 제공할 수 있음
- 미친 듯한 TypeScript 타입 정의를 본 적이 있음
-
타입을 쓰지 않으면 개발 속도가 빠르다는 장점이 있다는 말도 내 경험상은 아님. 정적 타입은 일상 프로그래밍을 더 빠르게 해줌
IDE가 정적 타입 덕분에 더 좋아진다는 점은 뒤에서 다뤘지만, REPL에서도 체감함. 정적으로 잡힌 타입 오류는 실제 근본 원인에 훨씬 가까운 의미 있는 오류 메시지를 주고, 런타임 오류보다 더 빨리 고치게 해줌
타입에 대해 너무 조심스럽게 생각해야 하는 부담도 덜어줌. 컴파일러가 규율을 지키니 내가 덜 신경 써도 됨. 큰 부류의 오류가 즉시 잡힌다는 확신으로 더 빨리 움직일 수 있음
내 경험상 정적 타입 시스템은 사용하기 쉽고 개발을 빠르게 하며 신뢰성을 높임. 비용은 지금까지 두 가지뿐이었음. 배우기 더 어려울 수 있고, 구현하기 더 어렵다는 것임- 완전히 동의함. 유지보수성 논쟁도 이걸로 거의 넘어감
6개월 뒤 채용할 주니어 개발자는 타입 없는 코드에 적응하는 데 훨씬 느릴 것임
어떤 사람에게는 처음 작성이 “더 빠를” 수 있다는 점은 인정하겠지만, 그 뒤로 그 코드를 읽는 모든 개발자는 느려짐 - 바로 그 신중한 사고가 소프트웨어를 더 깨끗하고 낫게 만든다고 주장할 수도 있음
컴파일러만 통과하는 뒤죽박죽이 아니라, 정확히 무엇을 넣고 빼는지, 왜 그런지를 생각하는 게 좋을 수 있음
- 완전히 동의함. 유지보수성 논쟁도 이걸로 거의 넘어감
-
글쓴이는 거의 모든 지점에서 틀렸다고 봄. 나도 수십 년 동안 그렇게 생각했지만 최근 몇 년 사이 완전히 생각을 바꿨음
타입이 버그를 줄인다? 아니오. 아주 약간일 수는 있지만 유의미하지 않음. 관련 연구를 보면 됨
타입이 더 나은 개발 경험을 준다? 아니오. 내 REPL과 IDE에는 모든 정의와 변수가 있음. 모든 심벌 자동완성, 호출 트리, 사용처 탐색, 자신 있는 리팩터링, 함수 단독 실행·교체·래핑을 REPL과 애플리케이션 안에서 할 수 있음
모든 것을 타입 시스템에 인코딩한다? 불가능함. 런타임 검증이 필요함
요구사항이 바뀔 때 타입 정의를 풀어내는 것도 행운을 빎. 이게 결정타임. 정적 타입은 현재 이해한 도메인 데이터 모델을 너무 이르게 굳혀버리는 것임. 그 모델은 바뀌고, 운이 나쁘면 같은 런타임 안에서 여러 도메인 모델 변형을 지원해야 함. 특히 상속을 썼다면 더 고생함
정적 타입이 컴파일러 최적화에 큰 지렛대를 주는 건 맞지만, 동적 타입 언어 중에도 정적 타입을 선택적 부가 기능으로 제공하는 언어가 있음
많은 사용 사례, 특히 엔터프라이즈 개발에서는 동적 타입에 불변성 우선의 함수형 언어가 장기적으로 큰 이득을 줌
강한 타입 지지자들이 자주 저지르는 범주 오류는 같은 코드를 타입만 없이 쓴다고 가정하는 것임. 실제로는 그렇게 쓰지 않음- 결국 모두가 자기 경험에서 완전히 다른 결론을 뽑아내기 때문에 필연적으로 답답한 논쟁임
거의 모든 지점에서 내 결론은 정반대임. 런타임 검증이 필요하다는 말은 당연히 맞지만, 대부분의 런타임 검증은 피할 수 있음
요구사항 변경에 관해서는 정적 타입이 오히려 적응을 쉽게 만든다고 봄. 내가 겪은 동적 타입 시스템에서는 데이터 구조에 대한 중요한 가정이 여기저기 흩어져 있었고, 때로는 전·후조건으로 동적 검사되거나 테스트에만 있거나 아예 검사되지 않았음
요구사항을 바꾸려면 이런 암묵적 가정에 미치는 영향을 전부 추론해야 해서 변경이 무서웠음. 새 코드로 앱을 띄우는 건 쉬워도, 생각 못 한 희귀 코드 경로를 깨뜨렸는지 아는 건 매우 어려웠음
“이 인터페이스를 바꿨는데, 여기 코드 경로가 그 부분에 의존하던 걸 알고 있나?”라고 알려주는 정적 분석 단계를 훨씬 선호함. 정적 타입만이 방법은 아니지만, 같은 수준의 동적 검증과 테스트를 갖추는 것보다 부담이 훨씬 적다고 봄 - REPL과 IDE에서 정의·변수를 보고 자동완성·리팩터링을 하는 건 좋지만, 검사하려는 코드를 실제로 실행하고 있을 때만 작동함. 내 경험상 그 방식은 잘 확장되지 않음
타입 시스템이 런타임 검증을 없애지는 않지만, 올바르게 쓰면 극적으로 줄여줌
요구사항이 바뀌면 컴파일러가 다시 동작하려면 무엇을 고쳐야 하는지 정확히 알려주는 게 제일 좋은 부분임. 동적 언어에서 같은 일을 하면 직접 추적하고, 단위 테스트가 실패하길 기다리고, 빠져나간 경로가 없기를 기도해야 함 - 나는 오히려 이 부분이 정적 타입에서 훨씬 쉬웠음
특정 타입이 쓰인 모든 위치를 훨씬 더 자신 있게 찾고, 각각이 변경이 필요한지 볼 수 있었음. 동적 타입 환경에서는 이 작업이 훨씬 더 자잘하고 번거로웠음 - 그건 당신 경험일 수 있지만 내 경험은 아님. 모두에게 맞는 정답은 없다고 봄. 자기에게 맞는 걸 고르고 넘어가면 됨
- 결국 모두가 자기 경험에서 완전히 다른 결론을 뽑아내기 때문에 필연적으로 답답한 논쟁임
-
C++, Python, JS로 수십만 줄을 써본 개발자로서도 잘 모르겠음. 그렇게 명확하지 않음
셋 모두에서 생산적이지만 Python이 대체로 이김. 다만 게임 엔진이나 비디오 코덱을 Python으로 쓰지는 않을 것임
JavaScript는 일관성 없고 이상하지만 Netscape의 유산이 이미 우리 모두를 거기에 묶어놨음
매우 객체지향적이고 거대한 중첩 클래스가 많은 스타일에서는 컴파일·파싱 시점의 정적 타입이 많은 실수를 줄여줄 수 있음. 하지만 객체지향은 대체로 재앙에 가깝다고 보게 됐고, 단순한 함수와 구조화된 데이터가 거의 항상 단순성과 유지보수성에서 이김
최신 언어 서버와 IDE는 JS/Python 개발 중에도 많은 타이핑 오류를 잡아줄 수 있음. 프로그래밍의 많은 부분에는 까다롭게 굴지만, 정적 타입 대 동적 타입에는 강한 입장이 없었음. 둘 다 수백만 개의 성공한 프로젝트를 갖고 있음- C++은 타입 시스템을 떠나 전반적으로 생산성을 내기 꽤 거친 언어임
언어가 제공할 수 있는 가장 큰 생산성 향상은 가비지 컬렉션이라고 봄. 훨씬 단순한 문법과 타입을 가진 Go가 어떻게 비교될지가 더 궁금함. Java도 더 나을 수 있음. 장황하긴 해도 대부분의 작업에서 인지 부하는 C++의 일부에 불과함 - Python에서는 주로 함수형 스타일로 코딩하지만 그래도 타입이 많이 도움이 됨
오타도 많이 내고 인자 순서도 자주 틀림. 특히 머신러닝을 할 때 타입이 큰 도움이 됨. 데이터 처리에 30분을 쓴 뒤 학습 코드가 죽는 건 가장 피하고 싶은 일임
Python의 점진적 타입은 빠른 프로토타이핑과, 함수가 충분히 성숙했을 때 타입 주석을 붙이는 방식 사이의 아주 좋은 중간 지점이라고 봄 - 몇 년 전 WWDC의 프로토콜 지향 프로그래밍(Swift) 영상이 마음에 들 수 있음: https://www.youtube.com/watch?v=p3zo4ptMBiQ
- C++은 타입 시스템을 떠나 전반적으로 생산성을 내기 꽤 거친 언어임
-
강한 타입이 약한 타입보다 낫다는 논의는 정리됐지만, 정적 타입이 동적 타입보다 나은지는 정리되지 않았음
정적 타입 지지자는 컴파일러가 타입 불변식을 검증해 “정확성”을 확인해야 한다고 생각하고, 동적 타입 지지자는 그게 시간 낭비라고 봄
나는 확실히 후자임. 컴파일러는 프로그램 정확성이 아니라 타입 정확성만 확인할 수 있기 때문임. 타입 정확성은 프로그램 정확성에 필요하지만 충분하지 않음. 정적 타입 지지자들은 이 점을 인정하지 못하고, 정적 타입이 실제보다 더 많은 것을 보장한다고 착각함
글의birthdayGreeting예를 보자. 글쓴이는birthdayGreeting("John", "20")에서"20"이 숫자가 아니기 때문에 정적 타입이 버그를 잡는다고 기뻐함. 하지만birthdayGreeting(" ", 123)은 잡히지 않음." "은 이름이 아님.birthdayGreeting("Anna," -12335)도 잡히지 않음. 반대로birthdayGreeting("Anna" 4.5)는 잡히는데, 4.5도 나이로 볼 수 있으니 오히려 틀렸다고 할 수 있음
이건 중요함. “타입 버그”는 잡기 사소하게 쉽지만, 의미론적 버그는 수년 동안 숨어 있을 수 있음.uint로 저장한 계좌 잔액의 오버플로, 특정 위치에서 소수여야 하는데 아닌 숫자, 비어 있으면 안 되는 리스트 등이 그렇음. 의존 타입조차 이런 불변식을 보장하지 못함
믿기 어렵다면 우주선 폭발이나 자동차 사고를 일으킨 대형 버그를 찾아보면 됨. 내가 아는 한 진짜 타입 오류가 원인인 경우는 없었고, 압도적 다수는 의미론적 오류였음
[1] 대부분은 타입을 최소 두 축, 강함/약함과 정적/동적으로 봐야 한다는 점을 이해하지 못하고 약한 타입과 동적 타입을 계속 혼동함. C는 정적이면서 약한 타입이고, Python은 강하면서 동적 타입이며, JavaScript는 약하면서 동적 타입임- 예시 중 상당수는 타입 시스템에 따라 정적 타입으로 충분히 잡을 수 있음. 하지만 더 중요한 건 “타입 버그는 잡기 사소하게 쉽다”는 부분임
바로 그래서 나는 정적 타입 쪽임. 너무 사소해서 선언적으로, 검사 대상 코드 바로 옆에, 즉각 피드백과 함께, 모든 호출 지점과 모든 하위 표현식·문에서 처리할 수 있음
타입 주석이 의미론이나 도메인 로직이 맞다는 뜻은 아니고, 그건 여전히 테스트해야 함. 하지만 관심 있는 로직과 직교하는 수십 개의 사소한 테스트를 대체할 수 있음. 솔직히 그런 테스트를 빠짐없이 쓰는 사람은 거의 없음 - 글쓴이임. 이 예시들은 반례가 아니라 오히려 내 요점을 보여준다고 봄
글 뒤쪽에서 사용자 입력 같은 곳에서 타입을 만들 때 검증한다고 했음. 그래서Name타입은 항상 유효하고" "는 이름이 아님. 타입이 유효한 이름을 보장하므로 우리 코드베이스에서는 확실히 잡힘
birthdayGreeting("Anna" 4.5)와birthdayGreeting("Anna," -12335)는 JS에서number가 부동소수라 실제로는 유효함. 다만 글을 쓸 때는 정수를 염두에 뒀음. TS보다 더 엄격한 타입, 예를 들어 Rust가 불변식을 더 잘 정의하는 데 도움이 되는 또 다른 사례임
요약하면 단순한 예시를 보이려다 평소처럼 모든 타입을 엄격히 정의하지 않았고, 그 결과 타입으로 잡혔을 버그가 더 드러났음 - 이 예시들이 흥미로운 이유는, 내가 보기엔 필요한 게 항상 유효한 이름을 나타내는
Name타입과 항상 유효한 나이를 나타내는Age타입이기 때문임
검증을 그 타입들의 생성자 한 곳에 두고,birthdayGreeting같은 여러 메서드는 그 타입의 값을 책임 없이 사용할 수 있음
타입 검사나 최소한 선택적 타입 힌트와 정적 분석 없이 이 패턴을 잘 구현하는 방법을 모르겠음. 대신 모든 메서드에서 입력값을 검증하는 건 너무 부담스럽고, 호출자가 유효한 값을 넘긴다고 가정한 뒤 큰 사고가 나지 않도록 테스트하는 것도 만족스럽지 않음 - 우주선 폭발을 언급하니 바로 [1]이 떠오름. 이 유명한 사고는 일부 타입에 허용 범위를 미리 정의한 타입 검사와 관련되어 있었고, 네 의견과도 맞닿음.
birthdayGreeting이 1~150 범위를 받게 만드는 식은 ADA에서 쉽게 할 수 있음
우주선 관련 공개된 문제 중 더 나은 타입 검사로 잡을 수 있었을 법한 것도 몇 가지 있음. 미터법/야드파운드법 변환도 단위를 타입에 넣으면 됨. 다만 [2]의 경우 통합 테스트 쪽 잘못이었을 가능성이 큼
물론 타입 검사가 모든 코드 문제, 특히 알고리즘 문제를 찾을 수는 없고 테스트를 대체하지도 않음. 그래도 개발 단계에서 즉각 피드백과 타입 힌트는 매우 귀중함
예시에서는 이름 문자열을 사람 타입이나 객체로 바꿀 수도 있음
[1]: https://en.m.wikipedia.org/wiki/Ariane_flight_V88
[2]: https://en.m.wikipedia.org/wiki/Mars_Climate_Orbiter - 컴파일러가 프로그램 정확성이 아니라 타입 정확성만 확인한다는 게 왜 시간 낭비라는 근거가 되는지 모르겠음
완벽한 해법은 없지만 가치 있는 해법은 많음
- 예시 중 상당수는 타입 시스템에 따라 정적 타입으로 충분히 잡을 수 있음. 하지만 더 중요한 건 “타입 버그는 잡기 사소하게 쉽다”는 부분임
-
이 문제에는 상당한 수렴이 있었음. 이제 대부분의 언어는 문장 수준에서 어느 정도 타입 추론을 제공함. C++에도
auto가 있음
덕분에 코드의 타입 보일러플레이트가 크게 줄었음. C++for문에 긴 반복자 타입을 전부 써야 했던 시절은 지나갔음
함수 선언과 구조체 필드는 코드를 읽기 위해 타입 정보가 필요한 곳임. 프로그램이 몇백 줄을 넘거나 개발자가 한 명을 넘으면 어느 정도의 주석은 필수임
주된 반대는 당연히 Python과 JavaScript 사용자에게서 나옴. Python은 아주 이상한 조언식 타입 시스템을 나중에 붙였고, JavaScript는 TypeScript를 나중에 붙였음. 둘 다 덧붙인 타입 시스템이고, 타입 있는 코드와 없는 코드가 섞인 환경에서 쓰임. 이건 고통스러움
LISP도 수십 년 전에 “flavors”와 Common LISP Object System으로 타입 시스템을 나중에 붙였고, 그것도 보기 좋지 않았음. 교훈은 타입 시스템을 나중에 붙이면 엉망이 된다는 것임- Python의 타입 시스템은 상황을 고려하면 꽤 괜찮다고 봄
Optional이None사용 전 검사를 강제한다든가,typing.Protocol을 통한 구조적 서브타이핑 같은 좋은 기능이 있음. Python이 처음부터 타입을 염두에 두고 설계됐다면 더 나았겠지만, 기존 Python 코드와 통합하고 아무 코드도 깨지 않아야 한다는 요구를 감안하면 꽤 잘한 편임
Python 정적 타입의 더 큰 문제는 생태계와 관례임. 특히 많은 개발자가 사실상 데이터 과학자로서 Python을 쓰면서 더 심해짐. 제대로 된 메서드 시그니처를 쓰기 귀찮아*args/**kwargs를 남용함
메서드들이 DataFrame이나 딕셔너리를 잡동사니 가방처럼 넘기는 일이 매우 흔함. 메서드가 컬럼이나 필드를 추가·삭제해서 코드를 실행하거나 모든 줄을 읽기 전까지 데이터 가방에 뭐가 들어 있는지 모르면 보너스 점수임
물론 거의 모든 언어에서 비슷하게 할 수 있음. C#에서 모든 타입을dynamic으로 쓰거나 Go 메서드가 전부interface{}를 받게 하는 것도 가능함. 하지만 Python은 오랫동안 이런 접근을 적극 장려했고, 지금도 많은 초보 튜토리얼이 “*kwargs를 받으면 함수 시그니처를 바꾸지 않아도 된다”를 끔찍한 함정이 아니라 똑똑한 사람을 위한 고급 기능처럼 소개함 - Python과 TypeScript의 “타입 시스템”은 점진적이고 비그린필드 환경의 타입 도입을 위해 설계됐음
점진적 마이그레이션에는 필수적이고, 이런 식으로 동작하는 건 완전히 이해 가능함 - Flavors와 CLOS는 “타입 시스템”이 아님. “객체 시스템”이고, 딱히 못생기지도 않았음
Flavors는 타입 시스템이 없던 Lisp에 도입됐고, 나중에 CLOS는 이미 타입 시스템이 있던 Lisp인 Common Lisp에 추가됐음 - TypeScript의 타입 시스템은 정말 놀라울 정도로 훌륭함. 더 많은 타입 시스템이 그만큼 표현력 있었으면 좋겠음
- Python의 타입 시스템은 상황을 고려하면 꽤 괜찮다고 봄
-
사람들은 늘 이성보다 감정적으로 붙잡고 있는 것에 광적으로 굴어왔음
“타입이 버그를 줄인다”는 주장은 사실이라기보다 그럴듯하게 느껴지는 주장임
https://blog.metaobject.com/2014/06/the-safyness-of-static-t...
시도가 부족했던 것도 아님. 그 주장은 반증됐다고 말할 수도 있음
다만 개인적으로는 정적 타입을 좋아함. 주로 문서화 효과 때문이고, 우연은 아닐지 몰라도 실제로 견고한 실증 근거가 있는 긍정적 효과도 그쪽뿐임- “오래 지속되는 코드베이스를 변경할 때 타입이 버그를 줄인다”는 말은 크게 논쟁할 여지가 없다고 봄
처음에 올바른 코드를 쓰는 것보다 회귀를 막는 게 더 중요할 수 있고, 진화하지 않는 코드를 볼 때는 그 부분을 평가하지 못함
구체적으로, 큰 순수 JavaScript 프로젝트에서 객체 필드를 제거하는 건 본질적으로 지뢰밭이고 과거에 많은 버그를 냈음. 반면 완전한 TypeScript 프로젝트에서는 같은 변경을 자신 있게 할 수 있음 - 매우 강한 주장이니 극단적인 사례를 보자면, 형식 검증된 CompCert의 중간단이 있음: https://users.cs.utah.edu/~regehr/papers/pldi11-preprint.pdf
이것은 매우 강한 정적 타입이 버그를 줄인다는 실증 근거로 볼 수 있다고 생각함
결국 많은 댓글이 말하듯 타입은 이분법적 예/아니오 문제가 아니라 정적/동적, 강함/약함 등 여러 축의 큰 스펙트럼임. 타입 시스템 사이에도 차이가 크고, 사람들이 그 타입 시스템을 문제에 적용하는 방식에도 큰 차이가 있음
정적이고 강한 타입 언어에서도 모든 것을 문자열로 표현하고 계속 변환할 수 있는데, 그건 사실상 동적 타입 언어처럼 일하는 것임. 반대로 타입 시스템이 주는 도구를 활용해 합법적 값을 나타내는 클래스를 만들고 중요한 불변식을 주장하면 이점을 얻을 수 있음 - 강한 정적 타입이 코드를 자기 문서화하게 만든다는 점 때문에 훨씬 선호함
생산성이 몇 배가 아니라 몇 자릿수로 올라감. C, C++, Java, Python, JavaScript, TCL 등 이 스펙트럼의 넓은 영역을 포괄하는 여러 언어를 많이 써본 입장에서 하는 말임
최근에 만지지 않은 코드, 현재 프로젝트든 의존성이든, 그 코드를 추론하기 훨씬 쉬워짐. 어떤 함수가 반환한 객체로 정확히 뭘 할 수 있는지 알아내려고 샛길로 계속 빠지지 않아도 되므로 당면한 문제에 더 집중할 수 있음
컴파일이 통과할 때의 기분 좋은 안도감도 있지만, 그건 부차적임 - 정적 타입의 주장은 타입이 정적이기 때문에 타입 버그를 불가능하게 만든다는 것임
감정적 집착은 “하지만 모든 버그를 없애지는 못하잖아!”를 마주했을 때의 분노 말고는 없음 - 누군가가 “감정적 논증은 이성적 논증보다 더 빨리 만들 수 있다”고 주장한다면 어떻게 느낄까?
이 논쟁이 딱 그렇게 느껴짐
해시맵을 Apple이나 String처럼 다루려 할 때 컴파일러가 “안 돼”라고 말해주는 단순한 합리성을 원함
- “오래 지속되는 코드베이스를 변경할 때 타입이 버그를 줄인다”는 말은 크게 논쟁할 여지가 없다고 봄