본문 바로가기
디자인패턴

[디자인패턴 - 전략패턴] 전략패턴이란? 예제를 통해 이해하기 (Strategy Pattern) for Head First Design Patterns (헤드퍼스트 디자인 패턴 참조)

by 지게요 2024. 1. 31.
728x90
반응형

이번 포스팅을 시작하기 앞서 개념과 포스팅의 전체적인 내용은 한빛미디어 / 에릭 프리먼 , 엘리자베스 롭슨 / 헤드 퍼스트 디자인 패턴(개정판)을 참조했음을 밝히고 시작하겠다.

# 전략패턴(Strategy Pattern)이란?

전략 패턴 ( Strategy Pattern ) 은 알고리즘 군을 정의하고 캡슐화해 서 각각 의 알고리즘 군을 수정해서 쓸 수 있게 해 줍니다. 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다.

위에서 나오는 말은 책에서 가져온 전략패턴의 사전적 정의이다.

나는 처음에 저런 말을 듣고 전혀 이해가 가지 않았다. 

그러므로 디자인패턴을 전혀 모른 상태에서 저 내용이 이해가 안 가는 건 정상이니 너무 걱정 안 해도 될 것이다.

 

이제 아래에서 예제를 통해 차근차근 전략패턴에 대해 알아보자!

# 전략패턴 예시 코드

먼저 패턴을 안 쓰면 어떠한 문제점이 있는지 코드로 정리하면서 전략패턴의 장점을 보여주도록 하고
책에는 오리로 예시를 들었지만 나는 로봇을 통해 한번 예시를 들어보겠다

 

기존 코드

// 메인 Robot 클래스
open class Robot{

    // 움직이는 메서드
    open fun move() { println("") }

    // 로봇의 정보를 알려주는 메서드
    open fun display() { println("로봇입니다.") }

}

class WalkingRobot: Robot() {

    override fun move() {
        println("걸어서 이동")
    }

    override fun display() {
        println("걸어서 이동하는 로봇")
    }

}

class RunningRobot: Robot() {

    override fun move() {
        println("달려서 이동")
    }

    override fun display() {
        println("달려서 이동하는 로봇")
    }
}

코드에서 보면 모든 로봇들은 Robot이라는 클래스를 상속받고 있다.

📋 이제 신규 기능으로 로봇이 공격을 해야 하는 상황이 생겨서 공격 로봇을 만들어야 하는 상황이 생겼다

만약 이대로 상속의 기능을 활용한다면 아래 코드 상황이 된다.

 

상속 코드

open class Robot{

    // 움직이는 메서드
    open fun move() { println("") }

    // 로봇의 정보를 알려주는 메서드
    open fun display() { println("로봇입니다.") }

    // 공격 메서드
    open fun attack(){ println("공격!") }

}

class WalkingRobot: Robot() {

    override fun attack() {
        println("공격 안함!")
    }

    override fun move() {
        println("걸어서 이동")
    }

    override fun display() {
        println("걸어서 이동하는 로봇")
    }

}

class RunningRobot: Robot() {

    override fun attack() {
        println("공격 안함!")
    }

    override fun move() {
        println("달려서 이동")
    }

    override fun display() {
        println("달려서 이동하는 로봇")
    }
}

// 새롭게 추가된 공격 로봇
class AttackRobot: Robot() {

    override fun attack() {
        super.attack()
    }

    override fun move() {
        println("멈춤")
    }

    override fun display() {
        println("공격하는 로봇")
    }

}

위 코드처럼 Robot 클래스에 attack이라는 메서드를 하나 추가하게 될 것이다.

이제 여기서 전략 패턴을 사용 안 하고 상속으로만 해결을 한다면 문제점이 나온다.

🔴 attack이라는 메서드를 추가함으로써 Robot 클래스를 상속받고 있던 모든 자식들은 attack 메서드를 추가하게 된다.
그렇게 되면 공격을 안 하는 로봇까지 attack 메서드에 영향을 받는 문제가 생긴다.
물론 오버라이드를 안 하면 괜찮겠지만 그것은 임시방편일 뿐 변경사항이 많고 코드가 거대해지면 

