Cocoa 객체

OSXDEV

Jump to: navigation, 찾기

목차

[편집] Cocoa 객체

Cocoa가 객체지향이라고 말하려 한다면, "Cocoa 객체가 대체 뭐지?"라는 질문이 자연스레 떠오를 것입니다. 이 섹션은 Objective-C 객체가 특별히 구별되는 점이 무엇이며 이 언어가 소프트웨어 개발에 어떠한 이점을 가져다 주는지를 얘기하고 있습니다. 또한 어떻게 Objective-C를 이용해 객체에 메시지를 보내고 메시지로부터 반환되는 값들을 어떻게 다룰지 보여주고 있습니다. (Objective-C는 우아하고 단순한 언어이며, 이런 일을 하는 것이 아주 어렵지 않습니다.) 이 섹션은 또한 루트 클래스인 NSObject에 대해 설명하고 있으며 객체를 생성하고 조사하고, 객체 생명주기를 관리하는 프로그래밍 인터페이스를 사용하는 방법을 설명합니다.

[편집] 단순한 Cocoa 커맨드라인 툴

단순한 커맨드라인 프로그램으로 시작합시다. 주어진 몇개의 글자를 인자로 사용하여 프로그램은 중복된 글자를 제거하고 알파벳 순서로 정렬한 후, 표준출력(standard output)을 이용해 목록을 프린트합니다. Listing 2-1은 이 프로그램의 전형적인 실행을 보여줍니다.

Listing 2-1 Output from a simple Cocoa tool

localhost> SimpleCocoaTool a z c a l q m z
a
c
l
m
q
z


Listing 2-2 shows the code for an Objective-C version of this program.

Listing 2-2 Cocoa code for a uniquing and sorting tool

#import <Foundation/Foundation.h>


int main (int argc, const char * argv[]) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSArray *args = [[NSProcessInfo processInfo] arguments];
NSCountedSet *cset = [[NSCountedSet alloc] initWithArray:args];
NSArray *sorted_args = [[cset allObjects]
sortedArrayUsingSelector:@selector(compare:)];
NSEnumerator *enm = [sorted_args objectEnumerator];
id word;
while (word = [enm nextObject]) {
printf("%s\n", [word UTF8String]);
}

[cset release];
[pool release];
return 0;

}


이 코드는 몇몇 객체를 생성하고 사용합니다: 메모리 관리를 위한 autorelease pool, 특정 단어를 "uniquing"하고 정렬하기 위한 컬렉션 객체들(array와 set), 최종 배열에서 항목을 돌며 표준출력으로 프린트하기위한 enumerator 객체들입니다.

이 코드에서 가장 먼저 눈에 들어오는 것은 짧다는 것입니다. 아마도 동일한 프로그램의 전형적인 ANSI C 버젼보다 훨씬 더 짧을 것입니다. 이 코드가 좀 이상해 보일 수도 있지만, 구성요소의 많은 부분들이 친숙한 ANSI C입니다. 이들은 할당연산자, 컨트롤-플로우 명령문(while), C-library 루틴 호출(printf), 원시 스칼라 타입 등등입니다. Objective-C는 분명 ANSI C의 기반을 갖추고 있습니다.

이 챕터의 나머지 부분에서는 이 코드의 Objective-C 요소들을 메시지 전송 작동 방식에서부터 메모리 관리 기술에 이르는 주제에 대한 예제로 살펴볼 것입니다. Objective-C 코드를 전에 본 적이 없다면 이 예제의 코드가 멋지게 뒤얽혀 있으며 불분명해 보일 수도 있지만, 그런 인상은 곧 사라질 것입니다. Objective-C는 단순하며 우아한 프로그래밍 언어로 쉽게 배울수 있으며 매우 직관적인 프로그래밍을 가능하게 합니다.



[편집] Object-Oriented Programming With Objective-C


Cocoa는 패러다임과 이벤트중심 구조의 작동방식에까지 객체지향이 깊숙히 퍼져 있습니다. Cocoa 의 가장 주요한 개발 언어인 Objective-C는 ANSI C에 기반하고 있음에도 불구하고 철저히 객체지향적입니다. 메시지 디스패치를 위한 런타임 지원을 제공하며 새로운 클래스를 정의할때 사용하는 신태틱 컨벤션을 정해놓았습니다. Objective-C는 C++이나 자바와 같은 다른 객체지향 언어에서 제공하는 대부분의 추상화와 메카니즘들을 지원하고 있습니다. 이들은 상속, 캡슐화, 재사용성, 다형성을 포함하고 있습니다.

그러나 Objective-C는 이들 다른 객체지향 언어와 다른 중요한 차이점을 가지고 있습니다. 예를 들어, Objective-C는 C++과 달리 연산자 오버로딩, 템플릿, 혹은 다중상속을 허용하지 않습니다. 또한 Objective-C는 필요하지 않은 객체를 자동으로 메모리에서 해지하는 자바의 "garbage collection"같은 기능도 (비록 같은 일을 하는 메카니즘과 컨벤션이 존재하지만) 없습니다.

Objective-C가 이런 기능들을 가지고 있지 않지만, 객체지향 프로그래밍으로서의 강점이 더욱 강합니다. Java 프로그래밍 언어의 Cocoa 버전 개요 뿐만 아니라 Objective-C의 특수한 기능들에 대해 계속 이어지는 글을 통해 알아갑시다.

더 읽을거리: 이 섹션의 대부분은 Objective-C 의 가이드인 The Objective-C Programming Language의 정보를 요약하고 있습니다. Objective-C의 상세하고 폭넓은 설명은 이 문서를 통해 얻을 수 있습니다.


[편집] The Objective-C Advantage

만약 당신이 절차적 프로그래머이고 객체지향 개념이 낯설다면, 처음에는 객체를 관련된 함수가 추가되어있는 구조체라고 생각하는 것이 도움이 될 것입니다. 특히 런타임 구현의 입장에서 보면 이런 생각이 현실과 크게 동떨어진 것도 아닙니다.

모든 Objective-C 객체는 첫 멤버(혹은 인스턴스 변수)가 "isa pointer"인 데이터구조를 숨깁니다. (남아있는 대부분의 멤버들은 객체의 클래스와 슈퍼클래스에 정의되어집니다.) isa pointer는 이름이 의미하는 것처럼, (Figure 2-1에서처럼) 객체 자신의 오른쪽에 위치한 객체이며 클래스 정의로부터 컴파일된 클래스를 가리킵니다. 클래스 객체는 구현한 메소드를 가리키는 포인터로 구성된 디스패치 표를 갖고 있습니다. 또한 슈퍼클래스를 가리키는 포인터를 가지고 있으며, 슈퍼클래스 역시 자신의 디스패치 표와 슈퍼클래스 포인터를 가지고 있습니다. 이 레퍼런스 체인을 통해 객체는 자신이 구현한 메소드와 슈퍼클래스들 모두가 구현한 메소드에 접근할 수 있게 됩니다. (물론 모든 상속받은 public, protected 인스턴스 변수도 접근할 수 있습니다.) isa 포인터는 메시지-디스패치 기능과 Cocoa 객체의 동적인 특징에 핵심적입니다.

Figure 2-1 An object’s isa pointer
그림:A cc cfg ns gadget.gif

객체의 속에 숨겨진 모습을 보여주는 이 그림은 Objective-C 런타임에서 메시지-디스패치, 상속, 다른 일반적인 객체 행동의 경우 어떤 일이 일어나는지를 단순하게 보여주고 있습니다. 하지만 이 정보가 Objective-C의 주요한 강점인 동적인 특징(dynamism)을 이해하는데 있어서 필수적입니다.

[편집] Dynamism

Objective-C 는 매우 동적인 언어입니다. 동적인 특징들로 프로그램을 컴파일과 링크 시간의 제약에서 벗어나 심볼 resolution 책임의 대부분을 사용자가 조작하고 있을 때인 런타임로 옮겨줍니다. Objective-C 는 다음 세가지 소스로부터 동적인 원천을 가지고 있기 때문에 다른 프로그래밍 언어보다 더욱 동적입니다.


  • Dynamic typing—객체의 클래스를 런타임시에 구별합니다
  • Dynamic binding—호출할 메소드를 런타임시에 확정합니다
  • Dynamic loading—런타임시에 새로운 모듈을 프로그램에 추가합니다


동적 타이핑을 위해 Objective-C는 어느 Cocoa 객체든 대표할 수 있는 id 데이터 타입을 새로 등장시켰습니다. 이 일반 객체 타입의 전형적인 사용 방법은 Listing 2-2의 예제 코드에서 찾아볼 수 있습니다.

id word;

