헤드 퍼스트 디자인 패턴 4장을 읽고 정리한 글이다.
처음엔 술술 읽혔는데, 다 읽고 나니 "팩토리 메소드랑 추상 팩토리가 뭐가 다르지?"라는 질문에 답을 못 하겠더라. 그래서 책의 피자 가게 예제를 다시 따라가며 정리했고, 처음 보는 사람도 이해하기 쉽게 더 익숙한 예제도 같이 넣었다.
■ 먼저, 팩토리가 뭔데?
팩토리는 말 그대로 공장이다. 프로그래밍에서는 객체를 대신 만들어 주는 코드를 말한다.
카페에서 손님은 직접 커피를 내리지 않는다. 카운터에 "아메리카노 주세요"라고 말하면 바리스타가 만들어 준다. 손님은 결과물만 받으면 된다.
코드도 마찬가지다. 객체가 필요한 쪽이 new로 직접 만들지 않고, 팩토리에게 "이거 만들어 줘"라고 부탁하는 구조가 팩토리 패턴의 기본 아이디어다.
■ 팩토리 패턴은 세 가지다

· 간단한 팩토리 : 주문 창구에 맡기기
· 팩토리 메소드 패턴 : 본사 매뉴얼의 빈칸을 지점이 채우기
· 추상 팩토리 패턴 : 재료 세트를 납품하는 공장 바꾸기
정식 GoF 패턴은 뒤의 두 개뿐이고, 간단한 팩토리는 자주 쓰는 관용구에 가깝다.
세 가지 모두 목적은 같다. 객체 생성 코드를 한곳에 모아서, 나머지 코드는 추상 타입만 알게 만드는 것이다. 차이는 "구체 클래스를 누가, 어떻게 고르느냐"뿐이다.
■ 1. 왜 팩토리가 필요할까
처음 피자 주문 코드는 이렇다.
Pizza orderPizza(String type) {
Pizza pizza;
if (type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("veggie")) {
pizza = new VeggiePizza();
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
신메뉴가 나오거나 메뉴를 뺄 때마다 이 메소드를 열어서 고쳐야 한다.

원인은 한 메소드 안에 성격이 다른 코드가 섞여 있기 때문이다.
· if 분기 → 메뉴가 바뀔 때마다 바뀜
· 준비, 굽기, 자르기, 포장 → 거의 안 바뀜
new 자체가 문제가 아니다. 객체는 언젠가 반드시 만들어야 한다. 문제는 바뀌는 코드와 안 바뀌는 코드가 붙어 있다는 것이다. 그래서 객체 생성 부분을 떼어 내는 데서 팩토리 이야기가 시작된다.
■ 2. 간단한 팩토리
if 분기를 별도 클래스로 옮긴다.
public class SimplePizzaFactory {
public Pizza createPizza(String type) {
if (type.equals("cheese")) return new CheesePizza();
if (type.equals("veggie")) return new VeggiePizza();
return null;
}
}
PizzaStore는 팩토리에 부탁만 한다.
Pizza pizza = factory.createPizza(type);
처음엔 "위치만 옮긴 거 아닌가?" 싶었다. 하지만 주문, 메뉴판, 배달처럼 피자가 필요한 곳이 여러 군데라면, 메뉴가 바뀌어도 고칠 곳은 팩토리 한 곳뿐이다.
참고로 createPizza()를 static으로 만드는 정적 팩토리도 많이 쓴다. 객체 없이 바로 호출할 수 있지만, 서브클래스로 생성 방식을 바꿀 수 없다는 단점이 있다.
■ 3. 팩토리 메소드 패턴
▶ 문제 상황
뉴욕, 시카고에 지점이 생겼다. 뉴욕은 얇은 크러스트, 시카고는 두꺼운 딥 디쉬로 스타일이 다르다.
지점별 팩토리를 만들어 줬더니, 지점들이 굽는 방식을 바꾸거나 자르는 걸 빼먹기 시작했다. 팩토리만 넘겨줬지 주문 절차를 강제할 방법이 없었던 것이다.
원하는 건 두 가지다.
· 주문 절차는 본사가 고정
· 어떤 피자를 만들지는 지점이 결정
햄버거 프랜차이즈를 떠올리면 쉽다. 주문, 조리, 포장 순서는 전 세계 매장이 똑같지만, 메뉴는 나라마다 조금씩 다르다. 본사 매뉴얼에 "여기 들어갈 버거는 지점이 정한다"라는 빈칸이 있는 셈이다.
▶ 해결
생성 메소드를 추상 메소드로 만든다.
public abstract class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
protected abstract Pizza createPizza(String type);
}
지점은 상속받아서 createPizza()만 구현한다.
public class NYPizzaStore extends PizzaStore {
protected Pizza createPizza(String item) {
if (item.equals("cheese")) return new NYStyleCheesePizza();
if (item.equals("veggie")) return new NYStyleVeggiePizza();
return null;
}
}
public class ChicagoPizzaStore extends PizzaStore {
protected Pizza createPizza(String item) {
if (item.equals("cheese")) return new ChicagoStyleCheesePizza();
if (item.equals("veggie")) return new ChicagoStyleVeggiePizza();
return null;
}
}
}
여기서 createPizza()가 팩토리 메소드다.

