전략(Strategy) 패턴
‘개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴' 책을 보고 정리한 글입니다.
한 과일 매장은 상황에 따라 다른 가격 할인 정책을 적용하고 있다.
매장을 열자마자 들어온 첫 손님을 위한 ‘첫 손님 할인' 정책과
저녁 시간대에 신선도가 떨어진 과일에 대한 ‘덜 신선한 과일 할인' 정책이 있다면,
다음 코드처럼 가격을 계산하는 모듈에 이런 가격 할인 정책을 적용하기 위한 if-else 블록이 포함될 것이다.
import java.util.List;
public class Calculator {
public int calculate(boolean firstGuest, List<Item> items) {
int sum = 0;
for (Item item : items) {
if(firstGuest) {
sum += (int) (item.getPrice() * 0.9); // 첫 손님 10% 할인
} else if (!item.isFresh()) {
sum += (int) (item.getPrice() * 0.8); // 덜 신선한 것 20% 할인
} else {
sum += item.getPrice();
}
}
return sum;
}
}
위 코드는 다음의 문제를 포함하고 있다.
- 서로 다른 계산 정책들이 한 코드에 섞여 있어, 정책이 추가될수록 코드 분석을 어렵게 만든다.
- 가격 정책이 추가될 때마다 calculate 메서드를 수정하는 것이 점점 어려워진다.
이런 문제를 해결하기 위한 방법 중의 하나는 가격 할인 정책을 별도 객체로 분리하는 것이다.
DiscountStrategy 인터페이스는 상품의 할인 금액 계산을 추상화하였고, 각 콘크리트 클래스는 상황에 맞는 할인 계산 알고리즘을 제공한다.
Calculator 클래스는 가격 합산 계산의 책임을 진다.
여기서 가격 할인 알고리즘을 추상화하고 있는 DiscountStrategy를 전략(Strategy)이라고 부르고
가격 기능 자체의 책임을 갖고 있는 Calculator를 콘텍스트(Context)라고 부르는데
이렇게 특정 콘텍스트에서 알고리즘을 별도로 분리하는 설계 방법이 전략 패턴이다.
전략 패턴에서 콘텍스트는 사용할 전략을 직접 선택하지 않는다.
대신, 콘텍스트의 클라이언트가 콘텍스트에 사용할 전략을 전달해 준다.
즉, DI를 이용해서 콘텍스트에 전략을 전달해 준다.
그리고 전략이 어떤 메서드를 제공할 지의 여부는 콘텍스트가 전략을 어떤 식으로 사용하느냐에 따라 달라진다.
위 코드를 DiscountStrategy로 분리한 코드를 살펴보자.
import java.util.List;
public class Calculator {
private DiscountStrategy discountStrategy;
public Calculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public int calculate(List<Item> items) {
int sum = 0;
for (Item item : items) {
sum += discountStrategy.getDiscountPrice(item);
}
return sum;
}
}
위 코드에서 Calculator 클래스는 생성자를 통해서 사용할 전략 객체를 전달받고, calculate() 메서드에서 각 Item의 가격을 계산할 때 전략 객체를 사용하고 있다.
위 코드에서 Calculator는 각 Item 별로 할인 정책을 적용하고 있으므로 DiscountStrategy 인터페이스는 다음과 같이 정의될 것이다.
public interface DiscountStrategy {
int getDiscountPrice(Item item);
}
만약 각 아이템 별로 할인 정책이 있고 전체 금액에 대한 할인 정책이 별도로 필요하다면, 위 인터페이스에 메서드를 추가해주면 된다.
또는, 전체 금액 할인 정책을 위한 전략을 별도 인터페이스로 분리할 수도 있을 것이다.
public interface ItemDiscountStrategy {
int getDiscountPrice(Item item);
}
public interface TotalPriceDiscountStrategy {
int getDiscountPrice(int totalPrice);
}
전략 객체는 콘텍스트를 사용하는 클라이언트에서 직접 생성한다.
예를 들어, 첫 번쨰 손님에 대해 할인을 해주는 FirstGuestDiscountStrategy 구현 클래스를 아래와 같이 구현했다고 치자.
public class FirstGuestDiscountStrategy implements DiscountStrategy {
@Override
public int getDiscountPrice(Item item) {
return (int) (item.getPrice() * 0.9);
}
}
첫 번째 손님이 들어와서 계산을 하면, 계산기에서 첫 번째 손님 할인 적용 버튼을 누른 뒤에 계산 버튼을 누른 것이다.
이를 처리하는 코드는 다음과 같은 방식으로 작성될 것이다.
public class Calculator {
private DiscountStrategy strategy;
public void onFirstGuestButtonClick() {
// 첫 손님 할인 버튼 누를 때 생성 됨
strategy = new FirstGuestDiscountStrategy();
}
public void onCalculationButtonClick() {
// 계산 버튼 누를 때 실행 됨
Calculator cal = new Calculator(strategy);
int price = cal.calculate(items);
}
}
위 코드를 보면 Calculator를 사용하는 코드에서 FirstGuestDiscountStrategy 클래스의 객체를 생성하는 것을 알 수 있다.
이는 콘텍스트를 사용하는 클라이언트가 전략의 상세 구현에 대한 의존이 발생한다는 것을 뜻한다.
콘텍스트의 클라이언트가 전략의 인터페이스가 아닌 상세 구현을 안다는 것이 문제처럼 보일 수 있으나, 이 경우에는 전략의 콘크리트 클래스와 클라이언트의 코드가 쌍을 이루기 때문에 유지 보수 문제가 발생할 가능성이 줄어든다.
[전략 패턴을 적용할 때 얻을 수 있는 이점]
콘텍스트 코드의 변경 없이 새로운 전략을 추가할 수 있다는 점이다.
전략 패턴을 적용함으로써 Calculator 클래스는 할인 정책 확장에는 열려 있고 변경에는 닫혀 있게 된다.
즉, 개방 폐쇄 원칙을 따르는 구조를 갖게 된 것이다.
완전히 동일한 기능을 제공하지만 성능의 장단점에 따라 알고리즘을 선택해야 하는 경우에도 전략 패턴을 사용한다.
XML을 파싱해서 객체를 생성하는 기능을 사용한다고 가정해보자.
이 경우 XML을 파싱하는 알고리즘을 Unmarshaller 타입으로 분리하고 성능 요구에 따라 DOM이나 StAX를 사용하는 콘크리트 Unmarshaller를 선택하도록 구현할 수 있을 것이다.