diff --git "a/jina/03.\353\215\260\354\275\224\353\240\210\354\235\264\355\204\260_\355\214\250\355\204\264.md" "b/jina/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..43b4971 --- /dev/null +++ "b/jina/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,422 @@ +# 데코레이터 패턴 + +## 개념 + +객체에 새로운 책임(기능)을 동적으로 추가할 수 있도록 하는 패턴 상속 대신 **합성(Composition)**을 사용 + +### 핵심 개념 + +- 기존 객체를 감싼다 (Wrap) +- 같은 인터페이스를 유지한다 +- 기능을 덧붙인다 + +## 데코레이터 패턴의 장점 + +### 1. 상속보다 유연한 기능 확장 + +상속은 컴파일 타임에 정적으로 결정되지만, 데코레이터는 런타임에 동적으로 기능을 조합 가능 + +### 2. 클래스 폭발 문제 해결 + +첨가물 조합마다 클래스를 만들 필요 없음 + +- 상속 방식: 커피 2개 × 첨가물 4개 = 16개 클래스 필요 +- 데코레이터 방식: 커피 2개 + 첨가물 4개 = 6개 클래스만 필요 + +### 3. 개방-폐쇄 원칙(OCP) 준수 + +기존 코드 수정 없이 새로운 기능(데코레이터) 추가 가능 + +```typescript +// 새로운 첨가물 추가 시 기존 코드 수정 불필요 +class Caramel extends CondimentDecorator { + getDescription(): string { + return this.beverage.getDescription() + ', 카라멜'; + } + cost(): number { + return this.beverage.cost() + 0.25; + } +} +``` + +### 4. 단일 책임 원칙(SRP) 준수 + +각 데코레이터는 하나의 기능만 담당 + +### 5. 런타임 동적 조합 + +실행 시점에 필요한 기능만 선택하여 조합 가능 + +```typescript +// 고객의 주문에 따라 동적으로 첨가물 추가 +let order = new Espresso(); +if (customer.wantsMocha) order = new Mocha(order); +if (customer.wantsWhip) order = new Whip(order); +``` + +### 6. 기능의 순서 제어 가능 + +데코레이터를 감싸는 순서에 따라 동작 순서를 제어 가능 + +## 커피 주문 시스템 만들기 + +```typescript +abstract class Beverage_old { + description: string = '제목 없음'; + + getDescription(): string { + return this.description; + } + abstract cost(): number; +} + +class Espresso_old extends Beverage_old { + description = '에스프레소'; + + cost(): number { + return 1.99; + } +} + +class HouseBlend_old extends Beverage_old { + description = '하우스 블렌드 커피'; + + cost(): number { + return 0.89; + } +} + +class EspressoWithWhip extends Beverage_old { + description = '에스프레소 휘핑크림'; + + cost(): number { + return 2.19; + } +} + +class HouseBlendWithWhip extends Beverage_old { + description = '하우스 블렌드 커피 휘핑크림'; + + cost(): number { + return 1.09; + } +} + +// 커피 + 서브주문(우유/두유/s모카등을 추가) class를 계속 생성 +// 문제: 커피 종류가 추가될수록 클래스가 계속 추가되어 코드가 복잡해짐 +``` + +### 인스턴스 변수와 슈퍼클래스 상속을 사용해서 첨가물을 관리하는 방식으로 변경 + +```typescript +abstract class Beverage_02 { + description: string = '제목 없음'; + milk: boolean = false; + soy: boolean = false; + mocha: boolean = false; + whip: boolean = false; + + getDescription(): string { + return this.description; + } + cost(): number { + let cost = 0; + if (this.hasMilk()) { + cost += 0.15; + } + if (this.hasSoy()) { + cost += 0.1; + } + if (this.hasMocha()) { + cost += 0.2; + } + if (this.hasWhip()) { + cost += 0.1; + } + return cost; + } + + hasMilk(): boolean { + return this.milk; + } + hasSoy(): boolean { + return this.soy; + } + hasMocha(): boolean { + return this.mocha; + } + hasWhip(): boolean { + return this.whip; + } + setMilk(milk: boolean) { + this.milk = milk; + } + setSoy(soy: boolean) { + this.soy = soy; + } + setMocha(mocha: boolean) { + this.mocha = mocha; + } + setWhip(whip: boolean) { + this.whip = whip; + } +} + +class Espresso_02 extends Beverage_02 { + description = '에스프레소'; + + cost(): number { + return super.cost() + 1.99; + } +} + +class HouseBlend_02 extends Beverage_02 { + description = '하우스 블렌드 커피'; + + cost(): number { + return super.cost() + 0.89; + } +} + +const espresso_02 = new Espresso_02(); +espresso_02.setMilk(true); +espresso_02.setWhip(true); +console.log(espresso_02.getDescription() + ' $' + espresso_02.cost()); + +const houseBlend_02 = new HouseBlend_02(); +houseBlend_02.setWhip(true); +console.log(houseBlend_02.getDescription() + ' $' + houseBlend_02.cost()); +``` + +### 데코레이터 패턴으로 수정 + +```typescript +//추상 구성요소 +abstract class Beverage { + description: string = '제목 없음'; + + getDescription(): string { + return this.description; + } + abstract cost(): number; +} + +// 첨가물 추상 클래스 +abstract class CondimentDecorator extends Beverage { + beverage: Beverage; + + constructor(beverage: Beverage) { + super(); + this.beverage = beverage; + } + + abstract getDescription(): string; + abstract cost(): number; +} + +// 음료코드(구상 구성요소) +class Espresso extends Beverage { + description = '에스프레소'; + + cost(): number { + return 1.99; + } +} + +class HouseBlend extends Beverage { + description = '하우스 블렌드 커피'; + + cost(): number { + return 0.89; + } +} + +//첨가물 코드(데코레이터) +class Mocha extends CondimentDecorator { + constructor(beverage: Beverage) { + super(beverage); + } + + getDescription(): string { + return this.beverage.getDescription() + ', 모카'; + } + + cost(): number { + return Math.round((this.beverage.cost() + 0.2) * 100) / 100; + } +} + +class Whip extends CondimentDecorator { + constructor(beverage: Beverage) { + super(beverage); + } + + getDescription(): string { + return this.beverage.getDescription() + ', 휘핑크림'; + } + + cost(): number { + return Math.round((this.beverage.cost() + 0.1) * 100) / 100; + } +} + +class Soy extends CondimentDecorator { + constructor(beverage: Beverage) { + super(beverage); + } + + getDescription(): string { + return this.beverage.getDescription() + ', 두유'; + } + + cost(): number { + return Math.round((this.beverage.cost() + 0.15) * 100) / 100; + } +} + +const beverage = new Espresso(); +console.log(beverage.getDescription() + ' $' + beverage.cost()); + +let beverage2 = new HouseBlend(); +beverage2 = new Mocha(beverage2); +console.log(beverage2.getDescription() + ' $' + beverage2.cost()); +beverage2 = new Mocha(beverage2); +console.log(beverage2.getDescription() + ' $' + beverage2.cost()); +beverage2 = new Whip(beverage2); +console.log(beverage2.getDescription() + ' $' + beverage2.cost()); +``` + +### 데코레이터 패턴의 구조 + +```uml + ┌────────────────────┐ + │ Component │◀───────────────┐ + │────────────────────│ │ + │ + operation() │ │ + └────────────────────┘ │ + ▲ │ + │ implements │ implements + ┌────────────────────┐ │ + │ ConcreteComponent │ │ + │────────────────────│ │ + │ + operation() │ │ + └────────────────────┘ │ + │ + ┌────────────────────┐ + │ Decorator │ + │────────────────────│ + │ - component │─────┐ + │ + operation() │ │ has-a + └────────────────────┘ │ + ▲ │ + │ extends │ + ┌────────────────────┐ │ + │ ConcreteDecorator │◀────┘ + │────────────────────│ + │ + operation() │ + └────────────────────┘ +``` + +#### Component (공통 인터페이스) + +모든 객체가 따라야 하는 공통 규격 + +```typescript +interface Component { + operation(): string; +} +``` + +#### ConcreteComponent (기본 객체) + +기본 기능을 수행하는 실제 객체 + +```typescript +class ConcreteComponent implements Component { + operation() { + return '기본 기능'; + } +} +``` + +#### Decorator (추상 데코레이터) + +Component를 내부에 가지고 있음 (합성) +동일한 인터페이스를 구현함 + +```typescript +class Decorator implements Component { + protected component: Component; + + constructor(component: Component) { + this.component = component; + } + + operation() { + return this.component.operation(); + } +} +``` + +#### ConcreteDecorator (실제 기능 추가) + +기능이 추가되는곳 + +```typescript +class LoggingDecorator extends Decorator { + operation() { + console.log('로깅 시작'); + const result = super.operation(); + console.log('로깅 종료'); + return result; + } +} +``` + +## 프론트엔드 데코레이터 패턴 예시 + +### HOC (High Order Component) + +```typescript +function withAuth(Component: React.ComponentType) { + return function AuthWrapped(props: any) { + const isLoggedIn = useAuth(); + + if (!isLoggedIn) { + return ; + } + + return ; + }; +} + +export default withAuth(MyPage); +``` + +### 함수 데코레이터 (로깅 추가) + +```typescript +function withLogging any>(fn: T): T { + return ((...args: any[]) => { + console.log('호출 시작'); + const result = fn(...args); + console.log('호출 종료'); + return result; + }) as T; +} + +function fetchUser() { + return fetch('/api/user'); +} + +const fetchUserWithLogging = withLogging(fetchUser); +``` + +### Axios Interceptor + +```typescript +axios.interceptors.request.use((config) => { + config.headers.Authorization = 'Bearer token'; + return config; +}); +``` diff --git "a/jina/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264.md" "b/jina/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264.md" new file mode 100644 index 0000000..babd9b3 --- /dev/null +++ "b/jina/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264.md" @@ -0,0 +1,186 @@ +# 팩토리 패턴 + +## 개념 + +팩토리 패턴은 객체 생성 로직을 한곳에 모아서 캡슐화하는 패턴이다. +핵심은 "무엇을 만들지"를 클라이언트 코드에서 분리하는 것이다. + +`new` 자체가 문제는 아니지만, 생성해야 하는 구상 클래스가 자주 바뀌는 상황에서는 `new`가 많아질수록 변경 지점이 폭발한다. +팩토리를 도입하면 생성 책임이 분리되어 변경에 유연해진다. + +## 구조 + +**Product** + +생성되는 객체의 공통 타입 (`Pizza`) + +**Concrete Product** + +실제 생성 대상 (`NYStyleCheesePizza`, `ChicagoStyleCheesePizza` 등) + +**Creator** + +객체 생성 메서드를 선언하고, 공통 흐름을 관리 (`PizzaStore_frameowrk`) + +**Concrete Creator** + +어떤 구상 객체를 만들지 결정 (`NYPizzaStore`, `ChicagoPizzaStore`) + +```txt +PizzaStore_frameowrk (Creator) + ├─ orderPizza() // 공통 처리: prepare/bake/cut/box + └─ createPizza() // 생성 책임(서브클래스에 위임) + +NYPizzaStore / ChicagoPizzaStore (Concrete Creator) + └─ createPizza() 구현 + +Pizza (Product) + └─ NYStyleCheesePizza, ChicagoStyleCheesePizza ... (Concrete Product) +``` + +> "어떻게 처리할지"는 Creator가 알고, "무엇을 만들지"는 Concrete Creator가 결정한다. + +## 예시(Class형) + +피자 가게 프레임워크 구현 + +1. Product 추상 클래스 + +```ts +abstract class Pizza { + prepare() {} + bake() {} + cut() {} + box() {} +} +``` + +2. Concrete Product + +```ts +class NYStyleCheesePizza extends Pizza {} +class ChicagoStyleCheesePizza extends Pizza { + cut() { + console.log("피자를 네모난 모양으로 자르기"); + } +} +``` + +3. Creator + +```ts +abstract class PizzaStore_frameowrk { + orderPizza(type: string) { + const pizza = this.createPizza(type); + pizza.prepare(); + pizza.bake(); + pizza.cut(); + pizza.box(); + return pizza; + } + + abstract createPizza(type: string): Pizza; +} +``` + +4. Concrete Creator + +```ts +class NYPizzaStore extends PizzaStore_frameowrk { + createPizza(type: string): Pizza { + if (type === "cheese") return new NYStyleCheesePizza(); + throw new Error(`지원하지 않는 피자 타입입니다: ${type}`); + } +} +``` + +5. 사용 + +```ts +const nyStore = new NYPizzaStore(); +const pizza = nyStore.orderPizza("cheese"); +console.log(pizza.getName()); +``` + +## 단순 팩토리 vs 팩토리 메서드 + +**단순 팩토리(Simple Factory)** + +객체 생성 전담 클래스(`SimplePizzaFactory`)를 별도로 두고, 클라이언트(`PizzaStore`)는 생성만 위임한다. + +**팩토리 메서드(Factory Method)** + +생성 메서드(`createPizza`)를 Creator 추상 클래스에 두고, 서브클래스가 구상 객체 생성을 결정한다. + +확장 포인트가 더 명확해서 지점/지역별 제품군 확장에 유리하다. + +## 추상 팩토리 패턴 + +추상 팩토리 패턴은 서로 연관된 객체들의 "제품군"을 생성하기 위한 인터페이스를 제공한다. +클라이언트는 구상 클래스를 직접 지정하지 않고, 팩토리 인터페이스를 통해 일관된 조합의 객체를 생성한다. + +예를 들어 피자 도메인에서 도우/소스/치즈를 각각 따로 `new` 하지 않고, +`NYPizzaIngredientFactory`, `ChicagoPizzaIngredientFactory` 같은 팩토리를 통해 같은 스타일의 재료군을 한 번에 받는 방식이다. + +### 구조 + +**Abstract Factory** + +제품군 생성 메서드 선언 (`createDough`, `createSauce`, `createCheese`) + +**Concrete Factory** + +스타일별 제품군 생성 (`NYPizzaIngredientFactory`, `ChicagoPizzaIngredientFactory`) + +**Abstract Product** + +제품군의 공통 타입 (`Dough`, `Sauce`, `Cheese`) + +**Concrete Product** + +실제 제품 (`ThinCrustDough`, `MarinaraSauce`, `ReggianoCheese` 등) + +```txt +Pizza (Client) + └─ PizzaIngredientFactory (Abstract Factory) + ├─ createDough(): Dough + ├─ createSauce(): Sauce + └─ createCheese(): Cheese + +NYPizzaIngredientFactory / ChicagoPizzaIngredientFactory (Concrete Factory) + └─ 지역 스타일에 맞는 재료군 생성 +``` + +### 팩토리 메서드와 차이 + +- 팩토리 메서드: "하나의 제품" 생성 책임을 서브클래스에 위임 +- 추상 팩토리: "관련된 여러 제품군"을 일관된 조합으로 생성 + +실무에서는 두 패턴을 함께 쓰는 경우가 많다. +예: `PizzaStore`는 팩토리 메서드로 피자 타입을 고르고, 피자 내부 재료는 추상 팩토리로 구성한다. + +## 언제 사용하면 좋은가? + +**✔ 생성 대상이 자주 바뀔 때** + +조건문(if/switch)으로 객체를 고르는 코드가 자주 수정됨 + +**✔ 공통 처리 흐름은 같고 생성만 달라질 때** + +`orderPizza`의 prepare/bake/cut/box는 고정, 피자 종류만 변경 + +**✔ 확장 가능한 구조가 필요할 때** + +`NYPizzaStore`, `ChicagoPizzaStore`처럼 지점별 정책을 독립 확장 + +**✔ 제품 간 호환성을 보장해야 할 때** + +뉴욕 스타일 피자에는 뉴욕 스타일 도우/소스/치즈처럼, 함께 쓰이는 객체 조합을 강제해야 함 + +## 의존성 뒤집기 원칙(DIP)와의 연결 + +고수준 모듈(`PizzaStore`)이 저수준 구상 클래스에 직접 의존하지 않고, 추상화(`Pizza`)에 의존하게 만든다. + +- 구상 타입 참조를 줄인다 +- 생성 책임을 캡슐화한다 +- 변경이 생겨도 클라이언트 코드는 닫혀 있게 만든다