컴퓨터공학

팩토리 vs 추상 팩토리 패턴

nyambu 2025. 5. 22. 19:00

팩토리 vs 추상 팩토리 패턴
팩토리 vs 추상 팩토리 패턴

1. 객체 생성의 유연성을 위한 패턴들

1-1. 생성 패턴이 중요한 이유

 객체 지향 프로그래밍에서 가장 먼저 부딪히는 설계 과제가 바로 객체 생성이다. 언뜻 보면 단순한 new 연산자만으로 충분해 보이지만, 규모가 커질수록 이 단순한 생성 방식이 구조를 경직시키고 테스트를 어렵게 만든다. 예를 들어, 어떤 객체를 생성하려면 내부 구성 객체들도 같이 생성해야 하고, 그에 따른 환경 설정도 초기화해야 한다면, 생성자가 너무 복잡해지고 객체 간의 의존성이 코드 깊숙이 박히게 된다. 이런 상황에서 등장하는 것이 팩토리 패턴이다. 객체 생성 자체를 캡슐화하고, 클라이언트는 객체를 사용하는 데만 집중할 수 있도록 설계한다. 팩토리 패턴은 특히 확장 가능한 구조, 느슨한 결합, 의존성 주입의 유연성이라는 측면에서 필수적인 설계 전략이다.

1-2. 팩토리 메서드와 추상 팩토리의 기본 개념

 팩토리 메서드 패턴(Factory Method)은 상속을 기반으로 하는 구조로, 상위 클래스에서 객체 생성 인터페이스를 정의하고, 실제 어떤 인스턴스를 만들지는 하위 클래스가 결정한다. 반면, 추상 팩토리(Abstract Factory)는 관련된 객체들의 집합을 생성하기 위한 인터페이스를 정의하고, 구현 클래스에서 관련 객체들을 통일성 있게 생성한다. 두 패턴은 공통적으로 객체 생성을 캡슐화하지만, 팩토리 메서드는 단일 객체 생성에, 추상 팩토리는 연관된 여러 객체 생성을 다룬다는 차이점이 있다.

1-3. 생성 패턴이 설계의 유연성을 만든다

 팩토리 계열 패턴은 생성과 관련된 책임을 클라이언트에서 제거함으로써, 객체 생성 전략을 쉽게 교체할 수 있도록 해준다. 이는 테스트 코드 작성, 구성 변경, A/B 테스트, 기능 토글, 다국어 지원 등 수많은 실무 시나리오에서 유리하게 작용한다. 예컨대 결제 수단이 확장되거나, OS 종류에 따라 다른 UI 컴포넌트를 생성해야 할 때, 팩토리 기반 구조는 변화에 강한 설계 구조를 만든다.

1-4. 실제 적용이 많은 이유

 팩토리 패턴은 단순한 이론 패턴이 아니라 실제 프레임워크 내부에서도 가장 많이 사용되는 패턴 중 하나다. 스프링의 ApplicationContext는 Bean 팩토리의 대표적인 예고, React의 JSX 내부에서도 컴포넌트 팩토리 방식이 활용된다. 즉, 팩토리는 구조 설계뿐 아니라 프레임워크 확장성과 DI 컨테이너 구성의 핵심 축이다.


2. 팩토리 메서드 패턴: 다형성 기반 생성 위임

2-1. 기본 구조와 의도

 팩토리 메서드(Factory Method) 패턴은 객체 생성 코드를 클라이언트로부터 분리하고, 하위 클래스가 어떤 객체를 만들지를 결정하도록 위임하는 방식이다. 이 방식은 OOP의 핵심 개념인 다형성(polymorphism)을 활용해 객체 생성 과정을 유연하게 만들어준다. 예를 들어 다음과 같은 상황을 생각해보자.

  • 웹 어플리케이션에서 사용자 로그인 수단이 카카오, 구글, 페이스북 등 다양해졌다고 가정한다.
  • LoginService가 어떤 로그인 전략을 사용할지는 실행 환경에 따라 달라진다.

 이때 팩토리 메서드는 다음과 같은 추상화를 제공한다.

abstract class LoginService {
    abstract AuthProvider createProvider();

