IT_Programming/Android_Java

[펌] Google App Engine XMPP를 활용하여 Android Push Service 만들기 (1~2)

JJun ™ 2013. 12. 16. 17:04

 


 

[출처]

http://softwaregeeks.org/2011/07/11/google-app-engine-xmpp%eb%a5%bc-%ed%99%9c%ec%9a%a9%ed%95%98%ec%97%ac-android-push-service-%eb%a7%8c%eb%93%a4%ea%b8%b01/

 

http://softwaregeeks.org/2011/11/13/google-app-engine-xmpp%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-android-push-service-%EB%A7%8C%EB%93%A4%EA%B8%B02/


 

 

 

※ 본 내용은 제11회 자바개발자컨퍼런스 “Google App Engine과 Android 하모니”의 발표내용으로 연재를 진행 합니다.

 

XMPP(eXtensible Messaging and Presence Protocol)은 이미 Google Talk, Facebook, Nimbuzz 등

이미 많은 서비스에서 사용되고 있습니다. ( XMPP 참고 설명 – http://www.slideshare.net/metajack/the-real-time-web-with-xmpp ) 구글앱엔진에서는 XMPP를 제공하고 있고, 구글앱엔진에서 제공하는 XMPP를

사용하면 쉽게 Push Service를 구축할 수 있다는 것을 설명하고자 합니다.

 

먼저, 구글앱엔진에서 제공하는 XMPP를 어떻게 사용할 수 있는지 알아보도록 하겠습니다.

 

구글앱엔진 사이트(http://code.google.com/intl/ko-KR/appengine/docs/)에서 Service > XMPP > Overview (http://code.google.com/intl/ko-KR/appengine/docs/java/xmpp/overview.html)로 들어가면 구글 앱 엔진에서

XMPP가 어떻게 구성되고 어떤 코드를 넣고 테스트를 해야하는지 나와있습니다.

 

XMPP는 메시지 프로토콜입니다. 그래서, 국민 메신저인 네이트온을 생각하면 이해하기가 쉬운데,

네이트온 클라이언트와 네이트온 서버, 네이트온 아이디가 있듯 XMPP 서버, XMPP 클라이언트,

ID에 해당하는 JID가 존재합니다.

 

구글앱엔진에서 제공하는 것은 XMPP 클라이언트입니다. 즉, 네이트온 아이디가 하나 발급되는 것이라고 생각하면 편합니다. JID의 주소는 applicationId@appspot.com가 되는데, applicationId는 구글앱엔진에서 인스턴스 계정을 만들 때 기입한 아이디입니다. 클라이언트가 있으니 어딘가에 접속할 수 있는 XMPP 서버가 있어야 하겠죠. 구글에서 내부적으로 appspot.com XMPP 서버를 관리하고 있습니다.

 

구글앱엔진에서 몇가지 설정만 한다면, applicationId@appspot.com 아이디가 하나 생성이 되고

이 아이디로 신청되는 친구, 메세지, 친구의 상태값을 모두 알 수 있는 것입니다.

 

Push Service는 서버에서 클라이언트로 메세지를 보내는 효과와 같다고 생각할 수 있습니다.

따라서, applicationId@appspot.com 계정에서 클라이언트 사용자ID로 메세지를 보내면

Push 가 되는 것이죠. 먼저 샘플프로젝트를 생성하여 보겠습니다.

 

 

샘플 프로젝트를 생성하면 기본적으로 친구추가를 하였을 경우, 메세지를 받았을 경우,

상태값이 변경되었을 경우 사용자가 원하는 특정한 작업 수행을 하지 합니다.

 

구글 앱 엔진에서는 특정 이벤트(친추 추가, 상태값변경, 메세지 수신 등)가 발생 했을 시

알려줄 수 있도록 inbound service를 활성화 시켜야 해당 이벤트에 대한 처리를 할 수 있습니다.

활성화 처리는 설정파일인 /war/WEB-INF/appengine-web.xml 파일을 열어 수정하면 됩니다.

 

 
<appengine-web-app
<application></application>
<version>1</version>
 
<system-properties>
<property
name="java.util.logging.config.file"
value="WEB-INF/logging.properties" />
</system-properties>
 
<inbound-services>
<service>xmpp_message</service>
<service>xmpp_presence</service>
<service>xmpp_subscribe</service>
</inbound-services>
 
</appengine-web-app>

 

 

굵게 처리된 부분을 활성화 하였다면 이제부터 특정 이벤트 처리를 할 수 있습니다.

정상적으로 활성화 되었는지 확인하기 위해서 웹어플리케이션을 구동시키고

아래 관리자 사이트(http://localhost:8888/_ah/admin)를 접속해보죠.

xmpp_message, xmpp_presence, xmpp_subscribe를 inbound-Service로 처리했기 때문에

왼쪽 메뉴에 XMPP가 보이는 것을 확인할 수 있습니다.

 

XMPP 메뉴로 들어가면 다음과 같이 로컬에서 XMPP를 테스트할 수 있도록 입력폼을 제공합니다.

 

 

inbound service로 설정된 xmpp_message의 경우는 메세지가 들어왔을 때마다 특정한 URL로

다시 요청을 포워딩 합니다. 메세지가 오면 /_ah/xmpp/message/chat/ 이라는 주소로

어플리케이션에게 요청을 포워딩 해주어 특정한 작업을 가능케 하는 것이죠.

 

1. 특정한 사용자가 applicationId@appspot.com 주소로 메세지를 보냄
2. appspot.com XMPP 서버가 메세지를 받음
3. 어플리케이션에서 xmpp가 inbound service 되어있는지 확인
4. 특정한 URL로 메세지 포워딩

 

xmpp_presence, xmpp_subscribe 도 주소만 다를 뿐 같은 과정을 거치게 됩니다.

요청되는 주소를 정리하면 아래 표와 같습니다.

inbound service
주소
xmpp_message
/_ah/xmpp/message/chat/
xmpp_presence
/_ah/xmpp/presence/available/

 

/_ah/xmpp/presence/unavailable/
/_ah/xmpp/presence/probe/
xmpp_subscribe
/_ah/xmpp/subscription/subscribe/

 

/_ah/xmpp/subscription/subscribed/
/_ah/xmpp/subscription/unsubscribe/
/_ah/xmpp/subscription/unsubscribed/

 

각 주소로 이벤트가 일어날 때마다 요청되므로 web.xml 파일에 해당 주소를 처리할 서블릿을

등록하고 사용자 정의 처리를 해보도록 하겠습니다.

 

 

1) 사용자등록, 삭제처리

서블릿 매핑을 위해서 /war/WEB-INF/web.xml 파일을 열어 굵게 표시한 부분을 추가하면,

/_ah/xmpp/subscription/subscribe 요청에는 SubscriptionServlet 서블릿이 호출될 것입니다.

 

 <web-app
 
 
version="2.5">
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>SubscriptionServlet</servlet-name>
<servlet-class>org.softwaregeeks.xmpp.SubscriptionServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>SubscriptionServlet</servlet-name>
<url-pattern>/_ah/xmpp/subscription/subscribe/</url-pattern>
</servlet-mapping>
</web-app>

 

 

실제로 처리할 서블릿을 만들면 다음과 습니다. 구글앱엔진에서는 XMPP를 쉽게 처리하기 위해서 XMPPService라는 인터페이스를 제공하고 XMPPServiceFactory를 사용하여 실제 객체를 얻은 후 사용할 수 있습니다. XMPPService 사용하면 개발자는 XMPP 프로토콜 스펙을 몰라도 쉽게 사용할 수 있는 객체로 반환하여 돌려줍니다. parseSubscription 메소드가 request 객체를 받아 개발자가 쉽게 사용할 수 있는 Subscription 객체로 반환하여 주는 것이죠.

 

 
package org.softwaregeeks.xmpp;
 
import java.io.IOException;
import javax.servlet.http.*;
 
import com.google.appengine.api.xmpp.Subscription;
import com.google.appengine.api.xmpp.XMPPService;
import com.google.appengine.api.xmpp.XMPPServiceFactory;
 
@SuppressWarnings("serial")
public class SubscriptionServlet extends HttpServlet 
{
 
    XMPPService xmppService = XMPPServiceFactory.getXMPPService();
 
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException 
    {
        doPost(request,response);
    }
 
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException 
    {
        Subscription subscription = xmppService.parseSubscription(request);
        String from = subscription.getFromJid().getId();
        String to = subscription.getToJid().getId();
 
        System.out.println("From : " + from);
        System.out.println("To : " + to);
    }
}
 
<span style="line-height: 1.714285714; font-size: 1rem;">[/</span><span style="line-height: 1.714285714; font-size: 1rem;">code</span><span style="line-height: 1.714285714; font-size: 1rem;">]</span>
 
어떻게 요청되고 응답받는지를 체크 하기 위해서 단순히 추가를 원하는 사용자의 ID값과 추가를 해야하는 사용자의 ID 값을 출력하는 것을 만들어 보았습니다. 이제 테스트를 위해서 웹어플리케이션을 동작 후에 관리자페이지(http://localhost:8888/_ah/admin/xmpp)를 접속하여 Subscription 을 테스트하기 위해서 Message Type을 Subscription 으로 선택하고 From 입력창에 from@xmpp.org, To 입력창에 to@xmpp.org 입력하고 Subscribe initiated 메뉴를 선택한 후 Make Request 버튼을 누르면, 요청이 이루어지고 이클립스의 콘솔 화면에는 아래 로그가 남게 됩니다.
 
From : from@xmpp.org
 To : to@xmpp.org
 
만일 서블릿이 맵핑되지 않은 상태에서 요청하면 Message send failure HTTP ERROR 404 오류가 출력될 것입니다.
 
<a href="http://softwaregeeks.files.wordpress.com/2011/07/4.png"><img class="alignleft size-full wp-image-1094" alt="" src="http://softwaregeeks.files.wordpress.com/2011/07/4.png" width="574" height="535" /></a>
 
친구 삭제는 /_ah/xmpp/subscription/unsubscribe/ URL을 서블릿 매핑하여 테스트를 진행하며 관리자페이지에서 Notification Type을 변경하여 테스트를 진행하면 됩니다.
 
<b>2) 사용자 상태변경</b>
 
네이트온을 생각하여 보면, 접속한 사용자의 상태를 변경할 수 있습니다. 온라인, 로그오프, 자리비움, 바쁨, 다른용무중 등 이러한 사용자의 상태를 다른 사용자에게 알려주는 기능도 있습니다. 여기에서 주목해야할 것은 로그오프 상황일 때인데, 사용자가 로그오프되면 구글앱엔진의 XMPP 계정(applicationId@appspot.com)으로 로그오프 되었다는 상태변경 이벤트가 호출된다는 것입니다. 다른 사용자의 상태 변경을 어떻게 알 수 있는지 알아보겠습니다.
 
1
 
<web-app
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
 
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
 
version="2.5">
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>PresenceServlet</servlet-name>
<servlet-class>org.softwaregeeks.xmpp.PresenceServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>PresenceServlet</servlet-name>
<url-pattern>/_ah/xmpp/presence/available/</url-pattern>
</servlet-mapping>
</web-app>

 

 

XMPP에서는 상태값을 Presence 라고 부릅니다.

web.xml에 /_ah/xmpp/presence/available/ 주소 매핑작업을 했으니

PresenceServlet 를 하나 만들어 보죠.

 

package org.softwaregeeks.xmpp;
 
import java.io.IOException;
 
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import com.google.appengine.api.xmpp.Presence;
import com.google.appengine.api.xmpp.PresenceShow;
import com.google.appengine.api.xmpp.PresenceType;
import com.google.appengine.api.xmpp.XMPPService;
import com.google.appengine.api.xmpp.XMPPServiceFactory;
 
@SuppressWarnings("serial")
public class PresenceServlet extends HttpServlet 
{
           XMPPService xmppService = XMPPServiceFactory.getXMPPService();
 
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException 
    {
       doPost(request,response);
    }
 
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException 
    {
       Presence presence = xmppService.parsePresence(request);
       String from = presence.getFromJid().getId();
       String to = presence.getToJid().getId();
 
       System.out.println("From : " + from);
       System.out.println("To : " + to);
       System.out.println(presence.getPresenceType());
 
       xmppService.sendPresence(presence.getFromJid(), PresenceType.AVAILABLE, PresenceShow.CHAT, "XMPP");
    }
}

 

 

 

동일하게 request를 Presence 객체로 반환하여 처리합니다. 여기서 다시 sendPresence 메소드를 사용하여 현재 계정의 상태를 다시 반환하여 서로 상태값을 공유할 수 있도록 합니다. 웹어플리케이션을 구동하고 구글앱엔진 관리자페이지에서 테스트를 해보면 콘솔화면에 다음 값이 출력되는 것을 확인할 수 있습니다.

 

 From : from@xmpp.org
To : to@xmpp.org
AVAILABLE
Sending an XMPP Presence:
To JID:
from@xmpp.org
Show:
chat
Status:
XMPP

 

 

 

 

3) 메세지 수신 발신

구글앱엔진에서는 메세지의 수신과 발신을 쉽게 할 수 있습니다. 수신은 동일하게 URL로 요청이

포워딩 되므로 서블릿을 만들어 매핑만 처리를 하면 할 수 있고, 발신은 MessageBuilder 클래스를 활용하여 쉽게 처리가 가능합니다. 예제로 살펴보죠.

 

 

<web-app
 
 
version="2.5">
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>MessageServlet</servlet-name>
<servlet-class>org.softwaregeeks.xmpp.MessageServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MessageServlet</servlet-name>
<url-pattern>/_ah/xmpp/message/chat/</url-pattern>
</servlet-mapping>
</web-app>

 

 

package org.softwaregeeks.xmpp;
 
import java.io.IOException;
 
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import com.google.appengine.api.xmpp.JID;
import com.google.appengine.api.xmpp.Message;
import com.google.appengine.api.xmpp.MessageBuilder;
import com.google.appengine.api.xmpp.MessageType;
import com.google.appengine.api.xmpp.XMPPService;
import com.google.appengine.api.xmpp.XMPPServiceFactory;
 
@SuppressWarnings("serial")
public class MessageServlet extends HttpServlet 
{
 
    XMPPService xmppService = XMPPServiceFactory.getXMPPService();
 
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException 
  {
        doPost(request,response);
    }
 
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException 
  {
        // 메세지 수신
        Message message = xmppService.parseMessage(request);
        String from = message.getFromJid().getId();
 
        System.out.println("From : " + from);
        System.out.println("MessageType : " + message.getMessageType());
        System.out.println("Body : " + message.getBody());
 
        // 메세지 발신
        MessageBuilder messageBuilder = new MessageBuilder();
        messageBuilder.withRecipientJids(new JID("to@gmail.com"));
        messageBuilder.withMessageType(MessageType.NORMAL);
        messageBuilder.withBody("Hi!");
 
        Message reply = messageBuilder.build();
        xmppService.sendMessage(reply);
    }
}

 

 

From : from@xmpp.org
MessageType : CHAT
Body : Message
Sending an XMPP Message:
Body:
Hi!
Type:
normal
RawXml:
false
To JIDs:
to@gmail.com

 

 

누구에게 보내고, 무슨 내용을 넣을 것인지만 넣으면 쉽게 메세지를 전송할 수 있습니다.

 

 

4) Google App Engine XMPP API

