diff --git "a/jihyeon/03.\353\215\260\354\275\224\353\240\210\354\235\264\355\204\260_\355\214\250\355\204\264.md" "b/jihyeon/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..d37349e --- /dev/null +++ "b/jihyeon/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,811 @@ +# 데코레이터 패턴 완벽 가이드: 객체에 동적 기능 부여하기 + +> 상속 없이 객체에 새로운 기능을 추가할 수 있는 방법 + +## 문제 상황 + +### 커피숍 주문 시스템 예시 + +커피숍 주문 시스템을 개발한다고 가정한다. + +``` +기본 음료: 에스프레소, 다크로스트, 하우스블렌드 +추가 옵션: 모카, 휘핑, 두유, 우유 +``` + +### 상속으로 접근할 경우 + +``` +Espresso +├── EspressoWithMocha +├── EspressoWithWhip +├── EspressoWithMochaAndWhip +├── EspressoWithMochaAndWhipAndSoy +└── ... (조합 폭발) +``` + +5개 음료와 4개 토핑의 조합은 수십 개의 클래스를 필요로 한다. + +### 문제점 + +- 토핑 가격 변경 시 모든 관련 서브클래스 수정 필요 +- 새로운 토핑 추가 시 클래스 수 기하급수적 증가 +- 토핑 적용 순서 추적 불가 + +## 데코레이터 패턴이란 + +### 정의 + +> 객체에 동적으로 추가 책임을 부여하는 패턴. 서브클래싱보다 유연하게 기능을 확장할 수 있다. + +### 핵심 개념 + +객체를 감싸는(wrapping) 방식으로 기능을 추가한다. 선물 상자에 포장지를 덧씌우는 것과 유사하다. + +- 상자: 기본 객체 +- 포장지: 첫 번째 데코레이터 +- 리본: 두 번째 데코레이터 +- 카드: 세 번째 데코레이터 + +## 실생활 비유 + +### 피자 주문 + +``` +기본: 얇은 도우 피자 (10,000원) + ↓ ++ 치즈 토핑 (+2,000원) + ↓ ++ 올리브 토핑 (+1,000원) + ↓ +최종: 얇은 도우 + 치즈 + 올리브 (13,000원) +``` + +### 의상 착용 + +``` +사람 (기본) + ↓ +속옷 착용 (데코레이터 1) + ↓ +티셔츠 착용 (데코레이터 2) + ↓ +재킷 착용 (데코레이터 3) + ↓ +코트 착용 (데코레이터 4) +``` + +## 패턴 구조 + +### 구성 요소 + +``` +┌─────────────────────────────────────┐ +│ Component (추상) │ +│ + operation() │ +└──────────────┬──────────────────────┘ + │ + ┌───────┴───────┐ + │ │ +┌──────▼──────┐ ┌─────▼────────────┐ +│ Concrete │ │ Decorator │ +│ Component │ │ - component │ +│ │ │ + operation() │ +└─────────────┘ └────────┬─────────┘ + │ + ┌──────┴──────┐ + │ │ + ┌──────▼──────┐ ┌────▼──────┐ + │ ConcreteA │ │ ConcreteB │ + │ Decorator │ │ Decorator │ + └─────────────┘ └───────────┘ +``` + +### 역할 설명 + +| 역할 | 설명 | 예시 | +| ----------------- | ------------------------------ | ------------------- | +| Component | 추상 클래스 또는 인터페이스 | Beverage | +| ConcreteComponent | 기본 구현체 | DarkRoast, Espresso | +| Decorator | Component를 감싸는 추상 클래스 | CondimentDecorator | +| ConcreteDecorator | 실제 기능 추가 구현 | Mocha, Whip, Soy | + +## 코드 구현 + +### 1단계: Component 정의 + +```typescript +// Beverage.ts - 모든 음료의 기반 클래스 +export abstract class Beverage { + protected description: string = "Unknown Beverage"; + + getDescription(): string { + return this.description; + } + + abstract cost(): number; +} +``` + +### 2단계: ConcreteComponent 구현 + +```typescript +// DarkRoast.ts - 기본 다크로스트 커피 +export class DarkRoast extends Beverage { + constructor() { + super(); + this.description = "Dark Roast Coffee"; + } + + cost(): number { + return 0.99; + } +} + +// Espresso.ts - 에스프레소 +export class Espresso extends Beverage { + constructor() { + super(); + this.description = "Espresso"; + } + + cost(): number { + return 1.99; + } +} + +// HouseBlend.ts - 하우스 블렌드 +export class HouseBlend extends Beverage { + constructor() { + super(); + this.description = "House Blend Coffee"; + } + + cost(): number { + return 0.89; + } +} +``` + +### 3단계: Decorator 정의 + +```typescript +// CondimentDecorator.ts - 모든 토핑의 기반 클래스 +export abstract class CondimentDecorator extends Beverage { + protected beverage!: Beverage; + + abstract getDescription(): string; +} +``` + +### 4단계: ConcreteDecorator 구현 + +```typescript +// Mocha.ts - 모카 토핑 +export class Mocha extends CondimentDecorator { + constructor(beverage: Beverage) { + super(); + this.beverage = beverage; + } + + getDescription(): string { + return this.beverage.getDescription() + ", Mocha"; + } + + cost(): number { + return 0.2 + this.beverage.cost(); + } +} + +// Whip.ts - 휘핑 토핑 +export class Whip extends CondimentDecorator { + constructor(beverage: Beverage) { + super(); + this.beverage = beverage; + } + + getDescription(): string { + return this.beverage.getDescription() + ", Whip"; + } + + cost(): number { + return 0.1 + this.beverage.cost(); + } +} + +// Soy.ts - 두유 토핑 +export class Soy extends CondimentDecorator { + constructor(beverage: Beverage) { + super(); + this.beverage = beverage; + } + + getDescription(): string { + return this.beverage.getDescription() + ", Soy"; + } + + cost(): number { + return 0.15 + this.beverage.cost(); + } +} +``` + +### 5단계: 클라이언트 코드 + +```typescript +// StarbuzzCoffee.ts - 주문 시스템 +export class StarbuzzCoffee { + static main(): void { + console.log("=== Starbuzz Coffee Orders ===\n"); + + // 주문 1: 에스프레소 (기본) + let beverage1 = new Espresso(); + console.log(`${beverage1.getDescription()}`); + console.log(`$${beverage1.cost().toFixed(2)}\n`); + + // 주문 2: 다크로스트 + 모카 2샷 + 휘핑 + let beverage2 = new DarkRoast(); // $0.99 + beverage2 = new Mocha(beverage2); // +$0.20 + beverage2 = new Mocha(beverage2); // +$0.20 + beverage2 = new Whip(beverage2); // +$0.10 + // 총: $1.49 + + console.log(`${beverage2.getDescription()}`); + console.log(`$${beverage2.cost().toFixed(2)}\n`); + + // 주문 3: 하우스블렌드 + 두유 + 모카 + 휘핑 + let beverage3 = new HouseBlend(); // $0.89 + beverage3 = new Soy(beverage3); // +$0.15 + beverage3 = new Mocha(beverage3); // +$0.20 + beverage3 = new Whip(beverage3); // +$0.10 + // 총: $1.34 + + console.log(`${beverage3.getDescription()}`); + console.log(`$${beverage3.cost().toFixed(2)}\n`); + } +} + +StarbuzzCoffee.main(); +``` + +### 실행 결과 + +``` +=== Starbuzz Coffee Orders === + +Espresso +$1.99 + +Dark Roast Coffee, Mocha, Mocha, Whip +$1.49 + +House Blend Coffee, Soy, Mocha, Whip +$1.34 +``` + +### 동작 원리 분석 + +```typescript +// 주문 2의 동작 과정 +let beverage2 = new DarkRoast(); +// beverage2.cost() → 0.99 + +beverage2 = new Mocha(beverage2); +// beverage2.cost() → 0.20 + DarkRoast.cost() = 1.19 + +beverage2 = new Mocha(beverage2); +// beverage2.cost() → 0.20 + Mocha(DarkRoast).cost() = 1.39 + +beverage2 = new Whip(beverage2); +// beverage2.cost() → 0.10 + Mocha(Mocha(DarkRoast)).cost() = 1.49 +``` + +--- + +## 장단점 + +### 장점 + +| 장점 | 설명 | +| ------------------- | ------------------------------------------- | +| 유연한 확장 | 상속 없이 런타임에 기능 추가 및 제거 가능 | +| 조합의 자유로움 | 데코레이터들을 자유롭게 조합 가능 | +| 단일 책임 원칙 준수 | 각 데코레이터는 하나의 책임만 가짐 | +| 개방-폐쇄 원칙 준수 | 기존 코드 수정 없이 확장 가능 | +| 클래스 폭발 방지 | 5음료 × 4토핑 = 9클래스 (상속 시 20개 이상) | + +### 단점 + +| 단점 | 설명 | +| ---------------- | --------------------------------------- | +| 작은 클래스 다수 | 각 기능마다 별도 클래스 필요 | +| 복잡성 증가 | 데코레이터 스택 구조 이해 필요 | +| 디버깅 난이도 | 여러 레이어의 데코레이터 추적 필요 | +| 타입 정보 손실 | 구체 컴포넌트의 타입 확인 불가 | +| 객체 식별 문제 | 래핑된 객체는 원본과 다른 객체로 식별됨 | + +## 실무 활용 사례 + +### 1. Java I/O 스트림 + +```java +InputStream is = new BufferedInputStream( + new FileInputStream("file.txt") + ); +``` + +### 2. TypeScript 메서드 데코레이터 + +```typescript +function Log(target: any, key: string, descriptor: PropertyDescriptor) { + const original = descriptor.value; + + descriptor.value = async function (...args: any[]) { + console.log(`Calling ${key}`, args); + const result = await original.apply(this, args); + console.log(`Result:`, result); + return result; + }; + + return descriptor; +} + +class UserService { + @Log + async getUser(id: number) { + return fetch(`/users/${id}`); + } +} +``` + +### 3. NestJS 데코레이터 + +```typescript +@Controller("users") +@UseGuards(AuthGuard) +@UseInterceptors(LoggingInterceptor) +export class UsersController { + @Get() + @Roles("admin") + findAll() { + return this.usersService.findAll(); + } +} +``` + +### 4. Express 미들웨어 + +```typescript +app.use(cors()); +app.use(helmet()); +app.use(morgan("dev")); +app.use(express.json()); +``` + +## 프론트엔드 활용 예시 + +### 1. React HOC (Higher-Order Component) + +React의 HOC는 데코레이터 패턴의 대표적인 프론트엔드 구현이다. + +```tsx +// 기본 컴포넌트 +const UserProfile = ({ user }: { user: User }) =>
{user.name}
; + +// 인증 데코레이터 +function withAuth

