iPhone, HTTP로 XML 데이터 요청 및 처리 예제

3월 10th, 2010

몇일 전 iPhone에서 HTTP로 XML 데이터 요청하는 예제를 올렸다가 iPhone SDK에서 NSXMLDocument 클래스를 지원하지 않는 다는 것을 확인하고 NSXMLParser 클래스를 이용해서 XML 데이터를 처리하는 예제를 다시 준비했습니다. 아마도 iPhone에서 NSXMLDocument를 지원하지 않는 건 NSXMLDocument가 메모리를 많이 먹기 때문인 아닐까 싶습니다.

NSXMLParser는 XML 문서를 순차적으로 분석(parse)하면서 이벤트별 처리를 위임(delegate)하는 방식입니다. 따라서 NSXMLDocument를 사용하는 예제보다는 훨씬 더 복잡합니다. 참고로 iPhone에서 사용할 수 있는 XML 관련 라이브러리들이 개발되어 있기 때문에 해당 라이브러리를 익혀서 사용하는 것이 개발 효율은 더 좋을 것 같습니다. 하지만 NSXMLParser를 이용해서 구현하는 것도 나름 장점은 있으니 직접 작성하는 것도 나쁘진 않을 듯 합니다.

예제에 포함된 파일 목록은 아래와 같습니다.

파일 설명
showrooms.xml XML 파일
Dealer.h
Dealer.m
Dealer 모델 클래스
Showroom.h
Showroom.m
Showroom 모델 클래스
ShowroomsXmlParse.h
ShowroomsXmlParse.m
showrooms.xml용 파서 클래스
showrooms_xml_parser.m 실행(main) 소스

우선 showrooms.xml은 아래와 같습니다.

<?xml version="1.0" encoding="UTF-8" ?>
 
<showrooms>
  <showroom>
    <name>강남전시장</name>
    <phone>02-111-1111</phone>
    <dealer>
      <name>김태연</name>
    </dealer>
  </showroom>
  <showroom>
    <name>삼성전시장</name>
    <phone>02-222-2222</phone>
    <dealer>
      <name>구하라</name>
    </dealer>
  </showroom>
</showrooms>

예제에서 showroom 및 dealer 엘리먼트가 각각 name 엘리먼트를 자식으로 가지도록 작성했습니다.

Dealer.h와 Dealer.m은 다음과 같습니다.

#import <Foundation/Foundation.h>
 
 
@interface Dealer : NSObject
{
  NSString* _name;
}
  @property( copy ) NSString* name;
@end
#import "Dealer.h"
 
 
@implementation Dealer
  @synthesize name = _name;
@end

Showroom.h와 Showroom.m은 다음과 같습니다.

#import "Dealer.h"
 
 
@interface Showroom : NSObject
{
  NSString* _name;
  NSString* _phone;
  Dealer* _dealer;
}
  @property( copy ) NSString* name;
  @property( copy ) NSString* phone;
  @property( retain ) Dealer* dealer;
@end
#import "Showroom.h"
 
 
@implementation Showroom
  @synthesize name = _name;
  @synthesize phone = _phone;
  @synthesize dealer = _dealer;
@end

NSXMLParser는 NSXMLDocument의 NSXMLNode처럼 모델이 없기 때문에 분석(parse) 결과를 담을 수 있는 모델이 필요합니다. 이 모델 클래스들은 MVC에도 적용될 수 있기 때문에 모델 클래스를 만들어 두면 조금 더 우아한 프로그래밍할 수 있습니다. 모델 클래스들은 간단하기 때문에 자세한 설명은 생략하도록 하겠습니다.

ShowroomsXmlParser.h는 아래와 같습니다.

#import "Showroom.h"
 
 
@interface ShowroomsXmlParser : NSObject < NSXMLParserDelegate >
{
  NSMutableArray* _showrooms;
  NSMutableArray* _elementStack;
}
  - (NSMutableArray*) parseContentOfUrl: (NSURL*) url;