구글앱엔진에서 XMPP는 com.google.appengine.api.xmpp 패키지에 정의되어 있는데,

표로 정리하면 아래와 같습니다.

 

인터페이스
역할
XMPPService
Interface for accessing XMPP status information, sending XMPP messages, and parsing XMPP responses.
클래스
역할
JID
Object representing a single Jabber ID.
Message
Class that represents an XMPP message.
MessageBuilder
Builder used to generate Message instances to represent incoming or outgoing XMPP messages.
Presence
Represents presence information returned by the server.
PresenceBuilder
Builder used to generate Presence instances to represent incoming XMPP presence stanzas.
SendResponse
Represents results of sending a message.
Subscription
Represents an incoming subscription stanza from the server.
SubscriptionBuilder
Builder used to generate Subscription instances to represent incoming XMPP subscription stanzas.
XMPPServiceFactory
Constructs an instance of the XMPP service.
ENUM
역할
MessageType
Types of messages, from RFC3921.
PresenceShowValues for the 'show' sub-stanza of presences, taken from RFC3921.
PresenceType
Values for the 'type' attribute of presences, taken from RFC3921.
SendResponse.Status
Possible per-id responses to sending a message.
SubscriptionType
Values for the 'type' attribute of presences, taken from RFC3921.
Exception
역할
XMPPFailureException
XMPPFailureException is thrown when any unknown error occurs while communicating with the XMPP service.

 

