CLOSE SEARCH

Cocoa Fundamentals Guide – Adding Behavior to a Cocoa Program

Objective-C를 사용해서 코코아 프로그램을 개발할 때 모든 것을 스스로 개발하지는 않을 것이다. Apple과 다른 다양한 개발사들이 개발하여 Objective-C 프레임워크 패키지로 제공하는 클래스들을 사용할 것이다. 이 프레임워크들은 프로그램을 구성하는데 사용되는 클래스 집합을 제공한다.

이번 장은 코코아 프레임워크를 통해서 Objective-C 프로그램을 작성하는 것에 대해서 설명한다. 또한 프레임워크 클래스를 서브클래싱하기 위해서 알고 있어야 하는 기본적인 정보를 제공한다.

Starting Up

Objective-C 클래스와 메소드로 구성된 프레임워크를 사용하는 것은 C 함수로 구성된 라이브러리를 사용하는 것과 다르다. C 라이브러리의 경우, 작성하고자 하는 프로그램에 따라서 어떤 함수를 사용해야 하고 언제 이들을 사용해야 하는지에 대해 다양하게 선택할 수 있다. 반면, 프레임워크는 프로그램 또는 프로그램에서 다루고자 하는 문제 공간(problem space)에 대한 설계를 필요로한다. 절차적 프로그램에서는 프로그램이 작업을 완료할 수 있도록 필요한 시점에 라이브러리 함수를 호출한다. 객체지향 프레임워크를 사용하는 것은 프로그램의 작업을 수행하기 위해 프레임워크의 메소드를 호출해야 한다는 점에서는 비슷하다. 하지만, 프레임워크를 커스터마이징하고 프레임워크가 적절한 시점에 호출하게 될 메소드들을 구현하여 당신의 필요에 맞게 적용하는 것도 필요하다. 이러한 메소드들을 훅(hook)이라고 한다. 훅은 작성한 코드를 프레임워크 구조로 통합하고 프로그램의 특징이 되는 동작들을 보강한다. 어떤 의미에서는 프로그램과 라이브러리의 일반적인 역할이 뒤바뀐 것이다. 라이브러리 코드를 프로그램으로 결합시키는 것이 아니라 프로그램의 코드를 프레임워크로 결합시키는 것이다.

코코아 프로그램이 실행될 때 어떤 작업들이 수행되는지를 살펴보면 프레임워크와 커스텀 코드의 관계에 관한 몇가지 통찰를 얻을 수 있다.

 

What Happens in the main Function

Objective-C 프로그램은 C 프로그램과 마찬가지로 main 함수에서 실행을 시작한다. 복잡한 Objective-C 프로그램에서 main 함수의 역할은 꽤 단순하다. 이 함수는 두가지 단계로 구성되어 있다.

[list type=”star”]

  • 핵심 객체들을 생성한다.
  • 이러한 객체로 프로그램 제어를 넘긴다.

[/list]

핵심 그룹(core group)에 있는 객체들은 프로그램을 실행하는데 필요한 다른 객체들을 생성할 수도 있고, 이렇게 생성된 객체도 다른 객체들을 생성할 수 있다. 때때로 프로그램은 필요에 따라 클래스를 로드하고, 인스턴스를 아카이브로부터 복구하고(unarchive), 원격 객체로 접근하거나 다른 리소스를 검색하기도 한다. 하지만, 시작 시점에 요구되는 모든 것들은 프로그램의 초기 작업을 처리하기에 충분한 구조이다. main 함수는 이러한 초기 구조들을 적소에 배치하고 작업을 시작할 준비가 되도록 한다.

일반적으로 핵심 객체 중 하나는 프로그램을 감독하거나 입력을 제어하는 역할을 담당한다. main 함수는 핵심 구조가 준비되면 이러한 감독 객체를 작동시킨다. 프로그램이 명령줄 도구 또는 백그라운드 서버라면 이들의 역할은 명령줄 파라미터를 전달하거나 원격 연결을 여는 것만큼 단순할 것이다. 하지만, 어플리케이션과 같이 좀 더 일반적인 형태의 코코아 프로그램에서는 더 많은 역할을 수행한다.

어플리케이션에서 main 함수가 설정하는 핵심 객체들은 반드시 사용자 인터페이스를 출력하는 몇가지 객체를 포함해야 한다. 인터페이스는 어플리케이션이 실행될 때 반드시 스크린에 표시되어야 한다. 어플리케이션은 사용자 인터페이스가 화면에 표시된 후 외부 이벤트에 의해 동작하며, 이 중 가장 중요한 것은 사용자 이벤트이다. 이러한 이벤트는 사용자의 동작에 대한 다양한 정보와 함께 어플리케이션으로 전달된다.

어플리케이션은 이벤트를 전달받아 분석한 후, 이벤트를 처리하고 다음 이벤트를 기다린다. 이러한 이벤트는 사용자 또는 타이머와 같은 다른 소스로부터 이벤트가 발생하는 동안 하나씩 순서대로 전달된다. 어플리케이션의 시작시점부터 종료시점까지 수행하는 대부분의 작업은 이벤트 형식의 사용자 액션을 통해 이루어진다.

이벤트 수신과 응답을 위해 main event loop를 사용한다. main이라고 부르는 이유는 어플리케이션에서 짧은 시간동안 동작하는 다른 하위 이벤트 루프도 만들 수 있기 때문이다. 이벤트 루프는 본질적으로 하나 이상의 입력소스를 가지는 런루프(run loop)이다. 핵심 그룹에 있는 하나의 객체는 메인 이벤트 루프를 실행하는 역할을 담당한다. 코코아 어플리케이션에서 이러한 협력 객체(coordinating object)는 전역 어플리케이션 객체이다. OS X에서는 NSApplication 객체의 인스턴스이고, iOS에서는 UIApplication 객체의 인스턴스이다. 그림 3-1은 OS X에서 실행되는 코코아 어플리케이션의 메인 이벤트 루프 동작방식을 보여준다.

Figure 3-1  The main event loop in OS X

Figure 3-1 The main event loop in OS X

대부분의 코코아 어플리케이션에 있는 main 함수는 매우 단순하다. OS X에서는 Listing 3-1과 같이 단 하나의 함수 호출로 구성되어 있다. NSApplicationMain 함수는 어플리케이션 객체를 생성하고, 자동해제 풀을 생성한 다음 메인 nib 파일에서 초기 사용자 인터페이스를 읽어온다. 그런 다음 어플리케이션을 실행하고 메인 이벤트 루프에서 전달된 이벤트를 처리한다.

iOS 어플리케이션의 main 함수는 위의 함수와 유사한 이름의 UIApplicationMain 함수를 호출한다.

 

Using a Cocoa Framework

라이브러리 함수는 약간의 제약이 존재하지만 필요한 시점에 언제나 호출할 수 있다. 반면, 객체지향 라이브러리나 프레임워크에 있는 메소드들은 클래스 정의와 밀접하게 연관되어 있기 때문에, 이러한 정의에 접근할 수 있는 객체를 직접 생성하거나 코드의 다른 위치에서 얻어온 경우에만 메소드를 호출할 수 있다. 또한, 객체는 대부분의 프로그램에서 작업을 수행하기 위해 적어도 하나 이상의 다른 객체와 반드시 연결되어야 한다. 클래스는 프로그램의 구성요소를 정의하며, 클래스의 기능을 사용하기 위해서는 어플리케이션으로 통합시켜야 한다.

하지만, 일부 프레임워크 클래스들은 함수 라이브러리와 매우 유사하게 (다른 객체와의 협력없이) 동작하는 인스턴스를 생성한다. 객체를 생성하여 초기화 한 다음, 특정 작업을 수행하기 위해 메시지를 보내거나 어플리케이션의 대기열에 추가할 수 있다. 예를 들어, NSFileManager 클래스를 통해 파일 이동, 복사, 삭제 등과 같은 다양한 파일시스템 작업을 수행할 수 있다. 경고창을 표시해야 한다면 NSAlert 클래스 또는 UIAlertView 클래스의 인스턴스를 생성한 후 적절한 메시지를 보내면 된다.

일반적으로 코코아와 같은 환경은 프로그램 개발에 필요한 기능을 제공하는 개별 클래스를 모아놓은 것 그 이상이다. 이들은 문제 공간(problem space)을 구성하고 이에 대한 통합된 해결책을 제공하는 객체지향 프레임워크와 클래스의 모음으로 구성되어 있다. 프레임워크는 함수 라이브러리와 같이 필요할 때마다 사용할 수 있는 개별 서비스를 제공하기 보다는 코드에 반드시 적용되어야 하는 전체 프로그램 구조나 모델을 설계하고 구현한다. 이러한 프로그램 모델은 일반적인 것이므로 특수한 기능이 필요한 경우에는 수정하거나 확장할 수 있다. 라이브러리 함수를 사용하는 프로그램을 설계하는 것이 아니라 프레임워크에서 제공하는 설계에 직접 작성한 코드를 통합하는 것이다.

프레임워크를 사용하기 위해서는 반드시 프레임워크에 정의되어 있는 프로그램 모델을 따르고, 필요에 따라 여기에 포함된 다수의 클래스들을 커스터마이징 해야 한다. 이러한 클래스들은 서로 의존적이며 그룹을 이루고 있다. 언뜻 보기에는 프레임워크의 프로그램 모델을 코드에 적용하는 것이 제한적으로 보일 수도 있다. 그러나 실제로는 완전히 정반대이다. 프레임워크는 자신의 일반적인 동작들을 변경하고 확장할 수 있는 다양한 방법을 제공한다. 이러한 모든 것들이 동일한 프로그램 모델을 기반으로 하기 때문에 모든 코코아 프로그램이 기본적으로 동일한 방식으로 동작한다는 사실을 받아들이기만 하면 된다.

 

Kinds of Framework Classes

코코아 프레임워크에 있는 클래스는 4가지 방식으로 자신의 서비스(기능)를 제공한다.

[list type=”star”]

  • Off the shelf. 일부 클래스들은 바로 사용할 수 있는 기본 객체(off-the-shelf object)를 정의한다. 이러한 클래스의 인스턴스를 생성하고 필요에 따라 적절히 초기화하여 사용할 수 있다. AppKit 프레임워크의 경우 NSTextField, NSButton, NSTableView와 같은 NSControl 하위 클래스들이 이러한 범주에 속한다. 이들과 관련된 UIKit 클래스는 UIControl, UITextField, UIButton, UITableView이다. 이러한 객체들은 코드를 통해서도 생성할 수 있지만 대부분 Interface Builder를 통해 생성하고 초기화한다.
  • Behind the scenes. 코코아는 프로그램이 실행될 수 있도록 몇가지 프레임워크 객체를 내부적으로 생성한다. 이러한 객체들은 자동적으로 생성되므로 직접 생성할 필요는 없다. 대부분 비공개 클래스이지만 원하는 동작을 구현하기 위해서 필요하다.
  • Generically. 일부 프레임워크 클래스는 일반적(generic)이다. 프레임워크는 있는 그대로(변경없이) 사용할 수 있는 몇가지 구상 하위 클래스를 제공하기도 한다. 이들을 서브클래싱 하거나 특정 메소드의 구현을 재정의할 수도 있다. AppKit의 NSView, NSDocument, UIKit의 UIView, UIScrollView가 이러한 종류에 속하는 클래스이다.
  • Through delegation and notification. 다수의 프레임워크 객체들은 자신의 동작을 다른 객체에 알려주고 특정 역할을 다른 객체에 위임하기도 한다. 이러한 정보를 전달하는 매커니즘은 델리게이션(delegation)과 통지(notification)이다. 위임 객체(delegating object)는 프로토콜 형태의 인터페이스를   공개한다. 클라이언트 객체는 반드시 대상 객체의 델리게이트로 등록하고 이 인터페이스에 있는 하나 이상의 메소드를 구현해야 한다. 통지 객체(notifying object)는 자신이 보내는 통지의 목록을 공개하고, 클라이언트는 이들을 자유롭게 감시(observe) 할 수 있다. 다수의 프레임워크 클래스들이 통지를 발송한다.

[/list]

