Search
☀️

NSXMLParser로 XML Object Mapper 구현하기 (공공데이터 Response 파싱하기)

생성일
2020/12/30 07:07
태그
iOS
속성
속성 1
속성 2
2021/01/30 08:41

공공데이터 Open API

data.go.kr에서 다양한 공공데이터 정보를 얻을 수 있습니다. 단순 데이터 파일 또는 Open API형태로 제공하고 있어서 이를 이용해 다양한 서비스를 구현할 수 있습니다.지역 미세먼지 농도, 날씨 예보, 수질 정보 등등 정말 다양한 정보를 얻을 수 있는데요, 스터디용도로 이 API 중 하나를 선택해서 앱을 구현중입니다.

지방선거 API 활용하기

가장 최신으로 등록된 API를 찾아보니 곧 6월 13일 지방선거를 앞두고 선거정보, 후보자 정보 Open API가 추가되어서 활용해보기로 했습니다.역시 공공데이터 답게 Response API는 XML을 지원합니다.. 제발 json을 사용해주세요 ㅠㅜ
XML 보다 json을 선호하는 이유는 다음과 같습니다.
json은 dict, list, string, number의 타입으로 구성되어 있죠. Objective-C 에서 사용하는 데이터 타입과 다르지 않아요. 그래서 json을 object로 매핑해주기 편합니다!
xml은 list 타입이 없죠, 같은 depth에 동일한 태그가 여러개 구성되어 있는걸 list라고 판단해야 합니다. 우연히 해당 태그가 1개만 오면 이게 단순 key-value인지 list인지 알 방법이 없죠.

XML 파싱하기

Objective-C 에서는 NSXMLParser 라는 클래스를 제공하여 쉽게 XML을 파싱할 수 있도록 도와주고 있습니다.NSXMLParser로 XML을 파싱하는 순서는 다음과 같습니다.
1.
NSXMLParser에 파싱할 XML데이터 주입
2.
parse 메소드 호출
3.
NSXMLParser의 델리게이트 호출 (문서 시작, 태그 시작, 태그 끝, 문서 종료 등..)

NSXMLParser Delegate

- (void)parserDidStartDocument:(NSXMLParser *)parser { }
Objective-C
복사
XML 문서가 시작했음을 알리는 Delegate입니다. 처음 1회만 수행합니다.
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary<NSString *,NSString *> *)attributeDict { }
Objective-C
복사
<태그> 를 발견했을 때 호출됩니다. elementName값으로 태그가 넘어옵니다.<태그 속성=값> 의 형식으로 된 XML태그의 경우 attributes dict 파라미터로 @{ @”속성”, @”값” } 이 전달됩니다.
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { }
Objective-C
복사
</태그> 를 발견했을 때 호출됩니다. elementName 값으로 태그가 넘어옵니다.
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { }
Objective-C
복사
일반 문자열을 발견했을 때 호출됩니다.<태그>안녕하세요</태그> 이 경우에 didStartElement 로 태그 값을 읽은 후 foundCharacters 메소드로 안, 녕, 하, 세, 요 가 한글자씩 전달됩니다.
- (void)parserDidEndDocument:(NSXMLParser *)parser { }
Objective-C
복사
5개 Delegate 외에 파싱 에러 등 다른 메소드도 있지만 option으로 꼭 구현이 필요하진 않습니다

XML Parsing 코드