십여개의 클래스와 인터페이스로 이루어진 간단한 패키지입니다.

사용자 추가 삭제, 상태값 변경, 메세지의 수신 발신을 쉽게 해결할 수 있습니다.

 

 

정리

Google App Engine에서 XMPP를 활용하면 Push Service를 쉽게 구축할 수 있는데

Google App Engine에서 XMPP가 어떻게 활용되고 있는지 예제를 중심으로 알아보았습니다.

다음에는 실제로 Push Service를 구축하기 위해서 서버의 아키텍쳐와 안드로이드의 아키텍쳐를

알아보도록 하겠습니다.

 

 

 


 

 

 

 

※ 본 내용은 제11회 자바개발자컨퍼런스 “Google App Engine과 Android 하모니”의 발표내용으로 연재를 진행 합니다.

 

 

Google App Engine XMPP를 활용하여 Android Push Service 만들기(1)
Google App Engine XMPP를 활용하여 Android Push Service 만들기(2)

 

 

1. 푸시 서비스 구축 Overview

구글 앱 엔진의 XMPP 사용하면 쉽게 푸시 서비스를 구축할 수 있습니다.

이전 연재에서는 푸시 서비스를 구축하기 위한 XMPP와 구글 앱 엔진에서 어떻게 설정을 하며