while (word = [enm nextObject]) {

// etc....


id 데이터 타입은 객체의 어느 타입이든 런타임시 대치할 수 있도록 합니다. 따라서, 런타임 요소가 코드에 어떤 종류의 객체가 사용될지를 결정하도록 할 수 있습니다. 동적 타이핑은 객체 사이의 연관성을 정적 디자인으로 코딩되도록 강제하기보다 런타임시에 결정되도록 허용해줍니다. 컴파일시의 정적 타입 확인은 더 엄격한 데이터 통일성을 보장할 수 있지만, 통일성 대신, 동적 타이핑은 더 많은 융통성을 제공해줍니다. 그리고 (동적으로 타이핑된 뭔지 알수없는 객체의 클래스를 묻는 등의 방법으로) 객체 조사를 통해 여전히 런타임시에 객체의 타입을 확인할 수 있고 특정 작업을 위한 적합성을 검증할 수 있습니다. (물론, 필요한 경우 언제든지 정적으로 타입 확인을 할 수 있습니다.)

동적 타이핑은 Objective-C의 두번째 동적 특징인 동적 바인딩에 중요한 역할을 합니다. 동적 타이핑이 객체의 클래스 멤버쉽 분별을 런타임시까지 미루는 것처럼, 동적 바인딩도 어느 메시지가 호출되어야 하는지에 대한 결정을 런타임시까지 미룹니다. 메소드 호출은 컴파일시에 코드에 묶이지 않고, 실제로 메시지가 불려질 때에만 코드에 묶이게 됩니다. 동적 타이핑과 동적 바인딩을 이용해 실행시킬 때마다 같은 코드로부터 다른 결과를 얻을 수 있습니다. 런타임 요소는 어느 리시버가 선택되어지고 어느 메소드가 호출되어지는지를 결정합니다.

런타임의 메시지-디스패치 기능은 동적 바인딩을 가능하게 합니다. 메시지를 동적으로 타이핑된 객체에 보낼때, 런타임 시스템은 리시버의 isa 포인터를 이용해 객체의 클래스의 위치를 알아내며 그곳에서 호출할 메소드의 구현을 찾습니다. 해당 메소드는 동적으로 메시지에 묶이게 됩니다. 이 동적 바인딩의 장점을 얻기 위해 Objective-C 코드에 무엇인가 특별히 할 필요가 없습니다. 메시지를 보낼때, 특히 동적으로 타이핑된 객체에 메시지를 보낼때 당연히 투명하게 작동합니다.

동적인 특징의 마지막 형태인 동적 로딩은 런타임 지원을 위해 Objective-C에 의존하는 Cocoa의 기능입니다. 동적 로딩을 이용해 Cocoa 프로그램은 실행 코드와 리소스를 프로그램이 실행될때 모두 로딩할 필요 없이 필요할 때에 로딩할 수 있습니다. (로딩 전에 링크되어지는) 실행 코드는 종종 프로그램의 런타임 이미지에 통합되어지는 새 클래스를 가지고 있습니다. 코드와 (nib 파일을 포함하는) 지역화된 리소스는 번들에 패키지화되어 Foundation의 NSBundle 클래스에 정의된 메소드를 이용해 명시적으로 로드되어집니다.

프로그램 코드와 리소스의 이런 "게으른-로딩"은 시스템에 적은 메모리를 요구하여 전반석인 성능향상을 가져옵니다. 더 중요하게도, 동적 로딩은 응용프로그램의 확장성을 증진시킵니다. 플러그인 구조를 만들어 직접 혹은 다른 개발자들로 하여금 추가적인 모듈을 만들어 소프트웨어가 출시된지 몇달 혹은 몇년 후에라도 동적으로 로드하여 사용자화 할 수 있습니다. 디자인이 제대로 되어있다면 이들 모듈의 클래스는 이미 존재하는 클래스와 충돌하지 않을 것입니다. 각각의 클래스가 자신의 구현을 캡슐화하며 자신만의 네임스페이스를 가지고 있기 때문입니다.

[편집] Langauge Extensions

Objective-C 는 기본 언어에 강력한 소프트웨어 개발 툴로 두가지 확장을 제공합니다: 카테고리와 프로토콜, 이 두가지의 확장은 메소드를 선언하고 클래스와 연관시키는 다른 기법들을 소개합니다.

Categories
카테고리는 서브클래스를 만들지 않고도 메소드를 클래스에 추가하는 방법을 제공합니다. 카테고리에 속한 메소드들은 (해당 응용프로그램의 범위 안에서) 클래스 타입의 일부가 되고 클래스의 모든 서브클래스에 상속됩니다. 런타임 시에는 원래 메소드와 추가된 메소드사이에 차이가 전혀 없습니다. 해당 클래스(혹은 서브클래스)의 어느 인스턴스에라도 메시지를 보낼 수 있으며 카테고리에 정의된 메소드를 호출할 수 있습니다.

카테고리는 클래스에 편하게 행동을 추가하는 것 이상입니다. 카테고리를 이용해 관련된 메소드들을 각기 다른 카테고리에 그룹지어 구분할 수 있습니다. 큰 클래스들을 정돈하는데 특히 카테고리가 편리할 수 있습니다. 예를 들어, 몇몇 개발자들이 해당 클래스에 작업하고 있는 경우, 다른 카테고리를 다른 소스파일에 집어넣을 수도 있습니다.

서브클래스하는 방식과 거의 비슷하게 카테고리를 선언하고 구현합니다. 문법적인 유일한 차이는 카테고리의 이름이 @interface나 @implementation 디렉티브 뒤에 괄호로 나타난다는 것입니다. 예를 들어, NSArray 클래스에 더 구조적인 방식으로 컬렉션의 설명을 인쇄하는 메소드를 더하고 싶다고 칩시다. 카테고리의 헤더 파일에 다음과 같은 선언 코드를 작성할 것입니다.

#import <Foundation/NSArray.h> // if Foundation not already imported

@interface NSArray (PrettyPrintElements)
- (NSString *)prettyPrintDescription;

@end


구현 파일(implementation)에는 다음과 같은 코드를 작성할 것입니다.

#import "PrettyPrintCategory.h"

@implementation NSArray (PrettyPrintElements)
- (NSString *)prettyPrintDescription {
// implementation code here...
}
@end


카테고리에는 한계가 있습니다. 카테코리를 이용해 클래스에 새로운 인스턴스 변수를 추가할 수 는 없습니다. 카테고리 메소드가 존재하는 메소드를 오버라이드 할 수는 있지만, 권장할만한 방법은 아닙니다. 특히 현재 행동(behavior)에 증가를 원하는 경우라면 더욱 그렇습니다. 한가지 이유는 카테고리 메소드는 클래스의 인터페이스의 부분이고 그래서 이미 클래스에 의해 정의된 행동(behavior)을 얻어오기위해 super로 메시지를 보낼 방법이 없기 때문입니다. 클래스에 이미 존재하는 메소드를 바꿀 필요가 있다면 서브클래스하는 방법이 더 낫습니다.

루트 클래스인 NSObject에 메소드를 더하는 카테고리를 정의할 수도 있습니다. 그런 메소드들은 코드에 연결된 모든 인스턴스와 클래스객체가 사용가능하게 됩니다. Cocoa delegation 기술의 초석인 비공식 프로토콜은 NSObject의 카테고리로 선언되어 있습니다. 하지만, 이런 넓은 범위의 노출은 효용만큼의 위험성도 가지고 있습니다. NSObject의 카테고리를 통해 모든 객체에 행동(behavior)을 추가시키면 예상하지 못했던 충돌이 일어나거나, 데이터 커럽션, 혹은 더 안좋은 일이 발생할 수도 있습니다.

Protocol
프로토콜이라고 불리우는 Objective-C 확장은 Java의 인터페이스와 아주 유사합니다. 둘 모두 단순히 메소드 선언의 목록으로 어느 클래스이건 구현하기로 선택할 수 있는 인터페이스를 공개하고 있습니다. 프로토콜에 있는 메소드는 다른 클래스의 인스턴스에 의해 보내지는 메시지에 의해 호출됩니다.

프로토콜의 주요한 가치는 카테고리처럼 서브클래스 대신 사용할 수 있다는 것입니다. (구현은 아닐지라도) 인터페이스의 공유를 허락하여 C++의 다중상속의 몇몇 장점을 포함하고 있습니다. 프로토콜은 클래스가 아이덴티티는 숨기면서 인터페이스를 선언하는 방법입니다. 해당 인터페이스는 클래스가 제공하는 서비스의 전부 혹은 (대부분의 경우처럼) 일정 범위의 서비스를 노출시킬 수 있습니다. 클래스 계층구조를 따르는 다른 클래스들, 그리고 (심지어 루트 클래스조차도)상속 관계에 있지 않은 클래스들도 프로토콜의 메소드를 구현할 수 있으며 공개된 서비스에 접근할 수 있습니다. 프로토콜을 이용하면, 다른 클래스의 정체성(클래스타입)을 알지 못하는 클래스도 프로토콜에 의한 특정 목적을 위한 커뮤니케이션을 할 수 있습니다.

프로토콜에는 공식과 비공식 두가지 종류가 있습니다. 비공식 프로토콜은 "카테고리"에서 잠시 소개되었습니다. 이들은 NSObject에 사용되는 카테고리입니다. 결과로 NSObject를 (클래스 객체 뿐만아니라)루트 객체로 가지고 있는 모든 객체가 카테고리에 공개된 인터페이스를 암묵적으로 받아들이게 됩니다. 공식적인 프로토콜과 다르게 클래스가 프로토콜의 모든 메소드를 구현할 필요가 없으며 관심있는 메소드들만 구현하면 됩니다. 비공식적인 프로토콜이 작동하기 위해서는 비공식 프로토콜을 선언하는 클래스가 프로토콜 메시지를 객체에 보내기 전에 해당 타겟 객체로부터 반드시 respondsToSelector: 메시지에 긍정적인 응답을 받아야만 합니다. (만약 타겟 객체가 해당 메시지를 구현해놓지 않았다면, 런타임 예외가 발생할 것입니다.)

공식적인 프로토콜은 주로 Cocoa에 "프로토콜"이라고 지정된 것입니다. 이들은 클래스가 공식적으로 공개된 서비스에 대한 인터페이스인 메소드 목록을 선언할 수 있도록 합니다. Objective-C 언어와 런타임 시스템은 공식적인 프로토콜을 지원합니다. 컴파일러는 프로토콜에 기반한 타입을 확인할 수 있으며, 객체는 런타임시에 프로토콜에 준수하는지를 검사할 수 있습니다. 공식적인 프로토콜은 자체의 용어와 문법을 갖추고 있습니다. 용어는 제공자와 클라이언트간에 차이가 있습니다.

  • 제공자(주로 클래스)는 공식적인 프로토콜을 선언합니다.
  • 클라이언트 클래스는 공식적인 프로토콜을 받아들여서 프로토콜의 모든 메소드를 구현하기를 동의합니다.
  • 프로토콜을 받아들이거나 프로토콜을 받아들이는 클래스를 상속하는 클래스는 공식적인 프로토콜을 준수하는 것이 됩니다.(프로토콜은 서브클래스에 의해 상속됩니다.)


Objective-C 에서 프로토콜을 선언하고 받아들이는 것은 그 자신의 문법 형태를 가지고 있습니다. 프로토콜을 선언하기 위해서는 @protocol 컴파일러 디렉티브를 이용해야만 합니다. 다음 예제에서 (Foundation 프레임웍의 헤더파일인 NSObject.h에 있는)NSCoding 프로토콜의 선언을 살펴봅시다.

@protocol NSCoding

- (void)encodeWithCoder:(NSCoder *)aCoder;

- (id)initWithCoder:(NSCoder *)aDecoder;

@end


선언하는 클래스는 이들 메소드를 구현할 필요가 없지만, 준수하는 객체에서 이들을 반드시 호출해야 합니다.

@interface 디렉티브의 끝, 즉 슈퍼클래스 다음에 '<','>'로 둘러싸인 프로토콜을 지정하는 것으로 클래스는 프로토콜을 받아들입니다. 한 클래스가 여러개의 프로토콜을 받아들이는 것이 가능하며 콤마','를 이용해 구분합니다. NSData 클래스가 세개의 프로토콜을 어떻게 받아들이는지 살펴봅시다.

@interface NSData : NSObject <NSCopying, NSMutableCopying, NSCoding>

이들 프로토콜을 받아들이는 것으로 NSData는 프로토콜에 선언된 모든 메소드를 구현해야 합니다. 카테고리 역시 프로토콜을 도입할 수 있으며 그들 클래스 정의의 일부가 됩니다.

Objective-C 는 클래스가 상속받는 클래스 뿐만 아니라 그들이 준수하는 프로토콜로 종류를 정합니다. conformsToProtocol: 메시지를 이용해 클래스가 특정 프로토콜에 준수하는지를 알아볼 수 있습니다.


if ([anObject conformsToProtocol:@protocol(NSCoding)]) {

// do something appropriate

}


타입의 선언에서—메소드, 인스턴스 변수, 혹은 함수—타입의 부분으로 프로토콜 준수를 지정할 수 있습니다. 따라서 컴파일러에 의한 다른 수준의 타입 확인을 얻을 수 있으며 특정 구현에 묶이지 않기 때문에 더욱 추상적입니다. 프로토콜을 받아들일때와 동일한 문법 컨벤션을 이용합니다. 프로토콜 이름을 '<', '>' 사이에 놓고 해당 타입이 프로토콜을 준수하는지를 확인합니다. 종동 동적 객체 타입인 id를 보게 되는데 다음의 예를 살펴봅시다.

- (void)draggingEnded:(id <NSDraggingInfo>)sender;

여기서 인자로 언급된 객체는 어느 클래스 타입이어도 상관 없으나 NSDraggingInfo 프로토콜을 준수해야만 합니다.

Cocoa는 위의 예제 혹은 지금까지 언급된 것들 외에도 몇몇 프로토콜 예시를 제공합니다. 흥미로운 것은 NSObject 프로토콜 입니다. 당연하게도, NSObject 클래스가 그 프로토콜을 도입하고 있으며, 다른 루트클래스인 NSProxy도 도입하고 있습니다. 이 프로토콜을 통해 NSProxy 클래스는 레퍼런스 카운팅, 검사, 그외의 객체 행동의 기본적인 측면들을 위해 Objective-C 런타임의 핵심적인 부분과 상호작용합니다.

공식 프로토콜은 자신들의 한계를 가지고 있습니다. 시간이 지나며 프로토콜에 선언된 메소드 수가 증가하면, 프로토콜을 받아들였던 이들이 프로토콜을 준수하지 않는 것이 됩니다. 그래서 NSCopying 이나 NSCoding과 같은 Cocoa에서 사용되는 공식 프로토콜들은 안정적인 메소드 모음입니다. 만일 프로토콜의 메소드 모음이 더 커질 것이 예상된다면 공식 프로토콜 대신 비공식 프로토콜을 선언하십시오.

[편집] Using Objective-C

객체지향 프로그램에서 작업은 메시지를 통해 수행됩니다. 한 객체가 다른 객체에 메시지를 보냅니다. 그 메시지를 통해 보내는 객체는 받는 객체(리시버)로부터 무언가를 요청합니다. 리시버가 어떠한 액션을 수행하거나, 어떤 객체나 값을 반환하거나, 두개를 모두 하도록 요청합니다.

Objective-C 는 메시지를 위한 독특한 문법형태를 갖고 있습니다. Listing 2-2의 SimpleCocoaTool의 다음 문장을 살펴보십시오.

NSEnumerator *enm = [sorted_args objectEnumerator];

메시지 표현은 대괄호 '[, ]' 안에 할당연산자의 우측에 위치합니다. 메시지 표현의 가장 왼쪽에 있는 아이템은 메시지가 전송될 객체를 표현하고 있는 리시버입니다. 이 경우 리시버는 NSArray 클래스의 인스턴스인 sorted_args입니다. 리시버 다음이 적절한 메시지인데 이 경우는 objectEnumerator 입니다. (지금은, SimpleCocoaTool의 이 메시지와 다른 메시지들이 실제로 무엇을 하는지 살펴보기 보다는 메시지 문법에 더 집중합시다.) objectEnumerator 메시지는 sorted_args 객체의 objectEnumerator 메소드를 호출하고 메소드는 할당연산자 왼쪽의 enm 변수가 가지게 될 객체에 대한 레퍼런스주소를 반환합니다. 이 변수는 NSEnumerator 클래스의 인스턴스로 정적으로 타입되어있습니다. 이 문장을 다음과 같이 도식화 시킬수 있습니다.

그림:A cc cfg msg syntax1.gif

메시지는 종종 파라미터나 인자를 갖습니다. 인자가 하나인 메시지는 메시지 이름 뒤에 콜론이 따라붙으며 그 뒤에 인자를 받습니다.

그림:A cc cfg msg syntax2.gif

함수 파라미터와 마찬가지로, 인자의 타입은 메소드 선언에 지정된 타입과 일치해야 합니다. SimpleCocoaTool의 다음 메시지 표현 예제를 보십시오.

NSCountedSet *cset = [[NSCountedSet alloc] initWithArray:args];

여기서 args는 NSArray 클래스의 인스턴스이며, initWithArray: 메시지의 인자입니다.

메시지가 다수의 인자를 받으면 메시지 이름이 복수의 부분으로 이루어집니다. 각 부분은 콜론으로 끝나며 클론 뒤에 각 인자가 붙습니다.

그림:A cc cfg+msg syntax3.gif

위에 인용된 initWithArray: 예제는 네스팅이 되어있어서 더욱 흥미롭습니다. Objective-C에서는 한 메시지를 다른 메시지 안에 네스팅할 수 있습니다. 한 메시지 표현에서 돌아온 객체가 둘러싸고 있는 메시지 표현의 리시버가 될 수 있습니다. 따라서 네스팅된 메시지 표현을 해석하기 위해서는 안쪽 표현부터 시작해서 바깥쪽으로 나가야 합니다. 위 문장의 해석은 다음과 같습니다.

1. alloc 메시지가 NSCountedSet 클래스로 전송되어서 NSCountedSet 클래스가 (메모리를 할당하는 것으로) 초기화되지 않은 클래스의 인스턴스를 만듭니다.

노트: Objective-C 클래스들은 그들 자신이 객체이며 클래스 객체에 메시지를 보내거나 그들의 인스턴스에 메시지를 보낼 수 있습니다. 메시지 표현에서 클래스 메시지의 리시버는 항상 클래스 객체입니다.


2. initWithArray: 메시지가 초기화되지 않은 인스턴스에 보내지고, 이 인스턴스는 변수 args로 자신을 초기화한 후 자신을 가리키는 레퍼런스를 반환합니다.

다음으로 SimpleCocoaTool의 main 루틴의 이 문장을 살펴봅시다.

NSArray *sorted_args = [[cset allObjects] sortedArrayUsingSelector:@selector(compare:)];

이 메시지 표현에서 주시할만한 것은 sortedArrayUsingSelector: 메시지의 인자입니다. 이 인자는 @selector는 셀렉터를 만들기 위해 컴파일러 디렉티브를 요구합니다. 셀렉터는 Objective-C 런타임이 유일하게 리시버의 메소드를 구별하는 이름입니다. 메시지의 이름과 콜론을 모두 포함하지만 그 외의 리턴 타입이나 파라미터 타입등은 포함하지 않습니다.

잠시 멈춰서 메시지와 메소드 용어에 대해 다시 살펴봅시다. 메소드는 메시지의 리시버가 멤버인 클래스에 의해 정의되고 구현된 함수입니다. 메시지는 셀렉터와 인자가 합쳐진 형태로 리시버에 보내지고 그 결과로 메소드가 호출(혹은 실행)됩니다. 메시지 표현은 리시버와 메시지를 모두 가지고 있습니다. Figure 2-2 에서 이들 관계를 묘사하고 있습니다.

Figure 2-2 Message terminology

그림:A cc cfg message terms.gif

Objective-C 는 ANSI C에는 없는 몇몇 정의된 타입과 리터럴(literal)을 사용합니다. 어떤 경우에는 이들 타입과 리터럴이 ANSI C에 해당하는것을 대치하기도 합니다. Table 2-1에서 중요한 것들 몇 개와 각 타입에 허용 가능한 리터럴을 설명하고 있습니다.

Type Description and literal
id 동적 객체 타입. 부정 리터럴은 nil.
Class 동적 클래스 타입. 부정 리터럴은 Nil.
SEL 셀렉터의 데이터타입(typedef), ANSI C에서 처럼 이런 종류의 부정 리터럴은 NULL.
BOOL 불리언 타입. 리터럴 값은 YES와 NO.



프로그램의 컨트롤-플로우 문에서, 어떻게 진행할지를 결정하기위해 적합한 부정 리터럴의 존재 여부를 테스트 할 수 있습니다. 예를 들어, 아래의 SimpleCocoaTool 코드의 문장은 반환된 객체의 존재여부(다른 말로, nil이 없는지)를 알기위해 word 객체 변수를 내부적으로 테스트합니다.

while (word = [enm nextObject]) {

printf("%s\n", [word UTF8String]);

}


Objective-C 에서는 종종 nil 로 메시지를 보내게 될 것이고 아무런 문제를 일으키지 않습니다. nil로 보내진 메시지는 반환되는 값이 객체타입이기만 하면 아무 문제없이 작동할 것입니다.

SimpleCocoaTool 코드에서 마지막으로 살펴볼 것은 Objective-C를 처음 접하는 사람에게는 바로 분명해 보이지는 않을 수 있습니다.

NSEnumerator *enm = [sorted_args objectEnumerator];


위의 코드와 아래의 코드를 비교해보십시오.

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];


