Strategy 패턴이란?
Strategy 패턴은 동일한 문제를 해결하는 여러 알고리즘이 있을 때, 이들을 독립된 클래스로 캡슐화하고 런타임에 알고리즘을 교체할 수 있도록 하는 설계 패턴입니다.
이 패턴은 코드 중복을 줄이고 확장성을 높이는 데 매우 유용합니다.
예를 들어, 결제 시스템을 설계한다고 가정해봅시다. 결제 방식은 신용카드, PayPal, 혹은 기타 방식으로 다양할 수 있습니다. 각 결제 방식을 동적으로 변경할 수 있다면 사용자의 요구에 더 잘 대응할 수 있습니다. 하지만 잘못된 설계로 인해 유지보수와 확장성이 떨어질 위험이 있습니다.
다음은 잘못된 설계와 그 문제점을 살펴보고, 이를 Strategy 패턴으로 해결하는 과정을 소개합니다.
이 글의 소스코드 및 설명은 헤드퍼스트 디자인패턴 책을 참조하였습니다.
단순 확장 방식의 문제점
아래는 Payment 클래스를 단순 확장하여 다양한 결제 방식을 구현한 코드입니다. 이 방식은 확장성이 부족하고, 새로운 결제 방식이 추가될 때마다 기존 코드를 수정해야 하는 문제를 야기합니다.
// Base class
class Payment {
public void pay(String type, int amount) {
if (type.equals("CREDIT")) {
System.out.println("Credit Card로 " + amount + "원을 결제합니다.");
} else if (type.equals("PAYPAL")) {
System.out.println("PayPal로 " + amount + "원을 결제합니다.");
} else {
System.out.println("결제 유형이 잘못되었습니다.");
}
}
}
public class Main {
public static void main(String[] args) {
Payment payment = new Payment();
payment.pay("CREDIT", 10000); // 출력: Credit Card로 10000원을 결제합니다.
payment.pay("PAYPAL", 20000); // 출력: PayPal로 20000원을 결제합니다.
}
}
문제점
- 결제 방식 추가 시 수정 필요:
- 새로운 결제 방식을 추가하려면 pay 메서드에 조건문을 추가해야 합니다.
- 이는 기존 코드를 수정하는 일이므로, **개방-폐쇄 원칙(OCP)**에 위배됩니다.
- 유지보수 어려움:
- 결제 방식이 많아질수록 pay 메서드의 조건문이 길어지고 복잡해집니다.
- 이는 코드의 가독성을 떨어뜨리고, 수정 시 오류가 발생할 가능성을 높입니다.
- 재사용성 부족:
- 특정 결제 방식에 대한 로직이 Payment 클래스에 고정되어 있어, 다른 곳에서 재사용하기 어렵습니다.
Strategy 패턴으로 문제 해결
위의 문제를 해결하기 위해 Strategy 패턴을 적용하면 결제 방식을 독립적인 클래스로 분리하여 유연성과 확장성을 높일 수 있습니다.
// Strategy Interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Credit Card로 " + amount + "원을 결제합니다.");
}
}
class PayPalPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("PayPal로 " + amount + "원을 결제합니다.");
}
}
// Context Class
class PaymentContext {
private PaymentStrategy strategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void pay(int amount) {
if (strategy == null) {
throw new IllegalStateException("Payment strategy is not set.");
}
strategy.pay(amount);
}
}
// Main Class
public class Main {
public static void main(String[] args) {
PaymentContext context = new PaymentContext();
// Credit Card 결제
context.setPaymentStrategy(new CreditCardPayment());
context.pay(10000); // 출력: Credit Card로 10000원을 결제합니다.
// PayPal 결제
context.setPaymentStrategy(new PayPalPayment());
context.pay(20000); // 출력: PayPal로 20000원을 결제합니다.
}
}
Strategy 패턴 적용 후 장점
- 확장성 증가:
- 새로운 결제 방식을 추가할 때 기존 코드를 수정하지 않고, 새로운 PaymentStrategy 구현체만 작성하면 됩니다.
- 유지보수 용이:
- 조건문 없이 각 결제 방식이 독립적인 클래스로 분리되어 있어 수정 및 테스트가 간단합니다.
- 유연한 런타임 동작:
- 런타임에 동적으로 결제 방식을 변경할 수 있습니다.
Strategy 패턴의 핵심 원리
- 행동 캡슐화:
- PaymentStrategy 인터페이스는 모든 결제 방식이 따라야 할 공통 계약을 정의합니다.
- 이를 통해 구체적인 구현은 전략 클래스 내부로 캡슐화되고, 외부에서는 인터페이스를 통해 동작을 호출합니다.
- 구성(Composition)을 통한 유연성:
- PaymentContext 클래스는 PaymentStrategy 객체를 구성 요소로 포함합니다.
- 이 구성 요소는 런타임에 동적으로 교체될 수 있으므로, 다양한 전략을 유연하게 적용할 수 있습니다.
- 개방-폐쇄 원칙(OCP) 준수:
- 기존 코드는 변경 없이 새로운 전략(결제 방식)을 쉽게 추가할 수 있습니다.
런타임에서의 변경이란?
"런타임 동적 변경"이란 코드 실행 중 사용자의 선택이나 특정 조건에 따라 전략(결제 방식)을 설정하고 변경할 수 있음을 의미합니다. 이를 통해, 컴파일 시점에 결제 방식을 고정하지 않고 실행 중에 유연하게 결제 방식을 교체할 수 있습니다.
public class Main {
public static void main(String[] args) {
PaymentContext context = new PaymentContext();
// 사용자 입력이나 조건에 따라 전략 선택
boolean userSelectedCreditCard = true;
if (userSelectedCreditCard) {
context.setPaymentStrategy(new CreditCardPayment());
} else {
context.setPaymentStrategy(new PayPalPayment());
}
context.pay(15000); // 선택된 전략에 따라 결제 수행
}
}
이처럼 런타임에 전략을 설정하거나 변경함으로써, 프로그램의 유연성과 확장성이 극대화됩니다.
결론
Strategy 패턴은 다양한 알고리즘을 유연하게 적용하고, 코드의 유지보수성과 확장성을 크게 향상시키는 데 유용합니다. 이를 통해 객체지향 설계의 중요한 원칙을 준수하면서 효율적인 소프트웨어 개발이 가능합니다.