본문 바로가기
TYLER SONGBlog
← Blog index
CS학습5 min read

Factory: 객체 생성 책임을 분리해 호출부와 구체 클래스의 결합을 끊는 패턴

팩토리(Factory)는 객체를 만드는 코드를 별도의 함수나 클래스로 분리해, 호출하는 쪽이 구체 클래스를 직접 알지 못하게 하는 생성 패턴이다. 단순 팩토리, 팩토리 메서드(Factory Method), 추상 팩토리(Abstract Factory)로 나뉘며 각각 해결하는 문제의 범위가 다르다. 이 글에서는 세 형태의 차이와 TypeScript 구현, 실무

By 송민성

1. 개념

팩토리(Factory)는 객체 생성 로직을 호출부에서 분리해 별도의 함수·메서드·클래스에 맡기는 생성 패턴(Creational Pattern)이다. 호출하는 쪽은 new ConcreteClass()를 직접 쓰지 않고, 팩토리에 "무엇이 필요한지"만 전달한다.

흔히 팩토리라고 부르는 것은 세 가지로 구분된다.

구분설명GoF 패턴 여부
단순 팩토리(Simple Factory)조건에 따라 객체를 만들어 반환하는 함수 또는 클래스아님(관용적 기법)
팩토리 메서드(Factory Method)생성 메서드를 상위 클래스에 선언하고, 하위 클래스가 어떤 객체를 만들지 결정GoF 패턴
추상 팩토리(Abstract Factory)서로 관련된 객체들의 "제품군"을 일관되게 생성하는 인터페이스GoF 패턴

2. 왜 사용하는가

생성 코드가 호출부에 흩어져 있으면 다음 문제가 생긴다.

  • 강한 결합(Tight Coupling): 호출부가 구체 클래스에 직접 의존하므로, 구현체를 바꿀 때 호출부를 모두 수정해야 한다.
  • 생성 로직 중복: 설정값 주입, 초기화 순서 같은 절차가 여러 곳에 복제된다.
  • 테스트 어려움: 구체 클래스를 직접 생성하면 가짜 객체(Mock)로 교체하기 어렵다.

팩토리는 생성 책임을 한곳에 모아 개방-폐쇄 원칙(OCP, Open-Closed Principle)과 의존성 역전 원칙(DIP, Dependency Inversion Principle)을 지키기 쉽게 만든다.

3. 동작 원리

핵심은 호출부가 인터페이스(추상)에만 의존하고, 구체 타입 선택은 팩토리 안에서 일어난다는 점이다.

  1. 제품(Product) 인터페이스를 정의한다.
  2. 구체 제품(Concrete Product)이 인터페이스를 구현한다.
  3. 팩토리가 입력(인자, 설정, 환경)에 따라 구체 제품을 생성해 인터페이스 타입으로 반환한다.
  4. 호출부는 반환된 인터페이스만 사용한다.

팩토리 메서드는 3번의 선택을 "하위 클래스의 메서드 오버라이드"로 해결한다. 추상 팩토리는 여러 제품을 만드는 메서드를 한 인터페이스로 묶어, 제품군이 섞이지 않도록 보장한다.

4. 코드 예제

알림 전송 기능을 예로 든다. 아래 코드는 하나의 파일로 실행할 수 있다.

typescript
// factory.ts interface Notifier { send(to: string, message: string): string; } class EmailNotifier implements Notifier { send(to: string, message: string): string { return `[EMAIL] ${to}: ${message}`; } } class SmsNotifier implements Notifier { send(to: string, message: string): string { return `[SMS] ${to}: ${message}`; } } // 1) 단순 팩토리: 조건 분기를 한곳에 모은다 type Channel = "email" | "sms"; function createNotifier(channel: Channel): Notifier { switch (channel) { case "email": return new EmailNotifier(); case "sms": return new SmsNotifier(); } } // 2) 팩토리 메서드: 하위 클래스가 생성할 객체를 결정한다 abstract class Alert { protected abstract createNotifier(): Notifier; fire(to: string, message: string): string { const notifier = this.createNotifier(); return notifier.send(to, `ALERT - ${message}`); } } class EmailAlert extends Alert { protected createNotifier(): Notifier { return new EmailNotifier(); } } class SmsAlert extends Alert { protected createNotifier(): Notifier { return new SmsNotifier(); } } // 3) 추상 팩토리: 관련 객체의 제품군을 함께 생성한다 interface Button { render(): string } interface Input { render(): string } interface UIFactory { createButton(): Button; createInput(): Input; } class DarkUIFactory implements UIFactory { createButton(): Button { return { render: () => "dark-button" }; } createInput(): Input { return { render: () => "dark-input" }; } } class LightUIFactory implements UIFactory { createButton(): Button { return { render: () => "light-button" }; } createInput(): Input { return { render: () => "light-input" }; } } function renderForm(factory: UIFactory): string { return `${factory.createButton().render()} / ${factory.createInput().render()}`; } // 실행 console.log(createNotifier("email").send("a@example.com", "hello")); console.log(new SmsAlert().fire("010-0000-0000", "disk full")); console.log(renderForm(new DarkUIFactory())); console.log(renderForm(new LightUIFactory()));