▶ 주문이 처리되는 흐름
PizzaStore nyStore = new NYPizzaStore();
Pizza pizza = nyStore.orderPizza("cheese");
① NYPizzaStore를 만든다
② 부모에 정의된 orderPizza("cheese")를 호출한다
③ 그 안에서 createPizza()가 호출되는데, NYPizzaStore가 구현했으니 뉴욕 치즈 피자가 만들어진다
④ 돌아온 피자로 준비, 굽기, 자르기, 포장을 한다
시카고 지점에서 주문하면 ③에서 시카고 피자가 만들어진다. 시카고 피자는 cut()을 오버라이드해서 네모로 잘린다.
▶ 정의
객체를 생성하는 인터페이스를 만들고, 어떤 클래스의 인스턴스를 만들지는 서브클래스가 결정한다.
▶ "서브클래스가 결정한다"의 의미
제일 헷갈렸던 문장이다. NYPizzaStore 어디에도 뭔가를 결정하는 코드는 없으니까.
여기서 결정은 실행 중 판단이 아니다. 부모는 어떤 피자가 나올지 모른 채로 작성되고, new NYPizzaStore()를 쓰는 순간 결과가 정해진다는 뜻이다.
빈칸 채우기 시험지를 떠올리면 쉽다. 문제지(부모)는 모두에게 똑같지만, 빈칸의 답은 학생(자식)마다 다르다. 그래서 이렇게 외웠다. 부모가 빈칸을 남기고, 자식이 채운다.
▶ orderPizza()는 무슨 피자인지 알까?
모른다. 그냥 Pizza로만 다룬다. 시카고 피자의 네모 자르기도 피자 객체가 스스로 처리한다. 그래서 피자가 몇 종류로 늘어나든 orderPizza()는 바뀌지 않는다.
▶ 더 쉬운 예제 : 알림 보내기
알림을 보내는 절차(전송 → 기록 저장)는 같고, 이메일로 보낼지 문자로 보낼지만 다르다고 해 보자.
interface Notification {
void send(String message);
}
class EmailNotification implements Notification {
public void send(String message) { System.out.println("이메일 전송: " + message); }
}
class SmsNotification implements Notification {
public void send(String message) { System.out.println("문자 전송: " + message); }
}
abstract class NotificationService {
public void notify(String message) {
Notification n = createNotification(); // 빈칸
n.send(message);
System.out.println("전송 기록 저장 완료");
}
protected abstract Notification createNotification();
}
class EmailService extends NotificationService {
protected Notification createNotification() { return new EmailNotification(); }
}
class SmsService extends NotificationService {
protected Notification createNotification() { return new SmsNotification(); }
}
카카오톡 알림이 추가되면 KakaoService와 KakaoNotification만 새로 만들면 된다. notify()는 그대로다. 피자 예제와 구조가 똑같다.
· PizzaStore → NotificationService
· orderPizza() → notify()
· createPizza() → createNotification()
· Pizza → Notification
▶ 등장인물
· Creator (PizzaStore) : 팩토리 메소드를 선언하는 추상 클래스
· ConcreteCreator (NYPizzaStore) : 팩토리 메소드를 구현
· Product (Pizza) : 만들어지는 객체의 추상 타입
· ConcreteProduct (NYStyleCheesePizza) : 실제로 만들어지는 객체
▶ 새 지점을 추가하면?
캘리포니아 지점이 생기면 CaliforniaPizzaStore와 캘리포니아 피자 클래스들만 만들면 된다. PizzaStore, orderPizza(), 기존 지점은 손대지 않는다. 이게 OCP를 지키는 구조다.
OCP(개방-폐쇄 원칙)는 클래스가 확장에는 열려 있고 변경에는 닫혀 있어야 한다는 원칙이다. 멀티탭처럼, 새 기기를 쓸 때 벽 속 배선을 뜯지 않고 플러그만 꽂으면 되는 구조라고 생각하면 된다.
반대로 팩토리 없이 한 클래스에서 모든 피자를 만들면, 뉴욕·시카고만으로 구체 피자 클래스 8개에 의존하고 캘리포니아가 들어가면 12개가 된다.
■ 4. 의존성 뒤집기 원칙 (DIP)
여기서 'A가 B에 의존한다'는 말은 'B가 바뀌면 A도 영향을 받는다'는 뜻이다.

