정적 타입 언어(TS)와 동적 타입 언어(JS)는 활용법이 다르다¶
HJ, Ph.D. / Software Architect (js.seth.h@gmail.com) 초안: 2024년 12월 / 개정: 2025년 12월
Abstract¶
- 프로그래밍 언어는 데이터 값의 형식을 다루는 방식에 따라 두 가지로 나눈다.
- 정적 타입: 타입을 사전에 지정하고, 정확하게 따라야 한다.
- 동적 타입: 타입을 사전에 지정하지 않고, 프로그램이 실행되면서 형식이 변한다.
- 일반적으로는 아래와 같이 주장한다. 그러나 현실적으로는 큰 연관성이 확인되지는 않는다.
- 정적 타입 언어(TS)의 장점: 코드의 안정성, 자동 완성, 리팩토링 용이, 버그 사전 방지 등
- 정적 타입 언어(TS)의 단점: 유연성 부족, 복잡한 타입 선언, 러닝 커브 등
- 동적 타입 언어(JS)의 장점: 빠른 프로토타이핑, 유연성, 간결한 코드 등
- 동적 타입 언어(JS)의 단점: 런타임 오류 가능성, 대규모 프로젝트에서의 유지보수 어려움 등
- 실제로 많은 개발자가 타입 시스템을 잘못 이해하고 있다.
- 타입은 좁게는 데이터 값의 물리적/논리적 구조, 즉 해석 방식을 결정하는 체계이다.
- 필연적으로 타입은 정보처리의 일반 형태, 입력-처리-출력에서 입출력 형식을 결정하는 데 쓰인다.
- 이러한 체계는 생각보다 견고하지 않다.
- '취급 주의' 항목이기 때문에 제대로 활용하는 방법은 따로 있다.
- 따라서 각각의 장점을 살리고, 단점을 극복하는 활용법이 다를 수밖에 없다.
- 정적 타입 언어는 형식 검사가 컴파일 타임에 수행되므로 루틴의 실패를 최대한 형식 검사와 동화되도록 다루어야 한다.
- 동적 타입 언어는 형식의 미비로 인한 예외(Exception)를 개발, 검수, 운영 과정에서 탐지하고, 대응하는 표준적 절차를 코드 외부에서 규정해야 한다.
- 하지만, 양자택일의 문제가 아니다.
일반적 이해¶
프로그래밍 언어의 타입 시스템은 정보처리의 근간을 이루는 중요한 개념이다. 타입 시스템은 변수나 함수, 객체 등 데이터의 형식을 정의하여, 프로그램의 내부 구성요소에 대해 사용 가능한 연산을 결정하고, 반대로 연산의 입장에서는 연산이 정상 작동하기 위해 필요한 입력/출력 요건을 결정하는 체계다.
이러한 타입 시스템은 크게 정적 타입(static type)과 동적 타입(dynamic type)으로 구분된다.
정적 타입 언어는 타입을 코드 작성 시점에 명확히 지정하며, 컴파일 타임에 타입 검사가 이뤄진다. 대표적으로 TypeScript, Java, C#, Kotlin, Rust 등이 있다. 반면, 동적 타입 언어는 변수의 타입을 명시적으로 지정하지 않으며, 프로그램이 실행되는 시점에 타입이 결정된다. JavaScript, Python, Ruby, PHP 등이 대표적이다.
흔히 각각의 장단점을 아래와 같이 언급하지만, 엄밀히 말해 이는 '기대 효과'일 뿐이며, 프로그래밍 언어론(Programming Language Theory) 측면에서 인과관계가 성립하는 내용은 아니다.
- 정적 타입 언어는...
- 대규모 협업 프로젝트에서 타입 시스템이 팀원 간의 의사소통을 명확하게 한다.
- 코드 변경 시 발생할 수 있는 오류를 미리 탐지할 수 있게 한다.
- 하지만 타입 선언이 복잡해질 수 있고, 유연성이 떨어지며, 학습 곡선이 높다는 단점도 존재한다.
- 특히, 빠르게 변화하는 요구사항에 대응할 때 타입 시스템이 오히려 발목을 잡는 경우도 있다.
- 반면, 동적 타입 언어는...
- 빠른 프로토타이핑, 높은 유연성, 간결한 코드 작성이 가능하다는 장점이 있다.
- 하지만 런타임 오류가 발생할 가능성이 높고, 대규모 프로젝트에서 유지보수가 어려울 수 있다는 단점이 있다.
- 타입이 명확하지 않기 때문에, 코드가 복잡해질수록 버그가 숨어들기 쉽고, 새로운 팀원이 코드를 이해하는 데 시간이 더 걸릴 수 있다.
현대 컴퓨터 체계의 속사정¶
실제로 많은 개발자가 타입 시스템을 잘못 이해하고 있다.
우선 현대 컴퓨터의 물리적인 요소를 한 번 보자.
반도체를 소프트웨어 공학자의 입장에서 보면, 물리적으로 하드코딩된 기계어 처리 엔진이다.
소프트웨어는 기계어 엔진 위에서 실행되며, 사실상 타입 시스템은 반도체가 다룰 수 있는 데이터의 형태 및 그를 이용한 응용으로 이루어진다. 때문에 현대 컴퓨터 체계, 즉 튜링 머신 이후의 결정론적 시스템 체계에서는 타입 시스템이 정보처리에 있어 아주 근본적인 요소이다.
하드웨어의 실행 능력상에서 이미 연산은 어떤 규격의 데이터(정수 or 부동소수)를 다룰지가 물리 구조로 결정되어 있으며, 이를 이용해서 쌓아올린 1차적 논리 구조(문자열 등 프로그래밍 언어 내장 형식), 2차적 논리 구조(개발자가 정의한 형식), 3차적 논리 구조(솔루션/서비스의 기능으로 제공되는 사용자 커스터마이징 구조)까지, 모든 연산/루틴은 다룰 수 있는 값의 규격이 제한된다.
때문에 동적 타입의 언어라고 해서 입력 형식에 제한이 존재하지 않는 것이 아니다. 예를 들어 'obj.a / obj.b'를 코딩한다면, a와 b는 산술이 가능해야 하며, obj.b가 0이 아니라는 제한이 존재할 수밖에 없다. 동적 타입의 언어는 단지 이것을 명시적으로 선언하여 컴파일 시점에 검증하지 않을 뿐이다. 그래서 용어 자체가 '타입 없는 시스템(No Type System)'이 아니라 '동적 타입 시스템(Dynamic Type System)'이다.
한편, 동적 언어라도 JSON-schema와 같은 규격 검증 루틴을 도입할 수 있고, 반대로 정적 언어라도 데이터를 Key-Value Dictionary로 전송하여 타입 검사를 무효화할 수도 있다. 이러한 사례는 풍부하다.
- 실제로 JAVA에서 가변 데이터를 다루기 위해 Dictionary를 교환하는 코딩 스타일은 실무적으로도 많이 발견된다.
- 비정형 데이터베이스로 알려진 몽고디비가 Schema Validation 기능을 제공한다.
- Python에서 typing.Any 또는 dict를 사용해 타입 검증을 우회하는 API 설계가 흔하다.
- TypeScript에서 as any 또는 Record
로 타입 검사를 무력화하는 패턴이 자주 등장한다. - NoSQL 계열의 DynamoDB, CouchDB 등도 스키마리스 구조이지만, 실제로는 애플리케이션 레벨에서 JSON Schema, Joi 등으로 데이터 유효성 검사를 추가하는 사례가 많다.
- 다수의 API는 타입 시스템과 무관하게 입력값에 대한 방어적 코딩 관점에서 처리를 한다.
이제 알겠지만, 프로그래밍 언어의 타입 시스템은 생각만큼 그리 강력한 강제력을 행사하지 못하고, 코딩 방법에 따라 얼마든지 무력화된다. 때문에 타입 시스템의 분류에 따라 장단점을 예상하는 것은 중간 과정을 관찰조차 하지 않은, 성급한 일반화의 오류이다.
프로그래밍 언어의 특징으로서 타입 시스템은 해당 언어의 '디폴트' 타입 처리 방식을 의미할 뿐, 개발의 결과로서 나온 소프트웨어의 특성을 결정하는 데이터 형식 처리 방침을 의미하지 않는다.
타입 시스템을 존중해라 - 정적 타입¶
당연한 말이겠지만, 프로그래밍 언어가 타입 시스템을 채택할 때는 무엇을 선택하든 그 특성을 활용할 것을 염두에 두고 있다. 이는 단순히 하나의 코드가 실행될 때가 아니라, 그 언어를 통해 만들어지는 생태계 전체에서 받는 혜택을 의미한다.
정적 타입에서는 컴파일 시점에서 검사가 이루어지므로 이 시점에서 형식 미비에 대한 오류가 탐지되는 것이 가장 바람직하다. 그렇다면 형식 미비라는 것이 무엇인가? Int를 요구하는데 String을 넣는 상황을 예상할 수도 있으나, 위와 같은 처리는 애초에 불가능하므로 미비 사항을 탐지하는 것과는 다른 사항이다.
우선 '형식'이라는 것은 결국 '루틴'이 정상 처리되기 위해서 필요로 하는 것이다. 루틴 F가 Type A를 입력으로 취한다고 가정하자. 많은 경우 A의 기본 생성자를 허용할 텐데, 이렇게 되면 A가 실질적으로 계산 가능한 요건을 갖추지 않고도 생성이 가능하다. 따라서 형식 미비를 컴파일 오류로 동화시키는 것은 A의 생성자를 명확하게 제한(예: 생성 가능한 입력을 명시적으로 하드코딩하거나, Java Lombok의 of() 함수 사용 등)하여 연산이 불가능한 Type A가 실제로 인스턴스화되는 것을 막는 것을 말한다.
다른 예시로는 Divide By Zero 오류에 대한 대응을 들 수 있다. 형식적으로는 0이 아닌 수를 요구하는 것인데, 일반적으로 오류를 방지하기 위해 throw new Error('Parameter Invalid')로 처리하는 경우가 많다. 그러나, 이러한 처리 방식은 런타임에서 형식 요건을 확인하는 것으로 사실 정적 타입 시스템의 방식이 아니다. 커스텀 타입 NotZeroFloat/NotZeroInt를 규정하고 사용하는 게 정적 타입의 방식이다. 그러나 이 방법은 이론적으로는 타당하지만, 실용성은 낮다.
반면 정적 타입 시스템의 장점을 버리는 코딩 방법도 존재하는데, 대표적으로 Key-Value Dictionary나 Dynamic Object(C#)를 아키텍처의 중심부에서 다루는 것이다. 예를 들자면, Java로 개발한 웹서버 프레임워크가 모든 웹 요청 데이터를 중첩된 Dictionary
예시로 보듯이, 런타임 오류를 정적 분석 오류로 치환하여 최대한 실행 기반 검증을 줄이는 것이 정적 타입 시스템이 추구하는 방향이다.
그러니, 해야 할 것과 하지 말아야 할 것을 구분하여, 코딩 방법이 루틴의 실패를 최대한 형식 검사와 동화되도록 하고, 그 반대 작용을 하는 것은 적극적으로 피하는 것이, 다소 생산성이 낮은 정적 타입 시스템을 이용하면서 최대한의 이득을 보는 방법이다.
타입 시스템을 존중해라 - 동적 타입¶
그러면 동적 타입 언어에서는 어떤 접근이 타입 시스템을 존중하는 방법이 될까?
동적 타입 언어는 루틴이 주어지는 데이터를 처리할 수 있는지 정적인 분석을 하지 않는다. 그래서 형식에 대한 검사를 하지 않는다고 생각하는 사람이 많은 것 같다. 그러나 반도체 - 기계어 처리 엔진을 쓰는 이상 이진수 해석이 존재하니, 엄연히 타입이 존재한다. 그러니 동적 타입 시스템에서 올바른 형식 준수 여부를 검사하는 방법은 런타임 기반, 즉 QA/QC나 TDD 운영으로 귀결된다.
사실, 정적 타입 언어가 컴파일러가 제공하는 정적 분석을 통해 주된 형식 검사를 한다면,
동적 타입 언어가 런타임 기반의 실행 시 분석을 통해서 주된 형식 검사를 행하는 것은 어찌 보면 당연한 일이다.
다만, 우리가 염두에 두어야 하는 점은 모든 프로그램은 처리 가능한 자료의 형식이 있으며, 소프트웨어 서비스는 정상적인 기능을 제공하는 것에 목적이 있다는 점이다. 개발의 1차 목적은 약속된 서비스 경로의 정상적 제공이며, 비정상 데이터를 식별하는 것은 2순위 문제이다. 때문에 세밀한 방어적 코딩 등은 2순위 목표에 지나치게 많은 개발력을 낭비하는 것은 아닌지 체크가 필요하며, 그러한 관점에서 상세 메시지보다는 느슨하지만 포괄적인 처리가 더 우선시된다. 그렇지 않다면, 동적 타입 시스템의 장점을 전혀 살리지 못하는 결과로 이어진다.
한편, 실용적 측면에서 시스템이 외부와 접하는 면에 대해서만큼은 충분히 강력한 규격 검사(JSON-schema 등)를 도입하는 것이 필요하다. 일단 시스템 내부에 들어온 정제된 데이터에 대해서는 믿고 사용할 수 있으나, 외부 유입 데이터는 데이터 클렌징(Data Cleansing) 과정이 필요한 것이 현실이다.
Garbage in, Garbage out - 쓰레기가 들어가면 쓰레기가 나온다
이와 같이 동적 타입 시스템에서의 데이터의 규격 준수에 대한 문제는 코딩에서 풀기보다는, 개발 방법론과 조직의 운영이나 설계상 데이터 정제 구역의 명확한 지정 등 코딩 이외의 부분에서 대응이 필요하다.
결론: 양자택일의 문제인가?¶
양자택일, 즉 엄격한 정적 타입을 지키거나 코딩 외적인 요소로만 해결 가능한 문제일까?
답은 그렇지 않다. Divide By Zero나 Out Of Index는 정적 타입 시스템이 특정하기 어려우면서도 흔하게 겪는 Type 위반의 런타임 발견 사례이고, 동적 타입 시스템은 데이터에 대한 신뢰를 기반으로 작성되지만, 외부 데이터를 온전히 믿을 수 없는 것이 현실이다. 현실적으로는 정적 타입 시스템이라도 특정한 부분에서는 가변 데이터가 필요할 수도 있고, 동적 타입도 때로는 강력한 제약을 강요할 필요가 있다.
결과적으로 Type에 대한 문제는 무엇이 프로그래밍 언어의 디폴트로 장착되어 있는가일 뿐이다.
그러나, 디폴트를 잘 이용하는, 즉 가능하면 반대 방향을 적게 활용하여 쓸데없는 낭비를 하지 않는 방향이 거시적인 측면에서 매우 유리하다는 점은 분명하다. 따라서, 각 타입 시스템 별로 어떻게 하는 것이 기본 방침을 따르는 것이고 그렇지 않은 것인지를 구분하여, 잘 활용할 필요가 있다.
See Also¶
- 제발 catch 좀 줄여라 - 느슨하지만 포괄적인 오류 처리
- 소프트웨어 아키텍처는 확장보다 불변에 의해 결정된다 - 당연하지만 Type System은 불변사항이다.
- TDD - 단위 테스트는 가치를 주지 못했다 - 경험 기반의 생산적인 TDD 운영
- 불신의 코딩 vs 신뢰의 코딩 - 타입 시스템이 다른 언어를 보는 또 다른 관점
Author¶
HJ, Ph.D. / Software Architect
(js.seth.h@gmail.com)
https://js-seth-h.github.io/website/Biography/
Over 20 years in software development, focusing on design reasoning and system structure - as foundations for long-term productivity and structural clarity.
Researched semantic web and meta-browser architecture in graduate studies,
with an emphasis on structural separation between data and presentation.
Ph.D. in Software, Korea University
M.S. in Computer Science Education, Korea University
B.S. in Computer Science Education, Korea University
저작자표시-비영리-변경금지(CC BY-NC-ND 4.0)