(Component: React.ComponentType

) { + return function AuthenticatedComponent(props: P) { + const { user, isLoading } = useAuth(); + + if (isLoading) return ; + if (!user) return ; + + return ; + }; +} + +// 로깅 데코레이터 +function withLogging

(Component: React.ComponentType

) { + return function LoggedComponent(props: P) { + useEffect(() => { + console.log(`${Component.displayName} mounted`); + }, []); + + return ; + }; +} + +// 에러 바운더리 데코레이터 +function withErrorBoundary

( + Component: React.ComponentType

, +) { + return class ErrorBoundary extends React.Component { + state = { hasError: false }; + + static getDerivedStateFromError() { + return { hasError: true }; + } + + render() { + if (this.state.hasError) { + return ; + } + return ; + } + }; +} + +// 데코레이터 조합 +const EnhancedProfile = withErrorBoundary(withLogging(withAuth(UserProfile))); + +// 사용 +; +``` + +### 2. 권한 제어 데코레이터 + +```tsx +// 권한 데코레이터 +function withPermission

( + Component: React.ComponentType

, + requiredRole: string, +) { + return function PermissionCheckedComponent(props: P) { + const { user } = useAuth(); + + if (!user?.roles.includes(requiredRole)) { + return ; + } + + return ; + }; +} + +// 사용 예시 +const AdminPanel = withPermission(Panel, "admin"); +const ManagerDashboard = withPermission(Dashboard, "manager"); + +// 조합: 인증 + 권한 + 로깅 +const SecureAdminPanel = withLogging(withPermission(withAuth(Panel), "admin")); +``` + +### 3. 데이터 페칭 데코레이터 + +```tsx +// API 호출 데코레이터 +function withDataFetching

