2. State Pattern

Created: Oct 15, 2019 10:51 AM
Tags: strategy pattern,상태 패턴,스테이트 패턴

  • 스테이트 패턴(State Pattern)

: 실세계의 많은 객체는 "상태"에 따라 서로 다른 업무를 처리하는 경우가 많다.
이때, 가장 직관적인 방법은 수행하는 행동(메서드)마다 상태를 조건으로 체크해서 조건별로 수행하는 코드를 다르게 작성하는 것이다.
이럴경우 문제는 메서드마다 조건문이 중복으로 들어가게 되고, 추가로 상태코드가 추가될 경우 모든 메서드를 수정해야하며, 가독성 또한 잃게 된다.

이런 문제점을 개선하기 위해 **"변경되는 사항"**인 "**상태**"를 캡슐화하고 해당 상태객체를 관리할 수 있는 인터페이스를 만들어 수행하는 메서드에서 상태에 따른 구체적인 구현을 하는 것이 아닌 메서드별로 상태객체를 관리하는 인터페이스에만 의존하게 되어 상태가 추가되더라고 메서드 내에서 구현이 변경되지 않을 수 있게 된다. 구현은 각 상태 객체가 담당하게 되어 조건식 등이 불필요해 진다.

스테이트 패턴의 필요성을 설명하는 예>

: 다음은 형광등 객체를 구현한 것으로 상태(OFF/ON)에 따라 turnOn메서드와 turnOff메서드의 처리가 다르게 되어 있다.

다행히 메서드가 두개여서 망정이지 더 많은 메서드가 존재시 매 메서드마다 상태를 조건문 분기하며 처리시 가독성도 떨어지고, 상태가 하나 추가되어도 모든 메서드를 전부 수정해야 하는 문제점이 발생한다.

    public class Light {
        private static final int OFF = 0;
        private static final int ON = 1;
        private int state;

        public Light() {
            state = OFF; // 최초 OFF 상태
        }

        public void setState(int state) {
            this.state = state;
        }

        public void turnOn() {
            if(ON == state) {
                System.out.println("이미 켜져있습니다.");
            } else { // OFF 상태
                System.out.println("불이 켜졌습니다.");
                state = ON; // OFF -> ON으로 상태 변경
            }
        }

        public void turnOff() {
            if(OFF == state) {
                System.out.println("이미 불이 꺼져있습니다.");
            } else {
                System.out.println("불이 꺼졌습니다.");
                state = OFF; // ON -> OFF
            }
        }
    }

main

        Light light = new Light(); // state == OFF
            light.turnOff(); 
            light.turnOn();
            light.turnOff();
            /*
            이미 불이 꺼져있습니다.
            불이 켜졌습니다.
            불이 꺼졌습니다.
             * */

만약, 이 예제에서 state가 sleeping(취침모드)가 추가된다면 어떻게 수정될까?

    public class Light {
        private static final int OFF = 0;
        private static final int ON = 1;
        private static final int SLEEPING = 2;
        private int state;

        public Light() {
            state = OFF; // 최초 OFF 상태
        }

        public void setState(int state) {
            this.state = state;
        }

        public void turnOn() {
            if(ON == state) {
                System.out.println("취침모드로 전환합니다.");
                state = SLEEPING;
            } else if(SLEEPING == state) {
                System.out.println("불이 켜졌습니다.");
                state = ON;
            } else { // OFF 상태
                System.out.println("불이 켜졌습니다.");
                state = ON; // OFF -> ON으로 상태 변경
            }
        }

        public void turnOff() {
            if(OFF == state) {
                System.out.println("이미 불이 꺼져있습니다.");
            } else { // ON or SLEEPING
                System.out.println("불이 꺼졌습니다.");
                state = OFF; // ON or SLEEPING -> OFF
            }
        }
    }

main

            Light light = new Light(); // state == OFF
            light.turnOff(); 
            light.turnOn();
            light.turnOn();
            light.turnOff();
            /*
            이미 불이 꺼져있습니다.
            불이 켜졌습니다.
            불이 꺼졌습니다.
             * */

상태가 하나 추가됐을 뿐인데 turnOn과 turnOff모두 조건이 추가되고 if문으로 인해 가독성도 떨어지게 되었다.

스테이트 패턴을 적용해 수정해보자.

상태들을 관리할 State 인터페이스

    public interface State { // State를 관리할 인터페이스
        public void turnOn(Light light);
        public void turnOff(Light light);
    }

On상태 클래스

    public class On implements State{
      // 각 상태별로 메서드를 구현시킴으로 별도의 조건문 분기가 필요치 않다.
        @Override
        public void turnOn(Light light) {
            System.out.println("취침모드로 전환됩니다.");
            light.setState(new Sleeping());

        }

        @Override
        public void turnOff(Light light) {
            System.out.println("불이 꺼집니다.");
            light.setState(new On());
        }

    }