일부 클래스들은 이러한 종류의 서비스를 하나 이상 제공하기도 한다. 예를 들어, Interface Builder 라이브러리에서 NSWindow 객체를 화면으로 드래그 한 다음 약간의 초기화를 거쳐 바로 사용할 수 있다. 따라서 NSWindows 클래스는 기본 객체(off-the-shelf object)를 제공한다. 그러나 NSWindow 객체는 자신의 델리게이트로 메시지를 보내고 다양한 통지를 발송하기도 한다. 창에 라운드 효과를 적용하기 위해서 NSWindow를 서브클래싱 할 수도 있다.

프레임워크에서 제공하는 구조에 프로그램에 특화된 코드를 통합할 수 있도록 해주는 것은 대부분 마지막 두가지 범주에 포함된 코코아 클래스들이다. “Inheriting from a Cocoa Class”는 일반적인 관점에서 프레임워크 클래스를 서브클래싱 할 수 있는 방법에 대해 설명한다. 델리게이션, 통지 등과 같이 프로그램에 존재하는 객체들 사이의 통신에 사용되는 기술에 대한 정보는 “Communicating with Objects” 에서 제공한다.

 

Cocoa API Conventions

코코아 프레임워크에 선언되어 있는 클래스와 메소드 등을 사용할 때는 효율성과 일관성을 보장하기 위해 사용되고 있는 몇가지 규칙에 대해 알고 있어야 한다.

[list type=”star”]

  • 일반적으로 객체를 리턴하는 메소드는 객체를 생성할 수 없거나 어떤 이유로 인해 객체가 리턴되지 않았다는 것을 나타내기 위해서 nil을 리턴한다. 이러한 메소드는 상태 코드를 직접 리턴하지 않는다.nil을 리턴하는 규칙은 런타임 오류나 다른 일반적인 상태를 나타내는데 종종 사용되기도 한다. 코코아 프레임워크는 잘못된 배열 첨자나 인식할 수 없는 메소드 셀렉터와 같은 오류를 처리할 때 예외를 발생시키고, 메소드 시그니처에 nil을 리턴하도록 되어 있다면 nil을 리턴한다.
  • 위에서 설명한 메소드와 마찬가지로 nil을 리턴할 수 있는 일부 메소드들은 참조를 통해 오류 정보를 리턴하는 파라미터를 마지막 파라미터로 포함한다.이 파라미터는 NSError 객체에 대한 포인터를 받는다. 메소드 호출이 실패하였을 때 참조로 전달되는 오류 객체를 통해 오류의 원인을 파악하거나 대화상자에 오류를 표시할 수 있다.예를 들어, NSDocument 클래스에 다음과 같은 메소드가 선언되어 있다.
  • 파일 읽기와 쓰기 같은 일부 시스템 작업을 수행하는 메소드들은 성공 또는 실패를 나타내기 위해 종종 불린 값을 리턴하기도 한다.이러한 메소드들도 NSError 객체에 대한 포인터를 마지막 참조 파라미터로 포함하기도 한다. 예를 들어, NSData 클래스에 다음과 같은 메소드가 존재한다.
  • 빈 컨테이너 객체는 기본 값이나 “값 없음”을 나타내는데 사용된다. 일반적으로 nil은 유효한 객체 파라미터가 아니다.다수의 객체들은 값이나 객체의 집합을 저장한다. 예를 들어, NSString, NSDate, NSArray, NSDictionary 등이 있다. 이러한 객체를 파라미터로 받아들이는 메소드는 “값이 없다”는 것이나 기본값이 사용되어야 한다는 것을 요청하는 빈 객체를 받아들이기도 한다. 예를 들어, 다음과 같은 메시지는 빈 문자열을 통해 창에 표시되는 파일이름을 “no value”로 설정한다.

