[ios]리플렉션(인트로스펙션)을 사용해 Model을 구축할 때의 장점 고찰(Introspection 활용)

2013.03.18 14:01

이 글은 인트로스펙션이 무엇인가 아는데만 도움이 되는 수준이다. iOS Model구축을 위해서는 이 내용뿐 아니라 Key-Value Observing, Key-Value Coding도 아는게 좋다. http://blog.jidolstar.com/854


우리가 일상 c나 c++ 스타일로 개발하다보면 구조체나 클래스를 선언하고 그 선언된 구조체나 클래스를 malloc이나 new를 통해 직접 참조하고 사용한다. 이는 정적인 개발법으로 컨텍스트 통제를 철저히 하고 역할이 분명한 상태에서는 좋은 방법이다. 이 방법은 컴파일 타임에 에러를 추측할 수 있어 컴파일 기반 언어 장점을 가진다. 반대로 컴파일 타임이 아닌 런타임에 클래스를 생성하거나 메서드를 임의로 추가/삭제/교체 하던가 하는 동적인 개발법도 있다. 이 방법이 좋은 것은 매우 유연하고 통제에서 자유로워서 런타임에 컨텍스트를 내 마음대로 주무를 수 있다. 하지만 에러를 통제하기 힘들고 잘못사용하면 보안상 위험한데다가 메모리 관리도 신경쓰지 않으면 성능저하도 일으킬 수 있다는 단점이 있다.


요즘 개발법은 딱히 동적이냐 정적이냐에 치우치지 않으며 많은 언어가 이 두 방법을 섞어쓴다. 동적언어냐 정적언어냐라는 것은 사실 얼마나 한쪽에 치우쳐있는가로 갈리는 것뿐이다. 


리플렉션(Reflection)은 정의된 것을 찾는 행위로서 동적언어의 특징이다. 즉, 클래스나 메서드는 컴파일 입장에서 메모리에 올라가서 프로그램 입장에서는 그것을 호출하거나 사용할 수만 있는데, 리플랙션을 지원하는 언어는 그것 뿐 아니라 아에 정의자체를 찾아와서 조작할 수 있는 기능까지 지원한다. 



리플렉션(reflection)은 영어로 반사라는 뜻이 있다. 하지만 개발언어에서는 다른 뜻이며 한글로 반사라고 해석하면 안된다. 리플렉션은 결국 개발계의 고유명사이다. ^^



하지만 리플렉션은 컴파일 검사를 무시하게 되고 권한이나 보안을 깨뜨리기 된다. 그러므로 범용적으로 리플렉션을 쓰는 것은 그리 권장할 만한 것은 아니다. 대신 리플렉션이 정말 유용하게 쓰이는 것은 바로 펙토리 패턴(factory pattern)을 이용해 객체를 생성할 때이다. 정의된 클래스로부터 객체를 생성할 때 switch나 if문으로 분기처리하는 방법대신 클래스 이름만 알고 있으면 아름다운 방법으로 객체를 생성할 수 있다. Java의 경우 다음처럼 할 수 있다.


String[] clasNames = { "myClass1", "myClass2", "myClass3", "myClass4", "myClass5"};
try{
    @SuppressWarnings( "rawtypes" ) 
   Class[] t1 = {};
   for( int i = 0, j = clasNames.length ; i < j ; i++ ){ 
      _types.put( "test." + t0[i].substring( 2 ).toLowerCase(), 
       Class.forName( "com.cookilab.test." + t0[i] ).getConstructor( t1 ));
   }
}

위처럼 클래스의 Contructor를 저장해 두면 아래와 같은 형태로 클래스의 정의 참조가 필요없게 되어 조건문을 없애버리고 객체를 생성할 수 있다.

SuperClass r = (SuperClass) _types.get( "test.myClass2" ).newInstance(); 

iOS에서는 리플랙션(reflection)을 인트로스펙션 (Introspection)이라고 한다. 거의 마찬가지로 객체의 메타데이터(객체의 클래스, 구현 메소드, 프로퍼티, 프로토콜 등의 객체 정보)를 조사하는 과정을 의미한다.  아래 코드를 보자.

