팩토리 패턴이란?
객체 생성(인스턴스화) 과정을 캡슐화하여 감추고,
상황에 따라 다른 클래스의 객체를 생성할 수 있도록 하는 패턴이다.
개념만 보면 감이 안잡힐테니, 예시와 함께 살펴보자.
밑의 코드는 자동차 클래스를 생성하는 내용이다.
심플 팩토리
// 자동차 클래스 (부모 클래스)
class Car {
constructor(name) {
this.name = name;
}
drive() {
console.log(`${this.name}를 운전 중입니다.`);
}
}
// Sedan 클래스 (Car를 상속, 하위(자식)클래스)
class Sedan extends Car {
constructor() {
super("Sedan");
}
}
// SUV 클래스 (Car를 상속, 하위(자식)클래스)
class SUV extends Car {
constructor() {
super("SUV");
}
}
// 자동차 팩토리 (객체 생성을 담당)
class CarFactory {
static createCar(type) {
if (type === "sedan") {
return new Sedan();
} else if (type === "suv") {
return new SUV();
} else {
throw new Error("알 수 없는 자동차 타입입니다.");
}
}
}
// 사용 예시 (팩토리 메서드를 통해 자동차 객체 생성)
const myCar = CarFactory.createCar("sedan");
myCar.drive(); // "Sedan를 운전 중입니다."
const yourCar = CarFactory.createCar("suv");
yourCar.drive(); // "SUV를 운전 중입니다."
생성자를 가지는 부모 Car 클래스가 있고, extends를 통해 Car을 Sedan 클래스와 SUV 클래스가 상속받았다.
생성자 내에서 super을 사용하면 부모 클래스의 생성자 함수를 호출한다.
Sedan과 SUV는 부모 클래스의 함수인 drive 역시 사용할 수 있다.
사용자는 CarFactory.createCar("sedan")을 통해 CarFactory 객체를 생성한다.
CarFactory에서는 "sedan"이라는 타입이 있는지 검사하고,
조건문에 해당 타입이 있으면 생성자를 호출한다. 해당 타입이 없다면, 에러를 던진다.
즉, 선언된 클래스가 등록되어 있으면 객체를 생성하는 로직이다.
여기서 주목해야 할 점은,
자동차 클래스를 생성할 때, 자동차 클래스 자기 자신에서가 아닌
자동차 팩토리에서 객체를 만든다는 것이다.
이렇게 객체 생성 로직을 감춤으로써 코드의 유연성이 증가하고,
클라이언트는 생성 세부 사항을 몰라도 사용이 가능하다.
무엇보다 팩토리 패턴의 장점은 새로운 클래스를 추가할 때 기존 코드의 수정을 최소화한다는 것인데,
심플 팩토리 패턴 코드를 보면 그렇지 않아 보인다.
새로운 클래스도 정의해주고, 팩토리에서도 조건문을 추가해야 한다.
이건 객체 지향 설계의 원칙인 OCP(Open-Closed Principle)에 어긋난다.
새로운 코드 확장에는 열려있으면서, 기존의 코드는 닫혀있어야 한다는 원리이다.
즉 기존의 코드는 최대한 수정하지 않는 것이 바람직하다.
그래서 우리는 등록형 팩토리를 통해, 새로운 자동차 클래스가 추가되더라도
팩토리 코드는 수정할 필요가 없도록 코드를 짜볼 것이다.
등록형 팩토리
// 기존의 Factory는 새로운 차종이 생기면 CarFactory의 코드를 수정해야 함.
// 등록형 Factory로 CarFactory를 닫힌 상태로 유지하기
class Car {
constructor(name) {
this.name = name;
}
drive() {
console.log(`${this.name}를 운전 중입니다.`);
}
}
class Sedan extends Car {
constructor() {
super("Sedan");
}
}
class SUV extends Car {
constructor() {
super("SUV");
}
}
class CarFactory {
static registry = {};
static register(key, value) {
this.registry[key] = value;
}
static createCar(key) {
const CarClass = this.registry[key];
if (!CarClass) {
console.log("알 수 없는 자동차 타입입니다.");
} else return new CarClass();
}
}
CarFactory.register("sedan", Sedan);
CarFactory.register("suv", SUV);
const myCar = CarFactory.createCar("suv");
myCar.drive();
CarFactory에 registry라는 객체를 만듦으로써, key-value값을 등록해야 객체를 생성할 수 있도록 했다.
여기서 value는 클래스 자체이다.
key-value 값이 등록되어 있지 않으면, CarClass는 존재하지 않으므로 "알 수 없는 자동차 타입입니다."를 콘솔에 반환한다.
반대로, 등록되어 있다면 클래스 생성자를 호출한다.
이처럼 등록형 팩토리를 사용하면, 새로운 클래스가 추가되더라도 기존 코드(팩토리)에는 영향을 주지 않게 된다.
이건 "레지스트리 기반 팩토리(Registry-Based Factory)"라고도 부른다.
그런데 팩토리 패턴은 여기서 끝나지 않는다.
또다른 형태인 팩토리 메소드가 있다.
팩토리 메소드란, 객체 생성 책임을 서브클래스에게 위임하는 팩토리 구조이다.
이게 대체 무슨 말이지? 이해가 잘 안간다면 정상이다.
이번에는 알림 시스템 기능이 있다고 하자.
알림을 보내는 기능을 담당하는 Product 요소,
알림 기능을 생성하고 호출하는 Creator 요소가 있다.
Product에는 공통 기능을 가진 부모 클래스가 있고, 실제 기능을 구현하는 자식 클래스들이 있을 것이다.
여기서 상세 기능을 구현하는 자식 클래스는 Concrete Product 요소라고 한다.
사용자인 Creator 입장에서는,
인스턴스를 생성하는 메서드(=팩토리 메서드)를 갖고 있고, 공롱 로직을 갖고 있는 부모 클래스와, 어떤 Product를 생성할지 결정하는 자식 클래스가 있다.
여기서도 자식은 구체적으로 실제 생성 방식을 결정하므로, Concrete Creator 요소라 한다.
팩토리 메소드
// [1] 공통 인터페이스 역할: 알림을 보내는 방식
class Notifier {
// 모든 알림은 send() 메서드를 구현해야 함
send(message) {
throw new Error("send() 메서드를 오버라이드해야 합니다");
}
}
// [2] 실제 알림 클래스들
class EmailNotifier extends Notifier {
send(message) {
console.log(`[이메일] ${message}`);
}
}
class SMSNotifier extends Notifier {
send(message) {
console.log(`[문자] ${message}`);
}
}
// [3] Creator 클래스: 알림을 만드는 책임 담당
class NotificationSender {
// 이 메서드는 서브클래스에서 구현할 예정
createNotifier() {
throw new Error("createNotifier()를 서브클래스에서 구현하세요");
}
// 알림을 보낼 때 공통으로 사용하는 메서드
notifyUser(message) {
const notifier = this.createNotifier(); // 어떤 알림 객체를 만들지는 서브클래스가 결정!
notifier.send(message); // 알림 전송
}
}
// [4] 서브클래스에서 실제 생성 방식 결정!
class EmailSender extends NotificationSender {
createNotifier() {
return new EmailNotifier();
}
}
class SMSSender extends NotificationSender {
createNotifier() {
return new SMSNotifier();
}
}
// [5] 실제 사용
const sender1 = new EmailSender();
sender1.notifyUser("가입을 축하합니다!"); // [이메일] 가입을 축하합니다!
const sender2 = new SMSSender();
sender2.notifyUser("인증번호는 1234입니다"); // [문자] 인증번호는 1234입니다
복잡하니까 계층 구조를 그림으로 도식화하여 살펴보면 이해가 편하다.