[/list]
[note color=”#fffcf0″]Note: @”문자열”과 같은 형식의 Objective-C 코드는 NSString 객체를 생성한다. 그러므로 @””는 빈 문자열 객체를 생성할 것이다. 자세한 정보는 String Programming Guide 에서 제공한다.[/note]

[list type=”star”]

  • 코코아 프레임워크는 딕셔너리 키, 통지와 예외의 이름, 문자열을 받아들이는 일부 메소드 파라미터에 문자열 리터럴 대신 미리 정의된 전역 문자열 상수를 사용하도록 요구한다.항상 문자열 리터럴 보다는 문자열 상수를 사용하는 것이 좋다. 문자열 상수를 사용함으로써 잘못된 철자로 인해 발생하는 런타임 오류를 피할 수 있다.
  • 코코아 프레임워크는 자료형을 일관적으로 사용하며 연관된 API와의 임피던스 매칭을 제공한다.예를 들어, 프레임워크는 좌표 값을 나타내기 위해 float를 사용하고, 그래픽에 사용되는 값이나 좌표를 위해 CGFloat를 사용한다. NSPoint나 CGPoint는 좌표내의 위치를 표현하는데 사용되고, NSString 객체는 문자열 값, NSRange는 범위, NSInteger와 NSUInteger는 정수를 저장하는데 사용된다. API를 설계할 때는 유사한 자료형의 일관성을 위해 노력해야 한다.
  • frame과 bounds와 같은 메소드에서 리턴되는 크기는 문서나 헤더파일에 명시되어 있지 않다면 기본적으로 포인트 단위를 사용한다.

[/list]

대부분의 코코아 API 규칙은 클래스, 메소드, 함수, 상수 등의 이름을 지정하는 것과 관련되어 있다. 프로그래밍 인터페이스를 설계할 때 이러한 규약에 대해 잘 알고 있어야 한다. 그 중 몇가지 중요한 이름정의 규칙은 다음과 같다.

[list type=”star”]

  • 클래스의 이름과 클래스와 연관된 심볼의 이름에는 접두어를 사용해야 한다. 접두어는 이름충돌을 방지하고 기능적 영역을 구분하는데 도움을 준다. 접두어 규칙은 ACCircle에서 “AC” 부분에 해당된다.
  • API 이름은 단순한 것보다 명확한 것이 좋다. 예를 들어, removeObjectAtIndex:가 의미하는 것은 이해하기 쉽지만 remove:는 모호하다.
  • 모호한 이름은 피해야 한다. 예를 들어, displayName은 이름을 표시하는지, 표시된 이름을 리턴하는지 명확하지 않기 때문에 모호하다.
  • 동작을 나타내는 메소드나 함수의 이름에는 동사를 사용해야 한다.
  • 메소드가 속성이나 계산된 값을 리턴한다면, 이 메소드의 이름은 속성의 이름과 동일해야 한다. 이러한 메소드를 “getter” 메소드라고 한다. 예를 들어, 속성이 배경색이라면 getter 메소드의 이름은 backgroundColor가 되어야 한다. 불린 값을 리턴하는 getter 메소드는 hasColor와 같이 “is” 또는 “has”를 접두어로 사용해야 한다.
  • 메소드가 속성값을 설정한다면 속성이름 앞에 “set” 접두어를 포함하는 이름을 사용해야 한다. setBackgroundColor:와 같이 속성이름의 첫 문자는 대문자로 적어야 한다.
  • HTML이나 TIFF와 같이 잘 알려진 약자가 아니라면 축약표현을 사용하지 않아야 한다.

[/list]

Objective-C 프로그래밍 인터페이스의 이름정의 규칙에 대한 모든 내용은 Coding Guidelines for Cocoa 에서 제공한다.

전반적으로 가장 중요한 API 규칙은 MRC 어플리케이션에서의 객체 소유권과 관계된 것이다. 이 규칙은 간단히 말해서 객체를 생성, 복사, 유지(retain)한 클라이언트(객체)가 해당 객체를 소유한다는 것이다. 객체의 소유자는 객체가 더 이상 필요하지 않을 때 release나 autorelease 메시지를 보내서 객체를 해제해야 한다. 이 주제에 대한 상세한 내용은 Advanced Memory Management Programming Guide 문서의 “How Memory Management Works” 에서 다룬다.

 

Inheriting from a Cocoa Class

AppKit이나 UIKit과 같은 프레임워크는 다양한 형태의 어플리케이션에서 공유할 수 있는 프로그램 모델을 정의한다. 이 모델은 포괄적이기 때문에 일부 프레임워크 클래스들이 추상이거나 의도적으로 완성되어 있지 않은 것은 당연하다. 클래스는 보통 저수준에서 일반적인 코드를 통해 대부분의 작업을 수행하지만, 중요한 부분은 미완성으로 남겨두거나 안전하게 동작할 수 있는 기본 방식으로 구현해 둔다.

어플리케이션은 상위 클래스의 부족한 구현을 채우기 위해서 하위 클래스를 생성(서브클래싱)해야 할 필요가 있다. 서브클래싱은 어플리케이션에 특화된 기능을 프레임워크에 추가하는 중요한 방법이다. 커스텀 하위 클래스의 인스턴스는 프레임워크가 정의하고 있는 객체 네트워크에 포함된다. 이 클래스는 프레임워크로부터 다른 객체들과 동작할 수 있는 능력을 상속한다. 예를 들어, NSCell의 하위 클래스를 생성한다면, 이 클래스의 인스턴스는 NSButtonCell, NSTextFieldCell 등과 같이 NSMatrix 객체에서 표시될 수 있다.

서브클래싱에서 중요한 작업 중 하나는 상위 클래스에 선언된 특정 메소드들을 구현하는 것이다. 상속된 메소드를 재구현하는 것을 재정의(overriding)이라고 한다.

 

When to Override a Method

프레임워크 클래스에 정의되어 있는 대부분의 메소드들은 완전히 구현되어 있으며, 호출을 통해 클래스에서 제공하는 기능을 사용할 수 있도록 해준다. 이러한 메소드는 재정의 할 필요가 거의 없으며, 대부분 이렇게 하지 않아야 한다. 프레임워크는 자신의 역할을 위해서 이러한 메소드에 의존한다. 어떤 경우에는 메소드를 재정의 할 수도 있지만 이렇게 해야할 이유는 존재하지 않는다. 프레임워크의 기본 메소드는 충분한 기능을 수행한다. 그러나, strcmp를 사용하지 않고 직접 문자열 비교 함수를 구현해야 하는 경우처럼 프레임워크 메소드를 재정의할 수도 있다.

하지만, 일부 프레임워크 메소드들은 반드시 재정의 되어야 하며, 이들은 프레임워크에 프로그램에 특화된 기능을 추가할 수 있도록 해준다. 이러한 메소드의 기본구현은 작업을 거의 수행하지 않지만, 다른 프레임워크 메소드에 의해 전달된 메시지를 통해 호출된다. 이러한 종류의 메소드에 구현을 추가하려면 반드시 재정의해야 한다.

Invoke or Override?

일반적으로 하위 클래스에서 재정의한 프레임워크 메소드는 직접 호출하게 되는 메소드는 아니다. 단순히 메소드를 재정의 한 다음 나머지는 프레임워크에게 맡긴다. 일반적인 관점에서 프레임워크 클래스는 다음 중 하나의 작업을 수행할 수 있도록 공개 메소드를 선언한다.

[list type=”star”]

  • 메소드 호출을 통해 클래스가 제공하는 기능을 이용할 수 있도록
  • 메소드 재정의를 통해 프레임워크에 정의되어 있는 프로그램 모델에 직접 작성한 코드를 통합할 수 있도록

[/list]

메소드는 때때로 두가지 범주에 모두 속하기도 한다. 하지만, 일반적으로 메소드가 직접 호출될 수 있는 것이라면 프레임워크에 의해 완전히 구현되어 있으며 코드에서 재정의할 필요가 없다. 메소드가 하위 클래스에서 재정의되어야 하는 것이라면, 적절한 시점에 프레임워크에 의해 호출될 수 있도록 해주는 일부분만 구현되어 있다. 그림 3-2는 두가지 일반적인 형태의 프레임워크 메소드를 보여준다.

Figure 3-2  Invoking a framework method that invokes an overridden method

Figure 3-2 Invoking a framework method that invokes an overridden method

코코아 프레임워크를 사용하는 객체지향 프로그래밍에서 필요로하는 대부분의 작업은 메시지를 통해 간접적으로 사용하는 메소드를 구현하는 것이다.

 

Types of Overridden Methods

하위 클래스에서는 다음과 같은 형식의 메소드를 정의할 수 있다.

[list type=”star”]

  • 일부 프레임워크 메소드들은 완전히 구현되어 있고, 다른 프레임워크 메소드에서 호출될 목적으로 사용된다. 다시 말해, 이러한 메소드들을 재구현 할 수도 있지만, 직접 작성한 코드에서 자주 호출하지는 않는다. 이러한 메소드는 프로그램이 실행되는 동안 특정 시점에 다른 코드가 필요로하는 데이터나 동작을 제공하며, 원하는 경우 재정의 할 수 있도록 공개 인터페이스에 존재한다. 그리고 프레임워크에서 사용되는 알고리즘을 새로운 것으로 대체하거나 확장, 수정할 수 있는 기회를 제공한다. NSMenuView 클래스의 정의되어 있는 trackWithEvent: 메소드가 이러한 메소드의 한가지 예이다. NSMenuView 클래스는 메뉴 추적과 항목 선택 처리와 같은 직접적인 요구에 부합하도록 메소드를 구현하고 있지만, 다른 동작을 구현하고자 하는 경우에는 재정의 할 수 있다.
  • 또 다른 형태의 메소드는 속성의 변경 여부나 특정 정책의 적용여부와 같은 객체별 선택사항을 지정하는 것이다. 프레임워크는 이 메소드의 기본 버전에서 한가지 선택(기본값)을 하도록 구현하고 있으므로, 다른 선택이 필요한 경우에는 반드시 자신만의 버전을 구현해야 한다. 대부분의 경우 YES나 NO를 리턴하거나 기본값이 아닌 다른 값을 계산하는 것과 같이 단순하게 구현된다.NSResponder의 acceptsFirstResponder 메소드가 이러한 종류의 메소드이다. 이 메시지는 키보드나 마우스 클릭에 반응해야 하는지 확인할 때 뷰로 전달된다. 기본적으로 NSView 객체는 이 메소드에서 NO를 리턴하며, 대부분의 뷰는 형식화된 입력을 받아들이지 않는다. 하지만, 입력을 받아들이는 일부 뷰는 acceptsFirstResponder 메소드가 YES를 리턴하도록 재정의해야 한다. UIView의 layerClass 클래스 메소드도 이러한 메소드의 한 종류이다. 뷰의 기본 레이어 클래스를 사용하지 않고자 한다면, 이 메소드를 재정의 해서 직접 작성한 클래스로 교체할 수 있다.
  • 일부 메소드는 반드시 재정의되어야 하지만, 프레임워크의 구현을 완전히 대체하는 것이 아니라 일부 기능을 추가하기만 해야 한다. 이 메소드의 하위 클래스 버전은 상위 클래스의 동작을 확장한다. 이러한 메소드를 구현할 때는 재정의하는 메소드를 상위로 통합하는 것이 중요하다. 이러한 통합은 super로 메시지를 보내 프레임워크에 정의되어 있는 상위 버전의 메소드를 호출하는 방식으로 구현된다.
  • 일부 프레임워크 메소드들은 아무런 작업을 수행하지 않거나 런타임 에러나 컴파일 에러를 방지하기 위해서 self와 같은 기본값을 리턴하기도 한다. 이러한 메소드들은 재정의할 목적으로 제공되며, 프로그램에 전적으로 특화된 작업을 수행하기 때문에 프레임워크는 이러한 메소드를 정의할 수 없다. 그리고 super로 보내는 메시지를 통해 프레임워크의 구현을 통합할 필요도 없다.하위 클래스가 재정의하는 대부분의 메소드는 이 범주에 속한다. 예를 들어, NSDocument의 dataOfType:error: 메소드와 readFromData:ofType:error: 메소드는 새로운 문서 기반의 어플리케이션을 만들 때 반드시 재정의되어야 한다.

[/list]

메소드를 재정의 하는 것이 반드시 복잡한 작업이어야 하는 것은 아니다. 한 두줄의 코드를 통해 상위 클래스의 동작에 의미있는 변화를 줄 수도 있다. 그리고, 메소드를 직접 구현할 때는 모든 것을 직접 만들지는 않으며, 코코아 프레임워크에서 제공하는 클래스, 메소드, 함수 등을 사용할 수 있다.

 

When to Make a Subclass

어떤 메소드를 재정의해야 하는가와 마찬가지로 어떤 클래스로부터 상속해야 하는가에 대해 아는것도 중요하다. 때때로 이러한 선택은 분명할 수 있지만, 때로는 간단하지 않을수도 있다. 다음과 같은 몇가지 설계 지침은 이러한 선택을 하는데 도움을 준다.

먼저, 프레임워크에 대해 알아야 한다. 프레임워크에 있는 각 클래스의 목적과 기능에 익숙해져야 한다. 아마도 원하는 기능을 제공하는 클래스가 이미 존재하고 있을 것이다. 그리고, 필요한 기능과 거의 유사한 기능을 제공하는 클래스를 발견했다면 당신은 운이 좋은 것이다. 이 클래스가 커스텀 클래스의 상위 클래스로 가장 적합한 것이다. 서브클래싱은 이미 존재하는 클래스를 재사용하고 목적에 맞게 새로운 기능을 추가하는 작업이다. 때로는 상속된 메소드를 재정의하여 기본 동작과 다른 무언가를 수행하도록 만드는 것이 하위 클래스에서 수행해야 하는 작업의 전부인 경우도 있다. 또 다른 하위 클래스들은 하나 이상의 속성을 인스턴스 변수로 추가하고, 이 속성을 사용하는 메소드를 정의하여 상위 클래스에 새로운 동작을 통합하기도 한다.

어떤 클래스가 하위 클래스에 가장 적합한가를 결정하는데 도움을 줄 수 있는 다른 고려 사항들도 존재한다. 만들고자 하는 어플리케이션이나 어플리케이션의 일부분이 어떤 특성을 가지고 있는가? 일부 코코아 아키텍처는 서브클래싱을 위해서 필요한 요구사항들을 가지고 있다. 예를 들어, 만들고 있는 것이 OS X에서 실행되는 다중문서 어플리케이션이라면, AppKit에 정의되어 있는 문서기반 아키텍처는 NSDocument를 서브클래싱 하고, 필요하다면 다른 연관된 클래스들도 서브클래싱 하도록 요구한다. Mac 앱에서 스크립트를 사용할 수 있도록 하려면, NSScriptCommand와 같은 스트립팅 관련 클래스 중 하나를 서브클래싱해야 할 것이다.

또 다른 요소는 하위 클래스의 인스턴스가 어플리케이션에서 수행하게 될 역할이다. MVC 설계 패턴은 객체의 역할을 지정한다. 뷰 객체는 사용자의 인터페이스에 표시되는 것이며, 모델 객체는 어플리케이션의 데이터와 이 데이터에서 동작하는 알고리즘을 담고 있는 객체이고, 컨트롤러 객체는 뷰 객체와 모델 객체를 연결하는 객체이다. 객체가 수행하는 역할을 이해하면 어떤 상위 클래스를 사용해야 하는지에 대한 선택사항을 좁힐 수 있다. 클래스의 인스턴스가 그리기와 이벤트 처리를 수행하는 뷰 객체라면, NSView나 UIView로부터 상속해야 할 것이다. 컨트롤러 객체가 필요하다면 NSObjectController와 같이 AppKit에서 제공하는 기본 컨트롤러 클래스를 사용하거나 NSController 또는 NSObject를 서브클래싱 할 수 있다. 클래스가 일반적인 모델 클래스라면 NSObject를 서브클래싱 하거나 코어 데이터 프레임워크를 사용해야 할 것이다.

[note color=”#fffcf0″]iOS Note: AppKit 프레임워크의 컨트롤러 클래스에 해당되는 UIKit 클래스는 존재하지 않는다. UIKit에는 UIViewController, UITableViewController, UINavigationController, UITabBarController와 같은 컨트롤러 클래스들이 존재하지만, 이러한 클래스들은 AppKit과는 다른 설계를 기반으로 하며, 어플리케이션 탐색, 항목 선택과 같이 다른 목적으로 사용된다.[/note]

때때로 서브클래싱은 문제를 해결하기 위한 최선의 방법이 아니며 더 좋은 접근법이 존재할 수도 있다. 단순히 클래스에 몇가지 편의 메소드(convenience methods)를 추가하고자 한다면, 서브클래싱 대신 카테고리를 만들수도 있다. 또는 델리게이션, 통지(notification), target-action 과 같은 다른 설계 패턴 중 하나를 사용할 수도 있다. 서브클래싱에 사용할 상위 클래스를 결정할 때, 상위 클래스의 헤더 파일에 서브클래싱 없이 원하는 기능을 사용할 수 있도록 해주는 델리게이션 메소드나 통지와 같은 매커니즘이 존재하는지 살펴보아야 한다.

이와 같은 맥락에서 프레임워크 프로토콜의 헤더 파일이나 문서를 살펴볼 수도 있다. 프로토콜을 적용함으로써 복잡한 서브클래싱의 어려움을 피하면서 원하는 목적을 달성할 수 있다. 예를 들어, 메뉴 항목의 활성화 상태를 관리하고자 한다면, 커스텀 컨트롤러 클래스에 NSMenuValidation 프로토콜을 적용할 수 있다. 이러한 기능을 구현하기 위해서 NSMenuItem이나 NSMenu를 서브클래싱 할 필요는 없다.

일부 프레임워크 메소드의 경우 재정의되지 않아야 하는 것처럼, NSFileManager나 UIWebView와 같은 일부 프레임워크 클래스들은 서브클래싱에 사용될 수 없다. 이러한 서브클래싱을 시도할 때는 주의를 기울이면서 진행해야 한다. 특정 프레임워크의 구현은 복잡하고 다른 클래스의 구현이나 OS의 다른 부분과 밀접하게 통합되어 있다. 프레임워크 메소드의 역할을 올바르게 재현하거나, 내부적인 의존성이나 메소드가 가진 효과를 예상하는 것은 어려운 일이다. 일부 메소드 구현을 변경하는 것은 예측할 수 없는 광범위한 문제를 발생시킬 수도 있다.

때로는 객체 조합(object composition)을 통해 이러한 서브클래싱의 어려움을 피할 수 있다. 객체 조합은 여러 객체를 하나의 호스트 객체로 묶는 일반적인 방법이다(그림 3-3). 복잡한 프레임워크 클래스를 직접 상속하지 않고, 이러한 클래스를 인스턴스 변수로 가지는 커스텀 클래스를 만들 수 있다. 이 커스텀 클래스는 그 자체로 매우 간단하며, 대부분 NSObject를 직접 상속할 것이다. 이 클래스는 상속의 관점에서만 간단할 뿐, 포함하고 있는 클래스를 필요에 따라 조작하거나 확장한다. 파운데이션 프레임워크에 있는 NSAttributedString이 이러한 객체 조합의 한 예이다. NSAttributedString은 NSString 객체를 인스턴스 변수로 가지고 있으며 string 메소드를 통해 이 객체를  제공한다. NSString은 문자열 인코딩, 검색, 경로 조작과 같은 복잡한 기능을 제공하는 클래스이다. NSAttributedString은 이러한 기능에 폰트, 색상, 정렬, 문단 스타일 등과 같은 문자 속성에 관련된 다양한 기능을 추가한다. 그리고 이러한 것들은 NSString 클래스를 상속하지 않고 구현한다.

Figure 3-3  Object composition

Figure 3-3 Object composition

 

Basic Subclass Design

모든 하위 클래스들은 올바르게 설계되었을 때 상위 클래스나 역할에 관계없이 몇가지 특징을 공유한다.   잘못 설계된 하위 클래스는 오류를 발생시키기 쉽고 사용하기 어려우며, 확장하기 어렵고 성능을 감소시킨다. 잘 설계된 하위 클래스는 이것과 완전히 반대이다. 이번 섹션은 효율적이고 견고하며, 사용하기 쉽고 재사용할 수 있는 하위 클래스를 설계하는 방법에 대한 조언을 제공한다.

Further Reading: 이 문서는 몇가지 서브클래싱 방식에 대해 설명하지만, 주로 기본적인 설계에 중점을 둔다. 클래스 정의의 일부를 자동화하기 위해 Xcode와 Interface Builder를 사용하는 방법에 대해서는 설명하지 않는다. 이러한 어플리케이션의 사용법에 대해 배우려면 A Tour of Xcode를 읽어 보아야 한다. 이번 섹션에서 제공하는 설계 정보는 특히 모델 객체에 적용할 수 있다. Model Object Implementation Guide는 모델 객체의 올바른 설계와 구현에 대해 설명한다.

 

The Form of a Subclass Definition

Objective-C 클래스는 인터페이스(interface)와 구현(implementation)으로 구성된다. 이 두가지 부분은 관례상 각각 분리된 파일에 위치한다. 인터페이스 파일은 .h 확장자가 포함된 이름을 가지고, 구현 파일은 .m 확장자가 포함된 이름을 가진다. 보통 확장자를 제외한 이름이 클래스의 이름이다. 그러므로 Controller라는 이름을 가진 클래스의 경우 파일 이름은 다음과 같을 것이다.

Controller.h
Controller.m

인터페이스 파일은 클래스의 공개 인터페이스를 구성하는 속성, 메소드, 함수 선언을 포함한다. 또한, 인스턴스 변수, 상수, 전역 문자열 등을 포함하기도 한다. @interface 지시자는 인터페이스의 선언을 시작하고, @end 지시자는 선언을 종료한다. @interface 지시자는 다음과 같은 형식을 사용해서 클래스의 이름과 직접적으로 상속하는 상위 클래스를 지정하기 때문에 특히 중요하다.

Listing 3-2는 가상의 Controller 클래스에 대한 인터페이스 파일의 예를 보여준다.

클래스 인터페이스를 완성하려면, 그림 3-4에서 보여주는 것과 같이 필요한 선언들을 인터페이스 내의 적절한 위치에 추가해야 한다.

Figure 3-4  Where to put declarations in the interface file

Figure 3-4 Where to put declarations in the interface file

이러한 형태의 선언에 대해서는 “Instance Variables”“Functions, Constants, and Other C Types”에서 자세하게 설명한다.

가장 먼저 적절한 코코아 프레임워크와 헤더 파일을 임포트해야 한다. #import 전처리 명령은 지정된 헤더 파일을 통합한다는 점에서 #include와 유사하다. 하지만, #import는 헤더 파일이 단 한번만 포함되도록 함으로써 효율성을 증가시킨다. 이 명령 뒤에 따라오는 <…> 부분은 헤더 파일이 포함된 프레임워크와 사용할 헤더 파일의 이름을 지정한다. 그러므로 #import 문법은 다음과 같이 사용한다.

프레임워크는 반드시 프레임워크의 표준 시스템 위치 중 하나에 존재해야 한다. Listing 3-2에서, 클래스 인터페이스 파일은 Foundation 프레임워크에 있는 Foundation.h 파일을 임포트한다. 이처럼, 프레임워크의 이름과 동일한 이름을 가지는 헤더 파일은 프레임워크의 모든 공개 인터페이스를 임포트하는  #import 명령을 포함하고 있는 것이 코코아의 관례이다.

프로젝트에 포함된 헤더 파일을 임포트해야 한다면, 다음과 같이 <…> 대신 따옴표를 사용해야 한다.

클래스 구현 파일은 Listing 3-3과 같이 단순한 구조를 가진다. 구현 파일에서는 클래스 인터페이스 파일을 임포트 해야 한다는 것이 중요하다.

반드시 @implementation 지시어와 @end 지시어 사이에 모든 메소드와 속성 구현을 작성해야 한다. 관례상 클래스와 연관된 함수의 구현 역시 두 지시어 사이에 위치하지만, 원한다면 다른 위치에 둘 수도 있다. 비공개 형식(함수, 구조체 등)의 선언은 보통 #import와 @implementation 사이에 위치한다.

 

Overriding Superclass Methods

커스텀 클래스는 프로그램에 특화된 방식으로 상위 클래스의 동작을 변경한다. 보통 이러한 변경을 적용하는 방법은 상위 클래스의 메소드를 재정의하는 것이다. 하위 클래스를 설계할 때 수행하는 첫번째 단계는 재정의할 메소드를 지정하고 재정의 방식에 대해 고려해보는 것이다.

“When to Override a Method”에서 몇가지 일반적인 지침들을 제공하지만, 어떤 메소드를 재정의 할지를 정하기 위해서는 클래스를 조사해보는 것도 필요하다. 특히, 클래스의 헤더 파일과 문서를 자세히 살펴보아야 한다. 또한, 클래스의 지정된 초기화 메소드(designated initializer)에서 상위 구현을 반드시 호출해야 하므로 상위 클래스의 지정된 초기화 메소드의 위치도 찾아보아야 한다.

재정의 할 메소드를 정하고 난 후에는 해당 메소드의 선언을 인터페이스 파일로 복사해서 재정의 작업을 시작할 수 있다. 그런 다음, 이 선언을 .m 파일로 복사하고 세미콜론을 { }로 교체하여 메소드 구현의 뼈대를 만들어준다.

“When to Override a Method”에서 설명했던 것처럼, 이렇게 재정의한 메소드는 대부분 코코아 프레임워크에 의해 호출된다는 것을 기억해야 한다. 어떤 상황에서는 프레임워크에게 원래의 메소드 대신 재정의된 메소드를 호출해야 한다고 알려주어야 한다. 코코아는 이러한 작업을 위해 다양한 방식을 제공한다. 예를 들어, Interface Builder 어플리케이션은 Info 창에서 프레임워크 클래스를 직접 작성한 클래스로 교체할 수 있도록 해준다. 커스텀 NSCell 클래스를 만든다면, NSControl 클래스의 setCell: 메소드를 통해 특정 클래스와 연관시킬 수 있다.

 

Instance Variables

커스텀 클래스를 만드는 이유는 상위 클래스의 동작을 변경하는 경우를 제외하면 대부분 속성을 추가하기 위해서이다. Circle이라는 가상의 클래스가 있고, 색상이 포함된 하위 클래스를 만들고자 한다면, 이 하위 클래스는 어떤 식으로든 반드시 색상 속성을 가져야 한다. 이 경우 클래스 인터페이스에 color 인스턴스 변수를 추가할 수 있다.

Objective-C의 인스턴스 변수는 객체나 구조체의 선언, 또는 클래스 정의의 일부분을 구성하는 자료형이다. 인스턴스 변수가 객체라면, 이 선언은 id를 통해 동적으로 형식화되거나 정적으로 형식화 될 수 있다. 아래의 예제는 이러한 두가지 스타일을 보여준다.

일반적으로 객체의 클래스 멤버쉽이 불확실하거나 중요하지 않은 경우에 객체 인스턴스 변수를 동적으로 형식화한다. 인스턴스 변수로 포함되는 객체들은 생성되거나 복사되어야 하고, 상위 객체에 의해 소유되지 않았다면 명시적으로 유지(retain)되어야 한다. 인스턴스가 아카이브로부터 복원되었다면, initWithCoder: 메소드에서 복원한 데이터를 각 인스턴스 변수에 할당하고 유지(retain)해야 한다.

인스턴스 변수의 이름정의 규칙은 구두점이나 특수 문자를 포함하지 않는 소문자를 사용하는 것이다. 이름에 다수의 단어가 포함되어 있다면, 다음과 같이 모든 단어를 붙이고 두번째 이후 단어의 첫번째 문자를 대문자로 써야 한다.

인스턴스 변수 선언 앞부분에 추가되어 있는 IBOutlet 태그는 인스턴스 변수를 nib 파일에 저장되어 있는 연결(connection)과 연관시킨다. 이 태그는 Interface Builder와 Xcode가 작업을 동시에 진행할 수 있도록 해준다. 아웃렛을 nib 파일에서 읽어올 때 이러한 연결도 자동적으로 재생성된다.

인스턴스 변수는 객체의 사용자에게 제공되는 속성 이상의 것들을 가질 수도 있다. 때때로 인스턴스 변수는 객체에 의해서 수행되는 특정 작업에 사용되는 비공개 데이터를 가질 수도 있다. 내부 저장소(backing store)나 캐시가 이러한 예이다. (데이터가 클래스의 인스턴스 사이에서 공유되어야 한다면 인스턴스 변수 대신 전역 변수를 사용해야 한다.)

하위 클래스에 인스턴스 변수를 추가할 때에는 다음과 같은 지침을 따라야 한다.

[list type=”star”]

  • 꼭 필요한 인스턴스 변수만 추가해야 한다. 인스턴스 변수를 추가할수록 그만큼 인스턴스의 크기도 증가한다. 그리고 생성된 인스턴스가 많아질수록 문제는 더 심해진다. 가능하다면 값을 저장하는 또 다른 인스턴스 변수를 추가하기 보다 이미 존재하는 인스턴스 변수에서 값을 계산하도록 해야 한다.
  • 클래스의 인스턴스 데이터를 간으한 효율적으로 표현해야 한다. 예를 들어, 다수의 플래그를 인스턴스 변수로 선언하고자 한다면, 일련의 불린 선언 대신 비트 필드를 사용해야 한다. (하지만, 비트 필드를 아카이빙 하는 것은 복잡한 작업이다.) NSDictionary 객체를 통해 다수의 연관된 속성들을 키-값 쌍을 사용해서 하나로 묶을수도 있다. 이 경우에는 사용한 키에 대한 내용을 적절히 문서화해야 한다.
  • 인스턴스 변수에 적절한 범위를 지정해야 한다. @public은 캡슐화의 원칙을 위배하기 때문에 사용하지 않아야 한다. 하위 클래스에서 효율적으로 접근해야 하는 경우에는 @protected를 사용한다. 그렇지 않다면, 가장 제한적인 @private을 사용하는 것이 적절하다. 이러한 구현 은닉은 프레임워크에 의해 공개되는 클래스와 어플리케이션이나 다른 프레임워크에 의해 사용되는 클래스에서 특히 중요하다. 구현 은닉은 모든 클라이언트를 다시 컴파일 하지 않고 클래스의 구현을 변경할 수 있도록 해준다.
  • 클래스의 기본적인 속성이나 관계를 나타내는 인스턴스 변수에 대한 선언된 속성(declared property) 또는 접근자 메소드(accessor method)가 존재해야 한다. 접근자 메소드와 선언된 속성은 인스턴스 변수의 값에 대한 읽기와 쓰기를 처리하면서 캡슐화를 유지한다. 이 주제는 “Storing and Accessing Properties”에서 상세하게 다룬다.
  • 하위 클래스를 다른 클래스들이 상속할 수 있도록 공개할 예정이라면 인스턴스 변수 목록의 마지막에 예약된 필드(일반적으로 id형)를 추가해야 한다. 예약된 필드는 향후 특정 시점에 인스턴스 변수를 추가해야 할 필요가 있을 때, 이전 버전과의 바이너리 호환성을 유지하는데 도움을 준다. 이에 대한 자세한 정보는 “When the Class Is Public (OS X)” 에서 제공한다.

[/list]

 

Entry and Exit Points

코코아 프레임워크는 다양한 시점에 객체로 메시지를 보낸다. 거의 모든 객체들은 자신의 런타임 생명주기의 시작과 끝에 특정 메시지를 전달받는다. 이러한 메시지를 통해 호출되는 메소드는 해당 시점과 연관된 작업을 수행할 수 있도록 해주는 일종의 훅(hook)이다. 다음과 같은 메소드들이 여기에 속한다. (호출 순으로 나열되어 있음)

[list type=”arrow”]

  • initialize- 이 클래스 메소드는 클래스 자신이나 자신의 인스턴스가 다른 메시지를 받기 전에 자신을 초기화 할 수 있도록 해준다. 이 메시지는 상위 클래스에 먼저 전달된다. 이전 방식의 아카이빙 매커니즘을 사용하는 클래스는 이 메소드 구현에서 클래스 버전을 설정할 수 있다. 또한, 클래스를 특정 서비스에 등록하고 모든 인스턴스에서 사용되는 전역 상태를 설정할 수도 있다. 하지만, 때로는 이러한 종류의 작업을 다른 인스턴스 메소드에서 지연적으로 수행하는 것이 더 좋다.
  • init- 상위 클래스의 지정된 초기화 메소드(designated initializer)의 구현이 초기화에 충분하지 않다면 반드시 init 메소드나 다른 초기화 메소드들을 구현해야 한다. 초기화 메소드와 관련된 내용은 “Object Creation” 에서 설명한다.
  • initWithCoder:- 클래스가 모델 클래스인 경우와 같이 아카이브를 지원할 것으로 예상된다면 NSCoding 프로토콜을 적용하고 initWithCoder: 와 encodeWithCoder: 메소드를 구현해야 한다. 그리고 이러한 메소드들은 인스턴스 변수를 아카이브에 적합한 형태로 인코드하거나 디코드 해야 한다.  NSCoder의 메소드들은 KSKeyedArchiver와 NSKeyedUnarchiver의 키 아카이빙 기능에 사용할 수도 있다. 객체가 명시적으로 생성되지 않고 아카이브를 통해 생성된다면(unarchived) 초기화 메소드 대신 initWithCoder: 메소드가 호출된다. 이 메소드에서 값을 디코딩 한 후 적절한 인스턴스 변수에 할당해야 한다. 이와 관련된 자세한 내용은 Archives and Serializations Programming Guide에서 제공한다.
  • awakeFromNib- 어플리케이션은 nib 파일을 로딩할 때, 아카이브에서 로딩된 각 객체로 awakeFromNib 메시지를 보낸다. 하지만, 객체가 이 메시지에 응답할 수 있는 경우에만 전달하고, 아카이브에 있는 모든 객체들이 로딩되어서 초기화가 완료된 후에만 전달된다. 객체가 이 메시지를 전달받은 시점에는 모든 아웃렛 인스턴스 변수들을 안전하게 사용할 수 있다. 일반적으로, File’s Owner와 같이 nib 파일을 소유하고 있는 객체에서 소스코드를 통해 아웃렛과 타겟-액션 연결을 초기화하기 위해 이 메소드를 구현한다.
  • encodeWithCoder:– 클래스의 인스턴스가 아카이브로 저장되어야 하는 경우에 이 메소드를 구현한다. 이 메소드는 객체가 해제되기 직전에 호출된다.
  • dealloc 또는 finalize- MRC 프로그램에서는 인스턴스 변수를 해제하고 클래스의 인스턴스에 의해서 요청된 메모리를 제거하기 위해서 dealloc 메소드를 구현한다. 인스턴스는 이 메소드가 리턴된 후에 제거된다. 가비지 컬렉션을 사용하는 코드에서는 finalize 메소드 구현을 통해 동일한 작업을 수행한다. 더 자세한 정보는 “The dealloc and finalize Methods” 에서 다룬다.

[/list]

어플리케이션의 싱글톤 어플리케이션 객체 또한 어플리케이션의 시작과 종료 시점에 객체(이 객체가 어플리케이션 객체의 델리게이트이고 해당 메시지를 구현하고 있다면)로 메시지를 보낸다. 어플리케이션 객체는 시작 직후에 자신의 델리게이트로 다음과 같은 메시지를 보낸다.

[list type=”star”]

  • AppKit에서는 델리게이트로 applicationWillFinishLaunching:과 applicationDidFinishLaunching: 메시지를 보낸다. 전자는 더블 클랙된 문서가 열리기 직전에 전달되고, 후자는 직후에 전달된다.
  • UIKit에서는 델리게이트로 application:didFinishLaunchingWithOptions: 메시지를 보낸다.

[/list]

이러한 메소드에서 어플리케이션의 상태를 복원하고 어플리케이션 실행 초기에 단 한번 실행되는 어플리케이션 로직을 지정할 수 있다. 두 프레임워크의 어플리케이션 객체는 종류 직전에 자신의 델리게이트로 applicationWillTerminate: 메시지를 보낸다. 이 메소드를 통해 문서와 어플리케이션의 상태를 저장함으로써 어플리케이션의 종료를 유연하게 처리할 수 있다.

코코아 프레임워크는 뷰 로딩부터 어플리케이션의 활성/비활성에 이르기까지 다양한 이벤트 훅을 제공한다. 이러한 훅들은 때때로 델리게이션 메시지로 구현되는데, 이 경우에는 객체가 프레임워크 객체의 델리게이트로 설정되고 필수 메소드를 구현해야 한다. 때로는 훅이 통지인 경우도 있다. 또한 iOS에서는 UIViewController에서 상속되는 메소드인 경우도 있다. (델리게이션과 통지에 대해서는 “Communicating with Objects”에서 상세하게 설명한다.)

 

Initialize or Decode?

클래스의 객체가 아카이빙을 지원해야 한다면 반드시 NSCoding 프로토콜을 따라야 한다. 다시 말해, 자신의 객체들을 인코드 하는 encodeWithCoder: 메소드와 이들을 디코드하는 initWithCoder: 메소드를 반드시 구현해야 한다. 객체가 아카이브로부터 생성되는 경우에는 일반적인 초기화 메소드가 아닌 initWithCoder: 메소드가 대신 호출된다.

클래스의 초기화 메소드와 initWithCoder: 메소드는 서로 매우 유사한 작업을 수행할 수 있기 때문에, 공통적인 작업을 다른 메소드로 작성하고 두 메소드에서 이 메소드를 호출하도록 하는 것이 좋다. 예를 들어, 객체가 설정 과정의 일부로 드래그 형식과 드래깅 소스를 지정한다면 Listing 3-4와 같이 구현될 수 있다.

클래스 초기화 메소드와 initWithCoder: 메소드는 유사한 역할을 수행하지만, Interface Builder 팔레트에 표시되는 프레임워크 클래스의 하위 클래스를 만들 때 차이점을 가진다. 프로젝트에서 이러한 클래스를 정의하는 일반적인 방식은 Interface Builder 팔레트에서 객체를 인터페이스로 드래그하고 Info 창의 Custom Class 부분에서 작성한 하위 클래스와 연결하는 것이다. 하지만 이러한 경우 하위 클래스의  initWithCoder: 메소드는 아카이브 복원과정에서 호출되지 않고, init 메소드가 대신 호출된다. 그러므로 커스텀 객체의 특별한 설정 작업은 awakeFromNib(nib 파일에서 모든 객체가 복원된 후에 호출됨)에서 수행해야 한다.

 

Storing and Accessing Properties

property라는 단어는 코코아에서 일반적인 의미와 언어적인 의미를 동시에 가진다. 일반적인 의미는 객체 모델링의 설계 패턴에서 유래된 것이다. 언어적인 의미는 선언된 속성(declared property)이라고 하는 언어 기능과 관련되어 있다.

“Object Modeling”에서 설명했듯이 속성(property)은 객체의 특징(객체 모델링 패턴에서는 entity라고 함)을 정의하고 캡슐화된 데이터의 일부가 된다. 속성은 제목이나 위치와 같은 특성(attribute)이나 객체 사이의 관계(relationship)와 같은 두가지 종류로 구분할 수 있다. 관계는 1:1 이나 1:다, 아웃렛, 델리게이트, 다른 객체의 컬렉션과 같은 다양한 형태를 가질 수 있다. 속성은 거의 대부분 인스턴스 변수로 저장된다.

잘 설계된 클래스의 전반적인 전략의 일환은 캡슐화를 강요하는 것이다. 클라이언트 객체는 속성에 직접  접근할 수 없어야 하며, 클래스의 인스턴스를 통해 접근해야 한다. 이와 같이 객체의 속성에 대한 접근을 허용하는 메소드를 접근자 메소드(accessor methods)라고 부른다. 접근자 메소드는 객체의 속성 값을 읽거나 설정한다. 이들은 객체로의 접근을 조절하고 객체의 인스턴스 데이터에 대한 캡슐화를 강요함으로써 해당 속성에 대한 출입문과 같은 역할을 수행한다. 보통 값에 접근하는 메소드는 getter method, 값을 설정하는 메소드는 setter method라고 부른다.

Objective-C 2.0에서 소개된 선언된 속성(declared property)은 속성에 접근하는 방식에 변화를 가져왔다. 선언된 속성은 클래스 속성에 대한 접근자 메소드를 선언하는 문법적 축약이다. 클래스의 구현 블록에서 이러한 메소드의 합성(또는 생성)을 컴파일러에게 요청할 수 있다. “Declared Properties”에서 살펴본 것처럼 클래스 선언파일의 @interface 블록에서 @property 지시어를 통해 속성을 선언한다. 이 선언은 속성의 이름과 형식을 지정하고 컴파일러에게 접근자 메소드를 구현하는 방식을 알려주는 한정자들을 포함하기도 한다.

클래스를 설계할 때에는 다양한 요소들이 속성을 저장하고 접근하는 방식에 영향을 준다. 선언된 속성을 통해 컴파일러가 접근자 메소드를 대신 생성하도록 할 수도 있고, 직접 구현할 수도 있다. 물론 선언된 속성을 사용하는 경우에도 접근자 메소드를 직접 구현할 수 있다. 또 다른 중요한 요소는 코드가 가비지 컬렉션 환경에서 실행되는가 하는 것이다. 가비지 컬렉터가 활성화 되어 있다면 접근자 메소드의 구현은 MRC 환경의 코드에 비해 훨씬 단순해 진다. 이어지는 섹션에서는 속성에 접근하고 저장하기 위한 다양한 대안에 대해 설명한다.

 

Taking Advantage of Declared Properties

반드시 접근자 메소드를 직접 작성해야 할 이유가 없다면 새로운 프로젝트에서 Objective-C 2.0의 새로운 기능인 선언된 속성(declared property)을 사용하는 것이 좋다. 먼저, 선언된 속성은 접근자 메소드를 구현해야하는 지루한 작업으로부터 해방시켜준다. 두번째로 컴파일러가 접근자 메소드를 대신 생성하도록 함으로써 프로그램 오류의 발생 가능성을 줄이고 클래스 구현이 더욱 일관적으로 동작하도록 해준다. 마지막으로 선언된 속성의 선언 방식은 다른 개발자들이 해당 속성의 사용의도를 명확히 파악할 수 있도록 해준다.

@property를 통해 선언된 일부 속성의 경우 여전히 접근자 메소드를 직접 구현해야 할 경우가 있다. 이러한 작업이 필요한 이유는 속성의 값을 읽거나 설정하는 것 이상의 작업이 필요하기 때문이다. 예를 들어, 속성에 저장할 이미지 리소스가 파일에서 로드하되, 성능상의 이유로 필요한 시점에 지연적으로 읽기를 원한다고 가정해보자. 이러한 이유로 접근자 메소드를 직접 구현해야 한다면  “Implementing Accessor Methods”에서 몇가지 팁과 지침을 얻을 수 있다. 클래스의 @implementation 블록에서 속성을 @dynamic 지시어로 지정하거나, 간단히 속성에 대한 @synthesize 지시어를 생략하는 방식을 통해 컴파일러가 특정 접근자 메소를 생성하지 않도록 지정할 수 있다. (@dynamic이 기본값이다.) 컴파일러는 직접 구현한 접근자 메소드가 이미 존재하고 이 속성에 대한 @synthesize 지시어가 존재하지 않는다면 접근자 메소드를 생성하지 않는다.

프로그램에서 가비지 컬렉션을 사용하지 않는다면 속성 선언을 구체화하기 위해서 사용하는 특성이 매우 중요하다. 기본적으로 컴파일러가 생성한 setter 메소드는 단순한 할당을 수행하며, 이것은 가비지 컬렉션 환경에 적합하다. 하지만 MRC 환경에서 객체 형태의 속성에 단순 할당을 사용하는 것은 적합하지 않다. 이 경우에는 반드시 할당된 객체를 유지(retain)하거나 복사해야 한다. 속성의 특성을 지정할 때 retain이나 copy 특성을 사용해서 컴파일러가 적절한 접근자 메소드를 생성하도록 지시할 수 있다. 속성 객체의 클래스가 NSCopying 프로토콜을 따르는 경우에는 항상 copy 특성을 지정해야 하며, 이것은 가비지 컬렉션이 활성화 된 경우에도 해당된다.

아래의 예제는 속성 선언을 사용하는 다양한 방식과 @synthesize, @dynamic 지시어를 사용하는 방식을 보여준다. 아래의 코드는 컴파일러가 name, accountID 속성에 대한 접근자 메소드를 생성하도록 지시한다. 컴파일러는 accountID 속성이 읽기 전용이므로 getter 메소드만을 생성한다. 이 예제에서는 가비지 컬렉션이 활성화 되어 있다고 가정한다.

[note color=”#fffcf0″]Note: 32비트 프로세스 환경에서 속성은 반드시 연관된 인스턴스 변수를 가져야 한다. 하지만 64비트 환경에서는 이러한 제약이 없다. [/note]

아래의 코드는 선언된 속성의 또 다른 면을 보여준다. 가비지 컬렉션이 활성화되어 있지 않다고 가정하므로, currentHost 속성 선언은 retain 특성을 가진다. 이 특성은 컴파일러가 접근자 메소드에서 새로운 값을 유지(retain)하도록 지시한다. 또한 hidden 속성의 선언은 컴파일러가 isHidden 이라는 이름의 getter 메소드를 생성하도록 지시하는 특성을 포함하고 있다. 이 속성의 값은 객체가 아니기 때문에 setter 메소드에서 단순 할당을 수행하는 것으로도 충분하다.

[note color=”#fffcf0″]Note: 이 예제는 속성에 대해 기본값인 atomic 대신 nonatomic 특성을 지정한다. 속성이 atomic 특성을 가질 때 생성된 접근자 메소드는 다수의 스레드에서 동시에 실행되는 경우에도 항상 유효한 값을 리턴한다. atomic과 nonatomic의 차이점에 대해서는 Objective-C Programming Language 에서 설명하고 있다. 스레드 안정성과 관련된 내용은 Threading Programming Guide 에서 제공한다. [/note]

아래의 예제에 있는 속성 선언은 컴파일러에게 previewImage 속성이 읽기전용이라는 것을 알려준다. 그리고 구현 블록에서 @dynamic 지시어를 통해 컴파일러가 getter 메소드를 생성하지 않도록 지시하고, 해당 접근자 메소드를 직접 구현한다.

 

Implementing Accessor Methods

접근자 메소드의 이름은 관습적인 이유와 클래스가 키-값 코딩에 호환되도록 한다는 점 때문에 반드시 특별한 형태를 가져야 한다. 인스턴스의 값을 리턴하는 getter 메소드의 경우 메소드의 이름이 인스턴스 이름과 동일하다. 인스턴스의 값을 설정하는 setter 메소드의 이름은 set으로 시작하고 뒤에 인스턴스 변수의 이름이 따라온다. 그리고 인스턴스 변수 이름의 첫번째 문자는 대문자로 작성한다. 예를 들어, color라는 이름의 인스턴스 변수가 있다면 접근자의 선언은 다음과 같을 것이다.

불린 속성의 경우 변형된 형태의 getter 메소드를 사용할 수 있다. 이 경우에는 is<속성이름>과 같은 형식을 사용한다. 예를 들어, hidden이라는 이름의 인스턴스 변수는 isHidden이라는 getter 메소드와 setHidden:이라는 setter 메소드를 가질 것이다.

인스턴스 변수가 int나 float와 같은 스칼라 자료형이라면 접근자 메소드의 구현은 매우 단순해지는 경향이 있다. float 자료형 인스턴스 변수의 이름이 currentRate이라고 가정한다면, 접근자 메소드는 Listing 3-5와 같이 구현할 수 있다.

가비지 컬렉션을 사용한다면 객체 값을 가지는 속성에 대한 setter 메소드 구현도 단순한 할당을 사용한다. Listing 3-6은 이러한 메소드를 구현하는 방법을 보여준다.

가비지 컬렉션을 사용하지 않을 때, 인스턴스 변수가 객체를 저장하는 경우에는 상황이 조금 달라진다. 이들은 인스턴스 변수이기 때문에 반드시 지속적으로 저장되어야 하고 할당시점에 생성, 복사, 유지(retain)되어야 한다. setter 메소드가 인스턴스 변수의 값을 변경할 때, 값을 저장함과 동시에 이전 값을 적절히 해제해야 한다. getter 메소드는 인스턴스 변수의 값을 요청한 객체로 전달한다. 이와 같은 접근자 메소드의 동작 방식은 메모리 관리와 관련하여 코코아의 객체 소유 정책을 감안한 두가지 가정에 영향을 줄 수 있다.

[list type=”star”]

  • 메소드로부터 리턴되는 객체들은 이를 호출하는 객체의 범위내에서 유효하다. 다시 말해, 이 객체는 해당 범위내에서 해제되거나 값이 변경되지 않는다. (값이 변경되는 경우에는 문서에서 따로 설명한다.)
  • 메소드를 호출한 객체는 메소드로부터 객체를 받을 때, 이 객체를 명시적으로 유지(retain)하거나 복사하지 않는 한 이를 해제하지 않아야 한다.

[/list]

이러한 두가지 가정을 염두에 두고 title이라는 이름을 가진 NSString 인스턴스 변수의 두가지 접근자 메소드 구현을 살펴보자. Listing 3-7은 첫번째 방법을 보여준다.

getter 메소드가 단순히 인스턴스 변수의 참조를 리턴하는 것에 주목하자. 반면, setter 메소드는 더 복잡하다. 파라미터로 전달된 값이 현재 값과 다르다면, 새로운 값을 복사하기 전에 현재 값을 autorelease 한다. (autorelease 메시지를 보내는 것은 release 메시지를 보내는 것에 비해 스레드 환경에서 더 안정적이다.) 하지만, 이런 접근방식에는 여전히 잠재적인 위험이 존재한다. 만약 클라이언트가 getter 메소드에서 리턴된 값을 사용하고 있고, 그 사이 setter 메소드가 이전 NSString 객체를 autorelease 한다면 이 객체는 해제되고 제거되는 것일까? 클라이언트 객체가 사용중인 참조는 더 이상 유효하지 않다.

Listing 3-8은 getter 메소드에서 인스턴스 변수를 retain하고 autorelease 하는 방식으로 이 문제를 피할 수 있는 다른 구현을 보여준다.

Listing 3-5와 3-7에서 보여준 두가지 setter 메소드에서 새로운 NSString 인스턴스 변수는 유지(retain)되지 않고 복사된다. 유지(retain)되지 않는 이유는 무엇일까? 일반적인 규칙은 다음과 같다. 인스턴스 변수에 할당된 객체가 값 객체일 때, 즉, 문자열, 날짜, 숫자 등과 같은 속성을 나타내는 객체의 경우에는 복사해야 한다.

하지만, 객체가 NSView나 NSWindow와 같은 entity 객체라면 유지(retain)해야 한다. 엔티티 객체는 다양한 속성의 집합체이고 관계적인 특성을 가지며, 이 객체를 복사하는 것은 고비용의 작업이다. 객체가 값 객체인지 엔티티 객체인지 판단하는 한가지 방법은 객체의 값을 원하는지 객체 자체를 원하는지를 선택하는 것이다. 값을 원한다면 값 객체일 것이므로 복사해야 한다.

setter 메소드에서 인스턴스 변수를 유지(retain)하거나 복사해야 하는지를 결정하는 또 다른 방법은 인스턴스 변수가 특성(attribute)인지 또는 관계인지 결정하는 것이다. 이것은 특히 어플리케이션의 데이터를 나타내는 모델 객체에 해당된다. 특성은 본질적으로 값 객체와 동일한 것이다. 반면, 관계는 하나 이상의 다른 객체와의 관계 그 자체이다. 일반적으로 setter 메소드에서는 특성의 값은 복사하고 관계는 유지(retain)한다. 하지만, 관계는 요소의 수(cardinality)를 가지므로 1:1관계나 1:다 관계를 가질 수 있다. 보통 NSArray나 NSSet 인스턴스와 같은 컬렉션 클래스에 의해 표현되는 1:다 관계는 단순히 인스턴스 변수의 값을 유지하는 것 이상의 작업을 수행하는 setter 메소드를 필요로 하기도 한다. 자세한 내용은 Model Object Implementation Guide에서 다룬다.

[note color=”#ffdad1″]중요: getter 메소드에서 인스턴스의 값을 지연적으로(처음으로 요청되는 시점에) 읽도록 구현하고 있다면, 여기에서 관련된 접근자 메소드를 호출하지 않아야 한다. 대신, getter 메소드 에서는 인스턴스 변수의 값을 직접 설정해야 한다. [/note]

클래스의 접근자 메소드가 Listing 3-5나 3-7에서 보여준 것과 같이 구현되어 있다면, 클래스의 dealloc 메소드에서 인스턴스 변수를 해제하기 위해서 필요한 작업은 적절한 setter 메소드를 통해 nil로 설정하는 것 뿐이다.

 

Key-Value Mechanisms

key-value binding(KVB), key-value coding(KVC), key-value observing(KVO)과 같이 이름에 “key-value”가 포함되는 다양한 매커니즘은 코코아에서 중요한 부분이다. 이들은 바인딩과 같은 코코아 기술을 위한 기본적인 재료가 된다. 또한 어플리케이션이 AppleScript 명령에 응답할 수 있도록 만드는 최소한의 기능도 제공한다. 특히 KVC와 KVO는 커스텀 클래스 설계에서 중요한 고려사항이다.

[note color=”#fffcf0″]iOS Note: NSController 류의 클래스와 같이 바인딩에 사용되는 컨트롤러 클래스와 스크립트 기능은 AppKit에만 정의되어 있고, UIKit에서는 제공하지 않는다.[/note]

key-value라는 용어는 속성의 이름을 key로 사용해서 값을 얻어오는 기술을 의미한다. 이 용어는 객체 모델링 패턴에서 사용하는 단어이다. 객체 모델링은 관계형 데이터베이스를 기술할 때 사용되는 엔티티-관계 모델링으로부터 파생되었다. 객체 모델링에서, 객체(특히, MVC 패턴에서 데이터를 저장하는 모델 객체)는 거의 대부분 인스턴스 변수 형태의 속성을 가진다. 속성은 이름이나 색상과 같은 특성이나 하나 이상의 다른 객체에 대한 참조일 수 있다. 이러한 참조를 관계(relationship)라고 하며, 관계는 1:1 또는 1:다 관계가 될 수 있다. 프로그램 내에서 객체의 관계는 서로간의 관계를 통해 객체 그래프로 형성된다. 객체 모델링에서는 key path( . 으로 구분된 키 문자열)를 통해 객체 그래프 상의 관계를 탐색하고 객체의 속성에 접근할 수 있다.

[note color=”#fffcf0″]Note: 객체 모델링에 대한 자세한 설명은 “Object Modeling”에서 다룬다.[/note]

[list type=”star”]

  • KVB는 객체 사이에 바인딩을 추가하거나 제거하며 몇가지 비공식 프로토콜을 사용한다. 속성에 대한 바인딩은 반드시 객체와 해당 객체에 대한 키패스를 지정해야 한다.
  • KVC는 NSKeyValueCoding 비공식 프로토콜의 구현을 통해, 객체의 접근자 메소드를 직접 호출하지 않고 키를 통해 속성 값을 얻거나 설정할 수 있도록 해준다. (코코아는 이 프로토콜에 대한 기본 구현을 제공한다.) 일반적으로 키는 인스턴스 변수의 이름이나 접근자 메소드에 해당된다.
  • KVO는 NSKeyValueObserving 비공식 프로토콜의 구현을 통해 객체가 자신을 다른 객체의 감시자로 등록할 수 있도록 해준다. 감시되는 객체는 자신의 속성 중 하나가 변경될 때 자신의 감시자로 직접 알려준다. 코코아는 KVO와 호환되는 객체의 각 속성에 대해 자동적인 감시 통지(automatic observer notifications)를 구현한다.

[/list]

하위 클래스의 각 속성이 KVC의 요구사항을 만족시키도록 하기 위해서는 다음과 같이 해야 한다.

[list type=”star”]

  • key라는 이름의 속성이나 1:1 관계의 경우 key라는 getter 메소드와 setKey:라는 setter 메소드를 구현해야 한다. 예를 들어, 속성의 이름이 salary라면 접근자 메소드는 각각 salary와 setSalary: 일 것이다. (컴파일러는 선언된 속성을 통해 접근자를 생성할 때, 별다른 지시가 없다면 이러한 패턴을 따른다.)
  • 1:다 관계의 경우, 속성이 컬렉션 형태의 인스턴스 변수이거나 접근자 메소드에서 컬렉션을 리턴한다면 getter 메소드의 이름에 속성의 이름을 사용해야 한다. 속성이 가변객체이지만 getter 메소드에서 가변 객체를 리턴하지 않는다면, insertObject:in<Key>AtIndex: 메소드와 removeObjectFrom<Key>AtIndex: 메소드를 반드시 구현해야 한다. 인스턴스 변수가 컬렉션이 아니고 getter 메소드가 컬렉션을 리턴하지 않는다면, 반드시 다른 NSKeyValueCoding 메소드들을 구현해야 한다.

[/list]

automatic observer notification 기능에 만족하는 경우, 객체를 KVO에 호환되도록 만들기 위해서는 단순히 객체를 KVC에 호환되도록 하면 된다. 하지만, KVC를 직접 구현할 수도 있다.

 

Object Infrastructure

하위 클래스가 잘 설계되어 있다면, 이 클래스의 인스턴스는 코코아 객체에 기대되는 방식으로 잘 동작할 것이다. 객체를 사용하는 코드는 인스턴스를 다른 클래스의 인스턴스와 비교하고, 내용을 살펴보고, 유사한 기본 작업들을 수행할 수 있다.

커스텀 클래스는 다음과 같이 최상위 클래스와 기본 프로토콜에 있는 대부분의 메소드를 구현해야 한다.

[list type=”star”]

[/list]

클래스의 조상들이 공식 프로토콜을 따른다면, 반드시 이 클래스도 해당 프로토콜들을 적절히 따르도록 해야 한다. 즉, 프로토콜 메소드의 상위 클래스 구현만으로 부족하다면 클래스에서 이 메소드를 다시 구현해야 한다.

 

Error Handling

어떠한 프로그래밍 학문에서도 프로그래머가 오류를 적절히 처리해야 한다는 것은 당연한 것이다. 하지만, 어떤 것이 적절한 것인가에 대한 문제은 프로그래밍 언어, 어플리케이션 환경 등과 같은 요소에 따라 달라진다. 코코아는 하위 클래스에서 오류를 처리하는데 사용되는 자신만의 관습과 정책을 가지고 있다.

[list type=”star”]

  • 메소드 구현에서 발생한 오류가 시스템 오류이거나 Objective-C 런타임 오류라면 필요에 따라 예외를 발생시키고, 가능한 경우에는 이것을 지역적으로 처리한다.
    코코아에서 예외(exception)는 일반적으로 컬렉션의 범위를 벗어난 접근, 불변 객체의 변형 시도, 잘못된 메시지 전달, 윈도우 서버와의 연결 상실과 같은 프로그래밍 오류나 예상치 못한 런타임 오류를 위해 예약되어 있다. 미리 정의된 예외와 예외를 발생시키고 처리하는 절차와 API에 대한 정보는 에서 Exception Programming Topics 제공한다.
  • 예상 가능한 오류를 포함한 다른 종류의 오류들의 경우 nil, NO, NULL, 또는 0을 나타내는 값을 호출자로 리턴한다. 이러한 오류의 예는 파일 읽기나 쓰기 실패, 객체 초기화 실패, 네트워크 연결 실패, 컬렉션 내의 객체 위치 파악 실패 등이다. 오류에 대한 부가 정보를 전달해야 한다고 생각된다면 NSError 객체를 사용한다.NSError 객체는 오류 코드, 부가 정보를 담고 있는 딕셔너리 등 오류에 대한 정보들을 캡슐화 한 객체이다. nil이나 NO 등과 같이 직접 리턴되는 부정 값이 오류를 나타내는 주요한 수단이 되어야 한다. 만약 오류에 대한 구체적인 정보를 제공해야 한다면 메소드이 파라미터를 통해 NSError 객체를 간접적으로 리턴해야 한다.
  • 오류가 사용자의 선택이나 동작을 요구하는 경우에는 경고 대화상자를 표시해야 한다.
    OS X에서는 오류를 표시하고 사용자의 응답을 처리하기 위해 NSAlert 클래스의 인스턴스를 사용한다. iOS 경우 UIActionSheet 또는 UIAlertView 클래스를 사용한다.

[/list]

NSError 객체, 오류 처리, 오류 경고 표시 등에 대한 자세한 내용은 에서 Error Handling Programming Guide 제공한다.

 

Resource Management and Other Efficiencies

객체는 물론 이 객체를 사용하는 어플리케이션의 성능을 향상시키기 위해 할 수 있는 다양한 작업들이 존재한다. 이러한 작업들은 멀티스레딩부터 메모리 점유율을 감소시키기 위한 최적화까지 다양하다. Cocoa Performance Guidelines와 다른 성능 문서들을 읽어보면 이러한 주제에 대해 자세하게 배울 수 있다.

하지만, 고급 기능들을 적용하기 전에 아래에 나열된 간단한 지침들을 따르는 것만으로도 객체의 성능을  상당부분 향상시킬 수 있다.

[list type=”star”]

  • 리소스나 메모리는 실제로 필요한 시점이 되기 전까지 읽거나 할당하지 않아야 한다.
    nib 파일이나 이미지 같은 리소스들을 로드한 뒤 오랫동안 사용하지 않거나 전혀 사용하지 않는다면 어플리케이션의 비효율성을 증가시키게 된다. 프로그램의 메모리 점유율 역시 쓸데없이 비대해진다. 그러므로 리소스 로딩이나 메모리 할당은 즉시 필요한 경우에만 수행해야 한다.
    예를 들어, 어플리케이션의 환경설정 창이 별도의 nib 파일로 존재한다면, 사용자가 메뉴에서 환경설정을 처음으로 선택하기 전까지 이 파일을 읽지 않아야 한다. 메모리 할당에도 동일한 주의사항이 적용된다. 메모리가 실제로 사용되기 전까지 할당을 미루어야 한다. 이러한 lazy-loading, lazy-allocation은 쉽게 구현될 수 있는 기술이다. 예를 들어, 사용자의 첫번째 요청이 있을 때 인터페이스에 표시하기 위해 로드되는 이미지가 있다고 가정해보자. Listing 3-9는 한가지 방법을 보여준다. 여기에서는 이미지의 getter 메소드에서 이미지를 로딩하고 있다.
  • 저수준의 프로그래밍 인터페이스를 파고들지 말고 코코아 API를 사용해야 한다. 코코아 프레임워크는 가능한 견고하고 안전하며 효율적으로 동작하도록 많은 노력을 기울였다. 게다가 이러한 구현들은 쉽게 파악할 수 없는 내부적인 의존성도 관리해준다. 만약 특정 작업에 코코아 API 대신 저수준의 인터페이스를 사용해서 직접 만든 API를 사용하고자 한다면, 더 많은 코드를 작성해야 하고, 여기에는 더 많은 오류와 비효율성이 존재하게 될 것이다. 또한, 코코아를 활용함으로써 향후 개선사항에 잘 대처할 수 있고, 동시에 내부 구현을 변경할 필요가 없게 된다. 그러므로 코코아에서 필요한 기능을 제공하고 있다면 그것을 사용해야 한다.
  • 바람직한 메모리 관리 기술을 사용해야 한다.
    가비지 컬렉션을 사용하지 않기로 했다면, 커스텀 객체의 효율성과 견고함을 향상시키기 위해 수행할 수 있는 중요한 작업은 아마도 바람직한 메모리 관리 기술을 사용하는 것일 것이다. 모든 메모리 할당과 copy, retain 메시지는 release 메시지와 쌍을 이루어야 한다. 올바른 메모리 관리 기술과 정책에 익숙해져야 하며, 이러한 기술들을 연습하고 실제로 적용해보고, 관련 정책들을 실천해야 한다. 자세한 정보는 Advanced Memory Management Programming Guide 에서 제공한다.

[/list]

 

Functions, Constants, and Other C Types

Objective-C는 ANSI C의 상위집합이기 때문에 함수, typedef 구조체, enum 상수, 매크로 등과 같은 C 자료형을 코드에서 사용할 수 있다. 중요한 설계 쟁점은 커스텀 클래스의 인터페이스와 구현에서 이러한 자료형을 사용하는 시점과 방식이다.

아래의 목록은 커스텀 클래스의 정의에서 C 자료형을 사용하기 위한 몇가지 지침을 제공한다.

[list type=”star”]

  • 자주 요청되지만 하위 클래스에서 재정의 될 필요가 없는 기능의 경우 메소드보다는 함수를 정의한다. 이것은 성능 때문이다. 이러한 경우에는 함수가 클래스 API의 일부가 되는 것보다는 비공개하는 것이 최선이다. 또한, 어떠한 클래스와도 연관되지 않은 동작이나 단순한 형태의 작업을 수행하기 위해서 함수를 구현할 수도 있다. 하지만, 전역적으로 사용되는 기능의 경우, 확장성 면에서 싱글톤 패턴을 사용하는 클래스를 만드는 것이 더 나을수도 있다.
  • 다음과 같은 상황에서는 간단한 클래스 대신 구조체를 정의한다.1. 필드를 확장하지 않을 것으로 예상되는 경우
    2. 성능상의 이유로 모든 필드가 public 인 경우
    3. dynamic 필드가 존재하지 않는 경우
    4. 서브클래싱과 같은 객체지향 기술을 사용하지 않을 경우
  • 위의 모든 상황에 해당되는 경우라고 하더라도 Objective-C 코드에서 구조체를 사용하는 가장 주요한 이유는 성능이다.
    다시 말해, 성능상의 문제가 없다면 간단한 클래스를 사용하는 것이 바람직하다.
  • #define 상수 대신 enum 상수를 선언해야 한다. enum 상수는 타이핑에 더 적합하며, 디버거에서 상수의 값을 확인할 수 있다.

[/list]

 

When the Class Is Public (OS X)

어플리케이션에서 사용하는 컨트롤러 클래스와 같이 직접 사용할 목적으로 커스텀 클래스를 정의하는 경우에는, 이 클래스에 대해서 잘 알고 있고 필요한 경우 직접 수정할 수 있기 때문에 많은 융통성을 가지게 된다. 하지만, 커스텀 클래스가 다른 개발자들이 사용할 상위 클래스가 될 수도 있다면 좀 더 신중하게 설계해야 한다.

다음과 같이 공용 코코아 클래스를 개발을 위한 몇가지 지침이 존재한다.

[list type=”star”]

  • 하위 클래스에서 접근해야 하는 인스턴스 변수의 접근 범위를 @protected로 지정해야 한다.
  • 향후 버전의 바이너리 호환성을 위해서 한두 개 정도의 예약된 인스턴스 변수를 추가해야 한다.
  • MRC 코드에서는 접근자 메소드가 리턴되는 객체의 메모리를 올바르게 처리하도록 구현해야 한다. (“Storing and Accessing Properties” 참고) 클래스의 사용자들은 getter 메소드에서 자동해제(autoreleased)된 객체를 리턴받아야 한다.
  • Coding Guidelines for Cocoa 에서 설명하고 있는 이름정의 지침을 따라야 한다.
  • 클래스를 문서화 해야 한다. 최소한 헤더 파일에 주석이라도 달아두어야 한다.

[/list]

 

Multithreaded Cocoa Programs

멀티스레딩을 통해 프로그램의 성능을 최적화하는 것은 오랜기간 표준적인 프로그래밍 기술이었다. 데이터 처리와 I/O 작업을 별도의 스레드에서 실행하고 메인 스레드는 사용자 인터페이스 관리를 전담하게 함으로써 어플리케이션의 응답성을 향상시킬 수 있다. 근래에는 멀티코어 컴퓨터와 대칭형 다중처리(SMP)의 등장으로 멀티스레딩의 중요성이 더욱 부각되고 있다. 이러한 시스템에서의 동시 처리는 동일한 프로세서에 있는 다중 스레드 뿐만 아니라 다수의 프로세스에 있는 스레드 실행에 대한 동기화도 포함하고 있다.

프로그램에 멀팅스레딩을 적용하는 것이 리스크나 비용을 발생시키지 않는 것은 아니다. 스레드가 프로세서의 가상 메모리 공간을 공유하기는 하지만, 여전히 메모리 점유율을 증가시킨다. 각 스레드가 추가적인 데이터 구조와 자신만의 스택 공간을 필요로하기 때문이다. 또한 멀티스레딩은 여러 스레드에서 공유하는 메모리에 대한 접근을 동기화하기 위해 더 복잡한 설계를 요구한다. 이러한 설계는 프로그램을 유지보수하거나 디버그하는 것을 어렵게 만든다. 게다가, 락(lock)을 남용하는 멀티스레딩 설계는 공유 리소스에 대한 경합으로 인해 실제로 성능을 감소시킬 수 있다. 멀티스레딩을 사용하도록 설계하는 것은 종종 성능과 보안 사이의 적절한 균형을 찾아야 하며, 데이터 구조와 의도된 사용 패턴에 대해 신중히 고려해보아야 한다. 실제로, 어떤 상황에서는 멀티스레딩을 전혀 사용하지 않고 메인 스레드에서 모든 작업을 실행하는 것이 가장 좋은 해결책이 되기도 한다.

 

Multithreading and Multiprocessing Resources

코코아는 멀티스레딩 프로그램에서 유용하게 사용할 수 있는 몇가지 클래스들을 제공한다.

Threads and Run Loops

스레드는 코코아에서 NSThread 클래스의 객체로 표현된다. 이 클래스는 POSIX 스레드를 기반으로 구현되었다. NSThread의 인터페이스가 POSIX 스레드만큼 다양한 옵셔을 제공하는 것은 아니지만 대부분의 멀티스레딩을 구현하기에는 충분하다.

런루프는 이벤트 전달을 위한 아키텍처의 핵심 요소이다. 런루프는 입력 소스(일반적으로 포트 또는 소켓)와 타이머를 가진다. 또한, 런루프가 감시할 입력 소스를 지정하기 위한 입력 모드도 가진다. NSRunLoop 클래스의 객체들은 코코아에서 런루프를 나타낸다. 각 스레드는 자신의 런루프를 가지며, 메인 스레드의 런루프는 프로세스가 실행을 시작할 때 자동적으로 설정되고 실행된다. 하지만, 부가적인 스레드들은 반드시 자신의 런루프를 직접 실행해야 한다.

 

Operations and Operation Queues

NSOperation, NSOperationQueue 클래스를 통해 하나 이상의 캡슐화된 작업의 실행을 관리할 수 있다. NSOperation 객체는 한번만 실행될 수 있는 개별 작업을 나타낸다. 일반적으로 NSOperationQueue 객체를 통해 우선순위와 내부적인 의존성에 따라 순서대로 작업을 예약한다. 의존성을 가지는 작업 객체는 자신이 의존하고 있는 모든 작업 객체들의 실행이 완료되기 전까지 실행되지 않는다. 그리고 명시적으로 제거되거나 실행이 완료되기 전까지 큐에 남아있는다.

[note color=”#fffcf0″]Note: NSOperation, NSOperationQueue 클래스는 OS X 10.5 버전에서 소개되었고, iOS SDK 최초 공개버전에 포함되었다. 이전 버전의 OS X에서는 사용할 수 없다.[/note]

 

Locks and Conditions

락은 서로 경합하는 스레드 사이의 리소스 보초(resource sentinels)와 같이 동작한다. 이들은 스레드들이 공유된 리소스에 동시에 접근하는 것을 방지해준다. 코코아에 있는 몇가지 클래스들은 서로 다른 형태의 락 객체들을 제공한다.

[list type=”star”]

  • NSLock : 뮤텍스 락(mutex lock)을 구현한다. 이것은 공유된 리소스에 대한 상호 베타적인 접근을 강제한다. 하나의 스레드만이 락을 획득해서 리소스를 사용하므로 다른 스레드들은 이 스레드가 락을 해제하기 전까지 블록킹되어 리소스를 사용하지 못한다.
  • NSRecursiveLock : 재귀적 락(recursive lock)을 구현한다. 이것은 현재 자신을 소유하고 있는 스레드에 의해 여러번 획득될 수 있는 뮤텍스 락이다. 이 락은 재귀적으로 획득된 모든 락이 해제될 때까지 존재한다.
  • NSConditionLock, NSCondition : 이 두 클래스는 condition lock을 구현한다. 이러한 형태의 락은 프로그램에서 정의한 상황을 기반으로 하는 잠금 동작을 구현하기 위해서 세마포어와 뮤텍스 락을 함께 사용한다. 하나의 스레드는 다른 스레드가 신호를 보내기 전까지 실행을 중지한채로 대기하고 있다가, 신호가 전달되는 시점에 블록을 해제하고 실행을 재개한다. 다수의 스레드에 동시에 신호를 전달할 수도 있다.

[/list]

NSConditionLock, NSCondition은 모두 pthread 컨디션 락의 객체지향적 구현이므로 사용목적이나 구현에 있어서 유사점을 가진다. NSConditionLock의 구현이 좀 더 완벽하지만 유연성은 떨어진다. 이것은 세마포어 시그널링(semaphore signaling)과 뮤텍스 락(mutex locking)을 모두 제공한다. 반면, NSCondition의 구현은 pthread 컨디션 변수와 뮤텍스 락을 포함하고 있으며, 잠금과 시그널링, predicate state management를 직접 수행해야 한다. 그러나 NSConditionLock에 비해 훨씬 유연하다. 이 접근법이 pthread의 구현 패턴을 더 밀접하게 따른다.

 

Interthread Communication

프로그램이 다수의 스레드를 가지고 있는 경우에는 스레드 사이에 통신을 위한 수단이 필요하다. 컨디션 락은 pthread 기반의 컨디션 시그널링을 스레드 사이의 통신 수단으로 사용한다. 하지만 코코아는 스레드 사이의 통신을 위한 다른 리소스들을 제공한다.

NSMachPort와 NSMessagePort 클래스의 객체들은 Mach port를 통한 스레드간 통신을 가능하게 해준다. NSSocketPort 클래스의 객체들은 BSD 소켓을 통한 스레드간 통신과 프로세스간 통신을 가능하게 해준다. NSObject 클래스는 부가적인 스레드가 메인 스레드와 통신하고 데이터를 보낼 수 있도록 해주는 performSelectorOnMainThread:withObject:waitUntilDone: 메소드를 제공한다.

 

Multithreading Guidelines for Cocoa Programs

프로그램이 코코아 프레임워크나 다른 프레임워크를 사용하는가에 관계없이 다수의 스레드를 사용하는 것이 적절한 것인가를 결정할 수 있는 몇가지 일반적인 견해들이 존재한다. 스스로에게 다음과 같은 질문을 해보는 것으로 시작할 수 있다. 다음 질문 중 하나에 대한 대답이 yes라면 멀티스레딩을 고려해 보아야 한다.

[list type=”star”]

  • 어플리케이션에 사용자 인터페이스를 느리게 할 수 있는 CPU 작업이나 I/O 작업이 존재하는가?메인 스레드는 어플리케이션의 사용자 인터페이스를 관리한다. 그러므로 어플리케이션은 어플리케이션의 데이터 모델과 관련된 작업들을 하나 이상의 부가적인 스레드에서 실행할 수 있다. 코코아 어플리케이션에서 컨트롤러 객체들은 사용자 인터페이스 관리와 직접적으로 관련되기 때문에 메인 스레드에서 실행해야 할 것이다.
  • 프로그램이 다수의 입출력 소스를 가지고 있는가? 그리고 이들을 동시에 처리할 수 있는가?
  • 프로그램이 수행하는 작업을 다수의 CPU 코어로 분산시키고자 하는가?

[/list]

이러한 상황 중 하나가 어플리케이션에 적용된다고 해보자. 이전에도 언급했듯이, 멀티스레딩은 그 이점만큼 비용과 리스크도 가질 수 있기 때문에, 멀티스레딩의 대안을 고려해보는 것도 가치있는 일이다. 예를 들어, 비동기 처리(asynchronous processing)를 시도해 볼 수 있다.

멀티스레딩이 적합한 상황이 있는 것처럼, 그렇지 못한 상황도 존재한다.

[list type=”star”]

  • 짧은 처리 시간을 요구하는 작업
  • 반드시 연속적으로 수행되어야 하는 작업
  • 하위 시스템(Underlying subsystems)은 스레드에 안전하지 않다.

[/list]

마지막 항목은 스레드 안정성에 관한 중요한 문제를 제기한다. 멀티스레딩을 구현하는 코드가 스레드에 안전하도록 준수해야하는 몇가지 지침들이 존재한다. 이러한 지침 중 일부는 일반적인 것들이지만, 또 다른 일부는 코코아 프로그램에 한정된 것들이다. 일반적인 지침들은 다음과 같다.

[list type=”star”]

  • 가능한 스레드 사이에서 데이터를 공유하는 것을 피해야 한다. 대신 각 스레드에 객체 복사본을 제공하고 트렌잭션 기반의 모델을 통해 변경들을 동기화해야 한다. 가능하면 리소스 경합상황을 최대한 줄여야 한다.
  • 올바른 단계에서 데이터 잠금을 사용해야 한다. 락은 멀티스레딩 코드에서 기본적인 요소이지만, 성능상의 병목형상을 발생시키고 잘못된 단계에서 사용하는 경우 예상대로 동작하지 않을 수도 있다.
  • 스레드에서 로컬 예외를 잡아야(catch) 한다. 각 스레드는 자신만의 호출 스택을 가지고 있으며, 로컬 예외를 처리하고 리소스를 정리할 책임을 가진다. 예외는 처리를 위해 메인 스레드나 다른 스레드로 전달(throw)될 수 없다. 예외를 잡아서 처리하지 못한다면 스레드나 프로세스가 비정상적으로 종료될 수도 있다.
  • 보호된 코드에서는 volatile 변수의 사용을 피해야 한다. 

[/list]

다음 지침들은 코코아 프로그램에 한정된 것이다.

[list type=”star”]

  • 코드에서는 가능한 불변 객체(immutable objects)를 사용해야 한다.값이나 내용을 변경할 수 없는 불변 객체는 일반적으로 스레드에 안전하다. 그러나 가변 객체(Mutable objects)는 대부분 스레드에 안전하지 않다. 이러한 지침에 따라 메소드 호출에서 리턴되는 객체의 가변성을 중요하게 생각해야 한다. 만약 불변형으로 선언된 객체를 전달받았지만, 실제로 이 객체가 가변형이고, 그에 따라 값을 변경하게 된다면 프로그램에 잘못된 결과를 초래할 수 있다.
  • 코코아 프로그램의 메인 스레드는 이벤트를 전달받아 프로그램의 각 부분으로 전달하는 역할을 담당한다. 이벤트 처리를 메인 스레드가 아닌 다른 스레드에서도 처리할 수 있는데, 이 경우에는 반드시 모든 이벤트 처리 코드를 해당 스레드에 두어야 한다. 이벤트 처리를 서로 다른 스레드로 분산시킬 경우 사용자 이벤트가 올바른 순서로 전달되지 않게 된다.
  • 2차 스레드에서 OS X용 코코아 어플리케이션의 그리기 작업을 수행한다면, 모든 그리기 코드를 lockFocusIfCanDraw 메소드와 unlockFocus 메소드 사이에 두어야 한다.

[/list]

 

Are the Cocoa Frameworks Thread Safe?

프로그램이 스레드에 안전하도록 작성하기 전에 프로그램에서 사용하는 프레임워크의 스레드 안정성에 대해 이해하는 것은 필수이다.  코코아 프레임워크의 주요한 프레임워크인 Foundation과 AppKit은 스레드에 안전한 부분과 그렇지 않은 부분을 가지고 있다.

NSArray, NSDictionary와 같은 불변형 컬렉션 객체와 NSString, NSNumber, NSException과 같이 원시값이나 구조체를 캡슐화하는 불변형 객체들은 일반적으로 스레드에 안전하다. 반대로, 가변형 객체들은 스레드에 안전하지 않다. NSTask, NSRunLoop, NSPipe, NSHost, NSPort 등과 같이 시스템 수준의 엔티티를 나타내는 다양한 객체들도 스레드에 안전하지 않다. (반면, NSThread와 락 클래스들은 스레드에 안전하다)

AppKit에서 NSWindow 객체로 표현되는 창은 일반적으로 스레드에 안전하기 때문에 2차 스레드에서 창을 생성하고 관리할 수 있다. 하지만, NSEvent 객체로 표현되는 이벤트는 동일한 스레드 내에서만 안전하게 처리될 수 있다. 여러 스레드에서 동시에 사용하게 되는 경우 이벤트가 올바른 순서로 전달되지 않는 리스크가 발생한다. 비록 뷰와 관련된 작업들은 메인 스레드에서 수행되어야 하지만, 그리기와 NSView 객체들은 일반적으로 스레드에 안전하다.

모든 UIKit 객체들은 메인 스레드에서만 사용되어야 한다.

 

 

(이 글은 Cocoa Fundamentals Guide 중 Adding Behavior to a Cocoa Program 부분을 번역한 글입니다. 개인적인 학습목적으로 번역한 것이기 때문에 잘못 번역된 부분이나 어색한 문장이 많이 있습니다.^^;;)

Filed under: Apple Developer Document, iOS