TypeScript에서 일반적인 디자인 패턴 구현
Grace Collins
Solutions Engineer · Leapcell

TypeScript 디자인 패턴 소개
끊임없이 발전하는 소프트웨어 개발 환경에서 강력하고 확장 가능하며 유지보수 가능한 애플리케이션을 구축하는 것이 가장 중요합니다. 디자인 패턴은 일반적인 소프트웨어 설계 문제에 대한 검증된 솔루션을 제공하며, 유연하고 이해하기 쉬운 코드 구조를 위한 청사진을 제공합니다. JavaScript는 막대한 유연성을 제공하지만, TypeScript의 타입 안전 환경 내에서 이러한 패턴을 활용하면 코드 품질을 크게 향상시킬 수 있습니다. 강력한 타이핑과 객체 지향 기능을 갖춘 TypeScript는 이러한 오래된 패턴을 구현하는 데 훌륭한 기반을 제공하여 개발자가 실수를 조기에 발견하고 협업을 개선할 수 있도록 합니다. 이 글에서는 세 가지 기본 디자인 패턴인 싱글턴, 팩토리, 옵저버를 살펴보고 TypeScript에서 구현을 시연하며 실질적인 이점을 강조합니다.
핵심 개념 이해
패턴에 대해 자세히 알아보기 전에 TypeScript에서의 구현을 이해하는 데 중요한 몇 가지 핵심 개념을 간략하게 정의해 보겠습니다.
- 클래스와 인터페이스: TypeScript는 클래스를 확장하여 객체에 대한 청사진을 정의하는 일반적인 방법을 제공합니다. 반면에 인터페이스는 구현 세부 정보를 제공하지 않고 객체 또는 클래스의 모양에 대한 계약을 정의하여 느슨한 결합과 유형 안전을 촉진합니다.
- 정적 멤버: 클래스의 인스턴스가 아닌 클래스 자체에 속하는 멤버입니다. 유틸리티 함수나 모든 인스턴스에 걸쳐 공통 상태를 유지하는 데 자주 사용됩니다.
- 캡슐화: 데이터와 데이터를 조작하는 메서드를 하나의 단위(예: 클래스)로 묶고 일부 구성 요소의 내부 부분에 대한 직접 액세스를 제한하는 원칙입니다. TypeScript의
private
및protected
한정자는 이를 용이하게 합니다. - 다형성: 객체가 여러 형태를 취할 수 있는 능력입니다. 객체 지향 프로그래밍에서 공유 인터페이스 또는 기본 클래스를 통해 다른 클래스가 공통 유형의 인스턴스로 취급되는 능력을 말합니다.
TypeScript의 싱글턴 패턴
싱글턴 패턴은 클래스가 단 하나의 인스턴스를 갖도록 보장하고 이에 대한 전역 액세스 지점을 제공합니다. 이는 데이터베이스 연결, 구성 관리자 또는 로거와 같이 여러 인스턴스가 불일치 또는 불필요한 리소스 소비로 이어질 수 있는 리소스를 관리하는 데 특히 유용합니다.
원칙 및 구현
핵심 아이디어는 직접 인스턴스화를 방지하기 위해 클래스의 생성자를 비공개로 만들고, 그런 다음 클래스의 단일 인스턴스를 반환하는 정적 메서드를 제공하여 이미 존재하지 않는 경우에만 생성하는 것입니다.
class ConfigurationManager { private static instance: ConfigurationManager; private settings: Map<string, string>; private constructor() { this.settings = new Map<string, string>(); // 파일 또는 환경 변수에서 구성 로드 시뮬레이션 this.settings.set('API_KEY', 'some_secret_key'); this.settings.set('LOG_LEVEL', 'info'); console.log('ConfigurationManager 인스턴스가 생성되었습니다.'); } public static getInstance(): ConfigurationManager { if (!ConfigurationManager.instance) { ConfigurationManager.instance = new ConfigurationManager(); } return ConfigurationManager.instance; } public getSetting(key: string): string | undefined { return this.settings.get(key); } public setSetting(key: string, value: string): void { this.settings.set(key, value); console.log(`'${key}' 설정이 '${value}'로 업데이트되었습니다.`); } } // 사용법 const config1 = ConfigurationManager.getInstance(); const config2 = ConfigurationManager.getInstance(); console.log(config1 === config2); // true, 두 참조 모두 동일한 인스턴스를 가리킵니다. console.log('API 키:', config1.getSetting('API_KEY')); config2.setSetting('LOG_LEVEL', 'debug'); console.log('로그 수준 (config1에서):', config1.getSetting('LOG_LEVEL')); // new ConfigurationManager(); // 이렇게 하면 TypeScript 오류가 발생합니다: // 문자열 'ConfigurationManager' 클래스의 생성자는 개인 정보이며 클래스 선언 내에서만 액세스할 수 있습니다.
응용 시나리오
- 로깅: 파일 또는 로깅 서비스에 로그를 작성하는 단일 로거 인스턴스.
- 구성 관리자: 애플리케이션 설정의 중앙 집중식 관리.
- 데이터베이스 연결 풀: 단일 연결 풀이 데이터베이스 연결을 관리하도록 보장합니다.
TypeScript의 팩토리 패턴
팩토리 패턴은 슈퍼 클래스에서 객체를 생성하기 위한 인터페이스를 제공하지만 하위 클래스가 생성할 객체의 유형을 변경할 수 있도록 합니다. 객체 생성 프로세스를 추상화하여 정확한 클래스를 지정하지 않고도 객체를 생성할 수 있으므로 느슨한 결합을 촉진하고 시스템을 더 확장 가능하게 만듭니다.
원칙 및 구현
핵심 아이디어는 객체를 생성하고 반환하는 "팩토리" 메서리를 중심으로 이루어집니다. 이 메서드는 기본 클래스에서 구현되고 하위 클래스에서 재정의되거나 독립적인 팩토리 함수/클래스로 존재할 수 있습니다.
다양한 유형의 차량을 만든다고 상상해 봅시다.
// 제품 인터페이스 interface Vehicle { drive(): void; getType(): string; } // 구체적인 제품 class Car implements Vehicle { drive(): void { console.log('차를 운전합니다.'); } getType(): string { return 'Car'; } } class Truck implements Vehicle { drive(): void { console.log('트럭을 운전합니다.'); } getType(): string { return 'Truck'; } } // 팩토리 클래스 class VehicleFactory { public static createVehicle(type: string): Vehicle | null { switch (type.toLowerCase()) { case 'car': return new Car(); case 'truck': return new Truck(); default: console.warn(`알 수 없는 차량 유형: ${type}`); return null; } } } // 사용법 const myCar = VehicleFactory.createVehicle('car'); if (myCar) { myCar.drive(); // 차를 운전합니다. console.log(myCar.getType()); // Car } const myTruck = VehicleFactory.createVehicle('truck'); if (myTruck) { myTruck.drive(); // 트럭을 운전합니다. console.log(myTruck.getType()); // Truck } const unknownVehicle = VehicleFactory.createVehicle('bike'); // 알 수 없는 차량 유형: bike
응용 시나리오
- UI 구성 요소 라이브러리: 특정 구성에 따라 다양한 유형의 UI 요소(버튼, 텍스트 필드 등) 생성.
- 데이터 파서: 입력 데이터 형식에 따라 다양한 파서(JSON, XML, CSV) 생성.
- 게임 개발: 게임 로직에 따라 다른 유형의 적 또는 게임 개체 스폰.
TypeScript의 옵저버 패턴
옵저버 패턴은 객체 간에 일대다 종속성을 정의하여 한 객체의 상태가 변경될 때 모든 종속성이 자동으로 알림을 받고 업데이트됩니다. 이 패턴은 이벤트 처리 시스템을 구현하는 데 기본적입니다.
원칙 및 구현
두 가지 주요 유형의 객체가 관련됩니다.
- 주제(게시자): 상태가 모니터링되는 객체입니다. 종속성(옵저버) 목록을 유지하고 상태 변경에 대해 알립니다.
- 옵저버(구독자): 주제의 상태 변경 알림을 받으려는 객체입니다. 주제가 호출하는 업데이트 메서드를 제공합니다.
// 옵저버 인터페이스 interface Observer { update(data: any): void; } // 주제 클래스 class StockMarket implements Subject { private observers: Observer[] = []; private stockPrice: number; constructor(initialPrice: number) { this.stockPrice = initialPrice; } public attach(observer: Observer): void { const isExist = this.observers.includes(observer); if (isExist) { return console.log('주제: 옵저버가 이미 연결되어 있습니다.'); } console.log('주제: 옵저버를 연결했습니다.'); this.observers.push(observer); } public detach(observer: Observer): void { const observerIndex = this.observers.indexOf(observer); if (observerIndex === -1) { return console.log('주제: 존재하지 않는 옵저버입니다.'); } this.observers.splice(observerIndex, 1); console.log('주제: 옵저버를 분리했습니다.'); } public notify(): void { console.log('주제: 옵저버에게 알리는 중...'); for (const observer of this.observers) { observer.update(this.stockPrice); } } public setStockPrice(newPrice: number): void { this.stockPrice = newPrice; console.log(`주가 변경: $${this.stockPrice}`); this.notify(); } } // 구체적인 옵저버 class Investor implements Observer { private name: string; constructor(name: string) { this.name = name; } update(price: any): void { console.log(`${this.name}: 주가가 $${price}로 업데이트되었습니다. 즉시 행동해야 합니다!`); } } // 사용법 const market = new StockMarket(100); const investor1 = new Investor('Alice'); const investor2 = new Investor('Bob'); market.attach(investor1); market.attach(investor2); market.setStockPrice(105); // 주제: 옵저버에게 알리는 중... // Alice: 주가가 $105로 업데이트되었습니다. 즉시 행동해야 합니다! // Bob: 주가가 $105로 업데이트되었습니다. 즉시 행동해야 합니다! market.detach(investor1); market.setStockPrice(98); // 주제: 옵저버에게 알리는 중... // Bob: 주가가 $98로 업데이트되었습니다. 즉시 행동해야 합니다!
응용 시나리오
- 이벤트 처리: UI 프레임워크(React, Angular 등)에서 이벤트(클릭, 입력 변경 등)는 종종 옵저버와 유사한 메커니즘을 사용하여 처리됩니다.
- MVC/MVVM 아키텍처: 모델은 데이터 변경 사항을 보거나 뷰 모델에 알립니다.
- 실시간 애플리케이션: 연결된 클라이언트에게 업데이트 알림(예: 채팅 애플리케이션 또는 주식 시세).
결론
TypeScript에서 디자인 패턴을 구현하는 것은 구조화된 문제 해결과 유형 안전의 강력한 조합을 제공합니다. 설명된 싱글턴, 팩토리, 옵저버 패턴은 일반적인 아키텍처 문제를 위한 강력한 솔루션을 제공하여 코드 모듈성, 테스트 용이성 및 유지보수성을 향상시킵니다. 이러한 패턴을 채택함으로써 개발자는 더 탄력적이고 확장 가능한 애플리케이션을 구축할 수 있으며, 이는 이해하고 발전시키기 쉽고 TypeScript의 기능을 진정으로 활용하여 전문적이고 미래 지향적인 코드를 작성할 수 있습니다.