    public void login() {
        AuthProvider provider = createProvider();
        provider.authenticate();
    }
}

 이렇게 하면 LoginService를 사용하는 클라이언트는 구체적인 인증 방식에 대해 몰라도 되고, 확장이 필요할 때는 새로운 하위 클래스를 하나 추가하면 된다. 클래스의 개방-폐쇄 원칙(OCP)을 만족시키는 구조가 되는 것이다.

2-2. 실전 사례: 결제 처리 구조

 다양한 결제 수단(Card, KakaoPay, PayPal 등)을 처리할 때 팩토리 메서드는 다음과 같이 쓰일 수 있다.

abstract class PaymentService {
    abstract PaymentGateway createGateway();
    public void pay(Order order) {
        PaymentGateway gateway = createGateway();
        gateway.process(order);
    }
}

 이제 KakaoPayService, CardPayService는 각자의 구현체에서 createGateway()를 통해 생성 전략을 바꿔주면 된다. 이 방식은 특히 결제 처리 로직이 복잡하고 수시로 변경되며 정책이 자주 바뀌는 환경에서 매우 유용하다.

2-3. 장점과 실무 효과

  • 전략별 클래스 분리로 기능 변경이 코드 전체에 영향을 미치지 않는다.
  • 생성 책임을 하위 클래스에 위임함으로써, OCP를 만족하면서 클라이언트 코드를 단순하게 유지할 수 있다.
  • 테스트 시에는 테스트 전용 팩토리를 만들어 FakeGateway나 MockGateway를 리턴하도록 할 수 있어, 테스트의 유연성도 높아진다.

 예를 들어 테스트 코드에서 다음과 같은 방식으로 결제 로직을 검증할 수 있다.

class TestPaymentService extends PaymentService {
    protected PaymentGateway createGateway() {
        return new MockPaymentGateway(); // 로깅 전용 객체
    }
}

 이는 팩토리 메서드가 생성 대상의 유연성과 테스트 가능성을 동시에 보장해주는 구조라는 것을 보여준다.

2-4. 한계와 주의할 점

  • 클래스를 상속해야 하므로, 상속 계층이 늘어나며 클래스 수가 많아질 수 있다.
  • 하위 클래스가 많아지면 코드 베이스가 복잡해지고, 유지보수 비용이 증가할 수 있다.
  • 하나의 객체가 아니라 객체군 전체를 생성해야 하는 경우에는 Abstract Factory로의 전환이 필요하다.

 또한, 팩토리 메서드를 너무 작은 단위에서 남발하면 오히려 불필요한 추상화로 인해 의미 없는 클래스만 잔뜩 생겨날 위험이 있다. 항상 “생성 책임이 정말 바뀔 가능성이 있는가?”를 기준으로 판단해야 한다.


3. 추상 팩토리 패턴: 일관된 객체군 생성

3-1. 의도와 구조 – 하나의 테마를 구성하는 생성자

 추상 팩토리(Abstract Factory)는 “관련 있는 객체들을 함께 생성해야 할 때” 사용하는 패턴이다. 예를 들어 다국어 지원 시스템에서 언어에 따라 버튼, 메뉴 이름, 알림 메시지 등의 UI 요소가 모두 달라져야 한다면, 이들을 하나의 팩토리 단위로 묶어 일괄 생성하는 구조가 이상적이다.

interface NotificationUIFactory {
    Button createButton();
    Toast createToast();
    Alert createAlert();
}

 이 구조는 서로 관련 있는 UI 구성 요소들을 테마, 언어, 플랫폼 등 기준에 따라 묶어서 생성할 수 있도록 한다. 즉, 어떤 객체가 아니라 어떤 컨텍스트에서의 객체군을 생성하는 것이 핵심이다.

3-2. 실제 사례: 운영 환경별 구성 분리

 실무에서는 Dev, Staging, Prod 환경에서 사용할 설정 객체들이 다를 수 있다. 예를 들어, 파일 스토리지 경로, API 게이트웨이 주소, DB 커넥션 정보 등을 모두 분리해야 할 때, 추상 팩토리를 다음과 같이 구성할 수 있다.

interface ConfigFactory {
    FileStorage createStorage();
    MessageQueue createQueue();
    MonitoringService createMonitoring();
}

 이렇게 하면 환경이 바뀔 때는 DevConfigFactory, ProdConfigFactory만 바꿔주면 되고, 나머지 비즈니스 로직은 전혀 수정할 필요가 없다. OCP와 DI의 결합 예시로 매우 이상적인 구조를 만들 수 있다.