표면적으로 이들은 동일한 일을 하는 것 같아 보입니다. 둘 모두 객체에 대한 레퍼런스를 반환합니다. 하지만 반환된 객체의 소유권에 관해서, 그리고 그것을 해제하는 책임에 있어서 중요한 의미 차이가 있습니다. 처음 문장은 SimpleCocoaTool 프로그램이 반환된 객체를 소유하지 않습니다. 두번째 문장에서는 프로그램이 객체를 생성하므로, 소유하는게 됩니다. 프로그램이 마지막으로 하는 것은 생성된 객체에 release 메시지를 보내서 해제하는 것입니다. 다른 유일하게 명시적으로 만들어진 객체(NSCountedSet 인스턴스) 역시 명시적으로 프로그램의 마지막에 릴리즈됩니다. 객체 소유권과 처분에 대한 정책과 이 정책을 강화하는 메소드 사용에 대한 요약을 “The Life Cycle of a Cocoa Object”에서 보십시오.



[편집] The Root Class

Objective-C language 와 runtime 만으로는 가장 단순환 객체 지향 프로그램을 만들기에 충분하지 않습니다, 적어도 쉽게 가능하지는 않습니다. 여전히 무엇인가 빠져있습니다. 모든 객체의 기본적인 행동과 공통적인 인터페이스의 정의가 부족합니다. 루트 클래스가 바로 그 정의를 제공합니다.

