[디자인 패턴] 1. 전략 패턴
개발을 하다 보면 비즈니스 로직이 복잡해짐에 따라 끝도 없이 늘어나는 if-else 문이나 switch 문을 마주하게 됩니다. 새로운 요구사항이 생길 때마다 기존 코드를 열어 분기문을 추가하고 있다면, 그리고 그 과정에서 혹시나 기존 로직에 버그가 생기지 않을까 걱정하고 있다면, 이제는 디자인 패턴을 적용해 볼 때입니다.
오늘은 실무에서 가장 빈번하게 사용되면서도 즉각적인 리팩토링 효과를 볼 수 있는 **전략 패턴(Strategy Pattern)**에 대해 이야기해보려 합니다.
왜 if-else가 문제일까?
간단한 예제로 시작해보겠습니다. 쇼핑몰의 결제 시스템을 개발한다고 가정해봅시다. 처음에는 신용카드 결제만 있었지만, 카카오페이와 무통장 입금이 추가되었습니다. 보통 우리는 이렇게 코드를 작성하곤 합니다.
public class PaymentProcessor {
public void pay(String type, int amount) {
if ("CREDIT_CARD".equals(type)) {
System.out.println(amount + "원을 신용카드로 결제합니다.");
// 신용카드 결제 로직
} else if ("KAKAO_PAY".equals(type)) {
System.out.println(amount + "원을 카카오페이로 결제합니다.");
// 카카오페이 결제 로직
} else if ("BANK_TRANSFER".equals(type)) {
System.out.println(amount + "원을 무통장 입금으로 처리합니다.");
// 무통장 입금 로직
} else {
throw new IllegalArgumentException("지원하지 않는 결제 방식입니다.");
}
}
}
이 코드는 당장은 잘 동작합니다. 하지만 새로운 결제 수단(예: 네이버페이, 페이팔 등)이 추가될 때마다 우리는 PaymentProcessor 클래스를 수정해야 합니다. 이미 잘 돌아가고 있는 코드를 건드리는 것은 언제나 위험 부담이 따릅니다. 이는 객체지향 설계 원칙 중 하나인 **OCP(개방-폐쇄 원칙)**를 위반하는 전형적인 사례입니다.
전략 패턴으로 구조 개선하기
전략 패턴의 핵심은 변하는 것과 변하지 않는 것을 분리하는 것입니다. 여기서 변하는 것은 '결제 방식에 따른 구체적인 로직'이고, 변하지 않는 것은 '결제를 수행한다'는 행위 그 자체입니다.
이 문제를 해결하기 위해 공통 인터페이스를 정의하고, 각 결제 방식을 별도의 클래스로 분리해보겠습니다.
1. 전략 인터페이스 정의
먼저 모든 결제 방식이 따라야 할 공통 규약을 만듭니다.
public interface PaymentStrategy {
void pay(int amount);
}
2. 구체적인 전략 클래스 구현
이제 각 결제 방식을 독립적인 클래스로 구현합니다.
public class CreditCardStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println(amount + "원을 신용카드로 결제합니다.");
// 신용카드 API 호출 로직
}
}
public class KakaoPayStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println(amount + "원을 카카오페이로 결제합니다.");
// 카카오페이 API 호출 로직
}
}
public class BankTransferStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println(amount + "원을 무통장 입금으로 처리합니다.");
// 계좌 이체 로직
}
}
3. 컨텍스트(Context) 수정
이제 PaymentProcessor는 더 이상 구체적인 결제 방식을 알 필요가 없습니다. 단지 주입받은 전략을 실행하기만 하면 됩니다.
public class PaymentProcessor {
private PaymentStrategy strategy;
public PaymentProcessor(PaymentStrategy strategy) {
this.strategy = strategy;
}
// 런타임에 전략 변경 가능
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void executePayment(int amount) {
strategy.pay(amount);
}
}
사용 예시
이제 클라이언트 코드는 다음과 같이 변합니다.
public class PaymentClient {
public static void main(String[] args) {
// 신용카드 결제
PaymentProcessor processor =
new PaymentProcessor(new CreditCardStrategy());
processor.executePayment(10_000);
// 런타임에 결제 수단 변경 (카카오페이)
processor.setStrategy(new KakaoPayStrategy());
processor.executePayment(25_000);
}
}
무엇이 좋아졌을까요?
가장 큰 변화는 확장성입니다. 만약 '네이버페이'가 추가된다면, 기존의 PaymentProcessor 코드를 단 한 줄도 수정할 필요가 없습니다. 그저 PaymentStrategy를 구현한 NaverPayStrategy 클래스를 새로 만들어서 주입해주기만 하면 됩니다.
또한, 각 결제 로직이 서로 다른 클래스로 격리되어 있기 때문에 코드를 읽기도 편하고, 특정 결제 방식에 대한 단위 테스트를 작성하기도 훨씬 수월해졌습니다. 거대했던 하나의 클래스가 여러 개의 작은 클래스로 나뉘어 역할과 책임이 명확해진 것입니다.
마치며
전략 패턴은 단순히 if-else를 없애는 기술이 아닙니다. 핵심 비즈니스 로직을 보호하고, 변경에 유연하게 대처할 수 있는 구조를 만드는 설계 방식입니다.
물론 모든 분기문에 전략 패턴을 적용할 필요는 없습니다. 로직이 단순하고 변경 가능성이 거의 없다면 if 문이 더 직관적일 수 있습니다. 하지만 분기문이 계속해서 늘어날 조짐이 보인다면, 오늘 다룬 내용을 떠올려 보시기 바랍니다.
다음 글에서는 이렇게 분리된 전략 객체들을 상황에 맞게 효율적으로 생성하고 관리할 수 있도록 도와주는 **팩토리 패턴(Factory Pattern)**에 대해 알아보겠습니다.