f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
4.6 KiB
Markdown
151 lines
4.6 KiB
Markdown
---
|
|
id: wiki-2026-0508-primitive-obsession-기본-타입-집착
|
|
title: Primitive Obsession (기본 타입 집착)
|
|
category: 10_Wiki/Topics
|
|
status: verified
|
|
canonical_id: self
|
|
aliases: [Primitive Obsession, 기본 타입 집착, stringly-typed]
|
|
duplicate_of: none
|
|
source_trust_level: A
|
|
confidence_score: 0.9
|
|
verification_status: applied
|
|
tags: [refactoring, code-smell, type-system, ddd]
|
|
raw_sources: []
|
|
last_reinforced: 2026-05-10
|
|
github_commit: pending
|
|
tech_stack:
|
|
language: python-typescript-rust
|
|
framework: refactoring
|
|
---
|
|
|
|
# Primitive Obsession (기본 타입 집착)
|
|
|
|
## 매 한 줄
|
|
> **"매 string/int이 매 domain concept를 매 가장한다"**. Primitive Obsession은 매 `UserId`, `Email`, `Money` 같은 매 domain concept를 매 raw `str`/`int`로 매 표현해 매 type system이 매 invariant 보장 못 하는 매 code smell. Fowler "Refactoring" (1999, 2nd ed. 2018) 의 매 classic smell — 매 modern fix는 매 newtype / value object.
|
|
|
|
## 매 핵심
|
|
|
|
### 매 증상
|
|
- **매 String 폭주**: 매 `email: str`, `phone: str`, `country_code: str` 매 swap 가능.
|
|
- **매 Magic numbers**: 매 `status: int = 3` 매 의미 불명.
|
|
- **매 Validation duplication**: 매 every callsite마다 매 `if "@" in email`.
|
|
- **매 Type confusion**: 매 `transfer(from_id, to_id)` 매 인자 swap 매 컴파일러 못 잡음.
|
|
|
|
### 매 Fix 전략
|
|
- **매 Newtype**: Rust `struct UserId(u64);`.
|
|
- **매 Value Object**: DDD 의 매 `Email`, `Money` immutable class.
|
|
- **매 Branded type**: TypeScript `type UserId = string & { __brand: "UserId" }`.
|
|
- **매 NewType pattern**: Python `typing.NewType("UserId", int)`.
|
|
|
|
### 매 응용
|
|
1. Domain modeling (DDD) — Bounded context의 매 first-class concept.
|
|
2. Money handling (currency + amount tied).
|
|
3. Identifier safety (UserId vs OrderId mix-up 방지).
|
|
|
|
## 💻 패턴
|
|
|
|
### Python NewType + dataclass
|
|
```python
|
|
from typing import NewType
|
|
from dataclasses import dataclass
|
|
from decimal import Decimal
|
|
|
|
UserId = NewType("UserId", int)
|
|
OrderId = NewType("OrderId", int)
|
|
|
|
@dataclass(frozen=True)
|
|
class Money:
|
|
amount: Decimal
|
|
currency: str
|
|
def __post_init__(self):
|
|
if self.amount < 0: raise ValueError("negative")
|
|
if len(self.currency) != 3: raise ValueError("ISO-4217")
|
|
|
|
def transfer(src: UserId, dst: UserId, m: Money): ...
|
|
# transfer(OrderId(1), UserId(2), Money(...)) # 매 mypy error
|
|
```
|
|
|
|
### Rust newtype
|
|
```rust
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct UserId(u64);
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct OrderId(u64);
|
|
|
|
fn fetch_user(id: UserId) { /* ... */ }
|
|
// fetch_user(OrderId(7)); // 매 compile error
|
|
```
|
|
|
|
### TypeScript branded types
|
|
```typescript
|
|
type Brand<T, B> = T & { readonly __brand: B };
|
|
type Email = Brand<string, "Email">;
|
|
type UserId = Brand<number, "UserId">;
|
|
|
|
function parseEmail(s: string): Email {
|
|
if (!/^[^@]+@[^@]+$/.test(s)) throw new Error("invalid");
|
|
return s as Email;
|
|
}
|
|
```
|
|
|
|
### Value object with invariant
|
|
```python
|
|
@dataclass(frozen=True)
|
|
class Email:
|
|
value: str
|
|
def __post_init__(self):
|
|
if "@" not in self.value: raise ValueError("invalid email")
|
|
object.__setattr__(self, "value", self.value.lower())
|
|
```
|
|
|
|
### Refactor: Replace Type Code with Subclass
|
|
```python
|
|
# Before — magic int status
|
|
class Order:
|
|
status: int # 0=pending, 1=paid, 2=shipped
|
|
|
|
# After — sealed states
|
|
class OrderStatus: pass
|
|
class Pending(OrderStatus): pass
|
|
class Paid(OrderStatus): pass
|
|
class Shipped(OrderStatus):
|
|
tracking: str
|
|
```
|
|
|
|
## 매 결정 기준
|
|
| 상황 | Approach |
|
|
|---|---|
|
|
| Domain identifier | Newtype |
|
|
| Domain value (Money, Email) | Value Object |
|
|
| 매 enum-like int code | Sealed subclass / Enum |
|
|
| Throwaway script | 매 raw primitive OK |
|
|
|
|
**기본값**: Newtype for IDs, Value Object for domain values.
|
|
|
|
## 🔗 Graph
|
|
- 부모: [[Code-Smell]] · [[Refactoring_Best_Practices|Refactoring]]
|
|
- 변형: [[Value-Object]] · [[Branded-Types]]
|
|
- 응용: [[DDD]] · [[Type-Safety]]
|
|
- Adjacent: [[Stringly-Typed]]
|
|
|
|
## 🤖 LLM 활용
|
|
**언제**: domain model 설계, 매 API boundary type 선택, 매 refactoring 제안.
|
|
**언제 X**: 매 1-time script.
|
|
|
|
## ❌ 안티패턴
|
|
- **매 Stringly-typed everything**: 매 모든 domain concept를 매 str.
|
|
- **매 Validation lazy**: 매 boundary 통과 후 매 raw str 그대로 흘림.
|
|
- **매 Tuple as struct**: 매 `(int, str, bool)` 매 의미 모름.
|
|
- **매 매 시점 validation**: 매 매번 caller가 매 validate 수행.
|
|
|
|
## 🧪 검증 / 중복
|
|
- Verified (Fowler "Refactoring" 2nd ed. 2018, Evans "DDD" 2003).
|
|
- 신뢰도 A.
|
|
|
|
## 🕓 Changelog
|
|
| 날짜 | 변경 |
|
|
|---|---|
|
|
| 2026-05-08 | Phase 1 |
|
|
| 2026-05-10 | Manual cleanup — newtype/value-object patterns + refactoring guide |
|