디자인 패턴(구)/행위 패턴

상태(state) 패턴이란?

공대키메라 2022. 4. 26. 23:29

1. 상태(state) 패턴이란?

의도

객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴. 이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보인다. 

사용 시기

  • 객체의 행동이 상태에 따라 달라질 수 있고, 객체의 상태에 따라서 런타임에 행동이 바뀌어야 할 때
  • 어떤 연산에 그 객체의 상태에 따라 달라지는 다중 분기 조건 처리가 너무 많이 들어 있을 때

구조

  • Context : 사용자가 관심있는 인터페이스를 정의. 객체의 현재 상태를 정의한 ConcreteState 서브클래스의 인스턴스를 유지 및 관리
  • State : Context의 각 상태별로 필요한 행동을 캡슐화하여 인터페이스로 정의
  • ConcreteState 들 : 각 서브 클래스들은 Context의 상태에 따라 처리되어야 할 실제 행동을 구현

2. 구현

다음 예시는 해당 사이트에서 참고하여 가져온 것이다.

궁금한 분은 직접 들어가서 보자!

 

출처 : https://www.newthinktank.com/2012/10/state-design-pattern-tutorial/

구현(Implementation) 동화

키메라는 은행에 볼일이 있어서 점심시간에 은행에 들렀다.

ATM기에서 돈을 뽑으려고 한다.

돈이 있는 카드를 넣고, pin 넘버를 입력한다.

그리고 잔액을 확인 후, 돈을 뽑는다.

마지막으로, 돈과 함께 카드를 뽑아가면 끝이다.

 

https://www.youtube.com/watch?v=MGEx35FjBuo

 

이 구조에서 다른것은 State 패턴에서 어느 구조와 매칭이 되는지 알 수 있다.

 

ATMMachine은 Context에 해당될 것이다.

ATMState.java

public interface ATMState {
    void insertCard();
    void ejectCard();
    void insertPin(int pinEntered);
    void requestCash(int cashToWithdraw);
}

 

ATM기의 큰 기능은 4가지로 나뉜다. 카드를 넣고,  pin번호를 입력하고, 돈을 인출하고, 그리고 카드를 뽑는 것이다.

ATMMachine.java

public class ATMMachine {

    ATMState hasCard;
    ATMState noCard;
    ATMState hasCorrectPin;
    ATMState atmOutOfMoney;

    ATMState atmState;

    int cashInMachine = 2000;
    boolean correctPinEntered = false;

    public ATMMachine() {
        hasCard = new HasCard(this);
        noCard = new NoCard(this);
        hasCorrectPin = new HasPin(this);
        atmOutOfMoney = new NoCash(this);

        atmState = noCard;

        if(cashInMachine < 0){
            atmState = atmOutOfMoney;
        }
    }

    void setAtmState(ATMState newATMState) {
        atmState = newATMState;
    }

    public void setCashInMachine(int newCashMachine) {
        cashInMachine = newCashMachine;
    }

    public void insertCard(){
        atmState.insertCard();
    }

    public void ejectCard(){
        atmState.ejectCard();
    }

    public void requestCash(int cashToWithdraw){
        atmState.requestCash(cashToWithdraw);
    }

    public void insertPin(int pinEntered){
        atmState.insertPin(pinEntered);
    }

    public ATMState getYesCardState() {return hasCard;}
    public ATMState getNoCardState() {return noCard;}
    public ATMState getHasPin() {return hasCorrectPin;}
    public ATMState getNoCashState() {return atmOutOfMoney;}

}

Context의 역할을 하는 ATMMachine.java를 생성했다. 다시 Context의 역할을 떠올려보면, 객체의 현재 상태를 정의한 ConcreteState 서브클래스의 인스턴스를 유지 및 관리하는 것이다.

 

이 곳에서 상태가 변할 때 마다 인스턴스를 관리해주고 있다.

 

현재 ATM기의 상태는 총 4가지이다.

 

카드를 가지고 있거나, 카드가 없거나, pin번호가 일치하거나, 돈이 바닥났을 때이다.

 

setATMState를 통해 각각의 상태에 대해 세팅을 해준다. 

 

그렇게 되면 우리는 State를 구현한 4개의 ConcreteState가 있지만, 각각의 세부적인 구현체에 따라서 기능이 다르게 작동한다!

HasCard.java

public class HasCard implements ATMState {

    ATMMachine atmMachine;

    public HasCard(ATMMachine atmMachine) {
        this.atmMachine = atmMachine;
    }

    @Override
    public void insertCard() {
        System.out.println("You can't enter more than one card");
    }

    @Override
    public void ejectCard() {
        System.out.println("Card Ejected");
        atmMachine.setAtmState(atmMachine.getNoCardState());
    }

    @Override
    public void insertPin(int pinEntered) {
        if(pinEntered == 1234){
            System.out.println("Correct PIN!");
            atmMachine.correctPinEntered = true;
            atmMachine.setAtmState(atmMachine.getHasPin());
        } else{
            System.out.println("Wrong PIN!");
            atmMachine.correctPinEntered = false;
            System.out.println("Card Ejected!");
            atmMachine.setAtmState(atmMachine.getNoCardState());
        }
    }

    @Override
    public void requestCash(int cashToWithdraw) {
        System.out.println("Enter PIN First!");
    }
}

 