사용하는지를 살펴보았습니다. 푸시 서비스를 구축한다는 것은 서버사이드인 구글 앱 엔진과

클라이언트 사이드인 안드로이드 모두를 이해해야 구축이 가능합니다.

푸시 서비스의 전체적인 그림을 먼저 살펴보겠습니다.

 

 

 

XMPP는 메시지 프로토콜이기 때문에 네이트온을 생각하면 이해하기가 쉬운데 네이트온 클라이언트와 네이트온 서버, 네이트온 ID가 있듯 XMPP 서버, XMPP 클라이언트, XMPP ID가 존재합니다.

 

구글 앱 엔진은 XMPP 클라이언트 서비스(appid@appspot.com)를 제공하기 때문에 네이트온으로 생각하면 네이트온 ID가 하나 발급되었다고 생각하면 편합니다. XMPP 클라이언트가 있으니 어딘가에 접속할 수 있는 XMPP 서버가 존재하며 서로 통신할 수 있죠.(사실 개인적으로 XMPP 서버로 gTalk 서버를 사용하지 않을까라고 추측하고 있습니다.)

 

그럼 안드로이드쪽으로 넘어와 살펴보겠습니다. 안드로이드에서 내장된 마켓을 사용하려면

구글계정(id@gmail.com)이 필수적으로 필요합니다. 안드로이드에서는 접속이 허용된