@end

ShowroomsXmlParser는 NSXMLParser가 발생시키는 event를 처리해서 Showroom 객체 배열을 넘겨주는 클래스입니다. NSXMLParser가 위임(delegate)하는 method들을 처리하기 위해서 NSXMLParserDelegate 프로토콜을 구현해야 합니다. 참고로 프로토콜은 자바에서 interface랑 동일합니다. 멤버 변수 _elementStack은 파서의 트리 탐색 경로 및 정보를 담아두는 스택입니다.

ShowroomsXmlParse.m은 아래와 같습니다.

#import "ShowroomsXmlParser.h"
 
 
NSString* const SHOWROOM = @"showroom";
NSString* const DEALER = @"dealer";
NSString* const NAME = @"name";
NSString* const PHONE = @"phone";
 
 
@implementation ShowroomsXmlParser
  - (void) parser: (NSXMLParser*) parser 
      didStartElement: (NSString*) elementName 
      namespaceURI: (NSString*) namespaceURI 
      qualifiedName: (NSString*) qualifiedName 
      attributes: (NSDictionary*) attributeDict
  {
    if ( TRUE == [ elementName isEqualToString: SHOWROOM ] )
    {
      Showroom* showroom = [ Showroom new ];
      [ _elementStack addObject: showroom ];
    }
    else if ( TRUE == [ elementName isEqualToString: DEALER ] )
    {
      Dealer* dealer = [ Dealer new ];
      Showroom* showroom = (Showroom*)[ _elementStack lastObject ];
      [ showroom setDealer: dealer ];
 
      [ _elementStack addObject: dealer ];
    }
    else if ( TRUE == [ elementName isEqualToString: NAME ] ||
      TRUE == [ elementName isEqualToString: PHONE ] )
    {
      NSString* element = [ NSString stringWithString: elementName ];
      [ _elementStack addObject: element ];
    }
  }
 
  - (void) parser: (NSXMLParser*) parser
      foundCharacters: (NSString*) string
  {
    NSCharacterSet* characterSet = [ NSCharacterSet 
      whitespaceAndNewlineCharacterSet ];
    NSString* trimmedValue = [ string stringByTrimmingCharactersInSet:
      characterSet ];
    if ( 0 == [ trimmedValue length ] )
    {
      return;
    }
 
    id element = [ _elementStack lastObject ];
    if ( TRUE == [ element isKindOfClass: [ NSString class ] ] )
    {
      [ _elementStack removeLastObject ];
      id parentElement = [ _elementStack lastObject ];
      if ( TRUE == [ element isEqualToString: PHONE ] )
      {
        Showroom* showroom = (Showroom*)parentElement;
        [ showroom setPhone: trimmedValue ];
      }
      else if ( TRUE == [ element isEqualToString: NAME ] )
      {
        if ( TRUE == [ parentElement isMemberOfClass: [ Showroom class ] ] )
        {
          Showroom* showroom = (Showroom*)parentElement;
          [ showroom setName: trimmedValue ];
        }
        else if ( TRUE == [ parentElement isMemberOfClass: [ Dealer class ] ] )
        {
          Dealer* dealer = (Dealer*)parentElement;
          [ dealer setName: trimmedValue ];
        }
      }
    }
  }
 
  - (void) parser: (NSXMLParser*) parser 
      didEndElement: (NSString*) elementName
      namespaceURI: (NSString*) namespaceURI 
      qualifiedName: (NSString*) qName
  {
    if ( TRUE == [ elementName isEqualToString: SHOWROOM ] )
    {
      Showroom* showroom = (Showroom*)[ _elementStack lastObject ];
      [ _showrooms addObject: showroom ];
      [ _elementStack removeLastObject ];
    }
 
    if ( TRUE == [ elementName isEqualToString: DEALER ] )
    {
      [ _elementStack removeLastObject ];
    }
  }
 
  - (NSMutableArray*) parseContentOfUrl: (NSURL*) url
  {
    NSXMLParser* xmlParser = [ [ NSXMLParser alloc ] 
      initWithContentsOfURL: url ];
    [ xmlParser setDelegate: self ];
 
    _showrooms = [ NSMutableArray arrayWithCapacity: 1024 ];
    _elementStack = [ NSMutableArray arrayWithCapacity: 1024 ];
 
    [ xmlParser parse ];
 
    [ xmlParser release ];
    [ _elementStack release ];
 
    return _showrooms;
  }
