Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 250 additions & 0 deletions jihyeon/12.복합_패턴.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
# 복합 패턴

**복합 패턴(Compound Pattern)**은 여러 디자인 패턴을 함께 사용해 하나의 복잡한 설계를 구성하는 방식이다.

Head First Design Patterns 12장은 오리 시뮬레이터 예제를 통해 어댑터, 데코레이터, 팩토리, 컴포지트, 옵저버가 한 구조 안에서 협력하는 과정을 보여준다.

프론트엔드에서는 하나의 화면이나 기능 안에 API 연동, 외부 SDK, 상태 관리, 이벤트 처리, 로깅, UI 조합이 함께 등장할 때 같은 관점이 적용된다.

## 책의 내용

12장의 오리 시뮬레이터는 다음 요구사항을 단계적으로 해결한다.

| 요구사항 | 사용 패턴 | 역할 |
| --------------------------- | ---------- | ---------------------------------------- |
| 거위를 오리처럼 다루기 | 어댑터 | 다른 인터페이스를 기존 인터페이스에 맞춤 |
| 꽥꽥 횟수 세기 | 데코레이터 | 기존 객체에 부가 기능 추가 |
| 카운터 적용 누락 방지 | 팩토리 | 생성과 조립 규칙을 한 곳에서 관리 |
| 오리 무리를 하나처럼 다루기 | 컴포지트 | 개별 객체와 그룹을 동일하게 처리 |
| 꽥꽥 이벤트 알림 | 옵저버 | 상태 변화와 반응을 분리 |

단일 패턴 하나로 모든 요구사항을 해결하지 않는다. 각 문제에 맞는 패턴을 사용하고, 최종적으로 하나의 동작 흐름으로 조합한다.

---

## 프론트엔드 대응

프론트엔드 기능도 여러 책임이 섞이면 복합 패턴 구조가 필요해진다.

| 책의 예제 | 프론트엔드 대응 | 예시 |
| --------------------- | -------------------------- | -------------------------------------------------------- |
| `Quackable` | 공통 인터페이스 | 공통 Props, 공통 API 타입, 공통 훅 반환 타입 |
| `GooseAdapter` | 외부 형식 변환 | SDK 어댑터, API 응답 변환, 레거시 컴포넌트 래핑 |
| `QuackCounter` | 부가 책임 추가 | analytics, logging, permission check, retry wrapper |
| `CountingDuckFactory` | 생성 규칙 고정 | API client 생성기, provider 조립 함수, feature flag 분기 |
| `Flock` | 그룹과 개별 요소 동일 처리 | 메뉴 트리, 라우트 트리, 폼 필드 그룹 |
| `Quackologist` | 상태 변화 관찰 | store 구독, query invalidation, WebSocket listener |

핵심은 화면 컴포넌트가 모든 구현 세부사항을 직접 알지 않게 만드는 것이다.

```text
컴포넌트: 렌더링과 사용자 입력
어댑터: 외부 API, SDK, 레거시 형식 변환
데코레이터: 로깅, 트래킹, 권한 검사 같은 부가 책임
팩토리: 생성과 조립 규칙
컴포지트: 개별 요소와 그룹의 동일 처리
옵저버: 상태 변화와 반응 분리
```

---

## 패턴별 역할

### 1. 어댑터

외부 인터페이스를 내부에서 사용하는 형태로 변환한다.

주요 사용 지점:

- REST API 응답을 화면 모델로 변환
- GraphQL 응답을 도메인 객체로 변환
- 외부 결제, 지도, 인증 SDK를 내부 인터페이스로 래핑
- 레거시 컴포넌트를 디자인 시스템 컴포넌트처럼 사용
- 브라우저 API를 테스트 가능한 함수로 감싸기

```typescript
function adaptUserResponse(response: UserApiResponse): UserProfile {
return {
id: response.user_id,
name: response.display_name,
avatarUrl: response.profile_image_url,
};
}
```

어댑터의 목적은 외부 형식이 화면 코드 전체로 퍼지는 것을 막는 것이다.

### 2. 데코레이터

기존 동작을 직접 수정하지 않고 바깥에서 부가 책임을 추가한다.

주요 사용 지점:

- analytics tracking
- logging
- permission check
- retry
- error boundary
- fetch wrapper
- React HOC

```typescript
function withRetry<TArgs extends unknown[], TResult>(
fn: (...args: TArgs) => Promise<TResult>,
) {
return async (...args: TArgs) => {
try {
return await fn(...args);
} catch {
return fn(...args);
}
};
}
```

데코레이터의 목적은 원래 책임과 부가 책임을 분리하는 것이다.

### 3. 팩토리

객체나 함수의 생성 규칙을 한 곳에서 관리한다.

주요 사용 지점:

