타입 검사는 해결책이 아니라 증상이다

ko생성일: 2025. 9. 10.갱신일: 2025. 9. 10.

타입 시스템에 대한 집착은 근본 문제를 가리는 미봉책일 수 있다. 진짜 과제는 격리, 명시적 인터페이스, 시간 중심의 설계 같은 아키텍처 원칙을 통해 복잡성을 애초에 만들지 않는 것이다.

프로그래밍 업계가 수십 년간 집착해 온 타입 검사가 아예 엉뚱한 문제를 해결하고 있다면 어떨까? 하스켈의 범주론부터 러스트의 borrow checker(빌림 검사기)에 이르는 갈수록 정교해진 타입 시스템들이, 우리가 처음부터 저질러 온 근본적 아키텍처 실수를 우회하기 위한 복잡한 꼼수일 뿐이라면?

소프트웨어 업계는 타입 검사가 유용할 뿐 아니라 필수적이라고 스스로를 설득해 왔다. 우리는 컴파일 시 타입 오류를 잡는 일을 소프트웨어 설계의 최우선 과제로 삼아, 그 전제 위에 온갖 프로그래밍 언어를 세웠다. 그리고 매번 더 강력한 타입 시스템을 만들기 위해 수없이 많은 인년(人年)을 쏟아부으며, 그 모든 발전을 의심할 여지 없는 진보로 취급했다.

하지만 한 걸음 물러나 아주 단순한 질문을 던져 보자. 애초에 왜 타입 검사가 필요한가?

표준 답은 “규모”다. “작은 프로그램에는 타입이 필요 없지만,” 이렇게 논리가 전개된다. “큰 프로그램은 타입이 없으면 유지보수가 불가능해진다.” 언뜻 그럴싸하지만, 사실 우리는 이렇게 고백하고 있는 셈이다. 우리의 시스템은 인간의 추론으로는 본질적으로 이해할 수 없도록 설계되어 있다고. 보이지 않는 연결과 암묵적 동작에 의존하는, 너무나 뒤얽힌 아키텍처를 만들어 놓고, 프로그램이 너무도 뻔한 방식으로 충돌하지 않는지 확인하려면 자동화 도구가 필요하다고 말이다.

다시 말해, 타입 검사는 복잡성의 해결책이 아니라, 애초에 불필요한 복잡성을 우리가 만들어 냈다는 자백이다.

생각해 보자. 전자공학자들은 수백만 개의 부품, 정교한 타이밍 관계, 서브시스템 간의 복잡한 상호작용을 가진 시스템을 일상적으로 설계한다. 그런데 그들은 우리 업계의 타입 체커와 유사한 것에 의존하지 않는다. 대신 격리, 명시적 인터페이스, 시간 인지적 설계 같은 다른 아키텍처 원칙을 사용해 시스템이 자연스럽게 더 견고하고 이해하기 쉽게 만든다.

문제는 소프트웨어가 하드웨어보다 본질적으로 더 복잡해서가 아니다. 우리가 인위적 복잡성을 만들어 내는 추상화와 아키텍처 패턴을 선택했고, 그다음에는 그 난장판을 다루기 위한 복잡한 도구를 쌓아 올렸다는 데 있다.

통설(受容된 지혜)

대규모 시스템에 관한 어떤 소프트웨어 엔지니어링 토론에 들어가도 같은 후렴구가 들린다. “타입은 버그가 프로덕션에 가기 전에 잡아준다.” “정적 타이핑이 리팩토링을 안전하게 해 준다.” “강한 타입 시스템 없이는 백만 라인 코드베이스를 유지할 수 없다.”

이 말들이 전혀 틀린 건 아니다. 우리가 현재 소프트웨어를 만드는 방식—스프롤링(sprwaling)한 상속 계층, 깊게 중첩된 함수 호출, 수십 개 모듈을 꿰뚫고 지나가는 보이지 않는 의존성—에서는, 타입 검사가 실제로 그렇지 않았다면 고통스러웠을 버그들을 잡아 준다. 러스트 컴파일러의 borrow checker는 C 프로그램을 괴롭히는 메모리 안전 문제를 막아 준다. 타입스크립트는 자바스크립트 코드베이스를 더 유지보수 가능하게 만든다.