어떤 XMPP 서버로 접속을 할 수도 있지만 사용자가 기본적으로 구글계정을 사용하기때문에

구글톡 서버로 연결을 하여 구축을 하였습니다.

 

구글의 공식적인 푸시서비스인 C2DM도 구글계정을 사용합니다. 구글 앱 엔진 XMPP 클라이언트가 접속하는 서버, 구글톡 서버가 서버 대 서버로 통신을 하며 메시지를 전송하게 되는 것입니다.

 

총 4단계에 걸쳐 처리가 되는데 내부적으로 자세히 살펴보겠습니다.

안드로이드 단말에서는 app@appspot.com 이라는 주소로 친구를 추가를 하게 됩니다.

네이트온도 친구를 추가해야지만 서로 대화를 할 수 있듯 XMPP 도 친구를 추가하는 프로토콜이

존재합니다. 그래서 안드로이드 단말에서 구글 앱 엔진의 XMPP ID를 친구추가 요청을 하게 되면

안드로이드와 연결하고 있는 XMPP 서버쪽으로 전송하게 되며 서버에서는 @ 뒤쪽의 주소를 분석하여 XMPP 서버쪽으로 요청을 하게 됩니다.

 

그리고 구글 앱 엔진 XMPP 서버쪽에서는 @ 앞쪽의 주소를 분석하여 구글 앱 엔진의 인스턴스와

연결이 되는 것입니다. 네이트온을 다시 생각하면 친구가 추가되었다고 바로 메시지를 보낼 수

없습니다. 메세지를 보내려면 대화방을 생성을 해야하는데 친구아이디를 클릭하고 대화창이 생성이 된 상태를 만들어야 합니다. 즉, 안드로이드 단말에서 채팅방을 생성하고 구글 앱 엔진쪽의 메시지를 기다리는 것입니다.

 

리고 구글 앱 엔진에서 메시지를 보내게 되면 서버에서 클라이언트로 메세지를 보내는 푸시서비스가 완성되는 것입니다.

 

 

 

2. Google App Engine 파트 아키텍쳐

 

구글 앱 엔진을 활용한 푸시서비스의 전체적인 구조를 살펴보았습니다.

푸시서비스를 구축하기 위해서는 구글 앱 엔진과 안드로이드 단말 2가지 관점에서 어떻게

구현되었는지를 알아야 합니다. 먼저 구글 앱 엔진 파트의 구조를 살펴보겠습니다.

 

 

 

크게 사용자층, 사용자의 상태변경로그, 메세지, 그리고 실패한 메시지를 처리하는 데몬 4가지로

나누어 볼 수 있습니다. 실제 친구를 서로 등록하는 것에서는 사용자를 입력, 수정, 삭제하는 과정들이 필요할 것이며, 사용자들이 항상 로그인 상태가 아니기 때문에 그들의 상태정보를 저장하여

메세지를 받을 수 있을지 여부를 체크하게 됩니다. 그리고 메세지 데이터를 저장하며 전송 성공여부를 기록하게 됩니다. 사용자가 메세지 전송이 가능한 온라인 상태이면 문제가 되지않지만 오프라인일 경우는 전송할 수 없기때문에 메세지를 저장하고 주기적으로 실패된 메시지를 전송하는 데몬이 동작하도록 구성하였습니다.

 

 

 

3. User, Presence, Message

 

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

 

1. XMPP 사용 설정
2. User, Presence 기록
3. Push Message 보내기
4. Demon – 실패한 푸시 메시지 처리

 

이미 지난호에서 구글 앱 엔진에서 XMPP 설정을 다루었으니 참고바라며, 구글 앱엔진에서

제공하는 XMPPService 클래스를 활용하면 쉽게 User, Presence 관련된 내용을 코드로 작성이

가능합니다.

 

/_ah/xmpp/subscription/ 매핑된 서블릿

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

 

 

 

