본문 바로가기
개발자 준비/디자인패턴 & 아키텍쳐

스테이트 패턴 (State Pattern)

by osul_world 2021. 12. 30.
728x90

스테이트 패턴 ( State Pattern )


컴퓨터의 전원 버튼을 생각해보자

컴퓨터가 꺼져있는경우 버튼을 누르면 컴퓨터가 켜진다. 반대로 컴퓨터가 켜진경우 버튼을 누르면 컴퓨터가 꺼진다.

if state on: 
	turn off
	state off
if state off: 
	turn on
	state on

위 처럼 켜진경우 전원을 끄고 상태를 꺼진 상태로 바꿔주고 꺼진경우 전원을 켜고 상태를 켜짐으로 바꾸는 코드로 구현이 가능하다.

 

만약 여기에 버튼 2번을 빠르게 누르면 절전모드가 되는 등 100가지 경우의 수가 더 생긴다고 가정해보자.

조건문은 무수히 많아질것이고 변경사항이 생길때마다 어려움에 봉착해야한다.

 

상태 패턴은 상태에 따라 다양한 행위가 되어야 할때 사용하는 패턴이다.

image

  • 객체의 행위가 현재 상태에 따라 변화가 필요한 경우
  • 객체의 여러 연산이 객체 상태에 의존하는 다중 조건문으로 구성된 경우

 

상태 패턴은 "상태중심전이" 와 "문맥중심전이" 로 나뉜다.

 

상태중심 전이

상태 전이가 상태 객체 내부에서 이루어 진다.

상태 변경

Context객체(==클라이언트)

public class Context { //Computer라고 생각하자
    private final State stateON = new StateON(this);
    private final State stateOFF = new StateOFF(this);
    //상태를 더 추가할수있다. 확장성
    
    private State current = stateON; //초기상태 ON
    
    public void changeState(State state){ 
    	current = state;
    }
    
    public void request(){ //button
    	current.handle();
    }
   
    //getter
}

State 객체

//StateON 상태
public class StateON implements State { //State interface implements
    private Context context; //context 맴버로 유지

    public StateON(Context context){
        this.context = context;
    }
    
    /**
	상태 객체에서 상태 전이 및 행위 처리는 문맥객체가 제공하는 메소드(changeState())를 이용하여 처리함
	button이 눌리면 종료후 off로 상태변경
	*/
    public void handle(){
        print("OFF");//종료 로직
        context.changeState(context.getStateOFF()); //상태 전이가 상태객체 내부에서 이루어짐! 상태중심전이!
        
        // context.changeToStateOFF(); //Context 객체에 상태변경 메서드를 각각 정의해두고 상태 객체에서 편            리하게 사용하도록 할수도있다.
    }
}

초기 상태는 StateON이다. request() 즉, 버튼을 누르면 StateON객체의 handle이 실행될것이고 컴퓨터를 종료하고 상태를 종료상태로 바꾼다(상태전이)

 

이렇게 상태에 따라 로직 처리가 달라야 하는 경우가 많은 경우 상태 패턴을 사용할수있다.

 

위 코드를 보면 State객체를 생성할때 Context 객체를 주입한다. StateOn은 Context하고 1:1로 묶여버렸다.

상태객체를 공유할수가 없다.

그런데 다른 Context들도 StateOn을 사용하는 경우가 있지 않을까?

그럼 상태객체를 공유하여 여러 Context들이 사용할수있도록 상태패턴을 업그레이드 하여보자.

 

상태 객체를 공유하여 여러 context 객체들이 사용할수있도록 설계하기

public class Context {
    // private static final State stateA = new StateA();
    private State current = StateA.getInstance(); //single-ton 객체 get
    
    public void changeState(State state){
    	current = state;
    }
    
    public void request(){
    	current.handle(this);//this를 State에게 넘겨줌
    }
}
public class StateA implements State {
    
    //single-ton 내부 클래스 이용 구현,private 외부 생성 불가
    private static class Holder{
    	private static final StateA unique = new AState(); 
    } 
    public static StateA getInstance() { return unique; } //static class로 생성한 유일 객체를 반환한다.
    
    
   /**
   context를 맴버변수로 유지하지 않고 handle에게 param으로 넘겨주는 식
   */
    public void handle(Context context){
        // do something
        context.changeState(StateB.getInstance());
    }
}

이전 예제와 다르게 이 예제는 context를 맴버변수로 유지하지 않고 handle메서드가 실행될때 param으로 넘겨주는 식으로 설계되어있다.

문맥객체를 멤버변수로 유지하지 않고 문맥객체를 param으로 넘겨받음

이렇게 하면 state 객체를 다른 context객체와 공유해서 사용할수도 있다.

확장적으로 사용할수있다.

같은 상태 객체를 공유해서 사용할수있기 때문에 상태 객체를 싱글톤으로 구현하여 리소스 효율을 높일수도있다.

 

문맥중심 전이

지금 까지 방법들 처럼 상태객체에서 상태 전이를 하는 방식을 상태 중심 전이 방식이라고 한다. 반대로 문맥객체(context)에서 상태전이를 하는 방식을 문맥중심 전이 방식이라고 한다.

문맥 중심은 상대적으로 문맥 클래스의 구현이 더 복잡하며, 상태가 추가되었을 때 상대적으로 더 많은 문맥 클래스의 수정이 필요할 수 있다.

적절한 방식은 아니니 그냥 이렇게 할수도있구나 하면된다.

public class Context {
 private State current = StateA.getInstance();

