Skip to content

타입 시스템 입문

서론

"1" + 1이 JavaScript에서는 "11"이 되고, Python에서는 에러가 발생할까요? 이면에서 작동하는 것이 바로 타입 시스템입니다. 타입 시스템은 프로그래밍 언어의 "교통 규칙"입니다 — 데이터가 어떻게 사용될 수 있는지, 어떤 데이터와 연산될 수 있는지, 언제 합법적인지 검사할지를 결정합니다. 타입 시스템을 이해하면 다른 언어들의 "성격 차이"를 이해할 수 있습니다.

이 글에서 무엇을 배우게 되나요?

이 장을 마치면 다음을 얻게 됩니다:

  • 분류 능력: 정적/동적, 강/약 타입의 사분면 분류법 습득
  • 문제 진단: TypeError를 볼 때 타입 불일치인지 암시적 변환인지 빠르게 파악
  • 언어 선택: TypeScript가 대규모 프로젝트에, Python이 빠른 프로토타입에 적합한 이유 이해
  • 타입 추론: 현대 언어가 간결함과 안전성을 어떻게兼顾하는지 이해
  • 실천 인식: 타입 안전한 코딩 습관 습득
내용핵심 개념
제1장타입 시스템이란타입의 본질, 타입이 필요한 이유
제2장정적 타입 vs 동적 타입검사 시점, IDE 지원, 안전성
제3장강타입 vs 약타입암시적 변환, 타입 안전성
제4장타입 추론자동 추론, 두 마리 토끼
제5장제네릭: 한 번 작성, 모든 타입에 적용타입 매개변수, 타입 제약, 재사용
제6장타입 안전 실전일반적인 함정, 방어 전략
제7장언어 타입 사분면사분면 분류, 언어 선택

0. 전경도: 타입은 데이터의 "신분증"

현실 세계에서 책을 커피잔에 끼워 넣지 않습니다 — 서로 다른 "타입"의 것이기 때문입니다. 프로그래밍 세계도 마찬가지입니다: 숫자, 문자열, 불리언, 배열... 각 데이터는 자신의 "신분"을 가지고 있으며, 이것이 어떤 연산에 참여할 수 있는지를 결정합니다.

타입 시스템은 프로그래밍 언어가 이러한 "신분"을 관리하는 규칙 체계입니다. 두 가지 핵심 질문에 답합니다:

타입 시스템의 두 가지 핵심 질문

  • 언제 검사할까? 코드를 작성할 때 검사(정적 타입)할까, 실행할 때 검사(동적 타입)할까?
  • 얼마나 엄격할까? 혼용을 엄격히 금지(강타입)할까, 자동 변환해 줄까(약타입)?

1. 타입 시스템이란: 데이터의 교통 규칙

Type System ExplorerStatic vs dynamic · strong vs weak typing · type inference
StrongWeakStaticDynamic
Strong + static
JavaRustHaskell
Weak + static
CC++
Strong + dynamic
PythonRuby
Weak + dynamic
JavaScriptPHP
Strong + static
Strict compile-time checking with no implicit conversion. Very safe and IDE-friendly, but more verbose.
Compile-time checksNo implicit conversionAutocomplete-friendlySafe refactoring
Core idea:Type systems choose along two dimensions: when checks happen (static/dynamic) and whether implicit conversion is allowed (strong/weak). There is no best combination, only the best fit for a scenario.

타입 시스템의 본질은 제약 규칙으로, 컴파일러나 인터프리터에게 알려줍니다:

  • 이 변수에는 어떤 값을 저장할 수 있는가?
  • 이 두 값은 덧셈을 할 수 있는가?
  • 이 함수의 매개변수는 무엇이어야 하는가?

타입 시스템이 없는 세계는 교통 규칙 없는 도로와 같습니다 — 어떤 데이터든 어떤 데이터와 연산할 수 있고, 결과를 전혀 예측할 수 없습니다.

타입 시스템의 역할설명예시
불법 연산 방지의미 없는 연산 차단문자열에 나눗셈 불가
문서 정보 제공타입이 최고의 문서function add(a: number, b: number)이 한눈에 이해됨
IDE 도구 지원자동 완성, 리팩토링, 이동user. 입력 시 모든 속성 자동 제안
성능 최적화컴파일러가 타입을 알면 더 빠른 코드 생성정수임을 알면 정수 명령어 사용

