디자인 패턴
디자인 패턴은 반복적으로 사용되는 설계 문제를 해결하기 위한 일반화된 솔루션이다.
주로 코드의 재사용성, 가독성, 유지보수성을 높이고, 설계의 복잡성을 줄이는 데 사용된다.
Module Pattern
모듈 패턴은 데이터 은닉과 캡슐화를 통해 코드를 구성하고 외부에 필요한 부분만 공개하는 데 사용된다.
- private 변수와 메서드를 제공
- 전역 네임 스페이스 오염을 방지
- 코드 분리가 용이
- 주로 클로저와 즉시 실행 함수 (IIFE: Immediately Invoked Function Expression)을 사용해 내부 상태를 숨기고, 필요한 부분만 외부로 노출함.
- 모듈이라는 취지에 맞게 캡슐화(내부 데이터, 메서드를 숨기고 외부에서 접근 못하게 제한)이 목적이다.
- 그리고 관련있는 데이터와 메서드를 한곳에 묶어 유지보수성을 향상시킨다.
const CounterModule = (() => {
// Private state and methods
let counter = 0;
const increment = () => {
counter++;
};
const decrement = () => {
counter--;
};
// Public API
return {
getCounter: () => counter,
increase: () => increment(),
decrease: () => decrement(),
};
})();
// 사용 예시
console.log(CounterModule.getCounter()); // 0
CounterModule.increase();
CounterModule.increase();
console.log(CounterModule.getCounter()); // 2
CounterModule.decrease();
console.log(CounterModule.getCounter()); // 1
counter 변수와 increment(), decrement() 함수는 외부에서 직접 접근할 수 없다.
외부에 노출되는 것은 getCounter(), increase(), decrease() 메서드 뿐이다.
// counter.module.ts
let counter = 0;
// Public API
export const CounterModule = {
getCounter: () => counter,
increase: () => {
counter++;
},
decrease: () => {
counter--;
},
};
// main.ts
import { CounterModule } from './counter.module';
console.log(CounterModule.getCounter()); // 0
CounterModule.increase();
CounterModule.increase();
console.log(CounterModule.getCounter()); // 2
CounterModule.decrease();
console.log(CounterModule.getCounter()); // 1
이 예제도 counter는 counter.module.ts 파일 내부에만 존재하고, 외부에선 접근이 불가하다.
싱글톤을 활용한 모듈 패턴
class CounterSingleton {
private static instance: CounterSingleton;
private counter = 0;
private constructor() {} // Private constructor to prevent direct instantiation
static getInstance(): CounterSingleton {
if (!CounterSingleton.instance) {
CounterSingleton.instance = new CounterSingleton();
}
return CounterSingleton.instance;
}
getCounter() {
return this.counter;
}
increase() {
this.counter++;
}
decrease() {
this.counter--;
}
}
// 사용 예시
const counter1 = CounterSingleton.getInstance();
const counter2 = CounterSingleton.getInstance();
counter1.increase();
counter1.increase();
console.log(counter2.getCounter()); // 2 (counter1과 counter2는 같은 인스턴스)
counter2.decrease();
console.log(counter1.getCounter()); // 1
이렇게 모듈패턴은 코드의 구조적 설계와 "캡슐화"를 제공하는 강력한 도구다.
하지만 간단한 애플리케이션에서는 필요 이상의 복잡성을 유발할 수 있다.
Singleton Pattern
싱글톤 패턴은 애플리케이션 전역에서 단 하나의 인스턴스만 생성되도록 보장하는 디자인패턴이다.
이를 통해 특정 클래스의 상태를 공유하거나, 불필요한 인스턴스 생성을 방지하여 메모리 사용을 최적화 할 수 있다.
나의 경우 앱 푸시알림으로 카프카를 사용할때 카프카 클래스를 싱글톤으로 구현하여, 앱 전역적으로 하나의 카프카 인스턴스만 생성되도록 설계해서 사용한 경험이 있다.
하지만 당시에는 하나의 싱글톤 클래스에 프로듀서와 컨슈머를 다 포함시켰는데 지금 생각해보면 프로듀서와 컨슈머를 따로 각각의 싱글톤 클래스로 구현하는게 더 좋아보인다. 추후 마루톡에서 사용할 때는 그렇게 해야겠다.
프로듀서와 컨슈머가 각각의 클래스에서 따로 구현되어야 각자 자신의 역할에 집중 할 수있고, 개별적으로 확장이나 수정이 용이할 것 같다는 생각이다.
어쨌든 다시 돌아와서, 싱글톤 패턴은
- 단일 인스턴스 보장
특정 클래스의 인스턴스가 하나만 존재하도록 보장 - 전역 접근 제공
애플리케이션 전역에서 동일한 객체에 접근 가능 - 리소스 절약
여러 개의 동일한 객체를 생성하는 대신 하나의 인스턴스를 재사용하여 메모리와 리소스 절약
class Singleton {
private static instance: Singleton; // 정적 변수로 단일 인스턴스를 저장
private constructor() {
// Private 생성자: 외부에서 직접 인스턴스 생성 불가
console.log('Singleton instance created!');
}
// 정적 메서드를 통해 인스턴스에 접근
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton(); // 인스턴스가 없으면 새로 생성
}
return Singleton.instance; // 기존 인스턴스 반환
}
public sayHello(): void {
console.log('Hello from Singleton!');
}
}
// 사용 예시
const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
singleton1.sayHello(); // Hello from Singleton!
console.log(singleton1 === singleton2); // true (같은 인스턴스)
싱글톤 패턴은 전역 상태 관리가 용이하고, 리소스 절약에 이점은 있지만,
전역 상태를 가지다보니 테스트 시 격리가 힘들고, 싱글톤 이느턴스에 대해 의존성이 높아질 수 있고, 멀티스레드환경에서는 레이스 컨디션이 발생할 위험이 있다.
실용적인 예시로 싱글톤 설명은 마친다.
데이터 베이스 연결 관리
class Database {
private static instance: Database;
private constructor() {
console.log('Database connected!');
}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
public query(sql: string): void {
console.log(`Executing query: ${sql}`);
}
}
// 사용 예시
const db1 = Database.getInstance();
db1.query('SELECT * FROM users');
const db2 = Database.getInstance();
db2.query('SELECT * FROM orders');
console.log(db1 === db2); // true
Factory Pattern
팩토리패턴은 객체 생성 로직을 캡슐화하여 클라이언트 코드에서 객체 생성 방식을 숨기는 디자인 패턴이다.
이를 통해 객체 생성과정을 단순화하고, 다양한 객체를 동적으로 생성할 수 있다.
- 객체 생성 로직이 복잡하거나 반복될 때
- 구체적인 클래스 이름을 클라이언트 코드에서 숨기고 싶을 때
- 런타임에 객체의 타입을 결정해야 할 때
구성 요소
- 팩토리 (Factory)
- 객체를 생성하는 역할을 담당하며, 생성 로직을 캡슐화.
- 클라이언트 (Client)
- 객체를 직접 생성하지 않고 팩토리를 통해 생성된 객체를 사용.
- 제품 (Product)
- 팩토리가 생성하는 객체(인스턴스).
예시
// 제품 인터페이스 정의
interface Button {
render(): void;
}
interface Checkbox {
check(): void;
}
// 구체적인 제품 클래스 1
class WindowsButton implements Button {
render(): void {
console.log("Rendering Windows Button");
}
}
class WindowsCheckbox implements Checkbox {
check(): void {
console.log("Checking Windows Checkbox");
}
}
// 구체적인 제품 클래스 2
class MacOSButton implements Button {
render(): void {
console.log("Rendering MacOS Button");
}
}
class MacOSCheckbox implements Checkbox {
check(): void {
console.log("Checking MacOS Checkbox");
}
}
// 추상 팩토리 인터페이스
interface GUIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
// 구체적인 팩토리 클래스 1
class WindowsFactory implements GUIFactory {
createButton(): Button {
return new WindowsButton();
}
createCheckbox(): Checkbox {
return new WindowsCheckbox();
}
}
// 구체적인 팩토리 클래스 2
class MacOSFactory implements GUIFactory {
createButton(): Button {
return new MacOSButton();
}
createCheckbox(): Checkbox {
return new MacOSCheckbox();
}
}
// 클라이언트 코드
function createUI(factory: GUIFactory): void {
const button = factory.createButton();
const checkbox = factory.createCheckbox();
button.render();
checkbox.check();
}
// 사용 예시
const windowsFactory = new WindowsFactory();
createUI(windowsFactory);
// Rendering Windows Button
// Checking Windows Checkbox
const macFactory = new MacOSFactory();
createUI(macFactory);
// Rendering MacOS Button
// Checking MacOS Checkbox
- GUIFactory 인터페이스는 createButton과 createCheckbox 메서드를 정의하여 관련 객체군 생성.
- WindowsFactory와 MacOSFactory는 구체적인 팩토리 구현체로, 특정 플랫폼에 맞는 객체를 생성.
- 클라이언트는 구체적인 클래스가 아닌 팩토리 인터페이스를 사용하여 객체를 생성.
팩토리 패턴은 복잡한 애플리케이션 구조에서 객체 생성 로직을 분리하는 데 효과적이며, 클라이언트 코드가 구체적인 클래스에 의존하지 않도록 설계할 수 있다.
Observer Pattern
옵저버 패턴은 객체 간의 일대다 관계를 정의하여 한 객체의 상태가 변경되었을 때, 그 객체에 의존하는 다른 객체들에게 자동으로 통지하고 업데이트하는 디자인 패턴이다. 주로 이벤트 처리 시스템이나 모니터링 시스템에서 사용된다고 한다.
- 구성요소
- Subject
상태를 보유하며, 상태 변경 시 옵저버들에게 알리는 역할
옵저버들을 등록, 제거, 알림을 관리 - Obeserver
주제의 상태를 감시하고, 상태 변경 시 통지를 받아 동작을 수행
- Subject
import { EventEmitter } from "events";
// 주제 역할
class Subject extends EventEmitter {
changeState(newState: string): void {
console.log(`State changed to: ${newState}`);
this.emit("stateChange", newState);
}
}
// 옵저버 등록
const subject = new Subject();
subject.on("stateChange", (state: string) => {
console.log(`Observer 1 received state: ${state}`);
});
subject.on("stateChange", (state: string) => {
console.log(`Observer 2 received state: ${state}`);
});
// 상태 변경
subject.changeState("New State");
// State changed to: New State
// Observer 1 received state: New State
// Observer 2 received state: New State
Node.js의 EventEmitter는 옵저버 패턴의 실용적인 구현 사례.
- 옵저버 패턴
- 주제(Subject)가 옵저버를 직접 관리하며, 상태 변경 시 즉시 통지.
- Subject와 Observer가 서로 알고 있어야 함.
- Pub/Sub 패턴
- 중간에 메시지 브로커(이벤트 채널)가 관여하여 발행자와 구독자가 서로 모른 채로 메시지를 교환.
- 더 느슨한 결합을 제공하며, 확장성이 높음.
옵저버 패턴은 상태 변경을 실시간으로 반영해야 하는 시스템에서 매우 유용하다.
TypeScript와 같은 언어에서는 인터페이스와 클래스를 활용해 명확하고 타입 안정성이 높은 구현을 제공할 수 있다.
이를 활용하면 복잡한 이벤트 처리 시스템이나 실시간 데이터 흐름을 효과적으로 관리할 수 있다.
NestJS와 Repository Pattern
NestJS에서 리포지토리 패턴은 "데이터 접근 로직을 캡슐화" 하여 비즈니스 로직과 데이터 액세스 로직을 분리하는 데 사용한다.
코드의 유지보수성과 테스트 가능성을 높이는 데 큰 역할을 하며, NestJS에서 TypeORM 같은 ORM 도구와 함께 자주 사용된다.
- Repository
데이터베이스 작업을 담당하는 계층
데이터 모델과 직접 상호작용하며, 데이터 생성, 읽기, 업데이트, 삭제 (CRUD) 를 처리 - Entity
데이터베이스 테이블 구조를 정의하는 클래스
ORM에서 데이터를 매핑하는 데 사용됌 - Service
Repository를 호출하여 비즈니스 로직을 처리하는 계층
데이터베이스 액세스 로직이 아닌 도메인 로직에 집중 - 장점
- 코드 재사용성
데이터베이스 로직을 중앙 집중화하여 여러 서비스에서 재사용 가능 - 유지 보수성
비즈니스 로직과 데이터 액세스 로직이 분리되어 변경 사항 관리가 용이 - 테스트 용이성
Mock Repository를 주입하여 독립적인 테스트 가능
- 코드 재사용성
- 결론
NestJS의 리포지토리 패턴은 데이터 접근과 비즈니스 로직을 명확히 분리하여 코드 품질을 높이고 유지보수성을 강화하는데 매우 유용하다. TypeORM과 같은 ORM과 함께 사용하면 객체지 향 프로그래밍의 장점을 살리면서도 효율적인 데이터 관리가 가능하다.
'BackEnd' 카테고리의 다른 글
| [Redis] Redis, Redis Pub/Sub (Redis Stream) (0) | 2025.06.06 |
|---|---|
| 소프트웨어 아키텍처 (Software Architecture) (0) | 2025.01.16 |
| 로드 밸런서 (0) | 2024.12.31 |
| [bcrypt] 비밀번호가 관리되는 방식 (0) | 2024.12.27 |
| 파일 송수신[1] (feat. S3, ws) (2) | 2024.12.19 |