[iPhone/Java] 내가 만든 어플에 Push Notification 적용하기

사용자 삽입 이미지
오늘은 애플사에서 제공하는 Apple Push Notification Service 줄여서 APNS를 사용해서 나의 어플리케이션이 메시지를 푸싱하는것에 대해 간단하게 정리해 보겠습니다. 애플에서 제공하는 푸시의 경우 위의 프리젠테이션에서 볼 수 있듯이 써드파티의 서버에서 APNS서버로 메시지를 보내면 APNS에서 해당 디바이스에 직접적으로 메시지를 전달해주는 일을 합니다. 실제로 테스트 해본 결과 단 몇초안에 메시지가 전달이 되더군요. 와우!

1. 인증서 준비

사용자 삽입 이미지
위의 화면은 개발자 사이트의 Provisioning Portal – App IDs 에 들어가면 볼 수 있는 화면입니다. 두번째 부분이 Push Notification에 대한 설정인데요. 첫번째 App ID의 경우에는 현재 APNS설정을 할 수 있는 상태입니다. 그리고 두번쨰는 아예 설정이 불가능한 경우입니다. 이 경우는 App ID의 Identifier에 *가 들어간 경우에 해당됩니다. 꼭 어플과 App ID가 1:1매치가 되어야 하겠죠. 3번째는 예상하시다시피 이미 설정이 끝난경우 입니다.

사용자 삽입 이미지
Configure를 눌러 들어간 화면에는 위와같은 화면이 나옵니다. Enable을 눌러주시면 아래의 Configure 버튼이 활성화가 됩니다. 보시면 대충 Push를 보내기 위한 관련 인증서를 발급 받는 부분이라는것을 아실수 있으실텐데요 두가지가 있네요. 한가지는 개발용 하나는 실제 서비스용입니다. 우선은 시범적으로 개발용만 발급받아보겠습니다. 실제 서비스때는 밑의 것을 발급받아 사용하시면 됩니다.

사용자 삽입 이미지
위와 같은 화면이 나옵니다. 아마 대부분의 개발자분들이 이 화면을 보시면 어떤것이 필요한 것인지 감이 오실것이라 생각되네요. 잘 모르시겠다면 [Apple Program 이용가이드]의 인증서 등록 부분을 참고하시면 될것 같네요. 화면이 요즘과 좀 많이 달라지긴 했지만 참고하시는데에는 무리가 없을것입니다. 저때에 사용하셨던 CSR파일을 그대로 이용하시면 됩니다.

사용자 삽입 이미지
잠시후에 자동으로 인증서가 발급됩니다. 위와같이 화면이 뜨게 됩니다.

사용자 삽입 이미지
이제 인증서를 다운받을 수 있게 되는데요, 위에 보이는 Download버튼을 눌러 인증서를 다운 받습니다. 그리고 더블클릭하여 시스템의 인증서로 등록하여 줍니다.

aps_developer_identity.cer – 개발용 인증서
aps_production_identity.cer – 실서비스용 인증서

맥의 유틸리티 – 인증서 메뉴에 가보면 위와 같이 Push Service에 대한 인증서가 등록된것을 확인하실 수 있습니다. 이제 해당 인증서와 아래에 선택한것과 같이 CSR 발급에 사용되었던 개인키(Private Key)를 동시에 선택을 한후에 마우스 오른쪽 버튼을 눌러 “외부로 보내기”(기억이 확실치 않네요; 아무튼 Export입니다)를 하여 줍니다. p12라는 확장자의 파일로 저장이 가능합니다. 이때에 암호를 정할 수 있는데 적절한 암호를 입력하여 줍니다.

2. 아이폰 어플리케이션 수정

이제 아이폰 어플리케이션에 Push Service를 등록할 수 있도록 몇가지 추가를 해야 합니다. -AppDelegate.m 파일에 다음의 내용을 추가합니다.

- (void)application:(UIApplication *)application 
    didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken 
{ 
 NSMutableString *deviceId = [NSMutableString string]; 
 const unsigned char* ptr = (const unsigned char*) [deviceToken bytes]; 

 for(int i = 0 ; i < 32 ; i++) 
 { 
  [deviceId appendFormat:@"%02x", ptr[i]]; 
 } 

 NSLog(@"APNS Device Token: %@", deviceId); 
}

