디자인 패턴 구현
타입스크립트에서 디자인 패턴을 구현하면 코드의 재사용성, 유지보수성, 그리고 확장성을 크게 향상시킬 수 있습니다.
주요 디자인 패턴은 생성 패턴, 구조 패턴, 행위 패턴으로 분류됩니다.
주요 디자인 패턴 소개
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
- 패턴의 목적 이해 : 각 패턴의 의도와 적용 시나리오를 명확히 이해합니다.
- 과도한 사용 지양 : 필요한 경우에만 패턴을 적용하고, 불필요한 복잡성을 피합니다.
- 타입 안정성 유지 : 제네릭과 인터페이스를 활용하여 타입 안전성을 보장합니다.
- 테스트 가능성 고려 : 패턴 적용 시 단위 테스트 용이성을 고려합니다.
- 문서화 : 사용된 패턴과 그 이유를 명확히 문서화합니다.
- 팀 합의 : 패턴 사용에 대한 팀 내 합의와 가이드라인을 수립합니다.
- 지속적인 리팩토링 : 패턴 적용 후에도 코드 품질을 지속적으로 개선합니다.
디자인 패턴을 효과적으로 사용하기 위해서는 지속적인 학습과 실제 적용 경험이 필요합니다.
패턴의 기본 개념을 이해하고 이를 타입스크립트의 특성에 맞게 적용하는 능력을 키우는 것이 중요합니다.