티스토리 툴바

NSInvocation은 커맨드 패턴의 전통적인 구현이다. NSInvocation은 타겟, 셀렉터, 메서드 시그너처, 그리고 모든 파라미터를 저장할 수 있고, 나중에 불러올 수 있는 객체 안에 담는다. 인보케이션이 호출되면 메시지가 보내지고 Objective-C 런타임은 실행을 위해 정확한 메서드 구현을 찾을 것이다.


Method Implementaiotn(IMP)는 다음 예와 같이 C 함수를 위한 함수 포인터이다.


id function(id self, SEL _cmd, ...)


모든 메서드는 구현 시 두 개의 파라미터인 self와 _cmd를 갖는다. 첫 번째 파라미터는 우리에게 친숙한 self 포인터 이다. 두 번째 파라미터인 _cmd는 객체에개 보내 진 셀렉터이다. 이 둘은 언어에서 예약된 심볼이다. 그리고 self와 같이 정확히 접근 된다. 


모든 Objective-C 메서드는 id를 반환하는 IMP 타입을 추천하기도 하지만, 명백하게 정수 또는 부동소숫점과 같은 타입을 반환하는 메서드도 많고 전혀 반환을 하지 않는 메서드도 많다. 실제 반환 타입은 메시지 시그너처에 의해 정의된다. IMP 타입을 사용하지 않는 경우는 아래에서 살펴 볼 것이다.


NSInvocation은 타겟과 셀렉터를 포함한다. 타겟은 메시지를 보내는 대상이 되는 객체이다. 그리고 셀렉터는 보내는 메시지 이다. 셀렉터는 대략적인 메서드의 이름이다. 대략적이라고 한 이유는 셀렉터가 정확한 메서드의 형태가 아니기 때문이다. 셀렉터는initWithBytes:length:encoding:과 같은 단순한 이름이다. 셀렉터는 특정한 클래스 또는 반환 타입 또는 파라미터 타입을 갖지 않는다. 또한 구체적인 클래스나 인스턴스 셀렉터도 아니다. 셀렉터는 문자열로 생각할 수 있다. 그래서 [NSString length]와 [NSData length]는 다른 메서드 구현 형태임에도 불구하고 동일한 셀렉터 이름을 갖는다. 

NSInvocation은 또한 메서드 시그너처(NSMethodSignature)를 포함한다. 이 것은 메서드의 반환 타입과 파라미터 타입을 캡슐화 한다. NSMethodSignature는 메서드의 이름을 포함하지 않는다. 오직 반환 값과 파라피터 뿐이다. 직접 생성하는 방법은 다음과 같다.

NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:”@@:*”];

위의 것은 [NSString initWithUTF8String:]의 시그너처이다. 첫 번재 문자(@)은 반환 값이 id임을 가리킨다. 시스템에 메시지를 전달하기 위해 모든 Objective-C 객체는 동일하다. NSString과 NSArray를 구별할 수는 없다. 두 번째 문자(@:)는 이 메서드가 id와 SEL을 갖고 있음을 가리킨다. 위에서 언급했듯이, 모든 Objective-C 메서드는 이 두 가지를 첫 번째 파라미터로 갖고 있다. 암묵적으로 self와 _cmd를 전달한다. 마지막으로, 마지막 문자(*)는 첫 번째 “실제” 파라미터가 문자 스트링(char*) 임을 가리킨다.

만약 타입 인코딩을 직접 다룬다면, 직접 코딩하는 대신 @encode(type)을 사용하여 문자열을 구할 수 있다. 예를 들면, @encode(id)는 문자열 “@” 이다.

signatureWithObjCTypes:을 직접 호출할 일은 거의 없다. 여기서는 메서드 시그너처를 직접 작성하는 것이 가능함을 보여 주기 위해 사용했다. 일반적으로 메서드 시그너처를 얻는 방법은 클래스 또는 인스턴스에게 직접 질의 하면 된다. 그렇게 하기 전에, 인스턴스 메서드 인지 또는 클래스 메서드 인지 구분할 수 있어야 한다. 메서드 –init는 인스턴스 메서드이고 하이픈(-)으로 표시한다. 메서드 +alloc은 클래스 메서드이고 더하기(+) 기호로 표시한다. 
methodSignatureForSelector:를 사용하여 인스턴스로 부터 인스턴스 메서드 시그너처와 클래스 메서드로 부터 클래스 메서드 시그너처를 요청할 수 있다. 만약 클래스에서 인스턴스 메서드 시그너처를 알고 싶다면, instanceMethodSignatureForSelector:를 사용하며 된다. 다음은  +alloc과 –init의 예 이다.

SEL initSEL = @selector(init);
SEL allocSEL = @selector(alloc);
NSMethodSignature *initSig, *allocSig;

// 인스턴스의 인스턴스 메서드 시그너처
initSig = [@”String” methodSignatureForSelector:initSEL];

// 클래스의 인스턴스 메서드 시그너처
initSig = [NSString instanceMethodSignatureForSelector:initSEL];

// 클래스의 클래스 메서드 시그너처
allocSig = [NSString methodSignatureForSelector:allocSEL];

initSig와 allocSig를 비교하면, 그 둘이 동일함을 알게 될 것이다. 둘 다 모두 추가 파라미터(self와 _cmd를 제외하고)가 없고 id를 반환한다. 메시지 시그너처의 중요한 것은 이 것 뿐이다.

이제 셀렉터와 시그너처에 대해 이해하였으므로, 이 둘을 조합하여 NSInvocation의 타겟과 파라미터 값을 구성할 수 있다. NSInvocation은 메시지 를 전달하기 위한 필요한 모든 것을 묶을 수 있다. [set addObject:stuff] 메시지의 인보케이션을 생성하고 호출하는 다음 예를 보자.

NSMutableSet *set = [NSMutableSet set];
NSString *stuff = @”Stuff”;
SEL selector = @selector(addObject:);
NSMethodSignature *sig = [set methodSignatureForSelector:selector];
                
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:set];
[invocation setSelector:selector];

// 첫 번째 인자가 두 번째에 위치 한다.
[invocation setArgument:&stuff atIndex:2]; 
[invocation invoke];

첫 번째 인자가 두 번째에 위치함에 주의하라. 위에서 설명했듯이, 0 번째 순서는 타겟(self)이고 1 번째는 셀렉터(_cmd)이다. NSInvocation은 이를 자동으로 설정한다. 또한 인자 자체가 아닌 인자에 대한 포인터를 반드시 전달해야 하는 것에 주의하자.

 