/_ah/xmpp/presence/ 매핑된 서블릿

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

 

 

실제 코드는 이렇게 간단하며, 사용자 등록요청이 오면 등록하며,

사용자의 상태값(온라인, 오프라인)이 변경되면 사용자의 상태값을 변경하는 내용입니다.

메세지를 보내게 되는 코드 또한 간단합니다.

 

메세지 전송

  

 // 사용자의 상태값이 수신가능일 때 메시지 전송
 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);
 

 

 

 

 

4. 전송 실패 메시지 처리

 

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


 

 

메세지 전송

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

 

 

구글 앱 엔진의 인스턴스를 베포하게 되면 관리자 화면에서 주기적으로 실행한 성공 실패 결과를

볼 수 있습니다.

 

 

 

5. 안드로이드 파트 아키텍쳐

 

구글 앱엔진 파트를 모두 살펴보았는데 이제 안드로이드 쪽의 아키텍쳐를 살펴보겠습니다.

안드로이드는 기본적으로 4가지 컴포넌트로 구성이 되는데 그것들은 다음과 같습니다.

화면을 구성하는 액티비티, 백그라운드 작업을 위한 서비스와 브로드캐스트 리시버,

컨텐트 프로바이더 이렇게 4가지입니다.

 

푸시 서비스를 구축하기 위해 안드로이드의 아키텍쳐를 다음과 같이 설계하였습니다.

 

 

 

푸시 메시지를 받기 위해서는 구글 앱 엔진과 안드로이드 간의 연결이 지속되고 있는지 주기적으로 확인할 필요가 있습니다. 이러한 주기적인 확인을 필요로 하는 작업은 백그라운드에서 실행되어야 하므로 서비스를 사용하였습니다.

 

그리고 XMPP 통신을 할 때 구글 앱 엔진의 XMPP 서버인 gTalk 서버를 이용하게 되는데, 서버와의 연결을 위하여 XMPP Connection을 사용하였습니다. XMPP를 사용한 푸시 서비스는 메신저의 일종이라고 생각하면 이해하기 쉬운데, 메신저를 통해 대화를 할 때 채팅방을 생성하듯 푸시 서비스

구축을 하기 위해서도 역시 채팅방을 생성해야 합니다. 채팅방을 만든 후에 메시지를 주고 받을 수 있게 되는데, 이 과정에서 채팅방이 생성될 때와 메시지를 주고 받을 때 알림을 받기 위한 리스너들을 등록하여 사용하게 됩니다.

 

구글 앱 엔진으로부터 메시지가 전송되면 안드로이드로 푸시 알림이 오게 되는데, 알림이 왔을 때

노티피케이션 바에 표시해야 하므로(필요하면) 노티피케이션 컴포넌트도 필요합니다. 푸시 서비스를 구축하기 위하여 안드로이드에서는 이러한 구조로 아키텍쳐를 설계하였습니다.

 

이러한 아키텍쳐를 기반으로 구글 앱 엔진과 안드로이드의 통신을 보다 쉽게 구현하기 위하여 XMPP 라이브러리를 사용하였습니다. 유명한 XMPP 라이브러리인 Smack을 사용하였는데,

그 중에서도 안드로이드에 최적화된 ASmack을 사용하였다.

 

 

 

6. XMPP 라이브러리 ASmack

 

 

 

안드로이드 XMPP 통신 환경을 구축하기 위하여 우선 ASmack 셋팅을 해야 합니다.

Asmack 사이트(http://code.google.com/p/asmack/)에서 [Downloads] 탭을 클릭하여

asmack jar 파일을 다운받을 수 있고, 다운받은 후 프로젝트에 library로 추가하면

실제 개발할 수 있는 환경이 갖추어집니다.

 

 

 

다운로드 받은 jar 파일을 프로젝트에 라이브러리로 추가하기 위해서는 다음과 같은 과정을

순서대로 수행하면 됩니다.

 

우선 안드로이드 프로젝트를 하나 생성한 후, lib 폴더를 만들어서 다운받은 파일을 복사하여 넣습니다. 그 다음 프로젝트 폴더를 선택 후 마우스 오른쪽 버튼을 누르면 [Properties] 메뉴가 나옵니다. [Properties] 메뉴에 들어가서 [Java Build Path]를 선택한 후, [Libraries] 탭에서 [Add JARs] 버튼을 누르고 복사해놓은 파일을 추가시키면 라이브러리 추가가 완료됩니다.

 

이해를 돕기 위해 Properties 창을 띄우고 실제로 라이브러리를 추가하는 그림을 상단에 넣었습니다. [OK] 버튼을 누르면 프로젝트 아래에 [Referenced Libraries]가 생긴 것을 확인할 수 있습니다. 이제 XMPP 라이브러리를 쓸 수 있는 환경은 만들어졌고, 구글 앱 엔진과 안드로이드 간의 XMPP 통신을 위한 본격적인 환경 구축을 해보도록 하겠습니다.

 

 

 

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

 

이제부터 구글 앱 엔진과 안드로이드의 XMPP 통신을 위한 안드로이드 파트의 환경 구축 단계를

순서대로 설명하려고 한다. 전체 순서를 간략히 요약하면 아래와 같습니다.

 

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

 

이 과정의 실제 코드를 보면서 단계별로 설명하도록 하겠습니다. XMPP smack 라이브러리를 이용하여 쉽게 구현할 수 있었으며 코드는 다음과 같습니다. 먼저 Connect 메소드를 살펴보겠습니다.

 

푸시 서비스 클래스 구현

 

 
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 = "momo-time@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.this);
 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.registerRestartAlram(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.");
 }
 }
 }

 

 

 

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

 