유지 보수와 생산성이 많이 저해 되는 문제도 생긴다.


## 전략 패턴 사용하기

이제는 전략 패턴을 사용해서 코드를 구성해 보겠다.

우선 전략 패턴을 사용하기 전에 디자인 원칙이 있는데 아래에 적어보겠다.

 

- 1️⃣ 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.(캡슐화)

- 2️⃣ 구현보다는 인터페이스에 맞춰서 프로그래밍한다.(상위 형식에 맞춰서 프로그래밍한다)

- 3️⃣ 상속보다는 구성을 활용한다.

 

이 3가지 원칙을 차근차근 따라 하면서 전략패턴을 사용해 Robot 클래스를 바꿔보겠다.

첫 번째 원칙은 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한 다이다.

Robot 클래스에 대입해 보면

달라지는 부분은 공격하는 메서드인 🔹attack 메서드, 움직이는 메서드인 🔹move 메서드

이 두 가지가 있다.

그리고 두 번째 원칙인 구현보다는 인터페이스에 맞춰서 프로그래밍한다를 적용해 보겠다.

아까 위에서 달라지는 부분을 인터페이스로 아래 코드처럼 만들면 되는 것이다.

interface MoveBehavior {
    fun move()
}

interface AttackBehavior {
    fun attack()
}

 

이런 식으로 각각 인터페이스로 만들고 인터페이스에 행동에 대한 클래스를 아래와 같이 만들어 준다.

class Walking : MoveBehavior {
    override fun move() {
        println("걸어서 이동")
    }
}

class NoMove : MoveBehavior {
    override fun move() {
        println("멈춤")
    }
}

class NoAttack : AttackBehavior {
    override fun attack() {
        println("공격 안함!")
    }
}

class Attack : AttackBehavior {
    override fun attack() {
        println("공격!")
    }
}

이제는 Robot의 행동은 별도의 클래스 안에 들어있고 Robot클래스에서는 그 행동을 구체적으로 구현할 필요가 없어진다.

만약 총으로 공격하는 로봇이 생겼다고 치자 그럼 아래와 같이 코드로 따로 클래스를 구현해 주면 된다.

class GunAttack : AttackBehavior {
    override fun attack() {
        println("총으로 공격!")
    }
}

이렇게 인터페이스로 구현한 별도의 클래스로 구분하면

Robot클래스를 전혀 건드리지 않고도 새로운 행동을 추가할 수 있는 장점이 있다.

아래는 방금 구현한 전체 클래스 다이어그램이다.

 

구분까지 다 마쳤으면 부모 클래스인 Robot클래스에 순서대로 적용을 해보겠다.

- 1️⃣ Robot클래스에 인터페이스 형식의 변수 추가 및 기존 move, attack 메서드 제거

- 2️⃣ 넘겨받은 인터페이스 변수에 있는 행동 클래스에 위임

open class Robot(
    // 1. 만들어준 인터페이스를 전달 받음
    private var moveBehavior: MoveBehavior,
    private var attackBehavior: AttackBehavior
){
    // 행동 클래스에 위임
    fun performMove() {
        moveBehavior.move()
    }

    // 행동 클래스에 위임
    fun performAttack() {
        attackBehavior.attack()
    }

    // 1. 기존 move 메서드 제거
    // open fun move() { println("") }

    // 1. 기존 attack 메서드 제거
    // open fun attack() {println("")}

    // 로봇의 정보를 알려주는 메서드
    open fun display() { println("로봇입니다.") }
}

위와 같이 코드를 작성하면 Robot클래스의 세팅은 끝이 난다.

이제 세팅한 Robot클래스를 가지고 아래 WalkingRobot를 바꿔보겠다.

// 변경 전 WalkingRobot 클래스
class WalkingRobot: Robot() {

    override fun attack() {
        println("공격 안함!")
    }

    override fun move() {
        println("걸어서 이동")
    }

    override fun display() {
        println("걸어서 이동하는 로봇")
    }

}

 

