본문 바로가기

2011
마이크로소프트웨어

글: 안병현, 진성주, 변현규, 김진영, 임선용 | http://momo-time.com / 2011-08


<실전 강의실>

구글 앱 엔진과 안드로이드 하모니

구글 앱 엔진 XMPP 활용해
안드로이드 푸시 서비스 구현Ⅱ


구글 앱 엔진의 XMPP를 사용하면 쉽게 푸시 서비스를 구축할 수 있다. 이전 연재에서는 푸시 서비스를 구축하기 위한 XMPP와 구글 앱 엔진에서 어떻게 설정하며 사용하는지 살펴봤다. 푸시 서비스 구축은 서버 사이드인 구글 앱 엔진과 클라이언트 사이드인 안드로이드 모두를 이해해야 가능하다.


먼저 푸시 서비스의 전체적인 그림을 살펴보자.

 

구글 앱 엔진을 활용한 푸시 서비스
<그림 1> 구글 앱 엔진을 활용한 푸시 서비스

 

푸시 서비스 구축 오버 뷰

XMPP는 메시지 프로토콜이기 때문에 네이트온을 생각하면 이해하기 쉽다. 네이트온 클라이언트와 네이트온 서버, 네이트온 ID가 있듯 XMPP도 XMPP 서버, XMPP 클라이언트, XMPP ID가 존재한다. 구글 앱 엔진은 XMPP 클라이언트 서비스(appid@ appspot.com)를 제공하기 때문에 네이트온을 예로 들어 네이트온 ID가 하나 발급됐다고 생각하면 편하다. XMPP 클라이언트가 있으니 어딘가에 접속할 수 있는 XMPP 서버가 존재하며 서로 통신할 수 있다.

 

안드로이드에서는 어떨까? 안드로이드에서 내장된 마켓을 사용하려면 구글 계정(id@gmail.com)이 필수로 필요하다. 안드로이드에서는 접속이 허용된 어떤 XMPP 서버로 접속을 할 수도 있지만 사용자가 기본적으로 구글 계정을 사용하기 때문에 구글 톡 서버로 연결해 구축했다. 구글의 공식적인 푸시 서비스인 C2DM도 구글 계정을 사용한다. 구글 앱 엔진 XMPP 클라이언트가 접속하는 서버와 구글톡 서버가 서버 대 서버로 통신을 하며 메시지를 전송하게 되는 것이다.

 

이 과정은 총 네 단계에 걸쳐 처리된다. 내부적으로 자세히 살펴보자. 안드로이드 단말에서는 app@appspot.com이라는 주소로 친구를 추가한다. 네이트온이 친구를 추가해야만 서로 대화할 수 있듯 XMPP도 친구를 추가하는 프로토콜이 존재하고 추가를 해야만 정상적으로 메시지를 보낼 수 있다. 안드로이드 단말에서 구글 앱 엔진의 XMPP ID에 대해 친구추가 요청을 하면 안드로이드와 연결하고 있는 XMPP 서버 쪽으로 전송하며 서버에서는 @ 뒤쪽의 주소를 분석해 XMPP 서버 쪽으로 요청한다. 그리고 구글 앱 엔진 XMPP 서버 쪽에서는 @ 앞쪽의 주소를 분석해 구글 앱 엔진의 인스턴스와 연결되는 것이다. 네이트온을 예로 이해해 보자. 친구가 추가됐다고 바로 메시지를 보낼 수는 없다. 메시지를 보내려면 대화방을 생성해야 하는데 친구 ID를 클릭하고 대화창이 생성된 상태를 만들어야 한다. 즉, 안드로이드 단말에서 채팅방을 생성하고 구글 앱 엔진 쪽의 메시지를 기다린다. 이렇게 구글 앱 엔진에서 메시지를 보내면 서버에서 클라이언트로 메시지를 보내는 푸시 서비스가 완성되는 것이다.

 

구글 앱 엔진 아키텍처

