Featured image of post [Computer Terms] 타입 시스템: 정적/동적, 강/약 타입

[Computer Terms] 타입 시스템: 정적/동적, 강/약 타입

타입 시스템은 정적/동적(검사 시점)과 강/약(암묵적 변환 허용 여부)이라는 서로 독립된 두 축으로 분류됩니다. TypeScript·Python·JavaScript·C 네 언어의 실제 코드로 네 조합의 차이와 언제 어떤 타입 시스템을 택할지 판단 기준까지 비교합니다.

이 장을 읽기 전에

컴파일러와 인터프리터에서 다룬 “오류를 언제 발견하는가”(컴파일 시점 vs 실행 시점) 논의를 이어받는다. 타입 시스템은 그 논의를 “이 값의 타입이 맞는지 언제, 얼마나 엄격하게 검사하는가"로 구체화한다.

두 개의 독립된 축: 검사 시점과 엄격함

타입 시스템을 이야기할 때 정적/동적강/약을 흔히 혼동하지만, 이 둘은 서로 다른 질문에 답하는 독립된 축이다. **정적(Static) vs 동적(Dynamic)**은 “타입 오류를 언제 검사하는가"를 묻는다 — 정적 타입은 컴파일러와 인터프리터에서 다룬 컴파일(또는 변환) 시점에, 동적 타입은 실제로 그 코드가 실행되는 순간에 검사한다. **강(Strong) vs 약(Weak)**은 “서로 다른 타입 사이의 암묵적 변환을 얼마나 허용하는가"를 묻는다.

정적 타입: 실행 전에 타입 오류를 잡는다

1
2
3
4
5
6
7
// TypeScript: 정적 타입 — 컴파일(변환) 시점에 오류를 잡음
function add(a: number, b: number): number {
    return a + b;
}

add(1, "2");   // 컴파일 오류: Argument of type 'string' is not assignable to parameter of type 'number'
                // 이 코드는 실행조차 되지 않는다

동적 타입: 실행하는 순간에야 알 수 있다

1
2
3
4
5
6
# Python: 동적 타입 — 실행 시점에야 타입이 결정되고 오류도 그때 발생
def add(a, b):
    return a + b

add(1, "2")   # 실행 시점에 TypeError: unsupported operand type(s) for +: 'int' and 'str'
              # 이 줄이 실제로 실행되기 전까지는 오류가 있는지 알 수 없다

같은 종류의 실수인데도, TypeScript는 코드를 실행하기 전에 오류를 알려주지만 Python은 그 함수가 실제로 문제의 인자로 호출될 때까지 오류를 발견하지 못한다 — 그 함수를 호출하는 코드 경로가 테스트에서 다뤄지지 않았다면, 이 버그는 운영 환경에서야 발견될 수도 있다.

강 타입 vs 약 타입: 암묵적 변환의 허용 정도

Python은 동적 타입이면서 동시에 비교적 강 타입이다 — "2" + 1처럼 문자열과 정수를 암묵적으로 섞으면 오류를 낸다. 반면 JavaScript는 동적 타입이면서 약 타입에 가깝다 — 서로 다른 타입끼리도 암묵적으로 변환해 연산을 시도한다.

1
2
3
4
// JavaScript: 동적 + 약 타입 — 서로 다른 타입도 암묵적으로 변환해 연산
console.log("2" + 1);     // "21"  (숫자 1이 문자열로 변환되어 이어붙임)
console.log("2" - 1);     // 1     (이번엔 문자열 "2"가 숫자로 변환됨)
console.log([] + {});     // "[object Object]"  (둘 다 문자열로 변환)
1
2
# Python: 동적이지만 강 타입 — 암묵적 변환을 허용하지 않고 명시적으로 오류를 냄
print("2" + 1)   # TypeError: can only concatenate str (not "int") to str

