
이 글은 헤드 퍼스트 디자인 패턴(개정판) - 에릭 프릭먼 외 4 | 한빛미디어 를 참고하여 작성되었습니다.
전략 패턴(Strategy Pattern)은 동일 계열의 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 하는 패턴이다. 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있게 된다!
예를 들어, 특정 행위에 대한 기본 인터페이스를 구현하고 구체적인 행동을 구현하는 클래스를 생성하는 방식으로 전략 패턴을 구현할 수 있다.
몇 가지 장점을 더 알아보자.
1. 유연한 확장 가능
전략 패턴을 사용하면 직접 행위에 대한 코드를 수정할 필요 없이 전략만 변경하면 된다. 때문에 코드의 확장이 더 유연해진다.
2. 메소드 수정 용이
위 정의에서 동일 계열의 알고리즘군을 캡슐화한다고 했다. 즉, 같은 문제를 해결하는 여러 알고리즘이 캡슐화되어 있기 때문에 필요시에 교체가 쉽고, 다른 클래스에 영향을 주지 않고도 수정이 가능하다.
그럼 전략 패턴은 언제 사용하면 좋을까?
우선 알고리즘의 여러 버전이 필요하거나 변형이 필요한 경우, 클래스화를 통해 쉽게 관리할 수 있다. 또는 알고리즘의 동작이 런타임에 실시간으로 교체되어야 하는 경우에도 유용하게 사용될 수 있다. 기본적으로 알고리즘을 캡슐화하기 때문에 알고리즘 코드가 노출되면 안 되는 데이터에 엑세스 하거나 데이터를 활용할 때도 전략 패턴을 활용할 수 있다.
다만 알고리즘이 많아지게 되면 관리할 객체의 수가 늘어나서 오히려 애플리케이션의 복잡도가 증가할 수 있으니, 이런 경우에는 주의해야 한다.
이쯤에서 이런 의문이 들 수 있다.
"그냥 상위 클래스 하나 만들어두고 상속해서 사용하면 되는 거 아닌가?"
왜 굳이 인터페이스를 만들어서 캡슐화하는 전략을 사용하는지 살펴보기 이전에, 클래스를 상속했을 때 발생할 수 있는 불편한 점을 먼저 살펴보자.

위 그림과 같이 Duck 클래스를 상속받은 서브 클래스가 있는 상태에서 Duck 클래스에 새로운 메소드 fly() 를 추가한다면 어떻게 될까?
모든 서브클래스에서 fly() 를 상속받게 되니까, 일부 서브클래스에서 원하지 않는 메소드를 상속받아 해당 기능을 구현하게 된다. 물론 메소드 내부를 비워둔 채로 오버라이드할 수도 있지만, 그럼 클래스 내에 사용하지도 않는 불필요한 메소드들이 존재하게 된다. 즉, 클래스의 의미가 불분명해지게 되는 것이다. 이는 객체지향 설계 5원칙(SOLID) 중 OCP(Open-Closed Principle, 개방-폐쇄 원칙) 를 위배하기도 한다.
🚨 슈퍼클래스의 행동을 모두 상속할 때 단점
- 서브클래스에서의 코드 중복
- 프로그램 실행 중 코드 수정 어려움
- 모든 서브클래스의 행동을 알 수 없음
- 코드 변경 시 다른 클래스에 의도치 않은 영향을 끼칠 수 있음
그렇다고 상속을 하지 않기 위해 무작정 특정 메소드를 인터페이스화하면 어떻게 될까?

위 그림처럼 코드 중복이 발생하게 될 뿐만 아니라 메소드 커스텀도 어려워진다. 인터페이스를 수정하면 해당 인터페이스를 구현한 서브클래스들을 모두 변경해줘야 하기 때문이다.
때문에 우리는 원하는 메소드만 사용하되, 연관된 다른 클래스와는 독립적으로 동작할 수 있는 방법을 찾아야 한다.
우선 여기에서 “인터페이스” 는 단순히 Java의 인터페이스만 지칭하는 것이 아닌 “상위 형식”을 의미하기도 한다.
즉, [인터페이스에 맞춰서 코드 작성] = [상위 형식에 맞춰서 코드 작성] 이라고 이해할 수 있고, 이는 실제 실행 시에 쓰이는 객체가 코드에 고정되지 않도록 상위 형식(supertype)에 맞춰 프로그래밍해서 다형성을 활용하기 위한 방법이다.
한 마디로 정리하면, 상위 형식을 구현할 때 인터페이스 또는 추상 클래스 모두 사용 가능하다는 뜻이다.
그럼 위에서 살펴본 예제를 인터페이스와 이를 구현하는 클래스로 분류해보자.