적용 전에는 PizzaStore가 모든 구체 피자 클래스에 직접 의존한다. 의존 방향이 위에서 아래로만 흐른다.
적용 후에는 PizzaStore도, 구체 피자 클래스들도 모두 추상 클래스 Pizza에 의존한다.
"추상화된 것에 의존하고, 구상 클래스에 의존하지 않게 만든다."
충전기로 비유하면, 예전엔 폰마다 전용 충전기가 필요했지만(구체적인 것에 의존) 지금은 USB-C라는 공통 규격만 맞추면 된다(추상화에 의존). USB-C 규격이 Pizza 추상 클래스 역할이다.
아래쪽 클래스들이 위쪽 추상화를 향하게 되면서 의존 방향이 뒤집혔다. 그래서 '뒤집기'다.
물론 완벽하게 지킬 수는 없다. String처럼 바뀔 일 없는 클래스는 그냥 new 해도 된다. 중요한 건 바뀔 가능성이 있는 클래스를 직접 생성하지 않는 것이다.
■ 5. 추상 팩토리 패턴
▶ 문제 상황
일부 지점이 싼 재료를 몰래 쓰기 시작해서, 본사가 재료를 직접 공급하기로 했다. 그런데 지역마다 재료가 다르다.
· 뉴욕 : 얇은 도우, 마리나라 소스, 레지아노 치즈, 신선한 조개
· 시카고 : 두꺼운 도우, 플럼토마토 소스, 모짜렐라 치즈, 냉동 조개
재료의 종류는 같지만 구체적인 재료가 다르고, 반드시 세트로 써야 한다. 뉴욕 도우에 시카고 소스가 섞이면 안 된다. 이렇게 함께 쓰이는 객체 묶음을 제품군(family)이라고 한다.
가구로 비유하면, 모던 의자에 앤티크 소파를 두면 어색하니까 "모던 세트", "클래식 세트"처럼 세트로 사는 것과 같다.
▶ 해결
제품군을 만드는 인터페이스를 정의한다.
public interface PizzaIngredientFactory {
Dough createDough();
Sauce createSauce();
Cheese createCheese();
Clams createClam();
}
지역별로 구현한다.
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
public Dough createDough() { return new ThinCrustDough(); }
public Sauce createSauce() { return new MarinaraSauce(); }
public Cheese createCheese() { return new ReggianoCheese(); }
public Clams createClam() { return new FreshClams(); }
}
피자는 공장을 주입받아서 재료를 요청한다.
public class CheesePizza extends Pizza {
PizzaIngredientFactory factory;
public CheesePizza(PizzaIngredientFactory factory) {
this.factory = factory;
}
void prepare() {
dough = factory.createDough();
sauce = factory.createSauce();
cheese = factory.createCheese();
}
}
지점은 자기 지역 공장을 피자에 넣어 준다.
Pizza pizza = new CheesePizza(new NYPizzaIngredientFactory());