첫 번째 단계는 gTalk 서버과 안드로이드 클라이언트 간의 connection 관련 설정을 셋팅해 주는

단계입니다. XMPP 서버로 gTalk을 사용하므로 Gmail 계정을 이용하게 됩니다.

 

Gmail 계정으로 gTalk 서버에 5222 Port로 연결할 것이라고 설정을 셋팅하게 되는데,

이 부분의 코드는 connect 메소드에서 ConnectionConfiguration 객체를 생성하는 부분입니다.

해당 객체를 생성할 때 인자로 설정 관련 정보들을 넘겨서 셋팅을 완료하게 됩니다.

 

ConnectionConfiguration 객체를 생성한 후, 안드로이드 인증을 위해 별도로 setTruststoreType, setTruststorePath 메소드를 호출하게 됩니다.

 

두 번째 단계는 실제로 gTalk 서버와 연결하는 것입니다.

앞 단계에서 셋팅해 준 설정을 XMPPConnection의 인자로 넘기고, 실제로 connect한다.

connect 메소드 호출을 통해 간단히 연결할 수 있습니다.

 

세 번째 단계는 로그인하고 상태값을 변경하는 것입니다.

수많은 User 중에 특정 사용자가 로그인했음을 gTalk XMPP Server에 알리고, 사용자와 채팅방을 생성할 수 있도록 available한 상태임을 알려줍니다. 메신저에 비유를 해보자면, 전 단계에서 connect한 것은 단순히 메신저 로그인창을 띄운 것이고 이번 단계는 아이디를 입력하여

특정 사용자로 로그인을 하고 ‘온라인’ 상태로 셋팅한 경우라고 볼 수 있습니다.

 

메신저창을 띄워도 실제 로그인을 하지 않는 이상 서버가 어떤 사용자인지 알 수 없으므로 connect만으로 끝내면 안되고, 로그인하고 상태값을 변경하는 이번 단계가 필요합니다. 코드에서 보면 id와 password, resourceId를 가지고 login 메소드를 호출하고, Presence 객체를 만들어 available한 상태임을 셋팅하고 sendPacket 메소드 호출을 통해 상태를 gTalk 서버에 전송합니다.

 

안드로이드와 구글 앱 엔진의 XMPP 통신을 위한 환경 구축 네 번째 단계는 메시지를 받기 위한

리스너를 등록하는 것입니다. ChatListener와 MessageListener 두 가지 리스너를 등록하게 되는데, ChatListener는 채팅방이 만들어지면 불리는 리스너이고, MessageListener는 구글 앱 엔진으로부터 오는 메시지를 받기 위한 리스너이다. 두 가지 리스너를 등록하면 구글 앱 엔진과의 채팅방이 생성되었을 때 알림을 받을 수 있게 됩니다.

 

코드를 보면 getChatManager 메소드 호출을 통해 chatManager를 얻어온 후, chatManager의 addChatListener 메소드를 통해 chatListener를 등록한다. Override한 chatCreated 메소드는

채팅방이 생성되면 호출이 되는데, 채팅방이 생성되었을 때 addMessageListener 메소드를 통해 MessageListener를 등록하게 됩니다. MessageListener의 processMessage 메소드를 보면

메시지를 받았을 때 인텐트를 이용하여 받은 메시지를 인자로 넘겨서 브로드캐스트함으로써

메시지를 전달하고 있음을 알 수 있습니다. 셋팅한 액션을 인텐트 필터로 설정한 리시버를 AndroidManifest.xml 파일에 등록해 놓는다면 리시버가 메시지를 받아서 원하는 처리를 할 수 있게 됩니다.

 

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

 

AndroidManifest.xml에 메시지 받는 리시버 등록

 

 

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

 

 

 

2) 재접속과 네트워크 상태

 