3-3. 장점 – 통일성 있는 설계의 핵심

  • 객체군 간의 일관성 유지가 가능하다.
  • 환경별, 플랫폼별 설정을 한 번에 바꿀 수 있어 유지보수가 쉬워진다.
  • 기능을 나누는 것이 아니라 컨텍스트 기반의 구성요소를 묶어내는 데에 매우 효과적이다.

 프론트엔드 개발에서도 Dark Mode/Light Mode UI 구성 시 ThemeFactory로 버튼, 배경, 폰트 등을 세트로 생성하면 전체 테마 전환이 코드 몇 줄로 가능해진다.

3-4. 단점 – 과도한 구조의 위험성

  • 새로운 구성 요소(Button, Toast 등)를 추가하면 모든 팩토리에 메서드를 추가해야 하므로 확장성이 낮아질 수 있다.
  • 객체 간 결합이 강한 구조에서는 적합하지만, 독립적인 객체 생성에는 오히려 팩토리 메서드보다 불편하다.

 즉, 변화가 세트로 발생할 가능성이 높고, 그 변화가 동일한 규칙에 의해 좌우될 때만 적용하는 것이 좋다. 추상 팩토리를 남용하면 설계가 경직되고, 실제로는 하나의 객체만 바꾸면 되는 경우에도 전체 팩토리를 수정해야 하는 상황이 발생할 수 있다.


4. 팩토리 vs 추상 팩토리: 언제 어떤 것을 선택할까?

4-1. 목적에 따른 선택 기준

 팩토리 메서드와 추상 팩토리는 모두 객체 생성 책임을 캡슐화하는 공통점을 가지고 있다. 하지만 이 둘은 적용 범위, 생성 대상, 복잡도에서 명확한 차이를 갖는다.

  • 팩토리 메서드는 “하나의 객체를 생성”하는 데 초점이 있다.
    특히, 동작이나 전략이 상황에 따라 바뀌는 경우(예: CardPay vs KakaoPay)에 유리하다.
  • 추상 팩토리는 “서로 관련 있는 객체들의 집합”을 통째로 생성해야 할 때 유용하다.
    UI 컴포넌트, 설정 모듈, 인증 정보 등 여러 객체가 세트로 바뀌는 경우에 적합하다.

 정리하자면 이렇게 비교할 수 있다.

비교 항목 팩토리 메서드 추상 팩토리
생성 대상 단일 객체 객체 집합(세트)
사용 목적 전략/정책 분리 일관성 있는 환경 구성
구조 복잡도 낮음 높음
OCP 만족도 좋음 매우 좋음
사용 예 결제 수단, 로그인 방식 테마, 환경별 설정, UI 구성
 

4-2. 실무 기준: 변경 단위가 “개별”이냐 “세트”냐

 팩토리 패턴 선택 시 가장 중요한 기준은 변경 단위가 무엇이냐이다.

  • 하나의 객체가 독립적으로 바뀐다면 팩토리 메서드가 적절하다.
  • 여러 객체가 세트로 함께 바뀐다면 추상 팩토리가 맞다.

 예를 들어, 배송 정책이 지역마다 달라지는 경우 지역별 배송 객체만 바꾸면 되므로 팩토리 메서드로 충분하다. 하지만 다국어 설정에 따라 날짜 포맷, 통화 기호, UI 컴포넌트까지 함께 바뀌어야 한다면 추상 팩토리 구조가 필요하다.

4-3. 함께 쓰는 경우도 많다

 실무에서는 이 두 패턴을 함께 사용하는 경우도 많다. 예를 들어 추상 팩토리 안에서 각 객체를 생성할 때 다시 팩토리 메서드를 사용하는 식이다. 이런 방식은 복잡한 시스템에서 생성 책임을 더 세분화하여 관리하고, 객체 간 의존성도 효과적으로 줄일 수 있게 해준다.

class WebThemeFactory implements UIThemeFactory {
    public Button createButton() {
        return ButtonFactory.create("flat");
    }
}

 이 구조는 “큰 틀은 추상 팩토리, 개별 객체는 팩토리 메서드로 생성”이라는 전형적인 구조 조합이다.

