푸시 메시지 포맷

아이폰 2011. 4. 7. 23:39



 Provider와 APNS 간에 SSL(Secure Socket Layer)를 통해 통신을 하며, 서로 간에 올바른 통신을 위한 몇 가지
요구사항이 존재한다.   다음은  Local, Remote Notification Programming Guide 문서에서 일부 발췌한 내용이다.
자세한 내용은 실제 문서를 참고하자. 


1. Provider가 APNS와 통신하기 위한 요구사항
 〮 Provider는 Apple Push Notification Service와 바이너리 인터페이스를 사용해 통신함
 〮 TCP Socket을 사용하며, 안전한 통신채널을 위해 TLS(또는 SSL)을 사용함
 〮 APNS와의  잦은 연결과 종료는 DOS 공격으로 오인받을 수 있음 
 〮 에러가 발생하면 APNS는 해당연결을 종료함 
 〮 서버에 접속해서 반복적으로 푸시전송에 실패한 클라이언트 디바이스 리스트를 조회할 수 있음 => Feedback service



2. Binary Interface and Notification Formats
〮순수 TCP 소켓을 사용하며, 최적의 성능을 위해서는 단일 연결을 맺고, 다중 통지를 보내야함 
〮인터페이스는 simple 포맷, enhanced 포맷 2가지의 notification format을 지원함 
〮Simple 포맷에서는 payload 데이터가 제한크기를 초과하면 통지를 거부하지만, enhanced 포맷에서는
    notification 에 임의의 식별자를 할당하여, 에러가 발생하면 식별자와 연관된 에러코드를 확인할 수 있음


위에서 언급된 것처럼 2가지 종류의 메시지 형태가 존재한다. 대부분의 앱에서는 Simple 포맷으로도 충분하다. 하지만    
Simple 포맷은 실제로 푸시 메시지가 전송됐는지를 보장하지 않기 때문에,   푸시 메시지에 대한 전송 신뢰성이 보장되야 할경우엔는
Enhanced 포맷을 사용하자. Enhanced 포맷에서는 메시지 전송 실패시 에러를 반환하기 때문에,  Provider가 주기적으로 APNS에서 
이 값을 체크하여 실패시 다시 전송을 요청할 수 있다. 



3. Simple Format
APNS와  SSL 연결과, peer-exchage 인증을 가정한다. (SSL을 사용한 1:1 통신을 한다는 의미같다.)
메시지 포맷은 다음과 같다. 




Command : Simple 포맷인지 Enhanced  포맷인지 구별하기 위한 값. simple 포맷은 0을 갖는다.
Token length     : DeviceToken의 길이
Device Token    : 바이너리 포맷으로 인코딩 되야함
Payload length : 푸시로 전달할 메시지의 길이로 256 바이트를 초과할 수 없으며,
                           널(null)문자로 종료해서는 안됨
* token length와 payload length 는 네트워크 오더(big endian)이어야 함




3.1 SimpleFormat을 사용하는 예제코드 
Local, Remote Notification Programming Guide 문서에 나와있는 샘플 코드이다. 실제 메시지 전송을 구현해야할 경우 참고하자

static bool sendPayload(SSL *sslPtr, char *deviceTokenBinary, char *payloadBuff, size_t payloadLength)
{
        bool rtn = false;
        if (sslPtr && deviceTokenBinary && payloadBuff && payloadLength) {
        uint8_t command = 1; /* command number */ 
        char binaryMessageBuff[sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t)
                + sizeof(uint16_t) + DEVICE_BINARY_SIZE + sizeof(uint16_t) + MAXPAYLOAD_SIZE];
        
        /* message format is, |COMMAND|ID|EXPIRY|TOKENLEN|TOKEN|PAYLOADLEN|PAYLOAD|  */
        char *binaryMessagePt = binaryMessageBuff; 
        uint32_t whicheverOrderIWantToGetBackInAErrorResponse_ID = 1234; 
        uint32_t networkOrderExpiryEpochUTC = htonl(time(NULL)+86400); 
        // expire message if not delivered in 1 day 

        uint16_t networkOrderTokenLength = htons(DEVICE_BINARY_SIZE); 
        uint16_t networkOrderPayloadLength = htons(payloadLength);
        
        /* command */ 
        *binaryMessagePt++ = command;
        
        /* provider preference ordered ID */
        memcpy(binaryMessagePt, &whicheverOrderIWantToGetBackInAErrorResponse_ID, sizeof
                                                                                                                (uint32_t));
        binaryMessagePt += sizeof(uint32_t);

        /* expiry date network order */ 
        memcpy(binaryMessagePt, &networkOrderExpiryEpochUTC, sizeof(uint32_t)); 
        binaryMessagePt += sizeof(uint32_t);
        
        /* token length network order */ 
        memcpy(binaryMessagePt, &networkOrderTokenLength, sizeof(uint16_t)); 
        binaryMessagePt += sizeof(uint16_t);

        /* device token */ 
        memcpy(binaryMessagePt, deviceTokenBinary, DEVICE_BINARY_SIZE); 
        binaryMessagePt += DEVICE_BINARY_SIZE;

        /* payload length network order */ 
        memcpy(binaryMessagePt, &networkOrderPayloadLength, sizeof(uint16_t)); 
        binaryMessagePt += sizeof(uint16_t);

        /* payload */ 
        memcpy(binaryMessagePt, payloadBuff, payloadLength); 
        binaryMessagePt += payloadLength; 
        
        if (SSL_write(sslPtr, binaryMessageBuff, (binaryMessagePt -binaryMessageBuff)) > 0) 
                rtn = true;
        } 
        return rtn;
}



