본문 바로가기
아이폰 개발/디자인 패턴

swift 디자인 패턴 - 상태패턴(스테이트 패턴)

by 인생여희 2022. 11. 3.

swift 디자인 패턴 - 상태패턴(스테이트 패턴)

 

목표

1.상태를 캡슐화로 처리하는 방법 이해하기

2.스태이트 패턴을 통한 상태 변화의 처리 방법 이해하기

3.새로운 상태를 추가할 수 있는 방법 이해하기

 

  • 실세계의 많은 시스템은 다양한 상태가 있고 상태에 따라 다른 행위를 한다. 
  • 상태란 객체가 시스템에 존재하는 동안, 즉 객체의 라이프 타임 동안 객체가 가질 수 있는 조건이나 상황을 표현한다.

예) 객체가 어떤 상태에 있는 동안 어떤 액티비티 등을 수행하거나 특정 이벤트가 발생하기를 기다리는 것.

 

형광등 만들기

작동방식

형광등이 꺼져 있을 때 외부에서 On 버튼을 누르면 형광등이 켜지고, 형광등이 켜져 있을 때 Off 버튼을 누르면 꺼진다.

 

만약 이미 형광등이 켜져 있는 상태에서 On 버튼을 누르면 형광등 상태는 그대로 켜져있고, 꺼져있는 상태에서 Off 버튼을 눌러도 형광등의 상태에는 아무런 변화가 없다.

 

OFF 와 ON 은 형광등의 상태를 나타낸다.

on_button_pushed 와 off_button_pushed는 각각 형광등 Off 버튼과 On 버튼이 눌러졌음을 나타낸다.

 

 

형광등의 각 상태를 표현하는 상수 정하기

 

현재 형광등의 상태를 저장하는 변수 만들기

이 변수는 위에서 선언한 ON이나 OFF 상태값을 가진다.

[코드]

class Light{
    
    static var ON = 0   //형광등이 켜진 상태
    static var OFF = 1  //형광등이 꺼진 상태
    var state = 1       //형광등의 현재 상태
   
    func on_button_pushed(){
        if self.state == Light.ON {
            print("반응 없음")
        }else{
            print("Light On !")
            state = Light.ON
        }
    }
    
    func off_button_pushed(){
        if self.state == Light.OFF {
            print("반응 없음")
        }else{
            print("Light off !")
            state = Light.OFF
        }
    }
}

let light = Light()
light.off_button_pushed()
light.on_button_pushed()
light.off_button_pushed()

//[출력]
//반응 없음
//Light On !
//Light off !

 

문제점

형광등에 새로운 상태를 추가할 때! 예를 들어 형광등에 ‘취침등’ 상태를 추가하려면 어떻게 해야 될까?

 

형광등이 켜져있을 때 On 버튼을 누르면 원래는 켜진 상태 그대로였지만 지금은 ‘취침등’(SLEEPING) 상태로 변경된다.

 

취침등 상태에 있을 때 On 버튼을 누르면 형광등이 다시 켜지고 Off 버튼을 누르면 꺼지게 된다.

 

 

이렇게 추가된 요구사항이 있을 때 코드는 어떻게 확장되어야 하나?

 

우선 취침등 상태를 나타내는 상수인 SLEEPING을 추가한다.

그리고 추가된 취침등 상태 값에서 on_button_pushed 상태와 off_button_pushed 상태를 어떻게 처리하는가를 코딩해야한다.

 

따라서 on_button_pushed와 off_button_pushed 메서드 안에서 현재 상태 값이 SLEEPING 변수 값과 같은지를 검사하고 값이 같다면 요구사항에 맞게 적절한 행위를 하도록 코딩한다.

 

예를 들어 on_button_pushed 상태는 아래와 같이 변경된다.

 

 

[코드]

//수정후
class Light{
    
    static var ON = 0           //형광등이 켜진 상태
    static var OFF = 1          //형광등이 꺼진 상태
    static var SLEEPING = 2;    //취침등 상태
    var state = 1               //형광등의 현재 상태
    
    func on_button_pushed(){
        
        //형광등이 켜져 있는 경우에 On 버튼을 누르면 취침등 상태로 전환
        if self.state == Light.ON {
            
            print("취침등 상태")
            state = Light.SLEEPING
            
        //형광등이 취침등 상태에 있는 경우 On 버튼을 누르면 켜진 상태로 전환됨
        }else if self.state == Light.SLEEPING {
            
            print("Light On !")
            state = Light.ON
        
        //꺼져 있는 상태라면 On 버튼을 눌렀을 때 켜진 상태로 전환됨
        }else{
            
            print("Light On !")
            state = Light.ON
        }
    }
    
    

    func off_button_pushed(){
        
        if self.state == Light.OFF {
            
            print("반응 없음")
            
        }else if self.state == Light.SLEEPING{
            
            print("Light off !")
            state = Light.OFF
            
        }else{
            print("Light off !")
            state = Light.OFF
        }
    }
    
}

