diff --git "a/jihyeon/09.\353\260\230\353\263\265\354\236\220_\355\214\250\355\204\264_\354\273\264\355\217\254\354\247\200\355\212\270_\355\214\250\355\204\264.md" "b/jihyeon/09.\353\260\230\353\263\265\354\236\220_\355\214\250\355\204\264_\354\273\264\355\217\254\354\247\200\355\212\270_\355\214\250\355\204\264.md" new file mode 100644 index 0000000..aab119d --- /dev/null +++ "b/jihyeon/09.\353\260\230\353\263\265\354\236\220_\355\214\250\355\204\264_\354\273\264\355\217\254\354\247\200\355\212\270_\355\214\250\355\204\264.md" @@ -0,0 +1,247 @@ +# 반복자 패턴과 컴포지트 패턴 + +## 1. + +반복자 패턴과 컴포지트 패턴은 서로 다른 목적을 가진다. + +- **반복자 패턴**은 컬렉션 내부 구조를 드러내지 않고 원소를 순회하기 위한 패턴이다. +- **컴포지트 패턴**은 개별 객체와 복합 객체를 동일한 방식으로 다루기 위한 패턴이다. + +이 둘은 별개의 패턴이지만, **계층 구조를 만들고 그 계층 전체를 탐색해야 하는 상황**에서 자연스럽게 함께 사용된다. +Head First Design Patterns의 메뉴 예제가 바로 이 조합을 보여준다. + +정리하면 다음과 같다. + +- 컴포지트 패턴은 **구조를 통일**한다. +- 반복자 패턴은 **순회를 통일**한다. + +--- + +## 2. 왜 두 패턴을 함께 배울까? + +헤드 퍼스트에서는 식당 메뉴 예제를 통해 패턴을 단계적으로 확장한다. + +### 2.1 처음 문제: 서로 다른 메뉴를 한 방식으로 순회하고 싶다 + +팬케이크 하우스 메뉴와 다이너 메뉴는 각각 다른 자료구조를 사용할 수 있다. + +- 한 메뉴는 배열을 사용할 수 있고 +- 다른 메뉴는 리스트를 사용할 수 있다 + +하지만 메뉴를 출력하는 쪽에서는 저장 방식보다 **메뉴 항목을 일관되게 순회하는 방법**이 더 중요하다. +이 문제를 해결하기 위해 먼저 반복자 패턴이 도입된다. + +### 2.2 다음 문제: 메뉴 안에 또 다른 메뉴가 들어간다 + +요구사항이 커지면 단순한 메뉴 목록이 아니라 다음과 같은 구조가 필요해진다. + +- 전체 메뉴 +- 아침 메뉴 +- 점심 메뉴 +- 디저트 메뉴 +- 카페 메뉴 + +즉, 메뉴 안에 다시 메뉴가 포함되는 **트리 구조**가 된다. +이때는 메뉴 항목 하나와 메뉴 묶음을 같은 방식으로 다루기 위해 컴포지트 패턴이 필요하다. + +### 2.3 마지막 문제: 트리 구조 전체를 탐색해야 한다 + +컴포지트 패턴으로 트리 구조를 만들었다고 끝이 아니다. 이제는 전체 트리를 순회하면서 다음 작업을 해야 한다. + +- 전체 메뉴 출력 +- 채식 메뉴만 출력 +- 특정 조건의 메뉴 검색 + +이 단계에서 반복자 패턴이 다시 중요해진다. +즉, 헤드 퍼스트에서는 **반복자 → 컴포지트 → 반복자와 컴포지트의 결합**이라는 흐름으로 두 패턴을 연결한다. + +--- + +## 3. 반복자 패턴 + +### 3.1 정의 + +반복자 패턴은 컬렉션의 내부 표현을 노출하지 않으면서, 원소에 순차적으로 접근할 수 있는 방법을 제공하는 패턴이다. + +> 반복자 패턴은 집합체 객체의 내부 표현을 드러내지 않으면서 그 안의 모든 원소에 순차적으로 접근하는 방법을 제공한다. + +### 3.2 문제 상황 + +서로 다른 자료구조를 사용하는 메뉴들을 하나의 클라이언트 코드에서 출력한다고 가정하자. + +```typescript +for (let i = 0; i < breakfastItems.length; i++) { + console.log(breakfastItems[i].getName()); +} + +for (let i = 0; i < lunchItems.length; i++) { + console.log(lunchItems[i].getName()); +} +``` + +위 방식은 단순해 보이지만 다음과 같은 문제가 있다. + +- 클라이언트가 자료구조를 직접 알아야 한다 +- 순회 코드가 여러 곳에 중복된다 +- 자료구조가 바뀌면 클라이언트도 함께 수정된다 +- 컬렉션 캡슐화가 깨진다 + +### 3.3 해결 방식 + +컬렉션이 직접 내부 데이터를 노출하는 대신, **반복자 객체**를 반환하게 만든다. + +```typescript +interface Iterator { + hasNext(): boolean; + next(): T; +} + +interface Menu { + createIterator(): Iterator; +} +``` + +이제 클라이언트는 반복자 인터페이스만 알면 된다. + +```typescript +const iterator = menu.createIterator(); + +while (iterator.hasNext()) { + const item = iterator.next(); + console.log(item.getName()); +} +``` + +### 3.4 핵심 장점 + +- 저장 구조와 순회 로직을 분리할 수 있다 +- 클라이언트가 자료구조에 의존하지 않는다 +- 새로운 컬렉션 타입이 추가되어도 같은 방식으로 다룰 수 있다 + +--- + +## 4. 컴포지트 패턴 + +### 4.1 정의 + +컴포지트 패턴은 객체를 트리 구조로 구성해 부분-전체 계층을 표현하고, 개별 객체와 복합 객체를 동일하게 다룰 수 있도록 하는 패턴이다. + +> 컴포지트 패턴은 객체를 트리 구조로 구성하여 부분-전체 계층을 표현하며, 클라이언트가 개별 객체와 복합 객체를 일관된 방식으로 다룰 수 있게 한다. + +### 4.2 문제 상황 + +메뉴 시스템이 단순 목록이 아니라 계층 구조를 갖는다고 가정하자. + +- `MenuItem`: 실제 판매 항목 +- `Menu`: 메뉴 항목이나 하위 메뉴를 담는 객체 + +만약 두 타입을 완전히 다르게 다뤄야 한다면, 클라이언트는 계속 타입을 분기해야 한다. + +```typescript +if (component instanceof MenuItem) { + component.print(); +} else if (component instanceof Menu) { + for (const child of component.getChildren()) { + child.print(); + } +} +``` + +이 방식은 구조가 깊어질수록 복잡해진다. + +- 항목인지 메뉴인지 계속 구분해야 한다 +- 재귀 탐색 코드가 클라이언트로 새어나간다 +- 출력, 검색, 필터링마다 비슷한 분기문이 반복된다 + +### 4.3 해결 방식 + +`MenuItem`과 `Menu`를 공통 상위 타입 `MenuComponent`로 묶는다. + +```typescript +abstract class MenuComponent { + add(component: MenuComponent): void { + throw new Error("Unsupported"); + } + + print(): void { + throw new Error("Unsupported"); + } +} +``` + +- `MenuItem`은 더 이상 내려갈 수 없는 **Leaf** +- `Menu`는 자식을 포함하는 **Composite** + +이제 클라이언트는 둘을 구분하지 않고 같은 타입으로 다룰 수 있다. + +```typescript +class Waitress { + constructor(private allMenus: MenuComponent) {} + + printMenu(): void { + this.allMenus.print(); + } +} +``` + +### 4.4 핵심 장점 + +- 개별 객체와 복합 객체를 동일하게 다룰 수 있다 +- 트리 구조를 자연스럽게 표현할 수 있다 +- 클라이언트 코드의 분기를 줄일 수 있다 + +--- + +## 5. 두 패턴의 관계 + +두 패턴은 목적이 다르다. + +| 항목 | 반복자 패턴 | 컴포지트 패턴 | +| --------- | ------------------------------------- | ------------------------------------------ | +| 초점 | 순회 방식 통일 | 구조 통일 | +| 해결 문제 | 서로 다른 컬렉션을 같은 방식으로 탐색 | 개별 객체와 그룹 객체를 같은 방식으로 처리 | +| 대표 질문 | "어떻게 순회할 것인가?" | "어떻게 같은 타입으로 다룰 것인가?" | +| 결과 | 클라이언트가 자료구조를 몰라도 됨 | 클라이언트가 객체 종류를 몰라도 됨 | + +하지만 실제 설계에서는 자주 함께 쓰인다. + +### 5.1 컴포지트 패턴이 구조를 만든다 + +```text +ALL MENUS +├── PANCAKE HOUSE MENU +│ ├── K&B's Pancake Breakfast +│ └── Regular Pancake Breakfast +├── DINER MENU +│ ├── Vegetarian BLT +│ └── DESSERT MENU +│ ├── Apple Pie +│ └── Cheesecake +└── CAFE MENU +``` + +이 구조에서는 `Menu`와 `MenuItem`을 모두 `MenuComponent`로 다룰 수 있다. + +### 5.2 반복자 패턴이 그 구조를 순회한다 + +트리 구조가 만들어지면 전체 메뉴를 탐색해야 한다. + +```typescript +const iterator = allMenus.createIterator(); + +while (iterator.hasNext()) { + const component = iterator.next(); + if (component.isVegetarian()) { + component.print(); + } +} +``` + +이 코드는 클라이언트가 내부 트리 구조를 몰라도 전체를 탐색할 수 있게 해준다. + +### 5.3 핵심 정리 + +- 컴포지트 패턴은 **부분과 전체를 같은 인터페이스로 묶는다** +- 반복자 패턴은 **그 구조를 외부에 노출하지 않고 순회하게 한다** + +즉, 두 패턴은 같이 사용할 때 설계가 더 단순해진다. diff --git "a/jihyeon/10.\354\203\201\355\203\234_\355\214\250\355\204\264.md" "b/jihyeon/10.\354\203\201\355\203\234_\355\214\250\355\204\264.md" new file mode 100644 index 0000000..61982b5 --- /dev/null +++ "b/jihyeon/10.\354\203\201\355\203\234_\355\214\250\355\204\264.md" @@ -0,0 +1,386 @@ +# 상태 패턴 (State Pattern) + +- 객체의 내부 상태가 바뀔 때, 그에 따라 객체의 행동도 달라지도록 만드는 패턴 +- 겉으로 보면 객체의 클래스가 바뀐 것처럼 보이지만, 실제로는 현재 상태를 나타내는 객체에 작업을 위임하는 방식으로 동작 + +## 예시 + +껌볼 머신(Gumball Machine) 예제 + +- 처음에는 상태를 정수 상수로 표현하고 하나의 클래스 안에서 `if/else`로 모든 동작을 분기한다. +- 하지만 상태가 늘어나면 조건문이 커지고, 결국 상태별 행동을 상태 객체로 분리하게 된다. + +> 상태를 값으로만 두지 않고, 행동을 가진 객체로 바꾼다. + +상태 패턴은 객체의 내부 상태가 바뀌면 그에 따라 객체의 행동도 바뀌도록 한다. 따라서 객체는 마치 클래스를 바꾼 것처럼 보이게 된다. + +#### 자판기 예시 + +- 동전을 넣지 않은 상태에서는 손잡이를 돌려도 아무 일도 일어나지 않는다. +- 동전을 넣은 상태에서는 손잡이를 돌리면 상품이 나온다. +- 재고가 없는 상태에서는 동전을 받을 수도 없다. + +같은 자판기라도 현재 상태에 따라 같은 입력에 대한 반응이 달라진다. + +## 프론트엔드 관점 + +폼 제출 버튼 + +- `idle` 상태에서는 제출 가능 +- `submitting` 상태에서는 중복 제출 금지 +- `success` 상태에서는 완료 메시지 표시 +- `error` 상태에서는 재시도 가능 + +같은 버튼 클릭이라도 현재 상태에 따라 다른 행동을 해야 한다. +이런 문제를 단순한 조건문으로만 관리하면 금방 복잡해지고, 이때 상태 패턴의 아이디어가 유용하다. + +## 문제 상황 + +```tsx +const [status, setStatus] = useState< + "idle" | "submitting" | "success" | "error" +>("idle"); + +async function handleSubmit() { + if (status === "submitting") return; + + try { + setStatus("submitting"); + await submitForm(); + setStatus("success"); + } catch { + setStatus("error"); + } +} +``` + +그리고 JSX 안에서도 상태 분기가 반복된다. + +```tsx + +``` + +처음에는 단순하지만, 여기에 다음 조건이 붙기 시작하면 금방 복잡해진다. + +- 상태별 버튼 텍스트 +- 상태별 설명 문구 +- 상태별 비활성화 여부 +- 중복 클릭 방지 +- 에러 메시지 처리 +- 재시도 로직 +- 특정 상태에서 입력 잠금 + +"상태에 따라 행동이 달라지는 문제"는 매우 흔하다. + +## 해결책 + +#### 기존 + +- 지금 `loading`인가? +- 지금 `error`인가? +- 지금 `success`인가? +- 지금 버튼을 눌러도 되는가? + +### 상태 패턴 적용 + +- `IdleState`는 제출 가능 +- `SubmittingState`는 중복 제출 차단 +- `SuccessState`는 완료 상태 유지 +- `ErrorState`는 재시도 허용 + +즉, 컴포넌트가 모든 분기를 아는 대신 현재 상태 객체가 상태별 규칙을 가지게 된다. + +## 프론트엔드에서 상태 패턴 사용 방식 + +프론트엔드에서는 백엔드 예제처럼 클래스를 많이 만드는 방식보다, 상태별 객체에 UI 정책과 행동을 모아두는 방식이 더 자주 쓰인다. + +- 상태를 단순 문자열로만 두지 않고 +- 상태별 동작을 분리하고 +- 현재 상태에 위임한다 + +### 대표적인 프론트엔드 사례 + +상태 패턴은 다음과 같은 문제에 잘 맞는다. + +- 폼 제출 상태: `idle`, `submitting`, `success`, `error` +- 업로드 상태: `idle`, `uploading`, `paused`, `completed`, `failed` +- 플레이어 상태: `stopped`, `playing`, `paused`, `buffering` +- 연결 상태: `disconnected`, `connecting`, `connected`, `retrying` +- 주문 상태 UI: `draft`, `submitted`, `approved`, `cancelled` + +공통점은 하나이다. + +> 같은 이벤트라도 현재 상태에 따라 허용되는 행동이 다르다. + +## React 예시: 폼 제출 상태 + +아래 예시는 React에서 상태 패턴의 아이디어를 적용한 형태이다. + +```tsx +import React, { useMemo, useState } from "react"; + +type FormStatus = "idle" | "submitting" | "success" | "error"; + +type FormContext = { + setStatus: React.Dispatch>; + setErrorMessage: React.Dispatch>; + submitForm: () => Promise; +}; + +type FormState = { + buttonText: string; + disabled: boolean; + helperText: string; + onSubmit: (ctx: FormContext) => Promise; +}; + +const formStates: Record = { + idle: { + buttonText: "저장", + disabled: false, + helperText: "입력한 내용을 저장할 수 있어요.", + async onSubmit(ctx) { + try { + ctx.setErrorMessage(""); + ctx.setStatus("submitting"); + await ctx.submitForm(); + ctx.setStatus("success"); + } catch { + ctx.setErrorMessage("저장 중 오류가 발생했어요."); + ctx.setStatus("error"); + } + }, + }, + + submitting: { + buttonText: "저장 중...", + disabled: true, + helperText: "잠시만 기다려 주세요.", + async onSubmit() {}, + }, + + success: { + buttonText: "저장 완료", + disabled: true, + helperText: "정상적으로 저장되었어요.", + async onSubmit() {}, + }, + + error: { + buttonText: "다시 시도", + disabled: false, + helperText: "실패했어요. 다시 시도할 수 있어요.", + async onSubmit(ctx) { + try { + ctx.setErrorMessage(""); + ctx.setStatus("submitting"); + await ctx.submitForm(); + ctx.setStatus("success"); + } catch { + ctx.setErrorMessage("다시 시도했지만 저장에 실패했어요."); + ctx.setStatus("error"); + } + }, + }, +}; + +async function fakeSubmit(): Promise { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + if (Math.random() < 0.5) { + throw new Error("submit failed"); + } +} + +export default function ProfileForm() { + const [status, setStatus] = useState("idle"); + const [errorMessage, setErrorMessage] = useState(""); + + const currentState = useMemo(() => formStates[status], [status]); + + const ctx: FormContext = { + setStatus, + setErrorMessage, + submitForm: fakeSubmit, + }; + + return ( +
+ + + + +