루트 클래스는 클래스 계층구조(여기서는 Cocoa 클래스 계층도)의 루트에 위치하기 때문에 그렇게 불립니다. 루트클래스는 다른 어느 클래스로부터 상속받지 않으며, 계층도의 다른 모든 클래스가 궁극적으로 루트클래스를 상속합니다. Objective-C 언어와 함께 루트 클래스가 Cocoa 가 Objective-C 런타임에 직접 접근하고 상호작용하는데 있어 가장 기초입니다. Cocoa 객체는 루트클래스로부터 객체로서 행동 할 수 있는 능력의 대부분을 파생하고 있습니다.

Cocoa는 두개의 루트 클래스를 제공합니다. NSObjectNSProxy가 바로 그것입니다. Cocoa 는 후자를 다른 객체들 대신 작동하는 클래스들을 위한 추상 슈퍼클래스로 정의하였고, 그래서 NSProxy 는 분산 객체 구조에 필수적입니다. 이런 특별한 역할 때문에 Cocoa 프로그램에서 NSProxy는 드물게 등장합니다. Cocoa 개발자가 루트 혹은 베이스 클래스를 말하는 경우는 거의 다 NSObject를 의미합니다.

이 섹션에서는 NSObject가 어떻게 런타임과 상호작용하는지, 모든 Cocoa 객체를 위해 기본 행동과 인터페이스를 어떻게 정의하고 있는지를 살펴볼 것입니다. 이들중 가장 먼저 할당, 초기화, 메모리 관리, 검사, 런타임 지원을 위해 선언한 메소드들을 살펴볼 것 입니다. 이 개념들이 Cocoa 이해의 기초입니다.

[편집] NSObject

NSObject는 대부분의 Objective-C 클래스 계층구조의 루트 클래스입니다. 따라서 당연히 슈퍼클래스를 갖고 있지 않습니다. NSObject로부터 다른 클래스들이 Objective-C 언어를 위한 런타임 시스템의 기본 인터페이스를 상속받고, 그 인스턴스들이 객체로 행동할 능력을 얻습니다.

아주 엄격한 추상 객체는 아니지만, 사실 NSObject는 추상객체입니다. NSObject 인스턴스 자체는 단순한 객체가 되는 것 외에는 유용한 무언가를 할 수없습니다. 어느 속성이나 로직을 추가하길 원한다면, 하나 혹은 이상의 클래스를 NSObject로부터 상속받거나 NSObject에서 나온 다른 클래스를 상속받아야 합니다.

NSObject 는 NSObject 프로토콜을 도입합니다. ( “Root Class—and Protocol”를 참고하십시오). NSObject 프로토콜을 이용해 복수의 루트 객체를 이용할 수 있습니다. 예를 들어, 다른 루트 클래스인 NSProxy 는 NSObject를 상속받지는 않지만, NSObject 프로토콜을 도입하여 다른 Objective-C 객체들과 공통의 인터페이스를 공유합니다.

NSObject는 java.lang.Object와 함께 Java 안의 Cocoa의 Foundation과 Application Kit을 포함한 모든 것을 위한 루트 클래스입니다.

[편집] Root Class—and Protocol

NSObject 는 클래스 뿐만 아니라 프로토콜의 이름이기도 합니다. 둘 모두 Cocoa의 객체 정의에 필수적입니다. NSObject 프로토콜은 Cocoa의 모든 루트 클래스에서 요구하는 기본 프로그래밍 인터페이스를 지정합니다. 따라서 NSObject 클래스 뿐만 아니라, 다른 Cocoa 루트 클래스인 NSProxy 역시 NSObect 프로토콜을 도입합니다. NSObject 클래스는 역에 더해 프록시 객체가 아닌 Cocoa 객체를 위한 기본적인 프로그래밍 인터페이스를 더 지정하고 있습니다.

NSObject와 같은 프로토콜은 (클래스 인터페이스의 프로토콜 메소드를 다 포함하는 방법 대신) Cocoa 객체의 전반적인 정의에 사용되어 복수의 루트 클래스를 만드는 것을 가능하게 합니다. 각각의 루트 클래스는 그들이 도입한 프로토콜에 정의된 대로 동일한 인터페이스를 공유합니다.

다른 의미로, NSObject는 유일한 "루트" 프로토콜이 아니라는 것입니다. 비록 NSObject가 보통 공식적으로 NSCopying, NSMutableCopying, NSCoding 프로토콜을 도입하지는 않지만, 이들 프로토콜과 관련된 메소드들을 선언하고 구현해 놓았습니다. (뿐만아니라, NSObject.h 헤더 파일에는 NSObject 클래스의 정의 외에도 위에서 언급한 모든 네개의 프로토콜 정의까지 포함하고 있습니다.) 객체 카피, 인코딩, 디코딩은 객체 행동의 기초적인 양상이기에, 항상은 아닐지라도 많은 경우 서브클래스들은 이들 프로토콜을 받아들이거나 준수할 것입니다.