위의 추가된 코드는 어플리케이션이 최초 실행될때에 어플리케이션이 푸시서비스를 이용함을 알리고 허용할지를 물어보게 하고 사용자의 동의를 얻었을 경우 실행됩니다. APNS에 디바이스 정보를 등록하고 64바이트의 문자열을 받아오게 됩니다. 마지막에 NSLog로 확인을 하게 되는 deviceId를 써드파티 서비스의 서버로 전송하여 관리하시면 됩니다.

- (void)application:(UIApplication *)application
    didReceiveRemoteNotification:(NSDictionary *)userInfo
{ 
 NSString *string = [NSString stringWithFormat:@"%@", userInfo]; 
 UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil 
                             message:string delegate:nil
                             cancelButtonTitle:@"OK"
                             otherButtonTitles:nil]; 
 [alert show]; 
 [alert release]; 
}

위의 새로 추가된 메서드는 어플리케이션이 푸시를 받았을 경우 호출되는 메서드입니다. 그냥 받은 메시지를 그대로 찍도록 해보았습니다. 넘어오는 데이터에 대해서는 직접 한번 디버깅하여 보시길 추천해드립니다.

사용자 삽입 이미지

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{ 
 NSDictionary *userInfo = [launchOptions objectForKey:
              UIApplicationLaunchOptionsRemoteNotificationKey]; 

 if(userInfo != nil) 
 { 
  [self application:application didFinishLaunchingWithOptions:userInfo]; 
 } 

 // APNS에 디바이스를 등록한다. 
 [[UIApplication sharedApplication] registerForRemoteNotificationTypes: 
  UIRemoteNotificationTypeAlert| 
  UIRemoteNotificationTypeBadge| 
  UIRemoteNotificationTypeSound]; 

 return YES; 
}

이제 하이라이트입니다. 위의 메서드는 어플리케이션이 실행될때에 호출이 됩니다. 요즘은 URI 스키마 혹은 푸시 서비스를 통해 어플리케이션을 실행할 수 있게 되었고 관련 옵션들이 launchOptions인자로 넘어오게 됩니다.

위의 경우에는 어플리케이션이 실행되어있지 않은 경우 푸시를 받아서 어플리케이션이 실행될때 관련 정보가 launchOptions에 담겨오게 되며 그 정보를 가지고 didFinishLaunchingWithOptions를 재호출하는 로직입니다. 그 뒤에 보면 나와있는 APNS등록 부분이 사용자에게 푸시를 허용할지 물어보는 부분이 되겠습니다.

이때에 알아두실 점은 위의 메서드가 구현된 시점부터 원래 사용되던 다음의 메서드는 사용되지 않습니다.

- (void)applicationDidFinishLaunching:(UIApplication *)application 
{ 
 // didFinishLaunchingWithOptions를 구현할 경우 사용되지 않는다. 
}

3. 푸시 서버 준비

이 블로깅에서는 푸시 서버를 Java기반에서 개발하는것으로 설명합니다. 검색해 보면 PHP, Ruby, Python등 매우 다양한 언어로 이미 포팅되어 잘 만들어진 라이브러리도 많이 있더군요. 저도 역시나 잘 만들어진(실제로 여러 포럼에서 추천받는) javapns를 사용하도록 하겠습니다.

필요한 JAR 파일은 다음과 같습니다.

javapns
log4j
bcprov-ext
commons-io
commons-lang

 

이제 해당 홈페이지에 나와있는 간단한 예제를 실행해 보도록 하겠습니다. 다음 소스에서 사용되는 iPhoneId의 경우 didRegisterForRemoteNotificationsWithDeviceToken 메서드에서 언급되었던 deviceId값이며 certificate의 경우 인증서 준비단계에서 만들었던 p12확장자 파일을 뜻합니다. passwd는 p12를 만들당시에 물어보았던 비밀번호입니다.

public class Push {
     // APNs Server Host & port
     private static final String HOST = "gateway.sandbox.push.apple.com";
     private static final int PORT = 2195;