실행 방법은 다음과 같다.

shell
npx tsx factory.ts

5. 시간 복잡도 또는 성능 특성

팩토리 자체는 알고리즘이 아니라 설계 기법이므로 별도의 점근적 복잡도를 갖지 않는다. 성능 특성은 다음과 같이 이해하면 된다.

  • 위 switch 기반 단순 팩토리의 분기 비용은 상수 시간이다. 실제 비용은 생성되는 객체의 생성자가 결정한다.
  • 팩토리를 거치면 함수 호출이 한 단계 늘어나지만, 일반적인 애플리케이션에서 체감될 만한 차이는 아니다.
  • 팩토리 안에서 객체를 캐싱하거나 풀(Pool)로 재사용하면 생성 비용이 큰 객체의 비용을 줄일 수 있다. 이는 팩토리의 장점이라기보다 팩토리가 제공하는 확장 지점이다.

6. 실무 사용 사례

  • DB·클라이언트 선택: 환경 설정(development, production)에 따라 인메모리 저장소나 실제 DB 클라이언트를 반환한다.
  • 결제·알림 채널 추상화: 결제 수단이나 알림 채널별 구현을 동일한 인터페이스로 감싼다.
  • UI 테마·플랫폼별 컴포넌트: 추상 팩토리로 테마나 플랫폼에 맞는 컴포넌트 묶음을 일관되게 만든다.
  • 프레임워크 내부: DOM의 document.createElement(tagName)은 태그 이름에 따라 알맞은 요소 객체를 만들어 주는 팩토리 형태의 API다. 의존성 주입(DI) 컨테이너가 빈(Bean)을 생성하는 방식도 팩토리 개념을 확장한 것이다.
  • 테스트: 테스트 데이터를 만드는 헬퍼(예: createUser({ role: "admin" }))도 팩토리 함수의 일종이다.

7. 주의할 점

  • 과설계: 구현체가 하나뿐이고 변경 가능성도 낮다면 팩토리는 간접 계층만 늘린다. 구현체가 둘 이상이 되거나 생성 절차가 복잡해질 때 도입해도 늦지 않다.
  • 단순 팩토리의 OCP 위반: 새 타입을 추가할 때마다 switch를 수정해야 한다. 타입이 자주 늘어난다면 레지스트리(Registry, 이름→생성 함수 맵) 방식이나 팩토리 메서드를 고려한다.
  • 추상 팩토리의 확장 비용: 제품군에 새 종류의 제품(예: createCheckbox)을 추가하면 인터페이스와 모든 구체 팩토리를 함께 수정해야 한다.
  • 용어 혼동: 단순 팩토리를 GoF의 팩토리 메서드와 같은 것으로 취급하면 설계 논의에서 오해가 생긴다. 팀 내에서 어떤 형태를 말하는지 명확히 한다.
  • 반환 타입: 팩토리가 구체 클래스 타입을 그대로 반환하면 결합을 끊는 효과가 사라진다. 인터페이스 타입으로 반환한다.

8. 핵심 정리

  • 팩토리는 객체 생성 책임을 분리해 호출부가 구체 클래스를 모르게 하는 생성 패턴이다.
  • 단순 팩토리는 조건 분기를 한곳에 모으고, 팩토리 메서드는 상속으로 생성 대상을 위임하며, 추상 팩토리는 관련 객체의 제품군을 일관되게 만든다.
  • 팩토리의 가치는 결합도 감소, 생성 로직 중앙화, 테스트 용이성에 있고 성능상 이점을 얻으려는 도구는 아니다.
  • 구현체가 여러 개이거나 생성 절차가 복잡할 때 도입하고, 단일 구현체에는 불필요한 추상화를 피한다.