{currentState.helperText}

+ + {status === "error" && errorMessage ? ( +

{errorMessage}

+ ) : null} +
+ ); +} +``` + +### 이 코드가 상태 패턴스러운 이유 + +핵심은 `formStates[status]`가 현재 상태 객체 역할을 한다는 점이다. + +```ts +const currentState = formStates[status]; +``` + +그리고 버튼 클릭 시 컴포넌트가 직접 조건문을 처리하는 대신, 현재 상태 객체의 `onSubmit()`을 호출한다. + +```ts +currentState.onSubmit(ctx); +``` + +즉, 컴포넌트는 현재 상태 객체에 처리를 위임하는 셈이다. + +> "지금 상태에 따라 내가 직접 분기하지 않을게. 현재 상태가 알아서 처리해." + +이것이 바로 상태 패턴의 핵심 감각이다. + +### 프론트엔드에서의 장점 + +이 방식은 다음 문제를 줄여준다. + +- JSX 안에 퍼진 상태별 문구 조건문 +- 클릭 핸들러 안의 반복 분기 +- 중복 제출 방지 로직 흩어짐 +- 에러/재시도 정책 중복 + +예를 들어 기존에는 버튼 렌더링이 이렇게 될 수 있다. + +```tsx + +``` + +상태별 객체로 정리하면 이렇게 단순해진다. + +```tsx + +``` + +즉, 렌더링과 행동이 상태별 정책으로 묶인다. + +### 프론트엔드에서는 클래스보다 객체 리터럴이 더 자연스러울 수 있다 + +헤드 퍼스트의 상태 패턴은 클래스를 중심으로 설명한다. 하지만 React와 TypeScript에서는 아래처럼 더 단순한 형태로 구현하는 경우가 많다. + +- 클래스 대신 상태별 객체 리터럴 사용 +- `Context` 대신 훅 상태와 setter 전달 +- 상태 전이를 `setStatus()`로 수행 + +구조는 다소 달라 보여도 핵심은 같다. + +- 상태별 행동을 분리하고 +- 현재 상태에 위임한다 + +--- + +## 장단점 + +### 장점 + +| 장점 | 설명 | +| ------------------------- | ---------------------------------------------------------- | +| 조건문 제거 | 거대한 `if/else` 또는 `switch`를 상태 클래스로 분산 가능 | +| 응집도 향상 | 특정 상태의 행동과 전이가 한곳에 모임 | +| 확장 용이 | 새 상태를 별도 클래스로 또는 별도 객체로 추가 가능 | +| 가독성 향상 | "이 상태에서 무엇을 하는가"를 바로 찾기 쉬움 | +| 프론트엔드 정책 정리 용이 | 버튼 문구, `disabled`, 재시도 규칙 등을 상태별로 묶기 쉬움 | + +### 단점 + +| 단점 | 설명 | +| ------------------------------------------ | -------------------------------------------------------------------- | +| 클래스 수 증가 | 전통적 구현에서는 상태마다 별도 클래스가 필요 | +| 작은 문제엔 과할 수 있음 | 단순 `isLoading` 수준이면 오히려 복잡해질 수 있음 | +| 전이 추적 필요 | 상태가 많아지면 흐름을 별도로 정리해야 이해가 쉬움 | +| React에서는 클래스가 무겁게 느껴질 수 있음 | 실무에서는 객체 리터럴, reducer, 상태 머신이 더 자연스러울 때도 있음 | + +--- + +## 사용 시점 + +다음과 같은 경우 상태 패턴을 고려할 수 있다. + +1. 객체의 행동이 내부 상태에 따라 크게 달라질 때 +2. 하나의 클래스나 컴포넌트 안에 상태 분기문이 반복될 때 +3. 상태 전이가 복잡하고 자주 바뀔 가능성이 있을 때 +4. 상태별 책임 분리가 조건문보다 더 읽기 쉬울 때 + +### 대표 예시 + +- 주문 상태: 결제대기, 결제완료, 배송중, 배송완료, 취소 +- 문서 워크플로: 초안, 검토중, 승인, 게시 +- 연결 상태: `Disconnected`, `Connecting`, `Connected`, `Retrying` +- 게임 캐릭터 상태: `Idle`, `Running`, `Jumping`, `Damaged` +- 폼 제출 UI: `idle`, `submitting`, `success`, `error` +- 업로드 UI: `idle`, `uploading`, `paused`, `completed`, `failed` + +### 굳이 쓰지 않아도 되는 경우 + +다음처럼 상태 차이가 매우 단순하면 상태 패턴까지 갈 필요는 없다. + +```ts +const isLoading = true; +const buttonLabel = isLoading ? "로딩 중..." : "저장"; +``` + +이런 경우는 boolean이나 간단한 enum만으로도 충분하다. + +--- + +## 관련 패턴 + +### 상태 패턴 vs 전략 패턴 + +두 패턴 모두 합성과 위임을 사용하고 구조도 비슷하다. 하지만 의도는 다르다. + +| 패턴 | 초점 | +| --------- | ------------------------------------------ | +| 전략 패턴 | 알고리즘을 교체 가능하게 만드는 것 | +| 상태 패턴 | 상태 변화에 따라 행동이 달라지게 만드는 것 | + +전략 패턴은 보통 클라이언트가 어떤 전략을 사용할지 선택한다. 반면 상태 패턴은 현재 상태 객체가 다음 상태를 결정하는 경우가 많다. + +### 상태 패턴 vs reducer + +프론트엔드에서는 상태 패턴 대신 `useReducer`를 쓸 때도 많다. + +- 상태 패턴: 상태별 책임 중심 +- reducer: 이벤트와 전이 중심 + +즉, + +- 상태 패턴은 "지금 이 상태라면 어떻게 행동하지?" +- reducer는 "이 액션이 들어오면 다음 상태가 뭐지?" + +를 더 잘 표현한다. + +### 상태 패턴 vs 상태 머신(FSM) + +상태 패턴은 상태를 객체로 나눠 책임을 분리하는 데 강하다. 반면 상태 머신은 전이 규칙을 더 명시적으로 모델링하는 데 강하다. + +상태가 아주 복잡하고 전이 시각화가 중요하다면 XState 같은 상태 머신 도구가 더 적합할 수 있다.