@end

NSXMLParser가 동작하기 위해서는 최소한 아래의 메소드들이 구현되어야 합니다.

didStart는 파서가 엘리먼트의 시작 태그를 만났을 때 호출됩니다. 비단말(nonterminal) 노드인 경우 엘리먼트 해당하는 모델 객체를 생성한 후 스택에 넣습니다. 단말(terminal) 노드인 경우 엘리먼트 이름으로 NSString 객체를 만들 후 스택에 넣습니다.

found는 파서가 엘리먼트를 제외한 스트링을 만날을 때 호출됩니다. 인자로 넘어온 스트링의 앞뒤 공백을 제거합니다. 유효한 스트링(이하 속성값)인 경우 스택에서 마지막으로 추가된 NSString 객체를 제거합니다. 제거된 NSString 객체의 값에 따라 적절한 부모 모델 객체의 속성(property)을 속성값으로 설정합니다. 만약 엘리먼트 이름이 NAME인 경우 중복되기 때문에 부모 모델 객체 클래스에 확인합니다.

didEnd는 파서가 엘리먼트의 종료 태그를 만났을 때 호출됩니다. 스택에서 모델 객체를 제거하고 SHOWROOM인 경우 _showrooms 배열 객체에 해당 객체를 추가합니다.

끝으로 콘솔 화면에서 결과를 확인할 수 있는 showrooms_xml_parser.m 파일은 아래와 같습니다.

#import "ShowroomsXmlParser.h"
 
 
int main( int argc, char* argv[] )
{
  NSAutoreleasePool* pool = [ NSAutoreleasePool new ];
 
  ShowroomsXmlParser* xmlParser = [ ShowroomsXmlParser new ];
  NSURL* url = [ NSURL URLWithString: 
    @"http://www.yourserver.com/showrooms.xml" ];
  NSMutableArray* showrooms = [ xmlParser parseContentOfUrl: url ];
 
  for ( Showroom* showroom in showrooms )
  {
    Dealer* dealer = [ showroom dealer ];
    NSLog( @"%@ %@ (%@)", [ showroom name ], [ showroom phone ], 
      [ dealer name ] );
  }
 
  [ xmlParser release ];
  [ url release ];
 
  [ pool drain ];
  return 0;
}

해당 소스들을 컴파일 하기 위해서는 아래의 명령어를 실행합니다.

$ gcc -framework Foundation Dealer.m Showroom.m ShowroomsXmlParser.m showrooms_xml_parser.m -o 
showrooms_xml_parser

실행 결과 화면은 아래와 같습니다.

$ ./showrooms_xml_parser
강남전시장 02-111-1111 (김태연)
삼성전시장 02-222-2222 (구하라)

프로그래밍에 도움되길 바랍니다~

Post to Twitter Post to Delicious

키포스 미분류 , , , ,

OS X, HTTP로 XML 데이터 요청 예제

3월 2nd, 2010

오래 간만에 포스팅합니다. 요즘 트위터 때문에 블로그에 소홀해진 것도 있지만 아이폰과 안드로이드 애플케이션(이하, 앱) 개발에 여념이 없습니다. 스마트폰 대세라 흐름을 따라 가는 것도 있지만 주로 웹 서비스만 개발하다 보니 오래 간만에 접하는 애플리케이션 형태의 제품 개발 자체가 매우 흥미롭습니다.

