[상태패턴] 상태(State)패턴 정리 + 적용해 보기

Jae Eon·2021년 6월 27일
1

백엔드 공부

목록 보기
6/17

들어가며

상태패턴은 무엇이며, 상태패턴을 사용하는 상황을 소개하고 실제 코드에 적용한 내용을 정리하기 위한 포스트 입니다.

🍊 상태패턴 정의

전략 패턴(State Pattern)은 행위패턴의 한 종류로 동일한 메소드를 객체 상태에 따라 다른 행위로 사용할 때 사용된다.

즉 객체가 상태에 따라 행위를 달리하는 상황에서,
자신이 직접 상태를 체크하여 상태에 따라 행위를 호출하지 않고,
상태를 객체화 하여 상태가 행동을 할 수 있도록 위임하는 패턴이다.

🍑 상태패턴을 사용하는 상황

이전에 소개한 전략패턴은 외부에서 전략을 주입해 줬어야 한다면 상태패턴은 State로직의 끝에 setState()메소드를 통해 State를 스스로 변환이 가능하다.

보통 다양한 상태가 있고 이를 구분할때 사용하는 if, else문의 복잡한 분기점을 제거하기위해 사용한다.

그예로 게임 캐릭터의 방향키가 게임 캐릭터의 상태에 따라 회전을 할 수도, 이동을 할 수도 있다.
노트북의 전원 버튼은 노트북의 상태에따라 노트북을 켤 수도, 끌 수도있다.

🍓 상태 패턴을 코드에 적용하기

상태코드를 블랙잭 미션에 적용해 보겠다.

카드는 Blackjack, Bust, Stay, Hit 4가지의 상태를 가지며
Hit을 제외한 나머지는 게임이 종료된 상태이다.

게임이 종료된 상태라면 플레이어는 보상을 획득 할 수 있고
게임이 진행 중인 상태라면 카드를 뽑을 수 있다.

위와 같이 상태에따라 메서드의 내용이 달라지는데 자세한건 코드에서 설명 하겠다.

  • 위 다이어그램을 보면 모든 상태는 draw(), profit()...등과 같은 기능을 가지는 것을 알 수있다.
    하지만 Finished를 상속하는 Stay,Bust,BlackJack은 draw()를 할 수없으며,
    Running을 상속하는 Hit은 draw()는 가능하지만 profit()은 불가능 하다.
    (위 와같이 상태에 따라 다르게 동작하는 코드들을 if조건문을 사용해 구현하려면 진짜 엄청나게 긴 코드가 필요할 것이다.. 가독성이 떨어지는건 덤)

  • Hit(Running을 상속)은 Stay 와 Bust로 상태 변환이 가능하다.
    하지만 나머지 3개의 상태(Finished를 상속)는 상태의 변환이 불가능 하다.

이제 실제 코드를 보자(불필요 하다고 생각되는 부분은 생략했다.)



  • Interface : 상태들이 공통적으로 가지는 기능을 명시한다.
public interface State {
    State draw(Card card);
    State stay();
    boolean isFinished();
    BunchOfCard getBunchOfCard();
    double profit(double betMoney);
}

  • abstract Class: 게임시작시 2장의 카드를 가지고 시작하기 위해 생성된 클래스
    (카드에 관련된 책임을 가진다.)
public abstract class Started implements State {
    protected final BunchOfCard bunchOfCard;
    protected Started(BunchOfCard bunchOfCard) {
        this.bunchOfCard = new BunchOfCard(bunchOfCard);
    }
    @Override
    public BunchOfCard getBunchOfCard() {
        return this.bunchOfCard;
    }
}

  • abstract Class : 끝난 상태를 표현하기 위한 클래스
    profit()을 얻을 수 있지만 draw(), stay()는 사용이 불가능 하도록 예외처리를 했다.
    (super()를 통해 부모인 Started를 호출한다.)
public abstract class Finished extends Started {
    private static final String CAN_NOT_DRAW_MESSAGE = "게임이 종료되어 카드를 뽑을 수 없습니다.";
    private static final String CAN_NOT_STAY_MESSAGE = "게임이 종료되어 Stay 할 수 없습니다.";
    protected Finished(BunchOfCard bunchOfCard) {
        super(bunchOfCard);
    }
    abstract double earningRate();
    @Override
    public State draw(Card card) {
        throw new IllegalArgumentException(CAN_NOT_DRAW_MESSAGE);
    }
    @Override
    public State stay() {
        throw new IllegalArgumentException(CAN_NOT_STAY_MESSAGE);
    }
    @Override
    public boolean isFinished() {
        return true;
    }
    @Override
    public double profit(double betMoney) {
        return 1 * earningRate();
    }
}

  • Class(State) Stay : 4개의 상태 중 하나
    Stay 상태의 보상은 배팅금액을 그대로 돌려받습니다.
public class Stay extends Finished {
    public Stay(BunchOfCard bunchOfCard) {
        super(bunchOfCard);
    }
    @Override
    double earningRate() {
        return 1;
    }
}

  • Class(State) Bust : 4개의 상태 중 하나
    Bust 상태의 보상은 배팅금액을 그대로 돌려받습니다.
public class Bust extends Finished {
    public Bust(BunchOfCard bunchOfCard) {
        super(bunchOfCard);
    }
    @Override
    double earningRate() {
        return -1;
    }
}

  • Class(State) Hit : 4개의 상태 중 하나
    Hit 상태는 draw(), stay()를 통해 상태를 스스로 변경합니다.
public class Hit extends Running {
    public Hit(BunchOfCard bunchOfCard) {
        super(bunchOfCard);
    }
    @Override
    public State draw(Card card) {
    	//카드를 뽑는다.
        bunchOfCard.addCard(card);
        //카드를 뽑은 후 Bust 판단
        if (bunchOfCard.isBust()) {
            return new Bust(bunchOfCard);
        }
        return new Hit(bunchOfCard);
    }
    @Override
    public State stay() {
        return new Stay(bunchOfCard);
    }
}

🍋 최종 정리

코드를 잘 살펴보면 최대한 코드의 중복을 없애기 위해 abstract Class를 이용해 상속을 활용하는 것을 볼 수있다.
4개의 상태는 Running과 Finished에 따라 다른 기능을 해야하기 때문이다.

처음에는 많이 헷갈렸지만 if조건을 어떻게 하면 최대한 없앨 수 있을까 고민하다보면 전체적인 윤곽이 보일 것 같다.

profile
🖋정리를 안하면 잊어버린다.👣한 발자국씩 가보자!

0개의 댓글