From 72357bf5d1d55ea881dc45b7440674a2af8a5041 Mon Sep 17 00:00:00 2001
From: Doeun <112849712+nemobim@users.noreply.github.com>
Date: Tue, 3 Mar 2026 23:51:10 +0900
Subject: [PATCH 1/2] =?UTF-8?q?docs:=20=EB=8D=B0=EC=BD=94=EB=A0=88?=
=?UTF-8?q?=EC=9D=B4=ED=84=B0=20=ED=8C=A8=ED=84=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...4\355\204\260_\355\214\250\355\204\264.md" | 540 ++++++++++++++++++
1 file changed, 540 insertions(+)
create mode 100644 "doeun/03.\353\215\260\354\275\224\353\240\210\354\235\264\355\204\260_\355\214\250\355\204\264.md"
diff --git "a/doeun/03.\353\215\260\354\275\224\353\240\210\354\235\264\355\204\260_\355\214\250\355\204\264.md" "b/doeun/03.\353\215\260\354\275\224\353\240\210\354\235\264\355\204\260_\355\214\250\355\204\264.md"
new file mode 100644
index 0000000..143d781
--- /dev/null
+++ "b/doeun/03.\353\215\260\354\275\224\353\240\210\354\235\264\355\204\260_\355\214\250\355\204\264.md"
@@ -0,0 +1,540 @@
+# 데코레이터 패턴
+
+## 01. 들어가며
+
+"이 버튼에 로딩 스피너 추가해줄 수 있어요?"
+"API 요청에 인증 헤더 자동으로 붙여줬으면 해요."
+"이 컴포넌트에 에러 바운더리 씌워주세요."
+
+이런 요구사항이 생길 때마다 기존 코드를 매번 수정하면 어떤 일이 벌어질까?
+
+초반에는 대응할 만하지만 수정이 반복될수록 코드는 비대해집니다.
+결국 개발자는 변경 사항이 어디까지 영향을 주는지 가늠하기 어려워지고, 한 곳을 고쳤는데 예상치 못한 다른 곳이 깨지는 사이드 이펙트(Side Effect)가 발생한다.
+
+**데코레이터 패턴(Decorator Pattern)**은 바로 이런 상황에서 **'기존 코드를 건드리지 않고 기능을 확장'**하는 방법이다.
+
+데코레이터 패턴은 마치 커피 커스텀과 같다.
+기본이 되는 '에스프레소' 샷은 절대 건드리지 않는다.
+그 위에 물을 부으면 아메리카노가 되고, 우유를 넣으면 라떼가 되며, 휘핑크림을 얹으면 프라푸치노가 된다.
+샷 자체의 로직(추출 방식)은 변하지 않지만, 바깥에서 무엇을 감싸느냐에 따라 기능과 성격이 확장되는 것이다.
+
+여기서 핵심은
+> 원본은 그대로 두고, 바깥에서 감싸 기능을 추가한다!
+
+---
+
+## 02. 데코레이터 패턴이 뭔데?
+
+> "객체에 추가 요소를 동적으로 더할 수 있다. 데코레이터를 사용하면 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있다." — Head First Design Patterns
+
+구조는 간단하다. 등장인물은 세 가지다.
+
+- **Component (원본)** — 실제 객체. 핵심 기능만 갖고 있다.
+- **Decorator (데코레이터)** — 원본을 감싸는 래퍼. 원본과 **같은 타입**을 구현하면서 기능을 추가한다.
+- **Client (사용하는 쪽)** — 원본인지 데코레이터인지 구분하지 않는다. 인터페이스가 같으니까.
+
+이걸 구조로 그려보면 이렇다.
+
+```
+ ┌─────────────┐
+ │ Component │ ← 공통 인터페이스
+ │ (interface) │
+ └──────┬───────┘
+ │
+ ┌────────┴────────┐
+ │ │
+ ┌─────┴─────┐ ┌─────┴──────┐
+ │ Concrete │ │ Decorator │ ← Component를 감싼다
+ │ Component │ │ (has-a │
+ │ │ │ Component)│
+ └────────────┘ └─────┬──────┘
+ │
+ ┌────────┴────────┐
+ │ │
+ ┌─────┴─────┐ ┌─────┴─────┐
+ │ DecoratorA │ │ DecoratorB │
+ └────────────┘ └────────────┘
+```
+
+핵심은 **데코레이터가 원본과 같은 인터페이스를 구현한다**는 점이다. 그래서 원본 자리에 데코레이터를 끼워 넣어도 클라이언트는 눈치를 못 챈다. 그리고 데코레이터가 `Component`를 내부에 갖고 있기 때문에(**has-a 관계**), 여러 겹으로 쌓을 수도 있다.
+
+```ts
+interface Coffee {
+ getDescription(): string;
+ getCost(): number;
+}
+
+// 원본
+class Espresso implements Coffee {
+ getDescription() { return '에스프레소'; }
+ getCost() { return 2000; }
+}
+
+// 데코레이터 — Coffee를 받아서 Coffee처럼 행동한다
+class MilkDecorator implements Coffee {
+ constructor(private coffee: Coffee) {}
+ getDescription() { return this.coffee.getDescription() + ', 우유'; }
+ getCost() { return this.coffee.getCost() + 500; }
+}
+
+class ShotDecorator implements Coffee {
+ constructor(private coffee: Coffee) {}
+ getDescription() { return this.coffee.getDescription() + ', 샷 추가'; }
+ getCost() { return this.coffee.getCost() + 500; }
+}
+
+// 사용: 데코레이터를 겹겹이 쌓는다
+let coffee: Coffee = new Espresso();
+coffee = new MilkDecorator(coffee);
+coffee = new ShotDecorator(coffee);
+
+coffee.getDescription(); // "에스프레소, 우유, 샷 추가"
+coffee.getCost(); // 3000
+```
+
+에스프레소 클래스를 전혀 건드리지 않았다. 우유 데코레이터와 샷 데코레이터가 각자 자기 기능만 추가했을 뿐인데, 둘을 조합하니 "에스프레소 + 우유 + 샷" 음료가 만들어졌다.
+
+여기서 한 가지 짚고 넘어갈 점이 있다. 데코레이터는 상속(`is-a`)이 아니라 합성(`has-a`)을 사용한다. `MilkDecorator`는 `Coffee`를 **상속**하는 게 아니라, 내부에 `Coffee`를 **갖고** 있다. 같은 인터페이스를 구현하는 건 "타입 호환성"을 위해서일 뿐, 부모의 구현을 물려받기 위해서가 아니다. 이 차이가 뒤에서 다룰 "상속 대신 합성" 이야기의 핵심이다.
+
+---
+
+## 03. 왜 필요할까? — OCP 원칙
+
+헤드 퍼스트에서 강조하는 설계 원칙이 하나 등장한다.
+
+> **핵심 개념 — OCP (Open/Closed Principle)**
+> "클래스는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다."
+> 새 기능을 추가하고 싶을 때 기존 코드를 건드리는 것이 아니라, 새 코드를 추가하는 방식으로 확장해야 한다.
+
+이 원칙이 왜 중요한지 체감하기 어려울 수 있다. 좀 더 현실적인 상황을 생각해보자.
+
+어떤 `Button` 컴포넌트가 프로젝트 전체에서 47곳에 사용되고 있다. 여기에 로딩 상태를 추가하라는 요구사항이 들어왔다. `Button` 클래스를 직접 수정하면 47곳 전부가 영향을 받는다. 하지만 실제로 로딩이 필요한 곳은 3곳뿐이다. 나머지 44곳에서 예상치 못한 사이드 이펙트가 생기지 않을까 불안해진다.
+
+OCP를 지키면 이런 불안에서 벗어날 수 있다. Before/After로 비교해보자.
+
+#### ❌ Before — 클래스 직접 수정
+```ts
+class Button {
+ label: string;
+ // 기능 추가마다 이 클래스를 열어야 한다
+ isLoading: boolean;
+ isDisabled: boolean;
+ hasIcon: boolean;
+ tooltip: string;
+
+ render() {
+ if (this.isLoading) { /* ... */ }
+ if (this.isDisabled) { /* ... */ }
+ if (this.hasIcon) { /* ... */ }
+ // if문이 계속 늘어난다
+ }
+}
+```
+
+#### ✅ After — 데코레이터로 확장
+```ts
+// Button은 그대로 — 47곳에서 안전하게 동작한다
+class Button {
+ render() { return ''; }
+}
+
+// 로딩 기능은 별도의 래퍼로
+class LoadingButton {
+ constructor(private btn: Button) {}
+ render() { return '⏳ ' + this.btn.render(); }
+}
+
+// 필요한 3곳에서만 래핑해서 사용
+new LoadingButton(new Button()).render();
+```
+
+Before 코드에서는 기능이 하나 추가될 때마다 `Button` 클래스에 새 속성과 분기문이 생기고, 그 `Button`을 쓰는 모든 곳에 잠재적 영향을 준다. After 코드에서는 `Button`은 절대 건드리지 않는다. 로딩 버튼, 아이콘 버튼 — 각 변형이 각자의 클래스를 가진다.
+
+---
+
+## 04. 프론트엔드에서의 데코레이터 패턴
+
+사실 우리는 이미 데코레이터 패턴을 매일 쓰고 있다. 가장 대표적인 예시가 React의 HOC, Axios 인터셉터, 그리고 고차 함수다.
+
+### HOC — 컴포넌트를 감싸는 컴포넌트
+
+HOC(Higher-Order Component)는 데코레이터 패턴의 React 구현이다. 컴포넌트를 받아서 기능이 추가된 컴포넌트를 반환한다.
+
+```tsx
+// 인증이 필요한 페이지에 withAuth를 씌운다
+function withAuth
) {
+ return function ErrorWrapper(props: P) {
+ return (
+ }>
+
+
+ );
+ };
+}
+
+// 데코레이터를 겹겹이 쌓을 수 있다
+const DashboardPage = withAuth(withErrorBoundary(Dashboard));
+
+// Dashboard는 인증이고 에러고 신경 쓸 필요가 없다.
+```
+
+**참고: HOC vs 커스텀 훅**
+
+React 생태계에서 HOC는 커스텀 훅에 자리를 많이 내줬다. 인증 같은 경우 `useAuth()` 훅을 직접 쓰는 게 더 직관적일 수 있다. 하지만 HOC가 여전히 유용한 경우가 있다.
+
+- **컴포넌트를 렌더링 자체에서 차단**해야 할 때 (인증 안 된 사용자에게 아예 다른 화면을 보여주기)
+- **여러 컴포넌트에 동일한 래핑 로직**을 반복 적용해야 할 때
+- **ErrorBoundary**처럼 클래스 컴포넌트 기반의 기능을 함수형으로 감쌀 때
+
+즉, HOC = 데코레이터 패턴의 원형에 가깝고, 커스텀 훅 = 같은 문제를 다른 방식(합성)으로 풀어낸 거라고 볼 수 있다.
+
+### Axios 인터셉터 — 요청/응답을 감싸는 데코레이터
+
+Axios 인터셉터도 정확히 같은 구조다. 실제 HTTP 요청 로직을 건드리지 않고, 앞뒤에 기능을 추가한다.
+
+```ts
+// 인증 토큰을 자동으로 붙이는 데코레이터
+axios.interceptors.request.use((config) => {
+ const token = getToken();
+ if (token) config.headers.Authorization = `Bearer ${token}`;
+ return config;
+});
+
+// 에러 응답을 처리하는 데코레이터
+axios.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ if (error.response?.status === 401) logout();
+ return Promise.reject(error);
+ }
+);
+
+// 실제 API 호출 코드는 이 사실을 모른다
+// 그냥 axios.get('/api/data') 하면 토큰도 붙고 에러도 처리된다
+```
+
+인터셉터의 동작 흐름을 그려보면 데코레이터의 "겹겹이 감싸기"가 명확하게 보인다.
+
+```
+요청 흐름:
+axios.get('/api/data')
+ → [Request 인터셉터 1: 토큰 추가]
+ → [Request 인터셉터 2: 로깅]
+ → 실제 HTTP 요청
+ → [Response 인터셉터 1: 에러 처리]
+ → [Response 인터셉터 2: 데이터 변환]
+→ 최종 응답
+```
+
+### 함수형 데코레이터 — 고차 함수
+
+클래스 없이도 된다. 함수를 받아서 기능이 추가된 함수를 반환하는 고차 함수가 곧 데코레이터다. 사실 자바스크립트에서 가장 자연스러운 데코레이터 형태가 바로 이것이다.
+
+```ts
+// 함수 실행 시간을 측정하는 데코레이터
+function withTiming any>(fn: T, name: string): T {
+ return function (...args) {
+ const start = performance.now();
+ const result = fn(...args);
+ console.log(`[Timing] ${name}: ${(performance.now() - start).toFixed(2)}ms`);
+ return result;
+ } as T;
+}
+
+// 에러를 잡아서 기본값을 반환하는 데코레이터
+function withFallback(fn: () => T, fallback: T): () => T {
+ return function () {
+ try { return fn(); }
+ catch { return fallback; }
+ };
+}
+
+// 여러 데코레이터 조합
+const safeParseJSON = withTiming(withFallback(JSON.parse, {}), 'JSON.parse');
+
+safeParseJSON('{"name": "도은"}'); // {name: '도은'} + 시간 출력
+safeParseJSON('invalid json'); // {} (에러 대신 기본값)
+```
+
+이런 패턴은 lodash의 `_.debounce`, `_.throttle`에서도 찾아볼 수 있다. 원래 함수는 그대로인데, 감싸는 것만으로 디바운스/쓰로틀 기능이 추가된다.
+
+### TC39 데코레이터 — 언어 차원의 지원
+
+잠깐 곁가지로 빠지면, JavaScript/TypeScript에서 데코레이터 문법 자체가 언어 표준으로 들어오고 있다. TC39 Stage 3에 도달한 Decorators 프로포절이 있고, TypeScript 5.0부터 이 표준 데코레이터를 지원한다.
+
+```ts
+// TC39 표준 데코레이터 문법
+function logged(originalMethod: any, context: ClassMethodDecoratorContext) {
+ return function (...args: any[]) {
+ console.log(`[Call] ${String(context.name)}`);
+ return originalMethod.call(this, ...args);
+ };
+}
+
+class UserService {
+ @logged
+ getUser(id: string) {
+ return fetch(`/api/users/${id}`);
+ }
+}
+```
+
+`@logged`라는 문법이 "이 메서드를 데코레이터로 감싸라"는 의미다. 우리가 직접 래퍼 함수를 만들 필요 없이 언어가 이 패턴을 지원하는 것이다. NestJS나 Angular 같은 프레임워크에서 `@Controller()`, `@Injectable()` 같은 문법을 봤다면, 그게 바로 이 패턴이다.
+
+---
+
+## 05. 실무 활용 — API 레이어 구성하기
+
+여러 기능이 조합되어야 하는 API 클라이언트 예시를 보자. 요구사항은 이렇다: 로깅, 재시도, 캐싱을 추가해야 한다. 각 기능을 독립적으로 개발하고, 필요에 따라 조합하고 싶다.
+
+```ts
+// 기본 인터페이스
+interface ApiClient {
+ get(url: string): Promise;
+}
+
+// 원본: 진짜 HTTP 요청만 담당한다
+class HttpClient implements ApiClient {
+ async get(url: string): Promise {
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ return res.json();
+ }
+}
+
+// 데코레이터 1: 로깅
+class LoggingClient implements ApiClient {
+ constructor(private client: ApiClient) {}
+ async get(url: string): Promise {
+ console.log(`[Request] GET ${url}`);
+ const start = performance.now();
+ try {
+ const result = await this.client.get(url);
+ console.log(`[Response] GET ${url} ✓ (${(performance.now() - start).toFixed(0)}ms)`);
+ return result;
+ } catch (error) {
+ console.error(`[Error] GET ${url} ✗ (${(performance.now() - start).toFixed(0)}ms)`);
+ throw error;
+ }
+ }
+}
+
+// 데코레이터 2: 실패 시 지수 백오프 재시도
+class RetryClient implements ApiClient {
+ constructor(private client: ApiClient, private maxRetries = 3) {}
+ async get(url: string): Promise {
+ let lastError: Error;
+ for (let i = 0; i < this.maxRetries; i++) {
+ try { return await this.client.get(url); }
+ catch (e) {
+ lastError = e as Error;
+ // 지수 백오프: 1초, 2초, 4초...
+ await new Promise(res => setTimeout(res, 1000 * Math.pow(2, i)));
+ }
+ }
+ throw lastError!;
+ }
+}
+
+// 데코레이터 3: 캐싱
+class CachingClient implements ApiClient {
+ private cache = new Map();
+ constructor(private client: ApiClient, private ttlMs = 60_000) {}
+ async get(url: string): Promise {
+ const cached = this.cache.get(url);
+ if (cached && cached.expiresAt > Date.now()) return cached.data as T;
+ const data = await this.client.get(url);
+ this.cache.set(url, { data, expiresAt: Date.now() + this.ttlMs });
+ return data;
+ }
+}
+```
+
+이제 이 데코레이터들을 조합한다. **안에서부터 바깥으로** 읽으면 실행 순서를 이해할 수 있다.
+
+```ts
+const apiClient: ApiClient = new LoggingClient( // 4) 모든 요청/응답 로그
+ new CachingClient( // 3) 캐시 확인 후 없으면 내부 호출
+ new RetryClient(new HttpClient(), 3), // 2) 실패하면 재시도 → 1) 실제 요청
+ 60_000
+ )
+);
+
+// 사용하는 쪽은 이 모든 걸 모른다
+const data = await apiClient.get('/api/users');
+```
+
+`HttpClient`는 자신이 재시도되는지, 캐싱되는지, 로깅되는지 전혀 모른다. 새 기능이 필요하면 데코레이터 하나를 추가하면 된다. 기존 코드는 건드리지 않는다.
+
+이 구조의 진가는 **환경별로 다른 조합**을 만들 수 있다는 데 있다.
+
+```ts
+// 개발 환경: 로깅만
+const devClient = new LoggingClient(new HttpClient());
+
+// 운영 환경: 재시도 + 캐싱
+const prodClient = new CachingClient(
+ new RetryClient(new HttpClient(), 3),
+ 60_000
+);
+
+// 테스트 환경: 모킹
+const testClient = new MockClient();
+
+// 모두 같은 ApiClient 인터페이스. 사용하는 쪽은 차이를 모른다.
+```
+
+---
+
+## 06. 상속 대신 합성
+
+헤드 퍼스트에서 데코레이터 패턴을 설명하는 이유 중 하나는 **상속의 한계**를 보여주기 위해서다.
+
+#### ❌ 상속으로 모든 조합 만들기
+```ts
+class Espresso {}
+class EspressoWithMilk extends Espresso {}
+class EspressoWithShot extends Espresso {}
+class EspressoWithMilkAndShot extends EspressoWithMilk {}
+// 옵션 3개 → 2³ = 8개 클래스
+// 옵션 5개 → 2⁵ = 32개 클래스
+// 옵션 10개 → 2¹⁰ = 1024개 클래스... 끝이 없다
+```
+
+이것을 **클래스 폭발(class explosion)** 문제라고 부른다. 상속은 "컴파일 타임에 구조가 고정"되기 때문에, 가능한 모든 조합을 미리 만들어 놔야 한다.
+
+#### ✅ 데코레이터로 자유롭게 조합
+```ts
+// 클래스는 4개면 충분하다
+class Espresso {}
+class MilkDecorator {}
+class ShotDecorator {}
+class VanillaDecorator {}
+
+// 조합은 런타임에 자유롭게
+new VanillaDecorator(
+ new ShotDecorator(
+ new MilkDecorator(new Espresso())
+ )
+);
+// 옵션이 10개여도 클래스 11개(원본 1 + 데코레이터 10)면 끝
+// 조합의 수는 사실상 무한
+```
+
+| 비교 항목 | 상속 | 데코레이터 (합성) |
+|-----------|------|-------------------|
+| 조합 방식 | 모든 경우의 수를 미리 클래스로 만듦 | 런타임에 필요한 것만 조합 |
+| 옵션 N개일 때 | 최대 2^N개 클래스 | N+1개 클래스 |
+| 구조 결정 시점 | 컴파일 타임 | 런타임 |
+| 새 옵션 추가 | 기존 조합 × 2만큼 클래스 추가 | 데코레이터 1개 추가 |
+| 기존 코드 수정 | 필요할 수 있음 | 불필요 |
+
+옵저버 패턴 글에서 "상속보다 합성"을 마지막에 다뤘는데, 데코레이터 패턴은 이 원칙이 **패턴 자체의 본질**이다. 데코레이터 패턴 = 합성으로 기능 확장하기.
+
+---
+
+## 07. 유사 패턴과의 비교
+
+데코레이터 패턴은 다른 구조 패턴들과 비슷해 보일 수 있다. 차이를 짚어두면 적재적소에 쓸 수 있다.
+
+**데코레이터 vs 어댑터(Adapter)**: 어댑터는 인터페이스를 **변환**한다. A 인터페이스를 B 인터페이스로 맞춰주는 게 목적이다. 반면 데코레이터는 인터페이스를 **유지**하면서 기능만 추가한다.
+
+**데코레이터 vs 프록시(Proxy)**: 프록시도 원본을 감싸지만, 목적이 다르다. 프록시는 접근 제어(지연 로딩, 권한 체크, 캐싱 등)가 주목적이고, 데코레이터는 기능 확장이 주목적이다. 구조는 거의 동일하기 때문에 "의도"로 구분한다.
+
+**데코레이터 vs 전략(Strategy)**: 전략 패턴은 **내부 알고리즘을 교체**한다. 데코레이터는 **외부에서 기능을 추가**한다. 전략은 "어떻게 할지"를 바꾸고, 데코레이터는 "무엇을 더할지"를 결정한다.
+
+---
+
+## 08. 언제 도입해야 할까
+
+아래 조건 중 2개 이상 해당되면 도입을 고려할 시점이다.
+
+1️⃣ **기존 클래스를 수정하기 어려울 때** — 라이브러리 코드거나 여러 곳에서 쓰이는 클래스라서 건드리기 부담스러울 때
+
+2️⃣ **기능 조합의 경우의 수가 많을 때** — 로딩 + 비활성 + 에러 버튼처럼 조합을 상속으로 커버하면 클래스가 폭발한다
+
+3️⃣ **런타임에 기능을 껐다 켰다 해야 할 때** — 상속은 컴파일 타임에 구조가 고정되지만, 데코레이터는 런타임에 조합을 바꿀 수 있다
+
+4️⃣ **횡단 관심사를 분리하고 싶을 때** — 로깅, 인증, 캐싱, 에러 처리처럼 여러 곳에서 반복되는 부가 기능
+
+반대로, 기능이 하나뿐이고 조합할 일이 없다면 굳이 데코레이터를 쓸 필요 없다. 간단한 if문이면 충분한 상황에서 패턴을 도입하면 오히려 과도한 추상화가 된다.
+
+---
+
+## 09. 주의할 점
+
+#### 타입이 깨질 수 있다
+
+데코레이터가 원본과 같은 인터페이스를 구현해야 한다는 게 핵심인데, TypeScript에서 타입을 제대로 관리하지 않으면 조합 과정에서 타입 안전성이 무너진다.
+
+```ts
+// ❌ 느슨한 타입 — 런타임에 터질 수 있다
+class BadDecorator {
+ constructor(private client: any) {} // any는 타입 체크를 포기하는 것
+ get(url: string) { return this.client.get(url); }
+}
+
+// ✅ 인터페이스를 명시적으로 구현
+class GoodDecorator implements ApiClient {
+ constructor(private client: ApiClient) {}
+ async get(url: string): Promise {
+ return this.client.get(url);
+ }
+}
+```
+
+인터페이스를 선언하고, 데코레이터가 그 인터페이스를 `implements`하게 강제하는 습관이 중요하다. 컴파일 타임에 문제를 잡을 수 있다.
+
+#### 디버깅이 어려워질 수 있다
+
+데코레이터가 여러 겹 쌓이면 에러 스택 트레이스가 래퍼 함수들로 가득 차서, 실제 문제가 어느 레이어에서 생겼는지 추적하기 까다롭다. 몇 가지 방법으로 완화할 수 있다.
+
+- 각 데코레이터에 **이름을 명시**한다 (함수형이라면 `Object.defineProperty`로 name 설정)
+- 개발 환경에서 **현재 쌓인 데코레이터 체인을 출력**하는 유틸리티를 만들어둔다
+- 각 데코레이터의 진입/퇴장에 **로그를 남기는 디버그 모드**를 둔다
+
+#### 데코레이터가 많아지면 복잡도가 올라간다
+
+`new A(new B(new C(new D(new E(원본)))))` 같은 코드가 보이기 시작하면 경고 신호다. 팀 내에서 어떤 기능을 데코레이터로 빼고, 어떤 기능은 클래스 내부에 두는지 기준을 명확히 해야 한다. 자주 쓰는 조합은 **팩토리 함수로 묶어두는 것**도 방법이다.
+
+```ts
+// 자주 쓰는 조합을 팩토리로 묶기
+function createApiClient(options?: { enableCache?: boolean }): ApiClient {
+ let client: ApiClient = new HttpClient();
+ client = new RetryClient(client, 3);
+ if (options?.enableCache) client = new CachingClient(client);
+ client = new LoggingClient(client);
+ return client;
+}
+```
+
+#### 순서가 중요하다
+
+데코레이터는 쌓는 순서에 따라 동작이 달라질 수 있다. 예를 들어 로깅 데코레이터를 캐싱 바깥에 두면 캐시 히트도 로깅되지만, 안쪽에 두면 실제 요청만 로깅된다. 팀 내에서 데코레이터 조합 순서에 대한 컨벤션을 정해두면 혼란을 줄일 수 있다.
+
+---
+
+## 10. 마무리
+
+| 구분 | 내용 |
+|------|------|
+| **패턴** | 데코레이터 패턴 (Decorator Pattern) |
+| **핵심 아이디어** | 원본을 감싸는 래퍼로 기능을 추가한다. 원본 코드는 건드리지 않는다. |
+| **핵심 원칙** | OCP — 확장에는 열려 있고, 변경에는 닫혀 있어야 한다 |
+| **상속 vs 합성** | 상속은 클래스 폭발을 일으킨다. 데코레이터는 합성으로 유연하게 조합한다. |
+| **프론트엔드 예시** | HOC, Axios 인터셉터, 고차 함수, TC39 데코레이터 |
+| **유사 패턴** | 어댑터(변환), 프록시(접근 제어), 전략(알고리즘 교체)과 구분 |
+
+도입 여부를 판단할 때 스스로에게 던질 질문은 하나다.
+
+> "기존 코드를 열지 않고 기능을 추가할 수 있는가?"
+> 답이 **"아니오"** 라면, 데코레이터 패턴은 자연스러운 선택이 된다.
From 521d2bcda0b97b2dd5a5b0846a95f5f369733954 Mon Sep 17 00:00:00 2001
From: Doeun <112849712+nemobim@users.noreply.github.com>
Date: Tue, 10 Mar 2026 20:42:46 +0900
Subject: [PATCH 2/2] =?UTF-8?q?Create=2004.=ED=8C=A9=ED=86=A0=EB=A6=AC=5F?=
=?UTF-8?q?=ED=8C=A8=ED=84=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...\240\353\246\254_\355\214\250\355\204\264" | 685 ++++++++++++++++++
1 file changed, 685 insertions(+)
create mode 100644 "doeun/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264"
diff --git "a/doeun/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264" "b/doeun/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264"
new file mode 100644
index 0000000..65c2c8b
--- /dev/null
+++ "b/doeun/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264"
@@ -0,0 +1,685 @@
+# 팩토리 패턴 — "new를 직접 쓰지 마세요"
+
+## 들어가며
+
+코드에서 `new`를 쓸 때마다 우리는 **구체 클래스에 의존**하게 됩니다.
+지금은 요구사항이 2개지만, 곧 5개가 되고, 10개가 됩니다.
+그때마다 `new` 주변의 코드가 줄줄이 바뀝니다.
+
+팩토리 패턴은 이 문제를 정면으로 해결합니다.
+**객체 생성 로직을 캡슐화**해서, 클라이언트 코드는 "무엇을 만들지"만 요청하고 "어떻게 만드는지"는 전혀 모르게 하는 것이죠.
+
+이 글에서는 팩토리 패턴의 세 가지 단계를 점진적으로 살펴봅니다:
+
+1. **Simple Factory** — 관용적 패턴 (GoF 공식 패턴은 아님)
+2. **Factory Method Pattern** — 서브클래스에게 생성을 위임
+3. **Abstract Factory Pattern** — 관련 제품군을 통째로 교체
+
+개념을 잡은 뒤, **프론트엔드 실무에서 팩토리 패턴이 어떻게 쓰이는지**까지 다룹니다.
+
+---
+
+## 1. 문제 상황 — Before
+
+피자 가게를 운영한다고 해봅시다.
+
+```typescript
+function orderPizza(type: string): Pizza {
+ let pizza: Pizza;
+
+ // 🚨 피자 종류가 바뀔 때마다 이 코드를 수정해야 한다
+ if (type === "cheese") {
+ pizza = new CheesePizza();
+ } else if (type === "pepperoni") {
+ pizza = new PepperoniPizza();
+ } else if (type === "clam") {
+ pizza = new ClamPizza();
+ } else {
+ throw new Error(`Unknown pizza type: ${type}`);
+ }
+
+ // 이 아래는 변하지 않는 로직
+ pizza.prepare();
+ pizza.bake();
+ pizza.cut();
+ pizza.box();
+
+ return pizza;
+}
+```
+
+얼핏 보면 문제 없어 보이지만, 이 코드에는 구조적인 약점이 있습니다.
+
+**첫째, 변경에 취약합니다.**
+새 피자를 추가하거나 기존 피자를 제거할 때마다 `orderPizza` 함수 자체를 열어야 합니다. 피자 종류가 10개, 20개로 늘어나면 이 if-else 체인은 걷잡을 수 없이 길어집니다.
+
+**둘째, 생성과 사용이 강하게 결합되어 있습니다.**
+`orderPizza`는 "어떤 피자를 만들지"와 "피자를 어떻게 처리할지"를 동시에 알고 있습니다. 단일 책임 원칙(SRP) 위반입니다.
+
+**셋째, OCP(개방-폐쇄 원칙)를 위반합니다.**
+새로운 피자 종류를 추가하려면 기존 코드를 수정해야 합니다. 확장에는 열려 있어야 하지만, 변경에는 닫혀 있어야 한다는 원칙에 정면으로 어긋나죠.
+
+---
+
+## 2. Simple Factory — 생성 로직을 분리하자
+
+가장 먼저 할 수 있는 일은 **생성 로직을 별도 클래스로 추출**하는 것입니다.
+
+```typescript
+class SimplePizzaFactory {
+ createPizza(type: string): Pizza {
+ switch (type) {
+ case "cheese":
+ return new CheesePizza();
+ case "pepperoni":
+ return new PepperoniPizza();
+ case "clam":
+ return new ClamPizza();
+ default:
+ throw new Error(`Unknown pizza type: ${type}`);
+ }
+ }
+}
+
+class PizzaStore {
+ private factory: SimplePizzaFactory;
+
+ constructor(factory: SimplePizzaFactory) {
+ this.factory = factory;
+ }
+
+ orderPizza(type: string): Pizza {
+ const pizza = this.factory.createPizza(type); // 생성은 팩토리에게 위임
+
+ pizza.prepare();
+ pizza.bake();
+ pizza.cut();
+ pizza.box();
+
+ return pizza;
+ }
+}
+```
+
+Simple Factory는 GoF가 정의한 공식 디자인 패턴이 아닙니다. 그보다는 **자주 쓰이는 프로그래밍 관용구(idiom)** 에 가깝습니다. 하지만 효과는 확실합니다.
+
+`PizzaStore`는 더 이상 구체 피자 클래스를 직접 참조하지 않습니다. 피자 메뉴가 바뀌면 `SimplePizzaFactory`만 수정하면 됩니다. 생성과 사용이 분리된 것이죠.
+
+**그런데 한계가 있습니다.**
+
+만약 피자 가게가 프랜차이즈로 확장되어 뉴욕 스타일, 시카고 스타일처럼 지역마다 다른 피자를 만들어야 한다면? Simple Factory 하나로는 모든 지역의 생성 로직을 담을 수 없고, 팩토리 자체를 통째로 교체해야 합니다. 확장에 유연하지 않죠.
+
+---
+
+## 3. Factory Method Pattern — 서브클래스가 결정한다
+
+> **정의:** 객체를 생성하기 위한 인터페이스를 정의하되, 어떤 클래스의 인스턴스를 만들지는 **서브클래스가 결정**하게 합니다.
+
+핵심 아이디어는 간단합니다. 상위 클래스에서 객체 생성 메서드를 `abstract`로 선언하고, 각 서브클래스가 자신만의 방식으로 이를 구현하는 것입니다.
+
+먼저 피자를 정의합니다:
+
+```typescript
+abstract class Pizza {
+ name: string = "";
+ dough: string = "";
+ sauce: string = "";
+ toppings: string[] = [];
+
+ prepare(): void {
+ console.log(`준비 중: ${this.name}`);
+ console.log(`도우: ${this.dough}`);
+ console.log(`소스: ${this.sauce}`);
+ console.log(`토핑: ${this.toppings.join(", ")}`);
+ }
+
+ bake(): void {
+ console.log("175도에서 25분 굽기");
+ }
+
+ cut(): void {
+ console.log("대각선으로 자르기");
+ }
+
+ box(): void {
+ console.log("포장하기");
+ }
+}
+
+// 뉴욕 스타일
+class NYStyleCheesePizza extends Pizza {
+ constructor() {
+ super();
+ this.name = "뉴욕 스타일 치즈 피자";
+ this.dough = "씬 크러스트";
+ this.sauce = "마리나라 소스";
+ this.toppings = ["모짜렐라 치즈"];
+ }
+}
+
+// 시카고 스타일
+class ChicagoStyleCheesePizza extends Pizza {
+ constructor() {
+ super();
+ this.name = "시카고 스타일 치즈 피자";
+ this.dough = "딥 디시";
+ this.sauce = "플럼 토마토 소스";
+ this.toppings = ["모짜렐라 치즈 (듬뿍)"];
+ }
+
+ override cut(): void {
+ console.log("네모나게 자르기"); // 시카고 스타일은 네모로 자른다
+ }
+}
+```
+
+이제 피자 가게를 팩토리 메서드로 구성합니다:
+
+```typescript
+abstract class PizzaStore {
+ // 전체 흐름은 고정 (템플릿 메서드)
+ orderPizza(type: string): Pizza {
+ const pizza = this.createPizza(type); // 👈 팩토리 메서드 호출
+
+ pizza.prepare();
+ pizza.bake();
+ pizza.cut();
+ pizza.box();
+
+ return pizza;
+ }
+
+ // 👇 팩토리 메서드: 서브클래스가 구현
+ protected abstract createPizza(type: string): Pizza;
+}
+
+class NYPizzaStore extends PizzaStore {
+ protected createPizza(type: string): Pizza {
+ switch (type) {
+ case "cheese":
+ return new NYStyleCheesePizza();
+ case "pepperoni":
+ return new NYStylePepperoniPizza();
+ default:
+ throw new Error(`Unknown type: ${type}`);
+ }
+ }
+}
+
+class ChicagoPizzaStore extends PizzaStore {
+ protected createPizza(type: string): Pizza {
+ switch (type) {
+ case "cheese":
+ return new ChicagoStyleCheesePizza();
+ case "pepperoni":
+ return new ChicagoStylePepperoniPizza();
+ default:
+ throw new Error(`Unknown type: ${type}`);
+ }
+ }
+}
+```
+
+```typescript
+// 사용
+const nyStore = new NYPizzaStore();
+const chicagoStore = new ChicagoPizzaStore();
+
+nyStore.orderPizza("cheese");
+// → "뉴욕 스타일 치즈 피자", 씬 크러스트, 대각선 컷
+
+chicagoStore.orderPizza("cheese");
+// → "시카고 스타일 치즈 피자", 딥 디시, 네모 컷
+```
+
+### 왜 이렇게 하는 걸까?
+
+`PizzaStore`(상위 클래스)는 `createPizza`가 어떤 피자를 만드는지 전혀 모릅니다. 알 필요도 없습니다. `orderPizza`의 prepare → bake → cut → box 흐름은 어떤 피자든 동일하게 적용되고, "어떤 피자를 만들지"라는 결정만 서브클래스에 위임하는 것이죠.
+
+이 구조는 **템플릿 메서드 패턴**과 자연스럽게 결합됩니다. `orderPizza`가 전체 알고리즘의 뼈대를 정의하고, 그 중 하나의 단계(생성)만 서브클래스에게 맡기는 형태입니다.
+
+새로운 지역 스타일이 추가되어도 `PizzaStore`는 건드릴 필요가 없습니다. 새 서브클래스를 만들면 됩니다. OCP를 만족합니다.
+
+---
+
+## 4. Abstract Factory Pattern — 제품군을 통째로 교체
+
+Factory Method가 **하나의 제품**을 만드는 메서드를 서브클래싱한다면, Abstract Factory는 **관련된 제품군(family) 전체**를 만드는 인터페이스를 제공합니다.
+
+피자로 다시 생각해봅시다. 뉴욕 피자와 시카고 피자는 도우, 소스, 치즈 같은 **재료 세트**가 통째로 다릅니다. 이걸 개별적으로 관리하면 뉴욕 도우에 시카고 소스가 조합되는 불일치가 생길 수 있죠.
+
+```typescript
+// 재료 인터페이스들
+interface Dough {
+ name: string;
+}
+interface Sauce {
+ name: string;
+}
+interface Cheese {
+ name: string;
+}
+
+// 추상 팩토리: 재료 세트 전체를 만드는 인터페이스
+interface PizzaIngredientFactory {
+ createDough(): Dough;
+ createSauce(): Sauce;
+ createCheese(): Cheese;
+}
+```
+
+```typescript
+// 뉴욕 재료 팩토리
+class NYPizzaIngredientFactory implements PizzaIngredientFactory {
+ createDough(): Dough {
+ return { name: "씬 크러스트 도우" };
+ }
+ createSauce(): Sauce {
+ return { name: "마리나라 소스" };
+ }
+ createCheese(): Cheese {
+ return { name: "레지아노 치즈" };
+ }
+}
+
+// 시카고 재료 팩토리
+class ChicagoPizzaIngredientFactory implements PizzaIngredientFactory {
+ createDough(): Dough {
+ return { name: "딥 디시 도우" };
+ }
+ createSauce(): Sauce {
+ return { name: "플럼 토마토 소스" };
+ }
+ createCheese(): Cheese {
+ return { name: "모짜렐라 치즈" };
+ }
+}
+```
+
+```typescript
+// 피자가 재료 팩토리를 사용
+class CheesePizza extends Pizza {
+ private ingredientFactory: PizzaIngredientFactory;
+
+ constructor(ingredientFactory: PizzaIngredientFactory) {
+ super();
+ this.ingredientFactory = ingredientFactory;
+ }
+
+ override prepare(): void {
+ console.log(`준비 중: ${this.name}`);
+ const dough = this.ingredientFactory.createDough();
+ const sauce = this.ingredientFactory.createSauce();
+ const cheese = this.ingredientFactory.createCheese();
+ console.log(`도우: ${dough.name}, 소스: ${sauce.name}, 치즈: ${cheese.name}`);
+ }
+}
+```
+
+핵심은 `CheesePizza`가 **어떤 재료를 쓰는지 직접 알 필요가 없다**는 것입니다. 주입받은 팩토리가 알아서 일관된 재료 세트를 제공합니다. 팩토리만 바꾸면 뉴욕 스타일에서 시카고 스타일로 재료 전체가 한 번에 바뀝니다.
+
+### Factory Method vs Abstract Factory
+
+| 비교 | Factory Method | Abstract Factory |
+|------|---------------|-----------------|
+| 무엇을 만드나 | 하나의 제품 | 관련된 제품군(family) |
+| 동작 방식 | **상속** — 서브클래스가 생성을 결정 | **구성(composition)** — 팩토리 객체를 주입 |
+| 확장 방법 | 서브클래스 추가 | 새 팩토리 구현체 추가 |
+| 새 제품 추가 시 | 비교적 쉬움 | 인터페이스 변경 필요 (모든 팩토리에 영향) |
+
+둘은 배타적이지 않습니다. Abstract Factory 내부에서 각 제품을 만들 때 Factory Method를 사용하는 경우도 흔합니다.
+
+---
+
+## 5. 디자인 원칙: 의존성 역전 원칙 (DIP)
+
+팩토리 패턴의 이론적 근간이 되는 원칙입니다.
+
+> **"추상화에 의존하라. 구체 클래스에 의존하지 마라."**
+
+팩토리 패턴을 적용하기 전에는 상위 모듈(`PizzaStore`)이 하위 모듈(`NYStyleCheesePizza`, `ChicagoStyleCheesePizza`)에 직접 의존합니다. 새 피자가 추가될 때마다 상위 모듈을 수정해야 하죠.
+
+```
+❌ 팩토리 적용 전 (의존성이 위에서 아래로만 흐름)
+PizzaStore → NYStyleCheesePizza
+PizzaStore → ChicagoStyleCheesePizza
+PizzaStore → NYStylePepperoniPizza
+...
+```
+
+팩토리를 적용하면 상위 모듈과 하위 모듈 모두 **추상화(Pizza)**에 의존하게 됩니다. 의존성의 방향이 역전되는 것이죠.
+
+```
+✅ 팩토리 적용 후 (의존성 역전)
+PizzaStore → Pizza (추상)
+NYStyleCheese → Pizza (추상)
+ChicagoStyleCheese → Pizza (추상)
+```
+
+이제 `PizzaStore`는 구체 클래스가 몇 개든, 어떤 것이든 상관없이 `Pizza` 추상화만 바라봅니다.
+
+**DIP를 지키기 위한 가이드라인:**
+
+- 변수에 구체 클래스 타입을 직접 쓰지 않기
+- 구체 클래스를 직접 상속하지 않기 (추상 클래스나 인터페이스에서 파생)
+- 베이스 클래스에 이미 구현된 메서드를 오버라이드하지 않기
+
+물론 이건 가이드라인이지 절대 법칙은 아닙니다. `new Date()`나 `new Map()` 같은 안정적인 클래스까지 팩토리로 감쌀 필요는 없습니다. **변경 가능성이 높은 구체 클래스에 대한 의존**을 줄이는 게 핵심입니다.
+
+---
+
+## 6. 프론트엔드에서의 팩토리 패턴
+
+"팩토리 패턴은 백엔드나 OOP에서나 쓰는 거 아닌가?" 싶을 수 있지만, 프론트엔드에서도 본질은 같습니다. **조건에 따라 다른 것을 만들어야 하는 상황**이 있다면, 그게 팩토리 패턴이 필요한 자리입니다.
+
+### 6-1. 컴포넌트 팩토리
+
+조건에 따라 다른 UI를 렌더링하는 상황, 프론트엔드에서 가장 흔하게 마주치는 팩토리 패턴의 적용 지점입니다.
+
+```tsx
+// ❌ Before: 컴포넌트 안에서 if-else 분기가 난무
+function NotificationBanner({ type, message }: Props) {
+ if (type === "success") {
+ return