4. Enhanced Format

Enhanced Format 를 사용하여 메시지를 전송하면, APNS가 인지 불가능한 명령을 만났을 경우 연결을 종료하기 전에 에러 응답을
반환해주어 에러의 원인을 확인할 수 있다. Provider 에서는 주기적으로 APNS의 에러응답에 접근하여 결과를 확인할 수 있다. 



first byte: 1
identifier: notification을 식별하기 위한 임의의 값. 에러가 발생하면 APNS는 이값을 반환함
expiry : notification이 더이상 유효하지 않는 때를 나타내는 초로 표현된 시간.  fixed UNIX epoch  date(UTC)
...나머지 필드는 Simple Format과 동일하다.




4.1 Enhanced Format을 사용하는 예제코드 
역시 실제 구현시에 참고하자. 
 
static bool sendPayload(SSL *sslPtr, char *deviceTokenBinary, char *payloadBuff, size_t payloadLength)
{
        bool rtn = false;
        if (sslPtr && deviceTokenBinary && payloadBuff && payloadLength) {
        uint8_t command = 1; /* command number */ 
        char binaryMessageBuff[sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t)
                + sizeof(uint16_t) + DEVICE_BINARY_SIZE + sizeof(uint16_t) + MAXPAYLOAD_SIZE];
        
        /* message format is, |COMMAND|ID|EXPIRY|TOKENLEN|TOKEN|PAYLOADLEN|PAYLOAD|  */
        char *binaryMessagePt = binaryMessageBuff; 
        uint32_t whicheverOrderIWantToGetBackInAErrorResponse_ID = 1234; 
        uint32_t networkOrderExpiryEpochUTC = htonl(time(NULL)+86400); 
        // expire message if not delivered in 1 day 

        uint16_t networkOrderTokenLength = htons(DEVICE_BINARY_SIZE); 
        uint16_t networkOrderPayloadLength = htons(payloadLength);
        
        /* command */ 
        *binaryMessagePt++ = command;
        
        /* provider preference ordered ID */
        memcpy(binaryMessagePt, &whicheverOrderIWantToGetBackInAErrorResponse_ID, sizeof(uint32_t));
        binaryMessagePt += sizeof(uint32_t);

        /* expiry date network order */ 
        memcpy(binaryMessagePt, &networkOrderExpiryEpochUTC, sizeof(uint32_t)); 
        binaryMessagePt += sizeof(uint32_t);
        
        /* token length network order */ 
        memcpy(binaryMessagePt, &networkOrderTokenLength, sizeof(uint16_t)); 
        binaryMessagePt += sizeof(uint16_t);

        /* device token */ 
        memcpy(binaryMessagePt, deviceTokenBinary, DEVICE_BINARY_SIZE); 
        binaryMessagePt += DEVICE_BINARY_SIZE;

        /* payload length network order */ 
        memcpy(binaryMessagePt, &networkOrderPayloadLength, sizeof(uint16_t)); 
        binaryMessagePt += sizeof(uint16_t);

        /* payload */ 
        memcpy(binaryMessagePt, payloadBuff, payloadLength); 
        binaryMessagePt += payloadLength; 
        
        if (SSL_write(sslPtr, binaryMessageBuff, (binaryMessagePt -binaryMessageBuff)) > 0) 
                rtn = true;
        } 
        return rtn;
}



5. Error 응답 포맷
notification 포맷을 인식할 수 없을 경우 APNS는 연결을 종료하기 전에 error 응답을 전송해준다.  error가 없다면 어떠한 값도 
반환하지 않는다.  에러 응답의 포맷은 다음과 같다. 



first byte: 8
status : 상태코드
identifier : notification을 구성할 때 사용한 식별자 

상태코드
0 : 에러없음
1 :  에러 처리중
2 : device token 이 없음
3 : topic이 없음
4 : payload 없음
5 : 잘못된 token 크기
6 : 잘못된 topic 크기
7 : 잘못된 payload 크기
8 : 잘못된 token
255 : 알수없음




'아이폰' 카테고리의 다른 글

수동으로 UI 컨트롤에 이벤트 전달  (0) 2011.11.13
NSNotification  (1) 2011.04.29
푸시 메시지 포맷  (0) 2011.04.07
애플 푸시 서비스  (0) 2011.04.07
Sprite Sheet 제작툴 Zwoptex  (0) 2011.03.16
애플리케이션 응답성 향상을 위해 동시성 사용하기  (0) 2011.03.15

애플 푸시 서비스

아이폰 2011. 4. 7. 23:01



이 포스팅에서는 애플 푸시 서비스를 사용하는 방법에 대해서 알아보자. 작년 겨울 앱공모전을 준비하면서 "분실물 다나와"
라는 앱을 개발했다.  서울 분실물 센터에 수거되어 있는 분실물 목록을 조회할 수 있고, 등록된 분실물과 유사한 분신물 목록을
사용자 아이폰으로 알리는 앱이었다. 이때 유사 분실물 발견유무를 푸시 기능을 통해 구현하였다. 당시에 푸시와 관련된 
자료가 별로 없어서(내가 못찾았겠지만...) 애플 개발자 가이드 문서를 참고하여 구현하였다. 그 때 정리한 내용을 올려본다.
(사진 캡처를 안해서 이해가 잘 안될 수도....-_-;; )




