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

필자는 요즘 안드로이드 코드를 만져보고 있다. 안드로이드를 공부하면서, 그리고 기존에 만들어 놓은 코드들을 보면서 그리고 책에 예제로 나와 있는 코드를 보면서 뭔가 정리하고 싶은 게 생겼는데, 그 부분은 바로 Handler의 사용과 관련된 부분이다. 특히 비동기로 서버에서 데이터를 읽어와 UI에 결과를 반영해주는 코드는 정말 최악인데, 이 때의 Handler 처리 과정을 정리해보면 다음과 같은 방식으로 이루어진다.




뭔가 복잡하다. 위 그림은 관련된 코드를 그림 하나로 표현한 것이어서 그나마 덜 복잡해 보이고는거고, 실제 코드에서는 handleMessage() 안에 상단히 많은 case 문이 위치하고, handler 코드와 someMethod() 사이에 많은 다른 코드들이 위치하게 된다. 그래서 버튼을 눌렀을 때 기능이 어떻게 동작하는지 확인하려면 코드를 이리 저리 탐색하면서 돌아다녀야 하고, 이는 코드 분석을 힘들게 만들어준다.


위 코드의 문제점들을 나열해보면 다음과 같은 것들이 있다.

  • 처리 결과를 Message를 이용해서 Handler에 전달할 때, 각 메시지를 구분하기 위해 정수값(message.what)을 사용한다. 
  • 비동기 방식 동작 코드에 Handler가 전달되어 불필요한 의존이 발생한다.
  • 무엇보다도 응집도가 떨어져 분석을 어렵게 만든다.

Handler는 결과에 따라 다른 행동을 하기 위해서 int 값을 사용한다. 아래 코드는 전형적인 Handler의 handleMessage() 메서드의 구현 모습이다.


Handler handler = new Handler() {

    public void handlerMessage(Message msg) {

        switch(msg.what) {

            case 0: // <--- 0 의 의미는?

                // msg.obj 사용

                // UI 처리 코드

                break;

            case 1: // <-- 새로운 조건 추가될 때 마다 case 비교 구문 추가

                // UI 처리 코드

            ...

            case SOME_CONSTANT: // <-- 새로운 조건 추가될 때 마다 상수 추가하는 번거로움

            ....

        }

    }

}


위 case에서 0과 1은 무엇을 의미할까? 의미를 부여하기 위해 상수를 사용할 수도 있다. 하지만, 그런다고 문제가 끝나는 것은 아니다. 새로운 조건이 추가되면 그 때 마다 case가 하나 추가되고 상수이름을 생각해서 추가해 주어야 한다. 실수도 서로 다른 상수가 같은 값을 갖게 되면 디버깅 하느라 (헛)수고 좀 해야 한다.


또 다른 문제는 비동기 처리를 위해 Handler 객체가 이곳 저곳으로 전달된다는 것이다. 예를 들어, HTTP 요청을 비동기로 보내고 그 결과를 받기 위해 HTTP 요청을 처리하는 객체에 Handler를 전달하고 (앞서 그림에서 과정1), 응답이 도착하면 Handler에 메시지를 보내는 방법으로 응답 결과를 UI에 반영한다. (앞서 그림에서 과정4와 과정5) HTTP 요청을 처리하는 객체는 사실 Handler와는 전혀 상관이 없는 객체이다. 그런데, 비동기로 들어오는 응답을 UI에 반영하기 위해 부득이 Handler 객체에 대한 의존이 발생하는 것이다.


Handler 객체가 날아다니기 시작할 때의 또 다른 문제점은 실제 handler.sendMessage()를 수행하는 Handler가 어떤 Handler인지 찾아다녀야 한다는 점이다. 예를 들어, HTTP 요청 처리 객체를 여러 액티비티에서 호출한다고 해 보자. 이 때, HTTP 요청 처리 객체 내부적으로 사용되는 handler가 어느 액티비티 객체의 멤버인지 확인하려면 코드를 탐색해야 하는 (헛)수고를 하게 된다.


낮아지는 응집도(cohesion)!