그런데 요즘 왠만한 앱에는 HTTP를 통해서 XML 데이터를 가져와서 보여주는 기능이 들어가는 데 막상 개발 참고하려고 구글 검색해보면 마땅한 예제가 없어서 간단하게 예제를 작성해보았습니다.

요청하는 XML 문서(hello.xml)는 아래와 같이 작성했습니다.

<?xml version="1.0" encoding="UTF-8" ?>
 
<showrooms>
  <showroom><name>강남전시장</name><phone_number>02-111-1111</phone_number></showroom>
  <showroom><name>삼성전시장</name><phone_number>02-222-2222</phone_number></showroom>
  <showroom><name>한남전시장</name><phone_number>02-333-3333</phone_number></showroom>
</showrooms>

해당 XML 데이터를 HTTP를 통해서 가져와서 처리하는 예제는 아래와 같습니다. 간단하게 실행 되는 것을 확인하기 위해서 컨솔 창에서 돌아가도록 작성했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#import <Foundation/Foundation.h>
 
 
int main( int argc, char* argv[] )
{
  NSAutoreleasePool* pool = [ NSAutoreleasePool new ];
  NSURL* url = [ NSURL URLWithString: @"http://www,yourserver.com/hello.xml" ];
  NSError* error;
  NSXMLDocument* xmlDocument = [ [ NSXMLDocument alloc ] 
    initWithContentsOfURL: url
    options: ( NSXMLNodePreserveWhitespace | NSXMLNodePreserveCDATA )
    error: &error ];
 
  if ( nil != error )
  {
    NSLog( @"%@", [ error localizedDescription ] );
    [ pool drain ];
    return 0;
  }
 
  NSXMLNode* rootElement = [ xmlDocument rootElement ];
  NSArray* showrooms = [ rootElement children ];
 
  int i, count = [ showrooms count ];
  for ( i = 0; i < count; ++i ) 
  {
    NSXMLNode* showroomNode = [ showrooms objectAtIndex: i ];
    NSXMLNode* nameNode = [ showroomNode childAtIndex: 0 ];
 
    NSArray* nodes = [ showroomNode nodesForXPath: @"phone_number" error: nil ];
    NSXMLNode* phoneNumberNode = nil;
    if ( 1 == [ nodes count ] )
    {
      phoneNumberNode = [ nodes objectAtIndex: 0 ];
    }
 
    NSLog( @"%@ (%@)", [ nameNode stringValue ], [ phoneNumberNode stringValue ] );
  }
 
  [ xmlDocument release ];
  [ url release ];
 
  [ pool drain ];
  return 0;
}

예제에서 특별히 어려운 내용은 없습니다. NSXMLDocument 클래스가 HTTP를 지원하기 때문에 initWithContentsOfURL 메소드를 호출하기만 하면 됩니다. 그리고 아이폰 런타임 환경에서는 가비지 컬렉션이 지원되지 않기 때문에 NSAutoreleasePool 클래스를 이용해서 메모리 관리를 하였습니다.

예제에서 3가지 다른 방법으로 자식 노드에 접근하고 있습니다. 각각 children, objectAtIndex, nodesForXPath 메소드를 이용합니다. 가장 일반적인 방법은 nodesForXPath을 이용하는 방법입니다. 약간의 성능 문제가 있을 수도 있지만 대부분의 경우 처리하는 XML 데이터의 량이 많이 않기 때문에 신경 쓰지 않아도 될 듯 합니다. 반면 objectAtIndex 메소드를 사용하는 경우 성능은 향상될 수 있겠지만 XML 문법에 종속적이기 때문에 그리 권장할만한 방법은 아닙니다.

해당 파일을 컴파일하는 쉘 구문은 아래와 같습니다.

$ gcc -framework Foundation hello.m -o hello

실행 결과 화면은 아래와 같습니다.

$ ./hello
강남전시장 (02-111-1111)
삼성전시장 (02-222-2222)
한남전시장 (02-333-3333)

프로그래밍에 도움되길 바랍니다~