하지만 여기서 무슨 일이 벌어지는지 주목하자. 우리는 인간의 추론이 실패할 정도로 복잡하고 상호 연결된 시스템을 만들고, 그 복잡성을 헤쳐 나가게 도와주는 도구를 필수라고 선언한다. 이것은 미로를 너무나 복잡하게 만들어서 걸어 나가려면 GPS가 필요해지고, 결국 GPS가 건축의 본질적 요소라고 결론 내리는 것과 같다.

소프트웨어 업계는 타입 검사를 유용한 기법에서 의심할 여지 없는 필수 요소로 격상시켰다. 우리는 동적 타이핑이 좋은지 정적 타이핑이 좋은지를 논쟁하면서, 타이핑이 그렇게까지 필수처럼 느껴지게 만드는 조건들이 정말로 불가피한지에 대해서는 거의 묻지 않는다. 프로그램은 인간의 이해 범위를 넘어서고, 의존성은 추적 불가능할 정도로 불어나며, 컴포넌트 간의 관계는 직접 추론하기엔 너무 복잡해질 것이라고 우리는 당연시한다.

이 당연시는 너무 깊게 뿌리 내려서, 그것을 의심하는 일조차 거의 불경처럼 느껴진다. 타입 검사가 필요 없을지도 모른다고 시사하면, “도면 없이 비행기를 어떻게 만들겠냐”는 식의 반응을 듣게 될 것이다. 이 비유가 문제를 드러낸다. 우리는 현재의 방법을 유일한 방법과 혼동하고 있다.

숨은 가정

현대 타입 시스템의 전체 구조는 하나의, 거의 검토되지 않은 가정 위에 세워져 있다. 소프트웨어 시스템은 필연적으로 인간의 추론 능력을 넘어설 정도로 커질 것이라는 가정이다. 이 가정은 너무나 근본적이어서, 우리는 더 이상 그걸 가정으로조차 인식하지 못한다.

“7의 법칙”—인간은 한 번에 대략 7개의 변수나 의존성만 효과적으로 다룰 수 있다는 인지과학적 발견—을 생각해 보자. 시스템이 이 임계치를 넘어서면, 우리는 실수를 하고, 엣지 케이스를 놓치고, 코드베이스 전반에 걸쳐 변경의 파급을 추적하지 못하기 시작한다. 타입 검사는, 논리대로라면, 시스템이 이 인지적 경계를 넘는 순간부터 필수적이 된다.

하지만 여기 숨어 있는 가정이 있다. 이 경계를 넘는 것이 불가피하다는 것. 대규모 소프트웨어 시스템은 그 본성상 인지적으로 압도적이 될 수밖에 없다는 것. 유일한 해법은 우리가 만들어 낸 복잡성을 헤엄쳐 나갈 만큼 정교한 도구를 만드는 것뿐이라는 가정 말이다.

만약 이 가정이 틀렸다면? 타입 검사의 필요가 규모의 자연스러운 결과가 아니라, 나쁜 아키텍처 선택의 증상이라면? 우리가 불필요하게 복잡한 시스템을 만들어 놓고, 그 복잡성을 소프트웨어의 본질적 속성으로 오해하고 있다면?

이 대안적 관점을 뒷받침하는 증거는 눈앞에 있다. 유닉스 파이프라인은 수십 개의 프로그램을 조합해 복잡한 워크플로를 만들어도, 전송 계층에서 타입 검사를 하지 않는다. 각 개별 프로그램은 서로 주고받는 데이터가 단순하고 합의된 형식—대개는 개행으로 구분된 텍스트 라인—일 것이라고 신뢰한다. 이것이 가능한 이유는 각 컴포넌트가 엄격한 격리를 유지하기 때문이다. 컴포넌트 내부에서 일어나는 일은 내부에만 머물고, 통신은 단순하고 명시적인 인터페이스로만 이루어진다.