     // Badge
     private static final int BADGE = 66;

     // iPhone's UDID (64-char device token)
     private static String iPhoneId = "2ed202ac08ea9...cf8d55910df290567037dcc4";
     private static String certificate = "/absolute/path/to/my/certificate";
     private static String passwd = "mycertificatepassword";

     public static void main( String[] args ) throws Exception {

        System.out.println( "Setting up Push notification" );

        try {
             // Setup up a simple message
             PayLoad aPayload = new PayLoad();
             aPayload.addBadge( BADGE );
             aPayload.addAlert("Hello World!");
             aPayload.addSound("default");
             System.out.println( "Payload setup successfull." );

             System.out.println ( aPayload );

             // Get PushNotification Instance
             PushNotificationManager pushManager = 
                PushNotificationManager.getInstance();

             // Link iPhone's UDID (64-char device token) to a stringName
             pushManager.addDevice("iPhone", iPhoneId);
             System.out.println( "iPhone UDID taken." );

             System.out.println( "Token: " +
                pushManager.getDevice( "iPhone" ).getToken() );

             // Get iPhone client
             Device client = pushManager.getDevice( "iPhone" );
             System.out.println( "Client setup successfull." );

             // Initialize connection
             pushManager.initializeConnection( HOST, PORT, certificate, passwd, 
                SSLConnectionHelper.KEYSTORE_TYPE_PKCS12);
             System.out.println( "Connection initialized..." );

             // Send message
             pushManager.sendNotification( client, aPayload );
             System.out.println( "Message sent!" );

             System.out.println( "# of attempts: " +
                pushManager.getRetryAttempts() );
             pushManager.stopConnection();

             System.out.println( "done" );

         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 }

사용자 삽입 이미지

예제코드에 이어 바로 결과 화면을 붙여보았습니다. 위와같이 메시지를 전송하면 잠자고 있던 아이팟이 알람을 울리게 됩니다.

여기서 한가지 더 알아두셔야 할 점으로 Feedback이라는 것이 있습니다. 위와 같은 Push의 경우에는 해당 사용자에게 제대로 메시지가 전달 되었는지 여부를 확인할 길이 없습니다. 만약에 10만명의 푸시 사용자가 가입을 했고 5만명이 해당 어플의 푸시를 받지 않겠다고 설정을 바꾸었거나 삭제를 했다면 서비스는 일일이 받지 않는 사용자에게 푸시를 날리느라 시간을 낭비하게 되겠죠. 그래서 이 Feedback을 이용하여 사용하지 않는 사용자의 리스트를 받아오게 됩니다.

public class Feedback {
     // APNs Server Host & port
     private static final String HOST = "feedback.push.apple.com";
     private static final int PORT = 2196;

     private static String certificate = "/absolute/path/to/certificate";
     private static String passwd = "certificatePassword";

     public static void main( String[] args ) throws Exception {

        try {
             // Get FeedbackServiceManager Instance
             FeedbackServiceManager feedbackManager = 
                FeedbackServiceManager.getInstance();

             // Initialize connection
             LinkedList<Device> devices = feedbackManager.getDevices( HOST, PORT, 
                certificate, passwd, SSLConnectionHelper.KEYSTORE_TYPE_PKCS12 );
             System.out.println( "Connection initialized..." );

             System.out.println( "Devices returned: " + devices.size() );

             ListIterator<Device> itr = devices.listIterator();
             while ( itr.hasNext() ) {
                 Device device = itr.next();
                 System.out.println( "Device: id=[" + device.getId() +
                " token=[" + device.getToken() + "]" );
             }

             System.out.println( "done" );

         } catch (Exception e) {
             e.printStackTrace();
         }
     }
}

사용자 삽입 이미지

테스트 몇번 해보았을 뿐이라 사용하지 않는 사용자는 존재하지 않는군요. 위와같은 명령을 하루 한번정도의 텀으로 주기적으로 실행하여 발송 목록에서 제거하는 등의 처리를 하시면 되겠습니다. 더 많은 관련 문서는 javapns의 WIKI 페이지에 잘 나와있습니다^^