1. Push Service 란?
서버에서 아이폰 애플리케이션으로 데이터를 역으로 전송할 수 있는 서비스이다. 
Remote  Notification을 등록하고 수신하는 애플리케이션과 푸시 서비스를 제공하는 APNS(Apple Push Notification Service),
APNS에 데이터 푸시요청을 수행할 수 있는 권한을 획득한  Provider(서버) 로 구성된다.



"분실물 다나와" 프로젝트로 예를 들면...Provider는 "분실물 다나와" 서버가 되고, Client App 은 "분실물 다나와" 아이폰 앱,
APNS는 애플에서 제공하는 푸시서버가 된다.  iPhone에 전송된 푸시메시지는 iOS 를 거쳐 실제 앱에 전달된다. 




2. Push 서비스 사용절차 
APNS는 실제 아이폰 기기로 푸시를 전송해주는 애플측에서 제공하는 서비스이다. 푸시서비스를 이용하는 서버인 Provider는
푸시할 데이터만을 가공하여 APNS에 푸시를 요청한다.  Provider가 APNS에 접속하여 푸시데이터를 전송하려 할 때, APNS는
푸시요청을  할수 있는 권한을 SSL 인증을 통해 검사하게 된다. 그래서 Provider는 사전에 이 인증을 위한 암호화 파일을 사전에
가지고 있어야 한다. 


① 개발자 포탈에서 애플리케이션에 대한 Push Serivce 사용권한 받기 
개발자 포탈에서 App Id에 대한  Development 또는 Product 푸시 서비스 옵션을 enable 시키고,  다운받은 인증파일을 KeyCain에
등록시킨다.  이 파일을 p12 파일로  export 하여, 파일, 비밀번호와 함께  Provider 에게 사전에 전달해야 한다. 


② 아이폰 애플리케이션 실행시 APNS에 RemoteNotification 등록 
아이폰 애플리케이션이 처음 구동시 APNS에 RemoteNotification 을 등록하고, 애플리케이션 DeviceToken을 수신받는다. 


③ Provider에게 DeviceToken 전송  
DeviceToken을 수신하면, 서버에 아이폰 UUID와 함께 전달한다. 이 토큰은 APNS에서 아이폰의 애플리케이션을 식별하는 키
되기 때문에 Provider가 보유하고 있어야 하는 데이터이다.  DeviceToken은 거의 변하지 않지만, 애플리케이션이 삭제되고 다시
설치될 때 변할 수도 있기 때문에 애플리케이션이 실행될 때마다,  Provider에게 전송하여 갱신여부를 확인하는 것이 좋다. Development용과 Product용의 DeviceToken 값은 다르다는 것에 주의한다. 
 




④ Provider 에서 푸시 메시지를 생성하여  APNS에 푸시 요청 
Provider 에서는 ①단계에서 전달받은 인증파일을 보유하고 있고, 인증파일 비밀번호를 알고 있어야 한다.
DeviceToken와 함께 다음의 주소로 push를 요청할 수 있다. 개발환경과 실제 서비스를 위한 APNS 의 주소가 다르다.
 
개발환경 푸시서버 주소: gateway.sandbox.push.apple.com / 2195
제품환경 푸시서버 주소: gateway.push.apple.com / 2195



⑤ APNS 에 전송하는 메시지의 포맷 
메시지는 JSON 포맷을 따르며 256바이트 이내야한다. 메시지를 구성하는 키는 다음과 같다. 


단순한 메시지 포맷은 위와 같으며, 좀더 세부적으로 메시지를 지정할 수도 있다. 더 자세한 사항은
Local, Remote Notification Programming Guide 를 참고하자. 

JSON 형태로 생성된 메시지 형태는 다음과 같다. 
{"aps":{"sound":"default","alert":"My alert message","badge":45}}

 
 
⑥ APNS에 푸시 요청하기 
Provider는 APNS가 요구하는 프로토콜을 따라 데이터를 전송해야 한다. 프로토콜은 APNS에 전송하는 Push 메시지 포맷을
참고하자.  이 메시지 포맷과 APNS 의 요구사항에 맞게 구현한 javapns라는 오픈소스 라이브러리를 사용하여 APNS에 푸시를
요청해보자 

javapns를 사용하기 위해서 아래의 의존 라이브러리를 가지고 있어야한다.
commons-lang-2.4.jar   // String클래스 등의 유틸리티 기능을 제
commons-io-1.4.ja
bcprov-jdk16-145.jar    // Bouncy Castle 단체의 암호화 관련 api
log4j-1.2.15.ja

 javapns 오픈소스 라이브러리 :  http://code.google.com/p/javapns/ 
Legion of the Bouncy Castle 사이트 : http://www.bouncycastle.org/java.html


다음은 javapns 라이브러리를 사용하여 Provider 에서 APNS 에 푸시를 요청하는  예이다. 
String password = "1q2w3e4r";
String certicatePath = "/home/CertificateSR/Interesting_Dev_Cert_pass.p12";
String apnsAddress = "gateway.sandbox.push.apple.com";
String apnsPort = "2195";

