예제 코드로 이해해보는 디자인 패턴 - 팩토리 메서드 패턴(Factory Method Pattern)

kindof

·

2023. 11. 14. 20:48

이번 글에서는 또 다른 디자인 패턴 중 하나인 팩토리 메서드 패턴(Factory Method Pattern)에 대해 정리해보겠습니다.

 

먼저, 간단하게 팩토리 메서드 패턴의 정의와 개념에 대해 소개하고 실제 예제 코드를 보면서 디자인 패턴을 구현하는 방법에 대해 이해해보겠습니다.

 

 

1. 팩토리 메서드 패턴?

팩토리 메서드 패턴은 구체 클래스의 생성 로직(Constructor, Builder 등)을 통한 객체의 생성 대신, 구체 클래스들의 공통 인터페이스를 바탕으로 객체를 생성하는 방식을 말합니다.

 

따라서 팩토리 메서드 패턴을 디자인할 때 전제가 되는 점은 각 구체 클래스들이 공통으로 하는 인터페이스가 존재해야 한다는 것인데요.

 

아래 예제 코드에서도 살펴보겠지만 팩토리 메서드 패턴의 목표는 구체 클래스를 명시하지 않고, 많은 곳에서 객체의 생성을 편리하게 한다는 데 있기 때문입니다. 

 

이러한 필요성이 없는 상황에서 팩토리 메서드 패턴은 실효성이 없을 뿐더러, 오히려 개발자로 하여금 객체의 생성 로직이 어떻게 되어 있는지 파악해야 하는 부담만 안겨주게 됩니다.

 

 

2. 코드로 이해해보기

설명을 위해 간단한 예시 상황을 가정해보겠습니다.

 

먼저, 세 가지 종류의 결제 수단이 있습니다. 

  • 현금
  • 체크카드
  • 신용카드

각 결제 수단은 상품을 결제하는 pay() 메서드와 환불하는 refund() 메서드가 있습니다. 각 결제 수단의 공통 인터페이스인 Payment에는 환불 시 영수증을 증빙하는 메서드를 정의해둡니다.

Java Diagram

 

[Payment.java]

public interface Payment {

    void pay(double cost);

    void refund(double cost, Receipt receipt);

    default void validateReceipt(Receipt receipt) {
        System.out.println("receipt validation!");
    }
}

 

[CashPayement.java]

public class CashPayment implements Payment {

    private final String tx_id;

    public CashPayment(String tx_id) {
        this.tx_id = tx_id;
    }

    @Override
    public void pay(double cost) {
        System.out.println("pay with cash, cost = " + cost);
    }

    @Override
    public void refund(double cost, Receipt receipt) {
        validateReceipt(receipt);
        System.out.println("refund with cash, cost = " + cost);
    }
}

 

[CreditCardPayemnt.java]

public class CreditCardPayment implements Payment{

    private final String tx_id;

    public CreditCardPayment(String tx_id) {
        this.tx_id = tx_id;
    }

    @Override
    public void pay(double cost) {
        System.out.println("pay with credit card, cost = " + cost);
    }

    @Override
    public void refund(double cost, Receipt receipt) {
        validateReceipt(receipt);
        System.out.println("refund with credit card, cost = " + cost);
    }
}

 

[DebitCardPayment.java]

public class DebitCardPayment implements Payment {

    private final String tx_id;

    public DebitCardPayment(String tx_id) {
        this.tx_id = tx_id;
    }

    @Override
    public void pay(double cost) {
        System.out.println("pay with debit card, cost = " + cost);
    }

    @Override
    public void refund(double cost, Receipt receipt) {
        validateReceipt(receipt);
        System.out.println("refund with debit card, cost = " + cost);
    }
}

 

이제, 어떤 매장의 카운터에서 고객이 세 가지 결제 수단 중 하나를 선택해서 상품을 결제한다고 해보겠습니다.

[Counter.java]

public class Counter {

    public void processPayment(PaymentType paymentType, double cost) {

        Payment payment = null;

        if (paymentType.equals(PaymentType.CASH)) {
            payment = new CashPayment("a");
        } else if (paymentType.equals(PaymentType.CREDIT)) {
            payment = new CreditCardPayment("b");
        } else if (paymentType.equals(PaymentType.DEBIT)) {
            payment = new DebitCardPayment("c");
        } else {
            throw new IllegalArgumentException("No matched payment.");
        }

        payment.pay(cost);
    }
}

 

예시로 작성한 코드라 비즈니스 로직의 전개가 완벽히 논리적이지는 않지만, 위 코드 예시에서 전하고자 하는 문제는 하나입니다.

 