▶ 달라진 점
CheesePizza는 어느 지역 재료인지 모른다. 받은 재료로 피자를 만들 뿐이다. 그래서 뉴욕 치즈 피자, 시카고 치즈 피자를 따로 만들 필요 없이 CheesePizza 하나에 공장만 바꿔 넣으면 된다.
▶ 정의
구상 클래스에 의존하지 않고, 서로 연관된 객체들의 제품군을 생성하는 인터페이스를 제공한다.
▶ 더 쉬운 예제 : 다크 모드
앱에서 다크 모드로 바꾸면 버튼, 체크박스가 한꺼번에 어두운 스타일로 바뀐다. 버튼만 다크인데 체크박스는 라이트면 이상하다. 바로 제품군이다.
interface ThemeFactory {
Button createButton();
Checkbox createCheckbox();
}
class LightThemeFactory implements ThemeFactory {
public Button createButton() { return new LightButton(); }
public Checkbox createCheckbox() { return new LightCheckbox(); }
}
class DarkThemeFactory implements ThemeFactory {
public Button createButton() { return new DarkButton(); }
public Checkbox createCheckbox() { return new DarkCheckbox(); }
}
class SettingsScreen {
private final ThemeFactory theme;
SettingsScreen(ThemeFactory theme) { this.theme = theme; }
void draw() {
theme.createButton().render();
theme.createCheckbox().render();
}
}
new SettingsScreen(new DarkThemeFactory()).draw();
SettingsScreen은 무슨 테마인지 모른다. 공장만 바꾸면 화면 전체 스타일이 한꺼번에 바뀐다.
· PizzaIngredientFactory → ThemeFactory
· NYPizzaIngredientFactory → DarkThemeFactory
· 도우, 소스, 치즈 → 버튼, 체크박스
· CheesePizza → SettingsScreen
▶ 등장인물
· AbstractFactory (PizzaIngredientFactory) : 제품군 생성 메소드를 선언한 인터페이스
· ConcreteFactory (NYPizzaIngredientFactory) : 특정 제품군을 실제로 생성
· AbstractProduct (Dough, Sauce) : 각 제품의 추상 타입
· ConcreteProduct (ThinCrustDough) : 실제 제품
· Client (CheesePizza) : 추상 타입만 알고 사용하는 쪽
■ 6. 둘의 차이