노트: 다른 Cocoa 클래스가 NSObject에 카테고리를 통해 메소드를 더할 수 있습니다. 이들 카테고리는 종종 delegation에 사용되는 비공식 프로토콜입니다. 이들은 delegate가 카테고리의 메소드중 어느 메소드를 구현할지를 고를 수 있도록 허용합니다. 그러나, NSObject의 카테고리는 기본적인 객체 인터페이스의 일부로 여겨지지는 않습니다.



[편집] Overview of Root-Class Methods

NSObject 루트 클래스는 도입된 NSObject 프로토콜과 다른 "루트" 프로토콜과 함께 모든 비-프록시 Cocoa 객체를 위해 다음 인터페이스와 행동적 특징들을 지정합니다.

  • 할당, 초기화, 복사 (도입된 프로토콜의 메소드들도 포함한) NSObject의 몇몇 메소드들은 객체의 생성, 초기화, 복사를 다룹니다.
    • allocallocWithZone: 메소드는 메모리 존에 객체를 위한 메모리를 할당하고 객체가 자신의 런타임 클래스 정의를 가리키도록 합니다.
    • init 메소드는 객체 초기화를 위한 프로토타입이며 인식된 초기 상태로 객체의 인스턴스 변수들을 지정하기 위한 절차입니다. 클래스 메소드인 initializeload는 클래스 자신을 초기화할 수 있도록 합니다.
    • new 는 단순 할당과 초기화를 혼합한 편리한 메소드입니다.
    • copycopyWithZone: 메소드는 (NSCopying 프로토콜로부터)이들 메소드를 구현한 클래스의 멤버인 객체의 복사본을 만듭니다. mutableCopy 와 (NSMutableCopying 프로토콜에 정의된) mutableCopyWithZone: 메소드는 그들 객체의 수정 가능한 복사본을 만들고 싶은 클래스에서 구현합니다.

더 많은 정보는 "Object Creation"에서 찾을 수 있습니다.

  • 객체 유지와 처분 다음 메소드들은 객체 지향 프로그래밍의 메모리 관리에서 특히 중요합니다.
    • retain 메소드는 객체의 retain count를 증가시킵니다.
    • release 메소드는 객체의 retain count를 감소시킵니다.
    • autorelease 메소드 역시 객체의 retain count를 감소시키지만 그 시기를 연기시킵니다.
    • retainCount 메소드는 객체의 현재 retain count를 반환합니다.
    • dealloc method 메소드는 클래스가 객체의 인스턴스 변수를 릴리즈하고 동적으로 할당된 메모리를 해제하기 위해 구현합니다.

더 많은 정보는 “The Life Cycle of a Cocoa Object”에서 찾을 수 있습니다.

  • 검사와 비교(Introspection and comparison). 많은 NSObject 메소들은 객체에 대한 런타임 질의를 가능하게 합니다. 이들 검사 메소드는 클래스 구조에서 객체의 위치를 알아내거나, 특정 메소드를 구현했는지를 알아내거나, 특정 프로토콜을 준수했는지를 테스트하도록 도와줍니다. 이 들중 몇몇은 클래스 메소드만 있습니다.

“Introspection”에서 더 많은 정보를 찾을 수 있습니다.

  • 객체 인코딩과 디코딩. 다음 메소드들은 (아카이빙 프로세스의 일부인) 객체의 인코딩과 디코딩에 관한 것들입니다
    • encodeWithCoder:initWithCoder: 메소드는 NSCoding 프로토콜의 유일한 메소드들입니다. 첫번째는 객체의 인스턴스 변수를 인코드하도록 하고 두번째는 객체가 디코드한 인스턴스 변수로부터 자신을 초기화할 수 있도록 합니다.
    • NSObject 클래스는 객체 인코딩과 관련된 다른 메소드들을 선언하고 있습니다. classForCoder:, replacementObjectForCoder:, awakeAfterUsingCoder:.

더 많은 정보는 Archives and Serializations Programming Guide for Cocoa에서 찾을 수 있습니다.

  • 메시지 포워딩 forwardInvocation:과 관련된 메소드들은 객체가 메시지를 다른 객체로 포워딩하도록 해줍니다.
  • 메시지 디스패치 performSelector...로 시작하는 메소드 모음을 이용해 메시지를 특정 시간이 지난후 디스패치하거나 (동기화 혹은 비동기화하여) 이차 쓰레드로부터 주 쓰레드로 디스페치할 수 있습니다.


NSObject 는 몇몇 다른 메소드들도 가지고 있습니다. 클래스 메소드인 버저닝과 포징(versioning, posing, 후자는 클래스가 자신을 런타임에 다른 클래스로 표현되도록 합니다)을 포함하고, 메소드 구현을 가리키고 있는 메소드 셀렉터와 펑션 포인터 같은 런타임 데이터 구조에 접근할 수 있도록 합니다.

[편집] Interface Conventions

몇몇 NSObject 메소드는 호출되도록 만들어져있는 반면 다른 메소드들은 오버라이드하도록 만들어져있습니다. 예를 들어 대부분의 서브클래스는 allocWithZone: 메소드를 오버라이드해서는 안되지만, 반드시 init 이나 궁극적으로 루트클래스의 init 메소드를 호출할 초기화메소드를 구현해야 합니다. (“객체 생성”을 보십시오). 물론 이들 메소드들이 오버라이드되도록 예상되지만, NSObject의 구현이 아무것도 안하거나 self와 같은 합리적인 기본값을 반환합니다. 이들 기본 구현을 통해 모든 Cocoa 객체에 init과 같은 기본적인 메시지를 보낼 수 있게 되는 것입니다. 심지어 클래스가 이들을 오버라이드 하지 않은 객체들에도 런타임 예외가 발생할 위험없이 메시지를 보낼 수 있습니다. 메시지를 보내기 전에 (respondsToSelector:를 사용하여) 확인할 필요가 없습니다. 더 중요한 것은, 이 NSObject의 "placeholder" 메소드가 Cocoa 객체의 공통 구조이며 모든 클래스가 다 따를경우 객체 상호작용을 더욱 안정적으로 만들어줄 언어규약(convention)을 세운다는 것입니다.

[편집] Instance and Class Methods

런타임 시스템은 루트 클래스에 정의된 메소드를 특별한 방식으로 처리합니다. 루트 클래스에 정의된 인스턴스 메소드는 인스턴스와 클래스 객체 모두에 의해 수행될 수 있습니다. 따라서 모든 클래스 객체는 루트 클래스에 정의된 인스턴스 메소드에 접근할 수 있습니다. 모든 클래스 객체는 같은 이름의 클래스 메소드를 가지고 있지 않은 경우라면, 어느 루트 인스턴스 메소드이건 수행할 수 있습니다.

예를 들어, 클래스 객체는 NSObject의 respondsToSelector: 와 performSelector:withObject: 인스턴스 메소드를 수행하도록 메소드를 받을 수 있습니다.

SEL method = @selector(riskAll:);


if ([MyClass respondsToSelector:method])

[MyClass performSelector:method withObject:self];


클래스 객체가 사용할 수 있는 인스턴스 메소드는 루트 클래스에 정의된 것 뿐임을 알아두십시오. 위의 예제에 만약 MyClass가 respondsToSelector: 나 performSelector:withObject:를 재정의 해놓았다면, 이들 새 메소드는 인스턴스 들만 사용 가능합니다. MyClass 클래스 객체 NSObject 클래스에 정의된 버젼만 수행할 수 있습니다. (물론 MyClass가 respondsToSelector: 와 performSelector:withObject: 를 클레스 메소드로 구현해 놓았다면, 이 클래스도 이들 새 버젼을 수행할 수 있습니다.)



[편집] The Life Cycle of a Cocoa Object

Cocoa 객체는 분명한 단계가 있는 수명 가지고 있습니다. 생성되고, 초기화되고, 사용되어집니다(다른 객체가 메시지를 보내는 것을 의미합니다). retain되고, 복사되고, 아카이브되고, 결국은 릴리즈되고 파괴됩니다. 아래의 논의에서 전형적인 객체의 생명을 상세한 설명은 제외한 채로 도표화합니다.

객체가 버려지는 마지막 단계부터 생각해 봅시다. 다른 몇몇 프로그래밍 언어와 다르게 Objective-C는 객체가 더이상 필요하지 않을때 자동으로 해제시키는 "가비지 컬렉션" 기능을 갖고 있지 않습니다. 가비지 컬렉션의 비융통성과 비용 대신, Cocoa와 Objective-C는 객체를 지속시키고 더이상 필요하지 않을때 폐기하는 것을 자발적이고 정책에 따르는 절차를 사용합니다.