카드가 있는 경우에 벌어질 일들이 구현되어있다.

 

  • 카드가 있는데 카드를 넣으려 할 때(insertCard() 메소드)는 다시 넣을수가 없다.
  • 카드가 꼽혀 있는데 뽑는다면 뽑은과 동시에 상태가 변경된다(getNoCardState)
  • 카드가 꼽혀 있다면 pin을 입력해서 돈을 빼낼 수 있도록 getHasPin상태로 변경된다.
  • 카드만 꼽힌 상태에서 현금을 인출하려하면 PIN을 입력하라는 메세지가 출력된다.

HasPin.java

public class HasPin implements ATMState {

    ATMMachine atmMachine;

    public HasPin(ATMMachine atmMachine) {
        this.atmMachine = atmMachine;
    }

    @Override
    public void insertCard() {
        System.out.println("You can't enter more than one card");
    }

    @Override
    public void ejectCard() {
        System.out.println("Card Ejected");
        atmMachine.setAtmState(atmMachine.getNoCardState());
    }

    @Override
    public void insertPin(int pinEntered) {
        System.out.println("Already Entered Pin");
    }

    @Override
    public void requestCash(int cashToWithdraw) {
        if(cashToWithdraw > atmMachine.cashInMachine){
            System.out.println("Don't Have that Cash");
            System.out.println("Card Ejected");
            atmMachine.setAtmState(atmMachine.getNoCardState());
        } else {
            System.out.println(cashToWithdraw + " is provided by the machine");
            atmMachine.setCashInMachine(atmMachine.cashInMachine - cashToWithdraw);

            System.out.println("Card Ejected");
            atmMachine.setAtmState(atmMachine.getNoCardState());

            if(atmMachine.cashInMachine <= 0) {
                atmMachine.setAtmState(atmMachine.getNoCashState());
            }

        }
    }
}

 

카드가 꼽혀 있고, 적절한 pin번호를 입력하게 되면 ATMMachine의 상태는 hasPin 상태로 변경된다. 

 

  • 카드를 더 꼽으려면, 이미 꼽혀있기에 하나만 꼽을 수 있다는 메시지를 출력한다. 
  • 카드를 빼내면 충분히 가능하다. 
  • pin 번호를 입력하려면 이미 입력이 되어있기에 이미 입력됐다는 메시지를 출력한다. 
  • 현금을 뽑으려면 현재 현금 보유량에 따라서 출력을 해준다. 

NoCard.java, NoCash.java도 같은 방식으로 작동한다. 

NoCard.java

public class NoCard implements ATMState {
    ATMMachine atmMachine;
    public NoCard(ATMMachine atmMachine) {
        this.atmMachine = atmMachine;
    }

    @Override
    public void insertCard() {
        System.out.println("Please Enter a Pin");
        atmMachine.setAtmState(atmMachine.getYesCardState());
    }

    @Override
    public void ejectCard() {
        System.out.println("Enter a card First");
        atmMachine.setAtmState(atmMachine.getYesCardState());
    }

    @Override
    public void insertPin(int pinEntered) {
        System.out.println("Enter a card First");
    }

    @Override
    public void requestCash(int cashToWithdraw) {
        System.out.println("Enter a card First");
    }
}

NoCash.java

public class NoCash implements ATMState {
    ATMMachine atmMachine;

    public NoCash(ATMMachine atmMachine) {
        this.atmMachine = atmMachine;
    }

    @Override
    public void insertCard() {
        System.out.println("We don't have money");
    }

    @Override
    public void ejectCard() {
        System.out.println("We don't have money and you didn't enter a card");
    }

    @Override
    public void insertPin(int pinEntered) {
        System.out.println("We don't have money");
    }

    @Override
    public void requestCash(int cashToWithdraw) {
        System.out.println("We don't have money");
    }

}

TestATMMachine.java

public class TestATMMachine {

    public static void main(String[] args) {
        ATMMachine atmMachine = new ATMMachine();

        atmMachine.insertCard();

        atmMachine.ejectCard();

        atmMachine.insertCard();

        atmMachine.insertPin(1234);

        atmMachine.requestCash(2000);

        atmMachine.insertCard();

        atmMachine.insertPin(1234);
    }

}

 

전체적으로 보면 어느 특정한 틀이 정해지면, 그 틀 안에서 우리가 충분히 예측 가능한 상황의 상태 class로 전환이 가능하다. 굉장히 흥미로운 패턴이다.

 

이렇게 한다면 모든 상황에 대해서 우리가 스스로 제어할 수 있는 프로그램을 만들 수 있다!

 

3. 고찰

자, 그럼 구조에 대해 다시 살펴보자. 

구조

이번 예제는 굉장히 본연의 구조와 잘 맞다. 

 

Context는 State를 집합으로 관리하고 있다. 그리고 State는 Context와 연관관계에 있다.

 

실제로도 Context에 해당하는 ATMMachine 내부에는 여러 State가 존재한다. 

 

그 State에 원하는 ConcreteState를 넣어주는 것 뿐이다.

장점

  • 상태에 따른 동작을 개별 클래스로 옮겨서 관리할 수 있다.
  • 기존의 특정 상태에 따른 동작을 변경하지 않고 새로운 상태에 다른 동작을 추가할 수 있다.
  • 코드 복잡도를 줄일 수 있다. 

단점

  • 복잡도가 증가한다.

결론만 본다면 에이~ 남의 코드 배꼇네~ 할 수 있는데... 

 

사실 맞는 말이다 올 ㅋ...(머쓱 ^^;;)

 

어떤 예제를 직접 생각해서 내것으로만 만드는것이 너무 어려워서 학습을 한다는 취지로 분석하고 적었다.

 

이상한 점이나 피드백을 주고 싶으면 언제든 환영한다.