본문 바로가기
디자인 패턴

Gof 행동 디자인 패턴 : State

by oncerun 2022. 11. 20.
반응형

행동 패턴 : 알고리즘 및 객체 간의 책임 할당과 관련이 있는 여러 가지 패턴들

 

최근 빌더패턴을 공부하면서 적합한 상황에 적용하여 많은 이점을 얻었다. 

 

그런데 작성하는 코드를 보면서 이게 과연 객체의 책임이 여기 부여되는 것이 맞을까? 

 

왜 이렇게 메소드에서 if - else if 문이 등장하지? 이러한 의문점을 가지고 행동 패턴을 공부하기 시작했다. 

 

혹시 상태 패턴이 나의 궁금증을 풀어줄지.. 공부 시작해보자.

 

 

상태 

 

 - 객체의 내부 상태가 변경될 때 해당 객체가 그의 행동을 변경할 수 있도록 한다. 이는 스스로 행동을 변경할 수 있게 허가하는 패턴이다. 마치 객체가 행동을 변경할 때 객체가 클래스를 변경한 것처럼 보일 수 있다. 

 

책의 예시를 한번 보자. 

 

TCP Conntection은 여러 상태를 가질 수 있다. 

 

연결 성공한 상태, 대기 상태, 연결 종료의 상태를 갖는다. 이 상태를 갖는 TCPConnection 객체는 다른 객체에서 동일한 요청을 받을 때, 현재 상태에 따라 다르게 동작해야 한다. 

 

Open 요청을 받으면 현재 상태가 연결 종료, 연결 성공 상태인지에 따라 처리하는 결과가 다르다. 

이때 우리는 상태 패턴을 사용한다. 

 

이 패턴의 중요한 아이디어는 네트워크 연결에서 나타나는 모든 상태를 표현하는 TCPState 추상 클래스를 도입하는 것이다.  TCPState 추상 클래스는 서로 다른 운영 상태를 표현할 다른 모든 서브클래스에 공통되는 인터페이스를 정의한다. 

 

이후 TCPState는 상태 종속적인 행동을 실제로 구현해야 한다. 

예를 들어 TCPEstablished 클래스는 연결 성공 상태에서 해야 할 행동을, TCPClosed 클래스는 연결 종료 상태에서 국한된 행동을 구현한다. 

 

즉 TCPConnection 클래스는 TCPState의 서브클래스에서 만든 인스턴스를 사용해서 연결 상태별로 특별한 연산처리를 한다. 

 

신기한 점은 연결 상태가 변할 때마다 TCPConnection 객체는 자신의 상태 객체를 TCPState의 서브 클래스 중 하나의 인스턴스로 변경한다.! 매우 신기하다. 

 

 

어떤 상황에서 사용해야 되는 거지?

 

우리 이런 코드 본 적 있지 않나요?

public class State {

    private String state;


    public void doIt() {
        switch (state) {
            case "A" :
                break;
            case "b":
                break;
            case "c":
                break;
            default:
                break;
        }
    }

}

 

뭔가 클래스의 내부 상태에 따라 분기하는 코드 말이에요.

 

이를 만약 설계 단계에서 이렇게 작성하고 있다면 수정될 여지가 매우 많은 코드로 보입니다.

설계 단계에서 가능한 모든 상태를 예측하는 것은 사실상 불가능하고 상태라는 것은 변경될 가능성이 매우 큰 단어 중 하나로 보이기 때문이죠.

 

 

  • 객체의 행동이 상태에 따라 달라질 수 있고, 객체의 상태에 따라 런타임에 행동이 바뀌어야 한다.
  • 그 객체의 상태에 따라 달라지는 다중 분기 조건 처리가 너무 많이 들어 있을 때, 객체의 상태를 표현하기 위해 상태를 enum으로 정의하여야 한다. 

 

구조

 

Context : State의 구현체들을 관리, 유지할 인터페이스입니다.

State : Context의 각 상태별로 필요한 행동을 캡슐화하여 인터페이스로 정의

ConcreateState : Context의 상태에 따라 처리되어야 할 실제 행동을 구현한다.

 

 

해결책

 

1. 우리는 Context를 찾아야 합니다. 상태에 의존하는 코드가 있는 클래스이거나, 상태별 코드가 여러 클래스에 분산된 경우 새로운 Context를 만들 수 있습니다. 

 

2. 상태 인터페이스를 선언해야 합니다. State 인터페이스에 정의될 메서드를 Context에서 찾는 것은 매우 쉬운 일입니다.

분명 상태 변수에 따라 분기하는 메서드들이 눈에 보일 것이며 이 메소드들이 실제 State 인터페이스의 구현체들이 공통적으로 오버 라이딩해야 하는 메서드이기 때문입니다. 

 

3.  객체의 모든 가능한 상태들에 대해서 새 클래스들을 만들고 모든 상태별 행동들을 이러한 클래스들로 추출할 수 있습니다. 

우리는 Context에서 State에게 요청을 위임하기만 하면 됩니다. 이를 통해 Context의 코드는 읽기 쉬운 코드로 변경될 것이 입니다.

 

4. Context에서 상태 인터페이스 유형의 참조 필드와 필드 값을 접근하여 상태를 변경할 수 있도록 setter를 추가합니다.

 

5. 이제 상태에 따라 달라지는 알고리즘을 실제 상태 인터페이스를 구현한 구현체로 옮기는 작업을 합니다.

 

 

다만 우리는 해결을 위해 고민해야 하는 부분이 존재합니다.

 

바로 상태 객체들의 생성과 소멸인데요. 

 

상태 객체를 필요할 때만 생성하고 필요 없게 되면 없앨 것인가?

 이 경우는 상태가 실행되기 전까지는 어떤 상태여야 하는지 모르거나 상황에 따라서 상태가 자주 바뀌지 않으면 고려해볼 만한 전략입니다.  

이는 State 객체가 무거울 때 적용하면 매우 유용할 수 있습니다.

 

필요하기 전에 미리 만들어 둔 후 없애지 않고 계속 유지할 것인가?

 이 경우는 상태 변화가 수시로 일어날 때 적용하면 좋은 고려사항입니다. 다만 이 방법에 단점은 Context 클래스가 언제나 모든 상태에 대한 참조자를 계속 관리해야 하는 부담이 생긴다는 것입니다.

 

 

언제 적용하는 게 좋을까?

 

1. 현재 상태에 따라 다르게 행동하는 객체가 있고 상태의 수가 많을 경우

 

2. 그에 따라 클래스 필드들의 현재 값에 따라 클래스가 행동하는 방식을 변경하는 거대한 조건문으로 구성된 것을 보았을 때

 

 

 

마무리

 

정말 행동 패턴에 부합하는 패턴이 아닐 수 없다. 상태를 통해 책임을 관련된 클래스로 분산시켰다. 

단일 책임 원칙을 지키며 기존 상태 클래스들, Context를 변경하지 않고 새로운 상태를 추가할 수 있는 개방/폐쇄 원칙도 지켰다. 

 

좋은 패턴이고 상태가 많은 경우 쉽게 접근하여 적용해볼 만한 패턴으로 보인다. 

 

다만 지금 내 상황에는 적합하지 않은 패턴이다. 

 

결국 이 상태를 관리하는 Context가 존재해야 하는데, 이 Context를 쉽사리 변경할 수 없는 입장이고 상태를 외부에서 전달받는 것도 이유 중 하나가 될 듯하다. 

 

음.. 사용하는 Enum을 좀 더 리팩터링 하는 방향으로 가보아야겠다. 

 

 

 

 

 

반응형

댓글