이 절차와 정책은 레퍼런스 카운팅이라고 하는 개념에 담겨있습니다. 각 Cocoa 객체는 해당 객체의 존속에 관심이 있는 다른 객체(혹은 절차적인 코드의 출현)의 수를 나타내는 정수를 가지고 있습니다. 이 정수는 객체의 리테인 카운트라고 불립니다(리테인은 레퍼런스라는 용어의 중복표현을 피하기 위해 사용됩니다.) 객체를 alloc 이나 allocWithZone: 클래스 메소드로 만들어낸다면 Cocoa는 중요한 일을 수행합니다.

  • 객체의 isa 포인터를 지정합니다—NSObject 클래스의 유일한 퍼블릭 인스턴스 변수인 isa 포인터는 객체의 클래스를 가리키며, 객체를 런타임의 클래스 계층 뷰에 통합시킵니다. (“Object Creation”에서 더 많은 정보를 볼 수 있습니다.)
  • 객체의 리테인 카운트를 지정합니다—모든 객체에 공통으로 숨겨진 인스턴스 변수인 리테인 카운트를 1로 지정합니다. (객체를 만드는 자가 객체 유지에 관심이 있다고 가정합니다.)


객체 할당이 끝난 후, 일반적으로 객체의 인스턴스 변수를 합리적인 초기값으로 설정하는 것으로 초기화 합니다. (NSObject는 이런 용도의 프로토타입으로 init 메소드를 선언합니다.) 객체는 이제 사용될 준비가 다 되었습니다. 객체에 메시지를 보내거나, 다른 객체에 객체를 보내는 등의 일을 할 수 있습니다.

노트: initializer 가 명시적으로 따로 할당된 경우를 제외하면 객체를 반환할 수 있기 때문에 alloc 메시지 표현이 init 메시지(혹은 initializer) 내부에 네스팅 되어집니다. 예를들면,
id anObj = [[MyClass alloc] init];


객체에 release 메시지를 보내 객체를 릴리즈할때, NSObject 가 객체의 리테인 카운트를 줄입니다. 만약 리테인 카운트가 1에서 0으로 떨어진다면, 객체는 할당해제됩니다. 할당 해제는 두 단계로 이루어집니다. 일단 객체의 dealloc 메소드가 호출되어 인스턴스 변수를 릴리즈하고 동적으로 할당된 메모리를 해제합니다. 그 후 운영체제가 객체 자체를 파괴하고 객체가 차지했던 메모리를 반환합니다.

중요: 절대로 객체의 dealloc 메소드를 직접 호출하지 않습니다.


만일 객체가 금세 사라지길 원치 않는다면 어떻게 할까요? 만일 객체를 만든 후에 retain 메시지를 객체에 보내면 객체의 리테인 카운트가 증가해서 2가 됩니다. 이제 릴리즈 메시지가 두개 보내져야 할당해제(deallocation)이 일어납니다. Figure 2-3에서 다소 단순한 시나리오의 경우를 묘사하고 있습니다.

Figure 2-3 The life cycle of an object—simplified view
그림:A cc cfg object lifecycle 1.gif

물론, 이 시나리오에서 객체의 생성자는 객체를 유지할 필요가 없습니다. 이미 객체를 소유하고 있습니다. 하지만 만일 이 생성자가 이 객체를 다른 객체에 메시지로 보내려 한다면, 상황이 바뀌게 됩니다. Objective-C 프로그램에서 다른 객체로부터 받은 객체는 획득한 범위 안에서 항상 유효하다고 가정됩니다. 받는 객체는 보내진 객체에게 메시지를 보낼 수 있으며, 받은 객체를 다른 객체에 보낼 수 있습니다. 이 가정은 클라이언트 객체가 보내지는 객체에 레퍼런스를 갖게 될 때 까지 "행동"하고 있어야 하며 미리 해제되지 않을 것을 요구합니다.

클라이언트 객체가 받은 객체를 프로그래밍 범위 밖에서도 유지하고 싶다면, 해당 객체를 리테인할 수 있습니다. retain 메시지를 보내면 됩니다. 객체를 리테인 하는 것은 객체의 리테인 카운트를 증가시키는 것이며, 객체의 소유권에 관심이 있음을 표현하는 것입니다. 이 클라이언트 객체는 언젠가 후에 객체를 릴리즈해야할 책임이 있다고 가정합니다. 객체의 생성자가 릴리즈하고, 클라이언트 객체가 동일한 객체를 리테인 했다면, 해당 객체는 클라이언트가 릴리즈할때 까지 존속합니다. Figure 2-4 에서 이 절차가 묘사되어 있습니다.

Figure 2-4 Retaining a received object
그림:A cc cfg object lifecycle 2.gif

객체를 리테인하는 대신 copycopyWithZone:메시지를 보내는 것으로 객체를 카피할 수 있습니다. (많은 경우, 데이터를 캡슐화하는 서브클래스들의 경우 이 프로토콜을 받아들이거나 준수합니다.) 객체를 복사하는 것은 객체의 성질과 의도하는 사용에 따라 깊어질 수도 있고 얕아질 수도 있습니다. 깊은 수준의 복사(deep copy)는 복사된 객체의 인스턴스 변수들이 쥐고있는 객체들까지 복사하지만, 얕은 수준의 복사(shallow copy)는 이들 인스턴스 변수의 레퍼런스만 복사합니다.



사용에 있어서, 카피와 리테인의 차이점은 카피가 객체를 새로운 소유자의 독점적인 사용을 위한다는 점입니다. 새로운 소유자는 복사된 객체를 원래 객체의 고려없이 변화시킬 수 있습니다. 일반적으로 객체가 원시값을 캡슐화하고 있는 밸류 객체(value object)일 때, 리테인하기보다 카피를 하게됩니다. 객체가 변하는 NSMutableString과 같은 경우라면 더욱 그렇습니다. 변하지 않는 객체(immutable)의 경우라면 복사와 리테인은 거의 동일하고 구현도 비슷할 것입니다.

Figure 2-5 Copying a received object
그림:A cc cfg object lifecycle 3.gif

객체의 수명주기를 이런식으로 관리하는데 있어 잠재적인 위험이 있을수 있다는 것을 눈치챘을지도 모르겠습니다. 객체를 만들고 다른 객체로 보내는 객체는 보내지는 객체가 언제 안전하게 릴리즈될 수 있는지 항상 알수 있는 것은 아닙니다. 콜 스택에 있는 해당 객체의 레퍼런스가 복수가 존재할 수도 있고 그중의 몇몇은 객체를 만든 객체가 알지 못하는 객체가 가지고 있을 수 있습니다. 만약 만든 객체가 만들어진 객체를 릴리즈하고 다른 객체가 파괴되어버린 객체에 메시지를 보내면 프로그램은 오류를 낼 수 있습니다. 이 문제를 해결하기 위해 Cocoa는 autoreleasing 이라고 불리우는 할당해제(deallocation) 연기 기법을 소개합니다.

오토릴리징은 (NSAutoreleasePool에 정의된) 오토릴리즈 풀을 이용합니다. 오토릴리즈 풀은 명시적으로 정의된 범위 내에 있는 객체들의 집합이며 이들은 결국 릴리즈되기위한 표시가 되어져 있습니다. 오토릴리즈 풀은 네스트될 수 있습니다. 만약 한 객체에 오토릴리즈 메시지를 보내면 해당 객체의 레퍼런스가 가장 가까이에 있는(immediate) 오토릴리즈 풀에 들어가게 됩니다. 여전히 유효한 객체이기 때문에 오토릴리즈풀에 정의된 범주내에 있는 다른 객체들이 이 객체에 메시지를 보낼 수 있습니다. 프로그램이 이 범위의 끝까지 돌아가게 되면, 풀이 릴리즈되고 결과적으로 풀 안에 있는 모든 객체들도 릴리즈됩니다. (Figure 2-6을 보십시오.) 만약 오토릴리즈 풀을 셋업할 필요가 없는 응용프로그램을 개발하고 있으면, Application Kit은 응용프로그램의 이벤트 사이클의 범주로 자동으로 오토릴리즈 풀을 셋업합니다.

Figure 2-6 An autorelease pool
그림:A cc cfg autoreleasepool.gif

지금까지의 객체의 생명주기에 대한 토론은 그 사이클 동안 객체를 관리하는 기법에 집중해 있었습니다. 하지만 객체의 소유권에 대한 정책은 이들 기법의 사용을 보여줍니다. 이 정책은 다음과 같이 요약될 수 있습니다.

  • 만얄 객체를 할당하고 초기화해서 만든다면 (예를 들어, [[MyClass alloc] init]), 객체를 소유하고 릴리즈할 책임을 갖게 됩니다. 이 규칙은 NSObject의 편리한 new 메소드를 사용했을 경우에도 동일하게 적용됩니다.
  • 만일 객체를 복사(copy)한다면, 복사한 객체를 소유하며 릴리즈할 책임이 있습니다.
  • 만일 객체를 리테인하면, 객체의 부분적인 소유권을 갖게되며 더 이상 필요하지 않을때 반드시 릴리즈해야 합니다.


반대로,


  • 만일 다른 객체로부터 객체를 받으면, 객체를 소유하지 않기 때문에 릴리즈해서는 안됩니다. (이 규칙의 예외가 몇가지 경우가 있습니다. 이 예외들은 참고문서에 명시적으로 표시되어 있습니다.)