let light = Light()
light.off_button_pushed()
light.on_button_pushed()
light.off_button_pushed()
light.on_button_pushed()
light.on_button_pushed()

//반응 없음
//Light On !
//Light off !
//Light On !
//취침등 상태

 

상태 진입이 복잡한 조건문에 내포된 지금의 코드 구조는 현재 시스템의 상태 변화를 파악하기에 용이하지 않다.

 

그리고 새로운 상태가 추가되는 경우에 상태 변화를 초래하는 모든 메서드에 이를 반영하기 위해 코드를 수정해야만 한다.

 

Key point 복잡한 조건문에 상태 변화가 숨어 있는 경우 상태 변화가 어떻게 이루어지는지 이해하기가 어렵다. 또한 새로운 상태 추가에 맞춰 모든 메서드를 수정해야 한다.

 

해결책

 

"이번에도 무엇이 변하는가를 찾아야한다. 변하는 부분을 찾아서 이를 캡슐화하는 것이 매우 중요하다."

목표는 현재 시스템이 어떤 상태에 있는지와 상관없게 구성하고 상태 변화에도 독립적이도록 코드를 수정해야 한다.

 

"이를 위해서는 상태를 클래스로 분리해 캡슐화하도록한다."

 

"또한 상태에 의존적인 행위들도 상태 클래스에 같이 두어 특정 상태에 따른 행위를 구현하도록 바꾼다."

 

이렇게 하면 상태에 따른 행위가 각 클래스에 국지화되어 이해하고 수정하기가 쉽다.

 

 

 

스트래티지 패턴과 구조가 비슷하면서 동일하다. Light 클래스에서 구체적인 상태 클래스가 아닌 추상화된 State 인터페이스만 참조하므로 현재 어떤 상태에 있는지와 무관하게 코드를 작성할 수 있다.

 

Light 클래스에서는 상태 클래스에 작업을 위임만 하면 된다.

 

Light 클래스의 상태인 ON과 OFF 클래스를 캡슐화해 State 인터페이스를 구현했다.

 

각 상태 클래스만 보더라도 각 상태에 따라 on_button_pushed와 off_button_pushed 메서드가 어떻게 실행되는지 쉽게 알 수 있다.

 

또한 상태 진입도 각 상태에서 처리하므로 if 문이나 switch 문을 사용해 상태 변화를 나타낼 필요가 없다.

 

[스테이트 패턴으로 작성된 코드]

 //스테이트 패턴으로 구현
protocol State{
    func on_button_pushed(light:Light)
    func off_button_pushed(light:Light)
}


class ON: State{
    func on_button_pushed(light: Light) {
        print("반응없음")
    }
    
    func off_button_pushed(light: Light) {
        print("Light off")
        light.setState(state: OFF())
    }
}


class OFF: State{
    func on_button_pushed(light: Light) {
        print("Light on")
        light.setState(state: ON())
    }
    
    func off_button_pushed(light: Light) {
        print("반응없음")
    }
}


class Light{
    private var state:State = OFF()
    
    
    func setState(state:State){
        self.state = state
    }
    
    func on_button_pushed() {
        state.on_button_pushed(light: self)
    }
    
    func off_button_pushed() {
        state.off_button_pushed(light: self)
    }
}


let light = Light()
light.off_button_pushed()
light.on_button_pushed()
light.off_button_pushed()
light.on_button_pushed()
light.on_button_pushed()
 
 //반응없음
 //Light on
 //Light off
 //Light on
 //반응없음

 

Light 클래스의 state 변수를 통해서 현재 시스템의 상태 객체를 참조한다.

 

상태에 따른 행위를 수행하려면 state 변수가 참조하는 상태 객체에 작업을 위임해야 한다.

 

Light 클래스안에는 구체적인 상태를 나타내는 객체를 참조하지 않는다.

 

즉, Light 클래스는 시스템이 어떤 상태에 있는지와 무관하다.

 

따라서 상태가 새로운 상태로 교체되더라도 Light 클래스는 영향을 받지 않는다.

 

참고로 상태 변화가 생길때마다 새로운 객체를 생성하면 메모리 낭비와 성능 저하를 가져 올 수 있다. 대부분 상태 객체는 한 번만 생성해도 충분하다. 따라서 객체를 하나만 만들 수 있는 방법인 싱글턴 패턴을 이용해서 상태 클래스를 만들자.

 

[스태이트 패턴과 싱글톤 패턴을 이용해서 작성한 코드]

//스테이트 패턴 + 싱글톤 패턴으로 구현
protocol State{
    func on_button_pushed(light:Light)
    func off_button_pushed(light:Light)
}

//MARK: 켜짐 상태
class ON: State{
    
    //싱글턴
    static var sharedInstance = ON()
    private init(){}
    
    // 조명이 켜져있을때 on 스위치를 누르면
    func on_button_pushed(light: Light) {
        print("SLEEPING on")
        light.setState(state: SLEEPING.sharedInstance)
    }
    