결국 앞에서 Handler의 사용으로 인해 발생하는 문제점은 코드의 응집도를 확 낮춰준다는 것이다. 이 말은 관련된 코드가 이곳 저곳으로 퍼진다는 것을 뜻하는데, 예를 들어, 뭔가 버튼 클릭시 서버에서 데이터를 읽어와 UI에 반영하는 코드는 다음과 같이 퍼지게 된다.

  • 버튼 클릭시 호출되는 메서드: HTTP 요청 처리 객체에 handler를 주고 비동기로 처리를 요청한다.
  • HTTP 요청 객체: 응답 처리 결과를 handler를 통해 전달한다.
  • handler의 handlerMessage() 메서드: case 문으로 메시지를 확인하고, 응답 처리 결과를 이용해서 UI를 조작한다.

뭔가 하나의 처리 흐름을 이해하기 위해 관련된 코드들이 위와 같이 세 곳에 퍼지게 된다. 이는 코드를 산만하게 이곳 저곳에 배치시켜서 가독성을 바닥으로 확 떨어뜨리고 유지보수 하기 어렵게 만들어준다. 게다가 Handler가 각각의 메시지를 구분할 수 있도록 나름대로 상수나 코드 값을 불필요하게 정의해 주어야 한다. (이런 거 은근 머리 아프다.)


커맨드와 콜백을 활용한 해결


앞에서 비동기 처리 상황에서 Handler를 사용하기 위해 발생했던 응집도 저하 문제를 어떻게 해결하면 좋을까 곰곰히 생각하다가, 다음의 두 가지를 생각해봤다.

  • 콜백
  • 커맨드

두 가지가 앞의 문제들을 어떻게 없애주는지 살펴보자.


1단계: 콜백 전달해서 호출해 달라고 요청하기


Handler가 이곳 저곳으로 전달되는 이유는 비동기로 처리한 결과를 UI로 받아야 하기 때문이다. 하지만, Handler가 다른 객체에 파라미터로 전달되는 순간부터 한 기능과 관련된 코드가 이곳 저곳으로 퍼지게 되고 이는 심각한 응집도 저하를 불러 일으킨다. 이를 방지하기 위한 한 가지 방법이 콜백을 사용하는 것이다. 우선, 다음과 같이 간단한 콜백 인터페이스를 만든다.


public interface AsyncCallBack<T> {

    public void call(T result);

}


이제 뭔가 비동기로 데이터를 읽어오는 코드가 위 콜백을 통해서 결과를 전달하도록 변경해 보자. 예를 들면, 아래와 같이 코드가 만들어질 것이다.


public class AsyncJob {

    public void loadSomeData(final AsyncCallBack<SomeData> callback) {

        new Thread() {

            // 데이터를 외부에서 로딩

           SomeData data = ....;

           callback.call(data);

        }.start();

    }

}


버튼이 클릭되었을 때 위 기능을 사용해야 한다면, 이제 Handler를 전달하는 대신 아래와 같이 콜백 객체를 전달하도록 코드가 바뀐다.


public void onClick(View v) {

    asyncJob.loadSomeData(

        new AsyncCallBack<SomeData> () {

            public void call(SomeData result) {

                // 여기서 뭔가 handler에 결과 전달함

                Message msg = handler.obtainMessage();

                msg.what = 0;

                msg.obj = result;

                handler.sendMessage(msg); // TODO: 아직도 데이터 로딩 후 UI 조작 코드가 분리됨 

            }

        }

    );

}


handler = new Handler() {

    public void handleMessage(Message msg) {

        switch(msg.what) {

            case 0:

                SomeData someData = (SomeData) msg.obj;

                // UI 조작

        }

    }

}


위와 같이 바꾸면, 이제 비동기 작업을 수행하는 코드에 Handler 객체를 전달할 필요가 없어진다. 위 코드의 경우 AsyncJob.loadSomeData() 메서드는 비동기로 데이터를 읽어오면 파라미터로 전달된 콜백 메서드를 호출한다. 즉, 콜백 메서드에서 Handler에 보낼 메시지를 생성하고 전달하게 되는 것이다.


하지만, 아직도 데이터를 읽어와서 UI를 변경한다는 하나의 작업과 관련된 코드가 onClick() 메서드와 Handler의 handleMessage() 메서드에 분리되어 있다. 이는 커맨드를 이용해서 해결할 수 있다.


2단계: 커맨드 패턴을 활용해서 Handler 처리 코드에서 what 제거하기


