저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

'스테이트 패턴'에 해당되는 글 1건

  1. 2012.09.28 생활 코딩에서 만나 보는 상태(State) 패턴 (3)

UI 관련된 코드를 살펴보다가 리팩토링이 필요한 부분이 있어 점진적으로 리팩을 하고 있는데, 최근에 State 패턴을 적용한 사례가 있어 내용을 공유해 본다.


구현된 결과물은 다음과 같은 UI를 갖고 있다.



검색 탭을 누르면 '전체' 또는 '비디오'와 '블로그'로 분류해서 검색할 수 있다. 검색 탭을 누르면 기본적으로 '전체'를 기준으로 검색을 하며, '비디오'와 '블로그'를 선택하면 각 분류에 해당하는 컨텐츠만 검색한다. 비슷하게 마이로그 탭을 누르면, 기본적으로 즐겨찾기와 전체가 선택된다. 내역을 누르면 동일하게 전체가 기본으로 선택된다. 즐겨찾기와 내역 둘 다 비디오와 블로그를 선택하면 각 분류에 속한 결과만 조회된다.


이러한 기능을 제공하는 UI를 구현하기 위한 코드는 다음과 같은 방식으로 구현되어 있었다. (아래 코드는 이해를 돕기 위해 간단하게 정리한 것이다. 실제로는 더 복잡하게 되어 있다.)


public class SearchUI extends .... {


    private int selectMode; // 0 검색, 1 즐겨찾기, 2 내역

    private int tabMode; // 0 전체, 1 비디오, 2 블로그


    public void onClick(View v) { // 탭이 눌릴 때 이벤트 처리

        switch(v.getId()) {

        case R.id.search:

            selectMode = 0;

            tabMode = 0;

            break;

        case R.id.mylog:

            selectMode = 1;

            tabMode = 0;

            break;

        case R.id.favorite:

            selectMode = 1;

            tabMode = 0;

            break;

        case R.id.history:

            selectMode = 2;

            tabMode = 0;

            break;

        case R.id.all:

            tabMode = 0;

            break;

        case R.id.video:

            tabMode = 0;

            break;

        case R.id.blog:

            tabMode = 0;

            break;

        }

    }


    // selectMode와 tabModel에 따라 알맞은 분기

    private void process() {

        if (selectMode == 0) {

            if (tabMode == 0) {

                processSearchAll();

            } else if (tabMode == 1) {

                processSearchVideo();

            } else if (tabMode == 2) {

                processSearchBlog();

            }

        } else if (selectMode == 1) {

            if (tabMode == 0) {

                processGetAllFavoriteContent();

            } else if (tabMode == 1) {

                processGetFavorite();

            } else if (tabMode == 2) {

                processGetFavoriteBlog();

            }

        } else if (selectMode == 2) {

            ... // 비슷한 코드

        }

    }

}


만약 이 상태에서 탭이라도 하나 덩 생긴다면,,,,,,  Oh NO!!! 생각도하기 싫어진다. 그래서 새로운 요구사항이 생기기 전에 위 코드의 설계를 변경하기로 했다. 설계를 변경하기에 앞서 코드의 의미를 먼저 파악해 보았다. 파악한 결과는 아래와 같다.


* 검색을 누르면

======> 검색 상태가 되고, 전체 검색 기능을 실행한다.

* 마이그로나 즐겨찾기를 누르면

======> 즐겨찾기 상태가 되고, 전체 즐겨찾기 조회 기능을 실행한다.

* 내역을 누르면

======> 내역 상태가 되고, 전체 내역 조회 기능을 실행한다.


* 검색 상태에서

======> 전체를 누르면, 전체 검색 기능을 실행한다.

======> 비디오를 누르면, 비디오 검색 기능을 실행한다.

======> 블로그를 누르면, 블로그 검색 기능을 실행한다.

* 즐겨찾기 상태에서

======> 전체를 누르면, 전체 즐겨찾기 조회 기능을 실행한다.

======> 비디오를 누르면, 비디오 즐겨찾기 조회 기능을 실행한다.