    // 조명이 켜져있을때 off 스위치를 누르면
    func off_button_pushed(light: Light) {
        print("Light off")
        light.setState(state: OFF.sharedInstance)
    }
}

//MARK: 꺼짐 상태
class OFF: State{
    
    //싱글턴
    static var sharedInstance = OFF()
    private init(){}
    
    func on_button_pushed(light: Light) {
        print("Light on")
        light.setState(state: ON.sharedInstance)
    }
    
    func off_button_pushed(light: Light) {
        print("반응없음")
    }
}

//MARK: 취침등 상태
class SLEEPING: State{
    
    //싱글턴
    static var sharedInstance = SLEEPING()
    private init(){}
    
    //취침모드 일때 스위치를 키면
    func on_button_pushed(light: Light) {
        print("Light on")
        light.setState(state: ON.sharedInstance)
    }
    
    //취침모드일때 스위치를 종료하면
    func off_button_pushed(light: Light) {
        print("Light off")
        light.setState(state: OFF.sharedInstance)
    }
}

class Light{
    
    private var state:State = OFF.sharedInstance
    
    func setState(state:State){
        self.state = state
    }
    
    func on_button_pushed() {
        state.on_button_pushed(light: self)
    }
    
    func off_button_pushed() {
        state.off_button_pushed(light: self)
    }
}

let light = Light()
light.off_button_pushed()
light.on_button_pushed()
light.off_button_pushed()
light.on_button_pushed()
light.on_button_pushed()

/*
 반응없음
 Light on
 Light off
 Light on
 SLEEPING on
 */

 

스테이트 패턴

실세계의 많은 개체는 자신이 처한 상태에 따라 일을 다르게 수행한다.

 

비오는 길을 걸을때나 눈이 내리는 길을 걸을때가 다른 것처럼 말이다.

 

이를 표현하는 가장 직관적인 방법은 일을 수행할 때의 상태에 따라서 상태 하나하나가 어떤 상태인지 검사해서 일을 다르게 수행하는 것이다.

 

이런 방식은 조건식이 있는 코드를 산출할 것이고, 결과적으로 코드를 이해하거나 수정하기 어렵게 만든다.

 

그래서 스테이트 패턴은 어떤 행위를 수행할 때 상태에 행위를 수행하도록 위임한다.

 

이를 위해서 스테이트 패턴에서는 시스템의 각 상태를 클래스로 분리해 표현한다.

 

또, 각 클래스에서 수행하는 행위들을 메서드로 구현한다.

 

그리고 이러한 상태들을 외부로 부터 캡슐화하기 위해서 인터페이스를 만들어서 시스템의 각 상태를 나타내는 클래스로 하여금 실체화하게 한다.

 

스테이트 패턴에서 나타나는 역할이 수행하는 작업은 아래와 같다.

 

State: 시스템의 모든 상태에 공통의 인터페이스를 제공한다.

따라서 이 인터페이스를 실체화한 어떤 상태 클래스도 기존 상태 클래스를 대신해 교체해서 사용할 수 있다. 

 

State1,2,3 : Context 객체가 요청한 작업을 자신의 방식으로 실제 실행한다. 대부분의 경우 다음 상태를 결정하고, 상태 변경을 context 객체에 요청하는 역할도 수행한다.

 

Context : State를 이용하는 역할을 수행한다. 현재 시스템의 상태를 나타내는 상태 변수와 실제 시스템의 상태를 구성하는 여러가지 변수가 있다. 또한 각 상태 클래스에서 상태 변경을 요청해 상태를 바꿀 수 있도록 하는 메서드(setState)가 제공된다. Context 요소를 구현한 클래스의 request 메서드는 실제 행위를 실행하는 대신 해당 상태 객체에 행위 실행을 위임한다.

 

인터페이스의 메서드는 각 상태에서 수행해야 하는 행위들이며, 상태 변경은 상태 스스로 알아서 다음 상태를 결정한다.

 

스테이트 패턴에서 Context 요소는 상태 클래스를 이용해 일을 수행하도록 지시한다.

 

이 요소를 구현한 클래스에는 현재 상태를 나타내는 state 변수가 있어 이에 바인딩되는 객체에 행위를 수행하도록 위임하는 책임이 있다.

 

이러한 state 변수의 변경은 각 상태 클래스의 객체가 다음 상태 객체를 Context 요소를 구현한 클래스에 알려줌으로써 이루어진다.

 

keyPoint : 스테이트 패턴은 상태에 따라 동일한 작업이 다른 방식으로 실행될 때 해당 상태가 작업을 수행하도록 위임하는 디자인 패턴이다.

 

-Light 클래스는 Context 역할을 수행한다.

-State 인터페이스는 State 역할을 수행한다.

-ON, OFF 클래스는 State1, State2 역할을 수행한다.

 

 

참고 : JAVA 객체지향 디자인 패턴