Cocoa 프로그램에 행동 추가하기
OSXDEV
[편집] Cocoa 프로그램에 행동 추가하기
Objective-C로 프로그램을 개발할때, 결코 혼자서 하게되지 않을 것입니다. 애플과 다른 사람들이 개발하고 Objective-C 프레임웍 안에 패키지화 해 놓은 클래스들을 기반해서 작업하게 될 것입니다. 이들 프레임웍은 프로그램의 -종종 중요한- 부분을 함께 구성하는 상호 보완적인 클래스 모음을 제공해 줍니다.
이 챕터는 Cocoa 프레임웍을 사용하여 Objective-C 프레임웍을 작성하는 것이 어떠한지를 보여주고 있습니다. 또한 프레임웍 클래스의 서브클래스를 만드는데 알아야하는 기본적인 정보도 제공합니다.
[편집] 시작하기
Objective-C 클래스들과 메소드들의 프레임웍을 사용하는 것은 C 함수의 라이브러리를 사용하는 것과 좀 다릅니다. 후자의 경우는 작성하고자 하는 프로그램에 맞춰 어떤 함수를 사용할지 고르고 사용하길 원할때 사용하는 것이 거의 전부입니다. 하지만 프레임웍은 프로그램의 디자인을 구성하거나 적어도 프로그램에서 해결하고자 하는 특정한 문제 분야를 부여합니다. 절차적 프로그램에서는 라이브러리 함수를 필요에 따라 프로그램의 작업을 해결하기 위해 호출합니다. 객체 지향 프로그램에서도 프로그램의 대부분의 작업을 수행하기 위해 프레임웍의 메소드를 호출해야 한다는 점에서 비슷합니다. 하지만, 프레임웍을 사용자화하고 필요에 맞추기 위해 프레임웍이 적절한 때에 호출할 메소드들을 구현해야할 필요도 있을 것입니다. 이들 메소드들은 프레임웍에 제시된 구조에 여러분이 작성한 코드를 소개하는 "고리" 같은 역할을 할 것이며, 프로그램의 독특한 특성을 갖게하는 행동을 추가시킬 것입니다. 말하자면, 일반적인 프로그램과 라이브러리의 역할이 바뀌게 되는 것입니다. 라이브러리 코드를 프로그램에 통합시키는 것 대신, 여러분의 프로그램 코드를 프레임웍에 병합시키는 것입니다.
Cocoa 프로그램이 실행되게 될때 어떤 일들이 일어나는지 살펴보는 것을 통해 커스텀 코드와 프레임웍 사이의 관계에 대해 더 잘 알수 있을 것입니다.
[편집] What Happens in the main Function
Objective-C 프로그램은 C 프로그램과 마찬가지로 main 함수에서 시작합니다. 복잡한 Objective-C 프로그램의 경우, main 함수의 역할은 매우 단순하며, 두 단계로 이루어져 있습니다.
- 객체의 핵심 그룹을 설정합니다.
- 이들 객체에 프로그램 컨트롤을 넘겨줍니다.
핵심 그룹안의 객체들은 프로그램이 돌아가는 가운데 다른 객체들을 만들 수 있고, 이 만들어진 객체들이 또 다른 객체들을 만들 수도 있습니다. 때때로, 프로그램은 필요에 따라 클래스를 불러들이고, 인스턴스를 언아카이빙하고, 원격 객체를 연결하고, 다른 리소스를 찾을 수 있습니다. 그러나, 프로그램의 초기 작업을 다루기 위해서는 객체의 네트웍에 충분한 구조의 아웃셋만 필요합니다. 메인 함수는 이 초기 구조를 위치시키고 앞으로의 작업에 준비시킵니다.
특히, 이들 핵심 객체들중의 하나는 프로그램을 감독하거나 입력을 컨트롤할 책임을 갖습니다. 프로그램이 커맨드라인 툴이거나 백그라운드 서버라면, 이 후에 커맨드라인 인자를 넘기거나 원격 연결을 여는등의 단순한 작업이 올수 있습니다. 그러나 대부분의 Cocoa 프로그램은 좀 더 복잡한 작업이 수행됩니다.
응용프로그램을 위해 메인 함수가 설정하는 객체의 핵심 그룹은 사용자 인터페이스를 그리는 몇몇 객체를 포함해야만 합니다. 이 인터페이스, 혹은 (응용프로그램의 메뉴와 같은) 최소한의 부분은 사용자가 응용프로그램을 실행시킬때, 화면에 떠야만 합니다. 초기 사용자 인터페이스가 화면에 뜨게되면, 그 후에 응용프로그램은 대부분 사용자에 의해 발생한 외부 이벤트에 의해 작동됩니다. 버튼을 클릭하거나, 메뉴 아이템을 선택하거나, 아이콘을 드래그하거나, 필드에 무엇인가를 입력하는 것 등의 작업을 포함합니다. 이런 각각의 이벤트는 응용프로그램에 사용자 액션의 상황에 대한 상당한 정보와 함께 응용프로그램에 보고됩니다. 예를 들어, 어느 키가 눌렸는지, 마우스 버튼이 눌렸는지, 아니면 버튼을 뗐는지, 커서가 어디에 위치되어 있는지, 어떤 창이 영향을 받는지 등의 정보가 전달됩니다.
응용프로그램은 이벤트를 받고, 살펴본 후 (주로 사용자 인터페이스의 일부를 그리는 것으로) 이벤트에 반응합니다. 그런 다음 다음 이벤트를 기다립니다. 계속해서 이벤트를 받으며 사용자 혹은 (타이머와 같은) 다른 소스가 이벤트를 발생시키는 한 계속됩니다. 프로그램이 작동해서 종료할때까지, 프로그램이 하는 거의 대부분의 작업은 이벤트의 형태로 사용자의 액션에 의해 이루어집니다.
이벤트를 받고 응답하는 기법은 메인 이벤트 루프입니다. (응용프로그램이 부차적인 이벤트 루프는 짧은 시기동안만 설정할 수 있기 때문에 "메인" 이벤트 루프라고 불립니다.) 이벤트 루프는 결국 하나 혹은 그 이상의 입력 소스가 붙어있는 런루프입니다. 핵심 그룹의 한 객체는 메인 이벤트 루프를 작동시킬 책임(이벤트를 받고, 이벤트를 가장 잘 다룰 수 있는 객체(들)에 이벤트를 디스패치하고, 다음 이벤트를 받는 책임)을 갖고 있습니다. Cocoa 응용프로그램에서는, 이 조절 객체가 글로벌 응용프로그램 객체인 NSApplication의 인스턴스입니다. Figure 3-1 은 메인 이벤트 루프를 설명하고 있습니다.
Figure 3-1 The main event loop
거의 대부분의 Cocoa 응용프로그램의 main 함수는 (Listing 3-1에 나온 것처럼) 단 하나의 함수만 호출하면 되므로 매우 단순합니다. NSApplicationMain 함수는 애플리케이션 객체를 만들고, 오토릴리즈 풀을 구성하고, 초기 사용자 인터페이스를 main nib 파일로부터 로드하고, 응용프로그램을 돌리고, 그 결과로 응용프로그램이 메인 이벤트 루프에 받은 이벤트를 다루도록 요청합니다.
Listing 3-1 The main function in a Cocoa application
#import <AppKit/AppKit.h>
int main(int argc, const char *argv[]) {
return NSApplicationMain(argc, argv);
}
“The Core Application Architecture”에서 메인 이벤트 루프, 글로벌 NSApplication 인스턴스, 그리고 그외에 핵심 응용프로그램 객체를 더 상세히 설명하고 있습니다.
[편집] Cocoa 프레임웍 사용하기
라이브러리 함수는 자신을 사용하는 프로그램에 아주 소수의 제약을 부과합니다. 라이브러리가 필요한 때에 언제든 호출할 수 있습니다. 반면에, 객체 지향 라이브러리나 프레임워크에서의 메소드는 클래스 정의에 묶여있고 그 클래스 정의에 접근 가능한 객체를 만들거나 획득하지 않으면 호출할 수 없습니다. 게다가, 대다수의 프로그램에서 객체는 프로그램 네트웍에서 작동하기 위해 적어도 하나의 다른 객체와 연결되어 있어야 합니다. 클래스는 프로그램 컴포넌트를 정의합니다. 서비스에 접근하기 위해서는 프로그램의 구조에 클래스를 끼워 맞출 필요가 있습니다.
위의 사항을 감안하고, 라이브러리 함수의 모음과 상당히 비슷하게 작동하는 인스턴스를 만드는 프레임웍 클래스들이 몇몇 있습니다. 사용자는 간단히 인스턴스를 만들고, 초기화하고, 특정 작업을 완성하기 위해 메시지를 보내거나 응용프로그램의 대기 슬롯에 집어넣습니다. 예를 들어, NSFileManager 클래스를 이용해 파일의 이동, 복사, 삭제와 같은 다양한 파일-시스템 작업들을 수행할 수 있습니다. 경고 창을 띄울 필요가 있다면, NSAlert 클래스의 인스턴스를 만들어서 그 인스턴스에 적절한 메시지를 보낼 수 있습니다.
그러나 일번적으로, Cocoa 와 같은 환경은 각기 자신의 서비스를 제공하는 개개의 클래스의 뭉텅이를 능가합니다. 문제 공간을 구성하고 통합된 해결첵을 제시하는 클래스의 모임인, 객체 지향 프레임웍으로 이루어져 있습니다. 함수 라이브러리 처럼 필요에 따라 사용하는 분명한 서비스를 제공하는 것 대신, 프레임웍은 전체 프로그램 구조나 모델을 그려내고 구현하여 사용자의 코드가 그 구조에 맞추도록 합니다. 이 프로그램 모델은 일반적이기 때문에, 사용자의 특정 프로그램의 요구사항에 맞추어 특화시킬 수 있습니다. 프로그램을 디자인하여 라이브러리 펑션을 끼워넣기 보다, 프레임웍에의해 제공된 디자인에 사용자의 코드를 끼워넣게 되는 것입니다.
프레임웍을 사용하기 위해서는 프레임웍이 정의하는 프로그램 모델을 받아들이고 사용자의 특정 프로그램을 모델에 맞추기 위해서 필요한 객체들을 이용하고 사용자화 해야 합니다. 클래스는 상호 의존적이며 그룹으로 제공됩니다. 첫 눈에는, 사용자의 코드를 프레임웍의 프로그램 모델에 적응시켜야한다는 것이 제한적으로 보일 수도 있습니다. 그러나 현실은 반대입니다. 프레임웍은 프레임웍의 일반적인 행동을 확장하거나 변경시킬수 있는 많은 방법을 제시합니다. 단순히 사용자가 모든 Cocoa 프로그램이 동일한 프로그램 모델을 기반하여 만들어졌기 때문에 같은 기초적인 방식으로 작동한다는 것을 받아들이는 것을 요구하는 것입니다.
[편집] Kinds of Framework Classes
Cocoa 프레임웍의 클래스들은 다음 네가지 방식으로 서비스를 제공합니다.
- 바로 사용할 수 있는 클래스 몇몇 클래스들은 바로 사용할 수 있는 객체들입니다. 필요할 때 간단히 클래스의 인스턴스를 만들고 초기화하면 됩니다. NSTextField, NSButton, NSTableView(와 각기 관련된 NSCell 클래스들)과 같은 NSControl의 서브클래스들이 이런 분류에 속합니다. 일반적으로 이런 바로 사용할수 있는 객체들은 Interface Builder를 이용해 만들고 초기화하지만, 코딩을 해서도 만들고 초기화할 수 있습니다.
- 뒤에서 작동하는 클래스 프로그램이 돌아가면서, Cocoa는 뒤에서 작업하는 프레임웍 객체를 만듭니다. 이들 객체들은 명시적으로 할당해주거나 초기화해줄 필요가 없습니다. 사용자를 위해 이 과정들이 알아서 수행됩니다. 종종 이런 클래스들은 프라이빗이지만, 원하는 행동을 얻기 위해 구현할 필요가 있습니다.
- 일반적인 클래스(Generic) 몇몇 클래스는 일반적, 혹은 추상적입니다. 프레임웍은 제너릭 클래스의 서브클래스로 구상 클래스를 제공하여 바꾸지 않고 사용할 수 있게 할 수도 있습니다. 하지만 (어느 경우에는 반드시) 자신만의 서브클래스를 정의하고 특정 메소드의 구현을 오버라이드 할 수 있습니다. NSView, NSDocument, NSFormatter가 이런 종류의 클래스의 예입니다.
- 딜리게이터와 노티파이어 많은 프레임웍 객체는 다른 객체에게 그들의 행동을 알려주며 심지어 특정 책임을 그들 객체에 떠넘기기도 합니다. 이런 정보를 제공하는 기법이 딜리게이션과 노티피케이션입니다. 책임을 넘기는 객체는 비공식 프로토콜로 알려진 인터페이스를 공개합니다. 클라이언트 객체는 대리인(딜리게이트)로 등록하고 이 인터페이스의 한개 혹은 그 이상의 메소드를 구현합니다. 정보를 알려주는(notifying) 객체는 공지하기 원하는 노티피케이션의 목록을 공개하고 이 노티피케이션을 원하는 클라이언트는 누구나 관찰할 수 있습니다. 이런 딜리게이터 클래스는 NSApplication, NSText, NSWindow 등이 있습니다. 많은 프레임웍 클래스들이 노티피케이션을 통지합니다.
몇몇 클래스는 이들 일반적인 종류의 서비스를 하나이상 제공합니다. 예를 들어, 이미 만들어진 NSWindow 객체를 Interface Builder의 팔렛트로부터 드래그해서 약간의 초기화를 거쳐 사용할 수 있습니다. 이렇게, NSWindow 클래스는 바로 사용할수 있는 인스턴스를 제공합니다. 하지만, NSWindow 객체는 딜리게이트에 메시지를 보내고 다양한 노티피케이션을 통지합니다. 심지어 NSWindow를 서브클래스해서 원한다면 동그란 모양의 창을 만들 수도 있습니다.
후자의 두 카테고리-제네릭, 딜리게이터/노티파이어-에 속하는 Cocoa 클래스가 사용자 프로그램의 특정 코드를 프레임웍에 의해 제공된 구조에 통합시킬 수 있는 가능성을 가장 많이 제공합니다. "Inheriting From a Cocoa Class”에서 프레임웍 클래스, 특히 제네릭 클래스로부터 어떻게 서브클래싱하는지 일반적인 용례를 다루고 있습니다. 딜리게이션과 노티피케이션, 또 프로그램 네트웍에서 객체들간 의사소통하는 다른 기법들에 대해 알고 싶으시다면 “Communicating With Objects”를 보십시오.
[편집] Cocoa API Conventions
Cocoa 프레임웍의 클래스, 메소드, 다른 API들을 사용하기 시작할 때, 사용의 효율성과 일관성을 유지할 목적을 위한 몇몇 컨벤션을 알고 있는 것이 좋습니다.
- 객체를 반환하는 메소드는 "객체 생성 실패" 혹은 "반환할 객체 없음"을 의미할때 보통 nil 을 반환합니다. 상태 코드를 반환하지는 않습니다. nil을 반환하는 이 컨벤션은 보통 런타임 에러나 다른 비-예외조건을 지시하는데 사용됩니다. Cocoa 프레임웍은 "array index out of bounds"나 "method selector not recognized"와 같은 에러를 (최고수준 핸들로로 다뤄지는)익셉션을 제기하고, 메소드 시그니춰가 요구한다면 nil을 반환합니다.
- 이 nil을 반환할 수 있는 몇몇 메소드들은 레퍼런스로 에러 정보를 반환하는데 최종 파라미터를 포함할 수 있습니다. 이 최종 파라메터는 NSError 객체의 포인터를 받습니다. (nil을 반환하여) 메소드 호출이 실패했을 때, 반환된 에러 객체를 조사하여 에러의 원인을 밝혀내거나 사용자에게 에러를 다이얼로그로 표시할 수 있습니다. 예로 여기 NSDocument 클래스의 메소드가 하나 있습니다.
- (id)initWithType:(NSString *)typeName error:(NSError **)outError;
- 비슷한 방식으로, 시스템 오퍼레이션을 수행하는 메소드는 종종 성공이나 실패를 불리언 값으로 반환합니다. 이들 메소드 또한 NSError 객체를 최종 레퍼런스 파라미터로 포함할 수 있습니다. 예로 NSData 클래스의 이 메소드를 봅시다.
- (BOOL)writeToFile:(NSString *)path options:(unsigned)writeOptionsMask error:(NSError **)errorPtr;
- 빈 컨테이너 객체는 기본 값이나 없는 값을 지시하기 위해 사용됩니다. nil은 주로 합당한 객체 인수가 아닙니다. 많은 객체들은 값이나 객체의 컬렉션을 캡슐화 합니다. 예를 들어, NSString, NSDate, NSArray, NSDictionary의 인스턴스들이 그렇습니다. 이들 객체를 파라미터로 받는 메소드들은 빈 객체(예를 들어, @"")를 받아 "값이 없음"혹은 "기본 값"으로 지시할 수 있습니다. 예로, 다음 메시지는 창에 표시되는 파일명을 빈 스트링을 지정하는 것으로 값이 없다고 지정하고 있습니다.
[aWindow setRepresentedFilename:@""];
- Cocoa 프레임웍은 딕셔너리 키, 노티피케이션, 익셉션 이름, 그리고 그외 스트링을 메소드 파라미터로 사용할 때 스트링 리터럴보다는 글로벌 스트링 상수를 기대합니다. 선택할 수 있다면 스트링 리터럴보다는 스트링 상수를 선택하는 것이 좋습니다. 스트링 상수를 이용하면 컴파일러가 스펠링 체크를 해주기 때문에 런타임 에러를 회피할 수 있습니다.
- Cocoa 프레임웍은 일관성 있게 타입을 사용하며 그들의 API 모음에 고수준의 임피던스 매칭을 제공합니다. 예를 들어, 프레임웍은 좌표 값에 float을, 그래픽과 좌표 값에 CGFloat을, 좌표계의 위치에는 (CGFloat 두개로 구성된) NSPoint를, 스트링 값에는 NSString 객체를, 범위(시작과 오프셋)에는 NSRange를, 정수와 양의 정수값을 위해 NSInteger 와 NSUInteger를 사용합니다. API를 디자인할때 이런 타입 일관성을 유지하도록 노력해야 합니다.
Cocoa API 서브셋의 상당수의 컨벤션은 클래스, 메소드, 함수, 상수, 다른 심볼들의 이름을 짓는것과 관계있습니다. 자신만의 프로그래밍 인터페이스를 디자인 하고자 할때 이런 컨벤션을 알고 있는 것이 좋습니다. 중요한 이름 컨벤션은 다음과 같은 것들이 있습니다.
- 클래스나 심볼의 이름에 클래스와 관련된(함수나 typedef같이) 접두어를 사용하십시오. 접두어는 이름의 충돌을 막고 기능의 범위를 구분하는데 도움을 줍니다. 접두어 컨벤션은 대문자로 두세자를 붙이는 것입니다 예로 ACCircle 의 "AC"가 접두어입니다.
- 더 짧은것보다는 API이름과 함께 써서 더 분명한 것이 좋습니다. 예를 들어 removeObjectAtIndex:가 하는 일이 무엇인지 이해하기 쉽지만, remove: 는 애매합니다.
- 애매한 이름을 피하십시오. 예를 들어 displayName 은 이름을 표시하는 것인지, 아니면 display name 을 반환하는 것인지 불명확하기 때문에 애매합니다.
- 메소드나 함수의 이름에 액션을 표현하는 동사를 사용하십시오
- 메소드가 애트리뷰트나 계산값을 반환하면 메소드의 이름은 해당 속성의 이름이 되어야 합니다. 이들 메소드는 "게터" 액세서 메소드라고 알려져 있습니다. 예를 들어, 속성이 배경색이라면, 게터 메소드의 이름은 backgroundColor 가 되어야 합니다.
불리언 값을 반환하는 게터 메소드는 약간의 변형된 이름을 갖는데 hasColor 처럼 "is"나 "has"를 사용합니다.
- 메소드가 속성의 값을 지정하는 "세터" 액세서 메소드라면 "set"으로 시작해서 속성 이름이 그 뒤에 나옵니다. 속성의 첫 글자는 대문자로 쓰입니다. 예로 setBackgroundColor: 를 들 수 있습니다.
- API의 이름이 (HTML이나 TIFF와 같이) 잘 알려진 경우를 제외하고는 축약하지 마십시오.
Objective-C 프로그래밍 인터페이스를 위한 이름 짓기의 완전한 가이드라인은 Coding Guidelines for Cocoa에서 찾을 수 있습니다.
일반적이고 통합적인 API 컨벤션은 객체 소유를 고려합니다. 짧게 언급하면, 객체를 만들거나(할당과 초기화를 통해서), 복사하거나, 리테인하면(리테인 메시지를 보내어) 클라이언트가 객체를 소유한다고 합니다. 객체의 소유자는 객체가 필요가 없어지면 객체에 release나 autorelease 메시지를 보내어 객체를 해제할 책임이 있습니다. 이 주제에 대해서는 “The Life Cycle of a Cocoa Object”와 Memory Management Programming Guide for Cocoa에서 살펴볼 수 있습니다.
[편집] Cocoa 클래스로부터 상속하기
Application Kit 과 같은 프레임웍은 일반적이기 때문에 많은 다른 종류의 프로그램들이 공유할 수 있는 프로그램 모델을 정의합니다. 모델이 일반적이기에 몇몇 프레임웍 클래스들은 추상적이거나 의도적으로 불완전할 수 있습니다. 클래스가 저수준과 공통 코드에서 상당한 작업을 수행하지만 상당한 양의 작업을 완성해놓지 않거나 안전하지만 일반적인 "기본" 방식으로 완성되어 있습니다.
응용프로그램은 종종 프레임웍 클래스가 빼놓은 조각을 제공해서 슈퍼클래스에 있는 이러한 빈틈을 채워넣는 서브클래스를 만들 필요가 있습니다. 서브클래스는 프레임웍에 응용프로그램의 특징을 추가하는 가장 우선적인 방법입니다. 커스텀 서브클래스의 인스턴스는 프레임웍이 정의하는 객체의 네트웍에 자리를 잡게 됩니다. 프레임웍의 다른 객체와 함께 작동할 수 있는 능력을 상속받습니다. 예를 들어, NSCell의 서브클래스를 만들었다면, 이 새 클래스의 인스턴스는 NSButtonCell, NSTextFieldCell, 그리고 프레임웍이 정의한 다른 셀 객체들과 마찬가지로 NSMatrix 객체에 나타나는 것이 가능합니다.
서브클래스를 만들때, 우선적으로 해야할 작업중의 하나는 슈퍼클래스(혹은 슈퍼클래스가 도입한 프로토콜)에 정의한 특정 메소드들을 구현하는 것입니다. 상속받은 메소드를 재구현하는 것은 그 메소드를 오버라이드 하는 것으로 알려져 있습니다.
[편집] 메소드를 오버라이드 해야할 때
프레임웍 클래스에 정의된 대부분의 메소드는 완벽하게 구현되어 있습니다. 이런 메소드들은 호출해서 클래스가 제공하는 서비스를 바로 얻을 수 있도록 존재합니다. 이런 메소드들을 오버라이드해야할 필요는 거의 없으며 오버라이드를 시도하지 않는 것이 좋습니다. 프레임웍은 이들 메소드가 수행하는 것 그 이상도 이하도 아닌 구현된 그 상태를 필요로 합니다. 다른 경우, 메소드를 오버라이드 할 수는 있지만 그렇게 해야할 현실적인 이유가 없을 수 있습니다. 프레임웍에서 정의된 메소드는 적절하게 작업을 수행합니다. 그러나 원한다면, strcmp 대신 스트링 비교 함수를 직접 작성할 수 있는 것처럼 프레임웍의 메소드를 오버라이드 할 수 있습니다.
반면, 몇몇 프레임웍 메소드는 오버라이드되도록 만들어져 있습니다. 사용자가 프로그램에 특정한 행동을 프레임웍에 추가하도록 메소드가 존재합니다. 이런 메소드는 종종, 프레임웍에 구현된 상태로는 프로그램에 가치있는 일을 거의 하지 못하지만, 다른 프레임웍 메소드에 의해 시작된 메시지에서 호출됩니다. 이런 종류의 메소드에 내용을 더해주기 위해서는 프로그램이 스스로의 버전으로 구현해야만 합니다.
[편집] Invoke or Override?
서브클래스에서 오버라이드하는 프레임웍 메소드들은 일반적으로 직접 호출하지는 않습니다. 메소드를 재구현하고, 나머지를 프레임웍에 맡겨두면 됩니다. 사실, 응용 프로그램의 특별한 필요에 맞는 메소드를 작성할수록 작성한 코드에서 직접 호출해줄 필요가 더 없어집니다. 이렇게 되는 이유가 있는데, 일반적으로 프레임웍 클래스는 퍼블릭 메소드를 선언하여 개발자가 아래의 두가지 일을 할수 있게 합니다.
- 클래스가 제공하는 서비스를 사용할 수 있게 하기 위해 퍼블릭 메소드를 호출합니다.
- 프레임웍에 의해 정의된 프로그램 모델에 작성한 코드를 소개하기 위해 오버라이드합니다.
때로 메소드는 이 두 카테고리에 모두 속할 수 있습니다. 이런 메소드는 호출을 통해 귀중한 서비스를 제공할 수 있으며 전략적으로 오버라이드 될수도 있습니다. 그러나 일반적으로 호출할 수 있는 메소드는 프레임웍에 의해 완전히 정의되어있고 코드를 재정의해야할 필요가 없습니다. 만일 메소드가 서브클래스에서 재정의해야할 필요가 있다면, 프레임웍에서 해당 메소드가 수행할 특별한 임무를 가지고 있으며 적절한 시간에 그 메소드 자체를 호출할 것입니다.
Figure 3-2 오버라이드된 메소드를 메시지로 보내는 프레임웍 메소드 호출하기
Cocoa 프레임웍의 객체지향 프로그래밍의 대부분의 일은 프레임웍에 의해 배치되어있는 메시지를 통해 사용자의 프로그램이 간접적으로 사용하는 메소드를 구현하는 것입니다.
[편집] Types of Overridden Methods
서브클래스에서 몇몇 종류의 메소드 타입을 정의할 수 있습니다.
- 어떤 프레임웍 메소드는 다른 프레임웍 메소드에 의해 호출되도록 완전히 구현되어 있습니다. 다른말로 하면, 이들 메소드를 재구현하더라도, 코드 어디선가 직접 호출해줄 일은 거의 없습니다. 이들 메소드는 프로그램 수행중에 다른 코드가 요청할 만한 (데이터나 행동) 서비스를 제공합니다. 이들 메소드는 사용자가 원할때 오버라이드 할 수 있도록 하기 위한 유일한 이유때문에 퍼블릭 인터페이스에 존재합니다. 프레임웍에서 사용되는 알고리즘을 사용자의 버젼으로 대치하거나 프레임웍 알고리즘을 수정하거나 확장할 수 있는 기회를 제공합니다. 이런 종류의 메소드의 한 예로 NSMenuView 클래스에 정의된 trackWithEvent:가 있습니다. NSMenuView는 이 메소드를 메뉴 트랙킹과 아이템 선택의 즉각적인 요구사항을 충족시킬 수 있도록 이 메소드를 구현하고 있으나, 다른 행동을 원한다면 오버라이드 할 수 있습니다.
- 다른 메소드의 종류는 애트리뷰트가 켜져 있는지, 특정한 정책이 효과를 발휘하고 있는지와 같은 객체에 특정한 결정들을 합니다. 프레임웍은 이 메소드의 디폴트 버젼을 구현하여 어느 한 방식으로 결정하도록 하고 있으며, 사용자는 다른 결정을 하길 원한다면 반드시 자신의 버젼을 구현해야 합니다. 대부분의 경우, YES나 NO를 반환하거나, 디폴트 값이 아닌 값을 계산해내는 것으로 단순히 구현합니다. NSResponder의 acceptsFirstResponder 메소드가 이런 종류의 전형적인 예입니다. View는 acceptsFirstResponder 메시지를 받고, 키 입력이나 마우스 클릭에 반응하는지를 질문받게 됩니다. 디폴트 값으로, NSView 객체는 이 메소드에 NO를 반환합니다. 대부분의 뷰는 타입한 입력을 받아들이지 않습니다. 그러나 받아들이는 몇몇 뷰에서는 acceptsFirstResponder를 오버라이드하여 YES를 반환하도록 해야합니다.
- 어떤 메소드들은 반드시 프레임웍에서 구현된 메소드의 기능을 교체하는 것이 아니라 무언가를 더하기 위해 오버라이드 해야합니다. 서브 클래스 버젼의 메소드는 슈퍼클래스 버젼의 메소드에 행동을 더합니다. 이들 메소드중의 하나를 사용자의 프로그램이 구현한다면, 오버라이드하는 바로 그 메소드를 통합시키는 것이 중요합니다. 그렇게 하기 위해 super(슈퍼클래스)에 메시지를 보내는 것으로 프레임웍에 정의된 버전의 메소드를 호출할 수 있습니다. 종종 이런 종류의 메소드는 상속 체인에 있는 모든 클래스들이 그 메소드에 공헌할 것이 있다고 기대되어지는 것들입니다. 예를 들어, 자신을 아카이빙하는 객체는 반드시 NSCoding 프로토콜을 따라야 하며, initWithCoder:와 encodeWithCoder: 메소드를 구현해야만 합니다. 그러나 클래스가 해당 인스턴스 변수에 특정한 인코딩 혹은 디코딩 작업을 수행하기 이전에, 슈퍼클래스 버젼의 그 메소드를 호출해야만 합니다
- 때로 서브클래스 버젼의 메소드가 슈퍼클래스의 행동을 재사용한 후, 얼마나 작건간에 최종 결과에 무엇인가를 추가하고 싶을 수도 있습니다. NSView의 drawRect: 메소드를 예로 보면, 무엇인가 복잡한 그리기 작업을 수행하는 뷰 클래스의 서브클래스는 그림 주변에 테두리를 그리길 원해서 super를 먼저 부를 수 있습니다. 몇몇 프레임웍 메소드는 작업을 전혀 수행하지 않거나, 런타임, 컴파일타임 오류를 방지하기 위해 self와 같은 단순한 기본값을 반환합니다. 이들 메소드들은 오버라이드되도록 만들어진 것입니다. 프레임웍은 이들 메소드들의 작업이 프로그램의 성격에 따라 완전히 바뀌기 때문에 이들을 기본적인 형태로도 정의할 수 없습니다. 이런 경우 이 메소드에 super 메시지를 이용해 프레임웍에 구현된 버젼을 통합시킬 필요가 없습니다. 서브클래스가 오버라이드하는 대부분의 메소드는 이 그룹에 속합니다. 예를 들어, NSDocument 메소드인 dataOfType:error:와 readFromData:ofType:error: 는 (다른 것들과 같이) 문서-기반 응용프로그램을 만들고자 할때 오버라이드 해야 합니다.
메소드를 오버라이드하는 것이 반드시 많은 양의 일을 의미하지는 않습니다. 단지 한두줄의 코드만 조심스럽게 재구현하는 것으로 슈퍼클래스 버젼의 행동에 상당한 변화를 줄 수 있습니다. 사용자가 사용자 버젼의 메소드를 구현할 때, 모든것을 혼자 해야 하는 것이 아닙니다. Cocoa 프레임웍에 의해 이미 제공되고 있는 클래스, 메소드, 함수, 그리고 타입에 의존할 수 있습니다.
[편집] When to Make a Subclass
클래스의 어떤 메소드를 오버라이드 할지, 그리고 이 결정을 실제로 수행하는 것 만큼 중요한 것은 어느 클래스로부터 상속할지를 알아내는 것입니다. 어떤경우 이런 결정은 분명할 수 있지만, 또 어떤 경우는 결정하는 것이 쉽지 않을 수 있습니다. 약간의 디자인 사안들을 고려하는 것으로 이 선택을 좀 더 쉽게 할 수 있습니다.
먼저, 프레임웍을 알아야 합니다. 각 프레임웍 클래스의 목적과 기능에 대해 익숙해져야 합니다. 여러분이 하고싶은 일을 수행하는 클래스가 이미 있을 수도 있습니다. 만약 원하는 것을 수행하는 작업을 거의 다 하는 클래스를 찾는다면 행운인 것입니다. 그 클래스는 사용자의 커스텀 클래스의 슈퍼클래스가 될 가능성이 높습니다. 서브클래싱은 이미 존재하는 클래스를 재사용하고 필요에 따라 전문화하는 과정입니다. 때로 서브클래스가 해야하는 모든 일이 상속받은 메소드 하나만 오버라이드해서 원래 그 메소드의 행동을 아주 약간만 바꾸는 정도일 수 있습니다. 또 다른 서브클래스의 경우는 하나 혹은 두개의 속성을 그 슈퍼클래스에 (인스턴스 변수로) 추가하고, 이들 속성에 접근하고 작동하고, 슈퍼클래스의 행동에 통합시키는 메소드를 정의할 수 있습니다.
사용자의 서브클래스가 클래스 계층도에 어디에 위치하게 될지 결정하는 것을 도와줄만한 사항들이 또 있습니다. 작성하고자 하는 응용프로그램 혹은 응용프로그램 일부의 성격이 어떠한지를 살펴보십시오. Cocoa 아키텍쳐는 자신이 원하는 서브클래싱 요구사항을 가지고 있습니다. 예를 들어, 사용자의 응용프로그램이 다중 문서 응용프로그램이라면(MDA), Cocoa의 문서 기반 아키텍쳐가 NSDocument와 다른 클래스들을 서브클래스하도록 요구합니다. 응용프로그램을 스크립팅 가능하도록(AppleScript에 응답하도록) 만들기 위해서는 NSScriptCommand와 같은 스크립팅 클래스중의 하나를 서브클래스해야할 것입니다.
또 다른 요소는 서브클래스의 인스턴스가 응용프로그램에서 수행하게 될 역할입니다. Cocoa의 주요 디자인 패턴인, 모델-뷰-컨트롤 디자인 패턴에서 객체의 역할을 부여합니다. 사용자 인터페이스에는 뷰 객체가 뜨며, 응용프로그램 데이터(와 데이터를 다루는 알고리즘)는 모델 객체에 담겨있으며, 뷰와 모델 객체 사이에서 컨트롤러 객체가 중간 다리 역할을 합니다. (상세한 내용은 “The Model-View-Controller Design Pattern” 에서 볼 수 있습니다. ) 객체의 역할을 알면, 어떤 슈퍼클래스를 선택하는 범위를 좁힐 수 있습니다. 만일 커스텀 드로잉과 이벤트 핸들링을 수행하는 뷰 클래스를 만들고 있다면, 클래스는 아마 NSView를 상속받을 것입니다. 만일 컨트롤러 객체가 필요하다면, (NSObjectController와 같은) 이미 만들어져 있는 컨트롤러 클래스를 사용해도 되고, 다른 행동을 원한다면 NSController나 NSObject를 서브클래스해도 됩니다. 만일 클래스가 스프레드쉬트에서 집합적인 데이터들의 행으로 표시되는 전형적인 모델 클래스라면, 아마 NSObject를 서브클래싱하거나 CoreData 프레임웍을 사용해야 할 것입니다.
그러나 때로 서브클래싱이 문제해결의 최상의 선택이 아닐 수 있습니다. 더 나은 접근방법이 있을 수 있습니다. 만일 클래스에 편리하게 사용할 수 있는 메소드를 몇개 추가하길 원한다면 서브클래스 대신 카테고리를 만들 수 있습니다. 혹은, 딜리게이션, 노티피케이션, 타겟-액션("Communicating With Objects"에서 논의되고 있습니다)과 같은 Cocoa 개발 "툴박스"에 기반한 디자인-패턴중 하나를 이용할 수 있습니다. 슈퍼클래스 후보를 결정할때는, 클래스의 헤더파일(혹은 레퍼런스 문서)를 살펴봐서 딜리게이션 메소드, 노티피케이션 혹은 다른 방법을 통해 서브클래싱 없이 원하는 작업을 수행할 수 있는지 알아보십시오.
위와 비슷하게, 프레임웍의 프로토콜도 헤더파일이나 문서를 통해 살펴볼 수 있습니다. 프로토콜을 받아들이는 것으로, 복잡한 서브클래스를 만드는 어려움들을 피하면서 원하는 목적을 달성할 수 있습니다. 예를 들어, 메뉴 아이템이 활성화 상태를 다루고 싶다면, NSMenuValidation 프로토콜을 커스텀 컨트롤러 클래스에서 받아들이면 됩니다. 이 기능을 이용하기 위해 NSMenuItem이나 NSMenu를 서브클래스할 필요가 없습니다.
몇몇 프레임웍 메소드가 오버라이드되도록 만들어지지 않은 것처럼, 몇몇 프레임웍 클래스들은(NSFileManager, NSFontPanel, NSLayoutManager) 서브클래스되도록 만들어지지 않았습니다. 만일 이들 클래스를 서브클래스하려 한다면, 주의깊게 진행해야 합니다. 특정 프레임웍 클래스의 구현은 복잡할 뿐만 아니라 다른 클래스들, 혹은 운영체제의 다른 부분과 긴밀히 통합되어 있습니다. 프레임웍 메소드가 수행하는 것을 정확히 복사하거나 메소드가 가지고 있는 효과나 상호의존을 기대하는 것은 보통 어렵습니다. 몇몇 메소드 구현에 변화를 주는 것은 예측 불가능하고 불쾌한 결과를 낳을 수 있습니다.
어느 경우에는 이런 어려움들을 해결하기 위해 객체들을 복잡하고 고수준으로 커스텀화된 행동을 하도록 객체들을 다루는 호스트 객체에 객체를 조립해넣는 일반 기술인 객체 컴포지션을 사용할 수 있습니다. (Figure 3-3을 보십시오) 복잡한 프레임웍 슈퍼클래스를 직접 상속받는 것 대신, 커스텀 클래스를 만들어서 슈퍼클래스의 인스턴스를 인스턴스 변수로 가지고 있을 수 있습니다. 루트 클래스인 NSObject를 직접 상속받으면 커스텀 클래스 그 자체는 상당히 단순할 수 있습니다. 상속의 측면에서는 단순하지만, 클래스는 포함된 인스턴스를 수정하고, 확장하고 증가시킬 수 있습니다. 비록 슈퍼클래스의 인터페이스를 공유하지는 않을 지라도 클라이언트 객체에게는 복잡한 슈퍼클래스의 서브클래스처럼 보일 수 있습니다. Foundation 프레임웍 클래스인 NSAttributedString 은 이런 객체 컴포지션의 한 예 입니다. NSAttributedString은 NSString 객체를 인스턴스 변수로 가지고 있어서 스트링 메소드를 통해 외부에 노출시킵니다. NSString은 스트링 인코딩, 스트링 검색, 패스 조작 등과 같은 복잡한 행동을 갖는 클래스입니다. NSAttributedString은 이런 행동에 폰트나 색상, 정렬, 문단 스타일을 선택한 글자에 입히는 기능을 추가합니다. 그리고 그것을 NSString의 서브클래싱 없이 해냅니다.
Figure 3-3 Object composition
때로 슈퍼클래스로 분명해보이는 후보가 가장 좋은 선택은 아닐 수 있습니다. 아시다시피, NSView 객체는 Cocoa가 대부분의 경우에 그림을 그리는데 사용합니다. 그렇지만 수백, 수천개의 그래픽 엘레먼트를 가지고 있는 드로잉이나 캐드 프로그램을 생각하고 있다면, NSView로부터 상속받지 않는 사용자만의 커스텀 그래픽-엘레먼트 클래스를 디자인하고 사용할 것을 생각해야 합니다. 객체의 크기에 있어서 NSView 객체는 인스턴스 데이터를 아주 많이 데리고 다닙니다. 사용자의 커스텀 클래스 그래픽-엘레먼트 인스턴스는 NSView 객체가 이들을 그리는데 필요로하는 모든 정보를 담고 있으면서 동시에 훨씬 더 가벼울 수 있습니다.
[편집] Basic Subclass Design
모든 서브클래스는 조상이나 역할에 상관없이 제대로 디자인되었다면 특정한 성질을 공유합니다. 잘 디자인되지 못한 서브클래스는 쉽게 에러를 내고, 사용하기 어렵고, 확장하기 어렵고, 성능을 저하시킵니다. 제대로 디자인된 서브클래스는 반대입니다. 이 글에서는 효율적이고 강력하고 사용하거나 재사용하기 쉬운 서브클래스를 디자인하는데 도움을 줄 제안들을 제공합니다.
[편집] The Form of a Subclass Definition
Objective-C 클래스는 인터페이스와 구현으로 구성되어 있습니다. 클래스 정의의 이 두 부분은 일반적으로 각기 다른 파일로 기록됩니다. 인터페이스 파일은 (ANSI C 헤더 파일처럼) .h 확장자를 갖습니다. 구현 파일은 .m 확장자를 갖습니다. 확장자 앞부분의 파일 이름은 일반적으로 클래스의 이름과 같습니다. 따라서 Controller 라는 클래스의 경우의 파일은 다음과 같습니다.
Controller.h Controller.m
인터페이스 파일은 클래스의 퍼블릭 인터페이스를 구성하는 프로퍼티, 메소드, 함수 선언 목록을 포함합니다. 또한 인스턴스 변수, 상수, string globals, 다른 데이터 타입도 포함합니다. 디렉티브 @interface는 인터페이스의 필수적인 선언들 위에 나오며, @end 디렉티브는 선언을 종결짓습니다. @interface 디렉티브는 클래스의 이름과 상속받는 형태를 지정하므로 매우 중요합니다. @interface는 다음의 형태를 갖습니다.
@interface ClassName : Superclass
Listing 3-2에서 가상의 Controller 클래스에 어느 선언도 추가하기 전의 interface 파일의 예를 보여줍니다.
Listing 3-2 The basic structure of an interface file
#import <Foundation/Foundation.h>
@interface Controller : NSObject {
}
@end
클래스 인터페이스를 완성하기 위해서는, Figure 3-4에서 처럼 이 인터페이스 구조의 적절한 위치에 필요한 선언을 수행해야 합니다.
Figure 3-4 Where to put declarations in the interface file
“Instance Variables”와 “Functions, Constants, and Other C Types”에서 이들 선언에 대해서 더 자세히 알 수 있습니다.
클래스 인터페이스는 인터페이스에 등장하는 타입의 헤더 파일과 적절한 Cocoa 프레임웍을 임포트하는 것으로 시작합니다. #import 선행처리명령은 #include와 비슷하게 특정 헤더 파일을 통합시킨다는 측면은 비슷하지만 #import는 이전에 직접 혹은 간접적으로든 포함되지 않은 파일만을 포함하기 때문에 더욱 효율적입니다. < 와 > 에서는 프레임웍과 /다음에는 그 안에 헤더파일을 지정합니다. #import 의 문법은 다음의 형태를 취합니다.
- import <Framework/File.h>
프레임웍은 프레임웍이 원래 있어야 할 곳에 있어야합니다. Listing 3-2의 예에서, 클래스 인터페이스 파일은 Foundation.h 파일을 Foundation 프레임웍으로부터 임포트합니다. 이 경우처럼, Cocoa 용례에서 프레임웍의 이름과 동일한 헤더파일은 프레임웍의 모든 퍼블릭 인터페이스(와 다른 퍼블릭 헤더들)을 #import 명령으로 포함합니다. 덧붙여 말하자면, 정의하고 있는 클래스가 응용프로그램의 일부라면, Cocoa 통합 프레임웍의 Cocoa.h 파일만 임포트하면 됩니다.
프로젝트에 속하는 헤더 파일을 임포트 해야 한다면, < > 대신 따옴표를 이용하십시오.
#import "MyDataTypes.h"
클래스 구현 파일은 Listing 3-3에서 처럼 더 단순한 구조를 가지고 있습니다. 클래스 구현 파일은 클래스 인터페이스 파일을 임포팅하는 것으로 시작해야합니다.
Listing 3-3 The basic structure of an implementation file
#import "Controller.h" @implementation Controller @end
@implementation과 @end 디렉티브 사이에 모든 메소드와 프로퍼티 구현을 적어야 합니다. 클래스에 관련된 함수 구현은 파일 내 어디에든 있을수 있지만, 일반적으로 두 디렉티브 사이에 위치합니다. 프라이빗 타입(함수, 구조체 등)의 선언은 일반적으로 #import 명령과 @implementation 디렉티브 사이에 위치합니다.
[편집] Overriding Superclass Methods
커스텀 클래스는 특정 프로그램에 맞도록 슈퍼클래스의 행동을 수정합니다. 슈퍼클래스의 메소드를 오버라이드하는 것이 일반적인 방법입니다. 서브클래스를 디자인할 때, 오버라이드할 메소드를 정하고, 어떻게 재구현할지 생각하는 것이 필수적입니다.
비록 "메소드를 오버라이드 해야할 때"에서 일반적인 가이드라인을 제공하지만, 어느 슈퍼클래스 메소드를 오버라이드할지 찾기 위해서는 클래스를 살펴보아야 합니다. 클래스 헤더 파일과 문서를 잘 살펴보십시오. 또한 슈퍼클래스의 지정된 초기화 메소드를 살펴보고 서브클래스의 지정된 초기화 메소드에서 슈퍼클래스의 지정된 초기화 메소드를 불러줘야만 제대로 초기화 할수 있습니다. (상세한 내용은 "Object Creation"에서 찾을 수 있습니다.)
메소드를 지정했다면 (실용적인 측면에서) 인터페이스 파일에 메소드 선언을 복사하는 것으로 시작할 수 있습니다. 그리고 이 선언을 .m 파일에 메소드 구현 뼈대로 복사해 넣고 세미콜론과 중괄호를 삭제하면 됩니다.
"메소드를 오버라이드 해야할 때"에서 논의되었던 것처럼, Cocoa 프레임웍(과 사용자가 만들지 않은 코드)은 사용자가 오버라이드하는 프레임웍의 메소드를 종종 호출합니다. 어떤 경우에는 프레임웍으로 하여금 원래 메소드가 아니라 새로 오버라이드한 메소드를 호출해야한다는 것을 알도록 할 필요가 있습니다. Cocoa는 이를 위한 많은 방식을 제공합니다. Interface Builder에서는 Info 창에서 호환 가능한 프레임웍 클래스를 사용자의 클래스로 대치할 수 있도록 합니다. 커스텀 NSCell 클래스를 만들었다면, NSControl의 setCell:메소드를 사용하여 특정 컨트롤과 연결할 수 있습니다.
[편집] Instance Variables
커스텀 클래스를 만드는 이유에는 슈퍼클래스의 행동을 수정하는 것 뿐만 아니라 프로퍼티를 추가하기위해서 이기도 합니다. (여기서의 프로퍼티는 일반적인 의미로 서브클래스의 인스턴스의 애트리뷰트와 인스턴스가 가지고 있는 다른 객체와의 관계를 모두 지시합니다.) 가상의 Circle 클래스에서 형태에 색을 더하는 서브클래스를 만들고자 한다면, 서브클래스는 어떻개든 색상 애트리뷰트를 가져야합니다. 이를 달성하기 위해 (아마 NSColor 객체로 타입된) 색상 인스턴스 변수를 클래스 인터페이스에 추가할 것입니다. 커스텀 클래스의 인스턴스는 새로운 애트리뷰트를 영속적으로 보존하며 데이터의 조각으로 특성화합니다.
Objective-C에서 인스턴스 변수는 클래스 정의의 부분인 객체, 구조체, 다른 데이터 타입의 선언입니다. 만일 객체라면 선언은 (id를 사용해서) 동적인 타입일 수 있고 정적인 타입의 객체일 수도 있습니다. 다음 예제는 두 스타일을 보여주고 있습니다.
id delegate; NSColor *color;
일반적으로, 객체 인스턴스 변수를 동적인 타입으로 지정할때는 객체의 클래스 멤버쉽이 확정되지 않았거나 중요하지 않은 경우입니다. 인스턴스 변수가 되는 객체는 (델리게이트의 경우처럼) 부모 객체에 의해 이미 리테인되는 경우가 아니라면 생성되거나 복사되거나(copy) 명시적으로 리테인되어야만 합니다. 인스턴스가 언아카이브되면, 객체 인스턴스들을 initWithCoder:에서 디코드하고 할당하면서 리테인해야만 합니다. (객체 아카이빙과 언아카이빙에 대해 “Entry and Exit Points”에서 더 자세히 다루고 있습니다.)
인스턴스 변수의 이름짓기 규칙은 특수문자나 구문기호 없이 소문자로 쓰는 것입니다. 이름이 다수의 단어를 포함하고 있다면 한번에 묶어서 두번째 단어부터 단어의 첫자만 대문자로 씁니다. 다음의 예를 보십시오.
NSString *title; NSColor *backgroundColor; NSRange currentSelectedRange;
인스턴스 변수의 선언 앞에 IBOutlet 태그가 나오면, nib 파일에 아카이브되어있는 연결(connection)의 아웃렛을 지정합니다. 이 태그는 Interface Builder와 Xcode가 서로의 작업을 일치시킬수 있도록 해줍니다. 아웃렛이 Interface Builder의 nib 파일로부터 언아카이브되면, 연결이 자동으로 재설정됩니다.
인스턴스 변수는 클라이어트에 제공할 수 있는 객체의 애트리뷰트 외에 다른 것도 포함할수 있습니다. 때로 인스턴스 변수가 프라이빗 데이터를 쥐고 있어서 그것을 가지고 객체가 어떤 작업을 수행할 수 있습니다. 뒤에서 저장하기나 캐쉬 같은 것을 예로 들 수 있습니다. (데이터가 인스턴스마다 있는 것이 아니라 한 클래스의 인스턴스들이 공유한다면 인스턴스 변수 대신 글로벌 변수를 사용하십시오.)
서브클래스에 인스턴스 변수를 추가할 때에 다음 가이드라인을 참고하는 것이 좋습니다.
- 반드시 필요한 인스턴스 변수만을 추가하십시오. 인스턴스 변수를 추가하면 추가할수록, 인스턴스의 크기가 더 커집니다. 클래스에 인스턴스가 더 많이 만들어지면, 이 문제가 더욱 커집니다. 가능하다면 필요한 값을 이미 있는 인스턴스 변수로부터 구해내고 그 값을 담고있는 새로운 인스턴스 변수를 만드는 것은 피하십시오.
- 같은 이유로, 클래스의 인스턴스 데이터를 효율적으로 표시하도록 하십시오. 예를 들어, 특정한 플래그값을 인스턴스 변수로 지정하길 원한다면, 불리언 선언들보다 비트 필드를 이용하십시오. (그렇지만, 비트 필드는 아카이빙 처리를 해주어야 한다는 것을 주의하십시오) 키-밸류 짝으로 관련된 애트리뷰트들을 통합하기 위해 NSDictionary를 사용할 수도 있습니다. 이렇게 한다면 키들이 적절히 문서화되도록 하십시오.
- 인스턴스 변수에 적절한 범위를 설정해주십시오. @public은 캡슐화의 원칙을 위반하므로 절대 사용해서는 안됩니다. 여러분의 클래스에 서브클래스가 만들어지게 되거나 데이터에 충분한 접근권한이 필요하다면 인스턴스 변수는 @protected를 사용하는 것이 좋습니다. 그렇지 않으면, @private을 사용하여 제공하는 구현을 숨기는 것이 좋습니다. 구현의 숨김은 프레임웍으로 노출되어 있는 클래스나 애플리케이션 혹은 다른 프레임웍에 의해 사용되는 클래스의 경우에 특히 중요합니다. 이를 통해 모든 클라이언트를 재컴파일 할 필요 없이 클래스 구현에 수정을 가할 수 있게 합니다.
- 클래스의 속성이나 관계에 중요한 인스턴스 변수를 위해서는 프로퍼티를 선언하거나 액세서 메소드를 꼭 만드십시오. 액세서 메소드와 선언된 프로퍼티는 인스턴스 변수의 값을 지정하고 받는 것을 허용하여 캡슐화를 보존합니다. 이 주제에 대해 “Storing and Accessing Properties”에서 더 자세히 다루고 있습니다.
- 서브클래스가 퍼블릭이 되도록 한다면(다른 사람들이 이 클래스를 서브클래스할 것이라 예상된다면) 인스턴스 변수 리스트의 끝에 예약된 필드(타입은 주로 id)를 붙이십시오. 이 예약된 필드는 미래에 클래스에 다른 인스턴스 변수를 추가하게 될 경우 바이너리 호환성을 보장하도록 해줍니다. 퍼블릭 클래스를 준비하고자 할 때 필요한 상세한 정보를 “When the Class Is Public”에서 찾을 수 있습니다.
[편집] Entry and Exit Points
Cocoa 프레임웍은 객체의 삶의 여러 지점에서 그 객체에 메시지를 보냅니다. (그 자체가 객체인 클래스를 포함한) 거의 모든 객체는 런타임 생의 처음에 특정 메시지를 받고 파괴되기 직전에 또 메시지를 받습니다. 이들 메시지에 의해 불려진 메소드는 때에 맞는 작업을 수행하도록 해주는 "훅"(hook) 역할을 합니다. 이 메소드들은 다음과 같습니다(호출의 순서대로 나열되어 있습니다.)
1. initialize—이 클래스 메소드는 클래스 자신이나 인스턴스들이 다른 메시지를 받기 전에 클래스 자신을 초기화하도록 해줍니다. 슈퍼클래스는 서브클래스가 이 메시지를 받기 전에 먼저 이 메시지를 받습니다. 구식 아카이빙 메카니즘을 사용하는 클래스들은 그들의 클래스 버젼을 지정하는데에 initialize를 구현할 수 있습니다. initialize는 특정 서비스에 클래스를 등록하거나 모든 인스턴스가 함께 사용하는 글로벌 상태를 초기화하는데 사용할 수도 있습니다. 그러나 때로 이런 종류의 작업은 게으르게(이들이 처음으로 필요하게 될때) initialize 대신 인스턴스 메소드로 수행하는 것이 더 나을 수 있습니다.
2. init (혹은 다른 초기화 메소드)—슈퍼클래스의 지정된 초기화 메소드가 사용자의 클래스에 사용되기에 충분한 경우가 아니라면 init이나 다른 초기화 메소드를 구현하여 인스턴스의 상태를 초기화 시켜주어야만 합니다.
3. initWithCoder:—만일 클래스의 객체가 아카이브 될 것이라고 예상한다면 (모델 클래스 처럼) 필요에 따라 NSCoding 프로토콜을 받아들이고 initWithCoder: 와 encodeWithCoder: 두 메소드를 구현해야만 합니다. 이들 메소드에서 객체의 인스턴스 변수들을 아카이빙하기에 적절하게 인코딩하고 디코딩해야 합니다. 혹은 NSCoder의 디코딩 및 인코딩 메소드들을 사용하거나 NSKeyedArchiver나 NSKeyedUnarchiver의 키-아카이빙을 이용할 수도 있습니다. 객체가 명시적으로 생성되지 않고 언아카이브될때는, initWithCoder: 메소드가 초기화메소드 대신 호출됩니다. 이 메소드에서 인스턴스 변수의 값을 디코딩한 후, 그 값을 적절한 변수에 할당해주고 필요에따라 리테인 혹은 카피합니다. 이 주제에 대해 더 자세한 설명은 Archives and Serializations Programming Guide for Cocoa에서 찾을 수 있습니다.
4. awakeFromNib—응용프로그램이 nib 파일을 로드할 때, 아카이브의 모든 객체가 로드되고 초기화된 후, awakeFromNib 메시지에 반응할 수 있는 모든 객체에 이 메시지가 보내집니다. 객체가 awakeFromNib 메시지를 받을때는, 모든 아웃렛 인스턴스 변수가 세팅된 상태입니다. 전형적으로 nib파일을 소유하고 있는 객체(File's Owner)가 awakeFromNib을 구현하여 아웃렛, 타겟-액션 연결의 설정을 요구하는 초기화과정을 수행합니다.
5. encodeWithCoder:—클래스의 인스턴스가 아카이브되어야 한다면 이 메소드를 구현하십시오. 객체의 파괴 바로 전에 이 메소드가 호출됩니다. 위의 initWithCoder: 설명을 참고하십시오.
6. dealloc 혹은 finalize—메모리가 관리되는 프로그램에서 dealloc 메소드를 이용해 인스턴스 변수를 릴리즈하고 클래스의 인스턴스에 의해 사용된 다른 메모리를 해제하십시오. 이 메소드가 리턴된 후에 그 인스턴스는 곧 파괴됩니다. 가비지-컬렉션을 사용하는 코드에서는, finalize 메소드를 대신 사용해야 할 수 있습니다. 더 많은 정보는 “The dealloc and finalize Methods”에서 찾을 수 있습니다.
프로그램의 (NSApp으로 지정된)글로벌 애플리케이션 객체 역시 애플리케이션의 생의 시작과 끝에서 (NSApp의 델리게이트이며 적절한 메소드를 구현하고 있는)객체에 메시지를 보냅니다. 애플리케이션이 구동된 직후에 NSApp은 델리게이트에 applicationWillFinishLaunching: 메소드와 applicationDidFinishLaunching: 메소드를 보냅니다. (전자는 더블 클릭된 문서가 열리기 전에, 후자는 문서가 열린 직후에 보내집니다.) 이들 두 메소드에서 델리게이트가 애플리케이션이 런타임에 존재하는 이른 시점에 한번 발생해야하는 글로벌 애플리케이션 로직을 지정할 수 있습니다. 응용프로그램이 종료하기 직전에 NSApp은 applicationWillTerminate: 메소드를 델리게이트에 보내고, 델리게이트는 문서와 애플리케이션 상태를 저장하는 것으로 프로그램 종료를 부드럽게 수행할 수 있습니다.
Cocoa 프레임웍은 윈도우 닫기부터 프로그램 활성화/비활성화에 이르는 다양한 이벤트를 위한 많은 훅을 제공합니다. 이들은 주로 델리게이션 메시지로 구현되어 있으며 사용자의 객체가 프레임웍 객체의 델리게이트가 되고 필요한 메소드를 구현할 것을 요구합니다. 때로 이들 훅은 노티피케이션일 수 있습니다. (델리게이션과 노티피케이션에 대해 더 알고싶으시다면 “Communicating With Objects”를 살펴보십시오.)
[편집] Initialize or Decode?
클래스의 객체가 아카이브/언아카이브되길 원한다면 NSCoding 프로토콜을 따라야만 합니다. 객체를 인코드하는 메소드(encodeWithCoder:)와 디코드하는 메소드(initWithCoder:)를 구현해야만 합니다. 초기화 메소드 대신 initWithCoder:메소드가 아카이브로부터 객체가 생성될때 초기화를 위해 호출되게 됩니다.
클래스의 초기화 메소드와 initWithCoder:메소드가 거의 동일한 일을 하게 될 수 있기 때문에 공통의 작업은 두 메소드가 모두 호출하여 사용하는 헬퍼 메소드를 통해 처리할 수 있습니다. 예를 들어, 셋업 루틴의 일부로 객체가 드래그 타입과 드래깅 소스를 지정해준다면, Listing 3-4에서 보이는 것처럼 구현할 수 있습니다.
Listing 3-4 Initialization helper method
-(id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setTitleColor:[NSColor lightGrayColor]];
[self registerForDragging];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aCoder {
self = [super initWithCoder:aCoder];
titleColor = [[aCoder decodeObject] copy];
[self registerForDragging];
return self;
}
- (void)registerForDragging {
[theView registerForDraggedTypes:
[NSArray arrayWithObjects:DragDropSimplePboardType, NSStringPboardType,
NSFilenamesPboardType, nil]];
[theView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES];
}
클래스 초기화 메소드와 initWithCoder:의 병행적인 역할의 예외는 프레임웍 클래스의 커스텀 서브클래스의 인스턴스가 Interface Builder의 팔렛트에 나타날 때 입니다. 프로젝트에서 클래스를 정의하는 일반적인 절차는, Interface Builder의 팔렛트로부터 객체를 인터페이스에 드래그하고 Interface Builder 의 Info 윈도우의 Custom Class 항목을 사용해 그 객체를 사용자의 커스텀 서브클래스와 연결을 지어주는 것입니다. 그러나 이렇게 하면 서브클래스의 initWithCoder: 메소드가 언아카이빙 단계에서 불려지지 않게 됩니다. 대신 init 메시지가 보내집니다. 이 커스텀 객체에서 특별한 셋업 작업을 하고자 한다면 모든 nib 파일이 언아카이브 된 후에 불려지는 awakeFromNib 메소드에서 수행해야 합니다.
[편집] Storing and Accessing Properties
Cocoa에서 "프로퍼티"라고 하는 단어는 일반적인 의미와 언어에 특징적인 의미를 동시에 가지고 있습니다. 일반적인 의미는 객체 모델링의 디자인 패턴으로부터 옵니다. 이 언어에 특징적인 의미는 선언된 프로퍼티의 언어 특성과 관련되어있습니다.
"Object Modeling"에서 설명된 대로, 프로퍼티는 객체(객체 모델링 패턴에서 개체(Entity)라고 부릅니다)의 특징들을 정의하고 있으며, 객체의 캡슐화된 데이터의 일부를 이룹니다. 프로퍼티는 타이틀과 위치같은 애트리뷰트이거나 관계일 수 있습니다. 관계는 -대일 혹은 -대다수일 수 있으며 아웃렛, 델리게이트, 다른 객체들의 콜렉션과 같이 많은 형태를 취할 수 있습니다. 반드시 그런 것은 아니지만 프로퍼티는 보통 인스턴스 변수로 저장됩니다.
클래스를 잘 디자인하기 위한 전반적인 전략은 캡슐화를 강화하는 것입니다. 클라이언트 객체가 저장된 프로퍼티에 직접 접근할 수 있어서는 안되며, 클래스의 인터페이스를 통해 접근할 수 있어야 합니다. 프로퍼티에 접근할 수 있도록 하는 메소드는 액세서(accessor) 메소드라고 불립니다. 액세서 메소드 (혹은 단순히 "액세서")는 객체의 프로퍼티의 값을 얻어오거나 지정합니다. 이들은 이런 프로퍼티에 접근하는 문과 같은 역할을 하며 접근을 관리하여 객체의 인스턴스 데이터를 캡슐화하도록 합니다. 값을 받아오는 액세서는 일반적으로 "게터" 메소드라고 불리며 값을 지정해주는 메소드는 "세터" 메소드라고 불립니다.
(Objective-C 2.0에 소개된) 선언된 프로퍼티(declared properties) 기능은 프로퍼티에 접근하는 방법에 약간의 변경사항을 제공합니다. 선언된 프로퍼티는 클래스의 프로퍼티의 게터와 세터메소드를 선언하는 것을 문법적으로 좀 더 짧게 만들어줍니다. 구현 블락에서 컴파일러가 이들 메소드를 작성해주도록 요청할 수 있습니다. "Declared Properties"에서 요약된 것처럼, 클래스 정의의 @interface 블락에서 @property 디렉티브를 이용해 프로퍼티를 선언합니다. 이 선언은 프로퍼티의 이름과 타입을 지정하고 컴파일러가 액세서를 어떻게 구현할지 알려주는 내용을 추가할 수 있습니다.
클래스를 디자인할때 클래스의 프로퍼티를 저장하고 접근하는 방식에 영향을 미칠 수 있는 요소들이 몇개 있습니다. 선언된 프로퍼티 기능을 이용해 컴파일러가 액세서 메소드를 만들어내도록 할 수 있습니다. 또 직접 클래스의 액세서 메소드를 구현할 수 있습니다. 선언된 프로퍼티를 이용할 때에도 직접 액세서 메소드를 구현할 수 있습니다. 다른 중요한 요인은 코드가 가비지-컬렉션 환경에서 작동하는가의 여부입니다. 만일 가비지 컬렉터를 사용한다면, 액세서 메소드의 구현은 메모리를 관리하는 코드보다 훨씬 단순해집니다. 다음 섹션에서 프로퍼티를 저장하고 접근하는 다양한 선택사항들에 대해 설명하고 있습니다.
[편집] Taking Advantage of Declared Properties
어쩔수 없는 다른 이유가 있는 것이 아니라면, 새로운 프로젝트에서 직접 액세서 메소드를 작성하기보다 Objective-C 2.0의 선언된 프로퍼티 기능을 사용하는 것을 추천합니다. 먼저 선언된 프로퍼티는 지루한 액세서 메소드 구현으로부터 해방시켜줍니다. 둘째로, 컴파일러가 액세서 메소드를 생성하기 때문에 프로그래밍 에러의 가능성을 줄이고, 클래스 구현에서의 행동이 좀더 일관성있게 됩니다. 마지막으로, 선언된 프로퍼티의 선언형 스타일이 다른 개발자들에게 여러분의 의도를 좀 더 분명히 전달하도록 할 것입니다.
@property로 몇몇 프로퍼티를 선언했지만, 그들을 위한 액세서 메소드를 직접 구현해야할 필요가 있을 수 있습니다. 전형적인 이유로는 메소드가 값을 받아오거나 지정하는 것 외에 더 많은 것을 수행하길 원하는 경우가 있습니다. 예를들어, 애트리뷰트를 위한 리소스가(큰 이미지 파일이) 파일시스템으로부터 로드되어야 하는데 성능의 이유로 게터 메소드에서 이 리소스를 게으르게(이 리소스가 처음으로 요청되어질 때) 로드하길 원할 수 있습니다. 액세서 메소드를 이런 이유로 구현한다면 "Implementing Accessor Methods”에서 팁과 가이드라인을 찾을 수 있습니다. @dynamic 디렉티브를 클래스의 @implementation 블락에서 @dynamic 디렉티브를 프로퍼티 지정하거나, @synthesize 디렉티브를 프로퍼티에 사용하지 않는 방법(기본값이 @dynamic입니다)으로 컴파일러가 액세서 메소드를 생성하지 않도록 할 수 있습니다. 액세서 메소드가 구현되어 있고 @synthesize 디렉티브가 지정되어있지 않다면, 컴파일러는 액세서 메소드를 생성하지 않습니다.
프로그램에서 가비지 컬렉션이 해제되어 있다면, 프로퍼티 선언을 꾸며주는 애트리뷰트는 매우 중요합니다. 기본적으로(할당 애트리뷰트) 생성된 세터 메소드는 가비지 컬렉션 코드에 적합한 단순 할당만을 수행합니다. 그러나 메모리 관리 코드에서 객체값을 갖는 프로퍼티의 단순 할당은 적합하지 않습니다. 세터 메소드에서 할당된 객체를 리테인하거나 카피해야 합니다. 컴파일러에게 리테인 혹은 카피로 프로퍼티 선언을 수식해줘서 컴파일러에게 이것을 하라고 말해주어야 합니다. 가비지 컬렉션이 활성화되었을지라도 객체 프로퍼티의 클래스가 NSCopying 프로토콜을 따른다면 항상 copy 애트리뷰트를 지정해주어야 합니다.
다음 예제는 @interface 블락에서 (한정해주는 애트리뷰트들을 이용하여)프로퍼티 선언과 @implementation 블락에서 @synthesize 와 @dynamic 디렉티브를 이용하여 만들어 낼 수 있는 비헤이비어를 보여주고 있습니다. 다음 코드는 컴파일러로 하여금 선언된 name과 accountID 프로퍼티를 위한 액세서 메소드를 생성하도록 합니다. accountID 프로퍼티가 읽기전용이기 때문에 컴파일러는 게터 메소드만 생성합니다. 이 경우에는 가비지 컬렉션이 활성화 되었다고 가정하고 있습니다.
@interface MyClass : NSObject {
NSString *name;
NSNumber *accountID;
}
@property (copy) NSString *name;
@property (readonly) NSNumber *accountID;
// ...
@end
@implementation MyClass
@synthesize name, accountID;
// ...
@end
다음 코드 목록은 선언된 프로퍼티의 다른 양상을 보여줍니다. 가비지 컬렉션이 활성화되지 않았다고 가정하고, currentHost 프로퍼티의 선언이 리테인 애트리뷰트를 가지고 있어서 생성된 게터 메소드에서 새 값을 리테인하도록 컴파일러에게 지시합니다. 게다가, 숨겨진 프로퍼티의 선언에서는 컴파일러로 하여금 isHidden이라는 이름의 게터 메소드를 생성하도록 지시합니다. (이 프로퍼티의 값이 객체값이 아니므로 세터 메소드의 경우는 단순한 할당이면 충분합니다.)
@interface MyClass : NSObject {
NSHost *currentHost;
Boolean *hidden;
}
@property (retain, nonatomic) NSHost *currentHost;
@property (getter=isHidden, nonatomic) Boolean *hidden;
// ...
@end
@implementation MyClass
@synthesize name, hidden;
// ...
@end
다음 예제의 프로퍼티 선언은 컴파일러에게 previewImage 프로퍼티가 읽기전용이기에 세터 메소드를 찾기를 기대하지 않는다는 것을 말해줍니다. @dynamic 디렉티브를 @implementation 블락에서 사용하여 컴파일러로 하여금 게터 메소드를 생성하지 않도록 지시합니다, 그 후 그 메소드의 구현을 제공하고 있습니다.
@interface MyClass : NSObject {
NSImage *previewImage;
}
@property (readonly) NSImage *previewImage;
// ...
@end
@implementation MyClass
@dynamic previewImage;
// ...
- (NSImage *)previewImage {
if (previewImage == nil) {
// lazily load image and assign to ivar
}
return previewImage;
}
// ...
@end
[편집] Implementing Accessor Methods
용어 규칙의 이유로 —이것을 지키면 클래스가 키-밸류 코딩을 준수하게 되므로 ("Key-Value Mechanisms”를 보십시오)— 액세서 메소드의이름은 특정한 형태여야만 합니다. 인스턴스 변수의 값을 반환하는 메소드(게터라고 부릅니다)는 인스턴스 변수의 이름을 메소드의 이름으로 사용합니다. 인스턴스 변수의 값을 설정하는 메소드(세터)는 인스턴스 변수의 이름 앞에 set이 붙고 인스턴스변수 이름의 첫자는 대문자로 이어서씁니다. 만일 "color"라고하는 인스턴스 변수가 있다면, 게터와 세터메소드의 선언은 다음과 같습니다.
- (NSColor *)color; - (void)setColor:(NSColor *)aColor;
불리언 애트리뷰트로 게터 메소드에 약간의 변형을 가할 수 있습니다. 이경우의 신택스는 isAttribute의 형태를 갖게 됩니다. 예를 들어 hidden 이라는 이름을 갖는 불리언 인스턴스 변수는 isHidden 게터 메소드와 setHidden: 세터 메소드를 갖습니다.
만일 인스턴스 변수가 int나 float같은 스칼라 C 타입이라면, 액세서 메소드의 구현은 매우 단순합니다. 인스턴스 변수가 float 타입의 currentRate이라는 이름을 갖고 있다면, Listing 3-5에서 액세서 메소드를 어떻게 구현하는지를 보여주고 있습니다.
Listing 3-5 Implementing accessors for a scalar instance variable
- (float)currentRate {
return currentRate;
}
- (void)setCurrentRate:(float)newRate {
currentRate = newRate;
}
가비지 컬렉션이 활성화 되었다면 객체 밸류를 갖는 프로퍼티의 세터 액세서 메소드의 구현도 단순한 할당만 하면 됩니다. Listing 3-6에서 이런 메소드를 어떻게 구현하는지 보여줍니다.
Listing 3-6 Implementing accessors for an object instance variable (garbage collection enabled)
- (NSString *)jobTitle {
return jobTitle;
}
- (void)setJobTitle:(NSString *)newTitle {
jobTitle = newTitle;
}
인스턴스 변수가 객체를 쥐고 가비지 컬렉션이 비활성화 되어있는 경우라면 상황이 좀 달라집니다 인스턴스 변수이기 때문에 이들 객체들은 영속적이어야 하고, 할당되면 생성되고 복사되고 리테인되어야 합니다. 세터 액세서가 인스턴스 변수의 값을 바꾼다면, 영속성을 유지할 뿐만 아니라 이전 값을 적절히 제거해줄 책임도 있습니다. 게터 엑세서는 요청하는 객체에 인스턴스 변수의 값을 제공합니다. 이 두 종류의 액세서 모두 Cocoa 객체 소유의 정책으로부터 아래의 두 사항을 가정하고 메모리 관리에 영향을 미칩니다.
- (게터 엑세서와 같은) 메소드로부터 반환된 객체는 호출한 객체의 범위 내에서 유효합니다. 다른 말로 하면, 객체가 그 범위 내에서는 릴리즈되거나 값이 변하지 않을 것이 보장됩니다.(물론 따로 언급한 사항이 없는 경우에 그렇습니다.)
- 호출하는 객체가 엑세서 같은 메소드에서 객체를 받으면, 먼저 명시적으로 리테인하거나 카피하지 않았다면 릴리즈해서는 안됩니다.
이 두 가정을 염두에 두고, NSString 인스턴스 변수인 title을 위한 게터와 세터 액세서 메소드의 두 개의 구현을 살펴봅시다. Listing 3-7은 첫번째 구현을 보여줍니다.
Listing 3-7 Implementing accessors for an object instance variable—good technique
- (NSString *)title {
return title;
}
- (void)setTitle:(NSString *)newTitle {
if (title != newTitle) {
[title autorelease];
title = [newTitle copy];
}
}
게터 액세서는 단순히 인스턴스 변수의 레퍼런스만 반환한다는 것을 주의하십시오. 반면에 세터 액세서는 좀 더 많은 일을 처리합니다. 인스턴스 변수에 들어갈 값과 현재 값이 동일하지 않다는 것을 확인한 후, 새로운 값을 인스턴스 변수에 복사해넣기 전에 현재값을 오토릴리즈합니다. (릴리즈(release)를 보내는 것보다 오토릴리즈를 객체에 보내는 것이 더 쓰레드에 안전합니다.) 그러나, 이런 접근법에 잠재적인 위협이 여전히 남아있습니다. 클라이언트가 게터 액세서에서 반환된 객체를 사용하는 동안 세터 액세서가 이전의 NSString 객체를 오토릴리즈하고, 곧 그 객체가 릴리즈되고 파괴된다면 어떻게 되겠습니까? 클라이언트 객체가 가지고 있는 그 인스턴스 변수의 레퍼런스는 더이상 유효하지 않을 것입니다.
Listing 3-8은 게터 메소드에서 인스턴스 변수를 리테인한 후 오토릴리즈하는 방식으로 이 문제를 해결하는 액세서 메소드의 다른 구현을 보여주고 있습니다.
Listing 3-8 Implementing accessors for an object instance variable-better technique
- (NSString *)title {
return [[title retain] autorelease];
}
- (void)setTitle:(NSString *)newTitle {
if (title != newTitle) {
[title release];
title = [newTitle copy];
}
}
위의 두 세터 메소드 예제(Listing 3-5와 3-7) 모두, 새로운 NSString 인스턴스 변수를 리테인하지 않고 카피하고 있습니다. 왜 리테인을 사용하지 않을까요? 일반적인 규칙은 이렇습니다. 인스턴스 변수에 할당되는 객체가 값(value) 객체이면(스트링, 날짜, 숫자, 혹은 통합 레코드와 같은 애트리뷰트를 표시하는 객체인 경우를 말합니다) 카피해야만 합니다. 애트리뷰트 값을 보존시키는데 목적이있으므로 값이 변경되지 않도록 해야합니다. 다른 말로 하면, 사용자가 자신이 사용할 객체의 사본을 가지고 있어야 한다는 것입니다.
그러나, 객체가 NSView나 NSWindow와 같은 엔티티 객체로 저장되고 접근되어진다면, 리테인해야만 합니다. 엔티티 객체는 더 집합적이고 관계적이어서 그들을 카피하는 것은 많은 손실을 가져올 수 있습니다. 객체가 값 객체인지 엔티티 객체인지 구별하는 방법은 객체의 값에 관심이 있는지 아니면 객체 자체에 관심이 있는지를 살펴보는 것입니다. 만일 값에 관심이 있다면, 그 객체는 값 객체일 것이고 이 객체는 카피해야 합니다. (당연히 NSCopying 프로토콜을 준수해야 합니다. )
세터 메소드에서 인스턴스 변수를 리테인할지, 카피할지를 결정하는 다른 방법은 인스턴스 변수가 애트리뷰트인지 아니면 관계[Relationship]인지를 구별하는 것입니다. 모델 객체의 경우, 애플리케이션의 데이터를 표시하기에 더욱 그러합니다. 애트리뷰트는 밸류 객체와 기본적으로 동일합니다. 이들은 색상(NSColor 객체)과 제목(NSString 객체)과 같이 그 객체를 캡슐화하는 객체의 특성을 정의하는 객체들입니다. 반면 관계는 그저 하나 혹은 여러 다른 객체들과의 관계(혹은 레퍼런스)입니다. 일반적으로 세터 메소드에서 애트리뷰트의 값을 카피하고, 관계는 리테인합니다. 그러나 관계는 카디날리티를 갖습니다. 일대 일일 수도 있고 일대다 일수도 있습니다. 일대다의 관계는 보통 NSArray 인스턴스나 NSSet 인스턴스와 같은 콜렉션 객체로 표시되며, 세터 메소드가 단순이 인스턴스 변수를 리테인하는 것 이상을 수행하도록 요구할 수 있습니다. 상세한 내용은 Model Object Implementation Guide에서 찾을 수 있습니다. 애트리뷰트나 관계로서 객체의 속성에 대해서는 “Key-Value Mechanisms.”에서 더 자세히 다루고 있습니다.
클래스의 세터 액세서가 Listing 3-5나 3-7과 같이 구현되어 있다면, 인스턴스 변수를 클래스의 dealloc 메소드에서 할당을 해제하고자 할때, 단순히 적절한 세터 메소드를 nil을 인수로 하여 불러주기만 하면 됩니다.
[편집] Key-Value Mechanisms
키-밸류 라는 이름으로 시작하는 키-밸류 바인딩, 키-밸류 코딩, 키-밸류 옵저빙 기법들은 Cocoa의 기본적인 부분을 구성합니다. 이들은 객체 사이의 통신과 동기화를 자동으로 해주는 바인딩과 같은 Cocoa 기술의 필수적인 요소입니다. 또한 AppleScript 명령에 반응하도록 하여 응용프로그램을 스크립팅 가능하게 만드는 인프라스트럭쳐의 기본적인 기반을 제공합니다. 키-밸류 코딩과 키-밸류 옵저빙은 커스텀 서브클래스를 디자인할때 특히 중요하게 고려해야할 사항입니다.
키-밸류라는 용어는 프로퍼티의 이름이 그 값을 얻어오는 키로 사용하는 기술을 의미합니다. 이 용어는 객체 모델링 패턴에서 사용되는 용어중의 일부이며 , 이 용어들은 “Storing and Accessing Properties”에서 간단히 논의되고 있습니다. 객체 모델링은 관계적인 데이터베이스를 묘사하는데 사용되는 엔티티-관계 모델링으로부터 파생됩니다. 객체 모델링에서, 객체 - 특히, MVC 패턴의 데이터를 갖는 모델 객체 -는 보통 인스턴스 변수의 형태를 갖습니다. 프로퍼티는 색상, 이름과 같이 애트리뷰트이거나 하나 혹은 다수의 객체를 가리키는 레퍼런스 일 수 있습니다. 이들 레퍼런스는 관계로 알려져 있으며, 관계는 일대일 혹은 일대다 일 수 있습니다. 프로그램의 객체 네트웍은 서로간의 관계를 통해 객체 그래프를 형성합니다. 객체 모델링에서는 마침표(.)로 구분되는 키들의 스트링인 키-경로(key-path)를 사용하여 객체 그래프에서의 관계를 탐색하며 객체의 프로퍼티에 접근할 수 있습니다.
키-밸류 바인딩, 키-밸류 코딩, 키-밸류 옵저빙은 이 이동을 가능하게 합니다.
- 키-밸류 바인딩(KVB)는 객체간의 바인딩을 생성하고, 이들 바인딩을 제거하고 알립니다. 비공식적인 프로토콜이 사용가능하도록 만들어 줍니다. 프로퍼티의 바인딩은 반드시 객체와 그 프로퍼티의 키 경로를 명시해야합니다.
- 키-밸류 코딩(KVC)은 NSKeyValueCoding 비공식 프로토콜의 구현으로, 객체의 액세서 메소드를 직접 호출할 필요 없이 키를 이용해 객체의 프로퍼티의 값을 얻어오고 지정해줄 수 있게 합니다. (Cocoa는 이 프로토콜의 기본 구현을 제공합니다.) 키는 일반적으로 인스턴스 변수의 이름이나, 접근되는 객체의 액세서 메소드의 이름을 반영합니다.
- 키-밸류 옵저빙(KVO)은 NSKeyValueObserving 비공식 프로토콜을 구현으로, 객체가 그들을 다른 객체의 옵저버로 등록하도록 합니다. 관찰되는 객체는 프로퍼티에 변화가 발생할 때, 그들의 옵저버에게 직접 통지합니다. Cocoa는 KVO에 따르는 객체의 각 프로퍼티에 해당하는 자동 옵저버 통지를 구현하고 있습니다.
서브클래스의 각 프로퍼티가 키-밸류 코딩의 요구사항을 따르도록 하려면, 다음을 따르십시오:
- key 라고 이름지어진 애트리뷰트나 일대일 관계를 위해 액세서 메소드를 key(게터)라는 이름과 setK



