JavaScript 디자인 패턴: Factory와 Singleton의 진짜 이야기
“왜 Class 하나로 다 만들면 안 되나요?” - 과거의 저도 이렇게 생각했습니다.
여러분도 이런 경험 있으신가요?
시나리오 1: 객체 생성의 악몽
저는 최근에 게임 프로젝트를 진행하면서 캐릭터 시스템을 구현했습니다. 처음에는 간단했어요:
// 처음엔 이렇게 시작했죠
class Warrior {
constructor(name, level) {
this.name = name;
this.level = level;
this.hp = 100;
this.attack = 50;
}
}
const player = new Warrior('아서', 1);
“괜찮은데? 뭐가 문제야?”
하지만 게임이 복잡해지면서…
// 마법사 추가
class Mage {
constructor(name, level) {
this.name = name;
this.level = level;
this.hp = 70;
this.attack = 30;
this.mana = 100;
}
}
// 궁수 추가
class Archer {
constructor(name, level) {
this.name = name;
this.level = level;
this.hp = 80;
this.attack = 40;
this.range = 'long';
}
// 어? 궁수는 특별한 스킬이...
specialAttack() { /* ... */ }
}
// 이제 캐릭터 생성 코드가...
function createCharacter(type, name, level) {
if (type === 'warrior') {
return new Warrior(name, level);
} else if (type === 'mage') {
return new Mage(name, level);
} else if (type === 'archer') {
return new Archer(name, level);
}
// 새 캐릭터 타입 추가할 때마다 이 함수 수정... 😫
}
문제점들:
- 새 캐릭터 타입 추가 시
createCharacter함수 수정 필요 - 캐릭터별 초기화 로직이 흩어져 있음
- 타입 체크가
if-else지옥으로…
시나리오 2: 설정 관리의 혼돈
다음 문제는 게임 설정 관리였습니다.
// 여러 파일에서 설정 객체를 생성
// utils.js
const config1 = {
volume: 50,
difficulty: 'normal'
};
// player.js
const config2 = {
volume: 70, // 어? 다른 값?
difficulty: 'hard'
};
// ui.js
const config3 = {
volume: 50,
difficulty: 'normal'
};
“이게 왜 문제죠?”
// utils.js에서 설정 변경
config1.volume = 100;
// 하지만 ui.js의 config3는 여전히 50...
console.log(config3.volume); // 50 (???)
각 파일이 다른 설정 객체를 사용하고 있었던 거예요!
문제점들:
- 설정 동기화 불가능
- 어디서든 새로운 설정 객체 생성 가능
- 일관성 없는 상태 관리
Factory Pattern: 객체 생성의 마법사
“공장”을 상상해보세요
실제 공장을 떠올려봅시다.
고객: "자동차 한 대 주세요!"
공장: "어떤 종류로 드릴까요? 세단, SUV, 트럭?"
고객: "SUV요!"
공장: "알겠습니다!"
→ [제조 과정...]
→ "여기 새 SUV입니다!"
Factory Pattern도 똑같습니다.
개발자: "캐릭터 하나 주세요!"
Factory: "어떤 타입으로 드릴까요?"
개발자: "전사요!"
Factory: "알겠습니다!"
→ [생성 로직 실행...]
→ "여기 새 전사입니다!"
Factory Pattern의 진짜 모습
Before: 혼돈의 객체 생성
// ❌ 문제투성이 코드
function createCharacter(type, name, level) {
if (type === 'warrior') {
const char = new Warrior(name, level);
char.weapon = 'sword';
char.armor = 'heavy';
return char;
} else if (type === 'mage') {
const char = new Mage(name, level);
char.weapon = 'staff';
char.spellbook = true;
return char;
} else if (type === 'archer') {
const char = new Archer(name, level);
char.weapon = 'bow';
char.arrows = 50;
return char;
}
}
문제점:
if-else지옥- 새 타입 추가 시 함수 수정
- 초기화 로직이 흩어짐
After: Factory Pattern 적용
// ✅ Factory Pattern
class CharacterFactory {
// 각 타입별 생성 메서드를 등록
static creators = {
warrior: (name, level) => {
const char = new Warrior(name, level);
char.weapon = 'sword';
char.armor = 'heavy';
return char;
},
mage: (name, level) => {
const char = new Mage(name, level);
char.weapon = 'staff';
char.spellbook = true;
return char;
},
archer: (name, level) => {
const char = new Archer(name, level);
char.weapon = 'bow';
char.arrows = 50;
return char;
}
};
// 팩토리 메서드
static create(type, name, level) {
const creator = this.creators[type];
if (!creator) {
throw new Error(`Unknown character type: ${type}`);
}
return creator(name, level);
}
// 새 타입 등록 (확장 가능!)
static register(type, creator) {
this.creators[type] = creator;
}
}
// 사용
const warrior = CharacterFactory.create('warrior', '아서', 1);
const mage = CharacterFactory.create('mage', '멀린', 1);
// 새 타입 추가가 쉬워요!
CharacterFactory.register('assassin', (name, level) => {
const char = new Assassin(name, level);
char.weapon = 'dagger';
char.stealth = true;
return char;
});
const assassin = CharacterFactory.create('assassin', '제로', 1);
개선 효과:
- ✅
if-else제거 - ✅ 새 타입 추가 시 기존 코드 수정 불필요
- ✅ 각 타입의 초기화 로직이 한 곳에
- ✅
register()메서드로 확장 가능
시각화: Factory Pattern의 흐름
┌─────────────────────────────────────────────────────┐
│ 개발자 코드 │
│ │
│ CharacterFactory.create('warrior', '아서', 1) │
│ │ │
│ ↓ │
├─────────────────────────────────────────────────────┤
│ CharacterFactory (공장) │
│ │
│ 1. creators['warrior'] 찾기 │
│ 2. creator 함수 실행 │
│ ├─ new Warrior() 생성 │
│ ├─ weapon = 'sword' 설정 │
│ └─ armor = 'heavy' 설정 │
│ 3. 완성된 객체 반환 │
│ │ │
│ ↓ │
├─────────────────────────────────────────────────────┤
│ 결과 │
│ │
│ { name: '아서', level: 1, weapon: 'sword', ... } │
└─────────────────────────────────────────────────────┘
실전 예제: UI 컴포넌트 생성
// ✅ 실무에서 자주 쓰는 패턴
class ButtonFactory {
static styles = {
primary: {
backgroundColor: '#007bff',
color: 'white',
border: 'none'
},
secondary: {
backgroundColor: '#6c757d',
color: 'white',
border: 'none'
},
outline: {
backgroundColor: 'transparent',
color: '#007bff',
border: '2px solid #007bff'
}
};
static create(type, text, onClick) {
const style = this.styles[type];
if (!style) {
throw new Error(`Unknown button type: ${type}`);
}
const button = document.createElement('button');
button.textContent = text;
button.onclick = onClick;
// 스타일 적용
Object.assign(button.style, style);
return button;
}
}
// 사용
const saveBtn = ButtonFactory.create('primary', '저장', () => {
console.log('저장됨!');
});
const cancelBtn = ButtonFactory.create('outline', '취소', () => {
console.log('취소됨!');
});
document.body.appendChild(saveBtn);
document.body.appendChild(cancelBtn);
왜 좋은가요?
- 버튼 생성 로직이 한 곳에
- 일관된 스타일 보장
- 새로운 버튼 타입 추가 쉬움
Singleton Pattern: 하나뿐인 존재
“대통령”을 떠올려보세요
한 나라에는 대통령이 단 한 명만 있어야 합니다.
국민1: "대통령 좀 불러주세요!"
→ 김대통령 등장
국민2: "저도 대통령 좀..."
→ 똑같은 김대통령 등장 (새 대통령이 아님!)
국민3: "대통령!"
→ 역시 김대통령 (동일 인물)
Singleton도 똑같습니다.
파일A: "Config 객체 주세요!"
→ Config 인스턴스 반환
파일B: "저도 Config 주세요!"
→ 똑같은 Config 인스턴스 (새로 만들지 않음!)
파일C: "Config!"
→ 역시 같은 Config
Singleton Pattern의 진짜 모습
Before: 혼돈의 설정 관리
// ❌ 문제: 여러 인스턴스 생성 가능
class Config {
constructor() {
this.theme = 'light';
this.language = 'ko';
this.volume = 50;
}
}
// utils.js
const config1 = new Config();
config1.theme = 'dark';
// ui.js
const config2 = new Config(); // 새 인스턴스!
console.log(config2.theme); // 'light' (???)
문제점:
- 각 파일이 다른 인스턴스 사용
- 설정 변경이 전역적으로 반영 안 됨
- 메모리 낭비 (여러 인스턴스)
After: Singleton Pattern 적용
// ✅ Singleton Pattern (Class 버전)
class Config {
static instance = null; // 유일한 인스턴스 저장
constructor() {
// 이미 인스턴스가 있으면 그것을 반환!
if (Config.instance) {
return Config.instance;
}
// 처음 생성될 때만 초기화
this.theme = 'light';
this.language = 'ko';
this.volume = 50;
// 인스턴스 저장
Config.instance = this;
}
setTheme(theme) {
this.theme = theme;
console.log(`테마가 ${theme}로 변경되었습니다.`);
}
getTheme() {
return this.theme;
}
}
// 테스트
const config1 = new Config();
config1.setTheme('dark');
const config2 = new Config(); // 새로 만들려고 했지만...
console.log(config2.getTheme()); // 'dark' (같은 인스턴스!)
console.log(config1 === config2); // true (완전히 동일!)
시각화:
첫 번째 new Config() 호출:
┌──────────────────────────────┐
│ Config.instance === null? │
│ ↓ YES │
│ 새 인스턴스 생성 │
│ Config.instance에 저장 │
│ ↓ │
│ 인스턴스 반환 │
└──────────────────────────────┘
두 번째 new Config() 호출:
┌──────────────────────────────┐
│ Config.instance === null? │
│ ↓ NO! │
│ 기존 인스턴스 반환 │
│ (새로 만들지 않음!) │
└──────────────────────────────┘
더 깔끔한 방법: 클로저 사용
// ✅ Singleton Pattern (함수형 - 더 안전함)
const Config = (function() {
let instance = null; // 클로저로 숨김!
function createInstance() {
// 실제 설정 객체
let settings = {
theme: 'light',
language: 'ko',
volume: 50
};
// Public API
return {
setTheme(theme) {
settings.theme = theme;
console.log(`테마가 ${theme}로 변경되었습니다.`);
},
getTheme() {
return settings.theme;
},
setLanguage(lang) {
settings.language = lang;
},
getLanguage() {
return settings.language;
},
getAllSettings() {
return { ...settings }; // 복사본 반환 (안전!)
}
};
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// 사용
const config1 = Config.getInstance();
config1.setTheme('dark');
const config2 = Config.getInstance();
console.log(config2.getTheme()); // 'dark' (같은 인스턴스!)
console.log(config1 === config2); // true
장점:
settings에 직접 접근 불가 (클로저로 보호)getInstance()로만 접근 가능new Config()처럼 실수로 생성 불가
실전 예제: Logger (로깅 시스템)
// ✅ 실무에서 자주 쓰는 Singleton
const Logger = (function() {
let instance = null;
function createLogger() {
const logs = []; // 로그 저장소
return {
log(message, level = 'info') {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level,
message
};
logs.push(logEntry);
console.log(`[${level.toUpperCase()}] ${timestamp}: ${message}`);
},
error(message) {
this.log(message, 'error');
},
warn(message) {
this.log(message, 'warn');
},
getLogs() {
return [...logs]; // 복사본 반환
},
clearLogs() {
logs.length = 0;
console.log('로그가 삭제되었습니다.');
}
};
}
return {
getInstance() {
if (!instance) {
instance = createLogger();
console.log('Logger 인스턴스가 생성되었습니다.');
}
return instance;
}
};
})();
// 여러 파일에서 사용
// utils.js
const logger1 = Logger.getInstance();
logger1.log('유틸리티 초기화');
// api.js
const logger2 = Logger.getInstance();
logger2.error('API 호출 실패');
// ui.js
const logger3 = Logger.getInstance();
logger3.warn('렌더링 지연');
// 모든 로그 확인
console.log(logger1.getLogs());
// [
// { timestamp: '...', level: 'info', message: '유틸리티 초기화' },
// { timestamp: '...', level: 'error', message: 'API 호출 실패' },
// { timestamp: '...', level: 'warn', message: '렌더링 지연' }
// ]
console.log(logger1 === logger2 && logger2 === logger3); // true
왜 Singleton이 필요한가?
- 모든 파일이 같은 로그 저장소 공유
- 로그가 한 곳에 모임
- 메모리 효율적 (인스턴스 하나만)
두 패턴의 비교
언제 어떤 패턴을?
| 상황 | Factory | Singleton |
|---|---|---|
| 목적 | 객체 생성 방법 캡슐화 | 인스턴스 하나만 보장 |
| 인스턴스 수 | 여러 개 생성 | 단 하나만 |
| 사용 예 | 캐릭터, 버튼, 위젯 | 설정, Logger, DB 연결 |
시각적 비교
Factory Pattern:
─────────────────────────────────────
개발자: create('warrior')
↓
Factory
↓
[생성 로직]
↓
새 Warrior 객체 ①
개발자: create('warrior')
↓
Factory
↓
[생성 로직]
↓
새 Warrior 객체 ② (다른 객체!)
Singleton Pattern:
─────────────────────────────────────
파일A: getInstance()
↓
Singleton
↓
Config 객체 ①
파일B: getInstance()
↓
Singleton
↓
Config 객체 ① (같은 객체!)
실전 조합: Factory + Singleton
// ✅ 두 패턴을 함께 사용
const CharacterFactory = (function() {
let instance = null; // Singleton
function createFactory() {
const creators = {
warrior: (name, level) => new Warrior(name, level),
mage: (name, level) => new Mage(name, level)
};
return {
// Factory 메서드
create(type, name, level) {
const creator = creators[type];
if (!creator) {
throw new Error(`Unknown type: ${type}`);
}
return creator(name, level);
},
register(type, creator) {
creators[type] = creator;
}
};
}
return {
getInstance() {
if (!instance) {
instance = createFactory();
}
return instance;
}
};
})();
// 사용
const factory1 = CharacterFactory.getInstance();
const warrior = factory1.create('warrior', '아서', 1);
const factory2 = CharacterFactory.getInstance();
const mage = factory2.create('mage', '멀린', 1);
console.log(factory1 === factory2); // true (Singleton!)
console.log(warrior === mage); // false (Factory로 다른 객체 생성!)
왜 이렇게 하나요?
- Factory는 Singleton으로 (팩토리는 하나면 충분)
- 생성되는 객체는 여러 개
함정과 주의사항
함정 1: Singleton의 테스트 어려움
// ❌ 문제: 테스트 간 상태 공유
const config = Config.getInstance();
config.setTheme('dark');
// Test 1
test('should use dark theme', () => {
expect(config.getTheme()).toBe('dark'); // ✓
});
// Test 2
test('should start with light theme', () => {
// 어? config는 아직 'dark' 상태...
expect(config.getTheme()).toBe('light'); // ✗ 실패!
});
해결책: Reset 메서드 추가
// ✅ 해결
const Config = (function() {
let instance = null;
function createInstance() {
let settings = { theme: 'light' };
return {
setTheme(theme) { settings.theme = theme; },
getTheme() { return settings.theme; },
// 테스트용 reset
reset() {
settings = { theme: 'light' };
}
};
}
return {
getInstance() {
if (!instance) instance = createInstance();
return instance;
},
// 테스트용: 인스턴스 제거
resetInstance() {
instance = null;
}
};
})();
// 테스트에서
afterEach(() => {
Config.getInstance().reset(); // 상태 초기화
});
함정 2: Factory의 타입 안전성
// ❌ 문제: 오타에 취약
const character = CharacterFactory.create('warroir', 'Bob', 1);
// TypeError: Unknown character type: warroir
해결책: 상수 사용
// ✅ 해결
const CHARACTER_TYPES = {
WARRIOR: 'warrior',
MAGE: 'mage',
ARCHER: 'archer'
};
// 사용
const character = CharacterFactory.create(
CHARACTER_TYPES.WARRIOR, // 자동완성!
'Bob',
1
);
// 잘못된 타입 방지
const invalid = CharacterFactory.create(
CHARACTER_TYPES.WARROIR, // ✗ 컴파일 에러!
'Bob',
1
);
함정 3: Singleton의 글로벌 상태
// ❌ 문제: 모듈 간 의존성
// moduleA.js
const config = Config.getInstance();
config.setTheme('dark');
// moduleB.js - 모르는 사이에 영향받음!
const config = Config.getInstance();
console.log(config.getTheme()); // 'dark' (예상치 못한 값!)
해결책: Event Bus와 함께 사용
// ✅ 해결
const Config = (function() {
let instance = null;
function createInstance() {
let settings = { theme: 'light' };
let listeners = [];
return {
setTheme(theme) {
const oldTheme = settings.theme;
settings.theme = theme;
// 변경 알림!
listeners.forEach(listener => {
listener({ oldTheme, newTheme: theme });
});
},
getTheme() {
return settings.theme;
},
// 구독
onChange(callback) {
listeners.push(callback);
// 구독 해제 함수 반환
return () => {
listeners = listeners.filter(l => l !== callback);
};
}
};
}
return { getInstance() { /* ... */ } };
})();
// moduleB.js - 변경 감지 가능!
const config = Config.getInstance();
config.onChange(({ oldTheme, newTheme }) => {
console.log(`테마 변경: ${oldTheme} → ${newTheme}`);
});
실전 체크리스트
Factory Pattern 사용 전
- 객체 생성 로직이 복잡한가?
- 여러 타입의 객체를 생성하는가?
- 생성 로직이 여러 곳에 흩어져 있는가?
- 새 타입 추가가 자주 일어나는가?
하나라도 Yes면 Factory 고려!
Singleton Pattern 사용 전
- 인스턴스가 진짜 하나만 필요한가?
- 전역 상태를 관리하는가?
- 여러 모듈이 같은 인스턴스를 공유해야 하는가?
- 리소스가 제한적인가? (DB 연결, Logger 등)
하나라도 Yes면 Singleton 고려!
주의사항
- Singleton은 꼭 필요할 때만! (테스트 어려움)
- Factory는 너무 복잡하게 만들지 않기
- TypeScript 사용 시 타입 명시
- 테스트 코드 작성
실전 예제 모음
예제 1: Modal Factory
// ✅ 실무: 모달 생성 팩토리
class ModalFactory {
static create(type, options = {}) {
const modal = document.createElement('div');
modal.className = 'modal';
switch (type) {
case 'alert':
modal.innerHTML = `
<div class="modal-content">
<h2>${options.title || '알림'}</h2>
<p>${options.message}</p>
<button onclick="this.closest('.modal').remove()">확인</button>
</div>
`;
break;
case 'confirm':
modal.innerHTML = `
<div class="modal-content">
<h2>${options.title || '확인'}</h2>
<p>${options.message}</p>
<button class="confirm-btn">확인</button>
<button class="cancel-btn">취소</button>
</div>
`;
// 이벤트 연결
modal.querySelector('.confirm-btn').onclick = () => {
options.onConfirm?.();
modal.remove();
};
modal.querySelector('.cancel-btn').onclick = () => {
options.onCancel?.();
modal.remove();
};
break;
case 'prompt':
modal.innerHTML = `
<div class="modal-content">
<h2>${options.title || '입력'}</h2>
<input type="text" placeholder="${options.placeholder || ''}" />
<button class="submit-btn">확인</button>
<button class="cancel-btn">취소</button>
</div>
`;
const input = modal.querySelector('input');
modal.querySelector('.submit-btn').onclick = () => {
options.onSubmit?.(input.value);
modal.remove();
};
modal.querySelector('.cancel-btn').onclick = () => {
modal.remove();
};
break;
}
return modal;
}
}
// 사용
const alertModal = ModalFactory.create('alert', {
title: '저장 완료',
message: '파일이 저장되었습니다.'
});
document.body.appendChild(alertModal);
const confirmModal = ModalFactory.create('confirm', {
title: '삭제 확인',
message: '정말 삭제하시겠습니까?',
onConfirm: () => console.log('삭제!'),
onCancel: () => console.log('취소!')
});
document.body.appendChild(confirmModal);
예제 2: Database Connection Singleton
// ✅ 실무: DB 연결 Singleton
const Database = (function() {
let instance = null;
function createConnection() {
let isConnected = false;
let connection = null;
return {
async connect() {
if (isConnected) {
console.log('이미 연결되어 있습니다.');
return connection;
}
// 실제로는 DB 연결 로직
console.log('데이터베이스 연결 중...');
await new Promise(resolve => setTimeout(resolve, 1000));
connection = {
query: async (sql) => {
console.log(`쿼리 실행: ${sql}`);
// 실제 쿼리 실행...
return { rows: [] };
}
};
isConnected = true;
console.log('연결 완료!');
return connection;
},
async disconnect() {
if (!isConnected) return;
console.log('연결 종료 중...');
await new Promise(resolve => setTimeout(resolve, 500));
connection = null;
isConnected = false;
console.log('연결 종료!');
},
isConnected() {
return isConnected;
}
};
}
return {
getInstance() {
if (!instance) {
instance = createConnection();
}
return instance;
}
};
})();
// 여러 모듈에서 사용
// userService.js
const db1 = Database.getInstance();
await db1.connect();
await db1.query('SELECT * FROM users');
// productService.js
const db2 = Database.getInstance();
await db2.connect(); // 이미 연결됨! 재사용!
await db2.query('SELECT * FROM products');
console.log(db1 === db2); // true
마무리
핵심 요약
Factory Pattern: “객체 생성의 공장”
- 여러 타입의 객체를 생성할 때
- 생성 로직을 한 곳에 모을 때
- 확장 가능한 구조가 필요할 때
Singleton Pattern: “하나뿐인 존재”
- 인스턴스가 하나만 필요할 때
- 전역 상태를 관리할 때
- 리소스를 공유해야 할 때
실제로 느낀 변화
Before (패턴 없이):
- 객체 생성 코드가 여기저기 흩어짐
- if-else 지옥
- 설정 동기화 불가능
- 테스트 어려움
After (패턴 적용):
- 생성 로직이 한 곳에 모임
- 확장 가능한 구조
- 일관된 상태 관리
- 코드가 훨씬 깔끔!
다음 단계
두 패턴을 마스터했다면:
- Observer Pattern: 이벤트 기반 통신
- Strategy Pattern: 알고리즘 교체
- Decorator Pattern: 기능 확장
디자인 패턴은 문제 해결의 도구입니다. 패턴을 위한 패턴이 아니라, 실제 문제를 해결하기 위해 사용하세요!
Happy Coding! 🏭✨
참고 자료
관련 문서
외부 자료
작성일: 2025-10-20 주제: Design Patterns (Factory & Singleton) 난이도: Beginner to Intermediate
댓글