======> 블로그를 누르면, 블로그 즐겨찾기 조회 기능을 실행한다.

* 내역 상태에서

======> 전체를 누르면, 조회한 전체 내역 조회 기능을 실행한다.

======> 비디오를 누르면, 조회한 비디오 내역 조회 기능을 실행한다.

======> 블로그를 누르면, 조회한 블로그 내역 조회 기능을 실행한다.


위에서 공통된 부분을 추려보면 다음과 같은 형식을 따른다.


* 특정한 상태에서 

======> 전체를 누르면, 전체 관련 기능 실행한다.

======> 비디오를 누르면, 비디오 관련 기능 실행한다.

======> 블로그를 누르면, 블로그 관련 기능 실행한다.

* 특정한 상태가 되면 전체 관련 기능 실행한다.


뭔가 상태에 변화가 있고, 동일한 기능 요청에 대해서 상태에 따라 다르게 동작해야 한다. 빙고! 상태(State) 패턴이다. 이제 위 코드를 상태 패턴을 이용해서 변경해 보자. 상태 패턴에 대한 설명은 필자가 예전에 정리한 패턴 스터디 발표 자료를 참고하기 바란다.


상태의 종류


상태는 크게 다음의 세 종류가 있다.

  • 검색 상태
  • 즐겨찾기 상태
  • 내역 상태
따라서, 다음과 같이 타입을 구성할 수 있을 것이다.


상태의 기능 정의


상태에서 제공할 기능은 다음의 3개다.

  • 전체 관련 기능
  • 비디오 관련 기능
  • 블로그 관련 기능
따라서, State 인터페이스는 다음과 같의 3개의 기능을 제공하도록 정의할 수 있다.

public interface State {
    void all();
    void video();
    void blog();

    void enter();
}

State의 3개의 하위 타입들은 각각 위 메서드를 알맞게 오버라이딩 할 것이다. 잠깐, 위에서 enter() 메서드는 상태가 바뀔 때 해당 상태로의 진입 처리를 하기 위한 메서드이다. 예를 들어, SearchState의 enter() 메서드는 화면상에 검색어를 입력하는 화면을 보여줄 수 있을 것이다.

상태를 정리했으니 다음으로 할 작업은 상태를 사용하도록 코드를 수정하는 것이다. 바뀐 코드는 아래와 같다.

public class SearchUI extends .... {


    private State currentState;


    public void onClick(View v) { // 탭이 눌릴 때 이벤트 처리

        switch(v.getId()) {

        case R.id.search:

            state = new SearchState();

            state.enter();

            break;

        case R.id.mylog:

        case R.id.favorite:

            state = new FavoriteState();

            state.enter();

            break;

        case R.id.history:

            state = new HistoryState();

            state.enter();

            break;

        case R.id.all:

            state.all();

            break;

        case R.id.video:

            state.video();

            break;

        case R.id.blog:

            state.blog();

            break;

        }

    }

}


앞의 코드와 비교해보자. 훨씬 간단해진 것을 알 수 있다. 하지만, 위 코드는 상태 변이와 기능 요청코드-State 객체를 생성하는 코드와 State의 all() 등의 메서드를 실행하는 코드-가 섞여 있다. 이는 다음과 같이 두 개의 리스너로 구분하는 것으로 정리할 수 있다.


public class SearchUI extends .... {


    private State currentState;


