![[디자인패턴] 전략 패턴 (Strategy Pattern)](/images/dp-1.jpeg)
[디자인패턴] 전략 패턴 (Strategy Pattern)
소개
알고리즘들의 집합을 정의하고 각각을 캡슐화하여, 이들을 상호 교환이 가능하도록 만드는 것을 목적으로 하는 패턴입니다. 이를 통해 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경하거나 확장할 수 있게 됩니다.
예시: 로그인 기능을 구현하고 싶은 상황
기본적인 아이디 패스워드 형태의 로그인뿐 아니라 다양한 소셜 로그인(네이버, 카카오 등) 기능을 구현하려는 상황입니다.
서너 가지의 로그인 방식을 구현하느라 코드가 길어져 가뜩이나 유지보수가 어려운데, 구글 로그인에 대한 수요가 생겨 기능을 추가해야 한다면 머리가 아프겠죠?
소셜 로그인은 OAuth라는 방식을 공통적으로 사용하기 때문에, ‘OAuth를 사용하는 소셜 로그인 알고리즘들의 집합’을 정의해두고 각 알고리즘을 Strategy로 구현해 캡슐화한다면 신규 알고리즘 추가 시 유지보수가 쉬워질 것 같습니다.
또한 로그인 컨텍스트도 상황에 따라 교체 가능한 Strategy 객체를 참조하게 됨으로써 기존보다 유연해집니다.
전략 패턴은 어떨 때 유용할까요?
- 행위(behavior)만이 조금씩 다른 여러 관련 클래스들이 존재할 때
- 알고리즘의 여러 변형판이 필요할 때
- 알고리즘이 클라이언트에게 노출되면 안 되는 복잡한 데이터 구조를 사용하고 있을 때
- 클래스 내부에 서로 다른 행위를 정의하기 위해 여러 조건문을 사용하고 있을 때
구조
전략 패턴의 구조는 크게 세 가지 종류의 클래스(참여자, participant)로 구성됩니다.
-
Strategy (전략): 지원할 모든 알고리즘에 대한 공통 인터페이스입니다.
-
ConcreteStrategy: Strategy 인터페이스를 구현하여 구체적인 알고리즘을 정의합니다.
-
Context: Strategy 객체에 대한 참조를 유지하면서 ConcreteStrategy 객체로 구성(configure)됩니다. Strategy 데이터에 접근할 수 있는 인터페이스를 정의할 수도 있습니다.
설명: Context에 대한 자세한 해설과 예시
Context와 Strategy, ConcreteStrategy 사이의 관계는 조금 어려울 수 있는 내용이라서 자세한 해설과 예시를 함께 들어보겠습니다.
해설 예시 Context는 특정 작업을 도와 줄 ‘도우미(Strategy 객체)’를 필요로 합니다. 어떤 애플리케이션에서 여러 가지 정렬 알고리즘을 사용할 필요가 있다고 해 봅시다. 프로그램 실행 중에 사용자 코드는 “이번엔 이 전략을 쓰세요!”라고 구체적인 전략(ConcreteStrategy)을 Context에 끼워넣습니다. 이것이 앞서 언급한 configure의 의미입니다. BubbleSortStrategy, QuickSortStrategy 등의 전략이 Context에 갈아끼워질 수 있습니다. Context는 어떤 ConcreteStrategy가 들어왔는지 알 필요 없고, 그래서 인터페이스 타입인 Strategy로 멤버 변수(참조)를 가지고 있습니다. Context는 구체적인 클래스 타입이 아닌 SortStrategy 타입으로 멤버 변수를 가지고 있습니다. Strategy 객체가 알고리즘을 수행하려면 데이터가 필요하므로, Context에게 이를 요청하거나 Context가 스스로 데이터를 공개해야 할 수도 있습니다. SortStrategy가 정렬을 하려면 정렬할 리스트가 필요하므로, Context에게 데이터를 달라고 요청하거나 Context가 public 메서드 등으로 데이터를 공개해야 할 수도 있습니다.
장단점
전략 패턴을 적용하면 클래스 상속에 비해서도 훨씬 재사용성이 높습니다. 상속은 행위를 부모 클래스에 고정시키므로 알고리즘을 동적으로 변경하거나 독립적으로 이해하기 어렵게 만들 수도 있습니다. 하지만 전략 패턴은 알고리즘을 독립적인 객체로 캡슐화하여 교체와 확장을 쉽게 합니다.
하지만 단점도 있습니다. 클라이언트가 적절한 전략을 선택하기 위해 각 전략의 구체적인 구현 차이를 이해하고 있어야 한다는 것이 가장 큰 단점이 될 수 있습니다. 또 모든 전략이 컨텍스트가 전달하는 모든 정보를 필요로 하지 않을 수 있음에도, 인터페이스의 통일성을 위해 통신 오버헤드가 발생할 수 있습니다. 마지막으로 전략 객체의 수가 늘면 애플리케이션의 객체 총량이 늘어나는 점도 고려가 필요합니다.
구현
Push와 Pull이라고 불리는 두 가지 구현 방식이 있는데, Strategy와 Context 간의 데이터 전달 방식이 다릅니다.
- Push: Context가 필요한 데이터를 Strategy에게 파라미터로 밀어넣습니다. 전략과 컨텍스트의 결합도를 낮추지만, 전략이 불필요한 데이터까지 전달받을 수 있습니다.
- Pull: Context가 자기 자신(this)을 Strategy에게 전달하고 Strategy가 필요한 데이터를 당겨옵니다. 데이터 효율이 높지만 결합도가 높아집니다.
ConcreteStrategy 객체를 선택적으로 만드는 방법도 고려할 만합니다. Context 내에 기본 동작을 구현해두고 ConcreteStrategy 객체를 받지 않을 때는 이를 수행하게 할 수 있습니다. 또 상태가 없는 ConcreteStrategy 객체들은 여러 Context 사이에서 공유(=Flyweight 패턴)할 수도 있습니다. 두 방법 모두 객체 생성 오버헤드를 줄일 수 있습니다.