행동 패턴 - 전략(Strategy)
- 참조: https://refactoring.guru/design-patterns/strategy
- 참조: GoF 의 디자인패턴
- 전략(Strategy) 패턴
정책(Policy) 라고도 하며 알고리즘 집단을 정의한다. 각각의 알고리즘을 분리된 클래스로 추출하고, 객체들간의 상호교환이 가능하도록 한다.
- 문제점
여행자를 위한 네비게이션 앱을 만들기로 했다고 가정하자. 이 앱은 어느 도시에서는 사용자가 빠르게 방향을 잡을 수 있도록 지도를 중심으로 구성되어 있다.
이 앱에서 제일 중요한 기능은 자동으로 경로를 찾는것이다. 유저가 주소를 입력하면 목적지까지 가장 빠른 경로가 지도상에 표시된다.
앱의 초기버전은 자동차 도로를 통해서만 경로를 찾을 수 있게 만들었다. 차로 여행하는 사람들은 아무런 문제가 없겠지만, 모든 사람이 여행지에서 차를 이용하지는 않는다. 이런 사람들을 위해 다음 버전에서는 걸어가는 경로를 찾도록 옵션을 추가할 수 있다. 또 그 다음 버전에서는 대중교통을 이용한 경로를 선택할 수 있는 옵션도 추가할 수 있을것이다.
하지만 여행을 즐기는 방식은 다양하다. 위의 옵션들로 모든 여행자를 만족시킬 수 있다고 생각했지만, 사용자들은 또 다른 옵션을 원한다. 자전거 여행자들을 위한 경로나, 도시의 여행지들을 모두 순회하는 경로를 추가하는 등 여러가지 옵션이 추가될 수 있다.
비즈니스 관점에서는 좋은 아이디어들이긴 하지만, 기술팀은 고통 그 자체이다. 새로운 알고리즘 경로를 추가할때마다, 네비게이터의 메인 클래스의 크기가 2 배로 늘어난다. 이대로 계속 방치하면 언젠가는 유지보수하기가 매우 어려워진다.
간단한 bugfix 를 하거나 거리의 수치에 관해 약간의 조정이 생기는것과 같이 알고리즘들 중 하나에 변화가 생기면 전체 클래스에 영향을 주는데, 이렇게되면 기존에 잘 동작하고 있던 코드에서도 에러가 발생할 확률이 높다.
- 해결책
전략패턴은 많은 방법들중에서 특정 방법을 수행하는 클래스를 취하고, 이런 모든 알고리즘들은 "strategies" 라고 불리는 분리된 클래스로 추출한다.
"context" 라고 불리는 원본 클래스는 strategies 중 하나에 대한 참조를 저장하는 필드를 가져야 한다. context 는 자신이 직접 행동을 수행하는 대신 전략 객체에 동작을 위임한다.
context 가 적절한 알고리즘을 선택하는 책임을 갖지는 않는다. 대신 클라이언트가 context 에 적절한 전략을 넘겨준다. 사실 context 는 전략에 대해 자세히 알지도 못한다. context 는 캡슐화된 알고리즘을 수행시키는 단일 메소드를 갖는 인터페이스를 통해 전략들과 교류한다.
이렇게하면 context 는 상세 전략들과 결합도가 높지 않아서, 다른 전략들이나 context 의 코드 수정없이 새로운 알고리즘들을 추가하거나 기존에 존재하는 알고리즘을 수정할 수 있다.
네비게이션 앱에서 각 경로 알고리즘은 단일 "buildRoute" 메소드를 갖는 클래스로 추출된다. 이 메소드는 출발지와 목적지를 받아서 경로들의 체크포인트들을 반환한다.
같은 인자를 넘긴다해도 각 경로 클래스는 다른 경로를 설정할것이고, 메인 네비게이터 클래스의 주요 임무는 맵의 체크포인트들을 그리는것이기 때문에 어떤 알고리즘이 선택되었는지 전혀 신경쓰지 않을것이다.
- 실생활 예제
공항으로 간다고 가정해보자. 버스나 택시를 탈수도 있고, 자전거를 이용해서 갈수도 있다. 이런 방법들은 일종의 운송 전략이라고 할 수 있다. 이 전략들 중 예산이나 시간의 제한에 따라 하나를 취할 수 있다.
- 구조
전략패턴의 구조에 대해서 알아보자.
- Context: 상세 전략들 중 하나를 참조하며 전략 인터페이스를 통해서만 해당 객체와 교류한다.
- Strategy: 전략 인터페이스는 모든 상세 전략들의 공통 인터페이스이다. Context 가 전략을 수행하는데 사용하는 메소드를 선언한다.
- ConcreteStrategies: Context 가 사용하는 다양한 알고리즘들을 실제로 구현한다.
Context 는 알고리즘이 필요할때마다 연결된 전략 객체의 실행 메소드를 호출한다. Context 는 어떤 형태의 전략인지 또는 알고리즘이 어떻게 실행되는지 전혀 알지 못한다.
Client 는 전략 객체를 생성하고, Context 에 이를 넘긴다. Context 는 Client 가 전략을 실행시간에 바꿀 수 있도록 setter 를 노출한다.
- 시나리오
이번 예제에서는 다양한 경로 알고리즘을 설정하는 Navigator 앱을 만들어본다.
Context 역할을 하는 Navigator 는 Strategy 에 해당하는 RouteStrategy 인터페이스를 참조하는 필드 routeStrategy 를 갖고 있다. 그리고 실제 경로 알고리즘을 구현하는 세부 전략들 RoadStrategy, PublicTransportStrategy, WalkingStrategy 은 Strategy를 구현하고 있다.
- Java 예제
우선 Strategy 인터페이스에 해당하는 RouteStrategy 인터페이스를 정의해보자.
public interface RouteStrategy {
void buildRoute(String from, String to);
}
위의 인터페이스를 구현하는 각 세부 전략들은 어떤 클래스에서 호출되었는지를 알기위해 간단하게 구현하였다.
public class RoadStrategy implements RouteStrategy {
@Override
public void buildRoute(String from, String to) {
System.out.println("Return checkpoints from, to: " + from + ", " + to + " in RoadStrategy");
}
}
public class PublicTransportStrategy implements RouteStrategy {
@Override
public void buildRoute(String from, String to) {
System.out.println("Return checkpoints from, to: " + from + ", " + to + " in PublicTransportStrategy");
}
}
public class WalkingStrategy implements RouteStrategy {
@Override
public void buildRoute(String from, String to) {
System.out.println("Return checkpoints from, to: " + from + ", " + to + " in WalkingStrategy");
}
}
다음은 Context 에 해당하는 Navigator 클래스이다. Strategy를 참조하는 routeStrategy 와 이를 설정할 수 있는 setter 를 선언하였다. buildRoute 에서는 전략이 설정되지 않았다면 일어날 수 있는 NPE 를 막기위해 default 경로 탐색으로 RoadStrategy를 택하였다.
public class Navigator {
private RouteStrategy routeStrategy;
public void setRouteStrategy(RouteStrategy routeStrategy) {
this.routeStrategy = routeStrategy;
}
public void buildRoute(String from, String to) {
if(routeStrategy == null) {
routeStrategy = new RoadStrategy();
}
routeStrategy.buildRoute(from, to);
}
}
이제 테스트를 해보자. 각각 설정한 전략에 따라 설정한 전략이 호출됨을 알 수 있다.
public static void main(String[] args) {
Navigator navigator = new Navigator();
navigator.setRouteStrategy(new WalkingStrategy());
navigator.buildRoute("Seoul", "Busan");
navigator.setRouteStrategy(new RoadStrategy());
navigator.buildRoute("Seoul", "Busan");
navigator.setRouteStrategy(new PublicTransportStrategy());
navigator.buildRoute("Seoul", "Busan");
}
===========================
Return checkpoints from, to: Seoul, Busan in WalkingStrategy
Return checkpoints from, to: Seoul, Busan in RoadStrategy
Return checkpoints from, to: Seoul, Busan in PublicTransportStrategy