2. 정적 타입 vs 동적 타입: 언제 검사할까?

이것은 타입 시스템의 가장 중요한 분류 차원 — 검사 시점입니다.

🔍 Static vs Dynamic Typing: Live Comparison

Choose a code sample and compare how the two type systems behave

Static typing (TypeScript)⏱ Checked at compile time
let name: string = "Alice"
name = 42  // ❌ compile error
❌ Type "number" is not assignable to type "string"
VS
Dynamic typing (JavaScript)⏱ Checked at runtime
let name = "Alice"
name = 42  // ✅ OK
✅ Runs normally; name becomes 42
💡 Static typing catches the error while you write code. Dynamic typing waits until runtime.

핵심 차이

  • 정적 타입: 변수의 타입이 컴파일 시점에 결정되며, 코드를 다 작성하고 실행하기 전에 타입 오류를 발견할 수 있습니다. 대표: Java, TypeScript, Rust, Go.
  • 동적 타입: 변수의 타입이 실행 시점에 결정되며, 같은 변수에 먼저 숫자를, 나중에 문자열을 저장할 수 있습니다. 대표: Python, JavaScript, Ruby, PHP.
차원정적 타입동적 타입
검사 시점컴파일 시(실행 전에 검사)실행 시(해당 줄에 도달해야 검사)
버그 발견빠름(작성 후 바로 앎)늦음(사용자 조작 시 노출)
유연성낮음(타입 고정)높음(타입 가변)
IDE 지원좋음(자동 완성, 리팩토링)약함(실행 시에야 타입을 앎)
개발 속도전기 느림(타입 작성 필요)전기 빠름(타입 신경 쓸 필요 없음)
유지보수 비용낮음(타입이 곧 문서)높음(타입 정보 부족)

트렌드: 동적 언어가 "정적화"되고 있음

Python에 Type Hints가 추가되고, JavaScript 커뮤니티가 TypeScript로 전환 — 동적 언어도 정적 타입의 장점을 받아들이고 있습니다. 이는 대규모 프로젝트에서 정적 타입의 안전성 장점이 점점 더 인정받고 있음을 보여줍니다.


3. 강타입 vs 약타입: "몰래 변환"을 허용할까?

두 번째 분류 차원은 타입 변환의 엄격함입니다.

⚡ Strong vs Weak Typing: Implicit Conversion Lab

Choose an expression and see how different languages handle it

JavaScriptWeak
"1" + 1
→ "11" (string concatenation)
PythonStrong
"1" + 1
→ TypeError: can only concatenate str to str
JavaWeak
"1" + 1
→ "11" (string concatenation)
RustStrong
"1" + 1
→ compile error: type mismatch
📌 Strongly typed languages refuse to guess your intent. Weakly typed languages may helpfully convert, but the result may be wrong.

핵심 차이

  • 강타입: 암시적 타입 변환을 허용하지 않으며, 타입이 일치하지 않으면 에러가 발생합니다. 언어에 "문자열을 숫자로 변환하겠다"고 명시적으로 알려야 합니다.
  • 약타입: 암시적 타입 변환을 허용하며, 언어가 "친절하게" 자동 변환해 줍니다. 하지만 이런 "친절함"이 종종 예기치 않은 버그를 낳습니다.
차원강타입약타입
"1" + 1에러 발생 또는 명시적 변환 필요자동 변환(결과가 "11" 또는 2일 수 있음)
안전성높음(조용히 오류 발생하지 않음)낮음(암시적 변환이 버그 유발 가능)
편의성낮음(수동 변환 필요)높음(자동 변환으로 편리)
예측 가능성높음(동작 확정적)낮음(변환 규칙 복잡)

4. 타입 추론: 현대적인 두 마리 토끼 해법

초기의 정적 타입 언어(Java 등)는 모든 변수의 타입을 명시적으로 선언해야 해서 작성이 번거로웠습니다. 현대 언어는 타입 추론으로 이 문제를 해결합니다 — 컴파일러가 타입을 자동으로 추론하므로, 작성하지 않아도 엄격하게 검사해 줍니다.

