전략 패턴(Strategy Pattern)

하루키·2024년 6월 22일

DesignPattern

목록 보기
1/7
post-thumbnail

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

전략 패턴(Strategy Pattern)은 런타임 시점에서 객체의 행위를 변경할 수 있도록 한다.
전략 패턴은 행위를 정의하는 알고리즘군을 정의하고, 각각의 알고리즘을 캡슐화하여 교체 가능하도록 한다.
이를 통해 객체가 특정 행위가 필요할 때 행위를 동적으로 변경할 수 있다.


💬 UML 및 유스케이스

설명
문제변하지만 서로 관련된 알고리즘이나 정책을 어떻게 설계 할 것인가?
어떻게 이러한 알고리즘이나 정책을 변경할 수 있도록 설계하는가?
해결책각각의 알고리즘을 공통 인터페이스를 가진 독립클래스에 정의한다.


이러한 설명으로는 전략패턴이 어떠한 것인지 개념을 잡기가 쉽지 않다.
시나리오를 통해 설계 해보며, 전략패턴에 대해 알아보자

여러 직업이 있으며, 직업에 따라 공격/방어가 다른 RPG게임을 생각해보자.

1. 직업에 따라 다른 공격기술을 사용한다.
2. 직업에 따라 다른 방어기술를 사용한다.


첫번째 방법)

이를 설계하기 위해서는 먼저 '상속' 이라는 개념을 적용할 수 있다.
공통 행동(공격, 방어)를 부모클래스에 추상화하여, 자식클래스에 상속을 한다.

이처럼 doAttack()과 doDefense()를 추상화 하여, 각 직업별로 상속받아 사용할 수 있다.
큰 문제는 없어보이나, 직업 클래스별 공격 행동과 방어 행동을 일일이 오버라이드 하여 구현해야 하므로 코드중복 및 코드 재사용성이 매우 떨어진다.

  • 🔔 상속은 기본적으로 부모 클래스의 메서드를 모두 사용함을 가정한다.
    예를 들어 공격과 방어가 없는 초보자 클래스가 추가될 경우, 아무것도 하지 않는 doAttack()과 doDefense()라는 메서드를 구현해주어야 함. (비효율적)


두번째 방법)

이를 해결하기 위해 인터페이스를 사용하면 어떨까?
공통행동을 인터페이스로 정의하여 필요한 클래스 별로 구현하여 사용하도록 한다.

공통 행동(Attack과 Defense)를 인터페이스에 정의하고, 직업별로 필요한 행동을 구현하여 사용하도록 설계했다.
이 또한 큰 문제는 없어보이나, 직업 클래스에서 결국 행동을 중복해서 구현하게 된다.

  • 유사한 행동을 하는 클래스 생성 시, 중복구현 문제


세번째 방법)

※ 디자인 원칙

1. 애플리케이션에서 달라지는 부분과, 달라지지 않는 부분을 분리 캡슐화
2. 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다
→ 각 행동은 인터페이스로 표현, 행동을 구현할 때 해당 인터페이스를 구현
→ 특정 행동을 원하는 클래스는 인터페이스를 구현하는 클래스를 선택 사용 위임

위 문제들을 해결하기 위해, 디자인 원칙을 적용할 수 있다.
인터페이스를 다른 클래스에서 사용할 수 있도록, 관련 클래스에 이를 구동시킬 수 있는 메서드를 추가한다.

💡 Attack 인터페이스와 Defense 인터페이스를 정의하고, 다른 클래스에서 사용할 수 있도록 하위클래스에 각각 구현한다
행동의 캡슐화

💡 추상클래스(Player)에 인터페이스와 연관을 형성하여 해당 행동들을 불러서 사용할 수 있게 된다.
LSP(리스코프 치환 원칙)에 의해 상위 클래스를 사용할 수 있으면, 하위 클래스를 사용할 수 있게 된다.

해당 UML에서는 Player는 Attack과 Defense 인터페이스 타입의 멤버 변수를 가지고 있다.
Player 클래스는 doAttack()과 doDefense() 메서드를 통해 공격과 방어를 수행하도록 한다.
Player 하위 클래스에는 공격과 방어 행동을 변경할 수 있는 기능을 구현한다.

쉽게 설명 하자면, 각 직업별로 필요한 행위 (알고리즘)들을 구현해 놓고, 원하는 기능만 골라서 설정해서 생성할 수 있다는 것이다. 또한 컴파일 후에도 객체의 특정 행위를 런타임에 변경할 수 있어(동적 교체), 매우 유연한 코드가 된다.