    private OnClickListener changeStateLitener = new OnClickListener() {

        void onClick(View v) { // 탭이 눌릴 때 이벤트 처리

            switch(v.getId()) {

                case R.id.search:

                    state = new SearchState();

                    break;

                case R.id.mylog:

                case R.id.favorite:

                    state = new FavoriteState();

                    break;

                case R.id.history:

                    state = new HistoryState();

                    break;

                default:

                    throw new ..... // 익셉션

        }

        state.enter();

    }


    private OnClickListener requestFunctionLitener = new OnClickListener() {

        void onClick(View v) { // 탭이 눌릴 때 이벤트 처리

            switch(v.getId()) {

            case R.id.all:

                state.all();

                break;

            case R.id.video:

                state.video();

                break;

            case R.id.blog:

                state.blog();

                break;

        }

    }

}


State 패턴을 적용한 결과 다음과 같은 일을 덜 고생하면서 할 수 있게 되었다.

  • 새로운 상태 추가 : if-else 블록의 경우 새로운 상태 추가와 함께 비슷한 if-else 블록이 이곳 저곳에 추가되어 코드가 더 복잡해진다. 하지만, State 패턴을 적용하면, State 객체를 사용하는 코드 입장에서 즉, 위 코드에서는 SearchUI 입장에서 새로운 State 구현 클래스와 해당 State로 전이하는 코드만 추가해 주면 된다.

  • 새로운 기능 요청 : if-else 블록의 경우 새로운 기능 요청이 추가되면 비슷한 if-else 블록이 이곳 저곳에 추가되어 코드가 더 복잡해진다. 하지만, State 패턴을 적용하면 각각의 State 구현체들이 해당 기능을 구현하므로 코드의 복잡도가 전혀 증가되지 않는다. SearchUI 입장에서 새로운 기능을 실행하는 코드만 추가해 주면 된다.


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. maruldy 2012.11.09 16:15 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요.
    Android 어플 개발 도중에, State 패턴을 적용해 보려다 궁금한 점이 있어서 댓글을 남깁니다..
    평소 최범균님 블로그에서 공부를 하려다가 댓글 남기는건 처음인데요..
    바쁘지 않으신 시간에 한번만 봐주시면 감사하겠습니다.. ^^;

    현재 작성중인 MaruActivity 클래스를 살펴보니, 여러 상태를 비교적으로 명확하게 가지게 되어서,
    기존에 if 문 들로 구분하던 부분을 각각의 State 로 분리 했습니다.
    그 결과 MaruAcitvity 는 SubState 라는 interface 를 구현한 AState, BState, CState, DState 를 갖게 되었습니다.

    또한 MaruActivity 에서 if 문으로 비교된 코드들이 상당히 간결화 되고, 화면 객체를 컨트롤 하는 Method 들 위주로 남아있게 되었습니다.

    문제는, 각각의 SubState 들로 옮겨진 코드들의 결과값을 MaruActivity 의 화면에 표현해 주기 위해서 각각의 SubState 들이 MaruActivity 의 직접적인 Reference 를 가지게 되었습니다.

    MaruActivity 와 SubState 가 각각 상호간에 참조를 하게 된겁니다.

    지금 이에 대한 처방으로는 MaruActivity 에서는 객체가 없어질 때, 각 SubState 로 부터 MaruActivity 의 Reference 를 해제해주는 메소드를 호출해주고 있습니다.

    하지만, 그동안 책에서 봤었던 State 패턴에서는 중간에 다른 클래스를 두어서 MaruActivity 와 SubState 들 간의 coupling 을 낮추는 방식을 많이 추천하고 있는데요.. 지금 이대로 가는게 좋을지, 아니면 중간에 다른 클래스를 두어서 결합도를 더 낮추는게 맞을지를 어떤 기준으로 판단해야 할지 몰라서 여쭙니다..

    • 최범균 madvirus 2012.11.12 00:18 신고  댓글주소  수정/삭제

      SubState가 MaruActivity에 요구하는 것이 무엇인지 확인하고, 그걸 추상화한 인터페이스로 분리하면 어떨까요? 대충 아래와 같이요.

      public class MaruActivity extends ... implements SomeIF {

      SubState ss = new ConcreteSubState(this);
      }

      public class ConcreteSubState implements SubState {
      private SomeIF someIF;

      public ConcreteSubState(SomeIF someIF) {
      this.someIF = someIF;
      }

      public void doAnything() {
      ...
      someIF.some();
      ...
      }
      }

    • maruldy 2012.11.12 10:22 신고  댓글주소  수정/삭제

      답변 감사합니다.

      답변해주신 부분 참고해 보겠습니다. ^^
      좋은 한 주 되세요~~ :)