전략 패턴 개념 정리(강의 - 얄팍한 코딩사전)
객체지향 디자인 패턴 중 하나인 전략 패턴에 대한 개념 정리 및 예제 코드
들어가며
소프트웨어 개발에서는 동일한 문제를 해결할 수 있는 다양한 알고리즘이나 행동을 선택할 수 있는 유연한 방법이 필요하다. 전략(Strategy) 패턴은 이러한 상황에서 특정 행동(알고리즘)을 동적으로 선택할 수 있도록 설계된 객체지향 디자인 패턴이다. 이 패턴을 사용하면 여러 행동을 미리 정의하고, 런타임에 동적으로 행동을 선택해 실행할 수 있다.
이 포스트는 ‘얄팍한 코딩사전’ 유튜브 채널의 02. 전략(Strategy) 패턴 강의를 참고하여 작성되었다. 강의는 Java로 진행되었으나, 이 포스트에서는 예제 코드를 C++로 작성하였다. 전략 패턴의 개념을 명확하게 이해할 수 있도록 설명하고, C++ 코드를 통해 개념을 구체화하였다.
선수 지식
클래스, 객체, static, 추상 클래스, 인터페이스 등 객체지향 프로그래밍의 기본 개념을 알고 있어야 전략 패턴을 쉽게 이해할 수 있다. 특히, 인터페이스 개념을 제대로 이해하고 있다면 이번 전략(Strategy) 패턴을 이해하는 데 수월할 것이다.
Strategy 패턴
Strategy 패턴은 동일한 문제를 해결하는 여러 방법을 인터페이스로 정의하고, 이 인터페이스를 구현한 구체적인 클래스를 통해 다양한 알고리즘을 구현할 수 있도록 하는 패턴이다. 주로 알고리즘의 변동이 잦거나, 다양한 방법으로 처리될 수 있는 경우 사용된다.
Strategy 패턴의 역할
Strategy 패턴의 주요 역할은 동일한 작업에 대해 여러 알고리즘을 유연하게 교체할 수 있도록 해주는 것이다. 클라이언트는 다양한 상황에 맞게 알고리즘을 선택하고, 이를 동적으로 실행할 수 있다. 각 알고리즘은 독립적인 클래스에 캡슐화되어 있으며, 필요한 경우 쉽게 교체되거나 확장될 수 있다. 즉, 코드의 유연성, 재사용성, 유지보수성을 크게 향상시킬 수 있다.
*캡슐화: 객체의 데이터와 메소드를 하나의 단위로 묶어, 외부에서 접근하지 못 하게 은닉하는 것.
언제 Strategy 패턴을 사용해야 하는가?
- 알고리즘이 여러 가지로 변동될 수 있을 때: 알고리즘이 변경되거나 확장될 가능성이 있을 때, Strategy 패턴을 적용하면 코드 수정 없이 새로운 알고리즘을 추가할 수 있다.
- 행동을 동적으로 변경할 필요가 있을 때: 런타임 시에 행동을 선택하고 바꿔야 하는 경우, Strategy 패턴을 사용하면 쉽게 동작을 변경할 수 있다.
- 조건에 따라 다른 알고리즘을 선택해야 할 때: 동일한 작업이지만 상황에 따라 서로 다른 알고리즘을 적용해야 하는 경우, Strategy 패턴이 적합하다.
Strategy 패턴의 구조
Strategy 패턴의 구조는 크게 3가지 주요 구성 요소로 나뉜다.
Context (문맥 클래스): 클라이언트가 사용하는 주요 클래스이다. 이 클래스는 Strategy 객체를 받아서 내부적으로 사용하며, 전략을 바꿀 수 있는 메소드가 포함된다.
Strategy (전략 인터페이스): 전략(알고리즘)을 정의하는 인터페이스이다. 모든 구체적인 전략 클래스는 이 인터페이스를 구현하며, 동일한 메소드를 정의한다. 이를 통해 다양한 전략이 동일한 방식으로 호출될 수 있다.
ConcreteStrategy (구체적인 전략 클래스): Strategy 인터페이스를 구현하는 구체적인 클래스이다. 각 클래스는 특정한 전략(알고리즘)을 구현한다.
게임으로 Strategy 이해하기
예를 들어, 전략 게임에서 플레이어는 여러 전술을 선택할 수 있다. 플레이어의 캐릭터는 여러 행동을 수행할 수 있으며, 공격, 방어, 도망과 같은 전략을 상황에 맞게 적용해야 할 수 있다.
- Context: 게임 캐릭터
- Strategy: 행동 전략 (공격, 방어, 도망)
- ConcreteStrategy: 공격 전략, 방어 전략, 도망 전략
캐릭터는 적의 상태에 따라 공격하거나 방어할 수 있으며, 필요에 따라 전략을 바꿀 수 있다. Context 클래스는 각 전략을 받아서 상황에 따라 실행하는 역할을 한다.
Strategy 패턴이 필요한 이유
유연성: Strategy 패턴은 알고리즘을 런타임에 쉽게 변경할 수 있는 유연성을 제공한다. 새로운 알고리즘을 추가하더라도 기존 코드를 수정할 필요가 없다.
유지보수성: 서로 다른 알고리즘은 각각 다른 클래스에 구현되기에 코드의 가독성이 높아지고, 유지보수하기 쉬워진다.
확장성: 새로운 전략을 추가하는 것이 매우 간단하다. 새로운
ConcreteStrategy
클래스만 구현하면 되기에 확장성 측면에서 매우 유리하다.
예제 코드
한 눈에 코드를 보기 위해 선언부(.h)와 구현부(.cpp)로 분리하지 않고, main.cpp에서 통합하여 코드를 구현했다.
실제로 C++로 코딩할 때는 선언부와 구현부를 나눠서 코딩하는 것을 추천한다. 프로젝트가 커질 수록 선언부와 구현부로 나눠서 작업해야 유지 보수하기 편리하고, 효율적이기 때문이다.
코드 설명
1. Strategy 인터페이스 (전략 인터페이스)
- BattleStrategy는 전략 패턴에서 공통적으로 사용할 전략(알고리즘)을 정의하는 인터페이스이다.
- Execute() 메소드는 순수 가상 함수로, 이 인터페이스를 구현하는 모든 구체적인 전략 클래스에서 반드시 구현되어야 한다.
- ~BattleStrategy()는 가상 소멸자로, 추상 클래스로부터 상속된 객체가 올바르게 소멸되도록 한다.
2. 구체적인 전략 클래스들
- AttackStrategy는 공격 전략을 나타내는 구체적인 클래스이다.
- Execute() 메소드를 오버라이드하여 공격을 실행하는 동작을 정의한다.
- 방어와 도망 전략도 각각 DefendStrategy, RunStrategy 클래스로 구현해서 방어와 도망의 행동을 정의한다(코드 설명에서의 구체적인 전략 클래스 코드는 AttackStrategy만 첨부하겠음)
3. Context 클래스 (캐릭터)
- Context 클래스 Character는 동적으로 전략 객체를 설정하고, 설정된 전략에 맞춰 행동을 수행할 수 있다.
- SetStrategy(): 캐릭터의 행동 전략을 변경하는 메소드이다. 다양한 전략(공격, 방어, 도망)을 설정 할 수 있으며, 포인터로 받아 동적으로 전략을 교체할 수 있다.
SetStrategy()에 포인터 사용 이유
SetStrategy()
메소드에서 전략 객체를 포인터로 받는 이유는 전략을 동적으로 교체할 수 있게 하기 위함이다. 포인터는 여러 객체를 동적으로 할당하거나 교체하는 데 유연한 방법을 제공한다. 참조(&)로 객체를 받으면 해당 참조는 한 번 설정된 이후 다른 객체를 참조할 수 없기 때문에, 전략을 동적으로 변경하는 패턴에 적합하지 않다.
- PerformAction(): 설정된 전략을 실행하는 메소드이다. 캐릭터가 현재 전략에 따라 행동을 취한다.
4. 클라이언트 (main 함수)
- Character 객체를 생성하고, 각각의 전략(공격, 방어, 도망)을 SetStrategy() 메소드를 사용해 캐릭터에 적용한다.
- 캐릭터가 각기 다른 전략을 수행할 수 있도록 PerformAction() 메소드를 호출하면, 설정된 전략에 따라 행동이 달라진다.
- SetStrategy()를 통해 캐릭터가 실행할 전략을 동적으로 교체할 수 있으며, 이는 전략 패턴의 핵심 기능이다.
Q&A
Q1. 전략(Strategy) 패턴과 상태(State) 패턴의 차이점은 무엇인가?
- A1: 전략 패턴과 상태 패턴은 비슷하게 보일 수 있지만, 전략 패턴은 행동(알고리즘)을 교체하는 데 중점을 두고, 상태 패턴은 객체의 상태 변화에 따른 행동 변경에 중점을 둔다. 전략 패턴에서는 전략을 명시적으로 설정하고 호출하는 반면, 상태 패턴에서는 상태가 객체의 내부에서 관리된다. 상태(State) 패턴 개념에 대해서는 이후에 다루게 된다.
Q2. Strategy 패턴을 사용하지 않고 if-else나 switch문으로 구현하는 것과 비교했을 때 어떤 차이가 있는가?
- A2: if-else나 switch문을 사용하면 코드가 길어지고, 새로운 알고리즘을 추가할 때마다 해당 문을 수정해야 하기 때문에 유지보수성이 떨어진다. 전략 패턴을 사용하면 알고리즘이 캡슐화되기 때문에, 기존 코드를 수정하지 않고도 새로운 전략을 쉽게 추가할 수 있어 확장성과 가독성이 높아진다.
Q3. 모든 경우에 전략 패턴을 사용하는 것이 좋은가?
- A3: 전략 패턴은 알고리즘이 자주 변경되거나 상황에 따라 행동이 달라져야 하는 경우에 매우 유용하다. 하지만 알고리즘이 고정적이거나 단순한 경우에는 굳이 전략 패턴을 도입하는 것이 과도한 설계일 수 있다. 코드의 복잡성이 불필요하게 증가할 수 있으므로, 적용 상황을 고려해야 한다.
Q4. 포인터 대신 참조(&)를 사용해도 전략 패턴을 구현할 수 있는가?
- A4: 포인터 대신 참조(&)로도 전략 객체를 설정할 수 있지만, 참조는 한 번 설정되면 다른 객체로 변경할 수 없다. 따라서 동적으로 전략을 변경할 필요가 없는 경우라면 참조로도 구현할 수 있다. 하지만 대부분의 전략 패턴은 동적인 전략 변경이 중요한 요소이기 때문에, 포인터를 사용하는 것이 더 적합한 경우가 많다. 즉, 동적인 변경이 필요한 경우라면 객체를 생성하고 삭제하는 것보다 포인터로 전략 패턴을 구현하는 것이 유지 보수에 훨씬 좋다.
결론
전략(Strategy) 패턴은 동적으로 알고리즘이나 행동을 변경할 수 있는 객체지향 디자인 패턴이다. 이 패턴은 프로그램의 유연성을 높이고, 유지보수성을 높이며, 여러 알고리즘을 독립적으로 캡슐화하여 서로 교체할 수 있게 만든다.
특히 게임과 같은 복잡한 시스템에서 다양한 행동을 선택해야 하는 경우, 전략 패턴을 사용하면 코드의 가독성과 확장성을 높일 수 있다. 클라이언트는 구체적인 알고리즘의 세부 사항을 몰라도 Context 클래스에 필요한 전략만 설정하여 행동을 유연하게 제어할 수 있다. 이로 인해 코드의 복잡성을 줄이고, 확장 가능성은 크게 향상된다.
또한 전략 패턴을 적용하면, 새로운 알고리즘을 추가하거나 수정할 때 기존 코드를 수정할 필요가 없기 때문에 OCP(Open-Closed Principle)를 준수하는 효과를 얻을 수 있다. 이러한 이유로 전략 패턴은 코드의 재사용성과 유지보수성을 극대화하는 중요한 디자인 패턴 중 하나이다.
시스템이 복잡해질수록 전략 패턴을 적용하여 코드의 유연성을 높이는 것이 바람직하다고 하며, 다양한 프로젝트에서 널리 사용될 수 있는 패턴이라고 한다.
함께 보면 좋은 자료
함께 보면 좋은 자료는 다음과 같다.
- SOLID 원칙과 객체지향 설계의 기본 정리(강의 - 얄팍한 코딩사전) by Kinesis - 객체지향 설계 원칙인 SOLID에 대한 상세 설명이 되어 있다.
- Strategy by Refactoring.Guru - 전략(Strategy) 패턴을 사용해야 하는 이유와 장단점 등에 대하여 설명하고 있다(디자인 패턴의 바이블 같은 플랫폼이 Refactoring.Guru이기에 애용하는 습관을 키우자).
- 전략 패턴(Strategy Pattern) by 지노원 - 전략 패턴에 대하여 스타크래프트를 예시로 설명이 되어 있다.
- 전략(Strategy) 패턴 - 완벽 마스터하기 by Inpa - 전략(Strategy) 패턴에 대한 개념이 자세하게 설명되어 있다(여담으로 전략 패턴에 대한 개념 외에 수준 높고 다양한 정보가 많아 내가 제일 좋아하는 블로그이다)
참고 자료
본 포스트를 작성할 때 참고한 자료들이다.
- 유튜브 채널 얄팍한 코딩사전: 02. 전략(Strategy) 패턴 - 본 포스트는 이 강의를 참고하여 작성되었다.