( + Component: React.ComponentType< + P & { data: any; loading: boolean; error: Error | null } + >, + fetchData: () => Promise, +) { + return function DataFetchWrapper( + props: Omit, + ) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchData() + .then(setData) + .catch(setError) + .finally(() => setLoading(false)); + }, []); + + return ( + + ); + }; +} + +// 사용 +const UserList = ({ data, loading, error }: Props) => { + if (loading) return ; + if (error) return ; + return ( +

+ ); +}; + +const EnhancedUserList = withDataFetching(UserList, () => + fetch("/api/users").then((r) => r.json()), +); +``` + +### 4. 스타일링 데코레이터 + +```tsx +// 스타일 데코레이터 (styled-components 패턴) +function withStyled

( + Component: React.ComponentType

, + styles: CSSProperties, +) { + const StyledComponent = styled(Component)

` + ${Object.entries(styles) + .map(([key, value]) => `${key}: ${value};`) + .join("\n")} + `; + + return StyledComponent; +} + +// 테마 데코레이터 +function withTheme

(Component: React.ComponentType

) { + return function ThemedComponent(props: P) { + const theme = useTheme(); + return ; + }; +} + +// 다크모드 데코레이터 +function withDarkMode

(Component: React.ComponentType

) { + return function DarkModeAware(props: P) { + const isDark = useMediaQuery("(prefers-color-scheme: dark)"); + return ; + }; +} +``` + +### 5. 성능 최적화 데코레이터 + +```tsx +// 메모이제이션 데코레이터 +function withMemo

( + Component: React.ComponentType

, + areEqual?: (prevProps: P, nextProps: P) => boolean, +) { + return React.memo(Component, areEqual); +} + +// 지연 로딩 데코레이터 +function withLazyLoading

( + importFn: () => Promise<{ default: React.ComponentType

}>, +) { + const LazyComponent = React.lazy(importFn); + + return function LazyWrapper(props: P) { + return ( + }> + + + ); + }; +} + +// 사용 +const HeavyChart = withLazyLoading(() => import("./HeavyChart")); +const MemoizedList = withMemo( + UserList, + (prev, next) => prev.items === next.items, +); +``` + +### 6. 폼 검증 데코레이터 + +```tsx +// 폼 검증 데코레이터 +function withValidation