마찬가지로, 인터넷 자체도 중앙집중식 타입 검사 없이 동작한다. HTTP 서버와 클라이언트, 이메일 시스템, DNS 리졸버—이 모두는 단순한 프로토콜과, 각 컴포넌트가 내부 복잡성을 책임감 있게 다룰 것이라는 가정에 기반해 상호 운용된다. 웹이 매일 수십억 건의 상호작용으로 확장되는 이유는 정교한 타입 시스템 때문이 아니라, 컴포넌트 간 결합을 느슨하게 하고 인터페이스를 단순하게 유지하는 아키텍처 원칙 덕분이다.

이 예들은 복잡성이 다루기 어려워지는 이유가 규모 그 자체가 아니라, 시스템 구조 방식 때문임을 시사한다. 컴포넌트가 진정으로 격리되어 있고, 단순하고 명시적인 채널로만 통신할 때, 큰 시스템이라도 인간의 추론으로 이해 가능하게 유지될 수 있다.

함수의 함정

우리가 불필요하게 복잡한 시스템을 만들어 온 이유를 이해하려면, 현대 프로그래밍의 근본 추상화—함수 호출—를 들여다봐야 한다. 함수는 너무나 자연스럽고 자명하게 올바른 것처럼 보이기에, 그것을 의심하는 일은 수학 자체를 의심하는 것처럼 느껴진다. 하지만 함수는 우리가 점점 더 많이 만드는 분산적이고 시간 기반의 시스템에는 부적합한 숨은 짐을 짊어지고 있다.

문제는 이렇다. 모든 함수 호출은 본질적으로 서로 다른 두 개념—데이터 흐름과 제어 흐름—을 혼합한다. 함수를 호출한다는 것은 단지 데이터를 다른 컴포넌트로 전달한다는 뜻이 아니라, 제어권을 넘긴다는 뜻이기도 하다. 호출자는 피호출자가 완료되어 값을 반환할 때까지 자신의 실행을 중단하고 기다려야 한다. 이 블로킹 동작은 컴포넌트 간에 긴밀한 결합을 만든다. 작은 프로그램에서는 무해해 보이지만, 규모가 커질수록 치명적이 된다.

함수 중심의 사고로 분산 시스템을 만들면 어떤 일이 벌어질까? 결국 원격 프로시저 호출(RPC)에 도달한다. 네트워크 요청이 함수 호출인 척하는 것이다. 호출자는 여전히 블록되지만, 이제는 네트워크 지연, 잠재적 실패, 원격 시스템의 예측 불가능한 타이밍에 의해 블록된다. 이를 성사시키려면 타임아웃, 재시도, 서킷 브레이커, 분산 트랜잭션 같은 정교한 장치가 필요하다. 본래 단일 스레드, 인메모리 계산을 위해 설계된 패러다임을 지리적 거리와 신뢰할 수 없는 네트워크를 가로질러 억지로 작동시키는 셈이다.

타입 검사와 관련된 복잡성은 바로 이 아키텍처의 불일치에서 흘러나온다. 함수가 모듈 경계를 넘나들며 다른 함수를 호출할 때, 호출 체인을 따라 흐르는 데이터의 일관성을 보장하려면 정교한 타입 시스템이 필요해진다. 함수가 반환한 값을 다른 함수에 전달할 때, 데이터가 호출 그래프를 통해 어떻게 변형되는지 추적하려면 제네릭과 고계 타입이 필요하다. 함수가 예외를 던져 호출 스택을 타고 전파될 수 있다면, 예외 명세나 Result 타입으로 오류 흐름을 관리해야 한다.