[ 수정 사항 ]
조사해 본 결과 NSXMLDocument 클래스는 iPhone SDK에 제외되어 있습니다. 따라서 위 예제는 OS X 애플리케이션에서만 동작합니다. 빠른 시일 내에 iPhone에서 돌아가는 예제를 작성해서 올리도록 하겠습니다. iPhone은 iPhone, HTTP로 XML 데이터 요청 및 처리 예제를 참고하세요~

Post to Twitter Post to Delicious

키포스 미분류 , , , ,

트위터 플러그인 설치했어요

1월 17th, 2010

요즘 트위터에 조금 익숙해진 것 같습니다. 아무래도 미투데이트위터를 동시에 사용하다보니 분산되긴 하지만 SNS 전체 활동량이 늘어났기 때문에 트위팅 시간도 많이 늘었습니다.  구독(follow)하는 사람들도 조금 늘었고 트윗 개수도 조금씩 늘어가고 있습니다.

몇일전 워드프레스를 2.9.1버전으로 업그레이드 했습니다. 저희 서버는 SFTP를 사용하기 때문에 자동 업그레이드가 실행이 안되서 다소 귀찮지만 수동으로 업그레이드 했습니다.  그나저나 처음에는 SFTP가 마냥 좋은 줄 알았는데 몇가지 단점도 있고해서 FTPS도 한번 고려해봐야 겠네요.

아무튼 워드프레스 업그레이드 하면서 트위터 플러그인도 몇가지 추가해봤습니다. 아무래도 블로그보다는 트위터에 포스팅을 많이 하기 때문에 포스팅이 빈한 블로그를 보완하기 위해 사이드바에 트위터 위젯을 추가했습니다. 처음에는 구글링의 가장 상단에 나오는 Twitter Tools라는 플러그인을 설치했습니다. 그런데 기능이 너무 많고 무거워서 너무 무거운게 제 취향이 아니었습니다.

그래서 Twitter for Wordpress를 설치했는데 기능도 단순하고 옵션도 적당해서 일단 제 마음에는 들었습니다. 하지만 처음 페이지를 렌더링할 때 다소 지연(delay)가 발생합니다. 아마도 트위터로부터 정보를 가져와서 캐싱하는데 시간이 좀 걸리는 것 같습니다. 일단 캐싱되면 빠르게 렌더링이 되지만 거슬려서 다른 플러그인을 찾아보았습니다.

결국 Twitter Wordpress Sidebar Widget이라는 플러그인을 설치했습니다. Twitter for Wordpress보다는 옵션이 부족하지만 렌더링 시간이 거슬릴 정도는 아닙니다. 그런데 트윗한 시각을 ‘2 house ago’ 형식으로 보여주는데 포스팅 시각이 그리 중요한 것 같지는 않아서 php 소스를 살짝 고쳐서 ‘#’로 표시되도록 했습니다.

트위터 위젯은 사이드바 가장 상단에 배치하였습니다. 아무래도 가장 업데이트가 자주 발생하는 정보인 만큼 우선순위를 가장 높였습니다.  그다음 최근 글, 태그, 월별 목록 순으로 위젯을 배치하였습니다. 블로그롤(링크 목록) 위젯은는 트위터 위젯과 다소 중복적으로 우선순위를 낮췄습니다.

그리고 Tweet This라는 플러그인도 설치했습니다. 이 플러그인을 설치하면 글 아래에 ‘Tweet this’라는 버튼이 표시됩니다. 이 버튼을 클릭하면 현재 보고 있을 글을 트위터로 포스팅할 수 있습니다. 그리고 제가 블로그에 포스팅하면 자동으로 트위터에 URL과 제목을 포스팅해주는 기능도 있습니다.

별건 아니지만 그래도 설치해놓고 보니 뭔가 작은 성취감을 느낄 수 있었습니다ㅎ. 당분간은 열심히 트위팅을 할 것 같습니다~

Post to Twitter Post to Delicious

키포스 미분류 , ,