선거 정보 API 에서 내려오는 XML 예제는 다음과 같습니다.
<?xml version="1.0" encoding="UTF-8"?> <response> <header> <resultCode>INFO-00</resultCode> <resultMsg>NORMAL SERVICE</resultMsg> </header> <body> <items> <item> <num>1</num> <sgId>20180613</sgId> <sgName>제7회 전국동시지방선거</sgName> <sgTypecode>0</sgTypecode> <sgVotedate>20180613</sgVotedate> </item> <item> <num>2</num> <sgId>220180613</sgId> <sgName>국회의원선거</sgName> <sgTypecode>2</sgTypecode> <sgVotedate>20180613</sgVotedate> </item> </items> <numOfRows>10</numOfRows> <pageNo>1</pageNo> <totalCount>10</totalCount> </body> </response>
XML
복사
중간에 <item> 이 엄청 많이 나오는데 list를 표현하기위해 2개만 남기고 나머지는 생략처리했습니다.
위 XML에 매핑할 Object는 다음과 같이 작성했습니다.
// // EHElectionSchedule.h // ElectionHelper // // Created by JingyuJung on 2018. 4. 29.. // Copyright © 2018년 JingyuJung. All rights reserved. // #import <Foundation/Foundation.h> @interface EHElectionSchedule : NSObject @property (nonatomic, strong) NSNumber *sgId; @property (nonatomic, strong) NSString *sgName; @property (nonatomic, strong) NSNumber *sgTypecode; @property (nonatomic, strong) NSString *sgVotedate; @end @interface EHElectionSchedule_XMLArray : NSMutableArray @end @interface EHElectionScheduleResponseHeader : NSObject @property (nonatomic, strong) NSString *resultCode; @end @interface EHElectionScheduleItems : NSObject @property (nonatomic, strong) EHElectionSchedule_XMLArray *item; @end @interface EHElectionScheduleResponseBody : NSObject @property (nonatomic, strong) EHElectionScheduleItems *items; @end @interface EHElectionScheduleResponse : NSObject @property (nonatomic, strong) EHElectionScheduleResponseHeader *header; @property (nonatomic, strong) EHElectionScheduleResponseBody *body; @end @interface EHElectionScheduleResponseContainer : NSObject @property (nonatomic, strong) EHElectionScheduleResponse *response; @end
Objective-C
복사
클래스를 읽는 방향은 아래쪽부터 위로 읽어가면 됩니다.규칙은 다음과 같습니다.
클래스는 XML에서 1 Depth를 의미합니다.
프로퍼티명은 XML에서 tag 값과 일치해야 합니다.
Array Type 을 표현할 경우 {Array에 담길 클래스}{Array타입임을 알릴 Suffix} 의 클래스를 추가적으로 선언해야 합니다. Suffix에 _XMLArray를 사용한 이유는 Parser코드에서 볼 수 있습니다.
프로퍼티의 클래스 타입은 Custom, NSString, NSNumber 입니다. (Primitive 타입 지원하지 않습니다.. 못합니다 ㅠㅜ)

XML Parser