지금까지의 과정을 잘 따라왔다면 구글 앱 엔진과 안드로이드 간의 메시지를 주고 받을 수 있게

됩니다. 그러나 해결해야 할 몇 가지의 이슈들이 있습니다. 위의 코드를 보면서 이야기를 계속 이어나가도록 합시다.

 

채팅방을 생성했다고 해도, 연결이 끊어진다면 XMPP 통신을 할 수 없으므로 주기적으로 연결을

확인해야 합니다. 연결되어있는지 체크하고 연결되어 있지 않다면 다시 연결하는 과정이

필요합니다.

 

코드에서는 loop 메소드를 주기적으로 호출하게 되고, 이 메소드 내에서 연결이 끊긴 경우

connect 메소드를 다시 호출해줍니다.

 

한 가지 더 생각해야할 것은 네트워크 상태입니다.

3G 환경에서 외부에서 돌아다닐 때나 지하철 혹은 엘리베이터 같은 곳 등 네트워크 상태가 불안정한 곳에서 끊기는 경우가 많은데, 이러한 경우 통신 자체가 안되므로 연결할 수 없게 됩니다.

 

다시 말하면, 네트워크가 끊겼을 경우 연결 확인을 계속 하는 것은 의미가 없습니다.

따라서 이러한 경우를 대비하여 네트워크 상태를 주기적으로 확인하고 네트워크가 끊겼을 경우에는 연결 시도를 하지 않도록 예외처리를 해주어야 합니다. 코드의 isNetworkAvailbe 메소드에서

네트워크 상태를 확인하게 되는데, 안드로이드에서 기본적으로 제공되는 ConnectivityManager와 NetworkInfo 클래스를 이용하면 네트워크의 연결 여부를 알 수 있습니다. 이를 통해 네트워크 연결 상태를 확인하고 네트워크가 연결되어 있을 때만 채팅방의 연결을 확인하도록 처리하였습니다.

 

 

3) 초기 서비스 설정

 

통신을 하기 위해서 연결을 계속 확인해야 하는데 이는 서비스를 실행시켜야 함을 의미합니다.

그래서 부팅시 자동으로 서비스를 실행할 수 있는 코드를 별도로 넣어주어야 합니다.

 

처음 설치시에는 초기 인증 액티비티에서 서비스를 실행시키면 됩니다.

부팅시 자동으로 서비스를 실행시키기 위해서, 우선 부팅이 완료되었다는 메세지를 받을 수 있도록 AndroidManifest.xml 파일에 리시버를 등록하고 BOOT_COMPLETED 액션을 받도록 인텐트 필터를 설정해줍니다. 그리고 RECEIVE_BOOT_COMPLETED 퍼미션을 주면 부팅이 완료되었을 때 알림을 받을 수 있습니다. 이 리시버는 인텐트 필터를 하나 더 가지고 있는데, 이것은 서비스가 종료되었을 때 다시 시작해주기 위해 설정해놓은 것입니다. 아래의 PushServiceRestarter.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>
 

 

 

서비스가 종료되었을 때 재실행하기 위한 리시버 구현

 

 

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_WAKEUP, 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 데이터 등에 제한이 있습니다. 이와 관련된 더 자세한 사항은 구글 앱 엔진 제약사항 사이트(http://code.google.com/intl/ko-KR/appengine/docs/quotas.html)를

참조하면 자세히 알 수 있습니다.

 

 

 

 

9. 대용량 메시지 처리

 

앞에서 언급했던 제약사항의 해결 방안으로 몇 가지를 생각해보았습니다.

대안으로 생각해본 것 중 하나는, 직접 서버를 구축하는 것입니다.

 

아마존이나 KT 등 인프라스트럭쳐를 서비스로 제공하는 기업들의 클라우드 컴퓨팅 서비스를

이용하여 직접 XMPP 서버를 운영한다면 대용량 처리의 한계를 극복할 수 있을 것입니다.

구글 앱 엔진을 선택하고 제약사항을 감수하는 것과 직접 서버를 운영하여 제약사항을

극복하는 것은 사용자의 선택에 달려있습니다.

 

아니면 다른 방법으로 다수의 인스턴스를 생성하여 대용량 메시지를 처리할 수도 있습니다.

이러한 경우 인스턴스들을 관리하기 위한 매니저 인스턴스가 별도로 필요할 것입니다.

이러한 것도 해결책으로 고민해 볼 수 있을 것이라 생각하고 이에 대해 궁금하다면 더 찾아보면

좋을 것 같습니다.

 

 

10. 맺음말

 

지금까지 구글 앱 엔진 XMPP를 사용하여 푸시 서비스를 구축하는 과정을 살펴보았다.

몇가지 기본 개념을 이해한다면 어렵지 않게 푸시 서비스를 구축하는 것을 알 수 있습니다.