( + Component: React.ComponentType

, + validators: Array<(value: string) => string | null>, +) { + return function ValidatedInput(props: P) { + const [error, setError] = useState(null); + + const handleChange = (value: string) => { + for (const validate of validators) { + const error = validate(value); + if (error) { + setError(error); + return; + } + } + setError(null); + props.onChange?.(value); + }; + + return ( +

+ + {error && {error}} +
+ ); + }; +} + +// 검증 함수들 +const required = (value: string) => (!value ? "필수 입력값입니다" : null); +const minLength = (min: number) => (value: string) => + value.length < min ? `${min}자 이상 입력하세요` : null; +const email = (value: string) => + !value.includes("@") ? "이메일 형식이 아닙니다" : null; + +// 사용 +const ValidatedEmailInput = withValidation(TextInput, [required, email]); + +const ValidatedPasswordInput = withValidation(TextInput, [ + required, + minLength(8), +]); +``` + +### 7. 분석 및 추적 데코레이터 + +```tsx +// 이벤트 추적 데코레이터 +function withTracking

( + Component: React.ComponentType

, + eventName: string, +) { + return function TrackedComponent(props: P) { + useEffect(() => { + analytics.track(`${eventName}_viewed`); + }, []); + + const handleClick = (e: React.MouseEvent) => { + analytics.track(`${eventName}_clicked`); + props.onClick?.(e); + }; + + return ; + }; +} + +// A/B 테스트 데코레이터 +function withABTest

( + ComponentA: React.ComponentType

, + ComponentB: React.ComponentType

, + testId: string, +) { + return function ABTestWrapper(props: P) { + const variant = useABTestVariant(testId); + return variant === "A" ? ( + + ) : ( + + ); + }; +} + +// 사용 +const TrackedButton = withTracking(SubmitButton, "submit_button"); +const TestedHero = withABTest(HeroA, HeroB, "hero_test"); +``` + +### 8. 실제 프론트엔드 조합 예시 + +```tsx +// 기본 컴포넌트 +const ProductCard = ({ product }: { product: Product }) => ( +

+ {product.name} +

{product.name}

+

{product.price}원

+
+); + +// 다중 데코레이터 조합 +const EnhancedProductCard = withErrorBoundary( + // 4. 에러 처리 + withTracking( + // 3. 이벤트 추적 + withDarkMode( + // 2. 다크모드 지원 + withMemo(ProductCard), // 1. 성능 최적화 + ), + "product_card", + ), +); + +// 커스텀 훅으로 조합 관리 +function useDecoratedComponent