구글 앱 엔진을 활용한 푸시 서비스의 전체적인 구조를 살펴봤다. 푸시 서비스를 구축하기 위해서는 구글 앱 엔진과 안드로이드 단말 두 가지 관점에서 어떻게 구현됐는지를 알아야 한다. 먼저 구글 앱 엔진 파트의 구조를 살펴보자.

 

구글 앱 엔진 XMPP를 활용한 푸시 서비스 아키텍처
<그림 2> 구글 앱 엔진 XMPP를 활용한 푸시 서비스 아키텍처

 

크게 사용자층, 사용자의 상태변경 로그, 메시지 그리고 실패한 메시지를 처리하는 데몬의 네 가지로 나눠볼 수 있다. 실제 친구를 서로 등록하는 것에서는 사용자를 입력∙수정∙삭제하는 과정들이 필요하며, 사용자들이 항상 로그인 상태가 아니기 때문에 그들의 상태정보를 저장해 메시지를 받을 수 있을지의 여부를 체크한다. 그리고 메시지 데이터를 저장하며 전송 성공 여부를 기록한다. 사용자가 메시지 전송이 가능한 온라인 상태면 문제가 되지 않지만 오프라인일 경우는 전송할 수 없기 때문에 메시지를 저장하고 주기적으로 실패된 메시지를 전송하는 데몬이 동작하도록 구성한다.

 

사용자, 로그인 여부, 메시지

푸시 서비스는 총 네 단계로 간단히 구축할 수 있다.

 

1. XMPP 사용 설정
2. 사용자, 로그인 여부 기록
3. 푸시 메시지 보내기
4. 데몬 - 실패한 푸시 메시지 처리

 

이미 지난 호에서 구글 앱 엔진에서 XMPP 설정하는 내용을 다뤘으니 참고하길 바란다. 구글 앱 엔진에서 제공하는 XMPP Service 클래스를 활용하면 쉽게 사용자, 로그인 여부와 관련된 내용을 코드로 작성할 수 있다.

 

// 사용자 등록 요청이 오면 등록
Subscription subscription =
xmppService.parseSubscription(request);
if(subscription.getSubscriptionType() ==
SubscriptionType.SUBSCRIBE ) {
    userDao.add(user);
}

<리스트 1> /_ah/xmpp/subscription/ 매핑된 서블릿

 

Presence presence = xmppService.parsePresence(request);
userDao.updatePresence(presence.getFromJid());
// 사용자 상태 변경
xmppService.sendPresence(presence.getFromJid(),
PresenceType.AVAILABLE, PresenceShow.CHAT, "MOMO");

<리스트 2> /_ah/xmpp/presence/ 매핑된 서블릿

 

실제 코드는 이렇게 간단하다. 사용자 등록 요청이 오면 등록하고, 사용자의 상태값(온라인, 오프라인)이 변경되면 그대로 변경하는 내용이다. 메시지를 보내는 코드 또한 간단하다.

 

// 사용자의 상태값이 수신 가능할 때 메시지 전송
XMPPService xmppService =
XMPPServiceFactory.getXMPPService();

// 메시지 생성
MessageBuilder messageBuilder = new MessageBuilder();
messageBuilder.withRecipientJids(new
JID("client2@gmail.com/A"));
messageBuilder.withMessageType(MessageType.NORMAL);
messageBuilder.withBody("Hi!");

// 메시지 전송
Message reply = messageBuilder.build();
xmppService.sendMessage(reply);

<리스트 3> 메시지 전송

 

전송 실패 메시지 처리

사용자가 오프라인일 때는 메시지를 보낼 수 없기 때문에 메시지 전송의 성공 여부를 저장하고 재전송할 메시지만 가져와 다시 메시지를 보낸다. 엔터프라이즈 환경에서는 스케줄 라이브러리를 사용해 주기적으로 특정 작업을 실행하는데 구글 앱 엔진에서는 백그라운드 스레드를 생성할 수 없어 크론 작업이라는 기능을 활용해 처리한다. /war/WEB-INF/cron.xml 파일에 정의하고 <화면 1>과 같이 등록하면 주기적으로 /Demon URL을 호출하게 된다. 분, 시, 일 단위로 설정할 수 있다.

 