 /**
 current를 상태객체 return으로 변경 -> 상태전이: 문맥중심전이
 */
 public void request(){
   current = current.handle(); //상태 전이 담당
   //current = current.handle(this); //문맥객체를 넘겨 활용할수도있다.
 }
}
//싱글톤으로 다음 상태 객체를 반환
public class StateA implements State {

private static class Holder{
	private static final StateA unique = new StateA();
} 

public static StateA getInstance() { 
		return Holder.unique; 
}

private StateA() {}

public State handle(){
	return StateB.getInstance(); //변경할 상태 singleton 객체 return
	}
}

상태 객체는 변경할 상태를 반환값으로 문맥객체에게 알려주고 문맥객체에서 반환값을 가지고 current를 변경하여 상태전이를 담당한다는걸 알수있다.

 

아래와 같은 방법도 있다.

public class Context {
 private static final State stateA = new StateA();
 …
 private static final State stateN = new StateN();

 private State current = stateA;

 public void request(){
     if(current.handle())
     	current = stateB; //상태전이
 }
}

//상태 전이 여부를 나타내는 boolean 값 반환
public class StateA implements State {
 public boolean handle(){
     return true; //전이가 일어나야 하는가?
 }
}

 

문맥 중심 전이 vs. 상태 중심 전이

상태 전이를 누가 담당하냐로 구분할수있다.

상태중심 전이

공유가능하도록 설계하지 않으면 공유가 불가능하다.

문맥객체를 멤버변수로 유지하지 않고 param으로 넘겨받으면 공유 가능

상태전이를 완전히 정의하기때문에 상태전이를 이해하기 쉽다.

문맥중심 전이

상태 객체의 공유가 항상 가능하다.

공유가 가능하기 때문에 형태만 정의가 되어있어 상태전이를 이해하기 힘들다.

받아온 객체를 가지고 전이를 진행하기때문

 

열거형 이용하기

상태객체가 단순한 경우 여러 객체를 정의할 필요없이 열거형을 이용할수있다.

상태중심전이

public class Context {    private State current = State.STATE_A;    //상태전이 method    protected void changeStateToSTATE(State state){        current = state;    }    public void request(){        current.handle(this);    }}
//열거형을 활용해서 여러 상태를 한번에 관리 가능하다.public enum State {    STATE_A{        @Override        public void handle(Context context){            //do something            context.changeStateToSTATE(STATE_B);        }        private void secretMethod(){            System.out.println("A사유 메서드");        }    },    STATE_B{        @Override        public void handle(Context context){            //do something            context.changeStateToSTATE(STATE_A);        }        private void secretMethod(){            System.out.println("B사유 메서드");        }    };    public abstract void handle(Context context); //! 상위의 개념이 됨}

문맥중심전이

public class Context {    private State current = State.STATE_A;    public void request(){        current = current.handle(); //상태 전이    }}
public enum State {    STATE_A {        @Override        public State handle() {            //do something            return STATE_B; //변경할 상태 반환        }    },    STATE_B {        @Override        public State handle() {            //do something            return STATE_A; //변경할 상태 반환        }    };    public abstract State handle();}

 

실습

실전에서는 더 많은 메서드 들을 정의해야 할것이다.

아래 예제는 문맥중심전이 이다.

public enum State {    STATE_A {        @Override        public State do1() {            //do something            return this; //상태 유지        }                @Override        public State do2() {            //do something            return this;         }                @Override        public State do3() {            //do something            return STATE_B; //상태 전이        }    },    STATE_B {        @Override        public State do1() {            //do something            return this;         }                @Override        public State do2() {            //do something            return STATE_A;         }                @Override        public State do3() {            //do something            return this;         }    };    //정의 해야하는 메서드들    public abstract State do1();	public abstract State do2();	public abstract State do3();}

정의한 메서드마다 각각 알맞는 상태를 반환한다.

상태 중심은 반환 없이 context.changeState() 등의 전이 메서드를 이용하면 된다.

 

 

[중요] 전략 패턴과 비교

전략 패턴은 한 메소드의 전략을 동적으로 바꾸는 것이며, 외부에서 이를 원할 때 바꾸는 것이다.

반면, 상태 패턴은 한 메소드의 전략으로 제한되지 않으며, 스스로 상태 전이가 이루어 진다. (전략 패턴에 비해 수시로 바뀜)

 

리펙토링

단순 간결화가 가능하면 굳이 상태패턴으로 파일을 늘릴필요는 없다 조건문 구조를 쓸땐 쓰자

 

상태 패턴 장점

확장성: 새 상태 클래스를 정의하여 새로운 상태를 쉽게 추가할 수 있음

가독성: 상태 전이가 명백해짐

독립성: 각 상태가 다른 클래스로 독립적 모델링

은닉성: 상태 객체의 내부를 사용자가 접근할수없도록 private하게 숨길수있다.

상태 패턴 단점

클래스가 매우 많아질 수 있음 (상태마다 하나)

간결한 조건의 경우 enum을 사용하는 것이 더 효과적일 수 있음

 

부가 정보

문맥 객체를 맴버로 유지하지 않는 상태객체를 순수상태 라고 한다.

공유가 가능한 상태객체

버튼 더블 클릭 상태를 클릭 상태를 이용해 설계할수있겠지만 별도로 분리하는것이 옳다.

두 가지 상태(유사하기 때문에)를 하나의 상태로 모델링하는 것은 바람직하지 않음 (SRP, 가독성, 나중에 변경 필요성 측면)

 

728x90

'개발자 준비 > 디자인패턴 & 아키텍쳐' 카테고리의 다른 글

리펙토링  (0) 2021.12.30
프록시 패턴(Proxy Pattern)  (0) 2021.12.30
반복자 패턴 & 합성 패턴  (0) 2021.12.30
템플릿 메소드 패턴  (0) 2021.12.29
어뎁터 패턴 & 파사드패턴  (0) 2021.12.29