팩토리 패턴

정의

  • 객체 생성 책임을 별도의 팩토리(Factory) 객체에 위임하는 패턴

목적

  • 생성 코드를 한곳에서 관리하기 위함
  • 인스턴스를 생성하는 곳(클라이언트)에서 구체 클래스(new XXX())를 몰라도 된다.
  • 코드 변경에 강한 구조

어떨 때 사용하나?

  • 동일한 역할을 하는(=같은 계열의) 여러 객체가 있고, 이를 쉽게 선택하거나 교체할 수 있어야 할 때
  • 새로운 타입의 추가(확장)이 예상되는 경우
  • 객체 생성과 사용을 명확하게 분리하고 싶을 때
  • 객체 생성 방식이 복잡하거나, 조건문이 많을 때

팩토리 패턴의 종류

번호 종류 설명
1 단순 팩토리
Simple Factory
단순히 객체 생성 전담 메서드를 한 곳에 모아두는 방식.
GoF디자인 패턴이 아닌, 코딩 관례
2 팩토리 메서드 패턴
Factory Method Pattern
하위 클래스가 생성 방식을 오버라이드 하는 구조
추후 별도 포스팅에서 살펴볼 예정
3 추상 팩토리 패턴
Abstract Factory Pattern
연관된 객체 집합(패밀리)을 한 번에 생성하는 패턴
추후 별도 포스팅에서 살펴볼 예정

단순 팩토리 패턴

  • 이번 포스팅에서는 단순 팩토리 패턴만 알아보도록 한다.
  • 나머지 두 패턴은 추후 별도 포스팅에서 살펴본다.

정의

  • 객체 생성을 전담하는 클래스/메서드(팩토리) 두는 방식
  • 객체의 생성을 팩토리에서 전담하므로 책임이 분리된다.
  • GoF디자인 패턴이 아닌, 코딩 관례
  • 즉, 단순 팩토리 패턴은 디자인 패턴으로 보지 않으며, 객체지향 프로그래밍에서 자주 사용되는 기법의 느낌이 강하다.
  • 나머지 두 가지 팩토리 패턴(팩토리 메서드, 추상 팩토리)의 베이스 역할을 하니, 기본 개념으로 알아두자.

코드

1
2
3
4
5
6
7
8
9
from enum import Enum
from abc import ABC, abstractmethod

# 결제 방법 종류
class PaymentMethod(str, Enum):
    CREDIT_CART = "credit_card"
    BANK_TRANSFER = "bank_transfer"
    NAVER_PAY = "naver_pay"
    KAKAO_PAY = "kakao_pay"

이번 코드에서는 여러가지 결제 방법을 예시로 들었다. 신용카드 결제, 계좌이체, 네이버페이, 카카오페이 총 4가지 결제 방법이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 결제 방법 인터페이스
class Payment(ABC):
    @abstractmethod
    def pay(self, amount:int) -> str:
        pass

# 결제 방법 구현체들  
class CreditCardPayment(Payment):
    def pay(self, amount:int) -> str:
        return f"신용카드로 {amount}원을 결제했습니다."

class BankTransferPayment(Payment):
    def pay(self, amount:int) -> str:
        return f"계좌이체로 {amount}원을 결제했습니다."

class NaverPayPayment(Payment):
    def pay(self, amount:int) -> str:
        return f"네이버페이로 {amount}원을 결제했습니다."

class KakaoPayPayment(Payment):
    def pay(self, amount:int) -> str:
        return f"카카오페이로 {amount}원을 결제했습니다."

위는 결제 방법들의 공통 인터페이스와 각 구현체들이다.

1
2
3
4
5
6
7
8
9
10
11
12
# 단순 팩토리  
class PaymentFactory:
    REGISTRY = {
        PaymentMethod.CREDIT_CART : CreditCardPayment,
        PaymentMethod.BANK_TRANSFER : BankTransferPayment,
        PaymentMethod.NAVER_PAY : NaverPayPayment,
        PaymentMethod.KAKAO_PAY : KakaoPayPayment
    }

    @classmethod
    def create(cls, payment_method:PaymentMethod) -> Payment:
        return cls.REGISTRY[payment_method]()

위 코드는 단순 팩토리이다. payment_method 값에 따라 미리 지정된 결제방식 인스턴스를 반환하는 구조이다.

1
2
3
payment = PaymentFactory.create("credit_card")
print(payment.pay(300000))
# >> 신용카드로 300000원을 결제했습니다.

구조 설명

  • 객체 생성을 전담하는 별도의 클래스/메서드를 둔다.
  • 위 코드에서는 PaymentFactory 가 객체 생성을 전담하는 클래스이며, create 메서드가 실제로 객체를 생성해 반환하는 메서드이다.

팩토리를 사용하지 않는 경우

1
2
payment_c = CreditCardPayment()
payment_b = BankTransferPayment()

팩토리를 사용하지 않으면 클라이언트 코드에서 구체 클래스에 직접 의존하여 객체를 생성해야 한다. 이 경우 각 클래스의 생성자 시그니처나 생성 과정이 변경되면, 해당 클래스를 생성하는 모든 클라이언트 코드를 함께 수정해야 한다.

위 예시처럼 객체 생성이 한두 곳에만 존재한다면 큰 문제가 되지 않을 수 있다. 그러나 동일한 객체가 수십, 수백 곳에서 생성된다면, 변경 비용은 급격히 증가하고 유지보수 리스크도 커진다.

장단점

장점

  1. 생성 코드의 중복성 제거
    객체 생성 로직을 팩토리 한 곳으로 모음으로써, 각 클라이언트에서 각각 new(또는 생성자 호출)를 사용하는 코드를 제거할 수 있다. 이를 통해서 인스턴스 생성 방식이 변경되더라도, 팩토리만 수정하게끔 한정지을 수 있으므로 유지보수 비용이 크게 감소한다.

  2. 복잡한 생성 조건을 캡슐화
    객체 생성 과정에 조건 분기, 설정값 해석, 환경별 분기 처리 등이 포함되는 경우, 이를 클라이언트 코드에서 분리해 팩토리 내부에 숨길 수 있다. 따라서, 클라이언트는 “무엇을 생성할지”만 전달하고, “어떻게 생성되는지”는 알 필요가 없다.

단점

  1. OCP(Open-Closed Principle) 위반
    단순 팩토리는 보통 if / elif 또는 위 예시 코드와 같은 registry기반 분기를 사용한다. 이 경우, 새로운 타입의 객체를 추가하려면 기존 팩토리 코드를 수정해야 한다. 이는 확장에 대해 닫혀 있어야 한다는 OCP 원칙을 위반하는 것이다.

  2. 팩토리 클래스에 책임이 집중됨
    생성 대상이 많아질수록 팩토리 클래스가 비대해지고, 변경의 중심점이 된다. 이로 인해 장기적으로는 팩토리 매서드 패턴이나 추상 팩토리 패턴으로 전환이 필요할 수 있다.

Reference

https://bcp0109.tistory.com/366

Comments