상태 state 패턴

  • 디자인 패턴중 상태 패턴에 대해 정리한다.

상태패턴이란

  • 행동 패턴(Behavioral Pattern)으로 분류
  • 객체지향방식으로 유한 상태 기계를 구현한것입니다.
  • 객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴
    • 객체는 마치 자신의 클래스를 바꾸는 것처럼 보입니다.
  • 사용이유
    • 상태 전이를 위한 조건 로직이 지나치게 복잡한 경우 이를 해소하는 것
      • 상태 전이 로직이란 객체의 상태와 이들 간의 전이 방법을 제어하는 것
    • 각 상태에 대응하는 별도의 클래스를 만들고 상태 전이 로직을 그 클래스들로 옮기는 작업
      • 자신이 직접 상태를 체크하여 상태에 따라 행위를 호출하지 않고, 상태를 객체화 하여 상태가 행동을 할 수 있도록 위임하는 패턴을 말합니다.
  • 하나의 예시로 사람의 돈이 얻는것에 따라 상태가 변화되는 예시입니다.
    • 상태가 Terrible→bad→soso→happy→perfect로 기분이라는 상태의 변화가 있습니다.
      • Terrible은 제일 불쾌, perfect는 제일 행복의 순입니다.
    • 상태가 변화되는것으로 돈을 벌다(earnmoney)와 돈을 잃다(losemoney)가 있다

단순 if로 상태를 관리한다면?

