생성 패턴 - 팩토리 메소드 패턴 (Factory Method Pattern)

French Marigold·2024년 5월 2일
1

디자인패턴

목록 보기
8/10

정의

  • 팩토리 메소드 패턴 (Factory Method Pattern)이란 객체 인스턴스를 직접 생성하는 것이 아닌 공장 클래스를 통해서 인스턴스를 간접적으로 받아오는 디자인 패턴이다.
  • 보통 인스턴스를 찍어낼 때, CreditCard(), Paypal(), Crypto() 이런 방식으로 인스턴스를 직접 찍어내는 것이 일반적인데, 팩토리 메소드 패턴에서는 공장 클래스 객체로부터 해당 인스턴스들을 받아오는 형식을 취한다.
  • 그렇다면 왜 굳이 직접 인스턴스를 찍어내지 않고 팩토리 메소드 패턴을 사용하여 인스턴스를 얻어오려는 것일까? 굳이 그렇게 해야 하는 이유가 있나?
    • 직접 객체를 생성할 경우 (CreditCard(), Paypal(), Crypto() 등) 결합도가 크게 증가한다. 하지만 팩토리 메소드 패턴을 사용하면 결합도를 크게 낮출 수 있다. ⭐️⭐️
      • 직접 객체를 생성하면 CreditCard 객체 내의 메소드가 변경될 경우, Client 단의 코드도 변경시켜주어야 한다.
      • 하지만 팩토리 메소드 패턴을 사용하면 “팩토리 객체 내의 메소드의 리턴 타입이 객체 타입이 아닌 프로토콜 타입이기에” Client와의 결합도를 크게 낮출 수 있다.
      • 즉, 문제가 발생한다면 Client 단의 코드를 살펴보는 게 아니라 팩토리 프로토콜 쪽을 살펴보면 되기 때문에 결합도가 크게 줄어든다.

팩토리 메소드 패턴 (Factory Method Pattern) 의 구조

  • Creator ⇒
    • “객체를 생성하기 위해” 만들어지는 추상적인 프로토콜 타입.
    • 공장 객체가 사용할 메소드를 프로토콜 내에서 정의하면 된다.
  • ConcreteCreator ⇒
    • 프로토콜이었던 Creator(추상 공장)을 실제 구체적인 객체로 옮기는 영역.
    • 추후, Client는 객체 인스턴스를 직접 찍어내는 것이 아닌 Concrete Creators를 통해서 간접적으로 인스턴스를 얻어온다.
  • Product ⇒
    • “객체 내부의 로직을 표현”하기 위해 만들어진 추상적인 프로토콜 타입
  • ConcreteProduct
    • 프로토콜이었던 Product(추상화 객체)를 실제 구체적인 객체로 옮기는 영역.

Swif로 팩토리 메소드 패턴 적용 과정 예시

  • 온라인 쇼핑몰이나 결제 서비스를 구현할 때, 사용자에게 다양한 결제 수단(신용카드, 페이팔, 암호화폐 등)을 사용하게끔 한다고 가정하자. 그런데 각 결제 수단마다 “결제 처리 방식이 다를 수 있다.” 예를 들어, 신용 카드는 결제를 위해 공인인증서 화면을 띄워야 한다면, 페이팔 같은 경우 휴대전화로 인증하는 화면을 띄워야 할 수 있다.
  1. 신용카드, 페이팔, 암호화폐 등으로 결제 처리 방법이 다를 경우를 예시로 팩토리 메소드 패턴을 적용해 볼 것이다. 우선 객체 내부의 로직을 표현하기 위한 Product 프로토콜을 생성한다. 즉, 결제 처리 방법을 다루는 메소드를 소유하고 있는 프로토콜을 생성한다.
// Product - **“객체 내부의 로직을 표현”**하기 위해 만들어진 
// 추상적인 프로토콜 타입
protocol PaymentProcess {
    func process()
}
  1. 그 후, Product 프로토콜을 채택한 구체적인 객체들을 생성하여 내부를 구현한다. 즉, 결제 처리 방법을 실제 로직으로 구현하는 영역이라고 보면 된다.
// Concrete Products - 객체 내부의 로직을 실제로 표현하는 영역.
// 즉, 결제 처리 방법을 실제 로직으로 구현하는 영역이라고 보면 된다. 
class CreditCard: PaymentProcess {
    func process() {
				// 신용카드에서 결제를 처리하는 로직을 입력한다.
    }
}

class Paypal: PaymentProcess {
    func process() {
				// 페이팔에서 결제를 처리하는 로직을 입력한다.
    }
}

class Crypto: PaymentProcess {
    func process() {
				// 암호화폐에서 결제를 처리하는 로직을 입력한다.
    }
}
  1. Client에서 바로 CreditCard, Paypal, Crypto 객체의 인스턴스를 직접 생성하는 것이 아닌 좀 더 간접적인 방법으로 인스턴스를 만들어낼 것이다. Client 단에서 특정 객체와의 결합도를 낮추기 위함이라고 앞에서 설명하였다. 우선 인스턴스를 찍어내는 공장 객체를 만들기 위해 Creator 프로토콜을 구현한다. 이 공장 프로토콜 객체는 “Product 프로토콜 타입을 리턴하는 메소드를 가지고 있으며” 이 메소드를 통해서 Product 프로토콜 타입을 만족하는 모든 객체 인스턴스를 생성해낼 수 있다. “직접 인스턴스 타입을 리턴하는 것이 아니므로 Client는 특정 객체와의 의존성이 크게 줄어들게 된다.“