try {
        PayLoad payload = new PayLoad();
                                
        payload.addAlert(pushMessage.getAlert());
        payload.addBadge(pushMessage.getBadge());
        payload.addSound(pushMessage.getSound());
                                
        HashMap customMap =  (HashMap) pushMessage.getCustomData(); 
        Iterator iterator = customMap.keySet().iterator();
        
        while (iterator.hasNext()) {
                String key = iterator.next();
                payload.addCustomDictionary(key, customMap.get(key));
        }
                                
        PushNotificationManager manager = PushNotificationManager.getInstance();
                        
        manager.addDevice("myIphone", pushMessage.getDeviceToken());
        
        Device device = PushNotificationManager.getInstance().getDevice("myIphone");
        manager.initializeConnection( apnsAddress, 
                                          Integer.parseInt(apnsPort), 
                                          certicatePath, 
                                          password,                                                                            
                                          SSLConnectionHelper.KEYSTORE_TYPE_PKCS12);
        manager.sendNotification(device, payload);

        manager.stopConnection();
        manager.removeDevice("myIphone");
                                
} catch (Exception e) {
        e.printStackTrace();
}

 


⑦  애플리케이션측에서 Push를 수신하기 위해서  UIApplicationDelegate 프로토콜의 메소드를 구현 

(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        // 배지넘버 초기화
        application.applicationIconBadgeNumber = 0;

        NSDictionary *aps = 
          [launchOptionsobjectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
        
        // 애플리케이션을 처음실행 : RemoteNotification을 등록함
        if (aps == nil) {       
                [[UIApplication sharedApplication]              
                                                registerForRemoteNotificationTypes:
                                                        (UIRemoteNotificationTypeAlert | 
                                                         UIRemoteNotificationTypeSound | 
                                                         UIRemoteNotificationTypeBadge)

                ];
                        
        } else {
                // 애플리케이션이 원격 통보에 의해 실행됐음
                // alert 추출 
                NSString *alert = [aps objectForKey:@"alert"];
                // custom 데이터 추출 
                NSString *pushSeq = [userInfo objectForKey:@"pushSeq"];
        }

        return YES;

}

// RemoteNotification 등록 성공. deviceToken을 수신
(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
        // Provider에게 DeviceToken 전송
        //[service registDeviceToken:[deviceToken description]];
}


// APNS 에 RemoteNotification 등록 실패
(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
        NSLog(@"fail RemoteNotification Registration: %@", [error description]);
}


// 애플리케이션 실행 중에 RemoteNotification 을 수신
(void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
        // push 메시지 추출
        NSDictionary *aps = [userInfo objectForKey:@"aps"];
        // alert 추출 
        NSString *alert = [aps objectForKey:@"alert"];
        // custom 데이터 추출 
        NSString *pushSeq = [userInfo objectForKey:@"pushSeq"]; 
}


RemoteNotification이 성공적으로 등록되면, 응답으로 호출되는 didRegistForRemoteNotificationWithDeviceToken 메시지의 파라미터에
DeviceToken 값이 담겨져 온다. 이  Token의 값은 다음과 같은 형태를 갖는다.  

<12345678 12345678 12345678 12345678 50cfb1 3f5febbe497df>

APNS에 전송하기 하기 위해서는  애플리케이션 또는 Provider 측에서 <, >, 공백문자를 제거해야 한다는 것을 잊지 말자!! 

 지금까지 애플 푸시서비스에 대해 알아보았다.  사실 푸시 서비스를 이용하는 과정은 그렇게 어렵지 않다. 오히려 개발자 포탈에서
푸시 서비스 권한을  설정하고, 키체인에서 인증파일을 생성하는 과정이 번거롭고... 푸시를 받을 앱이 실행중일 때와 실행중이지 
않을 때 푸시 수신 시 동작방식을 달리 처리해줘야 하는 것이 조금 번거로울 뿐이다.  다음 포스팅에서는 실제 푸시메시지 포맷에 
대해서 좀더 자세히 알아보자.
 



'아이폰' 카테고리의 다른 글

NSNotification  (1) 2011.04.29
푸시 메시지 포맷  (0) 2011.04.07
애플 푸시 서비스  (0) 2011.04.07
Sprite Sheet 제작툴 Zwoptex  (0) 2011.03.16
애플리케이션 응답성 향상을 위해 동시성 사용하기  (0) 2011.03.15
OpenGL ES 게임 프레임웍  (0) 2011.02.14

리플렉션으로 Getter 와 Setter 검사하기

언어로그/Java 2011. 4. 7. 20:35


리플렉션을 사용하여 해당 클래스가 어떤 getter와 setter를 갖는지 검사하는 예를 알아보자.


클래스에 getter와 setter 메서드만을 검사할 수는 없으며,  모든 메서드들을  스캔하여 getter인지 setter인지를 확인해야 한다.
getter와 setter 메서드가 나타내는 특성은 다음과 같다. 

Getter 는  “get” 이라는 이름으로 시작하며, 파라미터가 없고 하나의 값을 반환한다.
Setter 는  “set” 이라는 이름으로 시작하며, 하나의 파라미터를 취한다. 

몇몇 setter는 값을 반환하지 않을 수도 있고 또 어떤 것은 집합값을 반환하거나 어떤 것들은 메서드 chaining을 위해
값을 반환할 수 있기 때문에  setter의 반환타입에 대한 가정을 해서는 안된다.
다음은 getter와 setter를 찾아 출력하는 예이다.  

package com.tistory.hiddenviewer.reflection.executor; import java.lang.reflect.Method; import com.tistory.hiddenviewer.reflection.Board; public class GetterSetterPrinter { public static void main(String[] args) { printGettersSetters(Board.class); } public static void printGettersSetters(Class aClass){ Method[] methods = aClass.getMethods(); for(Method method : methods){ if(isGetter(method)) { System.out.println("getter: " + method); } if(isSetter(method)) { System.out.println("setter: " + method); } } // for } public static boolean isGetter(Method method){ // get 으로 시작하지 않으면 반환 if(!method.getName().startsWith("get")) { return false; } // 파라미터가 있으면 반환 if(method.getParameterTypes().length != 0) { return false; } // 반환값이 없으면 반환 if(void.class.equals(method.getReturnType())) { return false; } return true; }

public static boolean isSetter(Method method){ // set 으로 시작하지 않으면 반환 if(!method.getName().startsWith("set")) { return false; } // 파라미터가 개수가 1이 아니면 반환 if(method.getParameterTypes().length != 1) { return false; } return true; } }



출력결과


 


'언어로그 > Java' 카테고리의 다른 글

Inner Class(내부 클래스)  (0) 2011.04.12
컬렉션(Collection)  (0) 2011.04.11
리플렉션으로 Getter 와 Setter 검사하기  (0) 2011.04.07
(2) 리플렉션(Reflection) 사용하기  (0) 2011.04.07
(1) 리플렉션(Reflection)  (3) 2011.04.07
예외처리 (Exception Handling)  (0) 2011.03.24

(2) 리플렉션(Reflection) 사용하기

언어로그/Java 2011. 4. 7. 19:53



지난 포스팅에 이어 리플렉션을 사용하여 객체를 생성하고, private 필드에 접근하는 방법 그리고  메서드를 호출하는
방법에 대해 알아보자.



1. 리플렉션을 사용하여 객체생성
리플렉션을 사용하여 런타임에 클래스에 생성자들을 검사하고,  생성자 객체를 통해 객체를 생성하는 과정을 알아보자.
아래와 같이 3가지 단계를 거치게 되며 가장 먼저 클래스의 생성자 객체  java.lang.reflect.Consturctor 를 얻어야 한다. 


1.1  Constructor 객체 획득하기
다음과 같이 Class 객체로부터 Constructor 클래스를 얻는다. 
// 생성자 목록 얻기 
Class aClass = ... // 이전에 얻은 클래스 객체
Constructor[] constructors = aClass.getConstructors();


Constructor[] 배열은 클래스에 선언된 모든 public 생성자의 Constructor 인스턴스를 가집니다.  특정한 파라미터를 갖는
특정한 생성자는 다음과 같이 얻을 수 있다. 
// 특정 파라미터를 갖는 생성자 얻기 
Class aClass = ... // 이전에 얻은 클래스 객체
Constructor constructor =
        aClass.getConstructor(new Class[]{String.class});


위 예는 하나의 String 타입 파라미터를 갖는 생성자를 반환하는데, 일치하는 파라미터를 갖는 생성자가 없으면 
NoSuchMethodException 예외가 발생한다.


1.2 Constructor 파라미터 얻기  
 다음과 같이 생성자에 포함된 파라미터 타입 목록을 얻을 수 있다.
// 생성자의 파라미터 타입목록 얻기
Class aClass = ... // 이전에 얻은 클래스 객체
Class[] parameterTypes = constructor.getParameterTypes();


1.3 Constructor 객체를 사용하여 객체 생성하기
다음과 같이 생성자 객체로부터 객체를 생성한다. 
// 하나의 String 파라미터를 갖는 생성자를 얻는다. 
Constructor constructor = MyObject.class.getConstructor(String.class);
// 생성 
MyObject myObject = (MyObject)constructor.newInstance("constructor-arg1");

Constructor.newInstance() 메서드는 선택적인 개수의 파라미터를 취한다.  하지만 반드시 해당 생성자에 맞는 개수와 타입의 
파라미터를 제공해야 한다는 것에 주의하자.
 


 

2. 리플렉션을 사용하여 Field 에 접근하기
리플렉션을 사용하여 클래스의 모든 멤버 변수를 검사할 수 있으며, 런타임에 값을 얻어오거나 설정할 수 있다.
이때 하나의 프로퍼티 당 하나의 java.lang.reflect.Field 클래스 객체를 사용하게 된다. 

2.1 Field  객체 얻기 
다음과 같이 Field 객체를 얻는다. 
// 클래스에 선언된 public 속성의 Field 객체얻기
Class aClass = ... // 이전에 얻은 클래스 객체
Field[] fields = aClass.getFields();


Field[] 배열은  클래스에 선언된 각 public field  당 하나의 Field 객체를 갖는다.(public field 만을 갖는다는 것에 주의)
접근하려는 필드의 이름을 안다면,  다음과 같이 접근할 수  있다. 
// Field에 접근하기 
Class  aClass = MyObject.class
Field field = aClass.getField("someField");


위 예제는 아래 MyObject 에 선언된 someField 에 대응하는 Field 인스턴스를 반환합니다. 
public class MyObject{
  public String someField = null;
}

 getField() 메서드의 파라미터에 해당하는 이름의 필드가 클래스에 존재하지 않으면  NoSuchFieldException 예외가 발생한다.


2.2 Field 이름 얻기 
Field 인스턴스를 획득하면, 다음과 같이 Field.getName()을 사용하여 이름을 얻을 수 있다.
// Field 이름 얻기
Field field = ... // Field 객체를 얻는다. 
String fieldName = field.getName();


2.3 Field  타입 얻기
Field.getType() 메서드를 사용하여 필드의 타입을 얻을 수 있다. 
// Field 타입 얻기 
Field field = aClass.getField("someField");
Object fieldType = field.getType();


2.4 Field 값 조회하고 설정하기 
Field 에 대한 참조를 얻게되면, Field.get(), Field.set() 메소드를 사용하여 값을 얻거나 설정할수 있다.
// Field 값 설정하고 조회하기
Class  aClass = MyObject.class
Field field = aClass.getField("someField");
MyObject objectInstance = new MyObject();
Object value = field.get(objectInstance);
field.set(objetInstance, value);

field.get(), set() 메서드에는 해당 필드를 소유하는 객체가 인자로 전달되야 하며 만일 static 필드라면 null을 전달한다.
 
  
2.5 Private Field 에 접근하기 
클래스의 Private 필드는 외부클래스에서 접근 할 수 없지만,  리플렉션을 사용하면 접근이 가능하다.  캡슐화를 깨는 동작일 
수 있지만, 단위테스트와 하이버네이트와 같은 프레임워크에서 유용하게 사용되기도 한다. 
private 필드에 접근하기 위해서는 Class.getDeclaredField(String name)와 Class.getDeclaredFields() 메서드를 사용한다. (Class.getField(String name) 와 Class.getFields() 메서드는 public 필드만을 반환한다.)

다음은 private field에 접근하는 예이다. 

Board board = new Board();
board.setContents("test contents...");
		
Field field = cls.getDeclaredField("contents");
field.setAccessible(true);
String contents = (String) field.get(board);
		
System.out.println("Private Contents Field: " + contents);

Field 객체의 setAccessible(true)를 호출하지 않고, private  필드값을 조회하려고 하면 IllegalAccessException 예외가 발생한다. 





3. 리플렉션을 사용하여 Method 호출하기
 java.lang.reflect.Method 클래스를 사용하여  메서드를 검사하고 호출 하는 방법을 알아보자.



3.1 Method 객체 얻기  
Method 클래스를 다음과 같이 획득한다.  
// Method 객체목록 얻기 
Class aClass = ... // 이전에 얻은 클래스 객체
Method[] methods = aClass.getMethods();


역시 클래스에 선언된 public 메서드당 하나의 Method 인스턴스를 갖으며, 메서드의 구체적인 파라미터 타입들을 알고 있으면
해당 메서드의 인스턴스를 얻을 수 있다. 다음은 String 파라미터 하나를 갖는 doSomething 메소드의 Method 객체를 얻는다.
// 파라미터를 갖는 메소드의 Method 객체 얻기 
Class aClass = ... // 이전에 얻은 클래스 객체
Method method = aClass.getMethod("doSomething", new Class[]{String.class});


만일 일치하는 메서드가 없으면, NoSuchMethodException이 발생한다. 파라미터가 없는 메서드를 얻기 위해서는 파라미터
배열 대신 null을 전달한다.
// 파라미터가 없는 메서드의 Method 객체 얻기 
Class aClass = ... // 이전에 얻은 클래스 객체
Method method = aClass.getMethod("doSomething", null);



3.2 Method 파라미터와 반환값 얻기
다음과 같이 해당 메서드의 파라미터들을 얻을 수 있다.
// 메서드의 파라미터 타입목록 얻기 
Method method = ... //
Class[] parameterTypes = method.getParameterTypes();

메서드의 반환타입은 다음과 같이 접근한다.
// 메서드의 반환값 타입 얻기 
Method method = ... //
Class returnType = method.getReturnType();



3.3  Method 객체를 사용하여 메서드 호출하기(Invoking)
다음과 같이 메서드를 호출할 수 있다.
// 메서드 호출 
Method method = MyObject.class.getMethod("doSomething", String.class);
Object returnValue = method.invoke(null, "parameter-value1");

invoke() 메소드에는 호출하기를 원하는 객체를 전달하며, static 메서드이면 null을 대신 전달한다.
Method.invoke(Object target, Object...parameters) 메서드는 선택적인 개수의 파라미터를 취하지만,  메서드가 필요로 하는
정확한 개수의 파라미터를 전달해야 한다. 


3.4 Private Method 에 접근하기 
Private Field 에 접근하는 것과 유사하게 Class.getDeclaredField(String name)와 Class.getDeclaredFields() 메서드를 사용한다. 
(Class.getField(String name) 와 Class.getFields() 메서드는 public 필드만을 반환한다.)


'언어로그 > Java' 카테고리의 다른 글

컬렉션(Collection)  (0) 2011.04.11
리플렉션으로 Getter 와 Setter 검사하기  (0) 2011.04.07
(2) 리플렉션(Reflection) 사용하기  (0) 2011.04.07
(1) 리플렉션(Reflection)  (3) 2011.04.07
예외처리 (Exception Handling)  (0) 2011.03.24
[Java] 어노테이션 사용하기  (1) 2011.03.19

(1) 리플렉션(Reflection)

언어로그/Java 2011. 4. 7. 16:47




자바에서 리플렉션은 유연성을 제공하기 위해 필수적인 기법이다. 물론 리플렉션이 없더라도 훌륭한 코드를
작성할 수 있다. 하지만 리플렉션을 사용하면 좀더 유연한 프로그램을 작성할 수 있다.  자바에서 리플렉션을
이해하기 위해서,  자바 클래스 파일은 바이트 코드로 컴파일 되며 실행시간에 이 바이트 코드가 해석되어 
실행된다는 것을 아는 것이 첫 출발점이 된다.  이 바이트 코드에는 클래스에 대한 모든 정보를 포함하고 있다.  
클래스 파일이 있는 위치와 이 클래스 파일의 이름을 알수 있다면 언제든지 바이트 코드를 뒤져서 클래스에 대한
정보를 얻어낼 수 있다.  이제부터 리플렉션을 통해 어떻게, 어떤 정보를 얻을 수 있는지 알아보자. 



1. 리플렉션(Reflection) 
리플렉션은  구체적인 클래스 타입을 알지 못해도,  컴파일된 바이트 코드를 통해 역으로 클래스에 정보를 알아내어
클래스를 사용할 수 있는 기법을 의미한다.  마치 거울에 비친 모습과 유사하여 리플렉션이란 이름을 붙힌 것 같다. 



2. 리플렉션을 사용하는 이유
 리플렉션은  조합(Composition)과 함께 사용되어 다형성을 구현하는 강력한 도구이다. 조합을 사용하여 교체할 수 있는 
위임 클래스를 리플렉션을 통해 동적/정적으로 생성하고 교체하는 방식으로 사용된다.  프레임워크에서 유연성이 있는 동작을
위해 자주 사용되는 방식이기도 하다.  


3. 리플렉션을 통해 얻을 수 있는 정보 
리플렉션을 통해 얻을 수 있는 정보에 대해서 알아보자. 
 
ClassName
Class Modifiers (public, private, synchronized 등)
Package Info
Superclass
Implemented Interfaces
Constructors
Methods
Fields
Annotations

이외에도 더 많은 클래스 정보를 얻을 수 있다. java.lang.Class 클래스에 대한 JavaDoc 문서를 참고하자. 



3.1 Class Object
 클래스 정보를 얻기 위해 가장 먼저 해야할 일은 정보를 담고 있는 java.lang.Class 객체를  획득하는 것이다.
프리미티 타입과 배열 타입을 포함하여 자바의 모든 타입들은 연관된 Class 객체를 가지고 있으며,  컴파일 타임에  
클래스의 이름을 알수 있다면,  다음과 같이  Class 객체를 얻을 수 있다.
 Class myObjectClass = MyObject.class
 컴파일 타임에 이름을 알수 없다면, 런타임에 문자열로 된 이름으로 부터 클래스 객체를 아래와 같이 얻을 수 있다. 
String className =  ... // 클래스 풀네임 
Class myObjectClass = Class.forName(className); 
이때 문자열로 된 클래스 이름은 패키지 경로까지 포함한 풀네임이여야 하며, 해당 패키지에 클래스가 존재하지 않으면
Class.forName 메소드는 ClassNotFoundException 예외를 던지게 된다.


3.1 Class Name
Class 객체로부터 2가지 버전의 클래스 이름을 얻을 수 있다.  getName() 메소드를 사용하면 패키지까지 포함한 풀네임을 얻을 수
있고, getSimpleName() 을 사용하여 패키지가 포함되지 않은 클래스 이름을 얻을 수 있다. 
// 클래스 풀네임 
Class aClass = ... // 이전에 얻은 클래스 객체 
String className = aClass.getName();

// 클래스 심플 네임 
Class  aClass = ... // 이전에 얻은 클래스 객체
String simpleClassName = aClass.getSimpleName();


3.2 Modifier
Class 객체로부터 변경자에 접근할 수 있다.  클래스 변경자는 public, private, static 과 같은 키워드를 의미한다. 
클래스에 대한 플래그 비트가 설정된  int 값을 얻을 수 있으며  java.lang.reflect.Modifier 클래스에 있는 메소드를
통해 해당 플래그가 켜져있는지 확인할 수 있다. 
// 변경자 얻기 
Class  aClass = ... // 이전에 얻은 클래스 객체
int modifiers = aClass.getModifiers();
 
// 변경자 플래그 확인 메소드들
 Modifier.isAbstract(int modifiers)
 Modifier.isFinal(int modifiers)
 Modifier.isInterface(int modifiers)
 Modifier.isNative(int modifiers)
 Modifier.isPrivate(int modifiers)
 Modifier.isProtected(int modifiers)
 Modifier.isPublic(int modifiers)
 Modifier.isStatic(int modifiers)
 Modifier.isStrict(int modifiers)
 Modifier.isSynchronized(int modifiers)
 Modifier.isTransient(int modifiers)
 Modifier.isVolatile(int modifiers)
 
 
3.3 Package Info
다음과 같이 Class 객체로부터 패키지에 대한 정보를 얻는다. 
// 패키지 정보 얻기
Class  aClass = ... // 이전에 얻은 클래스 객체
Package package = aClass.getPackage();

Package 객체로부터 패지지 이름과 같은 정보에 접근할 수 있다. 또한 패키지가 위치한  classpath에 있는jar 파일의
Manifest 파일에서도 이 패키지에 대한 특정한 정보를 얻을 수 있다.  
(예를 들면 Manifest 파일에 지정된  패지키 버전 번호 같은...)


3.4 Superclass
아래와 같이 수퍼클래스의 class 객체를 얻을 수 있다. 
// 수퍼 클래스의 class 객체 얻기 
Class superclass = aClass.getSuperclass();


3.5 Implemented Interfaces
클래스 객체에 의해 구현된 인터페이스의 목록을 얻어보자. 
// 구현한 인터페이스 목록 얻기 
Class  aClass = ... // 이전에 얻은 클래스 객체
Class[] interfaces = aClass.getInterfaces();

수퍼클래스가 구현한 인터페이스지만, 자식클래스가 특별히 해당 인터페이스를 구현한다고 명시하지 않으면,
해당 인터페이스는 목록에 포함되지 않는 것에 주의하자. 구현하는 완전한 인터페이스의 목록을 얻기 위해서는
자신의 수퍼클래스의 구현 인터페이스 목록을 재귀적으로 확인해야 한다.


3.6 Constructors
다음과 같이 클래스의 생성자 목록에 접근한다.
// 클래스 생성자 목록 얻기 
Constructor[] constructors = aClass.getConstructors();



3.7 Methods 
다음과 같이 클래스의 메소드들에 접근한다. 
// 메소드 목록 얻기 
Method[] methods = aClass.getMethods();



3.8 Fields
다음과 같이 클래스의 멤버 변수들에 접근한다. 
// 필드 목록 얻기 
 Field[] fields = aClass.getFields();



3.9 Annotations
다음과 같이 클래스의 어노테이션에 접근한다. 
// 어노테이션 목록 얻기
Annotation[] annotations = aClass.getAnnotations();
 
※ 리플렉션을 사용하여 Annotation을 처리하는 것은 아래 포스트를 참고!!
Java-어노테이션(Annotation)
Java-어노테이션 사용하기




4. 리플렉션 사용 예
게시물 정보를 갖는 Board 클래스에 대한 정보를 리플렉션을 사용하여 출력하는 예를 보자.  
Board 클래스에 대한 정의는 아래와 같다.
package com.tistory.hiddenviewer.reflection;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;

public class Board implements ActionListener{

	public final static String boardName = "MyBoard"; 
	public ArrayList boardList;
	
	public int seq;
	protected String title;
	private String contents;
	
	public Board() {
		this(10);
	}
	
	public Board(int count) {
		this.boardList = new ArrayList(count);
	}
	
	
	public int getSeq() {
		return seq;
	}

	public void setSeq(int seq) {
		this.seq = seq;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getContents() {
		return contents;
	}

	public void setContents(String contents) {
		this.contents = contents;
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		// TODO Auto-generated method stub
		
	}

}



Board 클래스에 대한 정보를 리플렉션을 사용하여 출력하였다.  

package com.tistory.hiddenviewer.reflection.executor;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class BoardReflectionExecutor {


	public static void main(String[] args) throws IOException, ClassNotFoundException {
		
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		System.out.print("생성할 클래스 이름을 입력하세요(패키지 포함): ");
		String className = br.readLine();
		
		Class cls = Class.forName(className);
		
		// 클래스 이름얻기 
		String classFullName = cls.getName();
		String classSimpleName = cls.getSimpleName();
		
		System.out.println("class full name: " + classFullName);
		System.out.println("class simple name: " + classSimpleName);
		
		// 변경자 얻기 
		int modifiers = cls.getModifiers();
		
		if (Modifier.isPublic(modifiers)) {
			System.out.println("class is public class");
		} 
		if (Modifier.isFinal(modifiers)) {
			System.out.println("class is final class");
		}

		// 패키지 얻기 
		Package pkg = cls.getPackage();
		System.out.println("package name: " + pkg.getName());
		
		
		// 수퍼클래스 얻기 
		Class superCls = cls.getSuperclass();
		System.out.println("super class name :" + superCls.getName());
		
		
		// 구현 인터페이스 목록 얻기 
		Class[] interfaces = cls.getInterfaces();
		for (Class cs : interfaces) {
			System.out.println("this class implements " + cs.getName() + " interface");
		}
		
		// 생성자 목록 얻기 
		Constructor[] conturctors = cls.getConstructors();
		for (Constructor constructor : conturctors) {
			System.out.println("Constructor: " + constructor.getName());
		}
		
		// 메서드 목록 얻기 
		Method[] methods = cls.getMethods();
		for (Method method : methods) {
			System.out.println(method.getReturnType() + " " + method.getName() + "(...)");
		}
		
		
		// 프로퍼티 목록 얻기 
		Field[] fields = cls.getFields();
		for (Field field : fields) {
			System.out.println(field.getType() + " " + field.getName());
		}
		
	
		// 어노테이션 얻기 
		Annotation[] annotations = cls.getAnnotations();
		for (Annotation annotation : annotations) {
			System.out.println(annotation.toString());
		}
		
	}

}



출력결과
 


출력 결과를 보면 알겠지만 private,  protected  변경자를 갖는 필드들은 출력되지 않고 public 필드들만 출력이 되었다. 
다음 포스팅에서는  리플렉션을 사용하여 실세로  해당 타입의 객체를 생성 /  private 필드에 접근 / 메소드를 호출 하는 방법에
대해 알아보자.