🧠 Type Inference: How the Compiler Guesses Types

Click a code line to see how the compiler infers the type step by step

1let x = 42 → number
2let names = ["Alice", "Bob"]
3let result = x > 10 ? "big" : "small"
4const add = (a: number, b: number) => a + b
5let mixed = [1, "two", true]
Inference process
1The right side is literal 42
242 is an integer-like number
3Infer x as number
Type Inference Capability by Language
Rust
Almost fully inferred
TypeScript
Most types inferred
Kotlin
Strong local inference
Go
Mainly := short declarations
Java
var keyword (Java 10+)
C
Almost none

타입 추론의 가치

동적 언어처럼 간결하게 작성하면서, 정적 언어처럼 엄격하게 컴파일러가 검사합니다. 이것이 현대 프로그래밍 언어의 주류 방향입니다.

  • TypeScript: let x = 42 → 자동으로 number로 추론
  • Rust: let v = vec![1, 2, 3] → 자동으로 Vec<i32>로 추론
  • Kotlin: val name = "Alice" → 자동으로 String으로 추론
  • Go: x := 42 → 짧은 변수 선언으로 자동 타입 추론

5. 제네릭: 한 번 작성, 모든 타입에 적용

"배열의 첫 번째 요소 가져오기" 함수를 작성했다고 합시다. 숫자 배열용, 문자열 배열용, 객체 배열용... 코드는 완전히 같고 타입만 다릅니다. 제네릭(Generics)은 이 문제를 해결합니다 — 구체적인 타입 대신 "타입 매개변수"를 사용하여, 하나의 코드가 모든 타입에 적용되도록 합니다.

🧩 Generics: Write Once, Use with Any Type

Choose a scenario and see how generics keep code flexible and safe

❌ Without generics
// Need one function per type
function getFirstNumber(arr: number[]): number {
  return arr[0]
}
function getFirstString(arr: string[]): string {
  return arr[0]
}
// boolean, object... it never ends
You repeat the same code for every type.
✅ With generics
// One generic function handles all types
function getFirst<T>(arr: T[]): T {
  return arr[0]
}

getFirst<number>([1, 2, 3])   // → number
getFirst<string>(["a", "b"])  // → string
T is a type parameter and is replaced by the actual type at call time.
Type flow
T = numberarr: number[]return: number

제네릭의 핵심 가치

  • 코드 재사용: 하나의 함수/클래스가 모든 타입에 적용, 반복 작성 불필요
  • 타입 안전: any처럼 타입 검사를 포기하지 않고, 제네릭은 전 과정에서 타입 정보 유지
  • 타입 제약: extends로 제네릭의 범위를 제한하여 유연하면서도 안전
제네릭 특성설명예시
제네릭 함수함수의 매개변수/반환값에 타입 매개변수 사용function first<T>(arr: T[]): T
제네릭 클래스클래스의 속성/메서드에 타입 매개변수 사용class Box<T> { value: T }
제네릭 제약extends로 T의 범위 제한<T extends HasLength>
다중 타입 매개변수여러 타입 변수를 동시에 사용function pair<K, V>(k: K, v: V)

6. 타입 안전 실전: 일반적인 함정과 방어

이론을 배웠으니, 실제 개발에서 가장 흔히 겪는 타입 함정을 살펴보겠습니다. 이러한 함정은 언어를 불문하고 거의 모든 개발자가 만납니다.

🛡️ Type Safety in Practice: Traps and Defenses

Choose a common trap and learn how the type system protects code

⚠️ Dangerous code
function getLength(str) {
  return str.length  // what if str is null?
}
getLength(null)  // 💥 runtime crash
💥 TypeError: Cannot read properties of null
✅ Safe code
function getLength(str: string | null): number {
  if (str === null) return 0
  return str.length  // ✅ compiler knows str is not null here
}
✅ The compiler forces you to handle null
🔑 Defense strategy
  • Enable strictNullChecks
  • Use string | null to mark nullable values explicitly
  • Use optional chaining ?. for safe access