어느 규칙들이든 다 그렇듯이, 예외도 있고 "아하!"하고 느낄만한 것들도 있습니다.


  • 만일 클래스 팩토리 메소드를 통해 객체를 만들면 (예를 들어 NSMutableArray의 arrayWithCapacity: 메소드), 받는 객체가 오토릴리즈 될 것으로 가정합니다. 이들 객체를 직접 릴리즈해서는 안되며 계속 유지시키기 원하면 리테인해야 합니다.
  • 사이클 레퍼런스를 방지하기 위해, 자식 객체는 부모 객체를 리테인해서는 안됩니다. (부모는 자식의 창조자 이거나 자식을 인스턴스 변수로 쥐고 있는 객체입니다.)


노트: 위의 가이드라인의 "릴리즈"는 객체에 release나 autorelease 메시지를 보내는 것을 의미합니다.


만일 이 소유권 정책을 따르지 않으면 Cocoa 프로그램에 두가지 안좋은 일들이 생기게 됩니다. 생성하거나, 복사하거나, 리테인한 객체를 릴리즈하지 않았기 때문에 프로그램은 이제 메모리누수가 생기게 됩니다. 혹은 이미 할당해제된 객체에 메시지를 보냈기 때문에 프로그램이 크래쉬를 낼 수 있습니다. 그리고 더한 문제는 이런 문제를 디버깅하는 일은 시간이 상당히 드는 일이 될 수 있습니다.



객체의 생명주기에서 후에 생길수 있는 기본 이벤트는 아카이빙입니다. 아카이빙은 객체지향 프로그램(객체 그래프)를 구성하는 상호 연관된 객체의 망을 그래프 상의 각 객체의 정체성과 관계를 유지시키는 존속적인 형태(주로 파일)로 변환시키는 것 입니다. 프로그램이 언아카이브(unarchive)될 때에 객체 그래프는 이 아카이브로부터 재구성됩니다. 아카이빙(그리고 언아카이빙)에 참여하기위해, 객체는 반드시 인스턴스 변수들을 NSCoder 클래스의 메소드를 이용해 인코드하고 디코드할 수 있어야 합니다. NSObject는 이런 목적을 위해 NSCoding 프로토콜을 도입하고 있습니다. 객체 아카이빙에 대한 더 많은 정보는 "Object Archives" 에서 찾을 수 있습니다.


더 읽을 거리 : 이 Cocoa 객체의 생명주기 개요에서는 주제에 대해 말하고자 하는 것의 표면을 짧게 훑어본 것입니다. 메모리 관리와 Cocoa 객체에 관한 상세한 논의는 The Objective-C Programming Language의 The Objective-C Runtime System에서 찾을 수 있습니다. 또한 Memory Management Programming Guide for Cocoa 의 programming topics도 읽어보십시오.



[편집] 객체 생성

Cocoa 객체의 생성은 언제나 할당과 초기화의 두가지 단계를 거칩니다. 이 두 단계를 거치지 않은 객체는 일반적으로 전혀 사용가능하지 않습니다. 거의 모든 경우에 할당 직후에 초기화가 이루어지지만 이 두 작업은 객체의 구성에 있어 분명히 다른 역할을 합니다.

[편집] 객체 할당(Allocation)

객체를 할당할때 '할당하다'라는 용어에서 기대되는 것들의 일부분이 실제로 일어납니다. Cocoa는 응용프로그램 가상 메모리(application virtual memory) 지역으로부터 객체에 충분한 메모리를 할당합니다. 얼만큼의 메모리가 할당될지를 계산하기 위해 객체의 클래스에 지정된 대로 객체의 인스턴스 변수를 종류와 순서에 따라 고려하게 됩니다.

객체를 할당하려면, 객체의 클래스에 alloc 이나 allocWithZone: 메시지를 보냅니다. 결과로, 그 클래스의 초기화되지 않은 인스턴스를 받게 됩니다. alloc 변형 메소드는 응용프로그램의 디폴트 존(default zone)을 이용합니다. 존은 응용프로그램에 의해 할당된 관련된 객체와 데이터를 담기 위한 페이지정렬된 메모리 공간입니다. Memory Management Programming Guide for Cocoa 에서 존에대한 더 많은 정보를 찾을 수 있습니다.

할당 메시지는 메모리 할당 외에 다른 중요한 일들도 수행합니다.

  • 객체의 리테인 카운트를 1로 지정합니다. ([]http://wiki.osxdev.org/index.php/Cocoa_객체#The_Life_Cycle_of_a_Cocoa_Object | “The Life Cycle of a Cocoa Object”]]에서 언급하고 있는 것과 같습니다.)
  • 객체의 isa 인스턴스 변수를 객체가 객체의 클래스(클래스 정의에서 컴파일된 런타임 객체)를 가리키도록 초기화합니다.
  • 다른 인스턴스 변수들을 0으로 (혹은 nil, NULL, 0.0 과 같은 0에 대응하는 타입으로) 초기화합니다.


객체의 isa 인스턴스변수는 NSObject로 부터 상속받은 것이기에 모든 Cocoa 객체에 공통으로 존재합니다. allocation에서 isa를 객체의 클래스를 가리키도록 지정한 후, 객체는 상속 계층도의 런타임 뷰와 프로그램을 구성하는 현재 객체(클래스와 인스턴스) 네트웍에 통합됩니다. 이 결과로 객체는 런타임에서 상속 계층도에서 다른 객체의 위치, 다른 객체가 준수하는 프로토콜, 메시지에 응답하여 수행할 메소드 구현의 위치등 필요한 모든 정보를 찾을 수 있게 됩니다.

요약하면, allocation은 메모리에 객체를 할당시키는 것 뿐만 아니라 모든 객체에서 작지만 아주 중요한 두가지 특징을 초기화시킵니다. 바로 isa 인스턴스 변수와 리테인카운트입니다. 또한 남은 모든 인스턴스 변수를 0으로 초기화시킵니다. 그러나 아직 이 객체가 사용가능한 것은 아닙니다. init과 같은 메소드로 객체를 초기화시켜야만 객체의 독특한 특징들을 갖고 초기화되게 되며, 제대로 작동하는 객체가 반환되게 됩니다.

[편집] 객체 초기화하기

초기화는 객체의 인스턴스 변수들을 합리적이고 유용한 초기값으로 지정합니다. 또한 객체가 필요로하는 다른 글로벌 리소스가 파일과 같이 외부 소스인 경우 로딩하여 할당하고 준비시킵니다. 인스턴스 변수를 선언하는 모든 객체는 변수의 기본값이 0으로 지정하는 것으로 충분한 경우를 제외하면 초기화 메소드를 구현해야합니다. 만일 객체가 초기화메소드를 구현하지 않는다면, Cocoa는 가장 가까운 상위클래스의 초기화메소드를 호출합니다.

[편집] The Form of Initializers

NSObject는 초기화 메소드로 init 프로토타입을 선언하고 있습니다. id 형식의 객체를 반환하도록 되어있는 인스턴스 메소드입니다. 서브클래스가 객체를 초기화 하는데 다른 외부 데이터를 요구하지 않는다면 init을 오버라이드하는 것은 괜찮습니다. 그러나 종종 초기화는 객체를 의미있는 초기값으로 지정하기 위해 외부 데이터에 의존하곤 합니다. 예를 들어, Account 클래스가 하나 있다고 가정합시다. Account를 제대로 초기화하기 위해서는 유일한 계정 번호가 있어야 하며, 초기화 메소드에 이것이 제공되어야만 합니다. 따라서 초기화 메소드는 하나 혹은 복수의 인자를 받을 수 있습니다. 단 하나 지켜야 할 것은 초기화 메소드가 "init"으로 시작해야한다는 것입니다. (이 init... 스타일의 규약은 때로 초기화 메소드를 가리킬때 사용됩니다.)

노트: 초기화메소드를 인자와 함께 쓰는것 대신, 서브클래스는 단순한 init 메소드와 "set" 액세서(accessor) 메소드를 사용하여 초기화 직후 객체를 의미있는 초기값으로 지정할 수 있습니다. (액세서 메소드는 인스턴스 변수의 값을 지정하고 얻어오는 것으로 객체의 캡슐화를 강화합니다.)


Cocoa는 인자를 가지고 있는 초기화 메소드의 예를 많이 가지고 있습니다. 몇 개의 예가 여기 있습니다. (가로 안은 정의하는 클래스입니다.)

- (id)initWithArray:(NSArray *)array; (from NSSet)
- (id)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)anotherDate; (from NSDate)
- (id)initWithContentRect:(NSRect)contentRect styleMask:(unsigned int)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag; (from NSWindow)
- (id)initWithFrame:(NSRect)frameRect; (from NSControl and NSView)

이들 초기화메소드는 init으로 시작하여 동적 타입인 id 객체를 반환하는 인스턴스 메소드입니다, 그 외에는 Cocoa의 다수의 인자를 받는 메소드의 규약을 따릅니다. 종종 WithType: 이나 FromSource:와 같은 것들을 첫번째와 다른 중요한 인자들 뒤에 씁니다.

[편집] Issues with Initializers