// 변경 후 WalkingRobot 클래스
class WalkingRobot(
    moveBehavior: Walking = Walking(),
    attackBehavior: NoAttack = NoAttack()
): Robot(moveBehavior, attackBehavior) {
    override fun display() {
        println("저는 걷는 일반 로봇 입니다")
    }
}

 

위와 같이 WalkingRobot 로봇에 moveBehavior, attackBehavior 각각에 맞는 행동 클래스를 주입해 준다.

주입해 준 행동 클래스를 부모 클래스인 Robot에 그대로 넘겨주면 WalkingRobot 클래스도 전략 패턴에 맞게 완성이다.

확실히 패턴 적용을 한 후에 코드가 훨씬 깔끔해지고 유연한 설계가 가능해지는 것을 느낄 수 있다.

그 후 메인 클래스에 아래와 같이 WalkingRobot객체로 생성해서 정보를 출력해 보자

fun main() {
    val walkingRobot: Robot = WalkingRobot()

    walkingRobot.display()
    walkingRobot.performMove()
    walkingRobot.performAttack()
}

 

실행결과

 

이렇게 우리가 원하던 결과가 출력되는 것을 볼 수 있다.


## 동적으로 행동 지정하기

이제 마지막으로 로봇의 행동을 동적으로 지정을 해주는 작업이 남아있다.

open class Robot(
    private var moveBehavior: MoveBehavior,
    private var attackBehavior: AttackBehavior
){
    fun performMove() {
        moveBehavior.move()
    }

    fun performAttack() {
        attackBehavior.attack()
    }

    // 로봇의 정보를 알려주는 메서드
    open fun display() { println("로봇입니다.") }
}

기존에는 부모 클래스에서 넘겨받은 행동들을 그대로 실행해줬다.

그렇다면 만약 로봇이 업그레이드해서 공격을 안 하던 로봇이 총으로 공격을 하게 되었다.

이런 경우를 처리하기 위한 방법을 알아보자!

- 1️⃣ Robot클래스에 set method 추가

우선 행동들을 바꿔주기 위한 세터 메서드를 추가해 준다.

open class Robot(
	private var moveBehavior: MoveBehavior,
    private var attackBehavior: AttackBehavior
){

    // 세터 메소드 추가
    fun setMoveBehavior(mb: MoveBehavior){
        moveBehavior = mb
    }
    
    // 세터 메소드 추가
    fun setAttackBehavior(ab: AttackBehavior){
        attackBehavior = ab
    }
    
    fun performMove() {
        moveBehavior.move()
    }

    fun performAttack() {
        attackBehavior.attack()
    }
    
    // 로봇의 정보를 알려주는 메서드
    open fun display() { println("로봇입니다.") }
}

- 2️⃣ 메인 클래스에서 세터 메서드를 이용해 행동 바꾸기

만들어준 세터 메소드를 활용해 normalRobot의 기본행동(걸어서 이동, 공격 안 함)을 새로운 행동(멈춤, 총으로 공격)으로 바꿔준다.

fun main() {
    val normalRobot: Robot = NormalRobot()
    
    println("로봇 업그레이드 전\n")
    
    normalRobot.display()
    normalRobot.performMove()
    normalRobot.performAttack()
    
    println("----------------")
    
    println("로봇 업그레이드 후\n")

    // 만들어준 setMoveBehavior로 행동을 바꿔줌
    normalRobot.setMoveBehavior(NoMove())
    normalRobot.setAttackBehavior(GunAttack())

    normalRobot.performMove()
    normalRobot.performAttack()
    
}

 

실행결과

 

이렇게 각 로봇에는 moveBehavior, attackBehavior이 있고 각각 움직이는 행동과 공격하는 행동을 위임받는다.

이런 식으로 두 클래스를 합치는 것을 구상이라고 한다.

🔑 결론적으로 전략 패턴을 사용하면 유연성과 재사용성을 높이고, 변경에 대응하기 쉽게 만들 수 있다.


# 느낀 점

여태까지 개발하면서 이러한 객체지향에 원칙을 잘 생각 안 하고 개발한 거 같다.

쓰면서도 이해가 안 되는 부분이 많아서 주기적으로 보면서 공부해야겠다.

반응형