[디자인 패턴] 2. 팩토리 패턴
지난 글에서는 전략 패턴(Strategy Pattern)을 사용하여 거대한 if-else 비즈니스 로직을 깔끔하게 분리하는 방법을 알아보았습니다. 덕분에 결제 방식이 늘어나도 기존 코드를 건드리지 않고 새로운 클래스를 추가하는 것만으로 확장이 가능해졌습니다.
하지만 코드를 작성하다 보면 뭔가 개운치 않은 뒷맛이 남을 때가 있습니다. 전략 패턴을 적용한 클라이언트 코드를 다시 한번 살펴볼까요?
남겨진 문제점: 객체 생성의 번거로움
전략 패턴을 적용했지만, 정작 그 전략 객체를 '누가', '언제' 생성하느냐 하는 문제는 여전히 남아있습니다. 웹 애플리케이션의 컨트롤러나 서비스 계층 어딘가에는 필연적으로 다음과 같은 코드가 존재하게 됩니다.
// 클라이언트 코드 (Service 또는 Controller)
public void processOrder(String paymentType, int amount) {
PaymentStrategy strategy;
// 여전히 남아있는 분기문
if ("CREDIT_CARD".equals(paymentType)) {
strategy = new CreditCardStrategy();
} else if ("KAKAO_PAY".equals(paymentType)) {
strategy = new KakaoPayStrategy();
} else if ("BANK_TRANSFER".equals(paymentType)) {
strategy = new BankTransferStrategy();
} else {
throw new IllegalArgumentException("지원하지 않는 결제 방식입니다.");
}
PaymentProcessor processor = new PaymentProcessor(strategy);
processor.executePayment(amount);
}
비즈니스 로직은 전략 클래스로 이동했지만, 어떤 전략을 선택해서 생성할지 결정하는 로직은 여전히 클라이언트 코드에 남아있습니다. 클라이언트가 구체적인 클래스(CreditCardStrategy 등)를 직접 의존하고 있는 것도 문제입니다.
이때 필요한 것이 바로 **팩토리 패턴(Factory Pattern)**입니다.
팩토리 패턴으로 생성 책임 위임하기
팩토리 패턴의 핵심 아이디어는 간단합니다. **"객체를 생성하는 일도 누군가의 책임이다"**라는 것입니다. 객체 생성을 전담하는 공장(Factory)을 만들고, 클라이언트는 공장에 "이런 타입의 결제 기능이 필요해"라고 요청만 하면 됩니다.
1. 팩토리 클래스 구현
가장 단순한 형태인 '심플 팩토리(Simple Factory)' 방식으로 구현해 보겠습니다.
public class PaymentStrategyFactory {
// 객체 생성 책임을 팩토리로 집중
public static PaymentStrategy createStrategy(String type) {
switch (type) {
case "CREDIT_CARD":
return new CreditCardStrategy();
case "KAKAO_PAY":
return new KakaoPayStrategy();
case "BANK_TRANSFER":
return new BankTransferStrategy();
default:
throw new IllegalArgumentException(
"지원하지 않는 결제 방식입니다: " + type
);
}
}
}
이제 지저분했던 switch 혹은 if-else 문이 PaymentStrategyFactory 내부로 숨었습니다.
2. 클라이언트 코드의 변화
이제 클라이언트 코드는 더 이상 구체적인 전략 클래스들이 무엇인지 알 필요가 없습니다. 오직 인터페이스(PaymentStrategy)와 팩토리(PaymentStrategyFactory)만 알면 됩니다.
public void processOrder(String paymentType, int amount) {
// 팩토리에게 객체 생성을 위임
PaymentStrategy strategy =
PaymentStrategyFactory.createStrategy(paymentType);
// 실행 책임은 컨텍스트에 위임
PaymentProcessor processor = new PaymentProcessor(strategy);
processor.executePayment(amount);
}
무엇이 개선되었나요?
가장 큰 이점은 결합도(Coupling)의 감소입니다.
이전에는 클라이언트 코드가 CreditCardStrategy, KakaoPayStrategy 등 구체적인 클래스에 직접 의존했습니다. 만약 생성자 파라미터가 변경되거나 초기화 로직이 복잡해지면, 이를 사용하는 모든 클라이언트 코드를 찾아다니며 수정해야 했습니다.
하지만 팩토리 패턴을 적용하면, 객체 생성 로직이 변경되거나 새로운 결제 수단이 추가되더라도 PaymentStrategyFactory 한 곳만 수정하면 됩니다. 클라이언트 코드는 전혀 건드릴 필요가 없습니다. 즉, 객체의 **'사용'**과 **'생성'**을 명확하게 분리한 것입니다.
마치며
디자인 패턴을 공부하다 보면 "결국 코드를 다른 곳으로 옮긴 것뿐 아닌가?"라는 의문이 들 수 있습니다. 맞습니다. 하지만 그 코드를 어디에 배치하느냐가 유지보수성을 결정짓는 핵심입니다.
전략 패턴이 '어떻게(How)' 실행할지를 캡슐화했다면, 팩토리 패턴은 '무엇을(What)' 생성할지를 캡슐화하여 서로의 책임을 나눴습니다.
이제 우리는 결제 로직을 유연하게 교체할 수 있고(전략 패턴), 객체 생성의 복잡함도 숨길 수 있게 되었습니다(팩토리 패턴). 하지만 결제가 완료된 후, 사용자에게 이메일을 보내거나, 재고 시스템에 알림을 보내는 등 후속 처리가 주렁주렁 달린다면 코드는 다시 복잡해질 것입니다.
다음 글에서는 이러한 이벤트 흐름을 효과적으로 관리할 수 있는 **옵저버 패턴(Observer Pattern)**에 대해 알아보겠습니다.