- API client 생성
- 테스트용 mock client 생성
- 기능 플래그에 따른 구현체 선택
- provider 조립
- 컴포넌트 variant 생성

```typescript
function createApiClient(config: ApiConfig) {
return withAuth(withLogging(new HttpApiClient(config.baseUrl)));
}
```

팩토리의 목적은 조립 순서와 정책을 사용하는 쪽에 반복시키지 않는 것이다.

### 4. 컴포지트

개별 요소와 그룹을 같은 방식으로 다룬다.

주요 사용 지점:

- 메뉴 트리
- 라우트 트리
- 폼 필드 그룹
- 테이블 컬럼 그룹
- 권한 트리
- React children 기반 조합

```typescript
type MenuNode = MenuItem | MenuGroup;

interface MenuItem {
type: "item";
label: string;
href: string;
}

interface MenuGroup {
type: "group";
label: string;
children: MenuNode[];
}
```

컴포지트의 목적은 중첩 구조를 일관된 방식으로 처리하는 것이다.

### 5. 옵저버

상태를 변경하는 쪽과 상태 변화에 반응하는 쪽을 분리한다.

주요 사용 지점:

- React state update
- Zustand, Redux store 구독
- TanStack Query cache subscription
- WebSocket event listener
- DOM event listener
- form watch
- analytics event pipeline

```typescript
store.subscribe((state) => {
if (state.paymentStatus === "success") {
queryClient.invalidateQueries({ queryKey: ["order"] });
}
});
```

옵저버의 목적은 상태 변경 이후의 반응을 직접 결합하지 않는 것이다.

---

## 적용 기준

복합 패턴은 다음 상황에서 유용하다.

### 화면 컴포넌트가 많은 책임을 가질 때

API 호출, 응답 변환, 권한 검사, 트래킹, 상태 전파, UI 렌더링이 한 컴포넌트에 모이면 분리 대상이 된다.

### 외부 API나 SDK가 여러 개 섞일 때

SDK마다 호출 방식과 응답 형식이 다르면 내부 공통 인터페이스를 두고 SDK별 차이는 어댑터에 둔다.

### 동일한 부가 기능이 반복될 때

로깅, 인증 헤더, 에러 처리, 재시도 로직이 여러 곳에서 반복되면 wrapper나 데코레이터로 분리한다.

### 생성 순서와 조립 규칙이 중요할 때

`base client -> auth wrapper -> retry wrapper -> logging wrapper`처럼 조립 순서가 정책이라면 팩토리로 고정한다.

### 개별 요소와 그룹을 같은 방식으로 처리할 때

메뉴, 라우트, 폼, 권한 트리, 카테고리 트리처럼 중첩 구조가 있으면 컴포지트 관점이 적합하다.

---

## 주의점

### 패턴 이름을 먼저 정하지 않기

중요한 기준은 패턴 이름이 아니라 책임의 위치다.

```text
외부 형식이 화면에 퍼진다 -> 어댑터
부가 기능이 반복된다 -> 데코레이터
생성 규칙이 반복된다 -> 팩토리
개별 요소와 그룹을 함께 다룬다 -> 컴포지트
상태 변화와 반응이 결합된다 -> 옵저버
```

### 커스텀 훅에 모든 책임을 넣지 않기

`useSomething()`은 조립 지점이 될 수 있지만 모든 책임의 최종 위치가 되면 구조가 다시 복잡해진다.

### 요구사항이 단순할 때 과도하게 나누지 않기

결제 수단이 하나뿐인 화면에 `PaymentMethod`, `PaymentAdapter`, `PaymentFactory`, `PaymentGroup`을 모두 만들 필요는 없다.

### 공통 인터페이스를 크게 만들지 않기

공통 인터페이스가 커지면 모든 구현체가 필요 없는 메서드까지 구현해야 한다. 역할별로 작게 유지하는 편이 안전하다.

---

## 요약

복합 패턴은 여러 패턴을 나열하는 방식이 아니라, 여러 책임을 각자 맞는 위치에 배치하고 조합하는 설계 방식이다.

프론트엔드에서는 화면이 커질수록 렌더링, 외부 연동, 상태 변화, 부가 기능, 생성 규칙이 한곳에 섞이기 쉽다. 복합 패턴 관점은 이 책임들을 분리하고, 화면 컴포넌트가 UI 흐름에 집중하도록 돕는다.

핵심 정리:

1. 어댑터는 외부 형식을 내부 형식으로 바꾼다.
2. 데코레이터는 기존 동작에 부가 책임을 추가한다.
3. 팩토리는 생성과 조립 규칙을 고정한다.
4. 컴포지트는 개별 요소와 그룹을 동일하게 처리한다.
5. 옵저버는 상태 변화와 반응을 분리한다.
Loading