"2" + 1이 언어에 따라 "21"이 되거나, 오류가 나거나, 아예 컴파일이 안 되는 이 차이가 바로 강/약 타입 축이 정적/동적 축과 독립적이라는 것을 보여준다.

네 조합으로 보는 언어 분류

정적 타입동적 타입
강 타입Java, Rust, TypeScript(컴파일 후)Python, Ruby
약 타입C(포인터·정수 간 암묵적 변환 허용)JavaScript, PHP

이 네 칸 중 한 언어가 정확히 한 칸에만 속한다고 단정하기는 어렵다 — 예를 들어 C는 정적 타입이지만 intchar 사이의 암묵적 변환을 폭넓게 허용해 약 타입에 가깝게 분류되며, TypeScript는 컴파일 단계에서는 강한 정적 검사를 하지만 결국 약 타입인 JavaScript로 변환돼 실행된다는 점에서 완전히 깔끔한 분류는 아니다.

언제 정적 타입을, 언제 동적 타입을 선택할 것인가

판단 기준은 프로젝트 규모와 협업 방식에 있다. 팀 규모가 크고 여러 사람이 같은 코드베이스를 오래 유지보수한다면, 정적 타입이 함수 시그니처만 보고도 어떤 값이 들어와야 하는지 알 수 있게 해줘 협업 비용을 줄여준다 — 리팩토링 시에도 타입이 맞지 않는 호출부를 컴파일러가 즉시 찾아준다. 반대로 빠른 프로토타이핑이나 스크립트처럼 코드 수명이 짧고 한두 명이 다루는 경우, 동적 타입의 유연성(타입 선언 없이 바로 값을 다룰 수 있음)이 개발 속도에서 유리하다. 강/약 타입 축도 마찬가지다 — JavaScript처럼 약 타입 언어는 암묵적 변환이 편의를 주지만 "2" + 1 같은 예상치 못한 동작의 원인이 되므로, 실무에서는 TypeScript처럼 강 타입에 가까운 정적 검사를 얹어 이 위험을 줄이는 방향으로 수렴하는 경우가 많다.

흔한 오개념

“정적 타입이 항상 더 안전하다” — 정적 타입은 타입 불일치라는 한 종류의 오류를 실행 전에 잡아줄 뿐이다. 로직 오류(잘못된 계산식, 잘못된 조건문)는 타입이 아무리 정적이어도 컴파일러가 잡아주지 못한다. 동적 타입 언어도 소프트웨어 설계 갈래에서 다룬 테스트로 충분히 안전하게 개발할 수 있다 — 정적 타입은 안전성을 얻는 한 가지 방법이지 유일한 방법이 아니다.

“동적 타입 언어는 타입이 아예 없다” — Python의 모든 값은 런타임에 명확한 타입(int, str 등)을 갖는다. “동적"은 “타입이 없다"가 아니라 “타입 검사가 실행 시점에 이뤄진다"는 뜻이다. type(x)로 언제든 실제 타입을 확인할 수 있다는 것이 이를 보여준다.

다른 개념과의 연결

정적 타입 검사가 컴파일러와 인터프리터에서 다룬 컴파일 단계의 일부로 이뤄진다는 점에서 두 챕터는 직접 이어진다. 다음 챕터에서는 프로그래밍 언어론 갈래를 이어, 메모리 관리를 언어 런타임이 자동화하는 가비지 컬렉션을 다룬다.

평가 기준

이 챕터를 읽은 후에는 다음을 할 수 있어야 한다. 정적/동적(검사 시점)과 강/약(암묵적 변환)이 서로 독립된 축임을 설명할 수 있다. 같은 코드가 언어에 따라 오류를 컴파일 시점에 내는지, 실행 시점에 내는지, 아니면 암묵적으로 변환해 실행되는지 구분할 수 있다. “정적 타입이 항상 더 안전하다"는 단순화가 왜 부정확한지 설명할 수 있다.

참고 자료

Pierce, B. C. (2002). Types and Programming Languages, Chapter 1: Introduction. MIT Press.