인보케이션은 매우 유연하지만 빠르지 않다. 메시지를 전달하는 것 보다 인보케이션은 수백 배 느리다. 인보케이션을 호출하는 것은 비용이 적게 든다. 그리고 재 사용할 수 있다. invokeWithTarget: 또는 setTarget:을 사용하여 서로 다른 타켓에 보낼 수 있다. 또한 파라미터를 서로 바꿔 사용할 수 있다. 인보케이션 생성에 드는 비용 대부분은 methodSignatureForSelector:에서 발생한다. 그래서 이 결과를 캐시하면 상당한 성능을 향상시킬 수 있다.

 

기본적으로 인보케이션은 객체 인자를 리테인 하지 않은 뿐더러, C 문자열 인자 또한 복사하지 않는다. 나중에 사용하기 위해 인보케이션을 저장하기 위해서는 retainArguments를 호출한다. 이 것으로 모든 객체 인자를 리테인하고 모든 C 문자열 인자를 복사한다. 인보케이션이 릴리즈 되면, 객체가 릴리즈 되고 C 문자열의 복사본이 해제된다. 인보케이션은 다른 Objective-C 객체와 C 문자열을 다루기 위한 포인터를 제공하지 않는다. 만약 원초적인 포인터를 인보케이션에 전달하고자 한다면, 메모리 관리를 직접해야 한다.


NSTimer를 생성하기 위해 인보케이션을 사용하면, timerWithTimeInterval:invoc ation:repeats:를 사용하여, 타이머가 자동적으로 인보케이션의retainArguments를 호출한다.


인보케이션은 Objective-C의 메시지 전달 시스템의 중요한 부분 이다. 메시지 전달 시스템과 인보케이션의 통합이 트램폴린(tramppolines)과 되돌리기(undo) 관리의 중심점 이다. 다음 포스트에서는 트램폴린에 관해 살펴 보겠다.

저작자 표시

'Objective-C' 카테고리의 다른 글

메서드 시그너처와 인보케이션 사용하기  (0) 2012/04/27
접근자  (2) 2012/04/16
카테고리와 확장  (0) 2012/04/10
프라퍼티  (0) 2012/04/03
네이밍 컨벤션  (1) 2012/03/06
Block Cheat Sheet  (0) 2012/02/10

접근자

Posted 2012/04/16 21:31

인스턴스 변수에 직접 접근하는 것은 피하라. 대신 접근자(accessor)를 사용하라. 잠시 후에 몇 가지 예외를 살펴 볼 것이다. 그러나 먼저 왜 접근자를 사용해야 하는 지 알아 보자.


ARC 이전에 iOS의 가장 유명한 버그의 하나는 접근자 사용 실패이다. 개발자가 인스턴스 변수에 직접 리테인하고 릴리즈 하는 것에 실수하여 프로그램은 메모리 릭을 발생하거나 크래시 된다. ARC가 자동으로 리테인과 릴리즈를 관리함으로써 몇몇 개발자들은 이 규칙이 더 이상 중요하지 않다고 믿을지도 모르겠다. 그러나 접근자를 사용해야 하는 다른 이유들이 있다.


1. 키-밸류 감시: 아마도 접근자를 사용해야 하는 가장 중요한 이유는 접근자가 감시될 수 있기 때문이다. 만약 접근자를 사용하지 않는다면, 매번 프라퍼티를 수정할 때마다 willChangeValueForKey:와 didChangeValueForKey: 메서드를 호출해야 한다. 접근자를 사용함으로써  필요할 경우 자동으로 이 메서드가 호출될 것이다.


2. 부작용: 세터에 부작용이 포함된 서브클래스가 있다. NSUndoManager에 노티피케이션이 등록되거나 이벤트가 등록된다. 필요하지 않아도 건너 뛸 수 없다. 비슷하게, 정확한 인스턴스 변수 접근을 건너 뛰는 게터에 캐시를 추가하는 서브클래스가 있다.


3. 락: 멀티스레드 코드를 관리하기 위해 프라터티에 락을 건다면, 직접 인스턴스 변수 접근법은 깨질 것이고 프로그램은 크래시 될 것이다.


4. 일관성


그렇긴 하지만, 몇몇 곳에서는 접근자를 사용해서는 안된다.


1. 접근자 내부: 확실히 접근자 내부에서 사용할 수 없다. 보통 세터의 내부에서 게터 접근자를 사용해서는 안된다(어떤 패턴에서는 무한 루프에 빠질 수 있다). 접근자는 자신의 인스턴스 변수와 소통해야 한다.


2. dealloc: ARC는 dealloc의 사용을 많이 줄여 준다. 그러나 여전히 필요한 경우가 있다. dealloc의 내부에서 외부의 객체를 호출하지 않는 것이 좋다. 객체는 일관성 없는 상태에 빠질 수도 있다.  프라퍼티의 변경을 노티피케이션하여 수신을 하는 감시자가 혼라스러운 경우가 그렇다. 진짜 의미는 전체 객체가 삭제되었다는 것을 의미한다.


3. 초기화: dealloc과 비슷하게, 객체는 초기화하는 동안 일관성 없는 상태에 놓일 수도 있다. 그리고 보통은 노티피케이션을 공지하지 말거나 이 시점에 다른 부작용이 생길 수 도 있다. NSMutableArray와 같은 읽기 전용 변수를 초기화하기 위한 것이 그런 경우이다. readwrite 프라퍼티 선언을 피해야 한다. 그래야 초기화할 수 있다.


Objective-C의 접근자는 고도로 최적화되어 있다. 그리고 관리와 유연성을 위한 매우 중요한 기능을 제공한다. 일반적으로 모든 프라퍼티에 적용해야하고 접근자를 사용해야 한다.


저작자 표시

'Objective-C' 카테고리의 다른 글

메서드 시그너처와 인보케이션 사용하기  (0) 2012/04/27
접근자  (2) 2012/04/16
카테고리와 확장  (0) 2012/04/10
프라퍼티  (0) 2012/04/03
네이밍 컨벤션  (1) 2012/03/06
Block Cheat Sheet  (0) 2012/02/10

카테고리와 확장

Posted 2012/04/10 22:59