두 번째 과정은 커맨드를 사용하는 것이다. 우선 다음과 같은 간단한 커맨드 인터페이스를 정의한다.


public interface Command {

    void execute();

}


그리고, 다음과 같이 위 Command 만을 전문적으로 처리하는 CommandHandler 클래스를 정의한다.


public class CommandHandler {

    private Handler handler = new Handler() {

        public void handleMessage(Message msg) {

            if (msg.obj instanceof Command) {

                ((Command) msg.obj).execute();

            }

        }

    }

    public void send(Command command) {

        Message message = handler.obtainMessage();

        message.obj = command;

        handler.sendMessage(message);

    }

}


CommandHandler의 handler는 switch 문이 없다. 단지 Message를 통해서 전달받은 Command 객체의 execute() 메서드를 호출하는 것만 한다. CommandHandler의 send() 메서드는 Message에 Command 객체를 담아 handler에 전송한다.


이제 Handler 대신 CommandHandler를 사용하도록 코드를 변경해 보자. 


// 어떤 작업

final SomeData data = getSomeData();


// Command 전송

commandHandler.send(new Command() {

    public void execute() {

        // UI 변경

        changeUI(data);

    }

});


Handler를 사용하는 코드와 비교해 보자. 위와 같이 함으로써 다음과 같은 변화가 발생했다.

  • Handler.handleMessage() 메서드의 switch 문에서 사용할 상수 값 불필요
  • 작업 결과물을 UI에 반영하는 코드가 한 곳으로 모임
    • Handler를 사용하는 경우, Handler에 메시지를 보내는 코드와 Handler의 handleMessage() 메서드에서 분리되서 들어감
즉, 개발자는 Handler가 메시지를 구분하기 위해서 사용할 상수값(앞서 봤던 0, 1, SOME_CONSTANT 등)을 고민해서 만들 필요가 없고, 그 상수값을 맞출 필요가 없다. 그냥 알맞은 Command 구현 객체만 만들어서 전달해주면 된다.

그리고 응집도가 높아졌다. Handler를 사용하게 되면 데이터를 읽어오는 코드와 읽어온 데이터를 사용하는 코드가 분리되어 코드의 응집도가 떨어지고 이로 인해 코드 분석이나 변경이 어렵게 되는데, 위의 경우는 한 곳에 몰려 있어서 좀 더 빠르게 코드를 분석하고 이해할 수 있게 된다.

두 가지를 합친 결과 코드

두 가지를 합치면 앞서의 onClick() 메서드가 다음과 같이 바뀐다.

public void onClick(View v) {

    asyncJob.loadSomeData( // 1. 데이터 로딩 실행

        new AsyncCallBack<SomeData> () { 

            public void call(final SomeData result) { // 2. 로딩된 데이터 수신

                commandHandler.send(

                    new Command() {

                        public void execute() {

                            changeUI(result); // 3. 수신한 데이터를 이용 UI 변경

                        }

                    }

                );

            }

        }

    );

}


최초에 봤던 코드와 비교해 보자. 앞서 Handler를 전달하는 방식의 코드에서는 데이터가 처리되는 과정을 살펴보려면 이벤트 처리 코드(onClick과 같은), 비동기로 데이터 읽어오는 코드, 그리고 Handler 코드를 살펴봐야 했다. 또한, 그 과정에서 Handler가 각각의 메시지를 식별할 수 있도록 상수도 정의해 주어야 했다.

이랬던 것들이, 콜백과 커맨드를 사용함으로써 위 코드와 같이 응집도를 확 높일 수 있게 되었다. 관련 코드가 한 곳에 모여 있기 때문에 이제 데이터의 처리 과정을 확인하기 위해 이곳 저곳 흩어져 있는 코드를 살펴볼 필요가 없어 졌고, 메시지 구분 식별값 등 불필요한 고민을 하지 않아도 된다. 물론, 임의 객체 사용으로 인해 코드가 약간 복잡해 보이지만, 코드의 응집도가 낮아져서 코드 분석을 어렵게 하는 비용과 비교해보면 약간의 복잡한 코드가 비용이 (훨씬~~~) 작다.

자바에 클로저만 있었어도 더 간결한 코드를 얻을 수 있겠지만 지금은 이 정도만으로도 만족할 만한 코드 결과물을 얻을 수 있게 되었다.


관련자료



+ Recent posts