전략 패턴(Strategy Pattern)
전략 패턴은 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴이다.
여기서 '전략'이란 일종의 알고리즘이 될 수도 있으며, 기능이나 동작이 될 수도 있는 특정한 목표를 수행하기 위한 행동 계획을 말한다.
즉, 어떤 일을 수행하는 알고리즘이 여러 가지 일 때, 동작들을 미리 전략으로 정의함으로써 손쉽게 전략을 교체할 수 있는, 알고리즘 변형이 빈번하게 필요한 경우에 적합한 패턴이다.
전략 패턴 구조
- 전략 알고리즘 객체들 : 알고리즘, 행위, 동작을 객체로 정의한 구현체
- 전략 인터페이스 : 모든 전략 구현체에 대한 공용 인터페이스
- 컨텍스트(Context) : 알고리즘을 실행해야 할 때마다 해당 알고리즘과 연결된 전략 객체의 메소드를 호출.
- 클라이언트 : 특정 전략 객체를 컨텍스트에 전달 함으로써 전략을 등록하거나 변경하여 전략 알고리즘을 실행한 결과를 누린다.
전략 패턴은 OOP의 집합체
GoF의 디자인 패턴 책에서는 전략 패턴을 다음과 같이 정의한다.
- 동일 계열의 알고리즘군을 정의하고
- 각각의 알고리즘을 캡슐화하여
- 이들을 상호교환이 가능하도록 만든다.
- 알고리즘을 사용하는 클라이언트와 상관없이 독립적으로
- 알고리즘을 다양하게 변경할 수 있게 한다.
사실 전략 패턴은 SOLID 원칙의 OCP원칙, DIP원칙과 합성(composition), 다형성(polymorphism), 캡슐화(encapsulation) 등 OOP 기술들의 총집합 버전이라고 보면 된다.
따라서 위의 전략 패턴의 정의를 다음과 같이 빗대어 설명하면 이해하기 쉬울 것이다.
- 동일 계열의 알고리즘군을 정의하고 -> 전략 구현체로 정의
- 각각의 알고리즘을 캡슐화하여 -> 인터페이스로 추상화
- 이들을 상호 교환이 가능하도록 만든다. -> 합성(composition)으로 구성
- 알고리즘을 사용하는 클라이언트와 상관없이 독립적으로 -> 컨텍스트 객체 수정 없이
- 알고리즘을 다양하게 변경할 수 있게 한다. -> 메소드를 통해 전략 객체를 실시간으로 변경함으로써 전략을 변경
전략 패턴으로 게임하기
전략 패턴의 예시로 게임을 플레이하는 것을 코드로 구현해 본다.
우리는 FPS, RPG, AOS 게임을 구현할 것이다. 하지만 전체적인 골격은 아래와 같을 것이다.
로그인을 한다. -> 게임을 킨다. -> 게임을 플레이한다. -> 게임을 끈다. -> 로그아웃을 한다.
하지만 FPS, RPG, AOS 게임은 각각 플레이하는 방식이 다르다. 때문에 어떤 게임을 플레이하냐에 따라 게임의 play 방식이 달라질 것이다.
전략 패턴이 없다면
Game.java
import Strategy_Pattern.after.GamePlay;
public class Game {
public static final int RPG = 0;
public static final int FPS = 1;
public static final int AOS = 2;
private int gameKind;
public Game(int gameKind) {
this.gameKind = gameKind;
}
public void process() {
login();
turnOn();
play();
turnOff();
logout();
}
private void play() {
if(gameKind == RPG) {
System.out.println("몬스터를 잡는다");
} else if(gameKind == FPS) {
System.out.println("총을 쏜다.");
} else if(gameKind == AOS) {
System.out.println("타워를 부순다.");
}
}
private void logout() {
System.out.println("로그 아웃을 한다.");
}
private void login() {
System.out.println("로그인을 한다.");
}
private void turnOff() {
System.out.println("게임을 끈다.");
}
private void turnOn() {
System.out.println("게임을 킨다.");
}
}
위의 코드를 살펴보면 gameKind
매개 변수 값에 따라서 play()
함수의 동작을 제어하도록 되어 있다.
상수를 메소드에 넘겨 조건문으로 일일이 필터링하여 적절한 전략을 실행했다.
하지만 상태 변수를 통해 행위를 분기문으로 나누는 행위는 좋지 않은 코드이다. 자칫 잘못하면 if else 지옥에 빠질 수 있기 때문이다.
전략 패턴 적용시키기
위의 코드를 해결하는 가장 좋은 방법은 변경시키고자 하는 행위(전략)를 직접 넘겨주는 것이다.
우선 여러 게임들을 객체 구현체로 정의하고 이들을 GamePlay라는 인터페이스로 묶어 주었다.
그리고 인터페이스를 컨텍스트 클래스에 합성(composition) 시키고, setGamePlay()
메소드를 통해 전략 인터페이스 객체의 상태를 바로바로 변경할 수 있도록 구성했다.
GamePlay.java
public interface GamePlay {
public void play();
}
RPGGamePlay.java
public class RPGGamePlay implements GamePlay{
@Override
public void play() {
System.out.println("몬스터를 잡는다.");
}
}
FPSGamePlay.java
public class FPSGamePlay implements GamePlay{
@Override
public void play() {
System.out.println("총을 쏜다.");
}
}
AOSGamePlay.java
public class AOSGamePlay implements GamePlay{
@Override
public void play() {
System.out.println("타워를 부순다.");
}
}
Game.java
//컨텍스트 클래스
public class Game {
private GamePlay gamePlay;
public Game(GamePlay gamePlay) {
this.gamePlay = gamePlay;
}
public void setGamePlay(GamePlay gamePlay) {
this.gamePlay = gamePlay;
}
public void process() {
login();
turnOn();
gamePlay.play();
turnOff();
logout();
}
private void logout() {
System.out.println("로그 아웃을 한다.");
}
private void login() {
System.out.println("로그인을 한다.");
}
private void turnOff() {
System.out.println("게임을 끈다.");
}
private void turnOn() {
System.out.println("게임을 킨다.");
}
}
클린 하지 않은 코드에서는 메서드에 상수값을 넘겨주었지만, 전략 패턴에선 인스턴스를 넣어 알고리즘을 수행하도록 한 것이다.
이런 식으로 구성하면 좋은 점은 다른 게임의 종류들을 추가적으로 등록할 때, 코드의 수정 없이 빠르게 기능을 확장할 수 있다는 장점이 있다. (클래스를 추가하고 implements 해주면 끝)
결국 객체 지향 프로그래밍의 핵심인 유지보수를 용이하게 하기 위해, 약간 복잡하더라도 이러한 패턴을 적용하여 프로그램을 구성해 나가는 것이다.
전략 패턴 특징
전략 패턴 사용 시기
- 전략 알고리즘의 여러 버전 또는 변형이 필요할 때 클래스화를 통해 관리
- 알고리즘 코드가 노출되어서는 안 되는 데이터에 액세스 하거나 데이터를 활용할 때 (캡슐화)
- 알고리즘의 동작이 런타임에 실시간으로 교체되어야 할 때
전략 패턴 주의점
- 알고리즘이 많아질수록 관리해야 할 객체의 수가 늘어난다는 단점이 있다.
- 만일 어플리케이션 특성이 알고리즘이 많지 않고 자주 변경되지 않는다면, 새로운 클래스와 인터페이스를 만들어 프로그램을 복잡하게 만들 이유가 없다.
- 개발자는 적절한 전략을 선택하기 위해 전략 간의 차이점을 파악하고 있어야 한다. (복잡도 ↑)
전략 패턴 vs 템플릿 메소드 패턴
패턴 유사점
- 전략 패턴과 템플릿 메서드 패턴을 알고리즘을 때에 따라 적용한다는 컨셉으로써, 둘이 공통점을 가지고 있다.
- 전략 및 템플릿 메서드 패턴은 OCP 원칙을 충족하고 코드를 변경하지 않고 소프트웨어 모듈을 쉽게 확장할 수 있도록 하는 데 사용할 수 있다.
패턴 차이점
- 전략 패턴은 합성(composition)을 통해 해결책을 강구하며, 템플릿 메서드 패턴은 상속(inheritance)을 통해 해결책을 제시한다.
- 그래서 전략 패턴은 클라이언트와 객체 간의 결합이 느슨한 반면, 템플릿 메서드 패턴에서는 두 모듈이 더 밀접하게 결합된다. (결합도가 높으면 안 좋음)
- 전략 패턴에서는 대부분 인터페이스를 사용하지만, 템플릿 메서드 패턴에서는 주로 추상 클래스나 구체적인 클래스를 사용한다.
- 전략 패턴에서는 전체 전략 알고리즘을 변경할 수 있지만, 템플릿 메서드 패턴에서는 알고리즘의 일부만 변경되고 나머지는 변경되지 않은 상태로 유지된다. (템플릿에 종속)
- 따라서 단일 상속만이 가능한 자바에서 상속 제한이 있는 템플릿 메서드 패턴보다는, 다양하게 많은 전략을 implements 할 수 있는 전략 패턴이 협업에서 많이 사용되는 편이다.
참고자료 :
'백엔드 > Java' 카테고리의 다른 글
[Java] 상속(Inheritance)과 합성(Composition) (1) | 2023.05.19 |
---|---|
[Java] 옵저버 패턴(Observer Pattern) (1) | 2023.05.17 |
[Java] 템플릿 메서드 패턴(Template Method Pattern) (0) | 2023.05.17 |
[Java] 스트림(Stream) (0) | 2023.05.11 |
[Java] 람다(Lambda) (0) | 2023.05.09 |