메소드가 메소드 시그니춰에 객체를 반환하도록 요구되어지지만, 그 객체가 방금전에 할당된 init... 메시지의 리시버일 필요는 없습니다. 다른 말로, 초기화 메소드로부터 받은 객체가 초기화되는 객체일 필요는 없다는 것 입니다.

방금 할단된 객체가 아닌 다른 객체를 반환할때 두가지 조건이 있습니다. 먼저 다음의 두가지 관련된 상황과 관련합니다. 싱글톤 인스턴스(singleton instance)가 반드시 있는 경우, 혹은 객체의 정의된 특성이 유일한 경우여야 합니다. 몇몇 Cocoa 클래스—예를 들어, NSWorkspace—는 프로그램당 단 하나의 인스턴스만 허용합니다. 이런 경우의 클래스는 새로운 인스턴스를 요청할 경우 처음에 만든 인스턴스를 반환하여 단 하나의 인스턴스만 만들어지도록 해야합니다.(초기화 메소드나, 대부분의 경우는 클래스 팩토리 메소드에서 이 작업을 처리합니다.) (싱글톤 객체를 구현하는데 대한 정보를 “Creating a Singleton Instance” 에서 읽으십시오.

한 객체가 객체를 독특하게 만들어주는 특성을 가지고 있을때 비슷한 상황이 발생합니다. 앞에서 언급했던 가상의 Account 클래스를 기억해보십시오. 어느 종류의 account든 unique identifier가 필요합니다. 만일 클래스의 초기화메소드-말하자면 initWithAccountID:-에 특정 객체와 연결되어진 identifier가 보내진다면 두가지일을 반드시 처리해야 합니다.

  • 새로 할당된 객체를 릴리즈합니다
  • 해당 identifier로 초기화되었던 Account 객체를 반환합니다.


이것을 하는 것으로 초기화 메소드는 요청받은 것을 제공함과 동시에 identifier의 유일성을 보장합니다. 요청한 identifier와 연관된 Account 인스턴스를 제공합니다.

때로 init... 메소드는 요청받은 초기화작업을 수행할 수 없을수도 있습니다. 예를들어, initFromFile: 메소드는 인자로 받은 주소의 파일의 컨텐츠로부터 객체를 초기화할 것을 기대합니다. 그러나 해당 경로에 파일이 없다면, 객체는 초기화 될 수 없습니다. 만일 initWithArray: 초기화 메소드에 NSArray 객체대신 NSDictionary 객체가 인자로 전달되었다면 비슷한 문제가 발생합니다. init... 메소드가 객체를 초기화 할 수 없을때, 반드시 다음 작업을 수행해야 합니다.

  • 새로 할당된 객체를 릴리즈합니다.
  • nil을 반환합니다.


초기화 메소드에서 nil을 반환한다는 것은 요청된 객체를 생성할 수 없다는 것을 의미합니다. 일반적으로 객체를 생성할때는, 다음 진행이 있기 전에 밴환된 값이 nil인지 확인해야 합니다.

id anObject = [[MyClass alloc] init];

if (anObject) {    [anObject doSomething];

    // more messages...

} else {

    // handle error

}

init... 메소드가 nil 이나 명시적으로 할당된 객체가 아닌 다른 객체를 반환할 수 있기 때문에, 초기화 메소드에 의해 반환된 객체가 아닌 alloc이나 allocWithZone:으로 반환된 객체를 사용하는 것은 위험합니다. 다음 코드를 고려해보십시오.

id myObject = [MyClass alloc];

[myObject init];

[myObject doSomething];

이 예제의 init 메소드가 nil이나 다른 객체로 대치되었을 수도 있습니다. 익셉션이 발생하지 않고도 nil을 보낼 수 있기 때문에 이 앞의 경우 디버깅할때 골머리를 앓아야 하는 것을 제외하고는 별 일이 발생하지 않을 것입니다. 그러나 항상 그저 할당된 객체 대신 초기화된 객체에 의존해야 합니다. 할당과 초기화 메시지를 네스팅하고 더 진행하기 전에 반환된 객체를 테스팅하는 것이 좋습니다.

id myObject = [[MyClass alloc] init];

if ( myObject ) {

    [myObject doSomething];

} else {

    // error recovery...

}

일단 객체가 초기화되면, 다시 초기화해서는 안됩니다. 만일 객체를 다시 초기화하려 한다면, 초기화된 객체의 프레임웍 클래스가 종종 익셉션을 발생시킵니다. 예를들어, 다음 예제의 두번째 초기화는 NSInvalidArgumentException을 발생시킬 것입니다.

NSString *aStr = [[NSString alloc] initWithString:@"Foo"];

aStr = [aStr initWithString:@"Bar"];



[편집] Implementing an Initializer

클래스의 유일한 초기화 메소드로 작동하는 init... 메소드를 구현할 때나, 다수의 초기화 메소드가 존재할 시의 designated initializer(Multiple Initializers and the Designated Initializer에 설명되어 있습니다)를 구현하고자 할때 따라야하는 필수적인 단계가 있습니다.

  • 항상 슈퍼클래스(super)의 초기화메소드를 가장 먼저 부릅니다.
  • 슈퍼클래스로부터 반환된 객체를 확인합니다. 만약 nil이 반환되었다면 초기화는 더이상 진행될 수 없습니다. 리시버에 nil을 반환하십시오.
  • 객체를 가리키고 있는 인스턴스 변수를 초기화할 때, 필요에 따라 객체를 리테인하거나 카피하십시오.
  • 유효한 초기값으로 인스턴스 변수를 지정한 후, 다음의 경우를 제외하면 self를 반환해야합니다.
    • 만일 대체된 객체를 반환해야할 필요가 있을 경우, 새로 할당한 객체를 먼저 릴리즈합니다.
    • 어떤 문제가 초기화가 성공적으로 완료되는 것을 막았다면, nil을 리턴합니다.


Listing 2-3의 init... 메소드는 이 단계를 보여주고 있습니다.

- (id)initWithAccountID:(NSString *)identifier {
    if ( self = [super init] ) {
        Account *ac = [accountDictionary objectForKey:identifier];
        if (ac) { // object with that ID already exists
            [self release];
            return [ac retain];
        }
        if (identifier) {
            accountID = [identifier copy]; // accountID is instance variable
            [accountDictionary setObject:self forKey:identifier];
            return self;
        } else {
            [self release];
            return nil;
        }
    } else
        return nil;
}


노트:예제를 단순화하기 위해 이 예제에서 인자가 nil인 경우 nil을 반환하도록 되었지만, 더 나은 방법은 익셉션을 발생시키는 것입니다.



객채의 모든 인스턴스 변수들을 명시적으로 초기화해줄 필요는 없습니다. 객체가 작동하도록 하는데 필수적인 녀석들만 초기화 해주면 됩니다. 할당 단계의 인스턴스 변수를 0으로 기본 초기화 하는 것만으로도 종종 충분합니다. 필요에 따라 인스턴스 변수를 리테인하거나 카피하는 것을 잊지 마십시오.

맨 처음에 슈퍼클래스의 초기화 메소드를 호출하는 것은 매우 중요합니다. 객체가 자신의 클래스에 정의한 인스턴스 변수 뿐만 아니라 모든 조상 클래스가 정의한 인스턴스 변수도 캡슐화 하는 것을 기억하십시오. 슈퍼(super)의 초기화 메소드를 먼저 호출하는 것으로 상속연결고리의 위에 정의된 인스턴스 변수가 먼저 초기화되는 것을 확실히 할 수 있습니다. 바로 위의 슈퍼클래스는 그 초기화 메소드에서 자신의 슈퍼클래스의 초기화 메소드를 호출하며, 또 거기서 자신의 슈퍼클래스의 초기화 메소드를 호출하는 과정이 계속 반복됩니다. (Figure 2-7을 보십시오) 서브클래스의 초기화가 슈퍼클래스에 정의된 인스턴스 변수가 적절한 값을 가지고 있는 것을 요구할 수 있으므로 초기화의 올바른 순서는 매우 중요합니다.

Figure 2-7 Initialization up the inheritance chain
그림:A cc cfg init inheritance chain.gif

상속받은 초기화 메소드는 서브클래스를 만들때 고려해야하는 요소중의 하나입니다. 때로는 슈퍼클래스의 초기화메소드가 서브클래스의 인스턴스를 충분히 잘 초기화 시킬 수도 있습니다. 하지만 그렇지 않을 가능성이 더 높기에 초기화 메소드를 오버라이드 해야할 것입니다. 만일 그렇게 하지 않으면, 슈퍼클래스의 초기화 메소드가 호출될 것이고 슈퍼클래스는 서브클래스에 대해 아는 것이 전혀 없는 관계로 인스턴스를 제대로 초기화시키지 못할 수도 있습니다.

[편집] Multiple Initializers and the Designated Initializer

한 클래스가 하나 이상의 초기화메소드를 정의할 수 있습니다. 때로 복수의 초기화 메소드를 이용해 클래스의 클라이언트가 같은 초기화를 다른 형태로 할수 있는 입력을 제공하도록 해줍니다. 예를들어 NSSet 클래스는 클라이언트에게 몇몇 초기화 메소드를 제공하여 같은 데이터를 다른 형식으로 받아들이는 것을 가능하게 합니다. 하나는