
디자인 패턴: 바퀴를 다시 발명하지 마라
개발자들이 맨날 겪는 문제에 대해 선배들이 만들어둔 족보(Cheat Sheet). 싱글톤, 팩토리, 옵저버 패턴의 핵심.

개발자들이 맨날 겪는 문제에 대해 선배들이 만들어둔 족보(Cheat Sheet). 싱글톤, 팩토리, 옵저버 패턴의 핵심.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

3년 차 때 새 프로젝트에 투입됐다. 코드를 열어보는데... 묘한 기시감이 들었다.
"어? 이 구조... 저번 프로젝트에서 본 것 같은데?"Database 연결 관리하는 방식, API 객체 생성하는 패턴, 이벤트 리스너 구조... 전혀 다른 회사, 다른 팀, 다른 프로젝트인데 코드 패턴이 이상하리만치 비슷했다.
더 신기했던 건, 예전에 공부할 때 봤던 C# 코드와 지금 보는 JavaScript 코드가 언어는 다른데 구조는 똑같다는 거였다.
시니어에게 물었습니다:
"이 코드 구조, 의도적으로 이렇게 짠 거예요?"
시니어: "당연하지. 싱글톤이라고. 디자인 패턴이야."
저: "디자인 패턴이요?"
시니어: "족보야, 족보. 선배들이 30년 동안 같은 문제 풀면서 만든 모범 답안. 언어는 달라도 문제는 똑같으니까. GoF 책 한 번 읽어봐."
그날 저녁, GoF(Gang of Four)의 "Design Patterns" 책을 주문했다. 그리고 책이 도착하자마자... 700페이지를 보고 후회했다.
처음엔 왜 필요한지 전혀 몰랐다.
"패턴? 그냥 내 마음대로 짜면 안 돼? 내가 만든 코드가 제일 직관적이잖아."
하지만 오픈소스 프로젝트에서 코드 리뷰를 받으면서 현실을 깨달았다.
첫 번째 리뷰:// 제가 짠 코드
let dbConnection1 = createDatabaseConnection();
let dbConnection2 = createDatabaseConnection();
let dbConnection3 = createDatabaseConnection();
리뷰어: "DB 연결은 하나만 있어야 해요. 싱글톤 패턴 쓰세요."
저: "싱글톤이요?" (구글링 시작)
리뷰어: "getInstance() 메서드로 인스턴스 하나만 반환하도록..."
→ 30초 설명 듣고 겨우 이해
두 번째 리뷰 (한 달 후):리뷰어: "여기도 싱글톤 쓰시죠."
저: "아, 넵!" (1초 만에 이해)
이 순간 깨달았다.디자인 패턴은 "코드 짜는 방법"이 아니라 "개발자들의 공용 단어장"이었다.
같은 문제를 "싱글톤"이라는 한 단어로 압축해서 전달할 수 있는 거였다. 30초짜리 설명이 1초로 줄어드는 마법.
GoF 책을 펼쳤을 때 충격받았던 것들:
패턴이 23개나 된다고? "이거 다 외워야 해? 나 국비 과정도 아닌데..."
언제 어떤 패턴을 써야 하지? "Singleton이랑 Factory랑... 뭐가 달라?"
그냥 직관적으로 짜면 안 돼? "패턴 쓰니까 오히려 코드가 복잡해 보이는데?"
예제 코드가 너무 어렵다 "Shape, Circle, Rectangle... 현실에서 도형 그릴 일이 어디 있어?"
무엇보다 이해 안 갔던 건, "이게 진짜 실제로 쓰이긴 하나?"였다.
책 예제는 도형, 동물, 자동차... 너무 학문적이었다.
한 달 동안 책만 보다가 실제 코드를 다시 봤다. 그리고 충격을 받았다.
"어? 이게 다 패턴이었네?"new Promise() → Factory 패턴Array.map() → Strategy 패턴내가 이미 쓰고 있던 코드들이 전부 디자인 패턴이었다. 다만 이름을 몰랐을 뿐.
시니어의 비유가 그때 와닿았습니다:
"디자인 패턴은 요리 레시피야.
파스타를 처음 만들 때:
- 소금 얼마? 면 삶는 시간은?
- 물 양은? 오일은 언제 넣지?
레시피를 보면: '물 1L당 소금 10g, 끓으면 면 8분'
디자인 패턴도 마찬가지. '이 문제는 싱글톤 레시피. 저 문제는 팩토리 레시피.'
매번 새로 고민하지 말고, 검증된 레시피 따라 하면 실패가 적어."
결국 이거였다: 디자인 패턴은 "새로운 기술"이 아니라 "이미 쓰던 코드에 이름 붙이기"였다.
이름을 알면 의사소통이 빨라지는 거였다.
내가 5년 동안 실제로 가장 많이 쓴 3가지를 정리해본다.
제가 처음 만든 채팅 앱에서 이런 버그가 있었습니다:
// 메시지 보낼 때마다 DB 연결 생성
function sendMessage(text) {
const db = new Database(); // 새 연결 생성
db.insert({ message: text });
}
// 사용자가 메시지 100개 보내면...
// DB 연결이 100개 생성됨 💀
// 메모리 폭탄 + 서버 다운
테스트할 땐 괜찮았는데, 실제 사용자 100명이 동시에 쓰니까 서버가 다운됐습니다.
왜? DB 연결 객체를 매번 새로 만들어서, 메모리에 쌓이고 쌓여서 터진 겁니다.
class Database {
static instance = null;
constructor() {
if (Database.instance) {
return Database.instance; // 이미 있으면 기존 거 반환
}
Database.instance = this;
this.connection = this.createConnection();
console.log("Database 연결 생성됨");
}
static getInstance() {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
createConnection() {
// 실제 DB 연결 로직
return { connected: true };
}
}
// 사용
const db1 = Database.getInstance();
const db2 = Database.getInstance();
const db3 = Database.getInstance();
console.log(db1 === db2); // true
console.log(db2 === db3); // true
// 아무리 많이 호출해도 인스턴스는 1개
// 로그: "Database 연결 생성됨" (한 번만 출력)
class Config {
static instance = null;
constructor() {
if (Config.instance) return Config.instance;
this.settings = {
apiUrl: process.env.API_URL,
apiKey: process.env.API_KEY,
timeout: 5000
};
Config.instance = this;
}
static getInstance() {
if (!Config.instance) {
Config.instance = new Config();
}
return Config.instance;
}
get(key) {
return this.settings[key];
}
}
// 어디서든 같은 설정 객체 사용
const config1 = Config.getInstance();
const config2 = Config.getInstance();
console.log(config1.get("apiUrl")); // 같은 설정
console.log(config1 === config2); // true
2. Logger (로그 파일 중복 방지)
class Logger {
static instance = null;
constructor() {
if (Logger.instance) return Logger.instance;
this.logs = [];
Logger.instance = this;
}
log(message) {
const timestamp = new Date().toISOString();
this.logs.push(`[${timestamp}] ${message}`);
console.log(`[${timestamp}] ${message}`);
}
getLogs() {
return this.logs;
}
}
// 여러 모듈에서 같은 Logger 사용
const logger1 = new Logger();
logger1.log("User logged in");
const logger2 = new Logger();
logger2.log("Data fetched");
console.log(logger1.getLogs()); // 두 로그 모두 포함
console.log(logger1 === logger2); // true
싱글톤의 단점은 테스트가 까다롭다는 거다.
// ❌ 테스트 간 상태 공유 문제
test("첫 번째 테스트", () => {
const db = Database.getInstance();
db.insert({ id: 1 });
expect(db.count()).toBe(1);
});
test("두 번째 테스트", () => {
const db = Database.getInstance(); // 같은 인스턴스!
expect(db.count()).toBe(0); // ❌ 실패 (이전 테스트 데이터 남아있음)
});
해결책: reset() 메서드 추가
class Database {
// ...
static reset() {
Database.instance = null;
}
}
// 각 테스트 후 리셋
afterEach(() => {
Database.reset();
});
제가 크로스 플랫폼 앱을 만들 때 겪은 일입니다:
// 플랫폼마다 다른 버튼 생성
function createButton(platform, text) {
let button;
if (platform === "ios") {
button = new IOSButton(text);
button.setStyle({ borderRadius: 10, shadow: true });
} else if (platform === "android") {
button = new AndroidButton(text);
button.setStyle({ elevation: 5, ripple: true });
} else if (platform === "web") {
button = new WebButton(text);
button.setStyle({ hover: true, transition: "0.3s" });
} else if (platform === "windows") {
button = new WindowsButton(text);
button.setStyle({ flat: true });
}
return button;
}
// 문제:
// 1. 새 플랫폼 추가 시 이 함수를 찾아서 수정해야 함
// 2. 버튼 생성하는 곳이 100곳이면? 100곳 다 수정
// 3. iOS 버튼 스타일 바뀌면? 모든 곳을 찾아다니며 수정
실제로 macOS 지원 추가할 때 200개 파일을 수정했습니다. 2주 걸렸습니다.
// 1단계: 버튼 인터페이스 통일
class Button {
constructor(text) {
this.text = text;
}
render() {
throw new Error("Must implement render()");
}
}
class IOSButton extends Button {
render() {
return `<button class="ios">${this.text}</button>`;
}
}
class AndroidButton extends Button {
render() {
return `<button class="android">${this.text}</button>`;
}
}
class WebButton extends Button {
render() {
return `<button class="web">${this.text}</button>`;
}
}
// 2단계: Factory 만들기
class ButtonFactory {
static createButton(platform, text) {
switch(platform) {
case "ios":
return new IOSButton(text);
case "android":
return new AndroidButton(text);
case "web":
return new WebButton(text);
default:
throw new Error(`Unknown platform: ${platform}`);
}
}
}
// 사용
const platform = detectPlatform(); // "ios", "android", "web"
const button = ButtonFactory.createButton(platform, "Submit");
button.render();
// 새 플랫폼 추가 시:
// 1. MacOSButton 클래스 만들기
// 2. ButtonFactory에 case 하나만 추가
// 3. 끝! (기존 코드 수정 없음)
제가 이커머스 앱을 만들 때 결제 시스템을 Factory로 구현했습니다:
class PaymentFactory {
static createPayment(method) {
switch(method) {
case "creditCard":
return new CreditCardPayment();
case "paypal":
return new PayPalPayment();
case "kakao":
return new KakaoPayPayment();
case "toss":
return new TossPayment();
default:
throw new Error(`Unknown payment method: ${method}`);
}
}
}
// 사용
function checkout(amount, paymentMethod) {
const payment = PaymentFactory.createPayment(paymentMethod);
try {
const result = payment.process(amount);
if (result.success) {
console.log("결제 성공!");
}
} catch (error) {
console.error("결제 실패:", error);
}
}
checkout(10000, "kakao"); // 카카오페이로 결제
checkout(20000, "toss"); // 토스로 결제
장점:
나중에 네이버페이가 추가됐을 때, Factory만 수정하면 됐습니다:
class PaymentFactory {
static createPayment(method) {
switch(method) {
// ... 기존 코드
case "naver": // 이 한 줄만 추가
return new NaverPayment();
default:
throw new Error(`Unknown payment method: ${method}`);
}
}
}
기존 200개 파일을 수정하던 게 → 1개 파일만 수정으로 바뀌었습니다.
사용자 프로필을 만들 때 이런 코드를 짰습니다:
// 사용자 이름 변경
function updateUserName(newName) {
user.name = newName;
// 모든 UI를 수동으로 업데이트해야 함
updateHeaderUI(newName); // 헤더의 이름
updateProfileUI(newName); // 프로필 페이지
updateSidebarUI(newName); // 사이드바
updateChatUI(newName); // 채팅창
updateNotificationUI(newName); // 알림
sendAnalyticsEvent(newName); // 분석 이벤트
}
// 문제:
// 1. 새 UI 추가 시 여기도 수정해야 함
// 2. 하나라도 빠뜨리면 버그
// 3. 이름 외에 이메일, 프로필 사진도? 함수가 3배로 늘어남
실제로 겪은 버그: 채팅창 업데이트를 빠뜨려서, 채팅창에만 이전 이름이 표시되는 버그가 3주 동안 배포됐습니다.
// Subject: 관찰 대상
class Subject {
constructor() {
this.observers = []; // 구독자 목록
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(o => o !== observer);
}
notify(data) {
this.observers.forEach(observer => {
observer.update(data);
});
}
}
// 사용자 클래스
class User extends Subject {
constructor(name) {
super();
this._name = name;
}
setName(name) {
this._name = name;
this.notify({ name }); // 자동으로 모든 구독자에게 알림
}
getName() {
return this._name;
}
}
// 구독자들 (Observer)
const headerUI = {
update: (data) => {
console.log(`Header updated: ${data.name}`);
document.querySelector("#header-name").textContent = data.name;
}
};
const profileUI = {
update: (data) => {
console.log(`Profile updated: ${data.name}`);
document.querySelector("#profile-name").textContent = data.name;
}
};
const chatUI = {
update: (data) => {
console.log(`Chat updated: ${data.name}`);
document.querySelector("#chat-name").textContent = data.name;
}
};
// 구독 설정
const user = new User("Alice");
user.subscribe(headerUI);
user.subscribe(profileUI);
user.subscribe(chatUI);
// 이름 변경
user.setName("Bob");
// 출력:
// Header updated: Bob
// Profile updated: Bob
// Chat updated: Bob
// 새 UI 추가? subscribe만 하면 끝
const sidebarUI = {
update: (data) => console.log(`Sidebar: ${data.name}`)
};
user.subscribe(sidebarUI);
user.setName("Charlie");
// 자동으로 sidebar도 업데이트됨
제가 주식 시세 앱을 만들 때 옵저버 패턴을 썼습니다:
class StockMarket extends Subject {
constructor() {
super();
this.stocks = {};
}
updatePrice(symbol, price) {
this.stocks[symbol] = price;
this.notify({ symbol, price });
}
}
// 구독자: 가격 차트
const priceChart = {
update: (data) => {
console.log(`Chart: ${data.symbol} = ${data.price}`);
// 차트 그리기
}
};
// 구독자: 가격 알림
const priceAlert = {
update: (data) => {
if (data.price > 100) {
console.log(`Alert: ${data.symbol} exceeded $100!`);
}
}
};
// 구독자: 거래 자동화
const autoTrader = {
update: (data) => {
if (data.price < 50) {
console.log(`Auto-buy ${data.symbol} at ${data.price}`);
}
}
};
const market = new StockMarket();
market.subscribe(priceChart);
market.subscribe(priceAlert);
market.subscribe(autoTrader);
// 가격 변경
market.updatePrice("AAPL", 150);
// 출력:
// Chart: AAPL = $150
// Alert: AAPL exceeded $100!
market.updatePrice("TSLA", 45);
// 출력:
// Chart: TSLA = $45
// Auto-buy TSLA at $45
사실 이미 옵저버 패턴을 쓰고 있었다:
1. JavaScript의 이벤트 리스너// addEventListener = subscribe
// dispatchEvent = notify
const button = document.querySelector("#btn");
button.addEventListener("click", () => {
console.log("Clicked!");
}); // 구독
button.click(); // 알림
2. React의 useState
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`Count changed: ${count}`);
}, [count]); // count를 "구독"
setCount(1); // "알림" → useEffect 실행
3. Redux Store
store.subscribe(() => {
console.log("State changed:", store.getState());
}); // 구독
store.dispatch({ type: "INCREMENT" }); // 알림
이미 쓰던 패턴이었다. 이름만 몰랐을 뿐.
내가 실제로 가끔 쓰는 패턴들을 정리해본다.
// ❌ 매개변수가 너무 많음
class User {
constructor(name, email, age, phone, address, city, country, postalCode, avatar) {
this.name = name;
this.email = email;
this.age = age;
this.phone = phone;
this.address = address;
this.city = city;
this.country = country;
this.postalCode = postalCode;
this.avatar = avatar;
}
}
// 사용할 때 헷갈림
const user = new User(
"Alice",
"alice@example.com",
25,
"010-1234-5678",
"123 Main St",
"Seoul",
"Korea",
"12345",
"avatar.png"
);
// 순서 기억 못함 💀
class UserBuilder {
constructor() {
this.user = {};
}
setName(name) {
this.user.name = name;
return this; // 체이닝을 위해 this 반환
}
setEmail(email) {
this.user.email = email;
return this;
}
setAge(age) {
this.user.age = age;
return this;
}
setPhone(phone) {
this.user.phone = phone;
return this;
}
setAddress(address, city, country, postalCode) {
this.user.address = { address, city, country, postalCode };
return this;
}
setAvatar(avatar) {
this.user.avatar = avatar;
return this;
}
build() {
return this.user;
}
}
// 사용 (직관적!)
const user = new UserBuilder()
.setName("Alice")
.setEmail("alice@example.com")
.setAge(25)
.setPhone("010-1234-5678")
.setAddress("123 Main St", "Seoul", "Korea", "12345")
.setAvatar("avatar.png")
.build();
// 선택적 필드도 쉽게 처리
const simpleUser = new UserBuilder()
.setName("Bob")
.setEmail("bob@example.com")
.build(); // 나머지는 생략 가능
class RequestBuilder {
constructor(url) {
this.url = url;
this.method = "GET";
this.headers = {};
this.body = null;
}
setMethod(method) {
this.method = method;
return this;
}
setHeader(key, value) {
this.headers[key] = value;
return this;
}
setBody(body) {
this.body = body;
return this;
}
async send() {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: this.body
});
return response.json();
}
}
// 사용
const data = await new RequestBuilder("https://api.example.com/users")
.setMethod("POST")
.setHeader("Content-Type", "application/json")
.setHeader("Authorization", "Bearer token123")
.setBody(JSON.stringify({ name: "Alice" }))
.send();
이거 어디서 본 것 같지 않나? jQuery, Axios, Fetch API 전부 Builder 패턴이다.
// jQuery (Builder)
$.ajax({
url: "/api/users",
method: "POST",
headers: { "Authorization": "Bearer token" },
data: { name: "Alice" }
});
// Axios (Builder)
axios.post("/api/users", { name: "Alice" })
.then(response => console.log(response.data));
function processPayment(amount, method) {
if (method === "creditCard") {
// 신용카드 결제 로직
console.log("Processing credit card payment...");
// 100줄 코드
} else if (method === "paypal") {
// PayPal 결제 로직
console.log("Processing PayPal payment...");
// 100줄 코드
} else if (method === "kakao") {
// 카카오페이 결제 로직
console.log("Processing Kakao Pay...");
// 100줄 코드
}
// 새 결제 수단 추가 시 이 함수가 500줄 넘어감
}
// 전략 인터페이스
class PaymentStrategy {
pay(amount) {
throw new Error("Must implement pay()");
}
}
// 구체적 전략들
class CreditCardStrategy extends PaymentStrategy {
pay(amount) {
console.log(`Credit card payment: ${amount}`);
// 신용카드 결제 로직
}
}
class PayPalStrategy extends PaymentStrategy {
pay(amount) {
console.log(`PayPal payment: ${amount}`);
// PayPal 결제 로직
}
}
class KakaoPayStrategy extends PaymentStrategy {
pay(amount) {
console.log(`Kakao Pay: ${amount}`);
// 카카오페이 결제 로직
}
}
// Context: 전략을 사용하는 곳
class PaymentContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
processPayment(amount) {
this.strategy.pay(amount);
}
}
// 사용
const payment = new PaymentContext(new CreditCardStrategy());
payment.processPayment(100); // Credit card payment: $100
// 나중에 전략 교체
payment.setStrategy(new PayPalStrategy());
payment.processPayment(200); // PayPal payment: $200
// 또 교체
payment.setStrategy(new KakaoPayStrategy());
payment.processPayment(300); // Kakao Pay: $300
class SortStrategy {
sort(data) {
throw new Error("Must implement sort()");
}
}
class BubbleSort extends SortStrategy {
sort(data) {
console.log("Using Bubble Sort");
// 버블 정렬 로직
return data.sort((a, b) => a - b);
}
}
class QuickSort extends SortStrategy {
sort(data) {
console.log("Using Quick Sort");
// 퀵 정렬 로직
return data.sort((a, b) => a - b);
}
}
class Sorter {
constructor(strategy) {
this.strategy = strategy;
}
sort(data) {
return this.strategy.sort(data);
}
}
// 사용
const data = [5, 2, 8, 1, 9];
const sorter = new Sorter(new BubbleSort());
console.log(sorter.sort(data)); // Using Bubble Sort
// 데이터가 크면 QuickSort로 변경
const bigData = Array.from({ length: 10000 }, () => Math.random());
sorter.strategy = new QuickSort();
console.log(sorter.sort(bigData)); // Using Quick Sort
실제로 이미 쓰는 곳:
Array.sort() (비교 함수를 주입 = 전략 주입)fetch() (Request 옵션 = 전략)프로젝트에서 결제 API를 바꾸게 됐습니다. 문제는 기존 코드가 100개 파일에 퍼져있다는 거였습니다.
// 구 API
class OldPaymentAPI {
processTransaction(user, amount) {
return {
status: "success",
transactionId: "12345",
user: user,
amount: amount
};
}
}
// 신 API (형식이 완전히 다름)
class NewPaymentAPI {
pay(userId, amountInCents) {
return {
success: true,
id: "67890",
user_id: userId,
amount_cents: amountInCents
};
}
}
// 문제: 기존 코드 100곳에서 이렇게 쓰고 있음
const result = oldAPI.processTransaction("user123", 100);
if (result.status === "success") {
console.log(result.transactionId);
}
// 신 API로 바꾸려면 100곳 다 수정해야 함 💀
// Adapter: 구 API 형식을 신 API로 변환
class PaymentAdapter {
constructor(newAPI) {
this.newAPI = newAPI;
}
processTransaction(user, amount) {
// 신 API 호출 (cents 단위로 변환)
const result = this.newAPI.pay(user, amount * 100);
// 구 API 형식으로 변환해서 반환
return {
status: result.success ? "success" : "failed",
transactionId: result.id,
user: result.user_id,
amount: result.amount_cents / 100
};
}
}
// 사용
const newAPI = new NewPaymentAPI();
const adapter = new PaymentAdapter(newAPI);
// 기존 코드 그대로 사용 가능!
const result = adapter.processTransaction("user123", 100);
if (result.status === "success") {
console.log(result.transactionId); // 작동함!
}
// 100개 파일 수정 안 해도 됨!
jQuery에서 React로 마이그레이션할 때 썼던 방법입니다:
// jQuery 코드를 React로 감싸기
class JQueryAdapter {
constructor(selector) {
this.element = document.querySelector(selector);
}
// jQuery 스타일 메서드
text(value) {
if (value === undefined) {
return this.element.textContent;
}
this.element.textContent = value;
return this;
}
addClass(className) {
this.element.classList.add(className);
return this;
}
removeClass(className) {
this.element.classList.remove(className);
return this;
}
on(event, handler) {
this.element.addEventListener(event, handler);
return this;
}
}
// 기존 jQuery 코드
// $("#header").text("Hello").addClass("active");
// Adapter로 점진적 마이그레이션
const $ = (selector) => new JQueryAdapter(selector);
$("#header").text("Hello").addClass("active");
// jQuery 없이도 작동!
실제 꿀팁: 라이브러리 변경할 때 Adapter 쓰면 점진적으로 마이그레이션 가능하다. 한 번에 다 바꾸려다가 망하는 것보다 훨씬 안전하다.
"망치를 가진 사람에겐 모든 게 못으로 보인다."
내가 디자인 패턴을 처음 배웠을 때 한 실수를 공개한다.
Before (패턴 배우기 전):// 간단한 계산기
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
console.log(add(5, 3)); // 8
After (패턴 배운 직후):
// ❌ 과도한 패턴 남용
class CalculationStrategy {
calculate(a, b) {
throw new Error("Must implement");
}
}
class AdditionStrategy extends CalculationStrategy {
calculate(a, b) {
return a + b;
}
}
class SubtractionStrategy extends CalculationStrategy {
calculate(a, b) {
return a - b;
}
}
class CalculatorFactory {
static createCalculator(type) {
switch(type) {
case "add":
return new AdditionStrategy();
case "subtract":
return new SubtractionStrategy();
}
}
}
class CalculatorContext {
constructor(strategy) {
this.strategy = strategy;
}
execute(a, b) {
return this.strategy.calculate(a, b);
}
}
// 사용 (복잡해짐)
const factory = CalculatorFactory;
const addStrategy = factory.createCalculator("add");
const calculator = new CalculatorContext(addStrategy);
console.log(calculator.execute(5, 3)); // 8
// 간단한 덧셈에 5개 클래스 + 50줄 코드 💀
시니어의 코드 리뷰:
"이게 뭐야? 간단한 덧셈에 왜 이렇게 복잡하게 만들었어?"
이때 받아들였다. 패턴은 도구일 뿐이다. 과하면 독이 된다.
내가 정리해본 기준이다:
// ❌ 미래를 대비한 과도한 설계
class UserFactory {
static createUser(name, type = "normal") {
switch(type) {
case "normal":
return new NormalUser(name);
case "admin": // 지금은 안 쓰는데 "나중에 필요할 것 같아서" 추가
return new AdminUser(name);
case "guest": // 이것도
return new GuestUser(name);
case "premium": // 이것도
return new PremiumUser(name);
}
}
}
// ✅ 지금 당장 필요한 것만
class User {
constructor(name) {
this.name = name;
}
}
const user = new User("Alice");
교훈: "나중에 필요할 것 같아서"는 대부분 필요 없다. 필요해지면 그때 추가하면 된다.
// ❌ 간단한 로거를 복잡하게
class LoggerSingleton {
static instance = null;
constructor() {
if (LoggerSingleton.instance) {
return LoggerSingleton.instance;
}
this.logs = [];
LoggerSingleton.instance = this;
}
log(message) {
this.logs.push(message);
}
}
// ✅ 그냥 console.log 쓰세요
console.log("Hello");
// 정말 로그 저장이 필요할 때만 싱글톤 쓰기
// ❌ 게임 루프에서 패턴 남용
class GameLoop {
update() {
// 60fps로 돌아가는 곳에서
const renderer = RendererFactory.createRenderer("webgl"); // 매 프레임마다 생성 💀
const physics = PhysicsFactory.createEngine("box2d"); // 느려짐
renderer.render();
physics.update();
}
}
// ✅ 한 번만 생성
class GameLoop {
constructor() {
this.renderer = new WebGLRenderer(); // 초기화 시 한 번
this.physics = new Box2DEngine();
}
update() {
this.renderer.render(); // 재사용
this.physics.update();
}
}
제가 본 최악의 경우:
// 실제로 본 코드 (회사 이름은 비공개)
class SingletonFactoryObserverStrategyAdapterProxy {
// ...800줄
}
이 클래스는:
6개 패턴을 한 클래스에 다 넣었습니다.
결과:교훈: 패턴 1개 = 좋음 패턴 2개 = 괜찮음 패턴 3개 이상 = 의심 패턴 6개 = 재설계 필요
내가 5년 동안 정리한 가이드다:
| 문제 | 패턴 | 실제 예시 | 언제 쓰지 말까? |
|---|---|---|---|
| 객체가 프로그램에 1개만 필요 | Singleton | DB 연결, Config, Logger | 테스트가 많은 곳 (Mock 어려움) |
| 플랫폼/타입별 객체 생성 | Factory | 플랫폼별 UI, 결제 수단 | 타입이 2개 이하일 때 |
| 복잡한 객체 단계적 생성 | Builder | HTTP Request, Query Builder | 매개변수가 3개 이하일 때 |
| 데이터 변경 시 여러 곳 업데이트 | Observer | 이벤트, State 관리 | 구독자가 1~2개일 때 |
| 알고리즘을 바꿔 끼우고 싶음 | Strategy | 결제 수단, 정렬 알고리즘 | 알고리즘이 1개뿐일 때 |
| 인터페이스 불일치 해결 | Adapter | API 마이그레이션, 라이브러리 래핑 | 직접 수정 가능한 코드일 때 |
디자인 패턴을 공부하면서 가장 크게 깨달은 건, 패턴의 진짜 가치는 코드가 아니라 의사소통이라는 거였다.
Before (패턴 모를 때):나: "이 클래스요, 프로그램 전체에서 하나만 존재해야 해서요,
생성자를 private으로 만들고,
static 변수에 인스턴스 저장하고,
getInstance() 메서드로 반환하게..."
시니어: "아, 그래서?"
나: "그러니까... 전역에서 접근 가능하면서도
인스턴스는 하나만 있게..."
시니어: "음..." (3분 경과)
After (패턴 알고 난 후):
나: "싱글톤으로 할게요."
시니어: "오케이." (1초)
이렇게 이해했다: 1시간짜리 설명이 1초로 줄어드는 마법.
5년 차 때 신규 기능 회의에서:
PM: "사용자 알림 기능 어떻게 구현할까요?"
Before (패턴 모를 때):
나: "음... 사용자 데이터가 변경되면,
헤더, 프로필, 사이드바를 각각 업데이트하고..."
→ 30분 회의
After (패턴 알고 난 후):
나: "옵저버 패턴 쓰면 되겠네요.
User 객체를 Subject로,
각 UI를 Observer로 등록."
시니어: "그래, 그렇게 해."
→ 5분 회의
정리해본다: 디자인 패턴은 개발자들의 공용 단어장이다. 같은 언어를 쓰면 의사소통이 빨라진다.
처음으로 코드 리뷰에서 "여기 팩토리 패턴 쓰면 어떨까요?"라고 제안했을 때, 시니어가 웃으며 말했다:
"이제 개발자 언어를 하는구나. 환영해."
그 순간, 결국 이거였다고 깨달았다.
디자인 패턴은 "코드를 잘 짜는 방법"이 아니라 "개발자로 인정받는 통과의례"였다.
"싱글톤", "팩토리", "옵저버" 같은 단어를 자연스럽게 쓰는 순간, 누군가 이렇게 말할 것이다:
"이제 진짜 개발자네."