본문 바로가기

스위프트

[Swift/디자인패턴] State 패턴

State패턴은 Behavioral 패턴중 하나이다. 

Behavioral 패턴이 무엇인지 간략하게 살펴보자.

 

Behavioral 패턴

하나의 객체가 너무 많은 기능을 한다면 읽어야하는 코드 범위가 너무 길어지고 이해가 복잡해진다.

Behavioral패턴은 이런 하나의 객체가 수행하기 힘들거나 이해가 복잡해지는 구조를 만들어낼 때

이 기능을 수행하기 위해 객체를 분리하고, 책임 분배를 하는 아이디어를 정립한 패턴이다.

단, 책임 분배를 하는 과정에서 최대한 객체간에 결합성을 줄여

재활용성, 테스트 가능성을 높인다면 더할나위 없다.

 

State 패턴

패턴의 이름에도 적혀있듯이,

특정 상태 값의 변경에 따라 동일한 인터페이스여도

내부적으로 돌아가는 비즈니스 로직이 다른 형태로 작성되는 아이디어이다.

 

State패턴은 Context/State/ConcreteState 세 가지 요소로 구현된다.

Context는 State 값을 소유하고 있는 객체로

ConcreteState를 통해 특정 기능을 이용할 수 있는 객체다.

State는 특정 기능에 대한 인터페이스를 제공하는 추상화객체로

해당 State를 구현한 것이 ConcreteState이다.

 

그림으로 표현한다면 아래와 같다.

 

 

StateProtocol은 특정한 상태 변경에 따른 

구현시 기능에 대한 다른 동작이 필요한 메서드의 집합을 만들어야한다.

State를 Context에서 enum값의 형태가 아닌,

프로토콜을 구현한 ConcreteState 객체가 State그 자체라고 이해하는 것이 편할것이다.

즉, State = StateProtocol을 구현한 객체의 집합이라고 보자.

 

예제

실제 회사 프로젝트 중에서 웹소켓기반의 채팅 커넥션 관리하는 객체를 생성해본 경험이 있다.

채팅 커넥션을 연결하는 상태정보는 idle, connecting, connected, error가 있었다.

해당 상태가 어떤지 체크하고 각 기능 메서드마다 분기처리해서 진행할 수도 있지만,

위같은 패턴을 쓰면 어떤 형태로 나오게 되는지 확인해보자.

 

State를 정의해보자.

웹 소켓을 통한 기능의 기본 메서드를 추려보면

웹 소켓 연결/연결해제와 Data를 보내거나 Ping을 보낸다.

 

// State : 상태에 따른 기능에 메서드 집합을 인터페이스화한 추상화 객체이다.
protocol WebSocketState: AnyObject {
    func connect()
    func disconnect()
    func sendData(_ data: Data)
    func sendPing()
}

 

다음은 위에서 말한 것 처럼

State = State를 구현한 ConcreteState의 집합

상태는 위에서 말한것처럼 Idle/Connecting/Connected/Error 네 가지가 있다.

그럼 네 가지의 ConcreteState를 구현해주고,

내부적으로 각 상태에 알맞는 비즈니스 로직을 넣어주자.

 

// ConcreteState : State를 실제로 구현한 객체이다.
final class IdleWebSocketState: WebSocketState {
    func connect() {
        print("연결을 시도합니다.")
    }

    func disconnect() {
        print("연결이 되어있지 않습니다. 아무것도 하지 않고 종료됩니다.")
    }

    func sendData(_ data: Data) {
        print("연결이 되어있지 않습니다. 연결 후 재시도해주세요.")
    }

    func sendPing() {
        print("연결이 되어있지 않습니다. 연결 후 재시도해주세요.")
    }
}

final class ConnectingWebSocketState: WebSocketState {
    func connect() {
        print("연결을 시도중입니다. 응답을 기달려주세요.")
    }

    func disconnect() {
        print("연결을 시도중입니다. 응답을 기달려주세요.")
    }