타입 안전의 네 가지 황금 규칙

  1. 엄격 모드 활성화: TypeScript의 strict: true, Python의 mypy --strict
  2. any 피하기: any 대신 unknown을 사용하여 타입 검사 후 사용하도록 강제
  3. null 명시적 처리: 옵셔널 체이닝 ?.과 널 병합 ??으로 안전 접근
  4. API에 인터페이스 정의: 외부 데이터는 절대 신뢰하지 말고, 인터페이스 + 런타임 검증으로 이중 보장
함정위험도방어 수단
null/undefined 참조⭐⭐⭐⭐⭐strictNullChecks + 옵셔널 체이닝
any 타입 남용⭐⭐⭐⭐unknown + 타입 가드 사용
암시적 타입 변환⭐⭐⭐엄격 비교 === + ESLint
배열 타입 불일치⭐⭐⭐배열 요소 타입 명시적 선언

7. 언어 타입 사분면: 프로그래밍 언어에 "초상화" 그리기

"정적/동적"과 "강/약" 두 차원을 조합하면 사분면 분류도가 나옵니다. 모든 프로그래밍 언어를 이 도표에 배치할 수 있습니다.

Programming Language Type ModelsHow type systems differ across languages
When types are checked
Static typing
Java, C++, Rust, Go
Dynamic typing
Python, JavaScript, Ruby
Type strength
Strong typing
Python, Java, Rust
Weak typing
JavaScript, C, PHP
Type System Classification Matrix
Static + strong
Java, C++, Rust, Go
Compile-time checks with type safety
Static + weak
C
Compile-time checks with flexible conversion
Dynamic + strong
Python, Ruby
Runtime checks with type safety
Dynamic + weak
JavaScript, PHP
Runtime checks with flexible typing
Type Inference
Modern languages can infer variable types automatically without explicit declarations.
TypeScript
let x = 5; // inferred as number
let name = "Alice"; // string
Rust
let x = 5; // inferred as i32
let name = "Alice"; // &str
사분면특징대표 언어적용 시나리오
정적 + 강타입가장 안전, 컴파일 시 엄격 검사Rust, Java, Haskell대규모 시스템, 안전 중시
정적 + 약타입컴파일 시 검사하지만 암시적 변환 허용C, C++시스템 프로그래밍, 성능 민감
동적 + 강타입실행 시 검사, 암시적 변환 불허Python, Ruby스크립트, 빠른 프로토타입
동적 + 약타입가장 유연하지만 버그 발생도 쉬움JavaScript, PHP웹 프론트엔드, 소규모 스크립트

"최고의" 타입 시스템은 없다

언어를 선택할 때 타입 시스템은 중요한 고려사항 중 하나입니다:

  • 빠른 프로토타입: 동적 타입(Python) 개발 속도 빠름
  • 대규모 프로젝트: 정적 타입(TypeScript, Java) 유지보수 비용 낮음
  • 시스템 프로그래밍: 강타입 + 정적(Rust) 안전성 최고
  • 팀 협업: 정적 타입이 더 나은 코드 가독성과 IDE 지원 제공

요약

타입 시스템은 프로그래밍 언어의 차이를 이해하는 핵심 관점입니다. 지루한 이론이 아니라, 코드 작성 경험과 코드 품질에 직접적인 영향을 미칩니다.

이 장의 핵심 요점을 돌아보겠습니다:

  1. 타입은 신분증: 각 데이터에는 타입이 있으며, 타입은 데이터가 참여할 수 있는 연산을 결정
  2. 정적 vs 동적: 타입을 언제 검사할까 — 컴파일 시인가 실행 시인가
  3. 강 vs 약: 암시적 타입 변환을 허용할까
  4. 타입 추론: 현대 언어에서 동적의 간결함과 정적의 안전성을 동시에 누리기
  5. 제네릭: 타입 매개변수으로 코드 재사용, 유연성과 타입 안전성 겸비
  6. 타입 안전 실전: null 참조, any 남용, 암시적 변환이 가장 흔한 타입 함정
  7. 사분면 분류: 최고의 타입 시스템은 없고, 상황에 가장 적합한 선택만 있음

추가 읽기