안녕하세요. 오랜만에 포스팅을 올리는 것 같네요. 이번 포스팅에서는 디자인패턴 중 자주 사용되는 전략(Strategy)패턴에 대해서 포스팅하고자 합니다. 이를 이해하기 위해서는 Dependency Injection , 인터페이스 , 추상화 등에 대한 기본적인 지식이 필요함으로 이 부분은 추가적으로 업로드 하겠습니다.
전략 패턴(strategy pattern) 또는 정책 패턴(policy pattern)은 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다. 전략 패턴은 특정한 계열의 알고리즘들을 정의하고 각 알고리즘을 캡슐화하며 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.
전략은 알고리즘을 사용하는 클라이언트와는 독립적으로 다양하게 만든다. 전략은 유연하고 재사용 가능한 객체 지향 소프트웨어를 어떻게 설계하는지 기술하기 위해 디자인 패턴의 개념을 보급시킨 디자인 패턴(Gamma 등)이라는 영향력 있는 책에 포함된 패턴들 가운데 하나이다. — 위키피디아
전략패턴이란 특정 컨텍스트에서 알고리즘을 별도로 분리하는 설계 방법을 의미합니다. 말이 되게 어렵지 않나요? 다른 말로 정의하자면 특정한 기능을 수행하는데에 있어 다양한 알고리즘이 적용될 수 있는 경우, 이 다양한 알고리즘을 별도로 분리하는 설계 방법을 의미합니다. 쉽게 풀어서 설명해도 똑같이 어려운 것 같네요. 아래의 예제를 보며 이 말을 다시 상기해보도록 합시다.
한 과일 매장은 상황에 따라 다른 가격 할인 정책을 적용하고 있습니다. 제일 먼저 온 손님에게 10%를 할인해주고 마지막 손님은 20% 그리고 신선도가 떨어진 과일에 대해서는 20% 할인을 해주고 있습니다. 이러한 상황을 가정하고 이를 구현하는 코드를 작성해보겠습니다.
public class Calculator {
public double calculate(boolean isFirstGuest, boolean isLastGuest, List<Item> items) {
double sum = 0;
for (Item item : items) {
if (isFirstGuest) {
sum += item.getPrice() * 0.9;
} else if (!item.isFresh()) {
sum += item.getPrice() * 0.8;
} else if (isFirstGuest) {
sum += item.getPrice() * 0.8;
} else {
sum += item.getPrice();
}
}
return sum;
}
}
public class Item {
private final String name;
private final int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public int getPrice() {
return price;
}
public boolean isFresh() {
return true;
}
}
위의 코드에서 알 수 있듯이 특정한 할인정책을 사용하기 위해서는 할인 조건이 충족하는지를 if-else
분기를 타며 해결하고 있습니다. 이러한 코드의 문제는 하나의 메소드에 너무 많은 확인 로직이 추가되고 변경에 유연하지 않다는 단점을 가지고 있습니다. 구체적으로 얘기하자면
그렇다면 전략패턴을 적용하면 위의 문제점을 해결할 수 있을까요? 아래에서 전략패턴을 적용해보겠습니다. 전략패턴이란 특정 컨텍스트에 다양한 알고리즘을 별도로 분리해 관리한다고 하였습니다. 즉 위의 상황에서는 계산이라는 컨텍스트에서 적용되는 다양한 할인 알고리즘을 별도로 관리하는 것입니다.
위에서 말한 할인이라는 알고리즘을 DiscountPolicy
라는 인터페이스를 통해 분리하여 관리하겠습니다.
public interface DiscountPolicy {
double calculateWithDisCountRate(Item item);
}
public class FirstCustomerDiscount implements DiscountPolicy{
@Override
public double calculateWithDisCountRate(Item item) {
return item.getPrice() * 0.9;
}
}
public class LastCustomerDiscount implements DiscountPolicy{
@Override
public double calculateWithDisCountRate(Item item) {
return item.getPrice() * 0.8;
}
}
public class UnFreshFruitDiscount implements DiscountPolicy{
@Override
public double calculateWithDisCountRate(Item item) {
return item.getPrice() * 0.8;
}
}
그리고 이를 기존의 Calculator
클래스에서 생성자를 통해 필요한 하위 타입을 주입받아 사용하겠습니다.
public class Calculator {
private final DiscountPolicy discountPolicy;
public Calculator(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public double calculate(List<Item> items) {
double sum = 0;
for (Item item : items) {
sum += discountPolicy.calculateWithDisCountRate(item);
}
return sum;
}
}
이렇게 되는 경우 외부에서 특정 경우(첫번째 손님, 마지막 손님, 싱싱하지 않은 과일)에 대한 할인정책을 생성자를 통해 전달해줄 수 있습니다. 아래의 경우는 첫번째 손님 할인정책을 적용하는 코드입니다. 사전 지식으로 일반적으로 Controller는 사용자의 요청(클릭이나 입력) 등을 매핑하여 받아오기 때문에 특정 알고리즘(첫번째 손님 계산)을 눌렀다는 것을 알 수 있습니다. 요청에 맞는 객체를 Calculator에 주입해주는 방식을 통해 전략패턴을 구현한 것입니다.
public class FruitController {
public static void main(String[] args) {
Calculator calculator = new Calculator(new FirstCustomerDiscount());
calculator.calculate(Arrays.asList(
new Item("Apple", 3000),
new Item("Banana", 3000),
new Item("Orange", 2000),
new Item("Pitch", 4000)
));
}
}
전략패턴을 적용하기전과 후의 클래스의 차이점이 전략패턴의 장점이 될 것입니다. 전략패턴을 적용할때의 이점은 컨텍스트 코드의 변경 없이 새로운 전략을 추가할 수 있다는 점입니다. 예를 들어 새로운 전략인 중간 손님을 대폭할인하는 정책이 추가되었다고 가정해보겠습니다. 그러면 아래와 같이 변할 것입니다.
즉 요구사항이 변경되었을 때 기존의 코드를 변경하지 않아도 된다는 것이 전략패턴의 장점이며 새로운 전략에 대해서는 새로운 클래스를 통해 관리하기 때문에 OCP의 원칙을 준수할 수 있는 패턴입니다.
단순히 전략패턴이 장점만 가지고 있는 것은 아닙니다. 위 클래스에서 어떤 클래스가 보기 편하신가요? 물론 디자인 패턴에 익숙하신 분이라면 후자가 편할 수 있지만 일반적으로 짧은 코드에서는 전자가 보기 편할 수 있습니다. 즉 모든 상황에서 전략패턴이 사용되는 것은 유용하지 않습니다. 컨텍스트에 적용되는 알고리즘이 하나이거나 두개인 경우는 분기를 타는 것이 편한 경우도 있습니다. 그러나 요구사항의 변경으로 변경될 여지가 있고 변화의 형태가 다양함이 어느정도 보장될 때 전략패턴을 고려해보시길 추천드립니다.
전략패턴은 하나의 추상화기법이기 때문에 단순한 경우에도 전략패턴을 사용해야하는 경우도 있습니다. 예를들어 랜덤한 로직을 테스트한다거나 Mock 객체를 생성하여 Controller를 테스트하는 경우 등 다양한 경우에서 유용하게 사용됩니다. 저는 전략패턴이란 위와같은 것이라는 것을 설명드리고자 한 것입니다. 디자인 패턴의 본질은 추상화를 통한 변경에 유연한 대처를 만드는 것이 본질적인 목표이기에 추상화를 통해 얻을 수 있는 이점과 추상화가 필요한 상황들에 대해서는 추가적으로 공부하시기를 권장드립니다. 그러면 조금 더 상황에 맞게 적절하게 추상화된 패턴, 혹은 구현체만 가진 형태를 선택하여 사용하실 수 있을 거라 생각합니다.
잘읽고 갑니다~ 이해가 잘되네요 +ㅅ+