    func sendData(_ data: Data) {
        print("연결이 되어있지 않습니다. 연결 후 재시도해주세요.")
    }

    func sendPing() {
        print("연결이 되어있지 않습니다. 연결 후 재시도해주세요.")
    }
}

final class ConnectedWebSocketState: WebSocketState {
    func connect() {
        print("이미 연결되어있습니다.")
    }

    func disconnect() {
        print("연결을 끊습니다.")
    }

    func sendData(_ data: Data) {
        print("\(String(data: data, encoding: .utf8))을 보냈습니다.")
    }

    func sendPing() {
        print("핑을 전송했습니다.")
    }
}

final class ErrorWebSocketState: WebSocketState {
    func connect() {
        print("연결을 시도중입니다. 응답을 기달려주세요.")
    }

    func disconnect() {
        print("이미 실패한 연결 에러가 발생했습니다. 재연결해주세요.")
    }

    func sendData(_ data: Data) {
        print("연결이 되어있지 않습니다. 연결 후 재시도해주세요.")
    }

    func sendPing() {
        print("연결이 되어있지 않습니다. 연결 후 재시도해주세요.")
    }
}

Context는 웹 소켓 연결을 관리하는 객체이므로,

싱글톤 패턴으로 언제든 접근하여 위 4가지 기능 메서드를 사용할 수 있도록 인터페이스를 뚫었다.

그리고 아래처럼 호출했더니 Context내부에 State에 따른 구현을 하지 않았음에도

각 State마다 비즈니스로직이 정상적으로 동작하였다.

참고로, ConcreteState에서 Context를 참고하여 State를 변경할 수 있다.

 

// Context : State를 실제로 사용하는 객체
final class WebSocketConnectionManager {
    static let shared = WebSocketConnectionManager()
    private var state: WebSocketState = IdleWebSocketState()

    // MARK: - Internal Methods

    func connect() {
        state.connect()
        state = ConnectingWebSocketState()
        DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
            self.state = ConnectedWebSocketState()
        }
    }

    func disconnect() {
        state.disconnect()
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            self.state = ErrorWebSocketState()
        }
    }

    func sendData(_ data: Data) {
        state.sendData(data)
    }

    func sendPing() {
        state.sendPing()
    }
}

let data = "데이터를 보낼려구해요".data(using: .utf8)!
WebSocketConnectionManager.shared.connect()
WebSocketConnectionManager.shared.sendData(data)
DispatchQueue.global().asyncAfter(deadline: .now() + 6) {
    WebSocketConnectionManager.shared.sendData(data)
    WebSocketConnectionManager.shared.sendPing()
    WebSocketConnectionManager.shared.disconnect()
    DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
        WebSocketConnectionManager.shared.disconnect()
        WebSocketConnectionManager.shared.sendData(data)
        WebSocketConnectionManager.shared.connect()
    }
}


//연결을 시도합니다.
//연결이 되어있지 않습니다. 연결 후 재시도해주세요.
//Optional("데이터를 보낼려구해요")을 보냈습니다.
//핑을 전송했습니다.
//연결을 끊습니다.
//이미 실패한 연결 에러가 발생했습니다. 재연결해주세요.
//연결이 되어있지 않습니다. 연결 후 재시도해주세요.
//연결을 시도중입니다. 응답을 기달려주세요.

 

정리

실제로 구현해보니까 저 소스들이 Context안으로 들어가면 구조가 복잡해지고

소스량이 비대해졌을 것으로 보이지만, 분리하니 확실히 Context에서의 참조가 간단해지고 보기 쉬었다.

단, State 개수가 별로 없거나, State의 기능자체가 한 두개정도라면은 

별도의 객체를 생성하고 구현하는 시간이 더 걸릴것으로 보인다.

 

상태 변경에 따른 구현 로직이 복잡하고,

상태의 개수가 많을 경우 사용한다면 사용하는 측에서 더 이점을 가지고 사용할 수 있을 것으로 보인다.