본문 바로가기
ComputerScience/SoftwareEngineering

[SoftwareEngineering] 3. 디자인 패턴 - 행위

by Develaniper 2021. 6. 6.

3. 행위(Beavioral) 패턴

1) 전략 패턴

  •  같은 문제를 해결하는 여러 알고리즘이 클래스별로 캡슐화 되어 있고 이들이 필요할 때 교체할 수 있도록 함으로써 동일한 문제를 다른 알고리즘으로 해결할 수 있게 하는 패턴이다.
  • 이 것만 읽으면 너무 추상적인 말이라 이해가 잘 안갈 수 있다. 코드를 보고 이해한 후 이 말이 무슨 뜻인지 아래에서 다시한번 다뤄보겠다.

 

 

예제 코드를 보기전에 구성을 먼저 보고 가자

 

실선은 interface <-- 구현체 의 관계를 표시한 것이고, 점선은 사용하는 클래스 --> 사용되는 클래스를 표시한 것이다.

 즉, 여기서는 Robot이 Strategy들을 가지고 있으며 어떤 Strategy의 구현체를 사용하는지에 따라 전략을 바꾸는 것이다. 또한, 어떤 로봇인지에 따라 어떤 전략을 쓸지 setter로 지정해 주는 코드를 작성할 것이다.

 

 

로봇( 전략을 실행할 객체 )는 다음과 같이 작성한다.

public class Robot {
    String name;
    AttackStrategy attackStrategy;
    MoveStrategy moveStrategy;
    Robot(){}
    
    public void setMoveStrategy(MoveStrategy moveStrategy) {
        this.moveStrategy = moveStrategy;
    }
    public void setAttackStrategy(AttackStrategy attackStrategy) {
        this.attackStrategy = attackStrategy;
    }

    public void attack(){
        attackStrategy.attack();
    }
    public void move(){
        moveStrategy.move();    
    }
}

public class RMazingaZ extends Robot{
    RMazingaZ(){
        super();
        this.name="마징가 Z";
    }
}
public class RTaekwonV extends Robot{
    RTaekwonV(){
        super();
        this.name="태권V";
    }    
}

 

다음은 각 전략이다. 전략은 간단하게 move와 attack 전략으로 정했다.

// Attack 전략
public interface AttackStrategy {
    void attack();
}

public class APunchStrategy implements AttackStrategy{
    @Override
    public void attack() {
        System.out.println("get my Punch");
    }
}
public class AMissileStrategy implements AttackStrategy{
    @Override
    public void attack() {
        System.out.println("get my missile");
    }
}
// Move 전략
public interface MoveStrategy {
    void move();
}
public class MWalkStrategy implements MoveStrategy{
    @Override
    public void move() {
        System.out.println("I'm Walking....");
    }
}
public class MFlyStrategy implements MoveStrategy{
    @Override
    public void move() {
        System.out.println("I'm Flying...");
    }
}

 

 

위의 클래스들을 사용하여 아래와 같이 사용한다.

public class Main {
    public static void main(String[] args) {
        Robot taekwon = new RTaekwonV();
        Robot mazinga = new RMazingaZ();


        // ==== 이동 전략 ====
        taekwon.setMoveStrategy(new MWalkStrategy());
        mazinga.setMoveStrategy(new MFlyStrategy());
        taekwon.move();
        mazinga.move();

        // ==== 공격 전략 ====
        taekwon.setAttackStrategy(new AMissileStrategy());
        mazinga.setAttackStrategy(new APunchStrategy());
        taekwon.attack();
        mazinga.attack();
    }
}

// === 결과 ===
// I'm Walking....
// I'm Flying...
// get my missile
// get my Punch

위와 같이 각 이동, 공격 전략을 setter로 먼저 설정 한 후 move(), attack()이라는 같은 메서드를 사용한다.

이제 위에서 본 정의가 무슨 뜻인지 알아보자.

같은 문제(attack, move)를 해결하는 여러 알고리즘(missile, punch/ walk, fly)이 클래스(AttackStrategy, MoveStrategy를의 구현체들)별로 캡슐화 되어 있고 이들이 필요할 때 교체(Main에 전략을 설정한 setter들)할 수 있도록 함으로써 동일한 문제(attack, move)를 다른 알고리즘(missile, punch/ walk, fly)으로 해결할 수 있게 하는 패턴이다.

 

이점

  • punch, missile 외에 다른 전략이 생겨도 추가하기 쉽다. ( AttackStrategy의 구현체를 추가하면 된다.
  • move(), attack()에 추가적인 로직(공통일 때)이 필요해도 Robot 클래스만 바꿔주면 된다. - 단순히 Robot을 상속하여 메서드를 구현할 경우 각 자식클래스들의 메서드를 일일이 수정해야 한다.
  • OCP의 원칙을 준수할 수 있는 패턴
    Open Close Principle의 약어로 확장에대해 열려있어야 하고 수정에 대해 닫혀 있어야한다는 뜻이다.
     어떤 개체(entity)의 일부를 변경한 것이 그 개체에 의존하는 모든 모듈의 단계적 변경이 생기지 않게 하는 것이다.

 

2) 상태 패턴

  • 어떤 행위를 수행할 때 상태에 행위를 수행하도록 위임
  • 상태를 클래스로 표현
  • 외부로부터 캡슐화하기 위해 인터페이스를 생성하여 시스템의 각 상태를 나타내는 클래스로 실체화(구현)