이 모든 복잡성은 분산적이고 비동기적인 시스템을, 마치 단일 스레드의 동기식 프로그램처럼 프로그래밍할 수 있다는 허상을 유지하려는 데서 비롯된다. 우리가 점점 더 정교한 타입 시스템을 만든 이유는 우리가 해결하는 문제가 본질적으로 복잡해서가 아니라, 현대 컴퓨팅 도전에 부적합한 추상화를 쓰고 있기 때문이다.

C 같은 함수 중심 언어와 함수형 언어는, 격리된 컴포넌트의 내부를 개발하는 데에는 뛰어나다. 하지만 컴포넌트 간 조율, 특히 분산되어 있거나 장시간 실행되거나 이벤트 주도적인 컴포넌트를 조율하려 할 때는 무너진다. 우리는 본질적으로 조정과 통신의 문제인 시스템을, 계산을 위해 고안된 패러다임으로 만들려 하고 있다.

다른 분야에서의 교훈

함수 중심 프로그래밍이 현대 시스템에 부적합하다면, 올바른 패러다임은 무엇일까? 그 답은, 대부분의 소프트웨어보다 훨씬 더 복잡한 시스템을 일상적으로 구축하는 도메인에 눈에 띄게 자리하고 있다.

전자공학자들은 수십억 개의 트랜지스터, 정교한 타이밍 관계, 서브시스템 간 복잡한 상호작용을 가진 프로세서를 설계한다. 그들은 서로 다른 클록 도메인에 걸쳐 신호를 조율하고, 전력 분배 네트워크를 관리하며, 전자기 간섭이 데이터를 오염시키지 않도록 보장한다. 그런데도 우리 업계의 정교한 타입 시스템에 해당하는 무언가에 의존하지 않는다. 그들은 복잡성을 통제 가능하게 만드는 아키텍처 원칙—컴포넌트 간 엄격한 격리, 명시적 타이밍 제약, 단순하고 잘 정의된 인터페이스—을 사용한다.

전자공학에서 ‘시간’은 일급 개념이다. 신호는 지속 시간이 있고, 컴포넌트에는 셋업/홀드 타임이 있으며, 사건의 시퀀싱은 바람이 아니라 명시적으로 설계된다. 이는 우연이 아니다. 분산 시스템(회로 기판은 분산 시스템이다)에는 순차적 계산과는 다른 사고가 필요하다는 인식의 결과다.

데이비드 하렐(David Harel)은 1980년대 Statecharts(상태차트)를 개발하면서 이 점을 인식했다. 상태차트는 전자공학의 관점을 소프트웨어에 가져와, 시간을 사후적 고려가 아닌 근본적인 제어 흐름 개념으로 다뤘다. 하렐의 핵심 통찰은 상태 폭발 문제—가능한 시스템 상태 수가 지수적으로 증가해 복잡 시스템을 감당할 수 없게 만드는 현상—는 더 세련된 분석 도구가 아니라, 강력한 격리와 계층적 레이어링으로 해결할 수 있다는 것이었다.

상태차트는 우리가 필요하다고 스스로 설득해 온 정교한 타입 장치 없이도, 소프트웨어가 복잡하고 병행적인 시간 기반 동작을 다룰 수 있음을 보여 준다. 비결은 아키텍처다. 컴포넌트는 진정으로 격리되고(상태차트 내부에서 일어나는 일은 내부에만 머문다), 통신은 오직 명시적 이벤트를 통해서만 이루어지며, 전체 시스템 동작은 단순하고 이해 가능한 부분들의 합성에서 드러난다.

UNIX 파이프라인도 이런 대안적 접근을 엿보게 한다. grep | sort | uniq 같은 명령을 조합할 때, 전송 계층에서 타입 검사를 하지 않는다. 대신 아키텍처 원칙에 의존한다. 각 명령은 완전히 격리되어 있고, 통신은 단순한 데이터 스트림(텍스트 라인)을 통해서만 이루어지며, 복잡한 동작은 단순 컴포넌트의 합성에서 나온다. 전송 계층은 바이트 또는 문자처럼 극도로 단순한 데이터만 사용하므로 범용적 플러그인 가능성을 제공한다. 개별 컴포넌트는 이 단순한 기판 위에 데이터 의미에 관한 더 정교한 합의를 얹을 수 있다.