구글 앱 엔진 크론
<화면 1> 구글 앱 엔진 크론

 

// 전송 실패 메시지를 주기적으로 처리
for (PushMessageDto pushMessageDto :
pushMessageDao.getList(PushMessageType.RETRY)) {
    UserDto user = userDao.get(pushMessageDto.getToJID());
    if (user.getPresence() == PresenceType.AVAILABLE)
        sendMessage(message);
}

<리스트 4> 메시지 전송

 

구글 앱 엔진의 인스턴스를 배포하면 관리자 화면에서 주기적으로 실행한 성공 실패 결과를 볼 수 있다.

 

안드로이드 아키텍처

구글 앱 엔진 파트를 모두 살펴봤다. 이제 안드로이드 쪽의 아키텍처를 보자.

 

안드로이드는 기본적으로 네 가지 컴포넌트로 구성된다. 화면을 구성하는 액티비티, 백그라운드 작업을 위한 서비스와 브로드캐스트 리시버, 콘텐트 프로바이더가 그것들이다. 푸시 서비스를 구축하기 위해 안드로이드의 아키텍처를 <그림 3>과 같이 설계한다.

 

안드로이드 아키텍처
<그림 3> 안드로이드 아키텍처

 

푸시 메시지를 받기 위해서는 구글 앱 엔진과 안드로이드 간의 연결이 지속되고 있는지 주기적으로 확인할 필요가 있다. 이처럼 주기적인 확인을 필요로 하는 작업은 백그라운드에서 실행돼야하므로 서비스를 사용했다. XMPP 통신을 할 때 구글 앱 엔진의 XMPP 서버인 gTalk 서버를 이용하게 되는데 서버와의 연결을 위해 XMPP 커넥션을 사용한다. 푸시 서비스는 메신저의 일종이라고 생각하면 이해하기 쉽다. 메신저를 통해 대화할 때 채팅방을 생성하듯이 푸시 서비스를 구축하기 위해서도 역시 채팅방을 생성해야 한다. 채팅방을 만든 후에 메시지를 주고 받을 수 있게 된다. 이 과정에서 채팅방이 생성될 때와 메시지를 주고 받을 때 알림을 받기 위한 리스너들을 등록해 사용한다. 구글 앱 엔진으로부터 메시지가 전송되면 안드로이드로 푸시 알림이 온다. 알림이 왔을 때 노티피케이션 바에 표시해야 하므로 노티피케이션 컴포넌트도 필요하다. 푸시 서비스를 구축하기 위해 안드로이드에서는 <그림 4>와 같은 구조로 아키텍처를 설계한다. 이러한 아키텍처를 기반으로 구글 앱 엔진과 안드로이드의 통신을 보다 쉽게 구현하기 위해 XMPP 라이브러리를 사용한다. 유명한 XMPP 라이브러리인 Smack, 그 중에서도 안드로이드에 최적화된 ASmack을 사용한다.

 

XMPP 라이브러리 ASmack

ASmack 사이트
<화면 2> ASmack 사이트

 

안드로이드 XMPP 통신 환경을 구축하기 위해 우선 ASmack 세팅을 한다. Asmack 사이트(code.google.com/p/ asmack/)에서 [Downloads] 탭을 클릭해 asmack jar 파일을 다운받을 수 있다. 다운받은 후 프로젝트에 library로 추가하면 실제 개발할 수 있는 환경이 갖춰진다.

 

받은 jar 파일을 프로젝트에 라이브러리로 추가하기 위해서는 다음과 같은 과정을 순서대로 수행하면 된다. 우선 안드로이드 프로젝트를 하나 생성한 후 lib 폴더를 만들어서 다운받은 파일을 복사해 넣는다. 그 다음 프로젝트의 [Properties] 메뉴에 들어가서 [Java Build Path]를 선택한 후 [Libraries] 탭에서 [Add JARs] 버튼을 누르고 복사해 놓은 파일을 추가시키면 라이브러리 추가가 완료된다. 이해를 돕기 위해 Properties 창을 띄우고 실제로 라이브러리를 추가하는 <화면 3>을 참고하길 바란다.

 