"각 결제 수단을 생성할 때, new() 키워드로 구체적인 클래스의 객체를 만들어야 한다."

 

이로 인해, 결제나 환불이 진행되는 코드 혹은 카드사에서 카드를 발급하는 상황 등에서는 반복되는 코드가 나타날 수밖에 없게 됩니다.

 

또한, 위와 같이 하나의 인터페이스 아래 구체 클래스를 정의하게 되는 상황에서는 언제든지 새로운 구체 클래스가 생겨날 수 있고 이에 대한 생성 로직을 또 다시 모든 곳에서 정의해야 합니다.

 

이런 상황에서 팩토리 메서드 패턴은 구체 객체의 생성만을 담당하는 새로운 주체를 설정하는 방식으로 문제를 해결합니다.

팩토리 메서드 패턴 도입

 

위 그림을 보면 Counter에서 결제 수단을 생성하는 책임을 PaymentFactory에 위임합니다.

 

PaymentFactory는 추상 클래스로써 임직원 전용 결제 수단과 방문 고객 전용 결제 수단을 만드는 Factory의 부모가 됩니다. 이를 통해 Payment 자체의 확장 뿐만 아니라, PaymentFactory의 확장이 가능해집니다.

 

방문 고객은 자신이 가지고 있는 카드를 사용하면 되지만, 임직원은 회사에서 발급한 결제 수단을 사용해야 한다면 다른 방식의 구체 클래스 생성이 필요해질 수 있기 때문입니다.

 

위 구현을 코드로 살펴보면 아래와 같습니다.

 

[PaymentFactory.java]

public abstract class PaymentFactory {

    abstract Payment createPayment(PaymentType paymentType);

}

 

[EmployeePaymentFactory.java]

public class EmployeePaymentFactory extends PaymentFactory {

    @Override
    Payment createPayment(PaymentType paymentType) {
        Payment payment = null;

        if (paymentType.equals(PaymentType.CASH)) {
            payment = new CashPayment("EmployeeA");
        } else if (paymentType.equals(PaymentType.CREDIT)) {
            payment = new CreditCardPayment("EmployeeB");
        } else if (paymentType.equals(PaymentType.DEBIT)) {
            payment = new DebitCardPayment("EmployeeC");
        } else {
            throw new IllegalArgumentException("No matched payment.");
        }

        return payment;
    }
}

 

[VisitorPaymentFactory.java]

public class VisitorPaymentFactory extends PaymentFactory {

    @Override
    Payment createPayment(PaymentType paymentType) {
        Payment payment = null;

        if (paymentType.equals(PaymentType.CASH)) {
            payment = new CashPayment("VisitorA");
        } else if (paymentType.equals(PaymentType.CREDIT)) {
            payment = new CreditCardPayment("VisitorB");
        } else if (paymentType.equals(PaymentType.DEBIT)) {
            payment = new DebitCardPayment("VisitorC");
        } else {
            throw new IllegalArgumentException("No matched payment.");
        }

        return payment;
    }
}

 

이제 실제로 Counter의 결제 로직은 아래와 같이 진행됩니다.

[Counter.java]

public class Counter {

    private PaymentFactory paymentFactory;

    public Counter(PaymentFactory paymentFactory) {
        this.paymentFactory = paymentFactory;
    }

    public void processPayment(PaymentType paymentType, double cost) {
        Payment payment = paymentFactory.createPayment(paymentType);
        payment.pay(cost);
    }
}

 

[Main.java]

public class Main {

    public static void main(String[] args) {
        Counter counter = new Counter(new EmployeePaymentFactory());
        counter.processPayment(PaymentType.CASH, 500);
    }
}

 

Counter는 PaymentFactory라는 추상클래스에 의존하게 되며, 실제 어떤 팩토리를 사용할 지는 의존성을 주입하는 방식으로 사용할 수 있게 되었습니다.

 

 

3. 정리

이번 글에서는 간단한 상황 예시와 코드로 팩토리 메서드 패턴에 대해 알아봤습니다.

 

글을 쓰면서도 느꼈지만, 팩토리 메서드 패턴은 여러 형태의 반복되는 객체의 생성의 책임을 다른 클래스에 위임하는 것을 목적으로 합니다.

 

그래서 어쩔 수 없이 생기는 클래스 간의 계층 구조가 존재하게 되며 이를 남용하게 되면 복잡한 구조가 되버릴 수 있습니다. 따라서, 객체의 역할을 설계하면서 팩토리 메서드 패턴을 적극적으로 도입해볼 수 있겠다하는 시점에 기존 구조를 리팩토링하는 방식으로 사용해보는 것이 어떨까 싶습니다.

 

감사합니다.