주요글: 도커 시작하기
반응형

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 입장에서 새로운 기능을 실행하는 코드만 추가해 주면 된다.


+ Recent posts