또한 새로운 공격 방법이 추가 되더라도, 기존 코드를 수정할 필요 없이 새로운 클래스를 생성하고 설정만 하면 된다.

처음에는 잘 이해가 안됬었는데, 특히 확장성 면에서 매우 장점이 두드러진다.
현재는 4개의 직업밖에 없지만, 만약 궁수의 공격과 전사의 방어를 모두 사용할 수 있는 스페셜 직업이 추가되었다고 가정해 보자.

기존에 알고 있던 방법인 상속에 의한 구현이었다면, Special 클래스를 생성하고 클래스 내부에서 다시 필요 메서드들을 재정의 해야한다. 하지만 전략패턴을 사용하면 새로운 행동을 추가하거나 변경하는 작업이 훨씬 간단해진다.
그냥 Special 클래스를 생성하고 필요한 행동을 구현해놓은 클래스를 가져와서 행위를 설정해주기만 하면 된다!

코드

실제 코드

public class Player {
    
    public Attack attack_Behavior; // 연관 형성
    public Defense defense_Behavior; // 연관 형성

    public void doAtack() { // 플레이어 객체의 공격 행동. 어떤 직업의 공격을 할지는 하위 클래스에서 설정
        attack_Behavior.doAttack();
    }

    public void doDefense() { // 플레이어 객체의 방어 행동. 어떤 직업의 방어를 할지는 하위 클래스에서 설정
        defense_Behavior.doDefense();
    }
}
public class Archer extends Player { // 공격-활 방어-회피

    public Archer() {
        this.attack_Behavior = new AkBow(); // 플레이어의 공격 행위 설정
        this.defense_Behavior = new DfDodge(); // 플레이어의 방어 행위 설정
    }

}

public class Warrior extends Player { //공격-칼 방어-방패

    public Warrior() {
        this.attack_Behavior = new AkSword();
        this.defense_Behavior = new DfShield();
    }
}

테스트 코드

public class StrategyTest {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        Player p1 = null; // LSP에 의해 플레이어 타입 객체는 하위 클래스(직업)의 행동들을 수행 가능
        while (true) {
            if (p1 == null) {
                System.out.println("현재 직업이 없습니다. 직업을 골라주세요"); // 전략 설정 (동적 교체)
                System.out.println("1. 궁수 2. 닌자 3. 검사 4. 방패전사");
                int input = Integer.parseInt(br.readLine());
                switch (input) {
                    case 1:
                        p1 = new Archer();
                        break;
                    case 2:
                        p1 = new Ninja();
                        break;
                    case 3:
                        p1 = new DoubleSword();
                        break;
                    case 4:
                        p1 = new Warrior();
                        break;
                }
                input = 0;
            }
            
            System.out.println("1. 공격 2. 방어 3. 직업변경");
            while (p1 != null) {
                int input = Integer.parseInt(br.readLine());
                switch (input) {
                    case 1:
                        p1.doAtack();
                        break;
                    case 2:
                        p1.doDefense();
                        break;
                    case 3:
                        p1 = null;
                        break;
                }
            }
        }
    }
}

실행 결과


직업이 동적으로 교체되고, 교체 됨에 따라 해당 객체의 행위가 변경됨을 확인 할 수 있다.
이를 통해 코드 중복을 줄이고, 유지보수 및 확장성을 늘릴 수 있다.

📝 결론

전략 패턴(Strategy Pattern)은 행위를 클래스로 캡슐화하여 동적으로 행위를 자유롭게 변경할 수 있도록 하는 디자인 패턴이다.
1. 특정 행위를 인터페이스로 정의하고,
2. 그 인터페이스를 구현하는 여러 클래스들을 작성한 다음,
3. 필요에 따라 해당 행위를 사용하는 객체의 행위를 런타임에 설정하거나 변경할 수 있다.

전략 패턴의 주요 요소
Context(문맥): 행위를 사용하는 클래스. Player 클래스는 Attack과 Defense라는 행위를 가진다.
Strategy(전략): 행위를 나타내는 인터페이스입니다. Attack과 Defense 인터페이스가 이에 해당.
Concrete Strategy(구체적인 전략): 전략 인터페이스를 구현하는 구체적인 클래스들입니다. AkBow, AkShield 등

요약

동적으로(런타임 시점) 객체의 행위(알고리즘)을 선택 및 변경 가능하다
전략 패턴을 적용하면 특정 행위(알고리즘)가 변경되더라도 기존 코드를 수정하지 않고 새로운 전략 클래스를 작성하여 설정하면 되므로, 코드의 결합도를 낮추고 단일 책임 원칙을 준수할 수 있다!

profile
코딩 못하는 개발자(진)

0개의 댓글