두 예시는 중요한 통찰을 공유한다. 아키텍처가 올바르면, 복잡성은 정교한 분석 도구 없이도 관리 가능하게 남는다. 정교한 타입 검사의 필요는 엔지니어링 성숙의 징표가 아니라, 아키텍처 미성숙의 증상이다.

진짜 문제

소프트웨어 업계의 타입 검사 집착은 지적 자원의 심각한 오배분을 보여 준다. 우리가 점점 더 정교한 타입 시스템을 완성하는 데 수십 년을 보낸 사이, 현대 컴퓨팅의 근본 과제는 거의 손대지 않은 채로 남아 있었다.

우리가 집단적 두뇌를 어디에 쓰고 있는지 생각해 보자. 러스트의 borrow checker가 하스켈의 타입 클래스보다 나은지, 타입스크립트의 구조적 타이핑이 자바의 명목 타이핑보다 우월한지 논쟁한다. 함수가 안전하게 합성되는지, 자료구조가 불변식을 유지하는지, 널 포인터 예외를 컴파일 타임에 잡을 수 있는지에 엄청난 노력을 기울인다.

그 사이에, 실제로 현대 컴퓨팅에서 중요한 문제들은 여전히 크게 미해결이다. 진정한 분산 시스템을 어떻게 구축해 네트워크 분할을 우아하게 처리할 것인가? 지리적 거리를 가로질러 일관성을 희생하지 않고 소프트웨어를 어떻게 확장할 것인가? 전체 재작성 없이 시간에 따라 진화하고 적응할 수 있는 시스템을 어떻게 설계할 것인가?

이것이 21세기 컴퓨팅을 규정하는 도전이지만, 우리는 여전히 20세기 문제를 위한 추상화로 접근하고 있다. 컴포넌트가 수천 마일 떨어져 있는데도 여전히 공유 메모리 관점에서 사고하고, 처리 능력이 사실상 공짜에 가까운데도 여전히 비싸고 희소한 CPU를 상정해 설계하며, 실제로 중요한 것은 실패에 대한 회복탄력성인데도 여전히 완벽한 신뢰성을 최적화하려 한다.

비극은, 타입 시스템을 다듬는 데 쓰인 매 시간이 동시성, 창발, 내결함성을 이해하는 데 쓰이지 못한 시간이라는 것이다. 함수 호출을 더 안전하게 만드는 데 몰두한 뛰어난 두뇌는, 함수 호출이 야기하는 문제를 애초에 회피하는 시스템을 만드는 데 몰두하지 못한 두뇌다.

우리는 우리의 가장 정교한 도구들이 아키텍처 선택의 ‘결과’를 다루는 데 치중하면서, 정작 그 선택 자체를 의심하지 않는 기묘한 상황을 만들었다. 자동차의 발명을 외면한 채 말채찍의 공학을 완벽하게 만드는 것과 같다. 우리는 스스로 만든 복잡성을 관리하는 데 놀라울 정도로 능숙해졌지만, 애초에 그 복잡성을 만들 필요가 있었는지 묻는 일은 하지 않는다.

이 오배분은 단지 노력의 낭비를 넘어, 적극적으로 해롭다. 타입 검사를 필수로 취급함으로써, 우리는 타입 검사를 필요로 하는 아키텍처 패턴에 스스로를 가두었다. 그 결과, 대안들을 탐색하기가 더 어려워졌다. 현재의 패러다임 안에서는 그 대안들이 “안전하지 않다”거나 “확장 불가능하다”고 보이기 때문이다.

더 나은 아키텍처를 향해

앞으로의 길은 타입 검사를 전면 폐기하는 것이 아니다. 함수 중심 프로그래밍의 맥락에서 타입은 여전히 유용하다. 대신, 정교한 타입 시스템이 ‘필요’처럼 느껴지는 현상이 잘못된 아키텍처 토대의 신호임을 인식해야 한다.

