icon

디자인 패턴 구현


 타입스크립트에서 디자인 패턴을 구현하면 코드의 재사용성, 유지보수성, 그리고 확장성을 크게 향상시킬 수 있습니다.

 주요 디자인 패턴은 생성 패턴, 구조 패턴, 행위 패턴으로 분류됩니다.

주요 디자인 패턴 소개

 1. 생성 패턴 : 객체 생성 메커니즘을 다룹니다.

  • 싱글톤, 팩토리, 빌더 등

 2. 구조 패턴 : 객체 구조를 다룹니다.

  • 어댑터, 데코레이터, 프록시 등

 3. 행위 패턴 : 객체 간 상호작용을 다룹니다.

  • 옵저버, 전략, 커맨드 등

대표적인 패턴 구현

 싱글톤 패턴

 목적 : 클래스의 인스턴스가 하나만 생성되도록 보장합니다.

class Singleton {
    private static instance: Singleton;
 
    private constructor() {}
 
    public static getInstance(): Singleton {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}
 
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true

 팩토리 패턴

 목적 : 객체 생성 로직을 캡슐화합니다.

interface Product {
    operation(): string;
}
 
class ConcreteProduct1 implements Product {
    public operation(): string {
        return "ConcreteProduct1";
    }
}
 
class ConcreteProduct2 implements Product {
    public operation(): string {
        return "ConcreteProduct2";
    }
}
 
class Factory {
    public static createProduct(type: number): Product {
        if (type === 1) {
            return new ConcreteProduct1();
        } else {
            return new ConcreteProduct2();
        }
    }
}
 
const product = Factory.createProduct(1);
console.log(product.operation()); // "ConcreteProduct1"

 옵저버 패턴

 목적 : 객체 상태 변화를 다른 객체에 통지합니다.

interface Observer {
    update(data: any): void;
}
 
class Subject {
    private observers: Observer[] = [];
 
    public attach(observer: Observer): void {
        this.observers.push(observer);
    }
 
    public notify(data: any): void {
        for (const observer of this.observers) {
            observer.update(data);
        }
    }
}
 
class ConcreteObserver implements Observer {
    public update(data: any): void {
        console.log(`Received update: ${data}`);
    }
}
 
const subject = new Subject();
const observer = new ConcreteObserver();
subject.attach(observer);
subject.notify("Hello, Observer!");

타입스크립트의 고급 기능 활용

 제네릭을 사용한 팩토리 패턴

interface Product {
    use(): void;
}
 
class ConcreteProduct implements Product {
    use() {
        console.log("Using ConcreteProduct");
    }
}
 
class GenericFactory<T extends Product> {
    create(): T {
        return new ConcreteProduct() as T;
    }
}
 
const factory = new GenericFactory<ConcreteProduct>();
const product = factory.create();
product.use(); // "Using ConcreteProduct"

 데코레이터를 활용한 데코레이터 패턴

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        console.log(`Calling ${propertyKey} with`, args);
        return originalMethod.apply(this, args);
    };
    return descriptor;
}
 
class Example {
    @log
    greet(name: string) {
        return `Hello, ${name}!`;
    }
}
 
const example = new Example();
example.greet("TypeScript"); // Logs: Calling greet with ["TypeScript"]

타입 관련 문제 해결

 제네릭과 조건부 타입을 활용한 타입 안전성 강화

type Constructor<T = {}> = new (...args: any[]) => T;
 
function Mixin<T extends Constructor>(baseClass: T) {
    return class extends baseClass {
        mixinMethod() {
            console.log("Mixin method called");
        }
    };
}
 
class Base {
    baseMethod() {
        console.log("Base method called");
    }
}
 
const MixedClass = Mixin(Base);
const instance = new MixedClass();
instance.baseMethod(); // "Base method called"
instance.mixinMethod(); // "Mixin method called"

프론트엔드 프레임워크에서의 패턴

 React에서의 HOC (Higher Order Component) 패턴

import React from 'react';
 
interface WithLoaderProps {
    loading: boolean;
}
 
function withLoader<P extends object>(
    WrappedComponent: React.ComponentType<P>
) {
    return class extends React.Component<P & WithLoaderProps> {
        render() {
            const { loading, ...props } = this.props;
            return loading ? <div>Loading...</div> : <WrappedComponent {...props as P} />;
        }
    };
}
 
const MyComponent: React.FC<{ data: string }> = ({ data }) => <div>{data}</div>;
const EnhancedComponent = withLoader(MyComponent);
 
// 사용
<EnhancedComponent loading={true} data="Hello" />

백엔드 개발에서의 패턴

 리포지토리 패턴

interface Repository<T> {
    find(id: string): Promise<T>;
    save(item: T): Promise<void>;
}
 
class UserRepository implements Repository<User> {
    async find(id: string): Promise<User> {
        // 데이터베이스에서 사용자 조회 로직
    }
 
    async save(user: User): Promise<void> {
        // 데이터베이스에 사용자 저장 로직
    }
}

마이크로서비스 아키텍처에서의 패턴

 서킷 브레이커 패턴

class CircuitBreaker {
    private failureCount: number = 0;
    private lastFailureTime: number = 0;
    private readonly threshold: number = 5;
    private readonly timeout: number = 10000; // 10 seconds
 
    async execute(fn: () => Promise<any>): Promise<any> {
        if (this.isOpen()) {
            throw new Error("Circuit is open");
        }
 
        try {
            const result = await fn();
            this.reset();
            return result;
        } catch (error) {
            this.recordFailure();
            throw error;
        }
    }
 
    private isOpen(): boolean {
        if (this.failureCount >= this.threshold) {
            const now = Date.now();
            if (now - this.lastFailureTime < this.timeout) {
                return true;
            }
            this.reset();
        }
        return false;
    }
 
    private recordFailure(): void {
        this.failureCount++;
        this.lastFailureTime = Date.now();
    }
 
    private reset(): void {
        this.failureCount = 0;
        this.lastFailureTime = 0;
    }
}

Best Practices

  1. 패턴의 목적 이해 : 각 패턴의 의도와 적용 시나리오를 명확히 이해합니다.
  2. 과도한 사용 지양 : 필요한 경우에만 패턴을 적용하고, 불필요한 복잡성을 피합니다.
  3. 타입 안정성 유지 : 제네릭과 인터페이스를 활용하여 타입 안전성을 보장합니다.
  4. 테스트 가능성 고려 : 패턴 적용 시 단위 테스트 용이성을 고려합니다.
  5. 문서화 : 사용된 패턴과 그 이유를 명확히 문서화합니다.
  6. 팀 합의 : 패턴 사용에 대한 팀 내 합의와 가이드라인을 수립합니다.
  7. 지속적인 리팩토링 : 패턴 적용 후에도 코드 품질을 지속적으로 개선합니다.

 디자인 패턴을 효과적으로 사용하기 위해서는 지속적인 학습과 실제 적용 경험이 필요합니다.

 패턴의 기본 개념을 이해하고 이를 타입스크립트의 특성에 맞게 적용하는 능력을 키우는 것이 중요합니다.