// Creator - 객체(제품)의 인스턴스를 찍어내는 추상 공장.
// 즉, CreditCard, Paypal, Crypto 등의 인스턴스를 
// 직접 찍어내는 것이 아니라 (CreditCard(), Paypal(), Crypto())
// Creator라는 추상 공장의 메소드로 하여금 Product 프로토콜 타입을 리턴하도록 하여 
// Client가 특정 객체와의 의존성이 크게 줄어들게끔 만든다. ⭐️⭐️
// 다시 말해, 인스턴스를 직접 생성하는 것이 아닌 추상 공장을 통해서 간접적으로 인스턴스를 얻어낸다는 것이다.
protocol PaymentFactory {
    func createPaymentProcess() -> PaymentProcess
}
  1. Creator 프로토콜을 채택하여 구체적인 공장 객체 (Concrete Creators)를 만들어낸다. 해당 공장 객체는 인스턴스를 찍어내며 Client는 공장 객체(Concrete Creators)에게 인스턴스를 얻어내는 방식으로 구현이 된다.
// Concrete Creators - 프로토콜이었던 추상 공장을 실제 구체적인 객체로 옮기는 영역.
// 추후, Client는 객체 인스턴스를 직접 찍어내는 것이 아닌
// Concrete Creators를 통해서 간접적으로 인스턴스를 얻어온다.
// 리턴 타입이 Product 프로토콜 타입이므로 Client와 특정 객체와의 의존성이 크게 줄어든다. 
class CreditCardFactory: PaymentFactory {
    func createPaymentProcess() -> PaymentProcess {
        return CreditCard()
    }
}

class PaypalFactory: PaymentFactory {
    func createPaymentProcess() -> PaymentProcess {
        return Paypal()
    }
}

class CryptoFactory: PaymentFactory {
    func createPaymentProcess() -> PaymentProcess {
        return Crypto()
    }
}
  1. Client에서는 Concrete Creators로부터 간접적으로 인스턴스를 얻어와서 나머지 코드를 구현한다.
// Client
class Client {
    // Concrete Creators로부터 간접적으로 인스턴스를 얻어옴 ⭐️
    let creditCardFactory = CreditCardFactory()
    let paypalFactory = PaypalFactory()
    let cryptoFactory = CryptoFactory()
    
    func process(using payment: PaymentFactory) {
        let paymentProcess = payment.createPaymentProcess()
        paymentProcess.process()
    }
}

let client = Client()
client.process(using: client.creditCardFactory) // 신용카드의 결제 방식을 실행한다.

패턴 사용 시기

  • 팩토리 객체가 프로토콜을 리턴하게 함으로서 Client와 사용하는 객체 간의 결합도를 낮추고자 할 때 사용한다.

패턴의 장점

  • OCP ⇒ 기존 코드를 수정하지 않고도 새로운 유형의 Concrete Product (객체 인스턴스)를 계속해서 생성할 수 있으므로 OCP를 만족한다.
  • “객체 생성”에 대한 책임을 Creator & Concrete Creators가 대신 책임지고, ”객체 내부의 로직에 관련된 코드”는 Product & Concrete Product가 책임짐으로써 시스템의 결합도를 낮춘다. ⭐️⭐️

패턴의 단점

  • 코드의 복잡성이 증가한다. 대부분의 디자인 패턴이 가지고 있는 문제점일 듯하다.
  • Concrete Product (실제 객체 인스턴스)가 늘어날 때마다 공장 객체 클래스 또한 늘어날 것이므로 코드 양이 급속도로 늘어나게 된다.

참고 문헌

profile
꽃말 == 반드시 오고야 말 행복

3개의 댓글

comment-user-thumbnail
2024년 5월 3일

제가 이전프로젝트에서 swinject로 container를 사용할지 아니면 factory를 사용할지 고민을 했었는데 라이브러리대신 factory를 사용하는게 좀더 끌려서 팩토리패턴을 사용했었는데 이렇게하니까 MVVM에서 데이터를 전달할떄 조금 까다롭더라고요 물론 이부분에서 swinject도 자유로울순없겠지만 편한대신 데이터전달이 조금 불편하다?(저는 이중 delegate을 썼던기억이나에요)라는 생각이드는 패턴입니다 개인적으로는 ㅎㅎ

답글 달기
comment-user-thumbnail
2024년 5월 3일

Client가 Factory들을 프로퍼티로 가지고 있으면 이미 강하게 결합되어 있는 것으로 볼 수 있지 않나요?

만약 '계좌이체'라는 결제 방식이 생기면 Client 클래스에 해당 팩토리를 프로퍼티로 추가해 줘야 하는 거 아닌가 싶어서요

답글 달기
comment-user-thumbnail
2024년 5월 4일

팩토리패턴! swift로 상세히 풀어주셔서 너무나 재미있게 읽었습니다.
과거에 공장 이라는 표현이 가지는 느낌(객체생성 책임 전가?)에 꽂혀서 해당 패턴의 목적을 반복 생성! 이라 마음대로 풀이하고 혼란스러웠던 적이 있습니다.
예시들어주신 다양한 공장들을 일반적으로 처리하는 코드를 보니 팩토리 패턴의 특성을 편하게 이해할 수 있었던 것 같습니다.

func process(using payment: PaymentFactory) {
        let paymentProcess = payment.createPaymentProcess()
        paymentProcess.process()
    }

PaymentFactory를 통해 공장 객체에 접근하니 실제 구현을 몰라도 된다는 구절의 의미가 잘 와닿았습니다.
새로운 공장이 추가된다 한들 변하지 않는 코드이니 변경이 수정을 요하지 않아 자유롭구요!
^새로운 인스턴스가 필요하다면, 수정하지 않고 추가하면 된다^
는 문장 또한 많은 이해를 도왔습니다. 필요한 상황에 적절히 사용하겠습니다! 감사히 잘 읽었습니다!

답글 달기