진정한 격리는 설득력 있는 대안을 제시한다. 컴포넌트를 진짜 블랙박스로 설계하면—내부에서 일어나는 일은 내부에만 머물고, 통신은 오직 명시적인 입출력 포트로만—시스템은 자연스럽게 추론하기 쉬워진다. 인터페이스가 단순하고 동작이 내부에 갇혀 있다면, 그 컴포넌트가 무엇을 하는지 이해하는 데 정교한 타입 장치가 필요 없다.

이 접근이 추론 방식을 어떻게 바꾸는지 생각해 보자. 데이터가 시스템을 통해 어떻게 흐르는지 이해하려고 복잡한 호출 그래프와 타입 계층을 추적하는 대신, 각 컴포넌트를 격리된 채로 추론할 수 있다. 한 모듈의 변경이 수십 개 의존성으로 어떻게 파급되는지 걱정하는 대신, 컴포넌트 간의 명시적 계약에 집중할 수 있다. 상호작용 버그를 타입 체커가 잡아 주기를 기대하는 대신, 문제적 상호작용이 구조적으로 불가능한 시스템을 설계할 수 있다.

이것은 이론에 그치지 않는다. 현대의 발전들은 이미 이 방향을 가리킨다. 도커 같은 컨테이너 기술은 격리의 원칙을 체현한다—각 컨테이너는 완전하고 자족적인 환경이며, 다른 컨테이너와는 명시적 인터페이스로만 통신한다. 마이크로서비스 아키텍처는 현재 구현상의 도전에도 불구하고, 대규모 시스템을 작고 독립적으로 배포 가능한 컴포넌트로 쪼개려는 시도다. 이벤트 주도 아키텍처는 비동기 통신이 동기 함수 호출보다 더 자연스러운 경우가 많다는 사실을 인정한다.

부족한 한 조각은, 처음부터 이런 격리와 메시지 패싱 세계를 위해 설계된 프로그래밍 언어와 개발 환경이다. 시간(time)이 일급 개념이고, 컴포넌트가 자연스럽게 격리되며, 단순한 데이터 형식이 범용 합성을 가능하게 하는 언어. 유닉스 명령을 파이프로 엮듯이, 분산 컴포넌트를 손쉽게 배선할 수 있게 해 주는 도구.

그런 시스템이 등장하면, 타입 검사에 관한 논쟁은 한결 가벼워 보일 것이다. 타입이 나빠서가 아니라, 정교한 타입 시스템이 ‘필요’처럼 느껴지게 만들던 아키텍처 패턴이 더 이상 지배적이지 않을 것이기 때문이다. 우리는 지금의 타입 안전 집착을, 마치 과거의 goto 제거처럼 돌아볼 것이다—실제 문제를 해결하긴 했지만, 더 나은 추상화가 등장하면서 상당 부분 무의미해진 것으로.

목표는 안전을 포기해 타입 검사를 불필요하게 만드는 것이 아니라, 분산적이고 동시적이며 시간에 민감한 컴퓨팅의 현실과 더 잘 맞는, 본질적으로 더 이해 가능하고 더 견고한 시스템을 구축함으로써 타입 검사를 ‘굳이 필요 없게’ 만드는 것이다. 프로그래밍의 미래는 복잡한 시스템을 더 잘 분석하는 데 있지 않다. 더 단순한 시스템을 더 잘 만드는 데 있다.

참고 링크

Email: ptcomputingsimplicity@gmail.com

Substack: paultarvydas.substack.com

Videos: https://www.youtube.com/@programmingsimplicity2980

Discord: https://discord.gg/65YZUh6Jpq

Leanpub: [WIP] https://leanpub.com/u/paul-tarvydas

Twitter: @paul_tarvydas

Bluesky: @paultarvydas.bsky.social

Mastodon:@paultarvydas

(earlier) Blog:guitarvydas.github.io

References:https://guitarvydas.github.io/2024/01/06/References.html

댓글 남기기

공유하기