Off상태 클래스

    public class Off implements State{

        @Override
        public void turnOn(Light light) {
            System.out.println("불이 켜집니다.");
            light.setState(new On());

        }

        @Override
        public void turnOff(Light light) {
            System.out.println("이미 불이 꺼져있습니다.");
        }

    }

Sleeping 상태 클래스

    public class Sleeping implements State {

        @Override
        public void turnOn(Light light) {
            System.out.println("불이 켜집니다.");
            light.setState(new On());
        }

        @Override
        public void turnOff(Light light) {
            System.out.println("불이 꺼집니다.");
            light.setState(new Off());
        }

    }

Light 클래스

    public class Light {
        private State state;

        public Light() {
            state = new Off(); // 최초 OFF 상태
        }

        public void setState(State state) {
            this.state = state;
        }

        public void turnOn() {
            state.turnOn(this); // interface인 State에 의존함으로 상태가 추가되어도 코드가 변경될 일이 없다.
            // turnOn메서드가 수행되고 Light객체의 상태를 변경시켜주기 위해 this로 Light객체를 전달한다.
        }

        public void turnOff() {
            state.turnOff(this);
        }
    }

main

            Light light = new Light(); // state == OFF
            light.turnOff(); 
            light.turnOn();
            light.turnOn();
            light.turnOff();
            light.setState(new On()); // 이렇게 중간에 state를 지정해줄 수도 있음
            light.turnOn();
            /*
            이미 불이 꺼져있습니다.
            불이 켜집니다.
            취침모드로 전환됩니다.
            불이 꺼집니다.
            취침모드로 전환됩니다.
             * */

이렇게 state pattern을 적용하게 되면 추가적으로 상태가 추가되더라도 Light 클래스 자체는 더이상 수정이 필요하지 않을뿐 아니라 가독성 또한 좋게 된다.

하지만, 이 코드에서도 약간 아쉬운 점이 있는데 각 상태 메서드 내에서 메서드를 수행하고 상태를 변경하기 위해 setState하는 과정에서 상태 객체를 매번 생성하게 된다.

이는 메모리 낭비하게 되어 성능을 저하시킬 수 있음으로 각 상태 객체를 "싱글톤"으로 수정하도록 하자.

최종 state pattern Ex)

On 클래스

    public class On implements State{

        private static On on = null;

        private On() {} // private로 생성을 제한

        public static On getInstance() {
            if(on == null) {
                on = new On();
            }
            return on;
        }

        @Override
        public void turnOn(Light light) {
            System.out.println("취침모드로 전환됩니다.");
            light.setState(Sleeping.getInstance());

        }

        @Override
        public void turnOff(Light light) {
            System.out.println("불이 꺼집니다.");
            light.setState(On.getInstance());
        }

    }

Off 클래스

    public class Off implements State{

        private static Off off = null;

        private Off() {}

        public static Off getInstance() {
            if(off == null)
                off = new Off();

            return off;
        }

        @Override
        public void turnOn(Light light) {
            System.out.println("불이 켜집니다.");
            light.setState(On.getInstance());

        }

        @Override
        public void turnOff(Light light) {
            System.out.println("이미 불이 꺼져있습니다.");
        }

    }

Sleeping  클래스

    public class Sleeping implements State {

        private static Sleeping sleeping = null;

        private Sleeping() {}

        public static Sleeping getInstance() {
            if(sleeping == null)
                sleeping = new Sleeping();

            return sleeping;
        }

        @Override
        public void turnOn(Light light) {
            System.out.println("불이 켜집니다.");
            light.setState(On.getInstance());
        }

        @Override
        public void turnOff(Light light) {
            System.out.println("불이 꺼집니다.");
            light.setState(Off.getInstance());
        }

    }

Light 클래스

    public class Light {
        private State state;

        public Light() {
            state = Off.getInstance(); // 최초 OFF 상태
        }

        public void setState(State state) {
            this.state = state;
        }

        public void turnOn() {
            state.turnOn(this); // interface인 State에 의존함으로 상태가 추가되어도 코드가 변경될 일이 없다.
            // turnOn메서드가 수행되고 Light객체의 상태를 변경시켜주기 위해 this로 Light객체를 전달한다.
        }

        public void turnOff() {
            state.turnOff(this);
        }
    }

main

            Light light = new Light(); // state == OFF
            light.turnOff(); 
            light.turnOn();
            light.turnOn();
            light.turnOff();
            light.setState(On.getInstance()); // 이렇게 중간에 state를 지정해줄 수도 있음
            light.turnOn();
            /*
            이미 불이 꺼져있습니다.
            불이 켜집니다.
            취침모드로 전환됩니다.
            불이 꺼집니다.
            취침모드로 전환됩니다.
             * */

스테이트 패턴은 기본 골격 구성이 스트레티지 패턴(전략패턴)과 같은데 둘의 차이점은

스트레티지 패턴 ⇒ 자주 변동사항이 생기는 메서드 및 항목을 캡슐화

스테이트 패턴 ⇒ 조건 상태를 캡슐화

하는게 차이점이다.

+ Recent posts