[팩토리 메소드]
· 방식 : 상속
· 생성 담당 : 메소드
· 만드는 것 : 제품 한 종류
· 바꾸는 법 : 다른 서브클래스 선택
· 만든 쪽이 직접 사용 : O
· 비유 : 본사 매뉴얼의 빈칸
[추상 팩토리]
· 방식 : 구성 (객체 주입)
· 생성 담당 : 별도 객체
· 만드는 것 : 제품군 (세트)
· 바꾸는 법 : 다른 팩토리 객체 주입
· 만든 쪽이 직접 사용 : X
· 비유 : 가구 세트, 테마
▶ 상속 vs 구성
팩토리 메소드는 자기 메소드를 부르고, 바꾸려면 다른 서브클래스를 고른다.
Pizza pizza = createPizza(type);
추상 팩토리는 주입받은 객체에게 부탁하고, 바꾸려면 다른 공장을 넣는다.
dough = factory.createDough();
▶ 하나 vs 세트
팩토리 메소드는 결국 Pizza 한 종류를 만든다. 추상 팩토리는 도우, 소스, 치즈를 한 세트로 만들고, 공장 하나만 고르면 세트 전체가 맞춰진다.
▶ 만든 쪽이 직접 쓰는가
팩토리 메소드는 orderPizza()가 피자를 만들고 바로 굽는다. 추상 팩토리는 공장이 만들기만 하고, 재료로 피자를 만드는 건 CheesePizza다.
■ 7. 헷갈렸던 이유
첫째, 생김새가 같다. createDough() 같은 메소드는 팩토리 메소드와 똑같이 생겼다. 실제로 추상 팩토리는 팩토리 메소드로 구현되는 경우가 많다. 그래서 추상 팩토리는 "팩토리 메소드 여러 개를 객체 하나에 묶은 것"으로 이해하면 편하다.
둘째, 책의 최종 예제에 두 패턴이 같이 들어 있다.
public class NYPizzaStore extends PizzaStore {
protected Pizza createPizza(String item) { // 팩토리 메소드
PizzaIngredientFactory factory = new NYPizzaIngredientFactory();
if (item.equals("cheese")) return new CheesePizza(factory); // 추상 팩토리
return null;
}
}
· PizzaStore ↔ NYPizzaStore : 팩토리 메소드
· CheesePizza ↔ PizzaIngredientFactory : 추상 팩토리
이 두 관계를 따로 떼어 보니까 그제야 정리가 됐다.
■ 8. 구분하는 법
헷갈릴 땐 두 가지만 확인한다.
① 생성 메소드를 this에게 부르나, 주입받은 객체에게 부르나?
② 바꿀 때 서브클래스를 고르나, 객체를 넣나?
앞쪽이면 팩토리 메소드, 뒤쪽이면 추상 팩토리다.
쉬운 예제에 대입하면, NotificationService는 this.createNotification()을 부르니 팩토리 메소드, SettingsScreen은 theme.createButton()을 부르니 추상 팩토리다.
■ 9. 한계
▶ 팩토리 메소드
· 지점이 늘수록 클래스도 계속 늘어난다
· String으로 종류를 넘기면 오타를 컴파일 때 못 잡는다 (enum이나 상수로 보완)
▶ 추상 팩토리
세트를 바꾸는 건 쉽지만, 세트 구성을 바꾸는 건 어렵다.
· 한 지역이 조개 대신 새우를 쓴다 → 그 지역 공장만 수정
· 모든 피자에 버섯이 추가된다 → 인터페이스와 모든 지역 공장을 다 고쳐야 함
테마로 보면, 파란 테마 추가는 쉽지만 모든 테마에 '슬라이더'를 추가하려면 공장을 전부 고쳐야 한다.
▶ 공통
구현체가 하나뿐이고 바뀔 일이 없다면 굳이 쓸 필요 없다. 패턴은 클래스 수를 늘리기 때문이다.
■ 11. 언제 무엇을 쓸까
▶ 팩토리 메소드
· 바뀌는 대상이 한 종류의 객체일 때
· 공통 절차는 부모가 고정하고, 생성만 자식에게 맡길 때
· 예 : 알림 수단이 늘어나는 서비스
▶ 추상 팩토리
· 여러 객체가 세트로 같이 바뀌어야 할 때
· 실행 중에 제품군을 바꾸거나, 테스트용 가짜 공장이 필요할 때
· 예 : 라이트·다크 테마, 운영체제별 UI
결국 핵심 질문은 하나다. "이 시스템에서 앞으로 뭐가 자주 바뀔까?"
팩토리 메소드는 생산자가 늘어나는 변화에, 추상 팩토리는 제품군이 바뀌는 변화에 맞춘 설계다.
■ 마무리
· 간단한 팩토리 : 생성 코드를 한곳에 모은다 (카페 주문 창구)
· 팩토리 메소드 : 부모가 빈칸을 남기고 자식이 채운다 (본사 매뉴얼의 빈칸)
· 추상 팩토리 : 공장 객체를 통째로 갈아 끼운다 (가구 세트, 테마)
세 가지 모두 코드가 구체 클래스가 아닌 추상화에 의존하게 만들기 위한 방법이다.
처음엔 "객체 하나 만드는 데 이렇게까지 해야 하나" 싶었는데, 정리하고 나니 이 장의 진짜 주제는 변화가 어디서 일어날지 예측하고 그 지점에 칸막이를 치는 연습이었던 것 같다.
'Design Pattern' 카테고리의 다른 글
| Singleton (0) | 2026.10.11 |
|---|---|
| Builder + Prototype (0) | 2026.10.11 |
| 데코레이터 패턴, 날씨 API 예제로 이해하기 (0) | 2026.10.05 |
| 옵저버 패턴으로 회원 탈퇴 후 남는 데이터 정리하기 (0) | 2026.09.30 |
| [디자인 패턴] 데코레이터 패턴, 기존 객체에 기능 덧붙이기 (0) | 2026.09.29 |