카테고리는 런타임 시 이미 존재하는 클래스에 메서드를 추가할 수 있게 해준다. 어떤 클래스라도, 심지어 애플에서 제공하는 코코아의 클래스 조차도, 카테고리에 의해 확장될 수 있다. 그리고 그 새 메서드는 클래스의 모든 인스턴스에서 사용할 수 있게 된다. 이 것은 스몰토크에서 상속 받은 것으로 어떤 면에서는 C#의 메서드 확장과 유사하다.


카테코리는 커다란 클래스를 관리하기 쉬운 작은 코드 조각으로 분리하기 위해 디자인 되었고 이름은 여기에서 유래되었다. 파운데이션의 클래스들을 살펴 보면, 몇몇 조각으로 나누어져 있음을 발견하게 될 것이다. 예를 들어, NSArray는 NSArray.h에 정의된 NSExtendedArray, NSArrayCreation, 그리고 NSDeprecated 카테고리를 포함한다. 더하여 NSArrayPathExtensions 카테고리는 NSPathUtilities.h에 정의되어 있다. 대부분의 카테고리는 여러 파일에 나누어져 있다. 그러나 NSString의 UIStringDrawing 카테고리와 같은 몇몇 카테고리는 런타임 시 특정한 다른 코드를 로드한다. Mac의 AppKit은 NSStringDrawing 카테고리를 로드하고 iOS는UIStringDrawing 카테고리를 로드한다. 이 것이 #ifdef로 코드를 사용하여 코드를 분리하는 것보다 좀더 우아한 방법이다. 각 플랫폼에 따라 적절한 구현 파일(.m)이 컴파일되어 그 기능을 사용할 수 있다.


Objective-C 2.0 이전에 @protocol 정의는 선택적(optional)인 메서드를 포함할 수 없었다. 대신 개발자는 “비공식 프로토콜(inform protocol)”로 카테고리를 사용했었다. 컴파일러는 카테고리에 있는 메서드 정의를 안다. 그러나 메서드가 구현되지 않은 경우에 경고를 생성할 수 없을 것이다.  이를 이용해 프로토콜의 선택적 메서드를 만들 수 있다. 이 것에 대해서는 좀 더 자세한 것은 다른 포스트인 “공식, 비공식 프로토콜”에서 설명할 예정이다. 그러나 iOS는 카테고리를 사용하는 이 방법을 추천하지 않는다. 공식 프로토콜은 이제 선택적 메서드를 직접 지원하기 때문이다.


컴파일러가 카테고리의 메서드를 구현했는지 확인하지 않기 때문에 카테고리를 오로지 커다란 클래스를 분리하기 위해 사용하는 것은 그에 상응하는 장단점이 있다. 구현 파일의 규모가 커졌을 때 카테고리를 정의하여 분리하는 것 보다 클래스에 촛점을 두어 리팩토링이 필요한 암시라고 보는 것이 좋다. 그러나 클래스가 정확한 범위안에 있다면, 카테고리로 분리하는 것이 더 유용하다. 다른 한편, 카테고리를 사용하면 메서드가 서로 다른 파일에 분산되어 혼란스러워 진다. 그래서 현명하게 판단해야 한다.


카테고리의 정의는 간단하다. 카테고리의 이름이 괄호안에 있다는 점 외에는 클래스의 인터페이스 정의와 비슷하다.


@interface NSMutableString (Capitalize)


- (void)capitalize;


@end


위에서 예로든 카테고리의 이름은 Capitalize이다. 이 곳에 인스턴스 변수의 선언이 없음에 주목하자. 카테고리는 인스턴스 변수를 선언할 수 없다. 또한 프라퍼티를 사용할 수도 없다. 이 글의 후반에 카테고리에 데이터를 저장하는 방법에 대해 설명할 것이다.


Capitalize 카테고리는capitalize 실제로 어떤 곳에 구현되었는지 상관하지 않는다. 만약 구현되지 않았고 호출되면 예외를 발생할 것이다. 컴파일러는 이 곳에서 안전을 보장하지 않는다. 만약 구현했다면, 관습에 따라 다음과 같을 것이다.


@implementation NSMutableString (Capitalize)


- (void)capitalize {

    [self setString:[self capitalizedString]];

}


@end


“관습에 따라”라고 한 이유는 카테고리 구현 시 정의 되어야 하거나 또는 카테고리 구현이 카테고리 인터페이스와 같은 이름이어야 한다는 요구사항이 없기 때문이다. 그러므로 @implementation의 블럭 이름을 Capitalize라고 하면, 

반드시 @interface 블럭 네임이 Capitalize에 선언된 모든 메서드를 반드시 구현해야 한다. 다른 파일(.m)에 메서드를 추가할 클래스 이름 다음에 괄호로 시작하여 카테고리 이름을 추가한다. 카테고리 메서드를 원래 클래스의 implementation 블럭 안과 카테고리 이름을 정의한  implementation 블럭 안에 구현할 수 있다. 또는 전혀 구현 하지 않을 수도 있다.


기술적으로 카테고리는 메서드 오버라이가 가능하다. 그러나 위험하므로 권장하지 않는다. 만약 두 개의 카테고리가 같은 메서드를 구현하면 어떤 것이 사용되는 지 확실하지 않게 된다. 나중에 관리의 이유로 클래스가 카테고리로 분리되면 확실하지 않은 것이 오버라이드될 수 있다. 이런 종류의 버그는 추적하기 매우 힘들다. 게다가 이 기능을 사용하면 코드를 이해하기 어렵게 만든다. 카테고리 오버라이드는 또한 원래의 메서드를 호출할 방법을 제공하지 않는다. 디버깅을 제외하고는 존재하는 메서드를 오버라이드하기 위해 카테고리를 사용하지 말기를 권한다. 심지어 디버깅일지라도... 차라리 스위즐링(swizzling)를 사용하길 권한다.


기존의 클래스에 유티리티 메서드를 추가하기 위해 카테고리를 사용하는 것은 좋은 예이다. 이렇게 할 때, 헤더와 구현 파일에 기존 클래스의 이름에 확장 이름을 사용할 것을 권장한다. 예를 들어 NSDate에 MyExtensions 카테고리를 생성할 수 있다.


@interface NSDate (MYExtensions)


- (NSTimeInterval)timeIntervalUntilNow;


@end



@implementation NSDate (MYExtensions)


