--- id: wiki-2026-0508-introduce-null-object-널-객체-도입하기 title: Introduce Null Object (널 객체 도입하기) category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Null Object Pattern, Null Object Refactoring] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [refactoring, design-pattern, null-safety] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: typescript framework: none --- # Introduce Null Object (널 객체 도입하기) ## 매 한 줄 > **"매 null check 의 polymorphism 으로 대체"**. Fowler 의 Refactoring 카탈로그 entry — `if (x == null)` scattered checks 를 default-behavior object 로 collapse. 2026 modern 에서는 Optional/Maybe monad, sealed class hierarchy, TypeScript discriminated union 으로 evolve 했지만 core idea 는 동일. ## 매 핵심 ### 매 문제 - Repeated null guards: `customer == null ? "unknown" : customer.name()` 의 매 caller 마다 반복. - Null-aware logic 의 매 client code 에 leak — encapsulation violation. - NullPointerException / undefined error 의 매 production crash 의 top cause. ### 매 solution - Null state 를 dedicated subclass 로 represent: `NullCustomer extends Customer`. - Default behavior 를 polymorphic method 로 push down. - Caller 는 `customer.name()` 의 null check 없이 호출. ### 매 응용 1. Fowler refactoring step-by-step (Replace Conditional with Polymorphism 의 sibling). 2. Domain-driven design 의 missing aggregate handling. 3. Strategy pattern 의 default no-op variant. ## 💻 패턴 ### 1. Before (null check scattered) ```typescript class Site { constructor(public customer: Customer | null) {} } const customer = site.customer; const name = customer === null ? "occupant" : customer.name; const plan = customer === null ? BillingPlan.basic() : customer.plan; const history = customer === null ? new PaymentHistory([]) : customer.history; ``` ### 2. After (Null Object) ```typescript abstract class Customer { abstract get name(): string; abstract get plan(): BillingPlan; abstract get history(): PaymentHistory; isNull(): boolean { return false; } } class RealCustomer extends Customer { constructor( public name: string, public plan: BillingPlan, public history: PaymentHistory, ) { super(); } } class NullCustomer extends Customer { get name() { return "occupant"; } get plan() { return BillingPlan.basic(); } get history() { return new PaymentHistory([]); } isNull() { return true; } } class Site { constructor(public customer: Customer = new NullCustomer()) {} } // Caller — 매 null check 없이 const name = site.customer.name; ``` ### 3. Special Case (richer variant) ```typescript class UnknownCustomer extends Customer { get name() { return "unknown"; } // ... default } class DeceasedCustomer extends Customer { get name() { return "[deceased]"; } // ... distinct semantics } // Factory dispatch function loadCustomer(id: string): Customer { const row = db.find(id); if (!row) return new UnknownCustomer(); if (row.deceased) return new DeceasedCustomer(row); return new RealCustomer(row.name, row.plan, row.history); } ``` ### 4. Optional / Maybe (functional alternative) ```typescript type Maybe = { kind: "some"; value: T } | { kind: "none" }; const customer: Maybe = loadCustomer(id); const name = customer.kind === "some" ? customer.value.name : "occupant"; // Or with helper function fold(m: Maybe, onSome: (v: T) => R, onNone: () => R): R { return m.kind === "some" ? onSome(m.value) : onNone(); } ``` ### 5. Sealed class (Kotlin / TS discriminated union) ```typescript type CustomerState = | { tag: "real"; data: RealCustomer } | { tag: "null"; reason: "missing" | "deceased" }; function displayName(s: CustomerState): string { switch (s.tag) { case "real": return s.data.name; case "null": return s.reason === "deceased" ? "[deceased]" : "occupant"; } } ``` ### 6. Logger Null Object (classic example) ```typescript interface Logger { log(msg: string): void; } class ConsoleLogger implements Logger { log(msg: string) { console.log(msg); } } class NullLogger implements Logger { log(_msg: string) { /* no-op */ } } class Service { constructor(private logger: Logger = new NullLogger()) {} // 매 logger null check 없이 항상 호출 가능 } ``` ### 7. React/UI default component ```tsx function UserBadge({ user }: { user: User | null }) { // 매 null branch 없이 — NullUser component 로 dispatch const u = user ?? NULL_USER; return {u.displayName}; } const NULL_USER: User = { id: "anonymous", displayName: "Anonymous", avatar: "/default.png", }; ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | OO codebase, 매 null check 의 3+ 위치 반복 | Null Object class. | | Functional / TS strict, 매 type-level safety 필요 | Maybe / Optional / discriminated union. | | Side-effect 의 default no-op (logger, audit) | NullLogger 의 dependency injection. | | 매 distinct missing-state semantics (unknown vs deceased) | Special Case (multiple null subtypes). | | 매 single-use null check | 매 그냥 inline `?? default`. | **기본값**: TypeScript / modern stack 에서는 `?? default` 또는 discriminated union. OO heavy refactor target 에서는 Null Object class. ## 🔗 Graph - 부모: [[Refactoring]] · [[Design Patterns]] - 변형: [[Special Case Pattern]] · [[Maybe Monad]] · [[Optional Type]] - 응용: [[Strategy Pattern]] · [[Dependency Injection]] - Adjacent: [[Replace Conditional with Polymorphism]] · [[Sealed Classes]] ## 🤖 LLM 활용 **언제**: legacy codebase 의 null-check noise 제거 refactor, library API 의 default behavior design. **언제 X**: 매 single null check, performance-critical hot path (allocation overhead), exception 이 의도된 signal 인 경우. ## ❌ 안티패턴 - **Hidden bugs**: NullCustomer 의 silent default 가 매 real bug 를 mask. Logging 또는 `isNull()` flag 로 trace 가능하게 유지. - **God Null Object**: 매 모든 method 에 default 의 over-design. 매 진짜 polymorphic 사용처만 cover. - **Equality confusion**: `nullCustomer === null` false 의 매 caller 혼란. 매 명확한 documentation + `isNull()` helper. - **Mutation on Null Object**: 매 shared instance 의 mutation 이 cross-contamination 의 cause. 매 immutable 유지. ## 🧪 검증 / 중복 - Verified (Fowler, *Refactoring* 2nd ed., "Introduce Special Case" — Null Object 의 modern rename). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — Null Object refactoring + modern Maybe/sealed union variants |