전략 패턴은 디자인 패턴 중 하나로 "알고리즘(전략)을 통째로 갈아 끼울 수 있게 만드는 패턴"이다. 전략 패턴은 if-else나 switch문으로 수많은 로직을 분기 처리하고 있을 때, OCP 원칙을 적용하여 코드를 개선하고 싶을 때 사용한다.
전략 패턴은 다음 과정을 따라 수행된다.
아래의 예제를 보며 더 정확히 알아보자.
배경: 플레이어의 아바타가 세계를 탐험하는 간단한 게임을 개발하고자 한다. 플레이어의 아바타를 나타내는 Character 클래스가 존재한다.
문제 상황: 캐릭터의 이동 방식은 현재 상태 및 지형에 따라 달라진다.
먼저 이 로직을 단순하게 Character 클래스 안의 move() 메소드 하나에 if-else문으로 구현해보자.
public class Character {
public void move(String movingMethod, double distance) {
if("walking".equals(movingMethod)) {
// 걷기 로직
} else if("swimming".equals(movingMethod)) {
// 수영하기 로직
} else if("flying".equals(movingMethod)) {
// 날기 로직
}
// 새로운 이동 방식이 추가될 때마다 이 클래스를 수정해야 한다 (OCP 위반)
}
}
만약 위와 같이 구현한다면 추후 '기어가기'나 '순간이동'과 같은 새로운 이동 방식을 추가하기 위해선 Character 클래스 코드를 직접 수정해야 한다. 이는 개방-폐쇄 원칙(OCP)를 위반하게 된다.
이제 전략 패턴을 적용해보자.
public interface MoveStrategy {
void move(double distance);
}
public class WalkingStrategy implements MoveStrategy {
public void move(double distance) {
System.out.println(distance+"km만큼 걸어가는 중...🚶♀️➡️");
}
}
public class SwimmingStrategy implements MoveStrategy {
public void move(double distance) {
System.out.println(distance+"m만큼 수영하는 중...🏊♀️");
}
}
public class FlyingStrategy implements MoveStrategy {
public void move(double distance) {
System.out.println(distance+"km만큼 잠깐 날아가는 중...🧚♀️");
}
}
public class Character {
public void moveCharacter(MoveStrategy moveStrategy, double distance) {
moveStrategy.move(distance);
}
}
전략패턴을 적용하여 '이동한다'는 전략을 Character 클래스로부터 완전히 분리하였다. 추후 다른 이동 방식이 추가된다고 하여도 Character 클래스의 코드는 전혀 수정할 필요 없이 새로운 이동방식 클래스만 만들면 된다.
// main 메소드에서 사용
public class Main {
public static void main(String[] args) {
// Character 생성
Character character = new Character();
// 각각의 움직임 strategy 객체를 생성
MoveStrategy walking = new WalkingStrategy();
MoveStrategy swimming = new SwimmingStrategy();
MoveStrategy flying = new FlyingStrategy();
System.out.println("모험가가 광활한 초원을 탐험합니다.");
character.moveCharacter(walking, 10);
System.out.println("모험가가 강을 넘으려 합니다.");
character.moveCharacter(swimming, 500);
System.out.println("강 속 보물상자에서 비행 물약을 발견했습니다!");
character.moveCharacter(flying, 1);
}
}
실행결과:
모험가가 광활한 초원을 탐험합니다.
10.0km만큼 걸어가는 중...🚶♀️➡️
모험가가 강을 넘으려 합니다.
500.0m만큼 수영하는 중...🏊♀️
강 속 보물상자에서 비행 물약을 발견했습니다!
1.0km만큼 잠깐 날아가는 중...🧚♀️
사실 위에서 전략 패턴을 적용해 구현한 코드는 완벽하지 않다. 자세히 보면 다른 이동방식은 이동 거리의 단위가 km인 것에 반해 SwimmingStrategy만 단위가 m인 것을 확인할 수 있다. 이렇게 전략마다 미묘한 차이가 나는 것은 좋지 않은 설계이다.
전략 패턴의 핵심은 MoveStrategy라는 추상화된 약속을 통해 세부적인 이동 방식을 숨기는 것이다. 하지만 SwimmingStrategy만 다른 단위를 사용한다면, 숨겨져야 할 내부 구현 방식이 밖으로 새어 나온 것이다. 이는 사용하는 입장에서 전략마다 어떤 단위를 쓰는지 미리 알고 있어야 하며, 만약 distance를 활용하여 계산을 하게 된다면 아래와 같은 지저분한 분기문을 사용해야 할 수도 있다.
// 클라이언트가 전략의 내부 사정을 알아야만 함
if (currentStrategy instanceof SwimmingStrategy) {
// 미터(m) 단위로 계산하는 로직
} else {
// 킬로미터(km) 단위로 계산하는 로직
}
이런 코드가 생기는 순간, 전략 패턴을 사용한 의미가 사라진다. 따라서 가장 좋은 해결책은 전략 인터페이스에서부터 미리 약속을 통일하는 것이다.
// 수정된 예
public class WalkingStrategy implements MoveStrategy {
public void move(double distanceKM) {
System.out.println(distanceKM + "km만큼 걸어가는 중...🚶♀️➡️");
}
}
public class SwimmingStrategy implements MoveStrategy {
public void move(double distanceKM) {
System.out.println(distanceKM + "km만큼 수영하는 중...🏊♀️");
}
}
public class FlyingStrategy implements MoveStrategy {
public void move(double distanceKM) {
System.out.println(distanceKM+"km만큼 잠깐 날아가는 중...🧚♀️");
}
}
현재 Character 클래스는 이동할 때마다 외부에서 전략을 주입받는 '상태가 없는' 방식이다. 전략 패턴의 또 다른 구현 방식으로 Character가 현재의 이동 전략을 '상태'로써 기억할 수 있다.
// 상태를 가지는 Character 클래스 예시
class Character {
private MoveStrategy currentMoveStrategy; // 현재 전략을 상태로 저장
// 현재 전략을 변경하는 메소드
public void setMoveStrategy(MoveStrategy moveStrategy) {
this.currentMoveStrategy = moveStrategy;
}
// 저장된 현재 전략을 사용하여 이동
public void move(double distance) {
if (currentMoveStrategy == null) {
System.out.println("이동 방법을 먼저 정해주세요!");
return;
}
currentMoveStrategy.move(distance);
}
}
// main 메소드
Character character = new Character();
character.setMoveStrategy(new WalkingStrategy()); // 현재 상태를 '걷기'로 설정
character.move(10); // "걷는 중..."
character.setMoveStrategy(new FlyingStrategy()); // 상태를 '비행'으로 변경
character.move(1); // "날아가는 중..."
전략 패턴을 적용하는 것은 OCP 원칙을 적용하는 것과 유사하여 실습에 큰 어려움은 없었다. 그리고 공부를 하면서 알게된 상태를 기억하는 Character 클래스는 좀 더 복잡한 코드에서 유용하게 사용할 수 있을 것 같았다. 다음에는 이를 활용한 좀 더 복잡한 실습을 해봐야겠다.
추가로 처음 전략 패턴을 연습할 때 무의식중에 전략끼리의 단위를 다르게 설정하였는데 나중에 다시 생각해보니 이것이 추상화를 해치는 행위였음을 깨달았다. 전략이 몇 개 없는 현재 코드에서는 이 차이가 사소해 보일 수 있지만, 이러한 비일관성을 미리 바로잡는 습관이 결국 클린 코드로 나아가는 중요한 밑거름이 될 것이라고 생각한다.