ASmack jar 파일을 프로젝트에 라이브러리로 추가
<화면 3> ASmack jar 파일을 프로젝트에 라이브러리로 추가

 

<OK> 버튼을 누르면 프로젝트 아래에 [Referenced Libraries]가 생긴 것을 확인할 수 있다. 이제 XMPP 라이브러리를 쓸 수 있는 환경이 만들어졌다. 구글 앱 엔진과 안드로이드 간의 XMPP 통신을 위한 본격적인 환경을 구축해 보자.

 

푸시 서비스 구현을 위한 안드로이드 환경 구축

이제부터 구글 앱 엔진과 안드로이드의 XMPP 통신을 위한 안드로이드 파트의 환경 구축 단계를 순서대로 알아본다. 전체 순서를 간략하게 요약하면 다음과 같다.

 

1. 연결 관련 설정 세팅
2. 연결
3. 로그인 & 상태값 변경
4. 리스너 등록

 

이 과정의 실제 코드를 보면서 단계별로 알아보자. XMPP smack 라이브러리를 이용해 쉽게 구현할 수 있다. 코드는 <리스트 5>와 같다. 먼저 Connect 메소드를 살펴보자.

 

/src/com/momotime/infrastructure/pushservice/PushService.java

package com.momotime.infrastructure.pushservice;

public class PushService extends Service {

   public static final String ACTION_MESSAGE =
"com.momotime.infrastructure.pushservice.message";
    public static final String ACTION_RESTART_SERVICE =
"com.momotime.infrastructure.pushservice.restart";

    private static final String TAG = LogUtils.getTag(PushService.class);
    private static final int sleepTime = 10 * 1000;
    private XMPPConnection connection = null;
    private Thread thread = null;

    private static String googleAppEngineJID = "momotime@appspot.com";
    private static String resourceId = "push";
    private static boolean isNetworkAvailable = false;

    private MessageListener messageListener = new MessageListener() {
        @Override
        public void processMessage(Chat chat, Message message) {
            String messageString = message.getBody();
            LogUtils.info(TAG, "Message - " + messageString);
            Intent intent = new Intent();
            intent.setAction(ACTION_MESSAGE);
            intent.putExtra("message", messageString);
            sendBroadcast(intent);
        }
    };

    private BroadcastReceiver networkBroadcastReceiver =
new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
                LogUtils.info(TAG, "NetworkBroadcastReceiver...");
                if (isNetworkAvailable = isNetworkAvailbe()) {
                    if (connection != null) {
                        connection.disconnect();
                        connection = null;
                    }
                } else {
                  LogUtils.info(TAG, "Network Unavailable...");
            }
        }
    }
};