- (NSTimeInterval)timeIntervalUntilNow {

    return [self timeIntervalSinceNow];

}


@end


오직 몇 개의 유티릴티 메서드만 있다면 모두를 MYExtensions(각자의 코드에서  사용하는 접두어를 사용면 된다)과 같은 이름으로 하나의 카테고리에 넣는 것이 유용하다. 이렇게 하면 각 프로젝트를 확장하기가 더 쉽다. 물론 이 방식은 코드가 팽창하는 단점이 있다. 그래서 “유틸리티” 카테고리에 얼마나 포함시킬 지에 주의해야 한다. Objective-C는 C 또는 C++과 같이 불필요한 코드를 효과적으로 제거할 수 없다.

만약 연관된 메서드의 커다란 그룹이 있다면 특히 그런 것이 항상 유용하지는 않지만, 각 그룹별로 카테고리로 나누는 것은 좋은 생각이다. UIKit에 있는 UIStringDrawing.h는 좋은 예이다.

+load
카테고리는 런타임에 클래스에 첨부된다. 카테고리가 정의되어 있는 라이브러리는 동적으로 로드된다. 그래서 카테고리는 추가 시 약간 지체된다. (동적 라이브러리를 직접 iOS에 추가할 수 없는 반면, 시스템 프레임워크는 동적으로 로드되고 카테고리를 포함한다. Objective-C는 카테고리가 처음 첨부되면 실행할 수 있는 +load를 제공한다. +initialize와 유사하게, 이 것을 정적 변수를 초기화하는 것과 마찬가지로 카테고리의 특정 설정을 구현하기 위해 사용할 수 있다. 클래스가 이미 구현하고 있기 때문에 +initialize를 카테고리에 안전하게 사용할 수는 없다. 만약 여러 카테고리에서+initialize를 구현한다면, 어떤 것이 실행될지 확실치 않다.

다음과 같은 질문을 하게 될 것이다. “만약 카테고리가 다른 카테고리와의 충돌 때문에 +initialize를 사용할 수 없다면,  여러 카테고리에서 +load은 어떻게 구현할까?” 이 것이 Objective-C 런타임의 마법같은 기능 중의 하나로 그 진가를 알 수 있다. +load 메서드는 런타임에서 특별한 경우이다. 그래서 모든 카테고리는 그 것을 구현한다. 그리고 구현된 모든 것이 실행될 것이다. 단, 순서는 보장되지 않는다. 그리고 직접 +load를 호출해서는 안된다.

+load는 카테고리가 정적인지 동적인지 상관 없이 호출된다. 카테고리가 런타임에 추가되면 호출된다. 보통 프로그램 시작 시, main 이전, 그러나 훨씬 나중에.

클래스 마다 각각 +load 메서드(카테고리에 정의된 것은 아니다)를 소유한다. 그리고 클래스가 런타임 시 추가되면 호출 된다. 동적으로 클래스를 추가하지 않는한 그리 유용하지는 않다.

+initialize가 그러하듯이 +load가 여러 번 실행되지 않도록 할 필요는 없다. +load 메시지는 실제 구현된 클래스에게 오직 한 번 전송된다. 그래서+initialize에서 사용했던 방식대로 서브클래스에서 우현히 호출하지 않아야 한다. 모든 +load는 정확히 한 번 호출된다. [super load]를 호출하지는 말라.

associative reference를 이용한 카테고리 데이터 저장
카테고리는 새로운 인스턴스 변수를 생성할 수 없다. Associative References를 사용할 수는 있다. Associative References를 사용하여 임의의 객체에 키-밸류 데이터를 추가할 수 있다.

Person 클래스의 경우를 생각해 보자. emailAddress라는 새로운 프라퍼티를 추가하기 위해 카테고리를 사용하는 것을 더 좋아하게 될 것이다. 아마도 다른 프로그램에서 Person을 사용할 것이고 때론 이메일  주소는 사용할 수도 안할 수도 있다. 그래서 카테고리라는 이메일 주소가 필요치 않을 경우에 오버헤드를 피할 수 있는 좋은 해결책을 사용할 수 있다. 또는 Person 클래스를 소유하지 않을 수도 있다. 그리고 클래스 소유자가 프라퍼티를 추가할 권한을 주지 않는다. 이 경우 이 문제를 어떻게 해결할 수 있을까? 우선 Person 클래스를 살펴 보자.

@interface Person : NSObject


@property (readwrite, copy) NSString *name;


@end



@implementation Person


@synthesize name = name_;


@end


이제associative reference를 사용하여 카테고리에emailAddress이라는 새로운 프라퍼티를 추가할 수 있다. 

#import <objc/runtime.h>

#import "Person.h"


@interface Person (EmailAddress)


@property (readwrite, copy) NSString *emailAddress;


@end



#import "Person+EmailAddress.h"


@implementation Person (EmailAddress)


static char emailAddressKey;


- (NSString *)emailAddress {

    return objc_getAssociatedObject(self, &emailAddressKey);

}


- (void)setEmailAddress:(NSString *)emailAddress {

    objc_setAssociatedObject(self, &emailAddressKey, emailAddress, OBJC_ASSOCIATION_COPY);


@end


associative reference는 값이 아닌 키의 주소를 기반으로 한다는 점에 주의하자.  emailAddress에 어떤 값이 저장되어도 상관 없다. 오직 고유의 주소가 필요하다. 그렇기 때문에unassigned static char룰 키로 사용했다.
associative reference는 메모리 관리에 잇점을 갖는다. 파라미터에 objective_setAssociatedObject를 전달하여 정확하게 copy, assign, 또는 retain 의미를 조정한다. 연관된 객체가 해제되면 정확히 릴리즈를 한다.

associative reference는 얼럿 창이나 컨트롤에 연관된 객체를 추가하는 좋은 방법이다. 예를 들어, 다음의 코드에서 처럼 “represented object”를 얼럿 창에 추가할 수 있다. 

- (IBAction)doSomething:(id)sender {

  UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Alert" 

                                                  message:nil 

                                                 delegate:self 

                                        cancelButtonTitle:@"OK" 

                                        otherButtonTitles:nil];

  objc_setAssociatedObject(alert, &kRepresentedObject, sender, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

  [alert show];

}


- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {

  UIButton *sender = objc_getAssociatedObject(alertView, &kRepresentedObject);

  self.buttonLabel.text = [[sender titleLabel] text];

}

많은 프로그램은 currentAlertObject와 같은 호출자에서 인스턴스 변수를 사용한다. 그러나 associative reference는 간결하고 단순하다. 맥 개발에 익숙한 사람에게는 representedObject와 유사할 것이다. 그러나 더 유연하다.

associative reference(또는 카테고리에 데이터를 추가하는 다른 접근 방식)의 한 가지 제약 사항은 encodeWithCoder:와 통합할  수 없다는 것이다. 그래서 카테고리를 직렬화하기가 어렵다.

플라이웨이트 패턴을 이용한 카테고리 데이터 저장
만약 Mac 10.6 이전 버전과 코드를 공유해야 한다면 associative reference를 사용할 수 없고 플라이웨이트 패턴(flyweight pattern)이라는 예전 방법을 사용해야 한다. 객체 내부에 데이터를 저장하는 대신 객체 밖에 저장한다. 그리고 그 데이터를 사용하기 위해 키를 관리한다. 이 경우 각 Person 인스턴스는 반드시 고유의 idenfifier가 필요하다. 다음 예를 보자.

@interface Person : NSObject


@property (readonly, copy) NSString *identifier;

@property (readwrite, copy) NSString *name;


- (Person *)initWithIdentifier:(NSString *)anIdentifier;


@end



@implementation Person


@synthesize identifier = identifier_;

@synthesize name = name_;


- (Person *)initWithIdentifier:(NSString *)anIdentifier {

  if ((self = [super init])) {

    identifier_ = [anIdentifier copy];

  }

  return self;

}


@end


이제 emailAddress 데이터를 저장하기 위해 정적 NSMutableDictionary를 생성한다.


@interface Person (EmailAddress)


@property (readwrite, copy) NSString *emailAddress;


@end



@implementation Person (EmailAddress)


static NSMutableDictionary *sEmailAddressForIdentifier = nil;


+ (void)load {

  sEmailAddressForIdentifier = [[NSMutableDictionary alloc] init];

}


- (NSString *)emailAddress {

  return [sEmailAddressForIdentifier objectForKey:[self identifier]];

}


- (void)setEmailAddress:(NSString *)anAddress {

  [sEmailAddressForIdentifier setObject:[anAddress copy]

                                 forKey:[self identifier]];

}


@end


이제 다음과 같이Persion 객체를 위한 이메일 주소를 저장할 수 있다. 

Person *person = [[Person alloc] initWithIdentifier:@"park"];

person.name = @"Lambert";

person.emailAddress = @"LambertPark@gmail.com";


이 방식은 몇 가지 문제를 안고 있다. Person 객체가 삭제될 때 어떻게든 추적하지 않는한, 메모리를 릴리즈할 좋은 방법이 없다. 문제의 범위가 광범위 하긴 하지만, 이 방식은 associative references를 선택할 수 없는 경우에 매우 잘 작동한다.

클래스 확장
Objective-C 2.0은 카테고리를 비틀은 클래스 확장(class extension)이라는 유용한 기능을 추가했다. 카테고리의 이름을 제외하고는 카테고리와 매우 비슷하다.

@interface Person ()


- (void)doSomething;


@end


클래스 확장은 구현 파일(.m)에 프라이빗 메서드를 추가하는 아주 좋은 방법이다. 카테고리와 확장이 다른 점은 확장에서 선언한 메서드들은 인터페이스에서 선언한 메서드와 정확히 일치한다는 점이다. 컴파일러는 구현된 모든 것을 확인할 것이다. 그리고 카테고리와 같이 런타임이 아닌 컴파일 시점에 추가할 것이다. 확장에 씬서사이즈된 프라퍼티 역시 추가할 수 있다.

저작자 표시

'Objective-C' 카테고리의 다른 글

메서드 시그너처와 인보케이션 사용하기  (0) 2012/04/27
접근자  (2) 2012/04/16
카테고리와 확장  (0) 2012/04/10
프라퍼티  (0) 2012/04/03
네이밍 컨벤션  (1) 2012/03/06
Block Cheat Sheet  (0) 2012/02/10

프라퍼티

Posted 2012/04/03 21:48

Objective-C 2.0에서 몇 가지 흥미로운 것이 소개되었다. 핵심적인 개선 사항은 인스턴스 변수가 깨지지 않게 하는 것이었다. 서브클래스의 재컴파일 없이 클래스에 인스턴스 변수를 추가할 수 있게 되었다. 이 기능은 애플리케이션 개발자 보다 주로 애플과 같은 프레임워크 개발자에게 영향을 미쳤다. 그러나 유용한 부작용를 갖고 있다. 가장 유명한 것인 신써사이즈된 프라터티이다. 

소수의 사람들이 신써사이즈된 프라터티가 인스턴스 변수를 생성할 수 있다는 것을 알아 챘다. 예를 들면,

@interface MyCar : NSObject

@property (copy) NSString *name;


@end



@implementation MyCar


@synthesize name = name_;


@end


인스턴스 변수 name_은 헤더에 선언되지 않았어도 자동으로 생성된다.  좀더 흥미로운 것은, 프라이빗 확장 또는 @implementation 블럭에 프라퍼티 선언을 넣을 수 있다. (카테고리에는 프라퍼티 선언으로 인스턴스 변수를 신써사이즈 할 수 없다. 클래스 확장(Class Extension)http://lambert.tistory.com/491을 참고하라.)

코드에서, 인터페이스에 모든 인스턴스 변수를 선언하거나 모든 인스턴스 변수를 씬서사이즈하기를 권장한다. 프라퍼티 모두를 완전히 씬서사이즈된 인스턴스 변수로 교체하라는 것이다. 퍼블릭 프라터리를 헤더에 선언하고, 프라이빗 프라퍼티를 .m 파일의 확장부분에 넣어라. 예는 다음과 같다.

@interface MyCar : NSObject


@property (readwrite, weak) id delegate;

@property (readonly, strong) NSString *readonlyString;


@end



@interface MyCar () // 프라이빗 메서드.

@property (readwrite, strong) NSString *readonlyString;

@property (readwrite, strong) NSString *privateString;

@end


@implementation MyCar


@synthesize delegate = delegate_;

@synthesize readonlyString = readonlyString_;

@synthesize privateString = privateString_;


@end


readonlyString은 클래스 확장에 readwrite로 재선언되었음에 주의하라. 이 것으로 프라이빗 세터를 생성할 수 있다.

프라퍼티 속성
프라퍼티를 사용할 때에는  프라퍼티의 속성에 대해서도 고려해야 한다.  각 속성별로 자세히 살펴 보자

원자성 – nonatomic. 대칭되는 atomic 속성은 없다. nonatomic으로 선언되지 않아도 원자이다. 이 속성은 오해하기 쉬운 속성이다. 스레드에 안전(thread safe)하게 프라퍼티를 설정하고 가져오기 위한 것이다. 객체 자체가 스레드에 안전하다는 의미는 아니다. 예를 들어, stuff라고 하는 NSMutalbeArray 프라퍼티를 선언하면 self.stuff는 스레드에 안전하다. 그리고 self.stuff = otherStuff도 스레드에 안전하다. 그러나 objectAtIndex:로 배열에 접근하는 것은 스레드에 안전하지 않다. 그 것을 조작하기 전에 추가적으로 락을 걸어야 한다. atomic 속성은 다음과 유사하게 구현한다. 

[_propertyLock lock];

id result = [[value retain] autorelease];

[_propertyLock unlock];

return result;

retain/autorelease 패턴은 객체가 호출자의 오토릴리즈 풀이 고갈되기 전까지는 삭제되지 않을 것이라는 것을 보장한다. 이 것은 다른 스레드의 호출자가 객체의 접근 도중에 릴리즈 하는 것을 보호한다. 락을 관리하고 retain과 autorelease를 호출하는 것은 비용이 많이 발생한다(비록 ARC에서 원자 프라퍼티가 훨씬 저렴할지라도). 다른 스레드에서 프라퍼티에 접근할 일이 없다거나 정교한 락이 필요하다면 이와 같은 원자성은 낭비이다. 대부분의 경우가 그렇고 보통 nonatomic을 사용한다. 비교적 적긴 하지만 원자성이 유용한 경우도 있다. 불행하게도 atomic 속성이 없기 때문에 그렇게 호출할 방법은 없다. 이런 경우 프라퍼티를 원자를 목적으로 한 곳을 지우고 주석을 추가 것이 더 도움이 된다.

ARC는 원자 프라퍼티에 중요한 성능의 잇점을 제공한다. 그리고 가까은 미래에 nonatomic은 변경할지도 모른다른 사례가 있다.

쓰기 가능 – readwrite, readonly. 이 두 속성은 따로 설명이 필요 없을 것 같다. 만약 프라퍼티가 readonly라면 게터만 사용할 수 있다. readwrite라면, 세터와 게터 모두 사용할 수 있다. writeonly  속성은 없다.

세터의 의미 – weak, strong, copy. 이들 속성은 아주 명백하다. 그러나 몇 가지 고려해야할 사항들이 있다. 첫 째, NSString과 NSArray와 같은 변경할 수 없는 클래스를 위해 보통 copy를 사용해야 한다. 프라퍼티의 클래스의 변경할 수 있는 서브클래스는 가능하다. 예를 들어, NSString타입의 프라퍼티에 NSMutableString을 전달할지도 모른다. 그렇게 하면 값(strong)의 참조만 유지하게 된다. 프라퍼티는 뒷단에서 변형되어 변경할 수 없는 상태로 변경될 것이다. 이 상황은 보통 원하던 것이 아닐 것이다. 그래서 대부분의 NSString 프라퍼티는 copy 의미로 사용된다는 것에 주의해야 한다. 이 것은 NSArray와 같은 컬렉션 계열에도 동일하게 적용된다. 변경할 수 없는 클래스를 복사하는 것은 보통 적은 비용이 적게 든다. 왜냐하면 거의 항상 retain과 함께 구현되기 때문이다.

프라퍼티는 객체의 상태를 나타낸다. 게터는 외부적으로 부작용이 없다(내부적으로는 캐시와 같은 부작용이 있다, 그러나 호출자에게 그 것이 보이지는 않는다). 일반적으로 호출하는데 효과적이고 분명히 차된되지 않는다.

프라이빗 인스턴스 변수
어떤 사람들은 인스터스 변수를 더 선호하지만, 본인은 대부분의 경우에 프라퍼티를 더 선호한다. 특히 프라이빗 변수일 경우에 그렇다. iOS 5에서는 다음과 같이 @implementation 블럭에 인스턴스 변수를 선언할 수 있다.

@implementation MyCar {

    NSString *name

}


위 구문은 퍼블릭 프라이빗 인스턴스 변수를 퍼블릭 헤더 밖으로 옮겨 준다. 캡슐화를 위한 좋은 방법이다. 그리고 퍼블릭 헤더의 가독성을 높여 준다. ARC는 다른 변수와 마찬가지로 자동으로 인스턴스 변수를 리테인하고 릴리즈 한다. 기본값은 strong이다. 그러나 다음과 같이 weak 인스턴스 변수를 선언할 수 있다.

@implementation MyCar {

    __weak NSString *name

}


저작자 표시

'Objective-C' 카테고리의 다른 글

접근자  (2) 2012/04/16
카테고리와 확장  (0) 2012/04/10
프라퍼티  (0) 2012/04/03
네이밍 컨벤션  (1) 2012/03/06
Block Cheat Sheet  (0) 2012/02/10
NSLog를 파일로 저장하기  (0) 2012/02/10

ARC(Automatic Reference Counting)

Posted 2012/03/22 23:08
iOS 5의 중요한 기능으로 Automatic Reference Counting(ARC)이 있다. LLVM 컴파일러에서  제공하는 컴파일러 수준의 기능이다. 이 말은 iOS 5를 지원하는 SDK가 아니어도 사용할 수 있다는 의미이다. ARC는 iOS 4에서도 사용할 수 있고 Xcode 4.2는 ARC를 사용기 위해 코드를 변환해 주는 Objective-C ARC 도구를 제공한다. 새 LLVM 컴파일러가 천천히 주류가 되어 가면 ARC는 현재의 retain/release와 같은 메모리 관리 방식을 대체할 것이다.

Automatic Reference Counting은 Mac OS X 버전 10.5(레오파드)부터 지원하기 시작한 가비지 컬레션과 같지 않다. 가비지 컬렉션은 자동 메모리 관리이다. 이 것은 개발자가 모든 retain 구문에 대응하는 release 구문을 작성하지 않아도 된다는 것을 의미한다. 컴파일러가 개발자를 대신해 자동으로 삽입해 준다.

ARC에는 두 개의 새로운 생명주기 식별자(strong과 weak)가 추가 되었고 새로운 규칙들이 도입되었다. 더이상 어떤 객체에게도 release, retain을 호출하지 않아도 된다. 이 방식은 dealloc 메서드에도 적용된다. ARC를 사용할 때 dealloc 메서드는 오직 파일 또는 포트와 같은 자원에 대해서만 사용하고 인스턴스 변수에 대해서는 사용하지 않아야 한다.

ARC에 관해 살펴보기 전에, 이전 포스트인 네이밍 컨벤션의 연장선으로 ARC에서도 네이밍 컨벤션에 주의 하여야 한다.
코코아 네이밍은 메모리 관리와 밀접하게 연관된다. ARC의 추가로 더 이상 중요하지는 않지만, ARC를 사용하지 않는 코드의 이해를 위해서는 중요하다. 네이밍 컨벤션은 아주 간단하다. 다음 내용은Memory Management Programming Guide(developer.apple.com)에서 인용한 일부분이다.

만약 alloc 또는 new 또는 copy를 포함하는 이름으로 시작하는 메서드(예를 들어, alloc, newObject, 또는 mutalbeCopy)로 객체를 생성하거나 retain 메시지를 보내면 객체의 오너쉽을 갖는다.  객체의 오너쉽을 포기하기 위해서는 release 또는 autorelease를 사용한다. 이 외의 다른 방법으로 객체를 생성했다면 release하지 않아야 된다.

ARC 코드 에서도, 네이밍 컨벤션을 알아야 하고 alloc, new, copy, retain, 그리고 release원래 의미 이외의 용도로 사용하는 것을 피해야 한다.
 
자 다시 본론으로 돌아와서...  iOS 5에 포함된 가장 강력한 기능 중의 하나는 ARC이다. ARC는 대부분의 개발자가 코코아 개발 시 retain과 release를 일치시키지 못해 발생시키는 에러를 크게 줄일 수 있다. ARC는 retain과 release를 제거하지 않는다, 주로 개발자의 문제라기 보다는 컴파일러의 문제이다. 대부분의 경우 이 것으로 충분하다. 그러나 여전히 retain과 release를 이해하는 것은 중요하다. ARC는 가비지 컬렉션이 아니다. 인스턴스 변수(ivar)에 값을 할당하는 다음 코드를 자세히 살펴 보자.

@property (strong, nonatomic) NSString *title;
... 

@synthesize title = title_;

... 

title_ = [NSString stringWithFormat:@"Title"];

 
ARC를 사용하지 않는다면 title_는 이전의 코딩 방식에서는 retain된다. NSString이 할당되고 autorelease된다. 그래서 런루프의 끝에서 자동 소멸될 것이다. 그리고 그 다음에 누군가가 title_에 접근하면 크래시 될 것이다. 이런 종류의 에러는 아주 일반적이다. 그리고 디버그 하기가 매우 어렵다. 만약 title_이 이전의 값을 갖고 있다면, release되지 않았기 때문에 이전의 값은 메모리 릭이 될 것이다.

ARC를 사용하면,  컴파일러는 자동으로 다음과 같이 추가적인 코드를 생성한다.

id oldTitle = title_;

title_ = [NSString stringWithFormat:@"Title"];

[title_ retain];

[oldTitle release];

 
release와 retain이 여전히 호출된다, 그래서 약간의 오버헤드가 존재한다, 그리고 release되는 동안 dealloc 메서드가 호출될 것이다. 그러나 일반적으로 이 것은 가비지 컬렉션 생성 단계 없이 프로그래머가 의도한 방법대로 코드가 반응한다. 메모리를 재생성하는 것이 가비지 컬렉션보다 훨씬 빠르다. 그리고 런타임 시점보다 컴파일 시점에 결정하는 것이 더 빠르다. 이렇게 함으로써 일반적으로 성능을 개선할 수 있다. 다른 컴파일러 최적화와 마찬가지로 프로그래머가 직접 다양한 방법으로 메모리 관리 최적화를 하는 것에서 자유롭게 만든다. ARC가 생성하는 메모리 관리는 직접 메모리 관리를 하는 것보다 보통 극적으로 더 빠르다. 

그러나 이 것은 가비지 컬렉션이 아니다. 특히 스노우 레오파드의 가비지 컬렉션이 할 수 있는 레퍼런스(retain) 루프를 다루듯이 할 수는 없다. 예를 들어, 다음 그림과 같은 객체 그래프는 객체 A와 객체 B 사이의 리테인 루프를 보여 준다. 


만약 “외부 객체”와 “객체 A”와의 연결이 끊어지면, 프로그램에서 그 둘은 고아가 되기 때문에 스노우 레오파드의 가비지 컬렉션은 객체 A와 객체 B를 삭제한다. ARC에서는 참조 횟수(retain count)가 0보다 크기 때문에 객체 A와 객체 B가 삭제되지 않을 것이다. 그래서 iOS에서는 레퍼런스 루프(참조 회귀)를 피하기 위해strong 관계를 추적할 필요가 있다.

프라퍼티에 두 개의 중요한 strong과 weak  속성이 있다.  이 둘은 예전의 retain과 assign에 연관된다. 객체에 strong 속성이 설정되면 계속해서 객체는 존재하 것이다. 이 것은 런타임시 연산자 오버로드에 의한 것이 아니라 컴파일러에 의해 참조 횟수가 관리된다는 점만 빼고는 C++의 shared_ptr과 거의 동일하다. 

Objective-C는 항상 레퍼런스 루프 문제를 안고 있다. 그러나 실전에서 자주 나오는 문제는 아니다. 예전에 assign은 사용했던 곳에 ARC에서는 weak 프라퍼티를 사용하면 된다. 대부분의 레러펀스 루프는 델리게이트에 의해 발생한다. 그리고 delegate 프라퍼티는 거의 항상 weak이다. weak를 사용하면 참조된 객체가 삭제되면 자동으로 nil이 설정되는 잇점을 얻을 수 있다. 이 것이 메모리에서 해제된 assign 프라퍼티를 넘어서는 중요한 개선점이다.

ARC 이전에는 프라터티의 synthesize를 위한 기본 스토리지 클래스 값은 assign이었다. ARC에는 기본값이 없다.  @property 줄에 둘 중 하나(strong 또는 weak)를 입력해야 한다.
 
코드를 ARC로 전환하기 위해 두 개의 중요한 변경 사항이 있다. 

retain, release 또는 autorelease를 사용해서는 안된다. 이 것들은 삭제하라. 나머지는 ARC가 알아서 처리해 줄 것이다.
인스턴스 변수를 릴리즈하기 위해서라면 dealloc 이 필요 없다. 이 것은 자동으로 처리될 것이다. 그리고 어떤 경우든 release를 호출할 수 없다. 만약 다른 것들(예를 들어 KVO 감시를 삭제하기 위해 )을 위해dealloc이 필요하다면, [super dealloc]를 호출하지 말라. 만약 호출한다면, 컴파일러는 에러를 낼 것이다. 

다시 한 번 상기할 것은 ARC는 가비지 컬렉션이 아니라는 점이다.  ARC는 코드의 적절한 곳에 retain과 release를 삽입해주는 기능이다. 이 말의 의미는 모든 코드가 정확한 네이밍 컨벤션을 따르기만 한다면 직접 메모리 관리 코드를 작성하는 것을 대체해 준다는 것이다. 예를 들어, copySomtinig이라는 이름의 메서드를 호출하면, ARC는 메서드가 참조 횟수를 +1 시킬 것이라고 예상한다. 필요하다면, 대칭되는 release를 삽입할 것이다. ARC에 의해 copySomething 내에 참조 횟수가 +1 증가 하였든 또는 직접 메모리 관리 방식에 의해 copySomething 내에 참조 횟수가 +1되었든 전혀 문제가 없다. 

만약 코코아의 네이밍 컨벤션을 어긴다면, 예를 들어, 오토릴리즈된 문자열인 카피라이트 경고문를 반환하는 메서드가 있다. 그리고 이 메서드의 이름은 copyRight이다. 그러면 ARC의 동작 방식은 호출과 호출된 코드가 ARC에 의해 컴파일되었는지에 따라 달라진다.

ARC는 copyright 이름을 조사하고, copy로 시작한다, 그래서 객체의 참조 횟수가 +1되어 반환된다고 가정한다. 만약 copyRight가 ARC에 의해 컴파일되었고 ARC에 의해 컴파일된 코드를 호출한다면, 모두 제대로 작동할 것이다. ARC는 이름 때문에 copyRight에 retain을 추가할 것이다. 그리고 코드에 의해 호출되었기 때문에releas 역시 추가할 것이다. 약간 비효율적이기는 하지만 코드는 크래시나 메모리 릭을 발생시키지는 않을 것이다.

만약 copyRight가 아니고ARC에 의해 컴파일된 코드를 호출한다면 호출된 코드는 release를 추가할 것이고 코드는 크래시를 일으킬 것이다. 만약 호출된 코드가 copyRight이고 ARC에 의해 컴파일되지 않은 코드를 호출한다면, ARC는  retain을 삽입할 것이고 코드는 메모리 릭을 발생시킬 것이다.

이 문제를 해결하는 가장 좋은 방법은 코코아 네이밍 컨벤션을 따르는 것이다. 위 예에서 메서드 이름은 copyright이어야 모든 문제를 피할 수 있다. ARC는 메서의 낙타 표기법(Camel Case)에 기반하여 메모리 관리를 결정한다.

잘못된 메서드의 이름을 변경하는 것이 불가능하다면, NS_RETURNS_RETAINED 또는 NS_ RETURNS_NOT_RETAINED 속성을 메서드 선언 시 추가하여 컴파일러에게 메모리 관리 규칙을 알려 줄 수 있다. 이 속성들은 NSObjecRuntime.h에 정의되어 있다.

코드에 retain과release를 적절하게 추가하기 위해  ARC는 다음 4 가지를 제한한다.

retain, release, 또는 autorelease를 호출하지 말라. 이 것은 가장 쉬운 규칙이다. 단지 그 것들을 제거하기만 하면 된다. 또한 이들 메서드를 오버라이드 하지 말아야 한다. 만약 싱글톤 패턴을 구현하기 위해 오버라이드 해야 한다면, 이 들 메서드의 오버라이드 없이 이 패턴을 적절히 구현하는 방법에 관해서는 패턴에 관한 포스트에서 자세히 다루어 보겠다.
C 구조체에 객체 포인터를 사용하지 말라. 드문 경우이긴 하지만, 만약 C 구조체에 객체를 저장해야 한다면 객체에 저장하거나 void*(void* 형변환에 관한 다음 규칙을 참고하라)로 캐스트 해야한다. C구조체는 free를 호출하여 언제든 삭제할 수 있다. 그리고 저장된 객체를 자동으로 추적하여 개입한다.
bridging cast 없이는 id와 void*를 캐스트 하지 말라. 이 것은 코어 파운데이션 코드 대부분에 영향을 준다. bridging cast에 대한 자세한 내용과 ARC와 같이 사용하는 방법에 대해서는 Core Foundation 프레임워크에 관한 포스트에서 다루어 보겠다.
NSAutoreleasePool를 사용하지 말라. 직접 오토릴리즈 풀을 생성하지 말고, @autorelease {} 블럭으로 감싸라. 만약 풀이 고갈되어 특정 코드를 직접 다루어야 한다고 해도 거의 필요하지 않다. @autorelease는 NSAutoreleasePool보다 20배 이상 빠르다.

이 규칙들은 대부분의 코드에서 문제가 없다. Xcode의 Edit > refactor 메뉴를 이용하여 기존 코드를 Objective-C ARC로 변환할 수 있다. 이 툴이 개발자를 위해 대부분의 작업을 해 줄 것이다.

ARC는 오토릴리즈풀 이후 Objective-C에서 가장 진보된 기능이다. 가능하다면, ARC로 전환해야 한다. 모든 것을 변환하지 않는다 해도, 가능한한 최대한 변환해야 한다. 그 것이 더 빠르고, 버그를 줄일 수 있으며,  직접 메모리 관리 코드를 작성하는 것보다 쉽다. 오늘 당장 ARC로 전환하라.

ARC의 실전 튜토리얼이 필요하다면, iOS 5 By Tutorials의 2 ~ 3장이 많은 도움이 될 것이다.
 
 
 
 
 
저작자 표시
« PREV : 1 : 2 : 3 : 4 : 5 : ... 111 : NEXT »