( + Component: React.ComponentType

, + decorators: Array<(c: React.ComponentType

) => React.ComponentType

>, +) { + return decorators.reduce((acc, decorator) => decorator(acc), Component); +} + +// 사용 +const FinalProductCard = useDecoratedComponent(ProductCard, [ + withMemo, + withDarkMode, + (c) => withTracking(c, "product_card"), + withErrorBoundary, +]); +``` + +## 상속과 데코레이터 비교 + +### 상속 방식 + +```typescript +// 클래스 폭발 발생 +class Beverage { + cost() { + return 1.0; + } +} + +class BeverageWithMocha extends Beverage { + cost() { + return 1.2; + } +} + +class BeverageWithMochaAndWhip extends BeverageWithMocha { + cost() { + return 1.3; + } +} +``` + +### 데코레이터 방식 + +```typescript +// 조합 가능 +let beverage = new Beverage(); +beverage = new Mocha(beverage); +beverage = new Whip(beverage); +``` + +### 비교표 + +| 기준 | 상속 | 데코레이터 | +| --------- | ------------------ | ------------- | +| 확장 시점 | 컴파일 타임 (정적) | 런타임 (동적) | +| 클래스 수 | 많음 (조합 폭발) | 적음 | +| 유연성 | 낮음 | 높음 | +| 복잡성 | 낮음 | 중간 | +| 기능 제거 | 어려움 | 쉬움 | + +## 정리 + +### 핵심 요약 + +``` +데코레이터 패턴 = 객체 감싸기 + 기능 위임 + +적용 상황: +- 상속으로 클래스 폭발 발생 +- 동적으로 기능 추가/제거 필요 +- 다양한 조합 필요 + +목적: +- 유연한 확장 +- SRP/OCP 준수 +- 클래스 수 최소화 +``` + +### 사용 권장 상황 + +- 피자 토핑, 커피 옵션 같은 조합 가능한 기능 +- 로깅, 캐싱, 압축 같은 횡단 관심사 +- 상속이 클래스 폭발을 일으킬 때 +- 런타임에 동적으로 기능 변경이 필요할 때 + +### 프론트엔드 적용 포인트 + +| 영역 | 데코레이터 적용 | +| --------- | -------------------------------------------- | +| 인증/인가 | withAuth, withPermission | +| 데이터 | withDataFetching, withCache | +| UI | withTheme, withDarkMode, withStyled | +| 성능 | withMemo, withLazyLoading | +| 품질 | withErrorBoundary, withLogging, withTracking | +| 폼 | withValidation | + +### 관련 패턴 + +- 어댑터 패턴: 인터페이스 변환 +- 컴포지트 패턴: 객체 트리 구조 +- 체인 오브 리스판서빌리티: 요청 처리 체인 +- 프록시 패턴: 접근 제어 diff --git "a/jihyeon/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264.md" "b/jihyeon/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264.md" new file mode 100644 index 0000000..b51f9ae --- /dev/null +++ "b/jihyeon/04.\355\214\251\355\206\240\353\246\254_\355\214\250\355\204\264.md" @@ -0,0 +1,703 @@ +# 팩토리 패턴 (Factory Pattern) + +## 1. 왜 팩토리 패턴이 필요한가? + +### 문제 상황 + +피자 가게에서 여러 종류의 피자를 만든다고 가정한다. + +```typescript +// 팩토리 패턴 없이 - 문제가 많은 코드 +class PizzaStore { + 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 if (type === "veggie") { + pizza = new VeggiePizza(); + } + + pizza.prepare(); + pizza.bake(); + pizza.cut(); + pizza.box(); + + return pizza; + } +} +``` + +### 이 코드의 문제점 + +| 문제 | 설명 | +| -------------------- | -------------------------------------------------------------------- | +| 🔴 **OCP 위반** | 새로운 피자 타입을 추가할 때마다 `orderPizza()` 메서드를 수정해야 함 | +| 🔴 **높은 결합도** | PizzaStore가 모든 구체적인 Pizza 클래스에 의존 | +| 🔴 **중복 코드** | 다른 가게에서도 같은 분기문이 반복됨 | +| 🔴 **테스트 어려움** | PizzaStore를 테스트할 때 모든 Pizza 구현체가 필요 | + +### 팩토리 패턴의 해결책 + +> **"객체 생성(new)을 직접 하지 말고, 누군가에게 맡겨라"** + +--- + +## 2. Simple Factory (간단 팩토리) + +### 개념 + +객체 생성 로직을 별도의 클래스로 분리하는 가장 단순한 형태 + +> **Note**: 정식 패턴은 아니지만, 널리 사용되는 관용구 + +### 구조 + +``` +┌─────────────────┐ ┌──────────────────────┐ +│ PizzaStore │────────>│ SimplePizzaFactory │ +│─────────────────│ │──────────────────────│ +│ - factory │ │ + createPizza(type) │ +│ + orderPizza() │ └──────────────────────┘ +└─────────────────┘ │ + ↓ + ┌───────────────────────┐ + │ Pizza (interface) │ + └───────────────────────┘ + ↑ + ┌─────────────────────┼─────────────────────┐ + │ │ │ + CheesePizza PepperoniPizza VeggiePizza +``` + +### 실제 코드 + +#### SimplePizzaFactory.ts + +```typescript +// 📁 factory/pizzas/SimplePizzaFactory.ts +export class SimplePizzaFactory { + createPizza(type: string): Pizza { + let pizza = null; + + if (type.equals("cheese")) { + pizza = new CheesePizza(); + } else if (type.equals("pepperoni")) { + pizza = new PepperoniPizza(); + } else if (type.equals("clam")) { + pizza = new ClamPizza(); + } else if (type.equals("veggie")) { + pizza = new VeggiePizza(); + } + + return pizza; + } +} +``` + +#### PizzaStore.ts + +```typescript +// 📁 factory/pizzas/PizzaStore.ts +export class PizzaStore { + 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; + } +} +``` + +### 사용 예시 + +```typescript +const factory = new SimplePizzaFactory(); +const store = new PizzaStore(factory); +const pizza = store.orderPizza("cheese"); +``` + +### 장단점 + +| 장점 | 단점 | +| --------------------------------------------------- | ------------------------------------------------- | +| 생성 로직이 한 곳에 집중 | 새로운 피자 타입 추가 시 팩토리 코드 수정 필요 | +| PizzaStore는 구체적인 Pizza 타입을 몰라도 됨 | 확장에는 열려있지 않음 (OCP 완벽하게 지키지 못함) | +| 재사용 가능 (다른 가게에서도 같은 팩토리 사용 가능) | | + +### 언제 사용할까? + +- 생성 로직이 단순하고 자주 변하지 않을 때 +- 한 가지 타입의 객체만 생성할 때 +- 빠르게 리팩토링하고 싶을 때 + +--- + +## 3. Factory Method (팩토리 메서드) + +### 개념 + +객체 생성을 **서브클래스에게 위임**하는 패턴 + +- 부모 클래스는 "무엇을 할지" 정의 (템플릿) +- 자식 클래스가 "어떻게 만들지" 결정 (구현) + +### 구조 + +``` + PizzaStore (abstract) + ├── orderPizza() // 공통 로직 (변하지 않음) + └── createPizza() // 추상 메서드 (서브클래스가 구현) + ↑ + ───────┴─────── + ↑ ↑ + NYPizzaStore ChicagoPizzaStore + createPizza() createPizza() + ↓ ↓ + NYStyle... ChicagoStyle... +``` + +### 실제 코드 + +#### PizzaStore.ts (추상 클래스) + +```typescript +// 📁 factory/pizzafm/PizzaStore.ts +export abstract class PizzaStore { + // ⭐ 팩토리 메서드 - 서브클래스가 구현해야 함 + abstract createPizza(item: string): Pizza; + + // 공통 로직은 부모가 관리 (템플릿 메서드) + orderPizza(type: string): Pizza { + const pizza = this.createPizza(type); // 서브클래스에게 위임! + + console.log("--- Making a " + pizza.getName() + " ---"); + pizza.prepare(); + pizza.bake(); + pizza.cut(); + pizza.box(); + + return pizza; + } +} +``` + +#### NYPizzaStore.ts (구체적인 서브클래스) + +```typescript +// 📁 factory/pizzafm/NYPizzaStore.ts +export class NYPizzaStore extends PizzaStore { + createPizza(item: string): Pizza { + if (item.equals("cheese")) { + return new NYStyleCheesePizza(); // 뉴욕 스타일! + } else if (item.equals("veggie")) { + return new NYStyleVeggiePizza(); + } else if (item.equals("clam")) { + return new NYStyleClamPizza(); + } else if (item.equals("pepperoni")) { + return new NYStylePepperoniPizza(); + } + return null; + } +} +``` + +#### ChicagoPizzaStore.ts + +```typescript +// 📁 factory/pizzafm/ChicagoPizzaStore.ts +export class ChicagoPizzaStore extends PizzaStore { + createPizza(item: string): Pizza { + if (item.equals("cheese")) { + return new ChicagoStyleCheesePizza(); // 시카고 스타일! + } else if (item.equals("veggie")) { + return new ChicagoStyleVeggiePizza(); + } + // ... 다른 피자들 + return null; + } +} +``` + +### 사용 예시 + +```typescript +const nyStore = new NYPizzaStore(); +const chicagoStore = new ChicagoPizzaStore(); + +const pizza1 = nyStore.orderPizza("cheese"); // 뉴욕 스타일 치즈 피자 +const pizza2 = chicagoStore.orderPizza("cheese"); // 시카고 스타일 치즈 피자 +``` + +### 실행 흐름 + +``` +1. nyStore.orderPizza("cheese") 호출 + ↓ +2. PizzaStore.orderPizza() 실행 + ↓ +3. this.createPizza("cheese") 호출 (다형성!) + ↓ +4. NYPizzaStore.createPizza() 실행 + ↓ +5. NYStyleCheesePizza 반환 + ↓ +6. prepare() → bake() → cut() → box() 실행 +``` + +### 장단점 + +| 장점 | 단점 | +| ----------------------------------------------------------------- | ---------------------------- | +| **OCP 완벽 준수**: 새로운 피자 스타일 추가 시 기존 코드 수정 없음 | 클래스 수 증가 | +| **확장성**: 새로운 가게 추가가 서브클래스 생성만으로 가능 | 상속 구조로 인한 유연성 부족 | +| **공통 로직 보호**: `orderPizza()`는 한 곳에서 관리 | | + +### 언제 사용할까? + +- 라이브러리/프레임워크에서 확장 포인트를 제공할 때 +- 클라이언트가 구체적인 타입을 알 필요 없을 때 +- 템플릿 메서드 패턴과 함께 사용할 때 + +--- + +## 4. Abstract Factory (추상 팩토리) + +### 개념 + +**연관된 객체들의 군(family)**을 생성하기 위한 인터페이스를 제공 + +- 구체적인 클래스는 지정하지 않음 +- 제품군 간의 일관성 보장 + +### 구조 + +``` + PizzaIngredientFactory (interface) + │ + ┌───────────────┼───────────────┐ + │ │ │ + createDough() createSauce() createCheese() ... + │ + ↓ +┌───────────────────────┐ +│ NYPizzaIngredient │ +│ Factory │ +├───────────────────────┤ +│ createDough() │──> ThinCrustDough +│ createSauce() │──> MarinaraSauce +│ createCheese() │──> ReggianoCheese +│ createClam() │──> FreshClams +└───────────────────────┘ + +┌───────────────────────┐ +│ ChicagoPizza │ +│ IngredientFactory │ +├───────────────────────┤ +│ createDough() │──> ThickCrustDough +│ createSauce() │──> PlumTomatoSauce +│ createCheese() │──> MozzarellaCheese +│ createClam() │──> FrozenClams +└───────────────────────┘ +``` + +### 실제 코드 + +#### PizzaIngredientFactory.ts (인터페이스) + +```typescript +// 📁 factory/pizzaaf/PizzaIngredientFactory.ts +export interface PizzaIngredientFactory { + createDough(): Dough; + createSauce(): Sauce; + createCheese(): Cheese; + createVeggies(): Veggies[]; + createPepperoni(): Pepperoni; + createClam(): Clams; +} +``` + +#### NYPizzaIngredientFactory.ts + +```typescript +// 📁 factory/pizzaaf/NYPizzaIngredientFactory.ts +export class NYPizzaIngredientFactory implements PizzaIngredientFactory { + createDough(): Dough { + return new ThinCrustDough(); // 얇은 도우 + } + + createSauce(): Sauce { + return new MarinaraSauce(); // 마리나라 소스 + } + + createCheese(): Cheese { + return new ReggianoCheese(); // 레지아노 치즈 + } + + createVeggies(): Veggies[] { + return [new Garlic(), new Onion(), new Mushroom()]; + } + + createPepperoni(): Pepperoni { + return new SlicedPepperoni(); + } + + createClam(): Clams { + return new FreshClams(); // 신선한 조개 (뉴욕 특산!) + } +} +``` + +#### ChicagoPizzaIngredientFactory.ts + +```typescript +// 📁 factory/pizzaaf/ChicagoPizzaIngredientFactory.ts +export class ChicagoPizzaIngredientFactory implements PizzaIngredientFactory { + createDough(): Dough { + return new ThickCrustDough(); // 두꺼운 도우 + } + + createSauce(): Sauce { + return new PlumTomatoSauce(); // 플럼 토마토 소스 + } + + createCheese(): Cheese { + return new MozzarellaCheese(); // 모짜렐라 치즈 + } + + createVeggies(): Veggies[] { + return [new BlackOlives(), new Spinach(), new Eggplant()]; + } + + createPepperoni(): Pepperoni { + return new SlicedPepperoni(); + } + + createClam(): Clams { + return new FrozenClams(); // 냉동 조개 (내륙이니까!) + } +} +``` + +#### CheesePizza.ts (팩토리 사용) + +```typescript +// 📁 factory/pizzaaf/CheesePizza.ts +export class CheesePizza extends Pizza { + ingredientFactory: PizzaIngredientFactory; + + constructor(ingredientFactory: PizzaIngredientFactory) { + super(); + this.ingredientFactory = ingredientFactory; + } + + prepare(): void { + console.log("Preparing " + this.name); + + // 팩토리에서 재료를 가져옴 - 어떤 지역인지는 모름! + this.dough = this.ingredientFactory.createDough(); + this.sauce = this.ingredientFactory.createSauce(); + this.cheese = this.ingredientFactory.createCheese(); + } +} +``` + +### 사용 예시 + +```typescript +// 뉴욕 스타일 +const nyFactory = new NYPizzaIngredientFactory(); +const nyStore = new NYPizzaStore(nyFactory); +const nyCheese = nyStore.orderPizza("cheese"); +// → ThinCrustDough + MarinaraSauce + ReggianoCheese + FreshClams + +// 시카고 스타일 +const chicagoFactory = new ChicagoPizzaIngredientFactory(); +const chicagoStore = new ChicagoPizzaStore(chicagoFactory); +const chicagoCheese = chicagoStore.orderPizza("cheese"); +// → ThickCrustDough + PlumTomatoSauce + MozzarellaCheese + FrozenClams +``` + +### 제품군 비교 + +| 재료 | 뉴욕 스타일 | 시카고 스타일 | +| ---- | ----------------------- | ------------------------------ | +| 도우 | ThinCrustDough (얇은) | ThickCrustDough (두꺼운) | +| 소스 | MarinaraSauce | PlumTomatoSauce | +| 치즈 | ReggianoCheese | MozzarellaCheese | +| 조개 | FreshClams (신선한) | FrozenClams (냉동) | +| 야채 | Garlic, Onion, Mushroom | BlackOlives, Spinach, Eggplant | + +### 장단점 + +| 장점 | 단점 | +| ---------------------------------------------------------- | ---------------------------------------------- | +| **제품군 일관성**: 뉴욕 스타일이면 모든 재료가 뉴욕 스타일 | 새로운 제품 타입 추가 시 모든 팩토리 수정 필요 | +| **클라이언트 독립**: 피자는 어떤 팩토리인지 몰라도 됨 | 복잡한 구조 | +| **쉬운 교체**: 팩토리만 바꾸면 전혀 다른 제품군으로 전환 | | + +### 언제 사용할까? + +- 연관된 객체들이 함께 사용되어야 할 때 +- 제품군 전체를 교체해야 할 때 (테마, 플랫폼 등) +- 구체적인 클래스에 의존하지 않고 인터페이스로 작업하고 싶을 때 + +--- + +## 5. 세 가지 패턴 비교 + +### 비교 테이블 + +| 특징 | Simple Factory | Factory Method | Abstract Factory | +| ------------- | ---------------- | ---------------------- | ------------------ | +| **목적** | 생성 로직 캡슐화 | 서브클래스에 생성 위임 | 제품군 생성 | +| **확장 방법** | 팩토리 수정 | 서브클래스 추가 | 새로운 팩토리 추가 | +| **OCP 준수** | ⚠️ 부분 | 완벽 | 완벽 | +| **복잡도** | 낮음 | 중간 | 높음 | +| **클래스 수** | 적음 | 중간 | 많음 | +| **사용 시기** | 단순한 생성 | 다양한 변형 | 연관된 제품군 | + +### 선택 가이드 + +``` + ┌─────────────────────┐ + │ 객체 생성이 필요한가? │ + └──────────┬──────────┘ + │ + ┌──────────↓──────────┐ + │ 생성 로직이 복잡한가? │ + └──────────┬──────────┘ + │ + ┌────────────────┼────────────────┐ + │ │ │ + 아니오 예 모르겠음 + │ │ │ + ↓ ↓ ↓ + 그냥 new 사용 ┌───────────┐ Simple Factory + │ 제품군이 │ + │ 필요한가? │ + └─────┬─────┘ + │ + ┌────────┼────────┐ + │ │ + 아니오 예 + │ │ + ↓ ↓ + Factory Method Abstract Factory +``` + +### 코드 비교 + +```typescript +// Simple Factory +const factory = new SimplePizzaFactory(); +const pizza = factory.createPizza("cheese"); + +// Factory Method +const store = new NYPizzaStore(); +const pizza = store.orderPizza("cheese"); + +// Abstract Factory +const ingredientFactory = new NYPizzaIngredientFactory(); +const pizza = new CheesePizza(ingredientFactory); +pizza.prepare(); +``` + +--- + +## 6. 실무 예시 + +### 6.1 UI 컴포넌트 팩토리 (Abstract Factory) + +```typescript +// 제품 인터페이스 +interface Button { + render(): void; +} +interface TextInput { + render(): void; +} + +// 추상 팩토리 +interface UIComponentFactory { + createButton(): Button; + createTextInput(): TextInput; +} + +// Material Design 팩토리 +class MaterialFactory implements UIComponentFactory { + createButton(): Button { + return new MaterialButton(); + } + createTextInput(): TextInput { + return new MaterialTextInput(); + } +} + +// Ant Design 팩토리 +class AntFactory implements UIComponentFactory { + createButton(): Button { + return new AntButton(); + } + createTextInput(): TextInput { + return new AntTextInput(); + } +} + +// 사용 +function createForm(factory: UIComponentFactory) { + const submitButton = factory.createButton(); + const nameInput = factory.createTextInput(); + // 일관된 스타일의 컴포넌트들 +} +``` + +### 6.2 데이터베이스 연결 팩토리 (Factory Method) + +```typescript +// 추상 클래스 +abstract class DatabaseConnection { + abstract createConnection(): Connection; + + async query(sql: string) { + const conn = this.createConnection(); + return conn.execute(sql); + } +} + +// MySQL +class MySQLConnection extends DatabaseConnection { + createConnection(): Connection { + return new MySQLClient({ + host: "localhost", + user: "root", + password: "password", + }); + } +} + +// PostgreSQL +class PostgreSQLConnection extends DatabaseConnection { + createConnection(): Connection { + return new PostgreSQLClient({ + host: "localhost", + user: "postgres", + password: "password", + }); + } +} + +// 사용 +const db = + process.env.DB === "mysql" + ? new MySQLConnection() + : new PostgreSQLConnection(); +await db.query("SELECT * FROM users"); +``` + +### 6.3 로깅 팩토리 (Simple Factory) + +```typescript +class LoggerFactory { + static createLogger(type: string): Logger { + switch (type) { + case "console": + return new ConsoleLogger(); + case "file": + return new FileLogger("/var/log/app.log"); + case "remote": + return new RemoteLogger("https://logs.example.com"); + default: + throw new Error(`Unknown logger type: ${type}`); + } + } +} + +// 사용 +const logger = LoggerFactory.createLogger(process.env.LOG_TYPE); +logger.info("Application started"); +``` + +### 6.4 테스트에서의 팩토리 + +```typescript +// 테스트용 Mock 팩토리 +class MockUserFactory implements UserFactory { + create(id: string): User { + return { + id, + name: "Test User", + email: "test@example.com", + createdAt: new Date("2024-01-01"), + }; + } +} + +// 실제 팩토리 +class RealUserFactory implements UserFactory { + async create(id: string): Promise { + const response = await fetch(`/api/users/${id}`); + return response.json(); + } +} + +// 테스트 코드 +describe("UserService", () => { + it("should create user", () => { + const factory = new MockUserFactory(); + const service = new UserService(factory); + + const user = service.createUser("123"); + + expect(user.name).toBe("Test User"); + }); +}); +``` + +--- + +## 7. 핵심 요약 + +### 팩토리 패턴의 본질 + +> **"객체 생성(new)을 직접 하지 말고, 누군가에게 맡겨라"** + +### 이점 + +| 이점 | 설명 | +| --------------- | ------------------------------------ | +| **결합도 감소** | 클라이언트는 구체 클래스를 몰라도 됨 | +| **테스트 용이** | Mock 팩토리로 쉽게 교체 가능 | +| **유지보수성** | 생성 로직이 한 곳에 집중 | +| **확장성** | 새로운 타입 추가가 쉬움 | + +### 디자인 원칙과의 관계 + +| 원칙 | 팩토리 패턴에서의 적용 | +| -------------------------- | ---------------------------------------------------------------------- | +| **OCP** (개방-폐쇄 원칙) | Factory Method, Abstract Factory는 확장에는 열려있고 수정에는 닫혀있음 | +| **DIP** (의존성 역전 원칙) | 구체 클래스가 아닌 추상(인터페이스)에 의존 | +| **SRP** (단일 책임 원칙) | 객체 생성 책임을 별도 클래스로 분리 | + +### 주의사항 + +| 피해야 할 것 | 설명 | +| --------------- | ------------------------------------------------ | +| 과도한 추상화 | 모든 `new`를 팩토리로 만들 필요는 없음 | +| God Factory | 모든 것을 만드는 거대 팩토리는 안티패턴 | +| 불필요한 복잡성 | 단순한 경우 그냥 `new`를 쓰는 게 더 나을 수 있음 |