4-4. 팀/프로젝트 레벨에서의 판단 기준

 패턴은 반드시 현실에 맞게 유연하게 적용해야 한다.

  • 작은 규모의 서비스에 추상 팩토리를 도입하면 오히려 구조가 복잡해진다.
  • 전략이 명확한 경우에는 팩토리 메서드가 가볍고 효과적이다.
  • 다국어, 멀티 플랫폼, A/B 테스트 등 복잡성이 높은 서비스에서는 추상 팩토리의 확장성이 필요하다.

 또한 개발자 간 공유가 쉬운 구조로 유지되어야 하므로, 팀의 이해 수준과 코드 리딩 속도도 반드시 고려해야 한다.


5. 실전 프로젝트에서의 팩토리 패턴 활용 사례

5-1. 도메인별 결제 시스템 확장

 상황: 한 쇼핑몰에서는 기존에 카드 결제만 제공했지만, 점차적으로 KakaoPay, NaverPay, 간편 계좌 이체, 법인 후불 정산  등 다양한 수단이 추가되었다.

 적용: 초기엔 if-else 분기문으로 paymentMethod에 따라 분기했지만, 결제 정책이 자주 바뀌고 수단도 늘어나면서 팩토리 메서드 패턴으로 전환.

  • PaymentProcessorFactory를 도입하여 각 결제 방식마다 PaymentStrategy 구현체를 반환
  • 주문 서비스는 전략 객체만 받아서 실행, 생성 로직은 전혀 알지 못함

 효과:

  • 신규 결제 수단 추가 시 기존 로직 변경 없음
  • 테스트 시 MockPaymentProcessor로 간편 테스트 가능
  • A/B 테스트도 쉽게 구현 가능 (ex. 무이자 프로모션)

5-2. 마이크로서비스에서 환경별 설정 분리

 상황: 운영 환경에 따라 DB 정보, 캐시 타입(Redis/Memcached), 메시지 브로커(RabbitMQ/Kafka) 구성이 달라져야 하는 MSA 구조의 백엔드.

 적용:

  • 각 환경별 ConfigFactory를 정의 (DevConfigFactory, ProdConfigFactory 등)
  • DI 컨테이너가 실행 시 해당 팩토리를 주입해줌
  • 내부 서비스는 ConfigFactory를 통해 구성 객체들을 일관되게 생성

 효과:

  • 설정 변경이 일관되게 반영됨
  • 환경별 테스트와 운영 환경 배포 시 분기 로직이 전혀 없음
  • 유지보수가 명확하고 안전

5-3. 프론트엔드 테마 구성 시스템

 상황:

  • 다크 모드/라이트 모드를 지원해야 하는 디자인 시스템.
  • 모든 버튼, 카드, 폰트 색상이 테마별로 다르며, 사용자 설정이나 시스템 설정에 따라 동적으로 전환됨.

  적용:

  • ThemeComponentFactory를 정의해서 각 테마에 맞는 UI 컴포넌트를 제공
  • DarkThemeFactory, LightThemeFactory로 나누고, 전체 UI는 팩토리를 통해 생성
  • 팩토리 내부에서도 세부 구성은 팩토리 메서드로 구성 (ex. createButton())

 효과:

  • 테마 전환을 위한 복잡한 if-else 제거
  • 유지보수성 향상, 새로운 테마 추가 시 구조적 간섭 없음
  • 사용자 설정 기반 개인화 구현 용이

5-4. 테스트용 객체 구성 자동화

 상황: CI/CD 환경에서 E2E 테스트와 통합 테스트에 사용하는 Mock 서버, 시뮬레이터, 가짜 API들이 복잡하게 얽혀 있음.

 적용:

  • TestObjectFactory를 통해 테스트 전용 객체군을 한 번에 생성
  • FakePaymentProcessor, DummyLogger, StubUserRepository 등 세트로 생성
  • 테스트 환경 분리에 따라 팩토리만 바꾸면 모든 테스트 대상이 전환됨

 효과:

  • 테스트 격리와 확장에 매우 유리
  • 새로운 테스트 시나리오 작성 비용 감소
  • QA 자동화에 적합한 구조 확보

'컴퓨터공학' 카테고리의 다른 글

전략, 옵저버, 커맨드 패턴  (0) 2025.05.23
싱글턴과 DI 컨테이너 구조  (0) 2025.05.23
디자인 패턴 총정리  (0) 2025.05.22
SRP vs OCP, 실제 적용 사례  (0) 2025.05.22
SOLID 원칙 완전정복  (0) 2025.05.21