소프트웨어 디자인, 설계
디자인 패턴
디자인 패턴은 소프트웨어 개발에서 자주 발생하는 문제를 해결하기 위한 일반적인 해결책을 말합니다. 특정 언어에 종속되지 않으며, 코드 재사용성과 유지보수성을 향상시키는 데 도움이 됩니다.
디자인 패턴은 크게 GOF (Gang of Four) 패턴과 POSA (Pattern-Oriented Software Architecture) 패턴으로 나눌 수 있습니다.
1. GOF (Gang of Four) 디자인 패턴
객체지향 프로그래밍(OOP)에 초점을 맞추어 클래스 간의 관계와 상호작용을 정형화한 패턴입니다.
GOF 패턴의 분류
- 행위(Behavioral) : 객체 간의 상호작용 및 책임 분배와 관련된 패턴으로, 객체 간의 커뮤니케이션 방식을 최적화함.
1.1 생성 패턴
객체의 생성 로직을 효율적으로 관리하여, 불필요한 객체 생성을 방지하고 코드의 유연성을 높이는 패턴입니다.
- 싱글톤 (Singleton) 하나의 인스턴스만 생성하고 이를 공유함.
- 팩토리 메서드 (Factory Method) 객체 생성을 하위 클래스에서 결정하도록 위임함.
- 추상 팩토리 (Abstract Factory) 관련 객체들을 일관된 방식으로 생성함.
- 빌더 (Builder) 복잡한 객체 생성을 단계별로 수행함.
- 프로토타입 (Prototype) 기존 객체를 복사하여 새로운 객체를 생성함.
글로 읽으면 이해가 잘 안되는데요. 소스코드로 예시 들게요. OOP, FP 두개다 예시 들겠습니다. 싱글톤만 예시를 듭니다.
싱글톤 패턴 예시
//OOP
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
sayHello() {
console.log("Hello from Singleton!");
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
instance1.sayHello(); // "Hello from Singleton!"
//FP
//클래스를 사용하지 않고 클로저(Closure)를 활용하여 하나의 인스턴스만 유지합니다.
const createSingleton = (() => {
let instance = null;
return () => {
if (!instance) {
instance = { message: "Hello from Singleton!" };
}
return instance;
};
})();
const instance1 = createSingleton();
const instance2 = createSingleton();
console.log(instance1 === instance2); // true
console.log(instance1.message); // "Hello from Singleton!"
1.2 구조 패턴
클래스와 객체의 구조를 다루며, 효율적인 클래스 설계 및 객체 조합 방법을 제공합니다.
- 어댑터 (Adapter) : 서로 다른 인터페이스를 가진 클래스를 호환되도록 연결함.
- 브리지 (Bridge) : 기능과 구현을 분리하여 독립적으로 확장 가능하도록 함.
- 컴포지트 (Composite) : 객체를 트리 구조로 구성하여 계층적인 구조를 표현함.
- 데코레이터 (Decorator) : 객체의 기능을 동적으로 확장할 수 있도록 함.
- 퍼사드 (Facade) : 복잡한 시스템을 단순한 인터페이스로 제공함.
- 플라이웨이트 (Flyweight) : 공유 가능한 객체를 사용하여 메모리 사용을 줄임.
- 프록시 (Proxy) : 접근을 제어하는 대리 객체를 제공함.
어댑터 예시
// 기존 시스템 (Old System)
class OldSystem {
oldMethod() {
console.log("Old system method");
}
}
// 새로운 시스템 (New System)
class NewSystem {
newMethod() {
console.log("New system method");
}
}
// 어댑터
class Adapter {
constructor(oldSystem) {
this.oldSystem = oldSystem;
}
newMethod() {
this.oldSystem.oldMethod(); // 기존 코드 재사용
}
}
const oldSystem = new OldSystem();
const adapter = new Adapter(oldSystem);
adapter.newMethod(); // "Old system method"
const oldSystem = () => "Old system method";
const adapter = (fn) => () => fn();
const newSystem = adapter(oldSystem);
console.log(newSystem()); // "Old system method"
1.3 행동 패턴
객체 간의 상호작용을 다루며, 효율적인 커뮤니케이션과 책임 분배를 위한 패턴입니다.
- 책임 연쇄 (Chain of Responsibility) : 요청을 처리할 객체를 체인 형태로 연결하여 순차적으로 처리함.
- 커맨드 (Command) : 요청을 객체로 캡슐화하여 실행을 나중에 할 수 있도록 함.
- 인터프리터 (Interpreter) : 언어의 문법을 정의하고 해석하는 구조를 제공함.
- 반복자 (Iterator) : 컬렉션 요소에 접근하는 방법을 표준화함.
- 중재자 (Mediator) : 객체 간의 직접적인 통신을 막고 중재자를 통해 상호작용하도록 함.
- 메멘토 (Memento) : 객체의 상태를 저장하고 복원할 수 있도록 함.
- 옵저버 (Observer) : 상태 변화가 있을 때 여러 객체에게 자동으로 알림을 보냄.
- 상태 (State) : 객체의 상태에 따라 행동을 변경할 수 있도록 함.
- 전략 (Strategy) : 알고리즘을 캡슐화하여 동적으로 변경할 수 있도록 함.
- 템플릿 메서드 : (Template Method) 알고리즘의 구조를 정의하고, 세부 구현은 하위 클래스에서 처리하도록 함.
- 방문자 (Visitor) : 객체 구조를 변경하지 않고 새로운 연산을 추가할 수 있도록 함.
옵저버 패턴 예시
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
notifyObservers(message) {
this.observers.forEach(observer => observer.update(message));
}
}
class Observer {
constructor(name) {
this.name = name;
}
update(message) {
console.log(`${this.name} received: ${message}`);
}
}
const chatRoom = new Subject();
const user1 = new Observer("Alice");
const user2 = new Observer("Bob");
chatRoom.addObserver(user1);
chatRoom.addObserver(user2);
chatRoom.notifyObservers("New message in chat!");
// Alice received: New message in chat!
// Bob received: New message in chat!
const createSubject = () => {
let observers = [];
return {
subscribe: (observer) => (observers = [...observers, observer]),
notify: (message) => observers.forEach((observer) => observer(message))
};
};
const subject = createSubject();
const observer1 = (msg) => console.log(`Alice received: ${msg}`);
const observer2 = (msg) => console.log(`Bob received: ${msg}`);
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify("New message in chat!");
2. POSA 패턴
POSA 패턴은 아키텍처 수준의 패턴을 다루며, 크게 4가지 카테고리로 나뉩니다.
- 상호작용 (Distributed Systems) : 여러 컴포넌트가 통신하는 방식을 정의
- 적응성 (Adaptation & Extension) : 변경과 확장을 쉽게 할 수 있도록 설계하는 패턴
- 동시성 (Concurrency) : 병렬 실행을 다룰 수 있도록 설계하는 패턴
2.1 분할
- 소프트웨어 시스템을 명확한 모듈로 나누는 패턴
- 시스템을 여러 계층(layer) 으로 분리하여, 각 계층이 특정 역할을 수행하도록 설계.
- 보통 Presentation → Business Logic → Data Access 형태로 나뉨.
class DataAccess {
fetchData() {
return "Fetching data from database";
}
}
class BusinessLogic {
constructor(dataAccess) {
this.dataAccess = dataAccess;
}
processData() {
return this.dataAccess.fetchData() + " → Processing data";
}
}
class Presentation {
constructor(businessLogic) {
this.businessLogic = businessLogic;
}
render() {
console.log(this.businessLogic.processData() + " → Rendering UI");
}
}
// 계층 연결
const dataLayer = new DataAccess();
const businessLayer = new BusinessLogic(dataLayer);
const uiLayer = new Presentation(businessLayer);
uiLayer.render();
// Output: Fetching data from database → Processing data → Rendering UI
const fetchData = () => "Fetching data from database";
const processData = (fetchFn) => () => fetchFn() + " → Processing data";
const render = (processFn) => () => console.log(processFn() + " → Rendering UI");
// 계층 연결
const businessLayer = processData(fetchData);
const uiLayer = render(businessLayer);
uiLayer();
// Output: Fetching data from database → Processing data → Rendering UI
2.2 상호작용
- 중앙의 브로커(중개자)가 여러 개의 클라이언트와 서비스를 연결하는 구조.
- 주로 메시지 큐(Message Queue)와 이벤트 기반 시스템에서 사용됨.
class Broker {
constructor() {
this.services = {};
}
registerService(name, service) {
this.services[name] = service;
}
requestService(name, data) {
return this.services[name] ? this.services[name](data) : "Service not found";
}
}
// 서비스 정의
const broker = new Broker();
broker.registerService("logging", (message) => `Logging: ${message}`);
broker.registerService("auth", (user) => `Authenticating: ${user}`);
// 서비스 호출
console.log(broker.requestService("logging", "User logged in"));
console.log(broker.requestService("auth", "Alice"));
//Broker 클래스가 모든 요청을 중개
const broker = (() => {
const services = new Map();
return {
register: (name, service) => services.set(name, service),
request: (name, data) => services.get(name)?.(data) || "Service not found",
};
})();
// 서비스 등록
broker.register("logging", (message) => `Logging: ${message}`);
broker.register("auth", (user) => `Authenticating: ${user}`);
// 서비스 호출
console.log(broker.request("logging", "User logged in"));
console.log(broker.request("auth", "Alice"));
// 클로저(Closure)를 사용해 상태를 유지
// broker 객체가 직접 함수 등록 및 실행
2.3 적응성
- 핵심 기능(코어)을 최소화하고, 나머지는 플러그인(확장 기능)으로 추가하는 방식.
- 예시: VSCode의 확장 플러그인, Express.js 미들웨어
//OOP
class Microkernel {
constructor() {
this.plugins = [];
}
registerPlugin(plugin) {
this.plugins.push(plugin);
}
execute() {
this.plugins.forEach((plugin) => plugin());
}
}
// 플러그인 추가
const kernel = new Microkernel();
kernel.registerPlugin(() => console.log("Plugin 1 executed"));
kernel.registerPlugin(() => console.log("Plugin 2 executed"));
kernel.execute();
//Microkernel 클래스가 핵심 기능을 제공
//FP
const createKernel = () => {
let plugins = [];
return {
register: (plugin) => (plugins = [...plugins, plugin]),
execute: () => plugins.forEach((plugin) => plugin()),
};
};
// 플러그인 추가
const kernel = createKernel();
kernel.register(() => console.log("Plugin 1 executed"));
kernel.register(() => console.log("Plugin 2 executed"));
kernel.execute();
3. 결론: GOF, POSA 패턴의 차이점과 현업에서는 어떻게 사용될까?
GOF 패턴 : OOP 기반의 개발 (ex. Java, C#), 객체 간의 관계를 효율적으로 관리
POSA 패턴 : 대규모 시스템 설계 (ex. 마이크로서비스, 분산 시스템, 이벤트 기반 처리)
JavaScript/Node.js 기반 프로젝트에서는 다음 패턴이 많이 사용됨
- GOF 패턴 → 싱글톤, 팩토리, 옵저버
- POSA 패턴 → MVC, API Gateway, Event-Driven Architecture
적절한 패턴을 선택하면 유지보수성과 확장성이 뛰어난 소프트웨어를 만들 수 있다고 생각합니다.