사실 푸시 알림 코드도 존재했는데, 분량상 코드가 너무 길어져 삭제했다. 새로운 푸시 클래스가 추가되면 저렇게 되리라 이해하면 되겠다.
중요한 것은 객체 생성이 서브 클래스에서 이루어진다는 것이다.
즉 객체 각각의 생성과 구현을 서브 클래스에서 하면서 기존 코드를 안 건드리고 새로운 기능을 넣을 수 있고, 이로써 확장성과 안정성이 증가한다.
다만 새 클래스 작성량이 증가하고 구조가 복잡해진다는 단점이 있다.
그렇다면 등록형 팩토리와 팩토리 메소드 모두 OCP를 만족하는데,
어떤 방식을 선택하고 사용해야 할까?
선택 기준은 목표와 상황의 복잡도이다.
둘의 차이를 비교해보면,
- 등록형 팩토리는 구조가 동적이고 유연함
- 런타임에 클래스 등록 가능
- 함수형 스타일, 플러그인 시스템 등에 적합
- 단점: 클래스 관계가 느슨해서 구조적으로 "의도"가 흐릿할 수 있음
- 팩토리 메소드 패턴은 구조가 정적이고 엄격
- 클래스 간 관계가 명확
- 확장, 테스트, DI 설계에 강함
- 단점: 구조가 무겁고 클래스 수가 늘어남
따라서 등록이 간단하고 유연한 시스템에서는 등록형 팩토리, 구조화가 중요하거나 프레임워크 레벨의 설계를 할 때는 팩토리 메소드를 주로 사용한다.
이렇게 이번 포스팅에서는 심플 팩토리, 등록형 팩토리, 팩토리 메소드를 알아보았다.
+추상 팩토리 패턴이라는 것도 있다는데, 추후 공부하게 되면 추가하도록 하겠다.
필자도 공부하는 입장이므로 포스팅 내용에 대한 오류, 보충 설명 등이 있다면 알려주시길 바란다.
'프론트엔드 > TAVE-15기' 카테고리의 다른 글
| [React Native] npm 패키지 의존성 충돌(query-string) 해결하기 (0) | 2025.09.04 |
|---|---|
| 웨더타고 - TAVE 15기 연합프로젝트 프론트엔드 회고 (2) | 2025.08.03 |
| [디자인 패턴] 플라이웨이트 패턴 (Flyweight Pattern) (0) | 2025.04.10 |
| [디자인 패턴] 싱글톤 패턴(Singleton Pattern) (0) | 2025.03.31 |