이렇게 분류하면 상속을 사용할 때 갖는 단점을 버리고 재사용의 장점을 취할 수 있다. 우선 다른 형식의 객체에서도 재사용이 가능하고, 기존의 행동 클래스를 수정하거나 상위 클래스( Duck 클래스 )를 수정하지 않고도 새로운 행동을 추가할 수 있다!
앞서 우리는 상속이 아닌 다른 방법을 사용해서 클래스끼리 관계를 맺고 싶다고 했는데, 이 때 사용할 "다른 방법"이 바로 구성이다.
“A에는 B가 있다” 관계를 통해 구현이 아닌 위임을 하도록 두 클래스를 합치는 것을 “구성(composition)을 이용한다” 라고 한다.
그럼 이 구성을 사용해서 아까 본 상속 구조를 재구성해보자. 우선 위에서 만든 인터페이스는 Duck 이라는 추상 클래스를 구현할 때 다음과 같이 사용 가능하다.
코드로 나타내면 다음과 같다.
public abstract class Duck {
QuackBehavior quackBehavior;
...
public void performQuack() {
quackBehavior.quack();
}
...
}
Duck 클래스에 인터페이스 형식의 인스턴스 변수를 추가하고, 각 오래 객체에서 실행 시 이 변수에 특정 행동 형식의 레퍼런스를 다형적으로 설정하여 사용하게 되는 것이다.
앞서 말했듯이 각 행위를 상위 클래스(or 서브클래스)에서 정의한 메소드를 써서 구현하지 않고 다른 클래스에 위임한다는 것이 포인트이다!
이번에는 Duck 클래스를 상속받는 서브 클래스를 살펴보자.
public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
public void display() {
System.out.println("나는 청둥오리");
}
}
Duck 클래스에서 quackBehavior 와 flyBehavior 인스턴스 변수를 상속받은 것을 볼 수 있다. 또한 생성자에서 Duck 으로부터 상속받은 두 인스턴스 변수에 Quack 형식의 새로운 인스턴스를 대입하여 사용하게 된다( FlyWithWings 도 동일 ).
이 때 Duck 의 서브클래스에서 세터 메소드(setter method)를 호출하면 해당 메소드를 동적으로 활용할 수도 있다!
이런식으로 서브클래스를 생성하게 되면, 각 서브클래스는 FlyBehavior 와 QuackBehavior 를 갖고 있으며, 각 행동을 위임받는다. 즉, 상위 클래스( Duck )에서 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받게 되는 것이다.
앞서 말한 구성을 활용해서 시스템을 구성할 때 유연성을 향상시킬 수 있다는 것이 바로 이 뜻이다!
전체 코드(펼쳐보기)Duck 클래스public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck() {}
public abstract void display();
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.println("수영한다");
}
}
FlyBehavior 인터페이스와 구체적인 행동을 구현할 클래스QuackBehavior 인터페이스와 행동 클래스도 동일한 방식으로 구현public interface FlyBehavior {
public void fly();
}
public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("Fly with wings~");
}
}
public class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("I can't fly...");
}
}
public class MiniduckSimulator {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performFly();
}
}
지금까지 알아본 전략 패턴은 Java에서도 다양하게 사용되고 있다. 몇 가지만 살펴보자.
Collections의 sort() 메소드에 의해 구현되는 compare()Collections.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});javax.servlet.http.HttpServlet 에서 service() 와 모든 doXXX() 메소드들javax.servlet.Filter 의 doFilter()추가로 Spring Framwork에서도 각 ApplicationContext를 만들고 인스턴스를 만들어 사용하는 경우도 존재한다. 이 경우도 전략 패턴의 구현 방식을 사용했다고 볼 수 있다.
public class StrategyInSpring {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext();
ApplicationContext applicationContext1 = new FileSystemXmlApplicationContext();
ApplicationContext applicationContext2 = new AnnotationConfigApplicationContext();
BeanDefinitionParser parser;
PlatformTransactionManager platformTransactionManager;
CacheManager cacheManager;
}
}
슈퍼 클래스와 서브 클래스 구조를 가져가야 하지만, 각 클래스 별로 독립적으로 메소드를 활용하고 싶은 경우에는 전략 패턴을 사용할 수 있다. 상속이 아닌 구성을 활용하여 다형성도 높이고 확장 유연성도 가져갈 수 있으니까!
다만 메소드가 너무 많아지면 오히려 클래스 구조만 복잡해질 수 있으니 적절한 상황에 활용해야 함을 명심해야 한다.