Class class = NSClassFromString(@"NSString");
id idOfClass = [[class alloc] initWithString:@"Reflection"];
SEL selector = NSSelectorFromString(@"length"];
NSLog(@"%d", (int)[idOfClass performSElector:selector];
[str release];

클래스를 직접적으로 접근하지 않고 NSString 클래스 명만 가지고 문자열 객체를 만들었고 또한 문자열 객체의 메시지도 직접 접근하지 않고 selector를 만들어 접근한다. 물론 selector가 있는지 없는지도 respondsToSelector:를 통해 알 수 있다. 아무튼 어느것 하나 진짜 정의를 직접 접근하지 않는다. 결국 이런 형태는 컴파일 타임에 체크할 수 없게되고 런타임이 되어야 에러인지 판명이 난다. 


이쯤되면 리플렉션이나 인트로스펙션이 재미있어질지 모르겠다. iOS의 경우 다음 글을 보면 이해가 될지 모른다.


Objective-C instrospection : http://soulpark.wordpress.com/2012/09/03/objective_c_introspection/

Objective-C Runtime Programming Guide : https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html


인트로스펙션을 사용하면 아래처럼 재미있는 유틸을 만들 수 있다. 이 유틸의 정적 메서드들은 동적으로 클래스나 객체의 프로퍼티를 직접 접근하지 않고 프로퍼티 이름만 가지고 정의 유무를 체크할 수 있고 값을 설정하거나 가져올 수 있다. 또한 클래스나 객체에 정의된 프로퍼티 이름 리스트까지 반환할 수 있다. 또한 프로퍼티의 클래스 정의도 가져올 수 있다. (ARC환경이라고 가정하자)

//클래스로부터 클래스 이름을 가져온다.
+(NSString*)stringFromClass:(Class)clazz {
    return NSStringFromClass( clazz );
}
//클래스 이름으로부터 클래스를 가져온다.
+(Class)classFromString:(NSString*)className {
    return NSClassFromString( className );
}
//객체로부터 클래스를 가져온다.
+(Class)classFromObject:(id)object {
    return [object class];
}
//객체로부터 클래스 이름을 가져온다.  
+(NSString*)classNameFromObject:(id)object {
    return NSStringFromClass( [object class] );
}
//객체에서 주어진 이름을 가진 프로퍼티의 값을 셋팅한다.
+(void)setPropValueOfObject:(id)object name:(NSString*)name value:(id)value {
    Ivar ivar = class_getInstanceVariable([object class], [[NSString stringWithFormat:@"_%@", name] UTF8String]);
    object_setIvar( object, ivar, value );;
}
//객체에서 주어진 이름을 가진 프로퍼티의 값을 가져온다. 
+(id)getPropValueOfObject:(id)object name:(NSString*)name {
    Ivar ivar = class_getInstanceVariable([object class], [[NSString stringWithFormat:@"_%@", name] UTF8String]);
    return object_getIvar( object, ivar );
}
//객체에서 주어진 이름의 프로퍼티를 가지고 있는가?
+(BOOL)hasPropAtObject:(id)object name:(NSString*)name {
    return [self hasPropAtClass:[object class] name:name ];
}
//클래스에서 주어진 이름의 프로퍼티를 가지고 있는가?
+(BOOL)hasPropAtClass:(Class)clazz name:(NSString*)name {
    objc_property_t p0 = class_getProperty(clazz, [name UTF8String]);
    return p0 ? YES : NO;
}
//주어지는 이름을 가진 객체의 프로퍼티를 찾아 그것의 클래스를 가져온다. 
+(Class)getPropClassOfObject:(id)object name:(NSString*)name {
    return [self getPropClassOfClass:[object class] name:name];
}
//주어지는 이름을 가진 클래스의 프로퍼티를 찾아 그것의 클래스를 가져온다. 
+(Class)getPropClassOfClass:(Class)clazz name:(NSString*)name {
    const char *nm = [name UTF8String];
    objc_property_t p0 = class_getProperty( clazz, nm );
    if( p0 == NULL ) {
        return NULL;
    }
    NSString *attr = [NSString stringWithFormat:@"%s", property_getAttributes( p0 )]; 
    NSArray *attrSplit = [attr componentsSeparatedByString:@"\""]; //"T@"NSString",R,V_test"에서 NSString만 추출해야 한다.
    NSString *className = nil;
    if ([attrSplit count] >= 2) {
        className = [attrSplit objectAtIndex:1];
    }
    if( className == nil ) return NULL;
    return NSClassFromString( className );
}
//클래스의 프로퍼티 이름을 배열로 가져온다. superInquiry는 해당클래스의 부모클래스 프로퍼티도 탐색할 것인지 결정하는 플래그다. 
+(NSArray*)getPropNamesOfClass:(Class)clazz superInquiry:(BOOL)superInquiry{
    if( clazz == NULL || clazz == [NSObject class] ) {
        return nil;
    }
    NSMutableArray *r = [[NSMutableArray alloc] init];
    unsigned int count, i;
    objc_property_t *ps = class_copyPropertyList( clazz, &count );
    for( i = 0; i < count; i++ ) {
        objc_property_t p = ps[i];
        const char *pn = property_getName( p );
        if( pn ) {
            [r addObject:[NSString stringWithUTF8String:pn]];
        }
    }
    free( ps );
    if( superInquiry ) {
        NSArray *sr = [self getPropNamesOfClass:[clazz superclass] superInquiry:YES];
        if( sr != nil ) [r addObjectsFromArray:sr];
    }
    return [NSArray arrayWithArray:r];
}

사실 위 코드만 가지고는 별로 유용하다고 볼 수 없다. 일단 유용성을 떠나서 동적으로 프로퍼티 정의가 있는지 체크하는 로직은 뭔가 캐쉬처리를 통해 속도개선해야 한다. 또한 인터페이스를 더욱 세밀하게 만들어 다양한 곳에 활용할 수 있게 만들 수 있다. 가령 값을 셋팅한다고 할때... (구축한게 아니라 이런걸 지원하는 걸 만들겠다는 것이다.)

MyModel *model = [[MyModel alloc]init];
[PropUtil setPropValueOfObject:model key:@"node1.node2[]" value:@"myValue"];

이렇게 만들면 model에 정의된 node1이름의 프로퍼티에 node2 프로퍼티가 정의되어 있고 node2는 배열이다. 여기에 문자열 "myValue"를 추가할 수 있다. 이쯤 되야 비로서 유용해질 수 있을 듯하다. 만약, 매크로(define)이나 c함수등의 인터페이스를 더 만들면 아래처럼도 가능해진다.

model( @"node1.node2[]", @"myValue"); 
NSString *v = model( @"node1.node2[#last]" ); //myValue 
즉, node1.node2배열에 "myValue"를 추가하고 방금 추가한 값을 돌려받는다. get, set은 인자의 숫자로 생각하면 된다. 즉, 짝수이면 set 홀수이면 get이다. 이걸 더 확장해서 생각해보면 아래처럼도 된다.

NSString *v = model( @"node1.node2[]", @"myValue", @"node1.node2[#last]" ); //myValue 

결국 set하고 마지막에 홀수 인자를 가지므로 그 값을 반환한다. 이쯤되면 아래와 같은 인터페이스도 가능하겠다.

NSString *v = model( @"node1.node2[]", [@"v1", @"v2", @"v3"], @"node1.node2[#last]", @"v4", @"node1.node2[#last]", __DEL__ , @"node1.node2[#last]" ); //v3

정적방식이였으면 아래처럼 해야한다.

MyModel *model = [[MyModel alloc]init];
MyNode1 *node1 = model.node1;
NSMutableArray *node2 = nodel1.node2;
[node2 addObject:@"v1"];
[node2 addObject:@"v2"];
[node2 addObject:@"v3"];
[node2 addObject:@"v4"];
[node2 removeLastObject];
NSString *v = [node2 lastObject];

길이가 확 늘어났다. 이뿐이겠는가? 위 코드는 컨테이너인 배열(NSMutableArray)의 참조를 받아 addObject, removeObject하고 있다. Model입장에서는 이런식으로 처리되는 것을 원치 않을 것이다. 뭔가 Model 내에 값이 바뀔 때, Model의 변화를 한번에 통지해야하는 방식으로 취해야 하거나 값의 유효성을 검증시켜야 한다는 상황이 발생하면 분명 해결하기 어려운 방법이다. 하지만 그에 앞선 코드인 1줄 코드를 보면 코드만 짧아진게 아니라 model()내부적으로 값의 변화를 감지해서 통제할 수 있게 된다. Model에 들어오는 자신의 데이터를 통제하고 변화를 외부에 통지할 수 있을때 진정한 Model이 아니었던가? 리플렉션(인트로스펙션)을 사용하면 바로 이러한 점을 장점으로 승화시킬 수 있게 된다.


참고 

SEL Method IMP 자료형에 대해서 : http://10apps.tistory.com/131

Java Reflection API의 사용 http://micropilot.tistory.com/entry/Java-Reflection-API-introduction

Java 리플렉션에 대한 재고(reflection)(1) http://www.hanb.co.kr/network/view.html?bi_id=1369 

Java 리플렉션에 대한 재고(reflection)(2) http://www.hanb.co.kr/network/view.html?bi_id=1370 


글쓴이 : 지돌스타(http://blog.jidolstar.com/851)




iOS , , , , , , ,