GoF 디자인 패턴: 생성 패턴 - 🚀정처기 실기 대비 문제 포함
요약
객체지향 프로그래밍의 핵심, GoF(Gang of Four) 디자인 패턴 중 생성 패턴인 싱글톤(Singleton), 팩토리 메서드(Factory Method), 빌더(Builder), 프로토타입(Prototype), 추상 팩토리(Abstract Factory)를 자바스크립트 코드와 함께 알아봅니다. 정처기 실기 대비를 위한 주관식 문제가 포함되어 있습니다.
포스팅 마지막에 정처기 실기 대비 실전 주관식 문제가 있습니다.
🎯 GoF 디자인 패턴이란?
GoF는 'Gang of Four'의 약자로, 객체지향 프로그래밍 분야에서 자주 발생하는 문제들에 대한 해결책을 담은 디자인 패턴 책의 저자 4명을 의미합니다. 이들이 정리한 패턴들은 코드의 재사용성, 유지보수성, 확장성을 높이는 데 큰 도움을 줍니다. ✨
디자인 패턴은 크게 생성(Creational), 구조(Structural), 행위(Behavioral) 세 가지로 나뉩니다. 이번 글에서는 객체를 생성하는 방법을 다루는 생성 패턴 중 가장 대표적인 다섯 가지를 살펴보겠습니다. 🏗️
1. 👑 싱글톤 (Singleton) 패턴
싱글톤 패턴은 이름 그대로, 한 클래스에 단 하나의 객체만 존재하도록 강제하는 디자인 패턴입니다. single
이라는 단어에서 알 수 있듯, 인스턴스가 유일하다는 것이 핵심입니다. 👑
🏗️ 기본 구조
클래스 기반 언어에서 싱글톤은 보통 다음과 같이 구현합니다.
class Singleton {
constructor() {
// 인스턴스가 이미 존재하는지 확인합니다.
if (Singleton.instance) {
return Singleton.instance;
}
this.message = "나는 클래스 기반 유일 인스턴스야";
// 없다면, 현재 인스턴스를 저장합니다.
Singleton.instance = this;
}
}
// 사용 예시
const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true ✅
console.log(a.message); // "나는 클래스 기반 유일 인스턴스야"
new Singleton()
을 두 번 호출했지만, 변수 a
와 b
는 정확히 동일한 인스턴스를 참조합니다. 🎯
🤔 언제 필요할까?
애플리케이션 전역에서 공유되어야 하는 자원이 있을 때 유용합니다. 대표적인 예로 로그 수집기(Logger) 가 있습니다. 📝
만약 Logger 인스턴스가 여러 개라면 로그가 분산되어 기록되는 문제가 발생할 수 있습니다. ⚠️
class Logger {
constructor(name) {
this.name = name;
this.logs = [];
}
log(message) {
this.logs.push(`[${this.name}] ${message}`);
}
printLogs() {
console.log(this.logs.join("\n"));
}
}
// 인스턴스가 두 개 생성된 경우 😵
const loggerA = new Logger("A");
const loggerB = new Logger("B");
loggerA.log("서버 시작");
loggerB.log("DB 연결됨");
loggerA.log("사용자 로그인");
// 로그가 각 인스턴스에 따로 저장됩니다. 😞
loggerA.printLogs();
// [A] 서버 시작
// [A] 사용자 로그인
loggerB.printLogs();
// [B] DB 연결됨
이 문제를 해결하기 위해 Logger에 싱글톤 패턴을 적용하면, 여러 곳에서 인스턴스를 생성하더라도 실제로는 단 하나의 인스턴스만 사용하게 되어 로그를 중앙에서 관리할 수 있습니다. ✅
class Logger {
constructor() {
if (Logger.instance) {
return Logger.instance;
}
this.logs = [];
Logger.instance = this;
}
log(message) {
this.logs.push(message);
}
printLogs() {
console.log(this.logs.join("\n"));
}
}
// 여러 곳에서 Logger를 호출해도 괜찮습니다. 😊
const logger1 = new Logger();
const logger2 = new Logger();
logger1.log("서버 시작");
logger2.log("DB 연결됨");
logger1.log("사용자 로그인");
// 모든 로그가 하나의 인스턴스에 기록됩니다. 🎉
logger1.printLogs();
/*
서버 시작
DB 연결됨
사용자 로그인
*/
console.log(logger1 === logger2); // true ✅
💡 싱글톤 패턴 중요 키워드
- 클래스에 오직 한 인스턴스만 존재하도록 강제한다. 👑
2. 🏭 팩토리 메서드 (Factory Method) 패턴
팩토리 메서드 패턴은 객체 생성을 위한 인터페이스는 상위 클래스에서 정의하되, 실제로 어떤 클래스의 인스턴스를 만들지는 하위 클래스에서 결정하도록 하는 패턴입니다. ⚙️
이 패턴을 사용하면 Creator
(생성자)와 Product
(제품)라는 두 가지 주요 구조가 나타납니다.
- Product: 생성될 객체의 공통 인터페이스를 정의합니다. 📋
- Creator:
Product
를 생성하는factoryMethod()
를 정의합니다. 실제 생성 로직은 하위Creator
가 담당합니다. 🏗️
🏗️ 기본 구조
결제 시스템을 예로 들어보겠습니다. 다양한 결제 수단(카드, 페이팔 등)이 있지만, '결제 처리'라는 큰 흐름은 동일합니다. 💳
// Product 인터페이스 (결제 게이트웨이)
class PaymentGateway {
pay(amount) {
throw new Error("pay()는 반드시 구현되어야 합니다.");
}
}
// Concrete Product 1 (카드 결제)
class CardGateway extends PaymentGateway {
pay(amount) {
console.log(`💳 카드로 ${amount}원 결제 완료`);
}
}
// Concrete Product 2 (페이팔 결제)
class PaypalGateway extends PaymentGateway {
pay(amount) {
console.log(`🅿️ 페이팔로 ${amount}원 결제 완료`);
}
}
// Creator (상위 결제 처리기)
class PaymentProcessor {
// 이 메서드는 구체적인 결제 방식에 의존하지 않습니다. 🎯
process(amount) {
const gateway = this.createGateway(); // Factory Method 호출
gateway.pay(amount);
}
// Factory Method (하위 클래스가 구체적인 내용을 구현)
createGateway() {
throw new Error("createGateway()는 반드시 오버라이드되어야 합니다.");
}
}
// Concrete Creator 1 (카드 결제 처리기)
class CardPaymentProcessor extends PaymentProcessor {
createGateway() {
return new CardGateway();
}
}
// Concrete Creator 2 (페이팔 결제 처리기)
class PaypalPaymentProcessor extends PaymentProcessor {
createGateway() {
return new PaypalGateway();
}
}
// 사용 예시 ✨
const cardProcessor = new CardPaymentProcessor();
cardProcessor.process(10000); // 💳 카드로 10000원 결제 완료
const paypalProcessor = new PaypalPaymentProcessor();
paypalProcessor.process(20000); // 🅿️ 페이팔로 20000원 결제 완료
🚀 확장성
팩토리 메서드의 가장 큰 장점은 확장성입니다. 새로운 결제 수단 '카카오페이'를 추가해야 할 경우, 기존 코드를 전혀 수정할 필요가 없습니다. 🎉
// 새로운 Concrete Product
class KakaoPayGateway extends PaymentGateway {
pay(amount) {
console.log(`💛 카카오페이로 ${amount}원 결제`);
}
}
// 새로운 Concrete Creator
class KakaoPayPaymentProcessor extends PaymentProcessor {
createGateway() {
return new KakaoPayGateway();
}
}
// 새로운 기능 바로 사용 가능 🚀
const kakaoProcessor = new KakaoPayPaymentProcessor();
kakaoProcessor.process(15000); // 💛 카카오페이로 15000원 결제
만약 팩토리 메서드를 사용하지 않고 단순 if/switch
분기문으로 처리했다면 어땠을까요? 🤔
function createGateway(type) {
if (type === "card") return new CardGateway();
if (type === "paypal") return new PaypalGateway();
// '카카오페이' 추가 시 아래 코드 수정이 불가피합니다. 😰
if (type === "kakaopay") return new KakaoPayGateway(); // ❌ OCP 위반
}
✅ 핵심 개념: OCP (Open-Closed Principle, 개방-폐쇄 원칙)
팩토리 메서드는 객체지향 설계 5원칙(SOLID) 중 하나인 OCP를 잘 보여줍니다. 📐
- 기능 확장에는 열려있다 (Open): 새로운 결제 수단(
KakaoPay
)을 쉽게 추가할 수 있습니다. ➕ - 코드 수정에는 닫혀있다 (Closed): 새로운 기능을 추가하기 위해 기존의
PaymentProcessor
나 다른 결제 관련 클래스를 수정할 필요가 없습니다. 🔒
이처럼 팩토리 메서드 패턴을 사용하면 변화에 유연하고 확장성 좋은 시스템을 만들 수 있습니다. 💪
💡 팩토리 메서드 패턴 중요 키워드
- 상위 클래스에서 객체를 생성하는 인터페이스를 정의하고 🏗️
- 하위 클래스에서 인스턴스를 생성한다. ⚙️
- 인터페이스와 실제 객체를 생성하는 클래스를 분리한다. 🔀
3. 🔨 빌더 (Builder) 패턴
빌더 패턴은 복잡한 객체를 생성하는 과정을 단계별로 분리하여, 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 디자인 패턴입니다. 특히 생성자의 매개변수가 많거나, 객체 생성 과정이 여러 단계로 나뉘어 복잡할 때 유용합니다. 🏗️
😵 이런 코드를 보신 적 있나요?
객체를 생성할 때 생성자의 매개변수가 너무 많아지는 경우를 '텔레스코핑 생성자 패턴(Telescoping Constructor Pattern)'이라고 부르기도 합니다.
// 💩 매개변수가 너무 많은 생성자
class HttpRequest {
constructor(method, url, headers, body, timeout, retries, cache, useHttps) {
this.method = method;
this.url = url;
this.headers = headers || {};
this.body = body || null;
this.timeout = timeout || 10000;
this.retries = retries || 3;
this.cache = cache || false;
this.useHttps = useHttps || true;
}
}
// 사용하기가 매우 불편합니다. 😫
const req = new HttpRequest(
"GET",
"https://api.example.com/data",
{ "Content-Type": "application/json" },
null,
15000,
5,
true,
true
);
위 코드의 문제점은 다음과 같습니다. ⚠️
- 가독성 저하: 각 매개변수가 어떤 역할을 하는지 파악하기 어렵습니다. 😵
- 실수 유발: 매개변수의 순서를 잘못 입력하기 쉽습니다. 🔀
- 유연성 부족: 일부 매개변수만 설정하고 싶어도
null
이나undefined
를 계속 전달해야 합니다. 😓
✨ 빌더 패턴으로 개선하기
빌더 패턴을 사용하면 이 문제를 깔끔하게 해결할 수 있습니다.
// Product (결과물)
class HttpRequest {
constructor(builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = builder.headers;
this.body = builder.body;
this.timeout = builder.timeout;
this.retries = builder.retries;
this.cache = builder.cache;
this.useHttps = builder.useHttps;
}
send() {
console.log(`[${this.method}] ${this.url} 로 요청 전송 🚀`);
// ... 실제 요청 로직 ...
}
}
// Builder
class HttpRequestBuilder {
constructor() {
// 기본값 설정 ⚙️
this.method = "GET";
this.headers = {};
this.timeout = 10000;
this.retries = 3;
this.cache = false;
this.useHttps = true;
}
// 각 설정 메서드는 빌더 인스턴스 자신(this)을 반환합니다. (메서드 체이닝) 🔗
setMethod(method) {
this.method = method;
return this; // 자신을 반환하여 메서드 체이닝 가능
}
setUrl(url) {
this.url = url;
return this;
}
setHeaders(headers) {
this.headers = headers;
return this;
}
setBody(body) {
this.body = body;
return this;
}
setTimeout(timeout) {
this.timeout = timeout;
return this;
}
// ... 다른 설정 메서드들 ...
// 최종적으로 build() 메서드가 HttpRequest 객체를 생성합니다. 🏗️
build() {
if (!this.url) {
throw new Error("URL은 필수 항목입니다. ⚠️");
}
return new HttpRequest(this);
}
}
// ✨ 사용법이 훨씬 명확하고 유연해졌습니다.
const request = new HttpRequestBuilder()
.setMethod("POST")
.setUrl("https://api.example.com/users")
.setHeaders({ "Content-Type": "application/json" })
.setBody({ name: "John Doe" })
.setTimeout(5000)
.build();
request.send(); // [POST] https://api.example.com/users 로 요청 전송 🚀
위 예시를 통해 복합 객체(많은 속성, 중첩 구조...)를 생성할 때 객체를 생성하는 방법(HttpRequestBuilder
클래스)과 객체를 구현하는 방법(HttpRequest
클래스)을 분리하는 빌더 패턴에 대해 알아봤습니다. 🎯
💡 빌더 패턴 중요 키워드
복합 객체(복잡한 객체)를 생성할때 🏗️
객체를 생성하는 방법(과정) ⚙️
객체를 구현(표현)하는 방법 📋
두 방법을 분리한다. 🔀
4. 🧬 프로토타입 (Prototype) 패턴
프로토타입 패턴은 기존 객체를 복제하여 새로운 객체를 생성하는 디자인 패턴입니다. new
키워드를 사용하여 클래스로부터 객체를 만드는 대신, 원본(prototype) 객체를 복사하여 필요한 부분을 수정한 뒤 사용하는 방식입니다. 특히 객체 생성 비용이 비싸거나, 클래스로부터 객체를 만드는 과정이 복잡할 때 유용합니다. 💡
자바스크립트는 프로토타입 기반 언어이기 때문에 이 패턴을 매우 자연스럽게 활용할 수 있습니다. 🚀
🏗️ 기본 구조
게임 캐릭터를 생성하는 예시를 통해 알아보겠습니다. 모든 캐릭터는 기본 능력치(체력, 공격력)를 공유하지만, 각자 다른 이름과 직업을 가질 수 있습니다. ⚔️
// Prototype (원본 객체)
const characterPrototype = {
hp: 100,
mp: 50,
attack(target) {
console.log(`${this.name}이(가) ${target}을(를) 공격합니다! ⚔️`);
},
// 객체를 복제하는 메서드
clone() {
// Object.create()를 사용해 프로토타입을 기반으로 새 객체를 만듭니다. 🧬
return Object.create(this);
},
};
// 프로토타입을 복제하여 새로운 객체 생성 ✨
const warrior = characterPrototype.clone();
warrior.name = "강한 전사";
warrior.job = "Warrior";
warrior.hp = 150; // 기본값에서 변경
const mage = characterPrototype.clone();
mage.name = "현명한 마법사";
mage.job = "Mage";
mage.mp = 120; // 기본값에서 변경
warrior.attack("오크"); // 강한 전사이(가) 오크을(를) 공격합니다! ⚔️
mage.attack("고블린"); // 현명한 마법사이(가) 고블린을(를) 공격합니다! ⚔️
console.log(warrior.hp); // 150 💪
console.log(mage.hp); // 100 (원본 프로토타입의 값을 그대로 사용) ✅
Object.create()
는 지정된 프로토타입 객체를 사용하여 새 객체를 만듭니다. 이렇게 생성된 객체는 원본의 속성과 메서드를 상속받으면서, 자신만의 고유한 상태를 가질 수 있습니다. 🎯
🤔 언제 필요할까?
- 객체 생성 비용이 클 때: 데이터베이스에서 무거운 데이터를 조회하여 객체를 만들어야 할 경우, 매번 생성하는 대신 한 번 만든 객체를 복제하여 사용하면 성능상 이점을 얻을 수 있습니다. 💾
- 다양한 종류의 객체를 생성해야 할 때: 팩토리 메서드 패턴처럼 하위 클래스를 많이 만들지 않고도, 프로토타입 객체 몇 개만으로 다양한 변형 객체를 쉽게 만들 수 있습니다. 🎨
자바스크립트에서는 Object.create()
나 전개 구문({...original, ...overrides}
)을 통해 프로토타입 패턴을 간단하게 구현할 수 있습니다. ⚡
💡 프로토타입 패턴 중요 키워드
원본 객체를 복제하여 새 객체를 생성한다. 🧬
new
키워드 없이 객체를 만들 수 있다. ✨객체 생성 비용이 비쌀 때 효과적이다. 💰
5. 🏪 추상 팩토리 (Abstract Factory) 패턴
추상 팩토리 패턴은 서로 관련이 있거나 의존적인 객체들의 집합을 생성하기 위한 인터페이스를 제공하되, 구체적인 클래스는 지정하지 않는 패턴입니다. 팩토리 메서드 패턴이 단일 객체를 생성하는 것에 초점을 맞춘다면, 추상 팩토리는 '제품군(Family of Products)'을 생성하는 데 사용됩니다. 👨👩👧👦
가장 흔한 예는 UI 컴포넌트의 테마(Theme)를 관리하는 것입니다. 라이트 모드와 다크 모드에 따라 버튼, 체크박스, 창(Window) 등 여러 UI 요소가 일관된 스타일을 가져야 합니다. 🎨
🏗️ 기본 구조
// Abstract Product A: Button
class Button {
paint() {
throw new Error("paint() must be implemented.");
}
}
// Concrete Product A1: LightButton
class LightButton extends Button {
paint() {
console.log("🎨 라이트 테마 버튼을 그립니다. ☀️");
}
}
// Concrete Product A2: DarkButton
class DarkButton extends Button {
paint() {
console.log("🎨 다크 테마 버튼을 그립니다. 🌙");
}
}
// Abstract Product B: Checkbox
class Checkbox {
paint() {
throw new Error("paint() must be implemented.");
}
}
// Concrete Product B1: LightCheckbox
class LightCheckbox extends Checkbox {
paint() {
console.log("✅ 라이트 테마 체크박스를 그립니다. ☀️");
}
}
// Concrete Product B2: DarkCheckbox
class DarkCheckbox extends Checkbox {
paint() {
console.log("✅ 다크 테마 체크박스를 그립니다. 🌙");
}
}
// Abstract Factory
class GUIFactory {
createButton() {
throw new Error("createButton() must be implemented.");
}
createCheckbox() {
throw new Error("createCheckbox() must be implemented.");
}
}
// Concrete Factory 1: LightThemeFactory
class LightThemeFactory extends GUIFactory {
createButton() {
return new LightButton();
}
createCheckbox() {
return new LightCheckbox();
}
}
// Concrete Factory 2: DarkThemeFactory
class DarkThemeFactory extends GUIFactory {
createButton() {
return new DarkButton();
}
createCheckbox() {
return new DarkCheckbox();
}
}
// Client Code 🎯
function renderUI(factory) {
const button = factory.createButton();
const checkbox = factory.createCheckbox();
button.paint();
checkbox.paint();
}
// 라이트 테마로 UI 렌더링 ☀️
console.log("--- 라이트 모드 UI ---");
const lightFactory = new LightThemeFactory();
renderUI(lightFactory);
// 🎨 라이트 테마 버튼을 그립니다. ☀️
// ✅ 라이트 테마 체크박스를 그립니다. ☀️
// 다크 테마로 UI 렌더링 🌙
console.log("\n--- 다크 모드 UI ---");
const darkFactory = new DarkThemeFactory();
renderUI(darkFactory);
// 🎨 다크 테마 버튼을 그립니다. 🌙
// ✅ 다크 테마 체크박스를 그립니다. 🌙
🆚 팩토리 메서드 vs. 추상 팩토리
- 팩토리 메서드: 하나의
Creator
클래스가 상속을 통해 하나의 제품을 생성하는 방법을 변경합니다. 🏭 - 추상 팩토리: 여러 제품군을 생성하는 인터페이스를 제공합니다. 클라이언트는 구체적인 제품 클래스를 알 필요 없이, 팩토리만 교체하면 전체 제품군을 한 번에 바꿀 수 있습니다. 🏪
💡 추상 팩토리 패턴 중요 키워드
- 관련 객체들의 '가족(family)'을 생성한다. 👨👩👧👦
- 클라이언트 코드는 구체적인 클래스에 의존하지 않는다. 🔒
- 전체 제품군을 쉽게 교체할 수 있다. 🔄
- 사용자에게 API를 제공하고, 구체적인 구현은 Concrete Product 클래스에서 이루어진다. 🎯
🤔 'API 제공'과 '구체적 구현'의 의미
여기서 "사용자에게 API를 제공한다"는 말의 '사용자'는 실제 사람이 아닌, 팩토리를 가져다 쓰는 코드(클라이언트 코드) 를 의미합니다.
- API 제공 (추상 클래스):
GUIFactory
,Button
,Checkbox
같은 추상 클래스는 일종의 '계약서' 또는 '설명서' 역할을 합니다. 📋 클라이언트 코드(renderUI
함수)는 이 '설명서'만 보고createButton()
이나paint()
같은 메서드를 호출합니다. 클라이언트는LightButton
이나DarkButton
의 실제 구현을 전혀 알 필요가 없습니다. 🔒 - 구체적 구현 (Concrete 클래스):
LightThemeFactory
나DarkButton
같은 Concrete 클래스는 '계약서'의 내용을 실제로 이행하는 '실무자'입니다. 👷♂️createButton()
요청이 오면 실제 라이트/다크 테마 버튼 객체를 생성하고,paint()
요청이 오면 실제 테마에 맞는 스타일을 그리는 로직을 수행합니다.
결론적으로, 클라이언트 코드는 구체적인 클래스 이름(LightButton
)을 코드에 직접 쓸 필요 없이, 추상적인 '설명서(API)'에만 의존하게 됩니다. 덕분에 팩토리만 교체하면 전체 제품군(테마)을 한 번에 바꿀 수 있는 유연한 구조가 만들어집니다. 🔄✨
정처기 실기 대비 실전 문제
문제 | 구체적인 클래스에 의존하지 않고 서로 연관되거나 의존적인 객체들의 조합을 만드는 인터페이스를 제공하는 패턴으로 이 패턴을 통해 생성된 클래스에서는 사용자에게 인터페이스(API)를 제공하고, 구체적인 구현은 Concrete Product 클래스에서 이루어지는 특징을 갖는 디자인 패턴 |
답 | |
정답 | 정답 확인하기 |
문제 | 처음부터 일반적인 원형을 만들어 놓고, 그것을 복사한 후 필요한 부분만 수정하여 사용하는 패턴으로, 생성할 객체의 원형을 제공하는 인스턴스에서 생성할 객체들의 타입이 결정되도록 설정하며 객체를 생성할 때 갖추어야 할 기본 형태가 있을 때 사용되는 디자인 패턴 |
답 | |
정답 | 정답 확인하기 |