
정적 타입 vs 동적 타입
변수의 타입을 언제 결정하느냐. 깐깐한 공무원(Static) vs 융통성 있는 스타트업(Dynamic).

변수의 타입을 언제 결정하느냐. 깐깐한 공무원(Static) vs 융통성 있는 스타트업(Dynamic).
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

스타트업 초기, Python으로 MVP를 만들었다. 빠르게 프로토타입을 만들고 고객 피드백을 받는 게 최우선이었으니까. 개발 속도가 중요했고, Python의 동적 타입 시스템 덕분에 코드를 쭉쭉 작성할 수 있었다.
def calculate_discount(price, discount):
return price - discount
# 잘 작동함
result = calculate_discount(100, 10) # 90
문제는 서비스가 커지면서 시작됐다. 어느 날 새벽 3시에 전화가 왔다. "결제 페이지가 안 열려요." 로그를 확인하니 이런 에러가 찍혀있었다.
# 실제 운영에서 발생한 에러
TypeError: unsupported operand type(s) for -: 'int' and 'str'
누군가가 calculate_discount(100, "10%")처럼 문자열을 넘겼고, 실행하는 순간 서버가 뻗어버렸다. 개발할 때는 멀쩡했는데 고객이 사용하는 순간 터진 것이다. 코드 리뷰를 해도, 테스트를 작성해도, 이런 타입 관련 버그는 런타임에 가서야 발견됐다.
이 문제를 반복적으로 겪으면서 의문이 생겼다. Java나 C를 쓰는 팀들은 이런 류의 버그를 얼마나 겪을까? 그들의 코드를 보니 변수마다 타입이 선언돼 있었다.
int price = 100;
String name = "Product";
price = "Product"; // 컴파일 단계에서 에러 발생
컴파일러가 코드를 실행하기 전에 타입을 체크해서, 잘못된 타입을 넣으면 아예 실행조차 안 시켰다. 마치 공항 보안검색대처럼, 위험한 물건을 미리 걸러내는 방식이었다. 반면 Python과 JavaScript는 보안검색 없이 일단 비행기에 태우고, 비행 중에 문제가 생기면 그때 처리하는 식이었다.
그렇다고 Java로 전환하기엔 이미 Python과 JavaScript로 쌓아놓은 코드베이스가 너무 컸다. 게다가 정적 타입 언어는 코드가 길어지고 개발 속도가 느려진다는 평판도 있었다. 하지만 더 이상 새벽 3시 장애 전화를 받고 싶지 않았다.
타입 시스템을 공부하면서 핵심을 이해했다. 정적 타입과 동적 타입의 차이는 타입을 언제 검사하느냐였다.
정적 타입(Static Typing)은 코드를 작성하고 실행하기 전, 컴파일 단계에서 모든 변수의 타입을 확정한다. 변수를 선언할 때 타입을 명시하고, 그 타입과 맞지 않는 값을 할당하려고 하면 컴파일러가 에러를 던진다. 실행 전에 모든 타입 관련 문제를 잡아낸다는 뜻이다.
// TypeScript (정적 타입)
function calculateDiscount(price: number, discount: number): number {
return price - discount;
}
calculateDiscount(100, "10%"); // 컴파일 에러: string은 number에 할당 불가
VS Code에서 코드를 작성하는 순간 빨간 줄이 그어지고, "Type 'string' is not assignable to type 'number'"라는 메시지가 뜬다. 실행하기 전에, 심지어 commit하기 전에 문제를 발견할 수 있다.
동적 타입(Dynamic Typing)은 코드를 실행하는 런타임에 타입을 결정한다. 변수를 선언할 때 타입을 명시하지 않고, 프로그램이 실행되면서 할당된 값에 따라 타입이 정해진다. 유연하고 빠르지만, 타입 에러는 실제로 그 코드가 실행될 때까지 발견되지 않는다.
# Python (동적 타입)
def calculate_discount(price, discount):
return price - discount
# 개발할 때는 문제없음
result = calculate_discount(100, 10)
# 운영에서 누군가 이렇게 호출하면 런타임 에러
result = calculate_discount(100, "10%") # TypeError!
비유하자면, 정적 타입은 비행기 탑승 전 보안검색이다. 수하물을 열어보고 위험물을 미리 걸러낸다. 시간은 좀 걸리지만 비행 중 사고를 예방한다. 동적 타입은 빠른 탑승(Fast Track)이다. 검색 없이 바로 태워서 출발이 빠르지만, 비행 중에 문제가 생길 수 있다.
타입 시스템은 단순히 정적 vs 동적의 이분법이 아니었다. 그 사이에 다양한 스펙트럼과 개념들이 존재했다.
정적 타입이라고 해서 항상 타입을 명시해야 하는 건 아니다. 현대의 정적 타입 언어들은 타입 추론을 지원한다. TypeScript는 값을 보고 타입을 자동으로 추론한다.
// 타입을 명시하지 않아도 TypeScript가 추론함
let price = 100; // number로 추론
let name = "Product"; // string으로 추론
price = "text"; // 에러: string을 number에 할당 불가
처음 할당된 값을 보고 타입을 추론한 뒤, 그 타입을 계속 강제한다. 정적 타입의 안전성과 동적 타입의 간결함을 동시에 얻는 방법이다.
동적 타입 언어에서 자주 보는 개념이다. "오리처럼 걷고 오리처럼 꽥꽥거리면, 그건 오리다"라는 철학이다. 변수의 타입을 명시적으로 검사하지 않고, 필요한 속성이나 메서드가 있는지만 확인한다.
# Duck Typing
class Duck:
def quack(self):
return "Quack!"
class Person:
def quack(self):
return "I'm imitating a duck!"
def make_it_quack(thing):
print(thing.quack()) # thing이 뭔지는 상관없음, quack만 있으면 됨
make_it_quack(Duck()) # Quack!
make_it_quack(Person()) # I'm imitating a duck!
타입을 체크하지 않으니 유연하다. 하지만 quack 메서드가 없는 객체를 넘기면 런타임 에러가 난다. TypeScript도 구조적 타이핑(Structural Typing)으로 비슷한 개념을 구현한다.
interface Quackable {
quack(): string;
}
function makeItQuack(thing: Quackable) {
console.log(thing.quack());
}
// Quackable 인터페이스를 명시하지 않아도, quack 메서드가 있으면 통과
makeItQuack({ quack: () => "Quack!" });
동적 타입 언어에 정적 타입을 선택적으로 추가하는 방식이다. TypeScript가 대표적이다. JavaScript 코드를 그대로 쓸 수 있지만, 타입을 추가하면 컴파일 타임 체크를 받을 수 있다.
// 타입 없이도 작동함 (동적 타입처럼)
function add(a, b) {
return a + b;
}
// 타입을 추가하면 체크받음 (정적 타입처럼)
function addTyped(a: number, b: number): number {
return a + b;
}
Python도 타입 힌트(Type Hints)를 통해 점진적 타이핑을 지원한다. 런타임에는 타입을 체크하지 않지만, mypy 같은 도구를 쓰면 정적 분석이 가능하다.
# Python 타입 힌트
def calculate_discount(price: int, discount: int) -> int:
return price - discount
# mypy로 체크하면 에러 잡아줌
calculate_discount(100, "10%") # mypy error: Argument 2 has incompatible type "str"
JavaScript 생태계에서 일하면서 TypeScript를 만난 건 행운이었다. TypeScript는 JavaScript에 정적 타입을 얹은 슈퍼셋이다. 기존 JavaScript 코드를 그대로 쓸 수 있으면서도, 타입을 추가하면 컴파일 타임 체크를 받는다.
// JavaScript 코드 (동적 타입)
function getUserData(userId) {
return fetch(`/api/users/${userId}`).then(res => res.json());
}
// TypeScript로 변환 (정적 타입)
interface User {
id: number;
name: string;
email: string;
}
function getUserData(userId: number): Promise<User> {
return fetch(`/api/users/${userId}`).then(res => res.json());
}
// 이제 타입 에러를 미리 잡을 수 있음
getUserData("123"); // 에러: string을 number에 할당 불가
getUserData(123).then(user => {
console.log(user.name.toUpperCase()); // name이 string임을 보장
});
TypeScript의 진짜 가치는 IDE 통합에서 나온다. VS Code에서 코드를 작성하면, 자동완성, 타입 체크, 리팩토링 지원을 실시간으로 받는다. 함수를 호출할 때 어떤 타입의 인자를 넘겨야 하는지 자동으로 알려주고, 잘못된 타입을 넘기면 즉시 에러를 표시한다.
// VS Code에서 user. 만 입력해도 자동완성 목록이 뜸
user. // id, name, email이 자동완성됨
// 리팩토링도 안전함
// User 인터페이스에서 email을 emailAddress로 변경하면
// 모든 참조 지점을 자동으로 찾아줌
이론을 배웠으니 실제 선택 기준을 정리했다. 프로젝트 상황에 따라 타입 시스템을 선택하는 나만의 매트릭스다.
| 특성 | 정적 타입 (Java, C++, Rust) | 점진적 타입 (TypeScript, Python+mypy) | 동적 타입 (Python, JS) |
|---|---|---|---|
| 안전성 | 높음 (컴파일 타임 보장) | 중간 (선택적 체크) | 낮음 (런타임 에러) |
| 개발 속도 | 느림 (타입 선언 필요) | 중간 (선택적 타입) | 빠름 (타입 신경 안 씀) |
| 유연성 | 낮음 (엄격한 규칙) | 중간 (타입/무타입 혼용) | 높음 (자유로운 변경) |
| IDE 지원 | 최고 (완벽한 자동완성) | 좋음 (타입 있는 부분만) | 제한적 (추론 어려움) |
| 리팩토링 | 안전함 (전체 영향 추적) | 보통 (타입 커버리지 의존) | 위험함 (런타임에서만 확인) |
| 러닝커브 | 높음 (타입 시스템 학습) | 중간 (점진적 학습) | 낮음 (바로 시작) |
| 적합한 시나리오 | 대규모 시스템, 금융 | 성장하는 스타트업 | MVP, 프로토타입 |
이전에 Python에서 겪었던 discount 버그를 TypeScript로 옮겼다.
// Before: JavaScript (동적 타입)
function calculateTotal(items) {
return items.reduce((sum, item) => {
const discount = item.discount || 0;
return sum + (item.price - discount);
}, 0);
}
// 운영에서 터짐
const total = calculateTotal([
{ price: 100, discount: "10%" } // discount가 문자열!
]);
TypeScript로 변환하면서 타입을 명시했다.
// After: TypeScript (정적 타입)
interface Item {
price: number;
discount?: number; // 선택적, 하지만 숫자만 허용
}
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => {
const discount = item.discount || 0;
return sum + (item.price - discount);
}, 0);
}
// 컴파일 에러: discount는 number여야 함
const total = calculateTotal([
{ price: 100, discount: "10%" } // Type 'string' is not assignable to type 'number'
]);
// 올바른 사용
const total = calculateTotal([
{ price: 100, discount: 10 } // OK
]);
코드를 작성하는 순간 VS Code가 빨간 줄을 그어줬다. 고객이 클릭하기 전에, 배포하기 전에, 심지어 테스트를 돌리기 전에 버그를 잡았다. 이게 정적 타입의 힘이다.
Python으로 API를 만들 때 겪었던 사례다. 함수를 작성하고 테스트도 통과했는데, 운영에서 예상치 못한 데이터가 들어오면서 에러가 터졌다.
# Python 코드 (동적 타입)
def format_price(price):
return f"${price:.2f}"
# 테스트할 때는 문제없음
print(format_price(19.99)) # $19.99
# 운영에서 누군가 이렇게 호출
print(format_price(None)) # TypeError: unsupported format string passed to NoneType.__format__
None 값이 들어올 거라고 예상하지 못했다. 방어 코드를 추가했지만, 이런 식의 버그는 계속 발생했다.
# 방어 코드 추가
def format_price(price):
if price is None:
return "$0.00"
if not isinstance(price, (int, float)):
raise ValueError(f"Expected number, got {type(price)}")
return f"${price:.2f}"
타입 힌트와 mypy를 도입하면서 상황이 개선됐다.
# Python + Type Hints
from typing import Optional
def format_price(price: Optional[float]) -> str:
if price is None:
return "$0.00"
return f"${price:.2f}"
# mypy로 체크하면 타입 에러를 미리 잡아줌
format_price("19.99") # mypy error: Argument 1 has incompatible type "str"; expected "Optional[float]"
런타임에는 여전히 동적 타입이지만, CI 파이프라인에 mypy를 추가해서 배포 전에 타입 에러를 잡을 수 있게 됐다.
서비스가 성장하면서 User 객체의 구조를 변경해야 하는 상황이 왔다. JavaScript로 작성된 코드베이스에서는 검색으로 수작업으로 모든 참조를 찾아야 했다.
// JavaScript (동적 타입)
const user = {
userName: "john_doe",
userEmail: "john@example.com"
};
// 코드베이스 전체에 흩어진 참조들
console.log(user.userName); // 여기
sendEmail(user.userEmail); // 저기
validateUser(user.userName); // 또 다른 곳
userName을 username으로 변경하려면 grep으로 검색해서 하나하나 수정해야 했다. 빠뜨리는 곳이 있으면 런타임 에러가 났다.
TypeScript로 마이그레이션한 후에는 상황이 달라졌다.
// TypeScript (정적 타입)
interface User {
userName: string; // 이걸 username으로 변경하면
userEmail: string;
}
const user: User = {
userName: "john_doe",
userEmail: "john@example.com"
};
// 모든 참조 지점에서 즉시 에러가 발생
console.log(user.userName); // Property 'userName' does not exist
인터페이스에서 userName을 username으로 변경하자, 코드베이스 전체에서 빨간 줄이 그어졌다. 어디를 고쳐야 하는지 명확했고, 컴파일러가 모든 참조를 추적해줬다. VS Code의 "Rename Symbol" 기능을 쓰면 한 번에 전체 코드베이스를 리팩토링할 수 있었다.
정적 타입이 무조건 좋고 동적 타입이 나쁜 게 아니다. 각각의 장단점이 있고, 프로젝트 상황에 맞게 선택해야 한다.
MVP를 빠르게 만들어야 하는 초기 스타트업이라면 Python이나 JavaScript의 동적 타입이 유리하다. 타입 선언에 시간을 쓰지 않고, 비즈니스 로직에 집중할 수 있다. 고객 피드백을 빠르게 받고 코드를 자주 변경해야 하는 상황에서 유연성은 큰 장점이다.
하지만 팀이 커지고 코드베이스가 복잡해지면, 동적 타입의 자유로움이 부담으로 바뀐다. 런타임 에러가 증가하고, 리팩토링이 두려워진다. 이때가 정적 타입(또는 점진적 타입)을 도입할 시점이다.
내가 내린 결론은 점진적 타입(TypeScript, Python+mypy)이 현실적인 중간 지점이라는 것이다. 기존 코드베이스를 버리지 않으면서도, 타입 안전성을 점진적으로 높여갈 수 있다. 중요한 부분부터 타입을 추가하고, 덜 중요한 부분은 나중에 처리해도 된다.
타입 시스템은 도구다. 못을 박을 때는 망치가 필요하고, 나사를 조일 때는 드라이버가 필요하다. 프로젝트의 단계와 팀의 상황에 맞는 타입 시스템을 선택하는 게 중요하다. 새벽 3시 장애 전화를 받고 싶지 않다면, 정적 타입을 진지하게 고려해볼 시점이다.