우선 Full Code 입니다.
// // EHXMLSerializer.m // ElectionHelper // // Created by JingyuJung on 2018. 4. 29.. // Copyright © 2018년 JingyuJung. All rights reserved. // #import "EHXMLSerializer.h" #import <objc/runtime.h> @interface EHXMLSerializer () @property (nonatomic, strong) NSXMLParser *parser; @property (nonatomic, strong) NSMutableArray<id> *keyStack; @property (nonatomic, strong) NSMutableString *value; @property (nonatomic, strong) Class wrapperClass; @property (nonatomic, strong) id result; @end static NSString *const kEHXMLArraySuffix = @"_XMLArray"; @implementation EHXMLSerializer #pragma mark - Constructor + (instancetype)serializer { static EHXMLSerializer *shared = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ shared = [[EHXMLSerializer alloc] init]; [shared EH_initializeXMLParser]; }); return shared; } #pragma mark - Public - (id)modelWithXMLResponse:(NSURLResponse *)response { [_parser parse]; return nil; } #pragma mark - Private - (void)EH_initializeXMLParser { _parser = [[NSXMLParser alloc] init]; _parser.delegate = self; } - (id)responseObjectForResponse:(NSURLResponse *)response data:(NSData *)data error:(NSError *__autoreleasing _Nullable *)error { NSError *anError = nil; if (![_responseClass isSubclassOfClass:NSData.class]) { NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; if (parser && !anError) { if (_responseClass) { parser.delegate = self; [parser parse]; } } } return _result; } #pragma mark - NSXMLParserDelegate - (void)parserDidStartDocument:(NSXMLParser *)parser { _result = [[_responseClass alloc] init]; _keyStack = [NSMutableArray array]; [_keyStack addObject:_result]; } - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary<NSString *,NSString *> *)attributeDict { NSObject *parentObject = _keyStack.lastObject; Class propertyClass = [self EH_classWithParentObject:parentObject propertyName:elementName]; if (propertyClass) { id childObject; if (propertyClass == [NSNumber class]) { // NSNumber의 경우 init 제공 X => nil 방지처리 childObject = [[NSNumber alloc] initWithInt:0]; } else if ([NSStringFromClass(propertyClass) hasSuffix:kEHXMLArraySuffix]) { NSString *elementClassString = [NSStringFromClass(propertyClass) stringByReplacingOccurrencesOfString:kEHXMLArraySuffix withString:@""]; NSMutableArray *array = [parentObject valueForKey:elementName]; if (![array isKindOfClass:[NSArray class]]) { [_keyStack addObject:[NSMutableArray array]]; } else { [_keyStack addObject:array]; } propertyClass = NSClassFromString(elementClassString); childObject = [[propertyClass alloc] init]; } else { childObject = [[propertyClass alloc] init]; } [_keyStack addObject:childObject]; _value = [NSMutableString string]; } } - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { NSObject *parentObject = [_keyStack objectAtIndex:_keyStack.count - 2]; NSMutableArray *wrapperArray; BOOL isArrayType = [parentObject isKindOfClass:[NSArray class]]; if (isArrayType) { wrapperArray = (NSMutableArray *)parentObject; parentObject = [_keyStack objectAtIndex:_keyStack.count - 3]; } Class propertyClass = [self EH_classWithParentObject:parentObject propertyName:elementName]; if (!propertyClass) { return; } id value = [_value length] ? _value : _keyStack.lastObject; if (isArrayType) { [(NSMutableArray *)wrapperArray addObject:_keyStack.lastObject]; [_keyStack removeLastObject]; value = wrapperArray; } [parentObject setValue:value forKey:elementName]; [_keyStack removeLastObject]; _value = nil; } - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { [_value appendString:string]; } - (void)parserDidEndDocument:(NSXMLParser *)parser { _result = _keyStack.firstObject; } - (Class)EH_classWithParentObject:(NSObject *)object propertyName:(NSString *)propertyName { Class objectClass = object.class; Class result; unsigned int propertiesCount = 0; objc_property_t *properties = class_copyPropertyList(objectClass, &propertiesCount); for (int index = 0; index < propertiesCount; index++) { objc_property_t property = properties[index]; const char *cname = property_getName(property); NSString *name = [NSString stringWithUTF8String:cname]; if ([name isEqualToString:propertyName]) { const char *type = property_getAttributes(property); NSString *typeString = [NSString stringWithUTF8String:type]; NSArray *attributes = [typeString componentsSeparatedByString:@","]; NSString *typeAttribute = [attributes objectAtIndex:0]; NSString *propertyType = [typeAttribute substringFromIndex:1]; NSString *propertyClass = [self EH_removeNotNeededChar:propertyType]; result = NSClassFromString(propertyClass); } } free(properties); return result; } - (NSString *)EH_removeNotNeededChar:(NSString *)originString { if (originString.length < 3) { return nil; } if ([[originString substringWithRange:NSMakeRange(0, 2)] isEqualToString:@"@\""] && [[originString substringWithRange:NSMakeRange(originString.length - 1, 1)] isEqualToString:@"\""]) { return [originString substringWithRange:NSMakeRange(2, originString.length - 3)]; } return nil; } @end
Objective-C
복사
이제 구간별로 살펴보겠습니다.
라인 : 13 - 23
@interface EHXMLSerializer () @property (nonatomic, strong) NSXMLParser *parser; @property (nonatomic, strong) NSMutableArray keyStack; @property (nonatomic, strong) NSMutableString value; @property (nonatomic, strong) Class wrapperClass; @property (nonatomic, strong) id result; @end
Objective-C
복사
parser는 파싱을 수행할 parser keyStack은 태그를 읽어나가면서 Depth를 관리하기 위한 Stack 입니다 value는 태그의 값을 관리하기 위한 MutableString 입니다 wrapperClass는 array 타입으로 된 부분을 파싱하기 위한 프로퍼티입니다 result는 최종 파싱 결과물이 될 프로퍼티입니다. * 라인 : 17 - 42
static NSString *const kEHXMLArraySuffix = @"_XMLArray"; @implementation EHXMLSerializer #pragma mark - Constructor + (instancetype)serializer { static EHXMLSerializer *shared = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ shared = [[EHXMLSerializer alloc] init]; [shared EH_initializeXMLParser]; }); return shared; }
Objective-C
복사
@"_XMLArray" 는 해당 클래스가 XML을 파싱할 때 Array 타입으로 해야한다는 걸 알리기 위해 Model 작성할 때 명시적으로 붙였던 Suffix 입니다.serialilzer 는 GCD를 이용해 싱글톤으로 구현한 생성자입니다.
라인 : 56 - 69
- (id)responseObjectForResponse:(NSURLResponse *)response data:(NSData *)data error:(NSError *__autoreleasing _Nullable *)error { NSError *anError = nil; if (![_responseClass isSubclassOfClass:NSData.class]) { NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; if (parser && !anError) { if (_responseClass) { parser.delegate = self; [parser parse]; } } } return _result; }
Objective-C
복사
response 로 받아온 XML Data를 이용해서 외부에서 호출하는 메소드입니다.외부에서 이 메소드를 호출하기 전에 responseClass에 이 XML에 매핑할 클래스를 set 해주는 과정이 꼬오옥 필요합니다.
[[EHXMLSerializer serializer] setResponseClass:[EHElectionScheduleResponseContainer class]]; EHElectionScheduleResponseContainer *container = [[EHXMLSerializer serializer] responseObjectForResponse:response data:responseObject error:&error];
Objective-C
복사
API 호출을 완료한 후 Model에 Mapping 하기 위해 Parser호출하는 부분
[parser parse]에 의해 파싱이 시작됩니다.
라인 : 73 - 77 
(void)parserDidStartDocument:(NSXMLParser *)parser {_result = [[_responseClass alloc] init];_keyStack = [NSMutableArray array];[_keyStack addObject:_result];}`XML 문서 파싱을 시작할 때 호출됩니다. 파싱에 필요한 프로퍼티들을 초기화합니다. 가장 첫번째 Response 객체를 keyStack에 담는것으로 파싱을 시작합니다.
(void)parserDidStartDocument:(NSXMLParser *)parser { _result = [[_responseClass alloc] init]; _keyStack = [NSMutableArray array]; [_keyStack addObject:_result]; }
Objective-C
복사
라인 : 79 - 105
(void)parser:(NSXMLParser )parser didStartElement:(NSString )elementName namespaceURI:(NSString )namespaceURI qualifiedName:(NSString )qName attributes:(NSDictionary *)attributeDict { NSObject *parentObject = _keyStack.lastObject; Class propertyClass = [self EH_classWithParentObject:parentObject propertyName:elementName]; if (propertyClass) { id childObject; if (propertyClass == [NSNumber class]) { childObject = [[NSNumber alloc] initWithInt:0]; } else if ([NSStringFromClass(propertyClass) hasSuffix:kEHXMLArraySuffix]) { NSString *elementClassString = [NSStringFromClass(propertyClass) stringByReplacingOccurrencesOfString:kEHXMLArraySuffix withString:@""]; NSMutableArray *array = [parentObject valueForKey:elementName]; if (![array isKindOfClass:[NSArray class]]) { [_keyStack addObject:[NSMutableArray array]]; } else { [_keyStack addObject:array]; } propertyClass = NSClassFromString(elementClassString); childObject = [[propertyClass alloc] init]; } else { childObject = [[propertyClass alloc] init]; } [_keyStack addObject:childObject]; _value = [NSMutableString string]; } }
Objective-C
복사
XML 에서 태그를 발견하면 keyStack의 lastObject (1 depth 상위 클래스) 에 포함된 프로퍼티인지 확인합니다. 없다면 굳이 매핑이 필요없으므로 패스!포함되어있다면 해당 프로퍼티의 클래스를 판별하고 할당하여 keyStack에 담아둡니다. _value NSMutableString 을 할당하여 값을 읽을 준비를 합니다.
라인 : 136 - 138
(void)parser:(NSXMLParser )parser foundCharacters:(NSString )string { [_value appendString:string]; }
Objective-C
복사
태그 사이의 값들을 _value 에 appending해 나가면서 값을 완성시켜 나갑니다.
라인 : 107 - 134
(void)parser:(NSXMLParser )parser didEndElement:(NSString )elementName namespaceURI:(NSString )namespaceURI qualifiedName:(NSString )qName { NSObject parentObject = [_keyStack objectAtIndex:_keyStack.count - 2]; NSMutableArray wrapperArray; BOOL isArrayType = [parentObject isKindOfClass:[NSArray class]]; if (isArrayType) { wrapperArray = (NSMutableArray *)parentObject; parentObject = [_keyStack objectAtIndex:_keyStack.count - 3]; } Class propertyClass = [self EH_classWithParentObject:parentObject propertyName:elementName]; if (!propertyClass) { return; } id value = [_value length] ? _value : _keyStack.lastObject; if (isArrayType) { [(NSMutableArray *)wrapperArray addObject:_keyStack.lastObject]; [_keyStack removeLastObject]; value = wrapperArray; } [parentObject setValue:value forKey:elementName]; [_keyStack removeLastObject]; _value = nil; }
Objective-C
복사
태그가 끝나면 keyStack의 마지막 object에 값을 KVC로 셋하고 _keyStack에서 완성된 lastObject를 제거합니다.만약 부모클래스가 Array 타입이었다면 현재 완성된 클래스를 Array에 추가시켜줍니다.
라인 : 140 - 142
(void)parserDidEndDocument:(NSXMLParser *)parser { _result = _keyStack.firstObject; }
Objective-C
복사
라인 : 144 - 172
(Class)EH_classWithParentObject:(NSObject )object propertyName:(NSString )propertyName { Class objectClass = object.class; Class result; unsigned int propertiesCount = 0; objc_property_t *properties = class_copyPropertyList(objectClass, &propertiesCount); for (int index = 0; index < propertiesCount; index++) { objc_property_t property = properties[index]; const char *cname = property_getName(property); NSString *name = [NSString stringWithUTF8String:cname]; if ([name isEqualToString:propertyName]) { const char *type = property_getAttributes(property); NSString *typeString = [NSString stringWithUTF8String:type]; NSArray *attributes = [typeString componentsSeparatedByString:@","]; NSString *typeAttribute = [attributes objectAtIndex:0]; NSString *propertyType = [typeAttribute substringFromIndex:1]; NSString *propertyClass = [self EH_removeNotNeededChar:propertyType]; result = NSClassFromString(propertyClass); } } free(properties); return result; }
Objective-C
복사
해당 obejct에 propertyName이라는 변수명을 가진 프로퍼티의 클래스를 리턴해주는 함수입니다. Objective-C의 인스트로펙션, 자바에서 리플렉션이라 불리는 기능을 이용해 구현합니다.
라인 : 174 - 183 
(NSString )EH_removeNotNeededChar:(NSString )originString { if (originString.length < 3) { return nil; } if ([[originString substringWithRange:NSMakeRange(0, 2)] isEqualToString:@”@\””] && [[originString substringWithRange:NSMakeRange(originString.length - 1, 1)] isEqualToString:@"\""]) { return [originString substringWithRange:NSMakeRange(2, originString.length - 3)]; } return nil; }
Objective-C
복사
runtime 메소드를 이용해 property의 클래스 타입을 가져왔을 때, @\”\” 의 불필요한 문자열이 추가되기 때문에 제거를 위해 만든 함수입니다.

결과

위에서 만든 XMl Serializer를 이용해 XML을 매핑하면 다음과 같은 결과를 얻을 수 있습니다.
더 보완이 필요한 내용
현재 최종 마지막 타입은 NSString 과 NSNumber 만 지원하고 있는데, NSDate 도 추가가 필요합니다!