팩토리 패턴
정의
- 객체 생성 책임을 별도의 팩토리(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()
팩토리를 사용하지 않으면 클라이언트 코드에서 구체 클래스에 직접 의존하여 객체를 생성해야 한다. 이 경우 각 클래스의 생성자 시그니처나 생성 과정이 변경되면, 해당 클래스를 생성하는 모든 클라이언트 코드를 함께 수정해야 한다.
위 예시처럼 객체 생성이 한두 곳에만 존재한다면 큰 문제가 되지 않을 수 있다. 그러나 동일한 객체가 수십, 수백 곳에서 생성된다면, 변경 비용은 급격히 증가하고 유지보수 리스크도 커진다.
장단점
장점
-
생성 코드의 중복성 제거
객체 생성 로직을 팩토리 한 곳으로 모음으로써, 각 클라이언트에서 각각new(또는 생성자 호출)를 사용하는 코드를 제거할 수 있다. 이를 통해서 인스턴스 생성 방식이 변경되더라도, 팩토리만 수정하게끔 한정지을 수 있으므로 유지보수 비용이 크게 감소한다. -
복잡한 생성 조건을 캡슐화
객체 생성 과정에 조건 분기, 설정값 해석, 환경별 분기 처리 등이 포함되는 경우, 이를 클라이언트 코드에서 분리해 팩토리 내부에 숨길 수 있다. 따라서, 클라이언트는 “무엇을 생성할지”만 전달하고, “어떻게 생성되는지”는 알 필요가 없다.
단점
-
OCP(Open-Closed Principle) 위반
단순 팩토리는 보통if / elif또는 위 예시 코드와 같은registry기반 분기를 사용한다. 이 경우, 새로운 타입의 객체를 추가하려면 기존 팩토리 코드를 수정해야 한다. 이는 확장에 대해 닫혀 있어야 한다는 OCP 원칙을 위반하는 것이다. -
팩토리 클래스에 책임이 집중됨
생성 대상이 많아질수록 팩토리 클래스가 비대해지고, 변경의 중심점이 된다. 이로 인해 장기적으로는 팩토리 매서드 패턴이나 추상 팩토리 패턴으로 전환이 필요할 수 있다.
Comments