위의 그림에서 State인터페이스는 같은 메서드를 사용하는 State1, 2, 3을 사용하여 기존의 상태를 바꿀 수 있게 사용한다. 즉, 실제 상태를 사용할때는 State 객체에 상황에 맞게 State1, State2, State3을 객체를 할당하여 사용할 수 있다.

 

예를 들어 메탈슬러그 게임의 경우 평지에 있을 때는 버튼을 누르면 앉기를 하고, 뛴 상태에서 를 누르면 아래를 보고, 움직이는 상태에서 를 누르면 기어간다. 이때, 상태를 state = 1; state = 2; state = 3; 과 같이 관리할 수 도 있지만, 상태패턴에서는 NormalState, MoveState, JumpState와 같이 클래스를 만들어서 각 움직임을 관리하는 것이다.

// 인터페이스
public interface State{
    public void downKey();
}

// 각 상태
// 일반 서있는 상태
public class NormalState implements State{
    static private State state;
    private NormalState(){}
    public static State getInstance(){
        if(state==null){
            state = new NormalState();
        }
        return state;
    }
    @Override
    public void downKey() {
        System.out.println("Sit dwon (앉기)");
    }
}
// 움직이는 상태
public class MoveState implements State{
    static private State state;
    private MoveState(){}
    public static State getInstance(){
        if(state==null){
            state = new MoveState();
        }
        return state;
    }
    @Override
    public void downKey() {
        System.out.println("Crawl (기어가기)");
    }
}
// 뛰어있는 상태
public class JumpState implements State{
    static private State state;
    private JumpState(){}
    public static State getInstance(){
        if(state==null){
            state = new JumpState();
        }
        return state;
    }
    @Override
    public void downKey() {
        System.out.println("Look Dwon (아래 보기)");
    }
}

위와같이 State를 구현하여 downKey() 메서드를 작성하고

package state;

public class Main {
    public static void main(String[] args) {
        Character character = new Character();
        
        character.keyDown();
        character.changeState(MoveState.getInstance());
        System.out.println("=== state Chagen to move ===");
        character.keyDown();
        character.changeState(JumpState.getInstance());
        System.out.println("=== state Chagen to jump ===");
        character.keyDown();
        character.changeState(NormalState.getInstance());
        System.out.println("=== state Chagen to normal ===");
        character.keyDown();
    }
}
// 전략패턴을 사용하는 클래스
public class Character {
    // int state; 전략패턴이 아닌 경우
    State state;
    Character(){
        state = new NormalState();
    }
    void changeState(State state){
        this.state = state;
    }
    void keyDown(){
        state.downKey();
    }
}


// Sit dwon (앉기)
// === state Chagen to move ===
// Crawl (기어가기)
// === state Chagen to jump ===
// Look Dwon (아래 보기)

이렇게 State를 바꿔가며 진행한다. 각 전략은 계속하여 사용될 수 있기 때문에 singletone으로 처리하여 메모리 낭비를 막을 수 있다.

 또한 상태를 변경하고 각 메서드를 사용하며 상태가 유연하게 변하게 작성해야하지만 위 예제는 이해를 위해 기본적인 상태패턴 적용에 초점을 두었다.

 

※ 전략패턴 vs 상태패턴

전략패턴과 상태패턴은 상태, 전략을 상황(객체)에 따라 다르게 설정한다는 공통점을 가지고 있어 자칫 헷깔릴 여지가 있다. 특히 맨 처음 구성도를 보면 같은 구성을 하고 있다. 또한 확실하게 짚고 넘어가지 않으면  공통적으로 각 객체마다 행위를 다르게 설정하는 방법이라는 점에서 유사해 보일 수 있다.

둘의 차이점은 다음과 같다.

  전략패턴 상태패턴
생성 특징 한번 인스턴스 생성 후 거의 바뀌지 않는 경우 한번 인스턴스 생성 후, 빈번하게 상태가 바뀌는 경우
사용하는 시점 행동을 소유한 객체를 실행 시에 선택할 수 있게 하여(사용자가) 행동하는데 유용
-> 다양한 알고리즘이 있을 때
상태변화가 아주 복잡한 로직을 제어하는데 유용
-> is, else문에 의한 분기점이 많아 복잡할 때
고려해봐야 할 때 전략이 많을 때(상속받는 객체의 행위가 모두 달라 Overriding되는 메서드를 따로 만들어야 할 때) 분기가 많을 때(상태에 따라 행동이 모두 다를 때)

 

3) 옵저버 패턴

  • 변화가 일어났을 때, 미리 등록된 다른 클래스에 통보해 주는 패턴을 구현한 것
  •  Event listener에서 이런 패턴을 사용한다.
  • 정보를 넘기고 받는 과정에서 객체의 규모, 단위가 크고 관계가 복잡해 질 수록 점점 복잡성이 증가할 수 있는데, 이때, 가이드 라인을 제시해 주는 것이 옵저버 패턴이다.

4) 커맨드 패턴

5) 템플릿 메서드 패턴

 

 

참조

https://effectiveprogramming.tistory.com/entry/Observer-패턴
https://velog.io/@y_dragonrise/디자인-패턴-상태-패턴State-Pattern
https://victorydntmd.tistory.com/294
https://jaeseongdev.github.io/development/2021/02/13/전략_패턴과_상태_패턴의_차이점/
https://velog.io/@y_dragonrise/디자인-패턴-전략-패턴Strategy-Pattern

 

댓글