iPhone, XML 처리 예제 – NSXMLParser 이용하기
몇일 전 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 { NSMutableArray* _showrooms; NSMutableArray* _elementStack; } - (void) parser: (NSXMLParser*) parser didStartElement: (NSString*) elementName namespaceURI: (NSString*) namespaceURI qualifiedName: (NSString*) qualifiedName attributes: (NSDictionary*) attributeDict; - (void) parser: (NSXMLParser*) parser foundCharacters: (NSString*) string; - (void) parser: (NSXMLParser*) parser didEndElement: (NSString*) elementName namespaceURI: (NSString*) namespaceURI qualifiedName: (NSString*) qName; - (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 ]; return _showrooms; } @end
NSXMLParser가 동작하기 위해서는 최소한 아래의 메소드들이 구현되어야 합니다.
- parser:didStartElement:namespaceURI:qualifiedName:attributes: (이하 didStart)
- parser:foundCharacters: (이하 found)
- parser:didEndElement:namespaceURI:qualifiedName: (이하 didEnd)
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 (구하라)
프로그래밍에 도움되길 바랍니다~
2010-03-10 04:14 델리 키포스