private boolean isNetworkAvailbe() {
    boolean isNetworkAvailbe = false;
    ConnectivityManager connectivityManager =
(ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo =
connectivityManager.getActiveNetworkInfo();
    if (networkInfo != null && networkInfo.isConnected()) {
        isNetworkAvailbe = true;
    }
    return isNetworkAvailbe;
}

private void createThread() {
    LogUtils.info(TAG, "Try create thread.");
    if (thread == null) {
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    loop();
                    SystemClock.sleep(sleepTime);
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        LogUtils.info(TAG, "Created thread.");
    }
}

private void loop() {
    if (!isNetworkAvailable) {
        return;
    }
    if (!GoogleMailInformationHelper.isValid(this)) {
        LogUtils.info(TAG, "Please configure Google
        Account Information.");
            try {
                disconnect();
            } catch (XMPPException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return;
        }
        if (connection == null || !connection.isConnected()) {
            try {
                connect();
            } catch (XMPPException e) {
                e.printStackTrace();
            }
        } else if (connection.isConnected()) {
            LogUtils.info(TAG, "[" + thread.getName() + "]["
+ connection.getUser() + "] PushServer connected...");
            sendPresenceAvailable();
        }
    }
public void disconnect() throws XMPPException {
    if (connection != null && connection.isConnected()) {
        connection.disconnect();
    }
}

public void connect() throws XMPPException {
    ConnectionConfiguration connectionConfiguration =
new ConnectionConfiguration("talk.google.com", 5222,"gmail.com");
    connectionConfiguration.setTruststoreType("BKS");
    connectionConfiguration.setTruststorePath("/system/etc/security/cacerts.bks");
    connectionConfiguration.setSASLAuthenticationEnabled(false);
    connection = new
    XMPPConnection(connectionConfiguration);
    connection.connect();
    if (connection == null)
        return;
        String id = GoogleMailInformationHelper.getId(this);
    if (id.indexOf("@") > -1)
        id = id.substring(0, id.indexOf("@"));
        String password = GoogleMailInformationHelper.getPassword(this);
        connection.login(id, password, resourceId);
        sendPresenceAvailable();
        String user = connection.getUser();
        LogUtils.info(TAG, "PushService Connected : " + user);
        ChatManager chatManager = connection.getChatManager();
            chatManager.addChatListener(new ChatManagerListener() {
            @Override
            public void chatCreated(Chat chat, boolean arg1) {
            LogUtils.info(TAG, "Notification Server Chat Created : " + chat.toString());
            chat.addMessageListener(messageListener);
        }
    });
}

public void sendPresenceAvailable() {
    Presence presence = new
    Presence(Presence.Type.available);
    connection.sendPacket(presence);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
PushServiceRestarter.unregisterRestartAlram(PushService.th
is);
LogUtils.info(TAG, "onCreate");
registerNetworkReciver();
isNetworkAvailable = isNetworkAvailbe();
createThread();
}
private void registerNetworkReciver() {
IntentFilter filter = new IntentFilter();
filter.addAction(ConnectivityManager.CONNECTIVITY_
ACTION);
registerReceiver(networkBroadcastReceiver, filter);
}
private void unregisterNetworkReciver() {
unregisterReceiver(networkBroadcastReceiver);
}
@Override
public void onDestroy() {
super.onCreate();
PushServiceRestarter.unregisterRestartAlram
(PushService.this);
LogUtils.info(TAG, "onDestroy");
unregisterNetworkReciver();
destroyThread();
}
private void destroyThread() {
LogUtils.info(TAG, "Try destory thread.");
if (thread != null) {
thread.destroy();
LogUtils.info(TAG, "Destroyed thread.");
}
}
}

<리스트 5> 푸시 서비스 클래스 구현

 

접속 설정과 메시지를 받기 위한 리스너

첫 번째 단계는 gTalk 서버과 안드로이드 클라이언트 간의 커넥션 관련 설정을 세팅한다. XMPP 서버로 gTalk을 사용하므로 메일 계정을 이용한다. 지메일 계정으로 gTalk 서버에 5222포트로 연결할 것이라고 설정을 세팅하는데 이 코드는 connect 메소드에서 ConnectionConfiguration 객체를 생성하는 부분이다. 해당 객체를 생성할 때 인자설정 관련 정보들을 넘겨서 세팅을 완료한다. ConnectionConfiguration 객체를 생성한 후 안드로이드 인증을 위해 별도로 setTruststoreType, setTruststorePath 메소드를 호출하게 된다.

 

두 번째 단계는 실제로 gTalk 서버와 연결하는 것이다. 앞 단계에서 세팅한 설정을 XMPPConnection의 인자로 넘기고 커넥트한다. connect 메소드 호출을 통해 간단히 연결할 수 있다.

 

세 번째 단계는 로그인하고 상태값을 변경하는 것이다. 수많은 사용자 중에 특정 사용자가 로그인했음을 gTalk XMPP 서버에 알리고 사용자와 채팅방을 생성할 수 있도록 이용 가능한 상태임을 알려준다. 메신저에 비유해 보자면 전 단계에서의 connect는 단순히 메신저 로그인창을 띄운 것이고 이번 단계는 아이디를 입력해 특정 사용자로 로그인하고‘온라인’상태로 세팅한 경우라고 할 수 있다. 메신저 창을 띄워도 실제 로그인을 하지 않는 이상 서버가 어떤 사용자인지 알 수 없으므로 connect만으로 끝내면 안 된다. 로그인하고 상태값을 변경하는 이번 단계가 필요하다. 코드에서 보면 id와 password, resourceId를 갖고 login 메소드를 호출한 후 Presence 객체를 만들어 이용 가능한 상태임을 세팅한다. 그 후 sendPacket 메소드 호출을 통해 상태를 gTalk 서버에 전송한다.

 

안드로이드와 구글 앱 엔진의 XMPP 통신을 위한 환경 구축 네 번째 단계는 메시지를 받기 위한 리스너를 등록하는 것이다.

 

ChatListener와 MessageListener 두 가지 리스너를 등록하게 되는데 ChatListener는 채팅방이 만들어지면 불리는 리스너이고, MessageListener는 구글 앱 엔진으로부터 오는 메시지를 받기 위한 리스너이다. 두 가지 리스너를 등록하면 구글 앱 엔진과의 채팅방이 생성됐을 때 알림을 받을 수 있다. 코드를 보면 getChatManager 메소드 호출을 통해 chatManager를 얻어온 후 chatManager의 addChatListener 메소드를 통해 chatListener를 등록한다. 오버라이드한 chatCreated 메소드는 채팅방이 생성되면 호출된다. 채팅방이 생성됐을 때 add MessageListener 메소드를 통해 MessageListener를 등록하게 된다. MessageListener의 processMessage 메소드를 보면 메시지를 받았을 때 인텐트를 이용해 받은 메시지를 인자로 넘겨 브로드캐스트함으로써 메시지를 전달하고 있다는 것을 알 수 있다.

 

세팅한 액션을 인텐트 필터로 설정한 리시버를 Android Manifest.xml 파일에 등록하면 리시버가 메시지를 받아서 원하는 처리를 할 수 있게 된다.

 

위의 네 단계를 거치면 실제로 구글 앱 엔진에서 안드로이드로 메시지를 전달할 수 있게 된다. 그 과정은 다음과 같다. 구글 앱 엔진이 보낸 메시지를 구글 앱 엔진의 XMPP 서버가 받고, 이것이 gTalk XMPP 서버에 전달된 후 등록해 뒀던 Message Listener를 통해 안드로이드에 최종적으로 전달된다.

 

AndroidManifest.xml

<receiver android:name="com.momotime.infrastructure.pushservice.PushMessageReceiver">
    <intent-filter>
        <action android:name="com.momotime.infrastructure.pushservice.message" />
    </intent-filter>
</receiver>

<리스트 6> AndroidManifest.xml에 메시지 받는 리시버 등록

 

재접속과 네트워크 상태

지금까지의 과정을 잘 따라왔다면 구글 앱 엔진과 안드로이드 간에 메시지를 주고 받을 수 있다. 하지만 해결해야 할 몇 가지의 이슈들이 있다. 앞의 코드를 보면서 계속 이야기해 보자.

 

채팅방을 생성했다고 해도 연결이 끊어지면 XMPP 통신을 할 수 없으므로 주기적으로 연결을 확인해야 한다. 연결돼 있는지 체크하고 연결돼 있지 않다면 다시 연결하는 과정이 필요하다. 코드에서는 loop 메소드를 주기적으로 호출하고, 이 메소드 내에서 연결이 끊긴 경우 connect 메소드를 다시 호출한다.

 

한 가지 더 생각해야할 것은 네트워크 상태다. 밖에 돌아다닐 때 지하철이나 엘리베이터 같은 곳에서 네트워크 상태가 불안정해 끊기는 경우가 많은데 이러한 경우 통신 자체가 안 되므로 연결할 수 없다. 다시 말하면 네트워크가 끊겼을 경우 연결 확인을 계속 하는 것은 의미가 없다는 말이다. 따라서 이같은 경우를 대비해 네트워크 상태를 주기적으로 확인하고 네트워크가 끊겼을 경우에는 연결 시도를 하지 않도록 예외처리를 해야 한다. 코드의 isNetworkAvailbe 메소드에서 네트워크 상태를 확인하게 되는데 안드로이드에서 기본적으로 제공되는 Connectivity Manager와 NetworkInfo 클래스를 이용하면 네트워크의 연결 여부를 알 수 있다. 이를 통해 네트워크 연결 상태를 확인하고 네트워크가 연결돼 있을 때만 채팅방의 연결을 확인하도록 처리한다.

 

초기 서비스 설정

통신하기 위해 연결을 계속 확인해야 하는데 이는 서비스를 실행시켜야 함을 의미한다. 그래서 부팅시 자동으로 서비스를 실행할 수 있는 코드를 별도로 넣어줘야 한다. 처음 설치시에는 초기 인증 액티비티에서 서비스를 실행시키면 된다. 부팅시 자동으로 서비스를 실행시키기 위해 우선 부팅이 완료됐다는 메시지를 받을 수 있도록 AndroidManifest.xml 파일에 리시버를 등록하고 BOOT_COMPLETED 액션을 받도록 인텐트 필터를 설정해 준다. 그리고 RECEIVE_BOOT_COMPLETED 퍼미션을 주면 부팅이 완료됐을 때 알림을 받을 수 있다. 이 리시버는 인텐트 필터를 하나 더 갖고 있다. 이것은 서비스가 종료됐을 때 다시 시작하기 위해 설정해 놓은 것이다. <리스트 7>의 PushService Restarter.java 파일에서 PushServiceRestarter 리시버를 구현한다. 알람 등록을 통해 서비스가 종료됐을 때 다시 실행되도록 하는 코드다. 여기까지 구현하면 서비스가 종료되지 않을 것이므로 통신을 위한 주기적인 연결 확인이 가능해진다.

 

AndroidManifest.xml

<receiver android:name="com.momotime.infrastructure.
pushservice.PushServiceRestarter">
    <intent-filter>
        <action android:name="com.momotime.infrastructure.pushservice.restart"/>
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

<리스트 7> AndroidManifest.xml 파일에 초기 서비스 설정을 위한 리시버 등록

 

 

/src/com/momotime/infrastructure/pushservice/PushServiceRestarter.java

package com.momotime.infrastructure.pushservice;

public class PushServiceRestarter extends
BroadcastReceiver {

    public static final String TAG = PushServiceRestarter.class.getName();
    @Override
    public void onReceive(Context context, Intent intent) {
        LogUtils.info(TAG, "Restart Service");
        if (intent.getAction().equals(PushService.ACTION_RESTART_SERVICE) || intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
            Intent i = new Intent(context, PushService.class);
            context.startService(i);
        }
    }

    public static void registerRestartAlram(Context context) {
        LogUtils.info(TAG, "registerRestartAlram");
        Intent intent = new
        Intent(PushService.ACTION_RESTART_SERVICE);
        PendingIntent pendingIntent =
        PendingIntent.getBroadcast(context, 0, intent, 0);
    long time = SystemClock.elapsedRealtime();
    time += 5 * 1000;
    AlarmManager alarmManager = (AlarmManager)
context.getSystemService(Context.ALARM_SERVICE);

alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WA
KEUP, time, 10 * 1000, pendingIntent);
    }
    public static void unregisterRestartAlram(Context context) {
        LogUtils.info(TAG, "unregisterRestartAlarm");
        Intent intent = new
Intent(PushService.ACTION_RESTART_SERVICE);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
        AlarmManager alarmManager = (AlarmManager)
context.getSystemService(Context.ALARM_SERVICE);
        alarmManager.cancel(pendingIntent);
    }
}

<리스트 8> 서비스가 종료됐을 때 재실행하기 위한 리시버 구현

 

푸시 서비스의 제약사항

지금까지 구글 앱 엔진 XMPP를 사용해 쉽게 푸시 서비스를 구축하는 과정을 살펴봤다. XMPP를 사용하면 이렇게 쉽게 구글 앱 엔진과 안드로이드의 푸시 서비스를 구축할 수 있다.

 

하지만 XMPP가 만능인 것은 아니다. 약간의 제약사항이 있는데 바로 처리량의 한계다. XMPP API 호출 횟수, 전송된 XMPP 데이터 등에 제한이 있는 것. 이와 관련된 더 자세한 사항은 구글 앱 엔진 제약사항 사이트(code.google.com/intl/ko-KR/appengine/docs/quotas.html)를 참조하면 자세히 알 수 있다.

 

XMPP
XMPP API 호출
애플리케이션이 XMPP에 액세스한 총 횟수

전송된 XMPP 데이터
XMPP 서비스를 통해 전송된 데이터의 양. 발신 대역폭 용량이 합산됨.

메시지 수신자
애플리케이션이 XMPP 메시지를 전송한 총 수신자 수

전송된 초대장
애플리케이션에 의해 전송된 총 채팅 초대장 수

 

리소스 무료 기본 용량 청구가 사용 설정된 기본 용량
일일 한도 최대 속도 일일 한도 최대 속도
XMPP
API 호출
호출
46,000,000건
호출
257,280건/분
호출
46,000,000건
호출
257,280건/분
전송된 XMPP
데이터
1GB 5.81GB/분 1,046GB 5.81GB/분
XMPP 메시지
수신자
수신자
46,000,000명
수신자
257,280명/분
수신자
46,000,000명
수신자
257,280명/분
전송된 XMPP
초대장
초대장
100,000장
초대장
2,000장/분
초대장
100,000장
초대장
2,000장/분

<표 1> 푸시 서비스의 제약사항

 

대용량 메시지 처리

앞에서 언급했던 제약사항의 해결 방안으로 몇 가지를 생각해봤다. 그 중 하나는 직접 서버를 구축하는 것이다. 아마존이나 KT 등 인프라스트럭처를 서비스로 제공하는 기업들의 클라우드 컴퓨팅 서비스를 이용해 직접 XMPP 서버를 운영한다면 대용량 처리의 한계를 극복할 수 있을 것이다. 구글 앱 엔진을 선택하고 제약사항을 감수하는 것과 직접 서버를 운영해 제약사항을 극복하는 것은 사용자의 선택에 달려있다. 다른 방법으로 다수의 인스턴스를 생성해 대용량 메시지를 처리할 수도 있다. 이럴 경우 인스턴스들을 관리하기 위한 매니저 인스턴스가 별도로 필요할 것이다. 이 역시 해결책으로 고민해 볼 수 있을 것이라 생각한다.

 

“푸시 서비스가 만능은 아니다”

이렇게 구글 앱 엔진 XMPP를 사용해 푸시 서비스를 구축하는 과정을 살펴봤다. 몇가지 기본 개념을 이해한다면 어렵지 않게 푸시 서비스 구축방법을 알 수 있을 것이다. 하지만 푸시 서비스라고 만능은 아니라는 것을 기억하자.

 

다음 호에서는 전통적으로 사용해 온 풀링방식을 어떻게 효과적으로 구축하는지 살펴본다.



/필/자/소/개/

필자 안병현, 진성주, 변현규, 김진영, 임선용

안병현, 진성주, 변현규, 김진영, 임선용 | http://momo-time.com

SW 마에스트로 1기 회원들로 현재 MOMO라는 팀명으로 활동하고 있고 <제11회 자바개발자컨퍼런스>에서 공동 발표를 진행한 바 있다. 함께 성장하며 배운 것을 나누려고 노력하고 있다.



※ 본 내용은 (주)마소인터렉티브(http://www.imaso.co.kr/)의 저작권 동의에 의해 공유되고 있습니다.
    Copyright ⓒ Maso Interactive Corp. 무단전재 및 재배포 금지

맨 위로
맨 위로