[Design Pattern] 내가 보려고 정리하는 Swift 디자인 패턴 (3) - 전략 패턴(Strategy Pattern)

2024. 2. 25. 20:56Architecture, Design Pattern

1️⃣ 전략 패턴 (Strategy Pattern)이란?

전략 패턴(Strategy Pattern)특정 상황에서 사용하는 알고리즘을 캡슐화하여 런타임 내에 변경할 수 있게 해주는 디자인 패턴이다.
다시 말해, 사용되는 알고리즘을 별도의 클래스에 배치/분리하여 이 알고리즘 객체를 동적으로 교환할 수 있도록 만드는 패턴이다.


특정 상황? 알고리즘? 변경? 전략? 이 말들이 나도 처음에 잘 이해가 되지 않았기에 하나의 예시를 가지고 설명해 보겠다.
게임 캐릭터를 만드는 개발 업무를 담당하고 있다고 가정해 보자.

해당 캐릭터 클래스에는기본적으로 상대를 공격할 수 있는 attack()이라는 메서드가 포함되어 있다.
단, 캐릭터가 같은 공격을 하더라도 "공격 전략(Attack Strategy)"은 여러 개를 사용할 수 있는 경우를 개발하고자 한다.
예를 들어, 기본 공격 (Basic Attack Strategy), 기본 공격보다 훨씬 강한 공격 (Power Attack Strategy), 그리고 특수한 스킬을 사용하는 공격 (Skill Attack Strategy)과 같이 3가지의 공격 전략이 있는 캐릭터를 만드는 경우다.

이런 경우 캐릭터 클래스를 바탕으로 사용자가 만든 캐릭터 인스턴스에 대하여
같은 공격이지만, 캐릭터가 동작하는 중에도 매번 전략을 바꾸어 사용하고자 할 때 사용할 수 있는 디자인 패턴이 바로 전략 패턴(Strategy Pattern)이다!

해당 예시를 바탕으로 전략 패턴을 다시 정의해보면 아래와 같이 정의할 수 있겠다.

💡 전략 패턴(Strategy Pattern)특정 상황(= 캐릭터가 공격하는 상황)에서 사용하는 알고리즘(= (공격)전략)을 런타임 내(= 캐릭터가 동작하는 동안)에 변경할 수 있게 해주는 디자인 패턴이다.

 

2️⃣ 전략 패턴 (Strategy Pattern) 예시 코드로 이해하기

그럼 이제 위의 예시를 코드로 작성해 볼 차례이다.

Swift에서는 Strategy Pattern을 구현할 때, 크게 세 부분으로 나누어서 구현하게 된다.

  • Context : 전략을 사용하는 객체 -> 해당 공간에서 알고리즘(전략)을 선택하고 실행하는 작업이 이루어지며, 해당 파일에서 알고리즘(전략)이 동적으로 변경될 수 있는 유연성을 제공한다.
  • Strategy : 특정 상황에서 사용하는 알고리즘(전략)을 만들기 위해 사용되는 프로토콜 (= 인터페이스, 전략에서 따라야 할 큰 틀)
  • Concrete Strategy : Strategy 프로토콜을 채택해서 구현한 각 알고리즘(전략)의 세부내용, Context에서 호출될 때 사용된다.


정리하자면, Strategy Pattern에 사용되는 각 전략들(Concrete Strategy)은 인터페이스(Strategy)에 내용을 받아 구현되며,
실제 전략이 동적(런타임 중)으로 선택되는 곳이 Context(캐릭터)라고 이해하면 되겠다. 

예제 기준으로 Strategy Pattern의 구성을 이렇게 나타낼 수 있겠다.

게임 캐릭터를 이용한 예시에서 전략의 공통적인 부분은 "캐릭터는 공격한다"라는 사실이었다.

그러니 모든 캐릭터에 포함되어야 하는 공격(attack())이라는 함수는 프로토콜로 구현해 두고,
이를 채택하는 세부 전략들에서 공격에 해당하는 세부 내용 (= 변하는 내용)들을 구현할 수 있도록 할 것이다.

protocol AttackStrategy {
    func attack()
}

아래에서 각 전략별로 위에서 만든 인터페이스 프로토콜을 채택해, 전략별로 다른 내용(= 알고리즘)을 작성해 준 것을 확인할 수 있다.

// 기본 공격을 쓰는 전략(알고리즘) 클래스
class BasicAttackStrategy: AttackStrategy {
    func attack() {
        print("기본 공격!")
    }
} 

// 기본 공격보다 강한 공격을 쓰는 전략(알고리즘) 클래스
class PowerAttackStrategy: AttackStrategy {
    func attack() {
        print("강력한 공격!")
    }
}

// 스킬을 활용한 공격을 쓰는 전략(알고리즘) 클래스
class SkillAttackStrategy: AttackStrategy {
    func attack() {
        print("스킬로 공격!")
    }
}

전략이 만들어졌으니, 이 전략을 동적으로 고를 수 있도록 Context 객체를 만들어줄 것이다.

AttackStrategy 객체를 참조하고 있는 내부 인스턴스를 클래스 안에 만들고,
이를 런타임 중에 수정할 수 있도록 클래스 내의 메서드(해당 코드에서 setAttackStrategy에 해당)를 통해 만들어두면 되겠다.
*attackStrategy 인스턴스는 private로 선언되어 있다. = 외부에서 알고리즘(전략)에 대해 접근할 수 없는 캡슐화가 이루어져 있다.

// 게임 캐릭터 클래스
class GameCharacter {
    private var attackStrategy: AttackStrategy?
    
    func setAttackStrategy(_ attackStrategy: AttackStrategy) {
        self.attackStrategy = attackStrategy
    }
    
    func execute() {
        self.attackStrategy?.attack()
    }
}

실제 사용은 아래와 같이 이루어진다.

Context 객체를 바탕으로 게임 캐릭터가 만들어질 것이고, 이 캐릭터가 채택하고 있는 전략(알고리즘)을 위에서 구현한 메서드로 넣어줄 수 있다.
만약, 공격 전략을 변경하고 싶을 경우에 위에서 만들어준 인수와 다른 객체를 집어넣어 동적으로 바꿀 수 있도록 만드는 것.
이게 이 전략 패턴(Strategy Pattern)의 전부이다.

// 기본 공격 전략을 먼저 채택
let character = GameCharacter()
character.setAttackStrategy(BasicAttackStrategy())
character.execute()     // "기본 공격!"

// 강한 공격 전략으로 변경
character.setAttackStrategy(PowerAttackStrategy())
character.execute()     // "강력한 공격!"

// 스킬 공격 전략으로 변경
character.setAttackStrategy(SkillAttackStrategy())
character.execute()     // "스킬로 공격!"

이처럼 Strategy Pattern은 런타임 중에 객체 내부에서 사용되는 알고리즘의 변경을 줄 수 있다는 점, 알고리즘의 세부 구현 내용이 완벽하게 분리가 이루어질 수 있다는 점에서 장점을 가지는 디자인 패턴이다.

하지만 너무 단순한 상황에 대해 무작정 디자인 패턴을 사용하게 될 경우,
오히려 "시스템의 복잡도"를 높일 수 있다는 점에서 항상 디자인 패턴을 사용할 때 그 필요성을 확실하게 준비하고 사용하도록 하자.