
팩토리 패턴(Factory): 객체를 찍어내는 공장
손님이 주방에 들어가서 직접 피자 도우를 반죽하지 않습니다. '페퍼로니 하나요'라고 주문하면 공장(Factory)이 알아서 만들어줍니다.

손님이 주방에 들어가서 직접 피자 도우를 반죽하지 않습니다. '페퍼로니 하나요'라고 주문하면 공장(Factory)이 알아서 만들어줍니다.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

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

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

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

제가 디자인 패턴을 처음 배울 때, 시니어가 이런 코드 리뷰를 남겼습니다:
// 내 코드
const button = new IOSButton();
리뷰: "팩토리 패턴 쓰세요."
저: "팩토리? 그냥 new로 만들면 안 돼요?"
시니어: "손님이 주방 들어가서 직접 빵 굽나요?"
"????"그때 저는 정말로 이해할 수 없었습니다. 객체 하나 만드는데 왜 이렇게 복잡한 개념을 들이대는 건지, 왜 Factory라는 또 다른 클래스를 만들어야 하는지 전혀 와닿지 않았습니다. 그냥 new 키워드 쓰면 되는 거 아닌가요? 이게 바로 제가 팩토리 패턴을 공부하게 된 첫 계기였습니다.
크로스 플랫폼 앱을 만들고 있었습니다. iOS, Android, Web 모두 지원하는 서비스였죠. 당시 저는 디자인 패턴 같은 건 잘 몰랐고, 그냥 필요할 때마다 객체를 직접 생성했습니다.
제 코드는 이랬습니다:
// ❌ 버튼 생성 (플랫폼마다 다름)
if (platform === "ios") {
button = new IOSButton();
} else if (platform === "android") {
button = new AndroidButton();
} else if (platform === "web") {
button = new WebButton();
}
처음에는 별 문제가 없었습니다. 버튼 하나 만드는 코드가 몇 군데 있으면 그만이니까요. 그런데 앱이 커지면서 이 코드가 100군데 이상에 중복되기 시작했습니다. 버튼뿐만 아니라 Input, Checkbox, Modal, Dropdown... 모든 UI 컴포넌트마다 이런 분기 로직이 필요했거든요.
그러던 어느 날, 기획자가 말했습니다: "Windows 데스크톱 앱도 만들면 좋을 것 같아요."
저는 그 순간 멘붕이 왔습니다. Windows 지원을 추가하려면 100군데를 찾아서 일일이 수정해야 했으니까요. 하나라도 놓치면 버그가 나고, 테스트도 끝도 없이 해야 하고... 생각만 해도 끔찍했습니다.
시니어: "팩토리 패턴 쓰면 1군데만 고치면 돼."
그때야 저는 진지하게 팩토리 패턴을 공부하기 시작했습니다.
팩토리 패턴 관련 글을 읽으면 읽을수록 혼란스러웠습니다.
첫 번째 의문: "그냥 함수로 감싸면 안 돼?"
제 생각엔 이렇게만 해도 될 것 같았거든요:
function createButton(platform) {
if (platform === "ios") return new IOSButton();
if (platform === "android") return new AndroidButton();
return new WebButton();
}
이게 팩토리 아닌가요? 왜 클래스를 만들고 인터페이스를 정의하고 상속을 하고... 그렇게 복잡하게 만들어야 하죠?
두 번째 의문: "Simple Factory, Factory Method, Abstract Factory... 뭐가 다른 거야?"
디자인 패턴 책을 보면 팩토리가 무려 3개나 나옵니다. 심지어 GoF 디자인 패턴에서는 Simple Factory는 정식 패턴도 아니라고 하더군요. 제가 보기엔 다 비슷해 보이는데, 대체 무슨 차이인지 도통 감이 안 왔습니다.
세 번째 의문: "이게 왜 패턴이야? 그냥 함수 아니야?"
무엇보다 이게 제일 이해가 안 갔습니다. 팩토리 패턴은 결국 "객체를 만드는 함수"잖아요? 그럼 그냥 함수인 거지, 왜 이게 특별히 "패턴"이라는 거창한 이름을 가지고 있는 건가요? 저는 혼자서 며칠을 고민했습니다.
시니어와 점심을 먹으면서 제가 물었습니다: "팩토리 패턴이 그냥 함수랑 뭐가 다른 건가요?"
시니어가 스타벅스 컵을 들어 보이며 말했습니다:
"아, 생성 로직을 숨기는 거구나!""스타벅스에서 아메리카노 주문할 때:
손님 (Client): '아메리카노 1잔 주세요.'
바리스타 (Factory): (원두를 갈고, 에스프레소를 추출하고, 물을 섞고...) '여기요!'
손님은 만드는 과정을 몰라도 됩니다. 바리스타가 레시피가 바뀌어도 손님은 똑같이 주문하면 됩니다.
팩토리 패턴 = 바리스타"
그 순간 저는 이해했습니다. 팩토리 패턴의 핵심은 "객체 생성"이 아니라 "생성 로직의 캡슐화"였던 겁니다.
만약 바리스타 없이 손님이 직접 커피를 타야 한다면? 손님은 원두가 어디 있는지, 기계 사용법은 뭔지, 물 온도는 어떻게 맞추는지 다 알아야 합니다. 그리고 레시피가 바뀌면 모든 손님이 새로 배워야 하죠.
팩토리 패턴도 마찬가지입니다. 클라이언트 코드가 new IOSButton(config, theme, language, permissions, ...)처럼 복잡한 생성자를 직접 다루지 않게 해주는 겁니다. 대신 Factory가 "이봐, 그냥 플랫폼 이름만 말해. 나머지는 내가 알아서 할게"라고 해주는 거죠.
이 비유를 듣고 나니, 왜 팩토리가 필요한지, 왜 이게 패턴인지, 무릎을 탁 쳤습니다.
먼저 팩토리 없이 코딩하면 어떤 문제가 생기는지 정리해봤습니다. 제가 실제로 겪은 문제들입니다.
크로스 플랫폼 앱에서 저장소(Storage), 카메라, 푸시 알림을 플랫폼별로 구현해야 했습니다. 제 코드는 이랬죠:
// 100군데에 이런 코드
if (OS === "ios") {
const storage = new IOSStorage(config);
const camera = new IOSCamera(settings);
const push = new IOSPushNotification(apiKey);
} else if (OS === "android") {
const storage = new AndroidStorage(config);
const camera = new AndroidCamera(settings);
const push = new AndroidPushNotification(apiKey);
}
처음에는 괜찮았습니다. 기능이 10개 정도일 때까지는요. 그런데 앱이 커지면서 이 패턴이 100곳 이상에 반복되기 시작했습니다.
첫 번째 문제는 중복 코드였습니다. 같은 if-else 로직이 100군데 반복되니, 버그를 고치거나 로직을 수정하려면 100군데를 다 찾아서 고쳐야 했습니다. 하나라도 놓치면? 버그가 납니다.
두 번째 문제는 결합도였습니다. 클라이언트 코드가 IOSStorage, AndroidStorage 같은 구체적인 클래스 이름에 직접 의존하고 있었습니다. 나중에 클래스 이름을 바꾸거나 생성자 인자를 변경하면? 100군데를 다 수정해야 합니다.
세 번째 문제는 확장의 어려움이었습니다. Windows 지원을 추가하려면 100군데를 찾아서 else if (OS === "windows") 분기를 추가해야 했습니다. 제가 앞서 말했던 그 악몽이죠.
네 번째 문제는 생성 로직의 노출이었습니다. 클라이언트가 config, settings, apiKey 같은 세부 인자를 모두 알아야 했습니다. 새로운 개발자가 코드를 보면 "이 객체 만들려면 config가 뭐고 settings는 뭐고..." 다 파악해야 했죠.
이 경험을 하고 나서야 저는 팩토리 패턴의 필요성을 뼈저리게 느꼈습니다.
제일 먼저 시도한 게 Simple Factory입니다. 가장 직관적이고 이해하기 쉬웠거든요.
class PlatformFactory {
static createButton(platform) {
switch (platform) {
case "ios":
return new IOSButton();
case "android":
return new AndroidButton();
case "web":
return new WebButton();
default:
throw new Error(`Unknown platform: ${platform}`);
}
}
}
// 사용
const button = PlatformFactory.createButton("ios");
button.render();
이 패턴을 적용하자마자 코드가 훨씬 깔끔해졌습니다. 100군데에 흩어져 있던 if-else 로직이 한 곳으로 모였거든요.
첫 번째 장점은 생성 로직의 집중화입니다. 버튼 생성 관련 로직은 PlatformFactory에만 있습니다. 나중에 로직을 수정하거나 버그를 고칠 때 한 곳만 보면 됩니다.
두 번째 장점은 클라이언트 코드의 단순화입니다. 클라이언트는 더 이상 if (platform === "ios") 같은 분기를 신경 쓸 필요가 없습니다. 그냥 PlatformFactory.createButton("ios")만 호출하면 끝이죠.
하지만 며칠 쓰다 보니 문제가 보이기 시작했습니다.
첫 번째 문제: 새 플랫폼 추가 시 코드 수정 필요. Windows를 추가하려면 PlatformFactory 클래스를 열어서 case "windows": 분기를 추가해야 합니다. 이게 왜 문제냐고요?
OCP(Open-Closed Principle)를 위반하기 때문입니다. "확장에는 열려있고 수정에는 닫혀있어야 한다"는 원칙인데, Simple Factory는 확장하려면 기존 코드를 수정해야 합니다.
처음엔 "그래도 1군데만 고치면 되잖아"라고 생각했는데, 팀이 커지고 여러 명이 동시에 개발하다 보니 이것도 문제가 되더군요. 예를 들어, 제가 PlatformFactory를 수정하는 동안 다른 개발자도 같은 파일을 수정하면 충돌이 나니까요.
이 문제를 해결한 게 Factory Method 패턴입니다.
Factory Method는 제가 실제로 가장 많이 쓰는 팩토리 패턴입니다. Simple Factory의 문제를 깔끔하게 해결해주거든요.
Simple Factory는 확장 시 코드 수정이 필요합니다. 새 플랫폼을 추가할 때마다 switch 문에 case를 추가해야 하죠.
Factory Method는 서브클래스가 객체 생성을 결정하게 합니다:
// Creator (추상 클래스)
class UIFactory {
createButton() {
throw new Error("Must implement createButton()");
}
render() {
const button = this.createButton();
button.render();
}
}
// Concrete Creators
class IOSFactory extends UIFactory {
createButton() {
return new IOSButton();
}
}
class AndroidFactory extends UIFactory {
createButton() {
return new AndroidButton();
}
}
// 사용
const factory = OS === "ios" ? new IOSFactory() : new AndroidFactory();
factory.render(); // 자동으로 올바른 버튼 생성
이 패턴을 처음 봤을 때 "음... 좀 복잡한데?"라고 생각했습니다. Simple Factory보다 코드가 길어졌거든요. 하지만 실제로 써보니 차이가 확 느껴졌습니다.
첫 번째 장점은 OCP 준수입니다. Windows 지원을 추가하려면? 그냥 WindowsFactory 클래스를 새로 만들면 됩니다. 기존 UIFactory, IOSFactory, AndroidFactory 코드는 한 줄도 수정하지 않아도 됩니다.
// Windows 추가 - 기존 코드 수정 없음!
class WindowsFactory extends UIFactory {
createButton() {
return new WindowsButton();
}
}
두 번째 장점은 확장성입니다. 팀원이 여러 명이어도 각자 다른 Factory를 만들 수 있습니다. 제가 IOSFactory를 작업하는 동안 동료가 AndroidFactory를 만들 수 있죠. 코드 충돌이 없습니다.
세 번째 장점은 프레임워크 설계에 유용하다는 점입니다. 예를 들어 UI 라이브러리를 만든다면, UIFactory만 제공하고 "버튼 생성 로직은 너희가 구현해"라고 할 수 있습니다. 라이브러리 사용자는 자신의 CustomFactory를 만들어서 원하는 방식으로 버튼을 생성하면 되죠.
이 패턴을 이해하고 나서야 저는 Spring Framework의 Bean Factory가 왜 그렇게 설계되었는지 깨달았습니다.
Factory Method를 쓰다 보니 또 다른 문제가 생겼습니다. 여러 관련 객체를 한 번에 생성해야 할 때였죠.
예를 들어 iOS UI를 만들려면 Button, Input, Checkbox를 모두 iOS 스타일로 만들어야 합니다. 그런데 실수로 IOSButton과 AndroidCheckbox를 섞어서 쓰면? UI가 엉망이 되죠.
Abstract Factory는 이 문제를 해결합니다:
// Abstract Factory
class PlatformFactory {
createButton() { throw new Error(); }
createInput() { throw new Error(); }
createCheckbox() { throw new Error(); }
}
// Concrete Factory (iOS)
class IOSFactory extends PlatformFactory {
createButton() {
return new IOSButton();
}
createInput() {
return new IOSInput();
}
createCheckbox() {
return new IOSCheckbox();
}
}
// Concrete Factory (Android)
class AndroidFactory extends PlatformFactory {
createButton() {
return new AndroidButton();
}
createInput() {
return new AndroidInput();
}
createCheckbox() {
return new AndroidCheckbox();
}
}
// 사용
function createForm(factory) {
const button = factory.createButton();
const input = factory.createInput();
const checkbox = factory.createCheckbox();
return { button, input, checkbox };
}
const factory = OS === "ios" ? new IOSFactory() : new AndroidFactory();
const form = createForm(factory); // 일관된 UI 컴포넌트들
이 패턴의 핵심은 일관성 보장입니다. createForm(new IOSFactory())를 호출하면 Button, Input, Checkbox가 모두 iOS 스타일로 생성됩니다. 실수로 섞일 일이 없죠.
첫 번째 장점은 일관성입니다. 같은 플랫폼의 컴포넌트만 생성되므로, iOS 버튼에 Android 체크박스가 붙는 황당한 상황을 방지할 수 있습니다.
두 번째 장점은 통일성입니다. 테마를 바꾸거나 플랫폼을 전환할 때 Factory만 바꾸면 전체 UI가 한 번에 바뀝니다.
저는 이 패턴을 다크 모드/라이트 모드 전환에 써봤는데, 정말 편했습니다. LightThemeFactory와 DarkThemeFactory만 만들어두면 테마 전환이 한 줄로 끝나거든요.
Abstract Factory는 다음 상황에 유용합니다:
이론만 배우면 금방 까먹습니다. 저는 실제로 써봐야 진짜 이해가 되더군요. 제가 회사에서 만든 코드를 공유해봅니다.
사용자 프로필 페이지를 만들고 있었습니다. 사용자 등급(무료/프리미엄/인증)에 따라 다른 아바타를 보여줘야 했죠.
처음 제 코드는 이랬습니다:
// 100군데에 중복
function UserProfile() {
let avatar;
if (user.isPremium) {
avatar = <PremiumAvatar user={user} size="large" border="gold" />;
} else if (user.isVerified) {
avatar = <VerifiedAvatar user={user} size="medium" border="blue" />;
} else {
avatar = <BasicAvatar user={user} size="small" />;
}
return <div>{avatar}</div>;
}
문제는 이 로직이 프로필 페이지뿐만 아니라 댓글, 채팅, 피드 등 100군데에서 반복되었다는 겁니다. 그러다가 VIP 등급이 추가되면? 100군데를 다 고쳐야 했죠.
팩토리 패턴을 적용했습니다:
// Factory
class AvatarFactory {
static create(user) {
if (user.isPremium) {
return <PremiumAvatar user={user} size="large" border="gold" />;
} else if (user.isVerified) {
return <VerifiedAvatar user={user} size="medium" border="blue" />;
} else {
return <BasicAvatar user={user} size="small" />;
}
}
}
// 사용 (1줄!)
function UserProfile() {
return <div>{AvatarFactory.create(user)}</div>;
}
이제 VIP 등급을 추가하려면 AvatarFactory만 수정하면 됩니다. 클라이언트 코드 100군데는 그대로 두고요.
이 경험을 통해 깨달은 건, 팩토리 패턴은 변경이 자주 일어나는 로직을 격리하는 데 정말 효과적이라는 겁니다. 사용자 등급은 자주 바뀌니까요. 신규 등급 추가, 기존 등급 변경, 등급별 혜택 조정... 이런 변경이 한 곳(Factory)에만 영향을 미치니 유지보수가 훨씬 쉬워졌습니다.
Java Spring을 공부할 때 Bean Factory를 봤는데, 처음엔 "이게 뭐지?"했습니다. 그런데 팩토리 패턴을 이해하고 나니 Spring의 설계가 보이더군요.
// ApplicationContext = Factory
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// Bean 생성 (Factory가 알아서 의존성 주입)
UserService userService = context.getBean(UserService.class);
Spring의 ApplicationContext가 바로 거대한 Factory입니다. 개발자는 "UserService 주세요"라고만 하면, Spring이 알아서 UserService를 생성하고, 필요한 의존성(UserRepository, PasswordEncoder 등)을 주입해줍니다.
@PostConstruct)와 소멸(@PreDestroy)을 관리모두 Factory 패턴의 응용입니다!
Spring을 처음 배울 때 "왜 이렇게 복잡해?"라고 생각했는데, 팩토리 패턴을 이해하고 나니 "아, 이게 다 생성 로직을 캡슐화한 거구나"라고 받아들였습니다.
개발자는 new UserService(new UserRepository(), new PasswordEncoder())처럼 직접 객체를 생성하지 않아도 됩니다. 그냥 "UserService 주세요"라고만 하면 Spring Factory가 알아서 만들어주죠.
이게 바로 "손님이 주방에 들어가지 않는다"는 비유의 실제 버전입니다.
제가 직접 만든 DB 연결 Factory도 소개해봅니다. 이건 제 사이드 프로젝트에서 실제로 쓰고 있는 코드입니다.
class DatabaseFactory {
static createConnection(env) {
const config = this.getConfig(env);
switch (config.type) {
case "mysql":
return new MySQLConnection(config);
case "postgres":
return new PostgresConnection(config);
case "mongodb":
return new MongoDBConnection(config);
default:
throw new Error(`Unknown DB: ${config.type}`);
}
}
static getConfig(env) {
if (env === "production") {
return {
type: "postgres",
host: "prod-db.company.com",
port: 5432
};
} else {
return {
type: "mysql",
host: "localhost",
port: 3306
};
}
}
}
// 사용
const db = DatabaseFactory.createConnection("production");
db.query("SELECT * FROM users");
제 서비스는 개발 환경에선 MySQL을 쓰고, 프로덕션에선 PostgreSQL을 씁니다. 팩토리 없이 만들면 이렇게 됐을 겁니다:
// ❌ 팩토리 없이
let db;
if (env === "production") {
db = new PostgresConnection({
host: "prod-db.company.com",
port: 5432,
user: "admin",
password: process.env.DB_PASSWORD
});
} else {
db = new MySQLConnection({
host: "localhost",
port: 3306,
user: "root",
password: "1234"
});
}
이 코드가 DB 연결이 필요한 모든 곳(컨트롤러, 서비스, 테스트 코드)에 반복되었을 겁니다. 나중에 MongoDB로 바꾸거나 연결 설정을 변경하려면? 지옥이었겠죠.
Factory를 쓰니 클라이언트 코드는 환경 변화에 전혀 영향을 받지 않습니다. 그냥 DatabaseFactory.createConnection(env) 호출만 하면 끝이니까요.
실제로 써보니 이런 점이 편했습니다:
이런 복잡한 로직을 클라이언트에서 일일이 처리하지 않아도 되니 코드가 훨씬 깔끔해졌습니다.
팩토리 패턴을 공부하면서 제일 고민했던 게 "언제 써야 하나?"였습니다. 모든 객체 생성에 팩토리를 쓰면 과도한 설계고, 안 쓰면 중복 코드가 생기니까요.
제 경험상 이럴 때 팩토리를 쓰면 좋습니다:
// ❌ 이런 거 100번 반복하기 싫다
const db = new Database({
host: "db.example.com",
port: 5432,
user: "admin",
password: process.env.DB_PASSWORD,
ssl: true,
poolSize: 20,
timeout: 30000,
retryAttempts: 3
});
// ✅ Factory로 감싸면
const db = DatabaseFactory.create("production");
2. 플랫폼/환경마다 다른 객체가 필요할 때
// iOS Button vs Android Button
// MySQL Connection vs PostgreSQL Connection
// Light Theme vs Dark Theme
제 경험상 이런 경우가 제일 많았습니다. 크로스 플랫폼 앱, 멀티 환경 배포, 테마 시스템 등...
3. 객체 조합에 일관성이 필요할 때// Dark 테마 → Button, Input, Checkbox 모두 Dark
// iOS → Button, Input, Checkbox 모두 iOS 스타일
Abstract Factory가 빛나는 순간입니다.
4. 객체 생성 방식이 자주 바뀔 때사용자 등급, 플랜, 권한 같은 건 자주 바뀝니다. 이런 로직을 100군데에 흩뿌려놓으면 유지보수가 지옥이죠.
하지만 과도한 설계도 문제입니다. 이럴 땐 팩토리를 쓰지 마세요:
// ❌ 과도한 설계
class UserFactory {
static createUser(name) {
return new User(name);
}
}
// ✅ 그냥 직접 만드세요
const user = new User("Alice");
User 객체가 단순하고, 생성 로직이 바뀔 일이 없고, 플랫폼별로 다를 이유가 없다면? 그냥 new User()로 만드세요. 팩토리는 오히려 복잡도만 증가시킵니다.
제가 배운 원칙: "필요할 때만 쓴다." 팩토리 패턴을 알았다고 모든 곳에 남발하지 말고, 정말로 생성 로직을 캡슐화할 필요가 있을 때만 쓰는 게 중요합니다.
팩토리 패턴을 공부하면서 가장 헷갈렸던 게 Simple Factory, Factory Method, Abstract Factory의 차이였습니다. 저는 이렇게 정리했습니다:
| 패턴 | 특징 | 구현 방식 | 장점 | 단점 | 예시 |
|---|---|---|---|---|---|
| Simple Factory | 한 메서드에서 분기 | static 메서드에 switch문 | 간단함, 이해 쉬움 | OCP 위반 | createButton(type) |
| Factory Method | 서브클래스가 생성 결정 | 추상 클래스 상속 | OCP 준수, 확장성 | 클래스 개수 증가 | IOSFactory.createButton() |
| Abstract Factory | 관련 객체 여러 개 동시 생성 | 여러 create 메서드 | 일관성 보장 | 복잡도 증가 | IOSFactory.createButton/Input/Checkbox() |
제가 실제로 쓰는 선택 기준입니다:
처음엔 Simple Factory로 시작해서, 필요할 때 Factory Method로 리팩토링하는 게 제일 현실적인 것 같습니다.
팩토리 패턴을 공부하기 전과 후의 제 생각이 완전히 바뀌었습니다.
Before: "그냥 new로 만들면 되는데 왜 이렇게 복잡하게 만들어?"
After: "생성 로직을 클라이언트 코드에 노출하면 나중에 지옥이야. Factory에 맡기자."
팩토리 패턴의 핵심은 다음 네 가지라고 정리했습니다:
제가 처음 "그냥 new로 만들면 안 돼요?"라고 물었을 때, 시니어가 한 말을 이제야 이해했습니다:
"객체 생성도 책임 분리다.""
new키워드는 강력하지만 위험합니다. 모든 곳에new IOSButton()하면, 나중에IOSButton생성자가 바뀌면 지옥이죠. Factory에 위임하세요. 변경은 한 곳에서."
지금은 코드 리뷰를 할 때 제가 이렇게 말합니다: "손님이 주방에 들어가지 마세요. Factory한테 주문하세요."
팩토리 패턴은 단순히 객체를 만드는 패턴이 아니라, 변경에 유연한 설계를 만드는 사고방식이라는 걸 받아들였습니다. 이 깨달음이 제 설계 철학을 완전히 바꿔놓았습니다.