void earnMoney() {
    if (currentState == SoSo) {
         currentState = Happy;
    } else if (currentState == Happy) {
        currentState = Perfect;
        ...
    } else if (currentState == Bad) {
        ...
    }
  • 이러한 기분이라는 상태를 관리하는데 있어 많은 조건문이 들어가고 추가를 하는데 복잡해질 것 입니다.

예시

state라는 기분을 관리하는 하나 만들어줍니다.

public interface State {
    State winLottery(); //로또 당첨

    State earnMoney();//돈을 벌다 -> 기분 상승

    State loseMoney();//돈을 잃다 -> 기분 하락

    State loseWinLottery();

    void printCurrentEmotion();//현재 기분 출력
}

사람이라는 실제 상태가 적용되는 class입니다.

public class Person {
    private State state;

    public Person(State state) {
        this.state = state;//상태를 받습니다.
    }

    public void winLottery() {// 로또가 되었을대
        state = state.winLottery();//상태에서 로또가됐다는 메소드를 실행하고
        state.printCurrentEmotion();//현재 상태를 출력합니다.
    }

    public void earnMoney() {
        state = state.earnMoney();
        state.printCurrentEmotion();
    }

    public void loseMoney() {
        state = state.loseMoney();
        state.printCurrentEmotion();
    }

    public void printCurrentEmotion() {
        state.printCurrentEmotion();
    }
}
  • 따로 상태의 조건이 없이 해당 상태가 되었을대 state의 메소드를 통해 상태가 변화되도록만 되어있습니다.
  • state의 earnMoney, loseMoney등은 어떻게 되었는지 확인해봅시다.

sate 인터페이스의 추상 클래스

public abstract class Emotion implements State {
    @Override // 로또 당첨은 기분이 최상이 됩니다.
    public State winLottery() {
        return new Perfect();
    }

    @Override // 당첨된 로또를 잃어버리면 기분이 최악이 됩니다.
    public State loseWinLottery() {
        return new Terrible();
    }
}
  • 먼저 State에서 로또의 당첨과 잃어버리는것은 무조건 최상의 기분, 최악의 기분으로 나뉘기 때문에 추상클래스에서 한번 거쳐서 해당 메소드를 구현했습니다.

SoSo라는 그저 그런 상태의 클래스입니다.

public class SoSo extends Emotion {
    @Override // 돈을 벌어서 한 단계 기분이 좋아집니다.
    public State earnMoney() {
        return new Happy();
    }

    @Override // 돈을 잃어서 한 단계 기분이 나빠집니다.
    public State loseMoney() {
        return new Bad();
    }

    @Override
    public void printCurrentEmotion() {
        System.out.println("그저그래요.");
    }
}
  • state→Emotion을 거쳐서 SoSo라는 그저그런 상태입니다.
  • earnMoney로 SoSo→Happy로 변화를 시키고, loseMoney로 SoSo→Bad로 변화가 일어나고 있습니다.
  • 이렇듯 해당 객체에서 현재 기분이라는 상태가 변화되도록 하는것이 state패턴입니다.

그외의 기분이라는 상태 클래스들입니다.

public class Perfect extends Emotion {
    @Override // 기분이 변경되지 않습니다.
    public State earnMoney() {
        // 기분이 더 좋아질 수 없기 때문에 예외를 발생시키는 방식을 사용할 수도 있다.
        return this;
    }

    @Override // 돈을 잃어서 한 단계 기분이 나빠집니다.
    public State loseMoney() {
        return new Happy();
    }

    @Override
    public void printCurrentEmotion() {
        System.out.println("최고입니다.");
    }
}
public class Happy extends Emotion {
    @Override // 돈을 벌어서 한 단계 기분이 좋아집니다.
    public State earnMoney() {
      	// 예) 기분이 너무 좋아서 주변 지인들과 파티를 하는 기능 처리
        return new Perfect();
    }

    @Override // 돈을 잃어서 한 단계 기분이 나빠집니다.
    public State loseMoney() {
        return new SoSo();
    }

    @Override
    public void printCurrentEmotion() {
        System.out.println("행복합니다.");
    }
}

public class Bad extends Emotion {
    @Override // 돈을 벌어서 한 단계 기분이 좋아집니다.
    public State earnMoney() {
        return new SoSo();
    }

    @Override // 돈을 잃어서 한 단계 기분이 나빠집니다.
    public State loseMoney() {
      	// 예) 기분이 너무 나빠지며 친한 친구와 술을 먹는 기능 처리
        return new Terrible();
    }

    @Override
    public void printCurrentEmotion() {
        System.out.println("별로입니다.");
    }
}

public class Terrible extends Emotion {
    @Override // 돈을 벌어서 한 단계 기분이 좋아집니다.
    public State earnMoney() {
        return new Bad();
    }

    @Override // 기분이 변경되지 않습니다.
    public State loseMoney() {
        // 기분이 더 나빠질 수 없기 때문에 예외를 발생시키는 방식을 사용할 수도 있다.
        return this;
    }

    @Override
    public void printCurrentEmotion() {
        System.out.println("최악입니다.");
    }
}
  • 기분이라는 상태는 terrible<bad<soso<happy<Perfect순입니다.
  • 돈을 벌면 기분이라는 상태가 변화되는것을 각 class내에서 정하고 있습니다.

해당 상태 변화를 확인해 봅시다.

public class Main {
	public static void main(String[] args) {
		Person person = new Person(new SoSo());
        person.printCurrentEmotion();//그저그래요
        person.earnMoney();//행복합니다
        person.winLottery();//최고입니다.
        person.loseMoney();//행복합니다.
        person.loseMoney();//그저그래요
        person.loseMoney();//별로입니다
        person.loseMoney();//최악입니다
	}
}

  • 이처럼 state라는 person에서 메소드 호출에 따라 상태가 변화되는것을 확인 할 수 있습니다.

만약 새로운 상태를 추가하고 싶다면?

public class Sleepy extends Emotion {
    @Override
    public State earnMoney() {
        return this;
    }

    @Override
    public State loseMoney() {
        return this;
    }

    @Override
    public void printCurrentEmotion() {
        System.out.println("졸려.");
    }
}
  • 위처럼 새로운 상태로 sleepy라는 상태를 추가 한다면 해당 상태 클래스를 정의 하고, 사용하기만 하면 된다.
public static void main(String[] args) {
        Person person = new Person(new Sleepy());
        person.printCurrentEmotion(); // 졸려.
        person.earnMoney(); //졸려.
        person.winLottery(); // 최고입니다.
        person.loseMoney(); //행복합니다.
	}

결론

  • 상태 패턴을 이용하면 상태 패턴 인터페이스의 파생 클래스로서 각각의 상태를 구현함
  • 패턴의 부모클래스에 의해 정의되는 메서드를 호출하여 상태 변화를 구현함으로써 상태 기계를 구현
  • 상태 패턴의 장점은 새로운 상태가 추가되더라도 콘텍스트 코드가 받는 영향은 최소화
  • 클래스를 추가하더라도 기존의 메서드의 코드는 그대로 유지
  • 상태에 따른 동작을 구현한 코드가 상태별로 구분되기 때문에 상태별 동작을 수정하기 쉽다.

참고