CLOSE SEARCH

Cocoa Fundamentals Guide – Cocoa Objects

Cocoa Objects는 Objective-C 객체의 특징은 무엇이고, Objective-C 언어가 코코아 소프트웨어 개발에 어떠한 이점을 가져오는지에 대해서 설명한다. 또한 Objective-C를 통해 객체로 메시지를 보내는 방법과 이러한 메시지에서 리턴되는 값을 처리하는 방법을 보여준다. (Objective-C는 간결한 언어이므로 이러한 작업은 어렵지 않다.) 이번 장은 최상위 클래스인 NSObject에 대해 설명하고, 객체의 생성, introspection, 객체의 생명주기 관리를 위해 프로그래밍 인터페이스를 사용하는 방식을 보여준다.

A Simple Cocoa Command-Line Tool

OS X의 Foundation 프레임워크를 통해 생성된 간단한 명령줄 도구 프로그램으로 시작해보자. 임의의 단어들을 파라미터라고 가정하면 이 프로그램은 중복되는 단어를 삭제하고, 남아있는 단어들을 알파벳 순서로 정렬한 다음 표준 출력을 통해 보여준다. Listing 2-1은 이 프로그램의 실행 결과를 보여준다.

Listing 2-2는 이 프로그램의 Objective-C 코드를 보여준다.

이 코드는 몇가지 객체들을 사용한다. 메모리 관리를 위해 자동해제 풀(autorelease pool)을 사용하고, 파라미터로 전달된 단어들의 uniquing(중복 객체를 제거하는 것)과 정렬을 위해 컬렉션 객체를 사용하며, 최종 배열에 있는 요소들을 순회하기 위해 열거자 객체를 사용하고 표준 출력을 통해 배열의 내용을 출력한다.

이 코드에서 주목하게 될 첫번째 사실은 코드가 일반적인 ANSI C 버전과 동일한 기능을 구현하고 있지만 코드의 길이가 비교적 짧다는 것이다. 이 코드의 대부분이 이상하게 보일수도 있지만, 많은 요소들이 ANSI C에서 익숙한 것들이다. 이러한 요소에는 할당 연산자, while 구문, C 라이브러리 호출(printf), 기본 스칼라 자료형이 포함된다. Objective-C는 분명히 ANSI C를 토대로 한다.

이번 장의 나머지 부분에서는 이 코드의 Objective-C 요소들을 살펴본다. 이전에 Objective-C 코드를 본 적이 없다면 이 예제에 있는 코드가 만만찮게 복잡하고 불분명해 보일수도 있지만 이러한 생각은 금세 사라질 것이다. Objective-C는 실제로 단순하고 우아한 프로그래밍 언어이며 배우기 쉽고 직관적으로 사용할 수 있다.

 

Object-Oriented Programming with Objective-C

코코아는 패러다임과 매커니즘부터 이벤트 기반 아키텍처에 이르기까지 모든 부분이 객체지향적이다. 코코아를 위한 개발 언어인 Objective-C 역시 ANSI C에 바탕에 두고 있다는 사실에도 불구하고 완벽하게 객체지향적이다. Objective-C는 메시지 전달에 대한 런타임 지원을 제공하고 새로운 클래스들을 정의하기 위한 구문 규칙을 지정한다. 이 언어는 C++, Java와 같은 다른 객체지향 언어에서 볼 수 있는 대부분의 추상화와 매커니즘을 지원한다. 여기에는 상속, 캡슐화, 재사용성, 다형성이 포함된다.

그러나 Objective-C는 몇가지 중요한 방식에서 다른 객체지향 언어와 차이점을 가진다. 예를 들어, C++와 달리 Objective-C는 연산자 오버로딩, 템플릿, 다중 상속을 허용하지 않는다.

Objective-C가 이러한 기능들을 가지고 있지는 않지만, 객체지향 프로그래밍 언어로써의 강점은 이러한 것들을 상쇄할 수 있는것 이상이다. Objective-C의 특별한 능력에 대한 내용이 앞으로 계속 이어진다.

[note color=”#fffae5″]Further Reading: 이번 섹션에 포함된 대부분의 내용들은 The Objective-C Programming Language에 있는 내용들을 요약한 것이다. 이 문서를 통해 Objective-C에 대한 상세하고 광범위한 정보를 얻을 수 있다.[/note]

The Objective-C Advantage

당신이 객체지향 개념들을 처음 접하는 절차지향 프로그래머라면, 객체는 기본적으로 연관된 함수를 가지는 구조체라고 생각하는 것이 도움이 될 수도 있다. 이러한 개념은 런타임 구현이라는 측면에서 실제 개념으로부터 동떨어진 것은 아니다.

모든 Objective-C 객체는 첫번째 멤버가 isa 포인터인 데이터 구조체를 은닉한다. (나머지 멤버들은 객체의 클래스와 상위 클래스에 의해 정의된다.) isa 포인터는 이름에서 유추할 수 있듯이, 객체의 클래스를 가리킨다. 클래스 객체는 기본적으로 자신이 구현하는 메소드에 대한 포인터로 구성되어 있는 dispatch table을 가진다. 이 테이블에는 상위 클래스에 대한 포인터도 포함되어 있다. 객체는 이러한 참조 체인을 통해 자신의 클래스에 있는 메소드 구현과 자신의 모든 상위 클래스에 있는 메소드 구현에 접근한다. isa 포인터는 메시지 전달 매커니즘과 코코아 객체의 역동성에 있어서 매우 중요한 요소이다.

Figure 2-1  An object’s isa pointer

Figure 2-1  An object’s isa pointer

이렇게 객체의 이면을 살펴보는 것은 Objective-C 런타임에서 메시지 전달, 상속 등에서 어떠한 일들이 일어나는지에 대해 아주 단순화된 관점을 제공한다. 하지만 이러한 정보는 Objective-C의 주요한 강점과 역동성을 이해하는데 필수이다.

 

The Dynamism of Objective-C

Objective-C는 매우 동적인 언어이다. 이러한 역동성은 프로그램을 컴파일과 링크의 제약으로부터 해방시켜주며 심볼 분석(symbol resolution)에 대한 대부분의 의무를 실행시점(runtime)으로 이동시킨다. Objective-C가 다른 프로그래밍 언어에 비해 더욱 동적인 것은 이 언어의 역동성이 다음과 같은 세가지 요소로부터 나오기 때문이다.

[list style=”star”]

  • Dynamic Typing : 런타임에 객체의 클래스를 결정
  • Dynamic Binding : 런타임에 호출될 메소드를 결정
  • Dynamic Loading : 런타임에 새로운 모듈 추가

[/list]

Objective-C는 동적 타이핑을 위해 모든 코코아 객체를 나타낼 수 있는 id 자료형을 사용하고 있다. 이러한 일반 객체 형식의 전형적인 사용법은 Listing 2-2에 있는 코드 중 아래와 같은 부분에서 확인할 수 있다.

id 자료형은 런타임에 어떤 형식의 객체로도 대체될 수 있다. 이를 통해 런타임 요소들이 코드에서 사용되고 있는 객체의 종류를 결정할 수 있다. 동적 타이핑은 객체들이 정적인 방식으로 인코딩 되도록 강제하기 보다는 런타임에 결정될 객체들 사이의 조합을 허용한다. 컴파일 시점에 진행되는 정적 형식 확인(Static type checking)은 더욱 엄격한 데이터 무결성을 유지할 수 있을지도 모르지만, 동적 타이핑은 무결성 대신 더욱 강력한 유연성을 제공한다. 그리고 객체의 introspection을 통해 런타임에 객체의 형식을 확인할 수 있으므로 특정 작업에 대한 객체의 적합성을 검증할 수 있다. (물론, 필요한 경우에는 언제라도 객체의 형식을 정적으로 확인할 수 있다.)

동적 타이핑은 동적 바인딩에 바탕을 제공한다. 동적 바인딩은 동적 타이핑이 객체의 class membership 분석을 런타임으로 연기하는 것과 마찬가지로 어떤 메소드가 호출될지에 대한 선택을 런타임으로 연기한다. 컴파일이 진행되는 동안에 메소드 호출과 코드는 서로 결합되지 않는다. 이들은 메시지가 실제로 전달되는 시점에만 결합된다. 동적 타이핑과 동적 바인딩을 통해 코드를 실행할 때마다 서로 다른 결과를 얻을 수 있다. 런타임 요소들은 어떤 receiver가 선택되고 어떤 메소드가 호출되어야 하는지를 선택한다.

런타임의 메시지 전송 장치는 동적 바인딩을 가능하게 한다. 런타임 시스템은 동적으로 타이핑된 객체로 메시지를 보낼 때 receiver의 isa 포인터를 통해 객체의 클래스를 찾고, 발견된 클래스의 dispatch table에서 호출할 메소드 구현을 선택한다. 이 메소드는 메시지와 동적으로 결합된다. 그리고 동적 바인딩의 이점을 활용하기 위해 Objective-C 코드에서 특별한 작업을 할 필요는 없다. 동적 바인딩은 메시지를 보낼 때마다, 특히 동적으로 타이핑된 객체로 보낼 때마다 항상 수행된다.

동적 로딩은 Objective-C의 런타임 지원에 의존하는 기능이다. 코코아 프로그램은 동적 로딩을 통해 시작 시점에 모든 프로그램 구성요소들을 읽어오지 않고, 필요한 시점에 실행 코드와 리소스들을 읽어올 수 있다. 실행 코드는 흔히 프로그램의 런타임 이미지에 통합될 새로운 클래스들을 포함한다. 코드와 지역화된 리소스들은 모두 번들로 묶어지고 Foundation의 NSBundle 클래스에 정의되어 있는 메소드를 통해 명시적으로 로드된다.

프로그램 코드와 리소스에 대한 lazy-loading(지연된 읽기)은 시스템에 적은 양의 메모리만을 요청하므로 전체적인 성능을 향상시킨다. 더욱 중요한 것은 동적 로딩이 어플리케이션을 확장할 수 있도록 해준다는 것이다. 어플리케이션에 플러그인 아키텍처를 마련할 수도 있다. 이것은 어플리케이션이 출시된 후 몇달 혹은 몇년 뒤에 당신이나 다른 개발자들이 동적으로 읽을 수 있는 새로운 모듈을 추가하여 기능을 변경할 수 있도록 해준다. 설계가 정상적이라면 이러한 모듈에 있는 클래스들은 구현을 캡슐화하고 자신만의 이름공간을 가지기 때문에 이미 존재하는 클래스들과 충돌을 일으키지 않을 것이다.

 

Extensions to the Objective-C Language

Objective-C는 네가지 종류의 확장을 제공하며, categories, protocols, declared properties, fast enumeration이 여기에 속한다. 일부 확장은 메소드를 정의하고 이들을 클래스와 연관시키는 방식에 있어서 이전과는 다른 기술을 채용하고 있다. 다른 것들은 객체의 속성을 선언하고 접근하기 위한 편리한 방식과 컬렉션 객체를 빠르게 열거하고, 예외를 처리하고, 기타 다른 작업들을 수행하는데 사용되는 편리한 방법을 제공한다.

 

Categories

카테고리는 서브클래싱 없이 클래스에 메소드를 추가할 수 있는 수단을 제공한다. 카테고리에 있는 메소드는 프로그램의 범위 내에서 클래스 형식의 일부가 되고 클래스의 모든 하위 클래스에 의해 상속된다. 런타임 시점에 클래스가 가진 초기 메소드와 카테고리를 통해 추가된 메소드에는 차이가 없다. 클래스의 모든 인스턴스에 메시지를 보내서 카테고리에 정의되어 있는 메소드를 호출할 수 있다.

카테고리는 클래스에 동작을 추가하는데 사용할 수 있는 좀 더 편리한 방법이다. 그리고 카테고리를 통해 연관된 메소드를 서로 다른 카테고리로 묶는 방식으로 메소드들을 구분할 수도 있다. 특히 큰 클래스를 정리하는데 유용하다. 다수의 개발자들이 동일한 클래스를 개발하고 있다면 카테고리를 서로 다른 소스 파일에 둘 수도 있다.

카테고리는 선언하고 구현하는 방식은 서브클래싱과 매우 유사하다. 문법적으로 봤을 때 유일한 차이점은 카테고리의 이름뿐이다. 카테고리 이름은 @interface나 @implementation 지시어 뒷부분에 위치하는 괄호내부에 작성한다. 예를 들어, NSArray 클래스에 컬렉션에 대한 설명을 좀 더 구조적인 방식으로 출력하는 메소드를 추가하길 원하다고 가정해보자. 카테고리의 헤더 파일에서 아래와 유사한 선언 코드를 작성할 것이다.

그런 다음 구현 파일에서 다름과 같은 코드를 작성할 것이다.

카테고리에는 몇가지 제약도 존재한다. 카테고리를 통해 클래스에 새로운 인스턴스 변수를 추가할 수는 없다. 카테고리 메소드가 기본 메소드를 재정의할 수 있지만 추천하는 방식은 아니다. 그 이유 중 하나는 카테고리 메소드가 클래스 인터페이스의 일부분이 되기 때문에, 이미 정의되어 있는 동작을 실행하기 위해서 super에 메시지를 보낼 방법이 없어진다. 클래스의 기본 메소드가 동작하는 방식을 변경해야 한다면 서브클래싱을 하는 것이 더 좋다.

NSObject 클래스에 메소드를 추가하는 카테고리를 정의할 수도 있다. 이렇게 추가된 메소드들은 코드에서 사용되는 모든 인스턴스 객체와 클래스 객체에서 사용할 수 있다. 비공식 프로토콜(Informal protocol)은 NSObject의 카테고리로 선언된다. 하지만 메소드가 널리 공개되기 때문에 편리한 만큼 위험도 존재한다. NSObject의 카테고리를 통해 모든 객체에 추가한 동작은 충돌, 데이터 오염 등과 같이 예상할 수 없는 결과를 만들어 낼 수도 있다.

 

Protocols

프로토콜은 자바의 인터페이스와 매우 유사하다. 두가지 모두 클래스에서 구현해야 하는 인터페이스를 나타내는 메소드 선언의 목록이다. 프로토콜에 있는 메소드는 다른 클래스의 인스턴스에 의해 보내진 메시지를 통해 호출된다.

프로토콜의 주요한 가치는 카테고리와 마찬가지로 서브클래싱의 대체수단이 될 수 있다는 것이다. 이것은 인터페이스를 공유할 수 있도록 함으로써 C++의 다중상속에서 얻을 수 있는 것과 유사한 이점을 제공한다. 프로토콜은 클래스가 자신의 구현을 숨기면서 인터페이스를 선언할 수 있는 수단을 제공한다. 이 인터페이스는 클래스가 제공하는 모든 서비스를 노출시키거나 특정 범위만을 노출시킬 수도 있다. 클래스 계층구조에 있는 다른 클래스들은 어떠한 상속관계에 있는가에 관계없이 프로토콜의 메소드를 구현할 수 있고 공개된 서비스에도 접근할 수 있다. 프로토콜을 사용하면 다른 클래스에 대한 정보를 모르는 클래스도 프로토콜에 의해 정해진 특정한 목적으로 통신할 수 있다.

프로토콜에는 공식 프로토콜(formal protocol)과 비공식 프로토콜(informal protocol)이 있다. 비공식 프로토콜은 이전에 카테고리를 설명하면서 NSObject 클래스의 카테고리라고 잠깐 소개했었다. 결과적으로 NSObject로부터 파생되는 모든 객체는 카테고리에 선언되어 있는 인터페이스를 암시적으로 적용하게 된다. 클래스는 비공식 프로토콜을 사용하기 위해서 여기에 포함된 모든 메소드를 구현할 필요는 없고 자신이 필요한 메소드만 구현해도 된다. 비공식 프로토콜을 선언하고 있는 클래스는 대상 객체로 프로토콜 메시지를 보내기 전에 반드시 이 객체로 respondsToSelector: 메시지를 보낸 후 YES 리턴값을 얻어와야 한다. (대상 객체가 메소드를 구현하고 있지 않은 상태에서 프로토콜 메시지를 바로 보내면 런타임 예외가 발생할 것이다.)

공식 프로토콜은 일반적으로 프로토콜이라는 용어로 부른다. 이것은 클래스가 공개되는 서비스의 메소드 목록을 공식적으로 선언할 수 있게 해준다. Objective-C 언어와 런타임 시스템은 공식 프로토콜을 지원하며, 컴파일러는 프로토콜에 기반한 형식 확인이 가능하고, 객체는 런타임에 프로토콜 적용여부를 확인할 수 있다. 공식 프로토콜은 고유의 용어와 문법을 가진다. 용어는 공급자와 소비자에 따라 다르게 사용된다.

[list style=”star”]

  • 공급자(일반적으로 클래스)는 공식 프로토콜을 선언한다.
  • 소비자 클래스는 공식 프로토콜을 적용하고 프로토콜의 모든 필수 메소드를 구현한다.
  • 프로토콜을 적용하거나 프로토콜이 적용된 클래스를 상속하는 클래스는 “공식 프로토콜을 따른다”고 한다.

[/list]

프로토콜의 선언과 적용에는 각각 고유의 문법이 사용된다. 프로토콜을 선언할 때는 반드시 @protocol 지시자를 사용해야 한다. 다음 예제는 NSCoding 프로토콜의 선언을 보여준다.

Objective-C 2.0은 공식 프로토콜에 부가적인(optional) 프로토콜을 선언할 수 있도록 개선되었다. Objective-C 1.0에서는 프로토콜을 따르는 클래스가 프로토콜에 선언되어 있는 모든 메소드를 구현해야 했었다. 프로토콜 메소드는 Objective-C 2.0에서도 여전히 기본적으로 필수 메소드이며 @required 지시어를 통해 필수로 표시할 수도 있다. 하지만 @optional 지시어를 통해 부가적인 메소드로 표시할 수도 있다. 이 지시어 뒤에 위치하는 모든 메소드는 또 다른 @required 지시어를 만나기 전까지 부가적인 메소드가 된다. 다음과 같은 선언 코드를 살펴보자.

일반적으로 프로토콜 메소드를 선언하는 클래스는 해당 메소드를 구현하지 않는다. 하지만, 이 클래스는 프로토콜을 따르는 클래스의 인스턴스에서 이 메소드를 호출해야 한다. 부가적인 메소드를 호출하기 전에는 respondsToSelector: 메소드를 통해 구현 여부를 검증해 보아야 한다.

클래스는 프로토콜을 적용할 때 @interface 지시자의 마지막 부분에 프로토콜의 목록을 꺽쇠기호로 감싸준다. 두개 이상의 프로토콜을 쉼표로 구분하여 클래스가 여러개의 프로토콜을 적용하게 할 수도 있다. 아래의 코드는 NSData 클래스가 세개의 프로토콜을 적용하는 방식을 보여준다.

NSData는 세개의 프로토콜을 적용하고 있기 때문에 프로콜에 선언되어 있는 모든 필수 메소드를 구현한다. 그리고 @optional 지시어로 표시된 메소드를 구현할 수도 있다. 카테고리도 프로토콜을 적용할 수 있으며 이 경우 클래스 정의의 일부분이 된다.

Objective-C는 클래스가 상속하는 상위 클래스 뿐만 아니라 클래스가 따르는 프로토콜을 통해서도 클래스를 형식화(어떤 종류인지 판단하는 것) 한다. 클래스에 conformsToProtocol: 메시지를 보내 특정 프로토콜을 따르고 있는지 확인할 수 있다.

메소드, 인스턴스 변수, 함수와 같은 형식(type) 선언에서 프로토콜 적용여부를 지정할 수도 있다. 이를 통해 컴파일러가 제공하는 다른 수준의 형식 확인(type checking)을 사용할 수 있고, 특정 구현과 연관되지 않기 때문에 더욱 향상된 추상화를 얻게 된다. 동일한 구문 규칙을 프로토콜 적용에도 사용할 수 있다. 형식 이름 뒤에서 프로토콜 이름을 꺽쇠로 감싸주면 된다. 동적 객체 형식인 id가 다음과 같이 사용되는 것을 자주 보았을 것이다.

파라미터에 어떠한 클래스 형식도 받아들일 수 있는 객체가 있지만, 이 객체는 반드시 NSDraggingInfo 프로토콜을 따라야 한다.

코코아는 지금까지 보여준 것보다 더 많은 프로토콜들을 제공한다. 한가지 흥미로운 것은 NSObject 프로토콜이다. NSObject 클래스는 당연히 이 프로토콜을 적용하며, 또 다른 최상위 클래스인 NSProxy 역시 이 프로토콜을 적용한다. NSProxy 클래스는 이 프로토콜을 통해 Objective-C 런타임의 필수적인 요소인 참조 카운팅, introspection, 그리고 객체 동작에 필요한 기본 요소들과 상호작용 할 수 있다.

 

Declared Properties

객체 모델링 설계 패턴에서 객체는 속성(property)을 가진다. 속성은 제목, 색상, 다른 객체와의 관계 등과 같은 객체의 특성(attribute)으로 구성된다. 전통적인 Objective-C 코드에서는 인스턴스 변수를 통해 속성을 정의하고 캡슐화를 구현하기 위해서 이 변수에 대한 접근자 메소드를 구현한다. 이 작업은 지루한 작업이고, 특히 메모리 관리 측면에서 오류를 발생시키기 쉽다.

OS X 10.5 버전에서 소개된 Objective-C 2.0은 속성(Property)을 선언하고 이들이 접근되는 방식을 지정할 수 있는 문법을 제공한다. 속성을 선언하는 것은 속성에 대한 접근자 메소드를 선언하는 일종의 속기법이다. 속성을 사용하면 더이상 접근자 메소드를 구현할 필요가 없다. 새로운 점(.) 표시 구문(dot-notation syntax)을 통해 속성 값에 직접 접근할 수도 있다. 속성 문법에는 선언, 구현, 접근과 같은 세가지 요소가 있다.

속성은 클래스, 카테고리, 프로토콜 선언 영역에서 선언할 수 있다. 속성을 선언하는 문법은 다음과 같다.

attributes… 부분에는 쉼표로 구분된 하나 이상의 부가적 특성이 위치한다. 이 특성들은 컴파일러가 인스턴스 변수를 저장하고 접근자 메소드를 합성하는 방식에 영향을 준다. type 요소는 is, NSString*, NSRange, float 등과 같은 객체, 클래스, 스칼라 형식을 지정한다. 속성은 반드시 동일한 형식과 이름을 가지는 인스턴스 변수를 가지고 있어야 한다.

속성 선언에서 사용할 수 있는 특성은 다음과 같다.

Table 2-1  Attributes for declared properties

Table 2-1 Attributes for declared properties

속성(property)의 아무런 특성(attribute)을 지정하지 않고 구현에서 @synthesize를 사용한다면, 컴파일러는 단순 할당을 사용하는 접근자 메소드를 합성하고 getter의 이름은 <속성이름>의 형태로, setter 이름은 set<속성이름>: 형태로 지정한다.

클래스 정의의 @implementation 블록에서 @dynamic, @synthesize 지시어를 통해 컴파일러가 특정 속성에 대한 접근자 메소드를 합성해야 하는지를 제어할 수 있다. 두 지시어는 동일한 문법을 가진다.

@dynamic 지시어는 컴파일러에게 속성에 대한 접근자 메소드를 직접 또는 동적으로 구현한다는 것을 알려준다. 반면에 @synthesize 지시어는 컴파일러에게 @implementation 블록에 포함되어 있지 않은 접근자 메소드를 합성하도록 지시한다. @synthesize 문법에는 속성과 이 속성의 인스턴스 변수에 대해 서로 다른 이름을 사용할 수 있도록 해주는 확장이 포함되어 있다. 다음과 같은 문장을 살펴보자.

이 문장은 컴파일러가 title, directReports, role 속성에 대한 접근자 메소드를 합성하고, role 속성에 대해서는 jobDescrip 인스턴스 변수를 사용하도록 지시한다.

마지막으로 Objective-C의 속성 기능은 점 표기법(dot notation)과 단순 할당(simple assignment)을 통한 간단한 문법을 제공한다. 다음 예제는 이런 문법을 통해 속성을 가져오고 설정하는 것이 얼마나 쉬운지를 보여준다.

점 표기법은 속성과 단순한 1:1 관계에서만 사용할 수 있고 1:다 관계에서는 사용할 수 없다.

[note color=”#fffae5″]Further Reading: declared property에 대한 상세한 정보는 Objective-C Programming Language 문서의 “Declared Properties” 에 설명되어 있다.[/note]

 

Fast Enumeration

빠른 열거는 Objective-C 2.0에서 소개된 언어 기능으로, 컬렉션을 효율적으로 열거하기 위한 간결한 문법을 제공한다. 이것은 NSEnumerator 객체를 통해 배열, 집합, 딕셔너리를 순회하는 전통적인 방식보다 훨씬 더 빠르다. 게다가 열거가 진행되는 동안 컬렉션의 변경을 방지하는 mutation guard를 포함하고 있기 때문에 안전한 열거를 보장해준다. (변경을 시도하면 예외가 발생한다.)

빠른 열거에서 사용하는 문법은 펄이나 루비와 같은 스트립트 언어에서 사용되는 것과 유사하고 다음과 같은 두가지 방식이 존재한다.

expression 부분은 반드시 NSFastEnumeration 프로토콜을 따르는 객체로 평가되는 표현식이어야 한다. 빠른 열거 구현은 Objective-C 런타임과 Foundation 프레임워크로 나뉘어 진다. Foundation은 NSFastEnumeration 프로토콜을 선언하고, 이 프로토콜을 적용하는 NSArray, NSDictionary, NSSet와 같은 Foundation 컬렉션 클래스와 NSEnumerator 객체를 선언한다. 다른 객체의 컬렉션을 유지하는 다른 클래스들도 이러한 기능을 활용하기 위해서 NSFastEnumeration 프로토콜을 적용할 수 있다.

다음 코드는 NSArray, NSSet 객체에서 빠른 열거는 사용하는 방법을 보여준다.

[note color=”#fffae5″]Further Reading: 빠른 열거에 대한 자세한 내용은 The Objective-C Programming Language 문서의 “Fast Enumeration” 부분에 설명되어 있다.[/note]

 

Using Objective-C

객체지향 프로그램에서 수행되는 작업은 메시지를 통해 이루어진다. 메시지를 보내는 객체는 메시지를 받는 객체(receiver)에게 특정 동작을 수행하도록 요청하거나 어떤 객체나 값을 리턴하도록 요청한다.

Objective-C는 메시지 기능에 독특한 문법을 사용한다. Listing 2-2에 있는 코드에서 다음과 같은 문장을 볼 수 있다.

할당문의 오른쪽에 각괄호로 감싸진 부분이 메시지 표현식이다. 메시지 표현식의 왼쪽에 있는 항목은 메시지를 받는 객체를 나타내는 변수나 표현식이며, 보통 receiver라고 부른다. 위의 코드에서 receiver는 NSArray의 인스턴스인 sorted_args이다. receiver 뒤에 있는 항목은 receiver로 보낼 메시지이며, 이 코드에서는 objectEnumerator 메시지를 보내고 있다. objectEnumerator 메시지는 sorted_args 객체에 있는 objectEnumerator 메소드를 호출한다. 이 메소드는 할당문의 왼쪽에 있는 enm 변수로 객체에 대한 참조를 리턴한다. 이 변수(enm)는 NSEnumerator 클래스의 인스턴스로 형식이 지정되어 있다.(이러한 방식을 정적 타이핑이라고 한다.) 이 문장은 다음과 같이 표현할 수 있다.

하지만 이런 표현은 지나치게 단순하고 정확한 것도 아니다. 메시지는 셀렉터의 이름과 메시지의 파라미터로 구성된다. Objective-C 런타임은 dispatch table에서 호출할 메소드를 찾기 위해서 셀렉터 이름을 사용한다. 앞에서 살펴본 코드에서 objectEnumerator가 여기에 속한다. 셀렉터는 메소드를 나타내는 유일한 식별자로 SEL이라는 특별한 형식을 사용한다. 셀렉터를 찾기 위해 사용되는 셀렉터 이름은 서로 간의 밀접한 연관성으로 인해 종종 셀렉터라고 불리기도 한다. 그래서 위의 문장은 다음과 같이 표현하는 것이 더 정확하다.

메시지는 종종 파라미터(인자-argument- 라고도 한다)를 가진다. 하나의 파라미터를 가지는 메시지는 셀렉터 이름 뒤에 콜론(:)을 붙이고, 그 뒤에 파라미터를 둔다. 이런 구조를 keyword라고 부른다. 이 구조에서 키워드는 콜론으로 끝나고 파라미터는 콜론 뒤에 위치한다. 그러므로 하나의 파라미터를 가지는 메시지 표현식을 다음과 같이 표현할 수 있다.

메시지가 다수의 파라미터를 가진다면 셀렉터는 다수의 키워드를 가진다. 셀렉터 이름에는 콜론을 포함한 모든 키워드가 포함되지만, 리턴 형식이나 파라미터 형식과 같은 나머지 요소들은 포함되지 않는다. 다수의 파라미터를 가지는 메시지 표현식은 다음과 같이 표현할 수 있다.

함수의 파라미터와 마찬가지로 파라미터의 형식은 반드시 메소드 선언에 지정된 형식과 일치해야 한다.

여기에서 NSArray 클래스의 인스턴스이기도 한 param은 initWithArray: 메시지의 파라미터이다.

위에서 예로 든 코드는 nesting의 예를 보여주기 때문에 흥미롭다. Objective-C에서는 하나의 메시지 내에 또 다른 메시지를 포함할 수 있다. 포함되는(내부) 메시지에서 리턴되는 객체는 포함하는(외부) 메시지의 리시버로 사용된다. 그래서 중첩된 메시지를 해석할 때는 내부에서 외부로 진행된다. 위의 문장에 대한 해석은 다음과 같이 진행될 것이다.

1. NSCountedSet 클래스에 alloc 메시지가 전달되고, 그 결과 초기화되지 않은 인스턴스가 생성된다.

[note color=”#fffae5″]Note: Objective-C의 클래스는 스스로 정당성을 가지는 객체이며, 클래스의 객체 뿐만 아니라 클래스 자체에도 메시지를 보낼 수 있다. 메시지 표현식에서 클래스 메시지의 리시버는 항상 클래스 객체이다.[/note]

2. 초기화되지 않은 인스턴스에 initWithArray: 메시지가 전달되면 배열은 파라미터로 전달된 배열을 통해 스스로 초기화 한 다음 자신에 대한 참조를 리턴한다.

다음으로 아래의 코드를 살펴보자.

이 메시지 표현식에서 주목할 점은 sortedArrayUsingSelector: 메시지의 파라미터이다. 이 파라미터는 @selector 컴파일러 지시자를 사용해서 파라미터로 사용될 셀랙터를 생성할 것을 요구한다.

잠시 멈추고 메시지와 메소드라는 용어에 대해 복습해보자. 메소드는 본질적으로 클래스에 의해 선언되고 구현된 함수이다. 메시지는 셀렉터 이름과 셀렉터의 파라미터가 합쳐진 것이다. 메시지는 리시버로 보내지고, 그에 따라 메소드가 호출된다. 메시지 표현식은 리시버와 메시지 모두를 포함한다. 그림 2-2는 이러한 관계를 보여준다.

Figure 2-2  Message terminology

Figure 2-2  Message terminology

Objective-C는 ANSI C에서 찾아볼 수 없는 다양한 형식과 리터럴을 사용한다. 어떤 경우에는 이러한 형식과 리터럴이 ANSI C에서 대응되는 것들을 대체하기도 한다. 표 2-2는 몇가지 중요한 형식과 리터럴를 보여준다.

Table 2-2  Important Objective-C defined types and literals

Table 2-2 Important Objective-C defined types and literals

프로그램의 흐름 제어 구문에서 적절한 negative literal을 검사해서 진행 여부를 결정할 수 있다. 예를 들어, 다음 while 구문은 리턴된 객체의 존재를 확인하기 위해서 word 객체 변수를 암시적으로 검사한다.

Objective-C에서는 아무런 악영향을 주지 않고 nil에 메시지를 보낼 수 있다. nil로 보낸 메시지에서 리턴되는 값은 객체의 형식을 가지기만 한다면 동작을 보장받는다. 자세한 내용은 The Objective-C Programming Language 문서의 “Sending Messages to nil” 에서 다룬다.
SimpleCocoaTool 코드에서 마지막으로 주목해서 보아야 할 내용은 Objective-C에 익숙하지 않은 경우 쉽게 보이지는 않을 것이다. 다음 두 문장을 비교해 보자.

표면적으로는 두 문장이 동일한 것처럼 보인다. 두 문장 모두 객체에 대한 참조를 리턴한다. 하지만 리턴되는 객체의 소유권과 이를 해제하는 역할에 있어서 중요한 차이점이 존재한다. 첫번째 문장에서는 리턴된 객체를 소유하지 않는다. 두번째 문장에서는 프로그램이 객체를 생성하고 소유한다. 프로그램이 마지막으로 수행하는 것은 객체가 해제되도록 객체로 release 메시지를 보내는 것이다. 명시적으로 생성된 객체인 NSCountedSet 인스턴스 역시 프로그램의 마지막 부분에서 명시적으로 해제된다. 객체의 소유권과 폐기에 관한 메모리 관리 정책의 개요와 이런 정책을 강제하기 위해 사용하는 메소드는 “How Memory Management Works” 에서 다루고 있다.

 

The Root Class

Objective-C 언어와 런타임은 그 자체로는 아주 간단한 객체 지향 프로그램을 만들기에도 부족하다. 가능은 하지만 쉽지는 않다. 여전히 기본적인 동작에 대한 정의와 모든 객체에서 공통적으로 사용되는 인터페이스가 빠져있다. 최상위 클래스(root class, 루트 클래스)는 이러한 정의를 제공한다.

최상위 클래스라고 부르는 이유는 이 클래스가 클래스 계층구조의 최상위(root)에 위치하기 때문이다. 최상위 클래스는 어떤 클래스도 상속하지 않으며, 계층구조의 다른 모든 클래스들은 최상위 클래스로부터 상속된다. 최상위 클래스는 Objective-C 언어와 함께 코코아가 Objective-C 런타임에 접근하고 상호작용하는데 주로 사용된다. 코코아 객체들은 객체로 동작할 수 있는 대부분의 능력을 최상위 클래스로부터 얻는다.

코코아는 NSObject, NSProxy라는 두개의 최상위 클래스를 제공한다. 후자는 다른 객체의 대역으로 동작하는 객체를 위한 추상 클래스이다. 그래서 NSProxy는 분산 객체 아키텍처에서 중요하게 사용된다. NSProxy는 이렇게 특수한 역할때문에 코코아 프로그램에서 보게 되는 경우는 드물다. 코코아 개발자가 최상위 클래스가 기초 클래스를 언급할 때는 거의 대부분 NSObject를 의미한다.

이 섹션은 NSObject의 기본 동작과 인터페이스, 그리고 런타임과 상호작용하는 방식에 대해 알아본다. 특히 할당, 초기화, 메모리 관리, introspection, 런타임 지원 등을 위해 NSObject가 제공하는 메소드에 대해 알아본다. 이러한 개념들은 코코아를 이해하는데 있어서 기본적인 내용이다.

 

NSObject

NSObject는 Objective-C 클래스 계층구조의 최상위 클래스로 상위 클래스를 가지지 않는다. 다른 객체들은 NSObject로부터 Objective-C 언어를 위한 런타임 시스템에 사용되는 기본적인 인터페이스를 상속받고, 이 객체들의 인스턴스는 객체로써 동작할 수 있는 능력을 얻게된다.

NSObject는 엄밀히 따지면 추상 클래스가 아니지만, 실질적으로는 추상 클래스이다. NSObject의 인스턴스는 스스로 간단한 객체로써의 존재 이상의 유용한 작업을 하지는 못한다. 어플리케이션에 필요한 특성이나 로직을 추가하기 위해서는 반드시 NSObject나 NSObject로부터 파생된 클래스로부터 하나 이상의 상속된 객체를 만들어야 한다.

NSObject는 NSObject 프로토콜을 사용한다. NSObject 프로토콜은 다수의 최상위 객체를 허용한다. 예를 들어, 또 다른 최상위 클래스인 NSProxy는 NSObject를 상속하지 않지만 NSObject 프로토콜을 통해 다른 Objective-C 객체들과 공통적인 인터페이스를 공유한다.

 

Root Class—and Protocol

NSObject는 클래스 이름이면서 프로토콜 이름이기도 하다. 두가지 모두 코코아의 객체 정의에 있어서 기본적인 것들이다. NSObject 프로토콜은 코코아의 모든 최상위 클래스들에게 필요한 기본적인 프로그래밍 인터페이스를 제공한다. 그래서 NSObject 뿐만 아니라 NSProxy도 이 프로토콜을 사용한다.  NSObject 클래스는 프록시 객체가 아닌 코코아 객체를 위한 기본 프로그래밍 인터페이스를 추가로 제공한다.

Objective-C의 설계는 다중 최상위 클래스를 사용할 수 있도록 코코아 객체의 전체 정의에서 NSObject와 같은 프로토콜을 사용한다. 각각의 최상위 클래스는 그들이 사용하는 프로토콜에 정의되어 있는 공통 인터페이스들을 공유한다.

또 다른 의미에서, NSObject가 유일한 최상위 프로토콜은 아니다. 비록 NSObject 클래스가 NSCopying, NSMutableCopying, NSCoding 프로토콜을 공식적으로 적용하고 있지는 않지만, 이러한 프로토콜과 관련된 메소드를 선언하고 구현한다. (게다가 NSObject 클래스의 정의를 담고 있는 NSObject.h 헤더파일에는 NSObject, NSCopying, NSMutableCopying, NSCoding과 같은 최상위 프로토콜의 정의도 포함되어 있다.) 객체의 복사, 인코딩, 디코딩은 객체의 동작에 있어서 기본적인 요소이다. 대부분은 아니지만 많은 하위 클래스들이 이러한 프로토콜들을 적용하거나 따를 것으로 예상된다.

[note color=”#fffae5″]Note: 다른 코코아 클래스들은 카테고리를 통해 NSObject에 메소드를 추가할 수 있다. 이러한 카테고리는 대부분 델리게이션에 사용되는 비공식 프로토콜이다. 하지만, 이러한 NSObject 상의 카테고리들이 기본적인 객체 인터페이스의 일부분으로 간주되지는 않는다.[/note]

 

Overview of Root-Class Methods

NSObject 클래스는 NSObject 프로토콜, 그리고 다른 최상위 프로토콜과 함께 프록시 객체를 제외한 모든 코코아 객체에 대해 다음과 같은 인터페이스와 행동적 특성을 구체화한다.

[list style=”star”]

  • Allocation, initialization, and duplication. NSObject와 이에 적용된 프로토콜의 일부 메소드들은 객체의 생성, 초기화, 복제를 처리한다.
    alloc, allocWithZone: 메소드는 memory zone(동적 메모리 관리를 위해 힙 영역에 생성되는 모메리 구역)에서 객체의 메모리를 할당하고, 이 객체에 자신의 런타임 클래스 정의에 대한 포인터를 설정한다.init 메소드는 객체 초기화의 기본형이다. initialize, load 클래스 메소드는 클래스 스스로 초기화 될 수 있는 기회를 제공한다.

    new 메소드는 단순 할당과 초기화가 결합된 편의 메소드(convenience method)이다.

    copy, copyWithZone: 메소드는 NSCopying 프로토콜에 정의되어 있는 메소드를 구현하는 클래스의 객체에 대한 복사본(불변형)을 생성한다. 객체의 대한 가변형 복사본을 원하는 경우에는 NSMutableCopying 프로토콜에 정의되어 있는 mutableCopy, mutableCopyWithZone: 메소드를 구현한다.

    상세한 정보는 “Object Creation” 에서 제공한다.

  • Object retention and disposal. 다음과 같은 메소드들은 전통적이고 명시적인 메모리 관리 형태를 사용하는 객체지향 프로그램에서 특히 중요하다.
    retain 메소드는 객체의 참조 카운트(코코아에서 retain count와 reference count는 동일한 의미로 사용됨)를 증가시킨다.release 메소드는 객체의 참조 카운트를 감소시킨다.

    autorelease 메소드 역시 객체의 참조 카운트를 감소시키지만, 지연된 방식을 사용한다.

    retainCount 메소드는 객체의 현재 참조 카운트를 리턴한다.

    dealloc 메소드는 객체의 인스턴스 변수를 해제하고 동적으로 할당된 메모리를 제거하기 위해 클래스에서 구현된다.

    명시적 메모리 관리에 대한 자세한 정보는 “How Memory Management Works” 에서 제공한다.

  • Introspection and comparison. 다수의 NSObject 메소드들은 객체에 대한 런타임 질의를 가능하게 해준다. 이러한 introspection 메소드들은 클래스 계층구조에서 객체의 위치를 밝히고, 특정 메소드 구현 여부나 특정 프로토콜을 따르는지를 확인하는데 도움을 준다. 이 중 일부는 클래스 메소드를 통해서만 제공된다.superclass, class 메소드는 리시버의 상위 클래스와 클래스를 각각 Class 객체 형태로 리턴한다.isKindOfClass:, iSMemberOfClass: 메소드를 통해 객체의 클래스 멤버쉽을 확인할 수 있다. 후자는 리시버가 특정 클래스의 인스턴스인지 확인하는데 사용된다. isSubclassOfClass: 클래스 메소드는 클래스 상속을 확인한다.respondsToSelector: 메소드는 리시버가 셀렉터에 의해 식별된 메소드를 구현하고 있는지를 확인한다. instancesRespondToSelector: 클래스 메소드는 특정 클래스의 인스턴스가 특정 메소드를 구현하고 있는지 확인한다.conformsToProtocol: 메소드는 리시버가 특정 프로토콜을 따르는지 확인한다.isEqual: 메소드와 hash 메소드는 객체 비교에 사용된다.description 메소드는 객체가 자신의 내용에 대한 설명을 리턴할 수 있도록 해준다. 이 설명은 종종 디버깅에 사용되며, 포멧 문자열에서는 %@ 변환 문자로 나타낼 수 있다.더 자세한 정보는 “Introspection”에서 제공한다.
  • Object encoding and decoding. 아래의 메소드들은 객체의 인코딩, 디코딩에 관련되어 있다.
    encodeWithCoder:, initWithCoder: 메소드는 NSCoding 프로토콜의 유일한 멤버들이다. 첫번째 메소드는 객체가 자신의 인스턴스 변수를 인코드 할 수 있도록 해주고, 두번째 메소드는 객체가 디코드된 인스턴스 변수로부터 초기화될 수 있도록 해준다.NSObject 클래스는 객체의 인코딩에 관련된 classForCoder, replacementObjectForCoder:, awakeAfterUsingCoder: 메소드도 선언하고 있다.

    Archives and Serializations Programming Guide 에서 더 많은 정보를 제공한다.

  • Message forwarding. forwardInvocation: 메소드와 이와 관련된 메소드는 하나의 객체가 다른 객체로 메시지를 전달할 수 있도록 해준다.
  • Message dispatch. performSelector로 시작하는 메소드들은 특정 지연시간 후에 메시지를 보내거나 2차 스레드에서 메인 스레드로 메시지를 보낼 수 있도록 해준다.

[/list]

NSObject는 versioning과 posing을 위한 클래스 메소드를 포함한 몇가지 다른 메소드들을 가진다. 또한, 메소드 구현에 대한 메소드 셀렉터와 함수 포인터와 같은 런타임 데이터 구조에 접근할 수 있도록 해주는 메소드도 포함하고 있다.

 

Interface Conventions

일부 NSObject 메소드는 호출 될 목적으로 사용되지만, 일부 메소들은 재정의 될 목적으로 사용된다. 예를 들어, 대부분의 클래스들은 allocWithZone: 메소드를 재정의하지 않아야 하지만, init 메소드는 구현해야 하며, 그렇지 않다면 적어도 최상위 클래스의 init 메소드를 호출하는 초기화 메소드를 구현해야 한다. NSObject는 하위 클래스에서 재정의 할 것으로 예상되는 메소드의 구현에서 아무런 작업을 하지 않거나 self와 같은 값을 리턴하기도 한다. 이러한 기본 구현은 init과 같은 기본 메시지를 런타임 예외에 대한 위험성없이 어떤 객체에도 보낼 수 있도록 해준다. 메시지를 보내기 전에 respondsToSelector:를 통해 확인할 필요는 없다. 더욱 중요한 것은 NSObject의 “placeholder” 메소드들이 코코아 객체의 일반적인 구조를 정의하고 객체 상호작용이 더욱 신뢰를 가질 수 있도록 해준다는 것이다.

 

Instance and Class Methods

런타임 시스템은 최상위 클래스에 정의되어 있는 메소드를 특별한 방식으로 다룬다. 최상위 클래스에 정의되어 있는 인스턴스 메소드는 인스턴스 객체와 클래스 객체에 의해 수행될 수 있다. 그러므로 모든 클래스 객체는 최상위 클래스에 정의되어 있는 인스턴스 메소드에 접근할 수 있다. 클래스 객체가 동일한 이름의 클래스 메소드를 가지고 있지 않다면, 최상위 인스턴스 메소드를 호출할 수 있다.

예를 들어, 클래스 객체는 respondsToSelector:, performSelector:withObject: 인스턴스 메소드를 실행하기 위해서 다음 예제와 같이 메시지를 보낼 수 있다.

클래스 객체에서 사용할 수 있는 유일한 인스턴스 메소드는 최상위 클래스에 정의되어 있는 것 뿐이다. 만약 위의 예제에서 MyClass가 respondsToSelector: 메소드나 performSelector:withObject: 메소드를 재정의 하고 있다면 새로운 버전의 메소드는 인스턴스에서만 사용할 수 있을 것이다. MyClass의 클래스 객체는 NSObject 클래스에 정의되어 있는 메소드 버전만을 실행할 수 있다. (물론 MyClass가 이러한 메소드들을 클래스 메소드로 재정의 했다면, 새로운 버전을 실행할 수 있을 것이다.)

 

Object Retention and Disposal

Objective-C는 객체가 요구되는 동안 유지되고, 더 이상 필요하지 않을 때는 객체를 제거하여 사용 가능한 메모리 공간을 확보할 수 있도록 해주는 두가지 방법을 제공한다. 선호되는 접근법은 가비지 컬렉션을 사용하는 것이다. 런타임은 더 이상 필요하지 않은 객체를 찾아서 자동적으로 제거한다. 메모리 관리(memory management)라고 하는 두번째 접근법은 참조 카운팅(reference counting)을 기반으로 한다. 객체는 현재 객체가 요구된 횟수를 나타내는 숫자값을 갖는다. 이 값이 0이 되면 객체가 해제된다.

가비지 컬렉션이나 메모리 관리의 이점을 활용하기 위해 수행해야 하는 작업의 양은 상당히 다르다.

[list style=”star”]

  • Garbage Collection. 가비지 컬렉션을 사용하려면 Xcode의 Enable Objective-C Garbage Collection 빌드설정을 활성화 한다. 그리고 커스텀 클래스에서 통지 옵저버인 인스턴스를 제거하거나 인스턴스 변수가 아닌 다른 리소스들을 해제하기 위해서 finalize 메소드를 구현해야 할 수도 있다.또한 nib 파일에서 File’s Owner의 역할을 하는 객체가 최상위 nib 객체 중에서 유지하고자 하는 객체와 아웃렛 연결을 유지하도록 해야 한다.
    [note color=”#fffae5″]iOS Note: iOS에서는 가비지 컬렉션을 지원하지 않는다.[/note]
  • Memory Management. 메모리 관리 코드에서 객체의 소유권을 차지하는 메소드 호출은 반드시 객체의 소유권을 제거하는 메소드 호출과 짝을 맞추어야 한다. 객체의 참조 카운트가 0이 되면 객체는 제거되고 객체가 점유하고 있던 메모리는 해제된다.

[/list]

가비지 컬렉션 코드는 구현의 용이성 뿐만 아니라 메모리 관리 코드를 뛰어넘는 몇가지 이점을 가진다. 가비지 컬렉션은 참조 사이클과 같은 문제를 일으키지 않는 간단하고 일관적인 모델을 제공한다. 또한 접근자 메소드의 구현을 단순화하고 스레드와 예외 안정성을 쉽게 보장할 수 있도록 해준다.

[note color=”#fffae5″]Important: 메모리 관리 메소드는 가비지 컬렉션 어플리케이션에서 동작하지 않지만, 두가지 모델에서 사용되는 특정 프로그래밍 패러다임과 패턴 사이에는 해결하기 어려운 차이점들이 존재한다. 그러므로 메모리 관리 어플리케이션을 메모리 관리와 가비지 컬렉션을 모두 지원하도록 수정하는 것은 추천되지 않는다. 대신 가비지 컬렉션을 지원하는 새로운 버전을 만들어야 한다.[/note]

이어지는 섹션은 객체의 생명주기를 따라가면서 가비지 컬렉션과 메모리 관리가 동작하는 방식에 대해 살펴본다.

[note color=”#fffae5″]Further Reading: Objective-C의 가비지 컬렉션 기능에 대한 모든 내용은 Garbage Collection Programming Guide 에서 제공한다. 메모리 관리는 Advanced Memory Management Programming Guide 에서 상세하게 다룬다.[/note]

 

How the Garbage Collector Works

가비지 컬렉션의 동작은 가비지 컬렉터라고 하는 개체를 통해 수행된다. 가비지 컬렉터에게 있어서 프로그램에 있는 객체는 도달할 수 있는 것과 도달할 수 없는 것 두가지이다. 컬렉터는 주기적으로 객체를 조사하고 도달 가능한 객체들을 수집한다. 도달 불가능한 객체들은 해제(즉, finalize 메소드가 호출됨)된다. 이어서 객체가 점유하고 있던 메모리가 해제된다.

Objective-C 가비지 컬렉터 아키텍처의 이면에 있는 중요한 개념은 도달 가능한 객체를 나타내는 요소의 모음이다. 이러한 요소에는 전역변수, 스택변수, 외부 참조를 가지는 객체와 같은 초기 최상위 객체의 집합이 포함된다. 초기 최상위 집합에 있는 객체들은 절대로 쓰레기(garbage)로 다루어지지 않기 때문에 프로그램의 런타임 주기동안 유지된다. 컬렉터는 이러한 초기 집합에 강한 참조를 통해 도달할 수 있는 모든 객체와 모든 코코아 스레드의 호출 스택에서 찾아볼 수 있는 참조들을 추가한다. 가비지 컬렉터는 객체의 최상위 모음에서부터 도달 가능한 모든 객체에 대한 강한 참조를 재귀적으로 순회한다. (하나의 객체에서 다른 객체에 대한 모든 참조는 기본적으로 강한 참조이다. 약한 참조를 사용하는 경우에는 명시적으로 표기되어야 한다.) 다시 말해, 최상위에 위치하지 않는 객체들은 컬렉터가 강한 참조를 통해 도달 할 수 있는 경우에만 유지된다.

그림 2-3은 컬렉터가 도달 가능한 객체들을 찾을 때 따르는 일반적인 경로를 보여준다. 그리고 가비지 컬렉터의 몇가지 다른 중요한 요소들을 보여준다. 컬렉터는 코코아 프로그램의 가상 메모리 영역에서만 객체들을 조사한다. 조사되는 메모리에는 스레드의 호출스택, 전역 변수, auto zone, 모든 가비지 컬렉션 메모리 블록이 생성된 메모리 영역이 포함된다. 하지만 malloc 함수를 통해서 생성된 메모리 블록이 위치하는 malloc zone은 조사하지 않는다.

Figure 2-3  Reachable and unreachable objects

Figure 2-3 Reachable and unreachable objects

위의 그림에서 보여주는 또 다른 점은 객체가 다른 객체에 대한 강한 참조를 가질 수 있지만, 최상위 객체로 돌아갈 수 있는 강한 참조 체인(chain of strong reference)이 존재하지 않는다면, 이 객체는 도달할 수 없는 것으로 간주되고 수집 주기(collection cycle)의 마지막 단계에서 제거된다는 것이다. 이러한 참조는 순환할 수 있지만, 가비지 컬렉션에서는 순환 참조(circular reference)가 메모리 누수를 발생시키지는 않는다. 이러한 객체들은 더 이상 도달할 수 없을 때 제거된다.

Objective-C의 가비지 컬렉터는 수요 주도형(demand-driven)이 아닌 요청 주도형(request-driven)이기 때문에, 요청이 있는 경우에만 새로운 수집 주기를 시작한다. 코코아는 성능에 최적화된 주기마다 요청하거나 특정 메모리 한계점이 초과되었을 때 요청을 진행한다. NSGarbageCollector 클래스의 메소드를 통해서 수집을 요청할 수도 있다. 또한, 가비지 컬렉터는 객체가 할당된 시점을 고려하는 방식을 사용한다. 주기적으로 프로그램 객체의 전체 수집을 수행할 뿐만 아니라 객체의 할당 시점(또는 세대, generation)을 기반으로 증분 수집(incremental collection)을 수행하기도 한다. 객체의 세대는 할당된 시점을 통해 결정된다. 전체 수집에 비해 빠르고 효율적인 증분 수집은 가장 최근에 할당된 객체를 대상으로 한다.

가비지 컬렉터는 하나의 스레드에서 실행된다. 수집 주기가 실행되는 동안, 2차 스레드를 중지하고 해당 스레드 내에서 도달할 수 없는 객체들을 확인한다. 하지만, 절대로 모든 스레드를 동시에 중지하지는 않으며, 각 스레드를 가능한 짧은 시간동안만 중지한다. 또한, 메모리 블록의 위치를 변경하거나 포인터를 업데이트 하는 방식을 통해 auto-zone 메모리를 축소하는 작업을 절대로 수행하지 않기 때문에 보수적인 특징을 가지기도 한다. 할당이 완료된 객체는 항상 최초 할당 위치에 존재한다.

 

How Memory Management Works

코코아 객체는 Objective-C 메모리 관리 코드에서 뚜렷한 단계를 가지는 생명주기 동안 존재한다. 이 객체는 생성, 초기화를 거쳐 사용된다. 그리고 다른 객체에 의해 소유되거나 복사될 수도 있고, 마지막에는 해제된 후 메모리 공간이 제거된다. 이어지는 내용은 일반적인 객체의 수명에 대해 간단히 설명한다.

가비지 컬렉션이 꺼져 있을 때 객체가 제거되는 방식에 대한 설명으로 시작해보자. 코코아와 Objective-C는 이러한 상황에서 객체를 유지하고 더 이상 필요하지 않은 시점에 제거하기 위해서 자발적이고 정책을 기반으로 하는 방식을 채택하고 있다.

이 방식과 정책은 참조 카운팅 개념에 기초를 두고 있다. 각각의 코코아 객체는 자신의 지속성에 관심을 가지는 다른 객체의 수를 나타내는 정수를 가지고 있다. 이러한 정수를 객체의 참조 카운트라고 한다. (영어로는 retain count라고 하는데, 이것은 참조라는 용어를 재정의하는 것을 피하기 위해서이다.) 코코아는 클랙스 팩토리 메소드나 alloc, allocWithZone: 메소드를 사용해서 객체를 생성할 때 다음과 같은 두가지 중요한 작업들을 수행한다.

[list style=”star”]

  • 객체의 클래스를 가리키는 isa 포인터를 설정한다. 이를 통해 객체는 런타임으로 통합된다.
  • 객체의 참조 카운트를 1로 설정한다.

[/list]

보통 객체를 할당한 후에는 각 인스턴스 값을 의미있는 초기값으로 설정하는 초기화를 수행한다. (NSObject는 이러한 목적을 위한 표준으로 init 메소드를 선언한다.) 이 시점에서 객체는 사용될 준비가 되어 있다. 객체로 메시지를 보내거나 다른 객체에 이 객체를 전달하는 등 다양한 작업을 할 수 있다.

[note color=”#fffae5″]Note: 초기화 메소드는 할당된 객체를 리턴할 수 있기 때문에 다음과 같이 alloc 메시지 표현식을 init 메시지 내에 포함시킬 수 있다.

[/note]

NSObject는 release 메시지를 통해 객체를 해제할 때 참조 카운트를 감소시킨다. 참조 카운트가 0이 되면 객체가 제거된다. 객체의 제거는 두 단계를 통해 수행된다. 먼저, 인스턴스 변수를 해제하고 동적으로 할당된 메모리를 제거하기 위해 사용되는 dealloc 메소드가 호출된다. 그 후, 운영체제는 객체 자체를 제거하고 객체가 점유하고 있던 메모리를 회수한다.

[note color=”#fffae5″]Important: dealloc 메소드는 절대로 직접 호출하지 않아야 한다. [/note]

객체가 즉시 제거되는것을 원하지 않는다면 어떻게 할까? 객체는 어딘가로부터 retain 메시지를 받으면 참조 카운트가 2로 증가된다. 이제 객체가 제거되기 위해서는 두개의 release 메소드가 필요하다. 그림 2-4는 이런 시나리오를 단순화 해서 보여준다.

Figure 2-4  The life cycle of an object—simplified view

Figure 2-4 The life cycle of an object—simplified view

물론, 이 시나리오에서는 객체의 생성자가 객체를 유지할 필요가 없다. 객체를 생성하면서 이미 그 객체를 소유하기 때문이다. 하지만, 생성자가 메시지 내에서 다른 객체로 이 객체를 전달했다면 상황이 달라진다. Objective-C 프로그램에서 다른 객체에서 전달된 객체는 항상 획득된 범위 내에서 유효한 것으로 간주된다. 전달하는 객체는 전달받는 객체로 메시지를 보낼 수 있고 다른 객체로 전달할 수도 있다. 이러한 가정을 위해서는 객체를 전달하는 것이 적절히 동작해야 하며, 클라이언트 객체가 대상 객체에 대한 참조를 가지고 있는 동안 너무 일찍 해제되지 않아야 한다.

클라이언트 객체가 사용범위를 벗어난 후에도 전달받은 객체를 유지하고자 하는 경우에는 retain 메시지를 보내서 이 객체를 유지할 수 있다. 객체를 유지하는 것은 객체의 참조 카운트를 증가시키므로 객체의 소유권에 대한 이해관계를 나타낸다. 클라이언트 객체는 나중에 객체를 해제할 책임을 가진다. 객체의 생성자가 객체를 해제하지만, 동시에 클라이언트 객체가 동일한 객체를 유지하고 있었다면 이 객체는 클라이언트 객체가 이 객체를 해제하기 전까지 계속 존재한다. 그림 2-5는 이러한 과정을 보여준다.

Figure 2-5  Retaining a received object

Figure 2-5 Retaining a received object

객체를 유지하지 않고 copy나 copyWithZone: 메시지를 보내서 객체를 복사할 수도 있다. 객체를 복사하는 것은 이를 복제할 뿐만 아니라 거의 항상 참조 카운트를 1로 초기화한다(그림 2-6 참고). 객체의 성질과 사용 의도에 따라서 깊은 복사 또는 얕은 복사가 수행될 수 있다. 깊은 복사는 복사되는 객체의 인스턴스 변수를 모두 복제한다. 반면에 얕은 복사는 인스턴스 변수에 대한 참조만을 복제한다.

사용적인 측면에서 copy와 retain의 차이점은 전자가 객체의 새로운 소유자로써 독점적인 사용권을 획득한다는 것이다. 새로운 소유자는 원본 객체에 영향을 주지 않고 복사된 객체를 변경할 수 있다. 일반적으로 값 객체인 경우 이를 유지하기보다는 복사하는 방식을 사용한다. 이것은 특히 NSMutableString의 인스턴스와 같이 객체가 가변적인 경우에 해당된다. 불변 객체의 경우 copy와 retain은 동일한 효과를 가질 수 있으며 비슷하게 구현되어 있을지도 모른다.

Figure 2-6  Copying a received object

Figure 2-6 Copying a received object

객체의 생명 주기를 관리하는데 있어서 이러한 구조가 잠재적인 문제를 일으킬 수도 있다는 것을 발견했을 것이다. 객체를 생성하고 이것을 다른 객체로 전달하는 객체는 객체를 안전하게 해제할 수 있는 시점에 대해 항상 알고 있을 수는 없다. 생성 객체(객체를 생성하는 객체)가 알지 못하는 객체들이 이 객체에 대한 다수의 참조를 가지고 있을 수도 있다. 생성 객체가 생성된 객체를 해제한 뒤에 또 다른 객체가 해제된 객체로 메시지를 보낸다면 프로그램에서 충돌이 발생할 수도 있다. 코코아는 이런 문제를 피하기 위해서 자동해제(autoreleasing)라고 하는 지연된 해제 매커니즘을 사용한다.

자동해제(autoreleasing)는 NSAutoreleasePool 클래스를 통해 정의되어 있는 자동해제 풀(autorelease pool)을 사용한다. 자동해제 풀은 언제가는 해제되도록 정의된 영역 내부에 위치하는 객체들의 집합이다. 자동해제 풀은 중첩될 수 있다. 객체에 대한 참조는 autorelease 메시지를 보낼 때 가장 인접한 자동해제 풀에 추가된다. 이 객체는 여전히 유효하기 때문에 자동해제 풀에서 정의하고 있는 범위에 있는 다른 객체들은 이 객체로 메시지를 보낼 수 있다. 범위의 마지막 부분까지 실행된 후에 풀은 해제되고, 그 결과 이 풀에 있는 모든 객체들도 함께 해제된다(그림 2-7). 어플리케이션을 개발하고 있다면 자동해제 풀을 직접 생성할 필요가 없다. AppKit 프레임워크는 자동적으로 어플리케이션의 이벤트 주기로 범위가 지정된 자동해제 풀을 생성한다.

Figure 2-7  An autorelease pool

Figure 2-7 An autorelease pool

[note color=”#fffae5″]iOS Note: iOS에서 어플리케이션은 좀 더 제한된 메모리 환경에서 실행되기 때문에, 다수의 객체를 생성하는 메소드나 코드 블록에서 자동해제 풀을 사용하는 것은 좋은 방법이 아니다. 가능하다면 객체를 직접 해제해야 한다.[/note]

지금까지 객체의 수명 주기에 대해 설명한 것은 주기를 통해 객체를 관리하는 방식에 초점을 맞추었다. 하지만 객체 소유권에 관한 정책은 다음과 같은 매커니즘을 사용한다. 이 정책은 다음과 같이 요약할 수 있다.

[list style=”star”]

  • [[MyClass alloc] init]와 같이 할당과 초기화를 통해 객체를 생성한다면, 이 객체를 소유하게 되며 이것을 해제할 책임도 함께 가지게 된다. 이런 규칙은 new 메소드를 사용할때도 적용된다.
  • 객체를 복사하는 경우에는 복사된 객체를 소유하게 되며 이것을 해제할 책임도 가지게 된다.
  • 객체를 유지하는 경우에는 이에 대한 부분적인 소유권을 가지며, 더 이상 필요하지 않으면 반드시 해제해야 한다.

[/list]

이와 반대로, 또 다른 객체로부터 객체를 전달 받았다면, 이 객체에 대한 소유권을 가지지 않으므로 객체를 해제하지 않아야 한다. (이 규칙에는 몇가지 예외가 있는데, 이 경우 해당 클래스 참조 문서에 명시적으로 언급되어 있다.)

다른 규칙과 마찬가지로 여기에도 몇가지 예외와 결함이 존재한다.

[list style=”star”]

  • NSMutableArray에 있는 arrayWithCapacity: 메소드와 같은 팩토리 메소드를 통해 객체를 생성한다면, 리턴되는 객체가 자동해제된 것으로 생각해야 한다. 이 객체를 직접 해제하지 않아야 하며, 계속 존재하게 하고 싶다면 이 객체를 유지해야 한다.
  • 자식 객체는 순환 참조를 피하기 위해서 자신의 부모 객체에 대한 참조를 유지하지 않아야 한다. (여기에서 부모 객체는 자식 객체를 만든 객체이거나 자식 객체를 인스턴스 변수로 가지는 객체이다)

[/list]

[note color=”#fffae5″]Note: 이 문서에서 사용된 “해제(release)”는 객체에 release 메시지나 autorelease 메시지를 보내는 것을 의미한다.[/note]

이러한 소유 정책을 따르지 않는다면 두가지 잘못된 현상이 발생할 것이다. 생성, 복사, 유지된 객체들을 해제하지 않았기 때문에 프로그램에서 메모리 누수가 발생할 수 있다. 또는, 이미 해제된 객체로 메시지를 보내기 때문에 프로그램에서 충돌이 발생한다. 이러한 문제를 디버깅하기 위해서는 많은 시간이 필요하다.

객체의 수명 주기동안 발생할 수 있는 또 다른 기본 이벤트는 아카이빙(Archiving)이다. 아카이빙은 객체지향 프로그램을 구성하는 객체들 사이의 관계(객체 그래프라고 함)를 파일과 같은 영구적인 형태로 변환한다. 프로그램이 언아카이브 될 때, 객체 그래프는 아카이브로부터 재구성된다. 객체는 아카이빙을 사용하기 위해서 반드시 NSCoder 클래스의 메소드를 통해 자신의 인스턴스 변수를 인코드하거나 디코드 할 수 있어야 한다. NSObject는 이러한 목적을 위해 NSCoding 프로토콜을 적용한다. 객체의 아카이빙에 대한 자세한 정보는 Archives and Serializations Programming Guide에서 제공한다.

 

Object Creation

코코아 객체의 생성은 항상 할당(allocation)과 초기화(initialization)의 두 단계로 진행된다. 일반적으로 이 두 단계를 거치지 않은 객체들은 사용할 수 없다. 대부분의 경우 할당 직후에 바로 초기화가 수행되지만, 이 두 작업은 객체의 형성에 있어서 서로 다른 역할을 수행한다.

 

Allocating an Object

객체가 할당될 때 실행되는 작업들은 “할당”이라는 용어에서 유추할 수 있을 것이다. 코코아는 어플리케이션의 가상 메모리 영역에서 객체를 위한 충분한 메모리를 할당한다. 이러한 메모리의 크기는 객체의 클래스에 정의되어 있는 인스턴스 변수의 형식이나 순서를 고려하여 계산된다.

객체를 할당하려면 객체의 클래스에 alloc이나 allocWithZone: 메시지를 보낸다. 그리고 리턴값을 통해 초기화되지 않은 클래스 인스턴스를 얻게된다. alloc 메소드의 변형 메소드들은 어플리케이션의 기본 구역(default zone)을 사용한다. 구역(Zone)은 어플리케이션에 할당된 객체와 데이터를 저장하기 위해 사용되는 메모리 영역이며, 페이지 단위의 정렬을 사용한다. Zone에 대한 상세한 정보를 Advanced Memory Management Programming Guide 에서 제공한다.

할당 메시지는 메모리 할당 외에 다른 중요한 작업들을 수행한다.

[list style=”star”]

  • 객체의 참조 카운트를 1로 설정한다.
  • 객체의 isa 인스턴스 변수가 객체의 클래스를 가리키도록 초기화한다.
  • 다른 모든 인스턴스 변수들을 0(또는 nil, NULL, 0.0과 같이 0의 의미를 가지는 값)으로 초기화한다.

[/list]

객체의 isa 인스턴스 변수는 NSObject로부터 상속되기 때문에 모든 코코아 객체에서 공통적으로 사용할 수 있다. 객체는 할당과정에서 isa 값이 설정된 후에 런타임과 프로그램을 구성하는 객체의 조직으로 통합된다. 결과적으로 객체는 런타임에 필요한 모든 정보를 찾을 수 있게 된다.

요약하면, 할당은 객체의 메모리를 할당할 뿐만 아니라 작지만 매우 중요한 두가지 속성을 초기화한다. 이 속성은 isa 인스턴스 변수와 참조 카운트이다. 또한 나머지 인스턴스 변수들을 0으로 초기화한다. 하지만, 여기에서 리턴하는 객체는 아직 사용할 수 없으므로 init과 같은 초기화 메소드가 객체를 초기화하고 사용가능한 객체를 리턴해야 한다.

 

Initializing an Object

초기화는 객체의 인스턴스 변수를 의미있고 유용한 초기값으로 설정한다. 또한 객체에서 필요로하는 다른 전역 리소스를 할당하고, 필요한 경우 파일과 같은 외부 소스로부터 이들을 읽어온다. 인스턴스 변수를 선언하는 모든 객체는 초기 값을 0으로 설정하는 방식(default set-everything-to-zero initialization 이라고 함)이 충분하지 않은 경우 별도의 초기화 메소드를 구현해야 한다. 코코아는 객체가 초기화 메소드를 구현하지 않는다면 가장 인접한 상위 클래스의 초기화 메소드를 대신 호출한다.

 

The Form of Initializers

NSObject는 초기화를 위한 원형 메소드인 init 메소드를 선언한다. 이 메소드는 id 형식의 객체를 리턴하는 인스턴스 메소드이다. 객체를 초기화하기 위해 부가적인 데이터를 필요로하지 않는 하위 클래스에서 init 메소드를 제정의하는 것은 아무런 문제가 없다. 그러나 종종 초기화는 객체를 의미있는 초기 상태로 설정하기 위해 외부 데이터에 의존한다. 예를 들어, Account 클래스를 가지고 있다고 가정하자. Account 객체를 적절하게 초기화하기 위해서는 유일한 계정 번호가 필요하고, 이 값은 반드시 초기화 메소드에 전달되어야 한다. 그렇기 때문에 초기화 메소드는 하나 이상의 파라미터를 가질 수 있다. 유일한 요구사항은 초기화 메소드가 “init”으로 시작해야 한다는 것이다.

[note color=”#fffae5″]Note: 하위 클래스는 파라미터를 가지는 초기화 메소드를 구현하는 대신, 단순히 init 메소드만을 구현한 다음 초기화 직후에 접근자 메소드를 통해 객체의 초기 상태를 설정할 수도 있다. 또는, 하위 클래스가 속성(property)과 이와 연관된 접근 구문을 사용하고 있다면 초기화 직후에 속성의 값을 할당할 수도 있다.[/note]

코코아에는 파라미터를 가지는 초기화 메소드의 예가 다양하게 존재한다. 다음은 이러한 몇가지 예를 보여준다. 괄호안에는 메소드가 정의되어 있는 클래스가 표시되어 있다.

이러한 초기화 메소드들은 “init”으로 시작하는 인스턴스 메소드이며 id 형식의 객체를 리턴한다. 이 외에도, 다수의 파라미터를 가지는 메소드에 대한 코코아 규약을 따른다. 이 규약은 다른 파라미터보다 좀 더 중요한 파라미터 앞에서 With<객체형식>: 또는 From<원본>: 과 같은 형식을 사용한다.

 

Issues with Initializers

init… 류의 메소들은 메소드 시그니처에 따라 객체를 리턴하도록 요구되지만, 리턴되는 객체가 반드시 가장 최근에 할당된 객체(init.. 메시지의 리시버)일 필요는 없다. 다시 말해, 초기화 메소드로부터 리턴받는 객체는 할당 후 초기화 될 것으로 예상했던 객체가 아닐 수도 있다.

방금 할당된 객체가 아닌 다른 객체를 리턴하는 것은 두가지 상황을 발생시킨다. 첫번째 상황은 반드시 단일 인스턴스(singleton instance)이어야 하거나 객체의 속성에 설정하는 값이 유일해야 하는 경우와 관련되어 있다. NSWorkspace와 같은 일부 코코아 클래스들은 프로그램 내에서 하나의 인스턴스만을 사용할 수 있다. 이러한 범주에 속하는 클래스는 반드시 하나의 인터스턴만 생성되어야 하고, 새로운 객체에 대한 생성 요청이 있을 경우에도 이미 생성되어 있는 객체를 리턴해야 한다. (싱글톤 객체의 구현에 관한 정보는 “Creating a Singleton Instance” 에서 제공한다.)

객체가 유일한 속성 값을 가져야 하는 경우에도 비슷한 상황이 발생한다. 앞에서 언급한 가상의 Account 클래스를 다시 생각해보자. 계정은 반드시 유일한 식별자를 가져야 한다. 이 클래스의 초기화 메소드(initWithAccountID: 라고 가정)로 객체와 연관된 식별자가 전달된다면, 이 초기와 메소드는 반드시 다음과 같은 작업을 수행해야 한다.

[list style=”star”]

  • 새롭게 할당된 객체를 해제해야 한다.(메모리 관리 코드에 해당됨)
  • 이전에 초기화 된 Account 객체를 새롭게 전달된 식별자로 초기화하여 리턴해야 한다.

[/list]

이를 통해 초기화 메소드는 요청된 식별자를 사용하는 Account 인스턴스를 리턴함과 동시에 식별자의 유일성을 유지하게 된다.

때로는 init… 메소드가 요청된 초기화를 수행하지 못한다. 예를 들어, initFromFile: 메소드는 파라미터로 전달된 경로에 있는 파일의 내용을 통해 초기화 되어야 한다. 하지만 해당 위치에 파일이 존재하지 않는다면 객체는 초기화될 수 없다. initWithArray: 메소드에 NSArray 객체 대신 NSDictionary 객체가 전달된 경우에도 비슷한 문제가 발생한다. init… 메소드가 객체를 초기화 할 수 없을 때는 다음과 같은 작업을 수행해야 한다.

[list style=”star”]

  • 새롭게 할당된 객체를 해제해야 한다.(메모리 관리 코드에 해당됨)
  • nil을 리턴해야 한다.

[/list]

초기화 메소드에서 nil을 리턴하는 것은 요청된 객체가 생성될 수 없다는 것을 나타낸다. 보통 객체를 생성할 때에는 작업을 진행하기 전에 리턴되는 값이 nil인지 확인해 보아야 한다.

초기화 메소드는 nil이나 명시적으로 할당되지 않은 객체를 리턴할 수 있기 때문에, alloc이나 allocWithZone: 메소드로부터 리턴된 인스턴스를 사용하는 것은 위험하다. 다음과 같은 코드를 살펴보자.

이 예제에 있는 init 메소드는 nil을 리턴하거나 다른 객체로 대체될 수 있다. nil로 메시지를 보내는 것은 예외를 발생시키지 않기 때문에, 머리아픈 디버깅 문제를 제외한다면 아무런 일도 발생하지 않을 것이다. 하지만, 방금 할당된 “raw” 인스턴스보다는 초기화 메소드를 통해 초기화된 인스턴스를 사용해야 한다. 그러므로, 초기화 메시지 내에 할당 메시지를 포함시키고, 작업을 진행하기 전에 초기화 메소드에서 리턴된 객체를 확인해 보아야 한다.

객체의 초기화가 완료된 후에는 이것을 다시 초기화하지 않아야 한다. 초기화된 프레임워크 클래스 객체들은 재초기화를 시도하면 종종 예외를 발생시키기도 한다. 예를 들어, 아래와 같은 예제에서 두번째 초기화는 NSInvalidArgumentException 예외를 발생시킬 수 있다.

 

Implementing an Initializer

클래스의 유일한 초기화 메소드를 구현하거나, 다수의 초기화 메소드와 지정된 초기화 메소드(designated initializer)가 존재하는 경우에는 다음과 같은 중요한 규칙을 따라야 한다.

[list style=”star”]

  • 항상 상위 클래스의 초기화 메소드를 먼저 호출해야 한다.
  • 상위 클래스에서 리턴된 객체를 확인해야 한다. 이 객체가 nil이라면 초기화를 더 이상 진행할 수 없으므로 nil을 리턴해야 한다.
  • 객체를 참조하는 인스턴스 변수를 초기화 할 때는, 필요한 경우 이 객체를 유지(retain)하거나 복사해야 한다. (메모리 관리 코드에 해당됨)
  • 다음과 같은 경우를 제외하고 인스턴스 변수를 유효한 초기 값들로 설정한 후에 self를 리턴해야 한다.
    • 대체된 객체를 리턴해야 한다면, 이 경우에는 최근에 할당된 객체를 먼저 해제해야 한다.(메모리 관리 코드에 해당)
    • 초기화를 진행할 수 없는 문제가 발생한다면, 이 경우에는 nil을 리턴해야 한다.

[/list]

Listing 2-3에 있는 메소드는 앞에서 설명한 규칙들을 보여준다.

[note color=”#fffae5″]Note: 이 예제는 쉬운 이해을 위해서 파라미터가 nil인 경우 nil을 리턴하고 있지만, 이 경우에 예외를 발생시키기는 것이 더 나은 코코아 사용방식이다.[/note]

객체의 모든 인스턴스 변수를 명시적으로 초기화하지 않아도 되며, 객체가 동작하는데 필요한 것만 초기화하면 된다. 대부분 할당시점에 객체의 값을 0으로 설정하는 기본적인 초기화(default set-to-zero initialization)로도 충분하다. 메모리 관리를 위해 요구되는 경우에는 인스턴스 변수를 유지(retain)하거나 복사해야 한다.

상위 클래스의 초기화 메소드를 처음에 호출해야 한다는 요구사항은 중요하다. 객체가 자신의 클래스에 정의되어 있는 인스턴스 변수 뿐만 아니라 자신의 모든 상위 클래스에 정의되어 있는 인스턴스 변수들도 함께 캡슐화 하고 있다는 점을 상기해보자. super의 초기화 메소드를 먼저 호출함으로써 상속 체인에서 상위에 위치하는 클래스에 정의되어 있는 인스턴스 변수들이 먼저 초기화 되도록 해준다. 인접한 상위 클래스는 자신의 초기화 메소드에서 자신의 상위 클래스의 초기화 메소드를 호출한다. 이러한 과정은 최상위 클래스까지 반복된다. 하위 클래스의 초기화는 적절한 값으로 초기화된 상위 클래스의 인스턴스 변수에 의존적일 수 있기 때문에 올바른 순서로 초기화 되는 것이 매우 중요하다.

Figure 2-8  Initialization up the inheritance chain

Figure 2-8 Initialization up the inheritance chain

상속된 초기화 메소드는 하위 클래스를 만들 때 고려된다. 때로는 상위 클래스의 초기화 메소드가 클래스의 인스턴스를 충분히 초기화 한다. 하지만 그렇지 못한 경우가 대부분이기 때문에 상위 클래스의 초기화 매소드를 재정의해야 한다. 그렇지 않을 경우에는 상위 클래스의 구현이 호출되고, 상위 클래스는 하위 클래스에 대해 아무것도 모르기 때문에, 하위 클래스가 올바르게 초기화 되지 않을 수도 있다.

 

Multiple Initializers and the Designated Initializer

클래스는 하나 이상의 초기화 메소드를 정의할 수 있다. 다수의 초기화 메소드는 클래스 사용자들에게 다양한 형식으로 동일한 초기화를 수행할 수 있도록 해준다. 예를 들어, NSSet 클래스는 다음과 같이 동일한 데이터를 다양한 형태로 전달받는 초기화 메소드를 제공한다.

일부 하위 클래스들은 초기화 메소드에 기본값을 제공하기 위해 전체 초기화 파라미터를 받아들이는 편의 초기화 메소드(convenience initializer)를 제공한다. 이런 초기화 메소드를 보통 지정된 초기화 메소드(designated initializer)라고 부르며 클래스에서 가장 중요한 초기화 메소드이다. 예를 들어, Task라고 하는 클래스가 있고, 이 클래스는 다음과 같은 시그니처를 가지는 지정된 초기화 메소드를 선언하고 있다고 가정해보자.

Task 클래스는 Listing 2-4와 같이 특정 파라미터에 대한 기본값을 사용해서 지정된 초기화 메소드를 호출하는 또 다른 초기화 메소드를 가질 수도 있다.

지정된 초기화 메소드는 클래스에서 중요한 역할을 수행한다. 이 메소드는 상위 클래스의 지정된 메소드를 호출하여 상속된 인스턴스 변수들이 올바르게 초기화 되도록 해준다. 일반적으로 지정된 초기화 메소드는 대부분의 파라미터를 받아들이고 대부분의 초기화를 수행하는 메소드이며, 클래스의 다른 초기화 메소드들이 self로 보내는 메시지를 통해 호출하는 메소드이다.

하위 클래스를 정의할 때 반드시 상위 클래스의 지정된 초기화 메소드를 확인하고 하위 클래스의 지정된 초기화 메소드에서 super로 보내는 메시지를 통해 해당 메소드를 호출하도록 해야 한다. 또한, 상속된 초기화 메소드들이 어떠한 방식을 통해 적용되도록 해야 한다. 그리고 필요하다고 판단되는 만큼 추가적인 초기화 메소드를 제공할 수도 있다. 클래스의 초기화 메소드를 설계할 때, super로 전달하는 메시지를 통해 서로 연결되는 지정된 초기화 메소드를 고려해야 한다. 반면에 다른 초기화 메소드들은 self로 보내는 메시지를 통해 자신의 클래스에 있는 지정된 초기화 메소드와 연결된다.

예제가 이러한 내용을 더 명확하게 이해시켜 줄 것이다. A, B, C라는 세개의 클래스가 있다고 가정하자. B 클래스는 A 클래스로부터 상속되고, C 클래스는 B 클래스로부터 상속된다. 각각의 하위 클래스는 인스턴스 변수를 추가하고 이것을 초기화하기 위해서 지정된 초기화 메소드를 구현한다. 이들은 추가적인 초기화 메소드들을 정의하며 필요한 경우 상속된 초기화 메소드를 재정의 한다. 그림 2-9는 세 클래스의 초기화 메소드와 이들의 관계를 보여준다.

Figure 2-9  Interactions of secondary and designated initializers

Figure 2-9 Interactions of secondary and designated initializers

각 클래스의 지정된 초기화 메소드는 가장 넓은 적용범위를 가지는 초기화 메소드이다. 또한, super로 보내는 메시지를 통해 상위 클래스의 지정된 초기화 메소드를 호출하는 초기화 메소드를 의미하기도 한다. 이 예제에서 C 클래스의 지정된 초기화 메소드는 initWithTitle:date:는 자신의 상위 클래스(B)에 있는 지정된 초기화 메소드인 initWithTitle:을 호출하고, 다시 이 메소드는 A 클래스의 init 메소드를 호출한다. 하위 클래스를 생성할 때는 상위 클래스의 지정된 초기화 메소드를 아는 것이 중요하다.

지정된 초기화 메소드는 super로 보내는 메시지를 통해 상속 체인의 위쪽에 연결되지만, 부가적인 초기화 메소드들은 self로 보내는 메시지를 통해 자신의 클래스에 있는 지정된 초기화 메소드를 연결된다. 이 예제에서 부가적인 초기화 메소드는 대부분 상속된 초기화 메소드를 재정의한 것이다. C 클래스는 기본 날짜 값으로 자신의 지정된 초기화 메소드를 호출하도록 initWithTitle: 메소드를 재정의한다. 이 지정된 초기화 메소드는 B 클래스의 지정된 초기화 메소드인 initWithTitle: 메소드를 호출한다. B 클래스와 C 클래스에 initWithTitle: 메시지를 보내면, 서로 다른 메소드 구현을 호출하게 된다. 반면에 C 클래스가 initWithTitle: 메소드를 재정의 하지 않았다면 B 클래스의 구현이 호출되었을 것이다. 그러나 이 경우에는 C의 인스턴스가 완전하게 초기화되지 않을 것이다. 하위 클래스를 만들 때 상속되는 모든 초기화 메소드가 적절히 적용되도록 하는 것은 매우 중요하다.

때때로 상위 클래스의 지정된 초기화 메소드가 하위 클래스를 충분히 초기화 할 수도 있고, 이 경우에는 하위 클래스가 자신의 지정된 초기화 메소드를 구현할 필요가 없다. 어떤 경우에는 클래스의 지정된 초기화 메소드가 상위 클래스의 지정된 초기화 메소드를 재정의한 것일 수도 있다. 이것은 비록 하위 클래스가 자신의 인스턴스 변수를 가지지 않지만 상위 클래스의 지정된 초기화 메소드에 의해 수행되는 작업을 보충할 필요가 있을 경우에 해당된다.

 

The dealloc and finalize Methods

finalize 메소드는 가비지 컬렉션을 사용하는 코코아 클래스에서 객체가 해제되기 전에 남아있는 리소스와 부가적인 것들을 제거하는데 사용되는 메소드이다. 전통적인 메모리 관리를 사용하는 코코아 클래스에서 이러한 역할을 수행하는 메소드는 dealloc 메소드이다. 사용되는 목적은 유사하지만 구현되는 방식에 있어서는 커다란 차이점을 가진다.

dealloc 메소드는 여러 가지 면에서 클래스의 초기화 메소드, 특히 지정된 초기화 메소드에 대응하는 메소드이다. 초기화 메소드가 할당 직후에 호출되는데 비해, dealloc 메소드는 객체가 해제되기 직전에 호출된다. 초기화 메소드에서는 객체의 인스턴스 변수가 적절히 초기화 되는 것에 비해, dealloc 메소드는 객체의 인스턴스 변수가 해제되고 동적으로 할당된 메모리가 제거될 수 있도록 해준다.

마지막으로 비교되는 점은 상위 클래스에 존재하는 동일한 메소드 구현을 호출해야 하는 부분이다. 초기화 메소드에서는 첫번째 단계로 상위 클래스의 지정된 초기화 메소드를 호출한다. dealloc 메소드에서는 마지막 단계로 상위 클래스의 dealloc 구현을 호출한다. 이것은 호출 순서가 초기화 메소드와 정반대이기 때문이다. 이 경우에는 상위 클래스의 인스턴스 변수들이 해제되거나 제거되기 전에 하위 클래스가 자신의 인스턴스 변수를 먼저 해제하거나 제거해야 한다.

Listing 2-5는 dealloc 메소드를 구현한 예를 보여준다.

이 예제는 accountDictionary를 해제하기 전에 nil이 아닌 값을 가지는지 확인하지 않는다. 이것은 Objective-C가 nil에 메시지를 보내는 것을 허용하기 때문이다.

finalize 메소드는 dealloc 메소드와 비슷하게, 객체가 제거되거나 메모리가 회수되기 전에 객체에서 사용하고 있는 리소스들을 닫을 수 있는 메소드이다. finalize 구현의 마지막 라인은 dealloc과 마찬가지로 상위 클래스의 구현을 호출해야 한다. 하지만, dealloc과 달리 가비지 컬렉터가 적절한 시점에 인스턴스 변수들을 해제하기 때문에 인스턴스 변수를 직접 해제할 필요가 없다.

dealloc 메소드와 finalize 메소드 사이에는 더 중요한 차이점이 존재한다. 대부분의 경우 dealloc 메소드의 구현은 필수이지만, finalize 메소드는 가능하다면 구현하지 않아야 한다. 그리고 finalize 메소드를 반드시 구현해야 하는 경우에는 가능한 적은 수의 객체만을 참조해야 한다. 이러한 충고를 하는 주요한 이유는 가비지 컬렉션의 대상이 되는 객체에게 finalize 메시지가 전달되는 순서가 불확실하기 때문이다. 제거될 객체 사이에서 메시지가 전달된다면, 실행 결과는 불확실하고 어쩌면 부정적일 수도 있다. 일반적으로 finalize 메소드가 호출되기 전에는 malloc를 통해 할당된 메모리를 제거하거나, 파일 디스크립터를 닫거나, 옵저버를 해제하는 작업이 수행되도록 코드를 설계해야 한다.

[note color=”#fffae5″]Further Reading: Garbage Collection Programming Guide 문서의 “Implementing a finalize Method”에서 finalize 메소드를 구현하는 방식에 대해 설명한다.[/note]

 

Class Factory Methods

클래스 팩토리 메소드는 사용자의 편의를 위해 구현되는 메소드이다. 이 메소드는 할당과 초기화를 하나의 단계로 통합하고 생성된 객체를 리턴한다. 하지만, 이 객체를 리턴받는 클라이언트는 객체를 소유하지 않으므로 객체 소유 정책에 따라 이 객체를 해제할 책임을 가지지 않는다. 이러한 메소드는 +(리턴형)클래스이름… 과 같은 형태를 가진다. 클래스 이름 앞에는 어떠한 접두어도 포함되지 않는다.

코코아는 특히 “값” 객체에서 이러한 메소드에 대한 다양한 예를 제공한다. NSDate 클래스는 다음과 같은 클래스 팩토리 메소드를 포함하고 있다.

그리고 NSData는 다음과 같은 팩토리 메소드를 제공한다.

팩토리 메소드는 단순히 편의를 제공하는 것 이상일 수도 있다. 이들은 할당과 초기화를 통합할 뿐만 아니라 할당은 초기화에 정보를 제공할 수도 있다. 예를 들어, 다수의 컬렉션 요소들을 담고 있는 프로퍼티 리스트 파일로부터 컬렉션 객체를 초기화해야 한다고 가정해보자. 팩토리 메소드가 컬렉션 메소드를 위해서 얼마나 많은 메모리를 할당해야 하는지 파악하기 전에 반드시 파일을 읽고 프로퍼티 리스트를 해석해서 얼마나 많은 요소가 존재하고 각 요소는 어떤 객체 형식인지 확인해야 한다.

클래스 팩토리 메소드의 또 다른 목적은 특정 클래스가 싱글톤 인스턴스를 제공하도록 하는 것이다. init… 메소드가 특정 시점에 오직 하나의 인스턴스만 존재하는지를 검증할 수도 있지만, 이 경우에는 사전에 할당된 “raw” 인스턴스가 필요하며, 메모리 관리 코드에서는 검증 후에 이 인스턴스를 해제해야 할 것이다. 반면에 팩토리 메소드는 사용하지 않는 객체의 메모리를 맹목적으로 할당해야 하는 것을 피할 수 있는 방법을 제공한다. (Listing 2-6 참고)

[note color=”#fffae5″]Further Reading: 코코아 객체의 할당과 초기화에 관한 자세한 내용과 문제점들은 The Objective-C Programming Language 문서의 “The Runtime System”에서 상세하게 설명한다.[/note]

 

Introspection

introspection는 객체지향 언어와 객체지향 환경의 강력한 특징이며, Objective-C와 코코아의 introspection 역시 예외가 아니다. introspection은 객체가 런타임에 자신의 상세 정보를 파헤칠 수 있는 능력을 나타낸다. 이러한 상세 정보에는 상속 구조에서의 객체의 위치, 특정 프로토콜 적용 여부, 특정 메시지에 대한 응답 가능성 등이 포함된다. NSObject 프로토콜과 NSObject 클래스는 객체를 규정하기 위해서 런타임에 질의하는데 사용할 수 있는 다양한 introspection 메소드를 정의하고 있다.

introspection은 현명하게 사용될 경우 객체지향 프로그램을 더욱 효율적이고 튼튼하게 만들어준다. 이것은 메시지 전달 오류, 객체 동일성에 대한 잘못된 추측 등과 같은 문제를 피하는데 도움을 준다. 이어지는 섹션은 NSObject의 introspection 메소드를 코드에서 효율적으로 사용할 수 있는 방법을 보여준다.

 

Evaluating Inheritance Relationships

객체가 속하는 클래스를 알고 있다면 해당 객체에 대해서 꽤 많이 알게 될 것이다. 어떠한 능력을 가지고 있는지, 대표하는 특성은 무엇인지, 응답할 수 있는 메시지는 어떤 종류인지에 대해서 알 수 있을 것이다. introspection 후에도 객체가 속하는 클래스에 대해 익숙하지 않다고 하더라도, 불필요한 메시지를 보내지 않을만큼의 충분한 정보를 얻게된다.

NSObject 프로토콜은 클래스 계층구조에서 객체의 위치를 확인하는데 사용되는 다양한 메소드를 선언한다. 이러한 메소드들은 다른 단위에서 동작한다. 예를 들어, class 메소드와 superclass 메소드는 각각 리시버의 클래스와 상위 클래스를 나타내는 Class 객체를 리턴한다. 이들은 하나의 Class 객체를 다른 Class 객체와 비교할 것을 요구한다. Listing 2-7은 이러한 사용법에 대한 간단한 예를 제공한다.

[note color=”#fffae5″]Note: 때때로 클래스 메시지를 보낼 적절한 리시버를 얻기 위해서 class 메소드나 superclass 메소드를 사용하기도 한다.[/note]

더욱 일반적으로, 객체의 클래스 소속을 확인하기 위해서 isKindOfClass: 메소드나 isMemberOfClass: 메시지를 보내게 될 것이다. 전자는 리시버가 특정 클래스의 인스턴스 또는 특정 클래스를 상속하는 클래스의 인스턴스인지를 리턴한다. 반면, 후자는 리시버가 지정된 클래스의 인스턴스인지를 리턴해준다. 일반적으로 isKindOfClass: 메소드는 객체에 보낼 수 있는 메시지의 전체 범위를 즉시 파악할 수 있기 때문에 더 유용하다. Listing 2-8에 있는 코드를 고려해보자.

이 코드는 item 객체가 NSData 클래스로부터 상속된다는 것을 것을 확인하고, 이 객체로 bytes, length 메시지를 보낼 수 있다는 것에 대해 알게된다. isKindOfClass:와 isMemberOfClass:의 차이는 item이 NSMutableData 클래스의 인스턴스일 때 드러난다. 위의 코드에서 isKindOfClass: 대신 isMemberOfClass: 메소드를 사용한다면, item은 NSData의 인스턴스가 아니고 이 클래스를 상속하는 NSMutableData의 인스턴스이기 때문에 if 문 내부 코드는 절대로 실행되지 않는다.

 

Method Implementation and Protocol Conformance

좀 더 강력한 NSObject의 두가지 introspection 메소드는 respondsToSelector:와 conformsToProtocol: 메소드이다. 이들은 각각 특정 메소드의 구현 여부와 지정된 공식 프로토콜의 적용여부를 알려준다.

이 메소드들은 유사한 상황에서 사용한다. 이들은 메시지를 보내기 전에 임의의 객체가 특정 메시지나 메시지 집합에 적절히 응답할 수 있는지 확인할 수 있도록 해준다. 메시지를 보내기 전에 이러한 확인 작업을 수행함으로써 인식되지 않은 셀렉터로 인해 발생하는 런타임 예외의 위험성을 피할 수 있다. AppKit 프레임워크는 메소드가 호출되기 전에 델리게이트가 델리게이션 메소드를 구현하고 있는 확인하는 방식을 사용해서 비공식 프로토콜을 구현한다.

Listing 2-9는 respondsToSelector: 메소드를 어떻게 사용할 수 있는지를 보여준다.

Listing 2-10은 conformsToProtocol: 메소드를 사용하는 방법을 보여준다.

 

Object Comparison

hash와 isEqual:은 엄밀히 말해서 introspection 메소드가 아니지만 비슷한 역할을 수행한다. 이들은 객체를 식별하고 비교하기 위해서 꼭 필요한 런타임 도구이다. 하지만, 객체에 대한 런타임 정보를 조회하지 않고 클래스에 특화된 비교 로직을 사용한다.

hash와 isEqual: 메소드는 모두 NSObject 프로토콜에 선언되어 있으며 서로 밀접하게 연관되어 있다. hash 메소드는 반드시 해시 테이블 구조에서 테이블 주소로 사용될 수 있는 정수를 리턴하도록 구현되어야 한다. 두 객체가 동일하다면 반드시 동일한 해시 값을 가져야 한다. 객차가 NSSet 객체와 같은 형식으로 컬렉션에 포함될 수 있다면, hash 메소드를 정의하고 두 객체가 동일한 해시 값을 리턴하는지 검증해 보아야 한다. isEqual: 메소드의 기본 구현은 단순히 포인터의 동일성을 확인한다.

isEqual: 메소드를 사용하는 것은 직관적이다. 이 메소드는 파라미터로 전달된 객체를 리시버와 비교한다. 객체 비교는 종종 객체를 통해 어떤 작업을 수행해야 하는지에 대한 선택을 런타임에 제공한다. Listing 2-11에서 보여주는 것과 같이 isEqual: 메소드를 통해 특정 작업을 수행할지를 결정할 수 있으며, 이 예제에서는 변경된 사용자 설정을 저장한다.

하위 클래스를 만들고 있다면 객체의 동일성을 확인하는데 사용되는 작업을 추가하기 위해서 isEqual: 메소드를 재정의해야 할 수도 있다. 하위 클래스는 동일한 객체로 판단될 수 있도록 두 인스턴스에서 동일한 값을 가지는 추가적인 속성을 정의할 수도 있다. 예를 들어, MyWidget라고 하는 NSObject의 하위 클래스를 만들고 있고, 이 클래스에는 name, data라는 두개의 인스턴스 변수를 포함하고 있다고 가정해보자. 이 두 개의 인스턴스 변수가 동일한 값을 가지고 있다면 반드시 객체들이 동일한 객체로 판단되어야 한다. Listing 2-12는 MyWidget 클래스에서 isEqual:을 어떻게 구현할 수 있는지를 보여준다.

isEqual: 메소드는 먼저 포인터의 동일성을 확인하고, 그 후 클래스의 동일성을 확인한 다음 마지막으로 객체 비교 메소드를 호출한다. 이렇게 전달된 객체의 형식 확인을 강제하는 방식의 비교 메소드는 코코아에서 공통적인 규약이다. NSString 클래스의 isEqualToString: 메소드나 NSTimeZone 클래스의 isEqualToTimeZone: 메소드가 이러한 예이다. 위의 예제에서 클래스에 특화된 isEqualToWidget: 메소드는 name과 data 인스턴스 변수의 동일성을 확인한다.

코코아 프레임워크에 존재하는 모든 isEqualTo<데이터형>: 형식의 메소드에서 nil은 유효한 파라미터가 아니며, 이러한 메소드들은 nil을 전달받을 때 예외를 발생시킬 수도 있다. 하지만, 코코아 프레임워크의 isEqual: 메소드는 후방 호환성(backward compatibility, 이전 버전과의 호환성)을 위해서 nil을 파라미터로 받아들이고, 이 경우에 NO를 리턴한다.

 

Object Mutability

코코아 객체는 가변(mutable) 객체이거나 불변(immutable) 객체이다. 불변 객체에 저장되어 있는 값은 변경할 수 없다. 이러한 객체에 저장된 값은 객체의 생명 주기동안 동일한 값을 유지한다. 하지만 가변 객체에 저장되어 있는 값은 언제라도 변경할 수 있다. 이어지는 섹션은 객체가 가변형과 불변형을 모두 가지는 이유와 객체의 가변성에 대한 특징과 부작용에 대해 설명하고, 이러한 가변성이 문제가 되는 경우 객체를 처리할 수 있는 가장 좋은 방법을 제시한다.

 

Why Mutable and Immutable Object Variants?

객체는 기본적으로 가변형이다. 대부분의 객체들은 접근자 메소드를 통해 저장된 값을 변경할 수 있도록 해준다. 예를 들어, NSWindow 객체의 크기, 위치, 제목, 버퍼링 방식 등을 변경할 수 있다. 잘 설계된 모델 객체는 자신의 인스턴스 데이터를 변경하기 위해 setter 메소드를 요구한다.

가변형 클래스는 보통 자신과 연관된 불변형 클래스의 하위 클래스이고 클래스 이름에 “Mutable”이라는 단어를 포함한다. 이런 클래스에는 다음과 같은 클래스들이 포함된다.

NSMutableArray
NSMutableDictionary
NSMutableSet
NSMutableIndexSet
NSMutableCharacterSet
NSMutableData
NSMutableString
NSMutableAttributedString
NSMutableURLRequest

모든 객체들이 변경될 수 있는 능력을 가진 경우를 생각해보자. 어플리케이션에서 메소드를 호출하고 문자열을 나타내는 객체에 대한 참조를 돌려받았다. 이 문자열을 통해 사용자 인터페이스에 특정 데이터를 표시한다. 이제 어플리케이션의 다른 부분에서 동일한 문자열에 대한 참조를 얻고 이것을 변경한다. 이 때, 인터페이스에 표시된 레이블은 당신의 의도와 관계없이 갑자기 변경된다. 배열에 대한 참조를 얻어 테이블 뷰를 조작하는 경우에는 상황이 더 심각해질 수 있다. 사용자가 프로그램의 특정 코드에 의해서 삭제된 객체 해당되는 행을 선택한다면 문제가 발생한다. 객체의 불변성은 객체의 값을 사용하는 동안 예기치 않게 변경되지 않도록 해준다.

불변성을 가장 잘 나타내는 객체는 개별 값의 집합을 저장하거나 버퍼에 저장된 값을 포함하는 컬렉션이다. NSNumber나 NSDate의 인스턴스와 같이 단순한 단일 값을 담고있는 객체들은 가변성에 적합하지 않다. 이러한 인스턴스에 저장되어 있는 값을 변경할 때는 이전 인스턴스를 새로운 인스턴스로 교체하는 것이 더 적절하다.

성능 또한 문자열이나 딕셔너리와 같은 객체의 불변형을 제공하는 이유 중 하나이다. 이러한 기본 형식에 대한 가변형 객체들은 약간의 오버헤드를 가진다. 이들은 필요에 따라 메모리 조각을 할당하고 해제할 수 있어야 하는 내부 저장소를 동적으로 관리해야 하기 때문에 불변형 객체에 비해 효율적이지 않다.

이론적으로 불변성은 객체 값의 안정성을 보장하지만, 현실적으로 항상 이러한 것을 보장하지는 않는다.  불변형을 리턴하는 메소드에서 가변형 객체를 리턴할 수도 있다. 나중에 불변형을 리턴하는 메소드가 가변형을 리턴하는 메소드로 수정되는 경우, 변경 전의 리턴값을 기반으로 판단한 내용과 구현들의 유효성이 훼손될 수 있다. 객체의 가변성은 다양한 변환을 거치면서 변경되기도 한다. 예를 들어, NSPropertyListSerialization 클래스 통해 프로퍼티 리스트를 직렬화하는 경우에는 객체의 가변성에 대한 내용를 보존하지 않는다. 그러므로 이 프로퍼티 리스트를 역직렬화 할 때, 결과 객체는 원본 객체와 같지 않을 수도 있다. 예를 들어, NSMutableDictionary 객체였던 것이 지금은 NSDictionary 객체일 수도 있다.

 

Programming with Mutable Objects

객체의 가변성이 문제가 되는 경우에는 몇가지 방어적인 프로그래밍 관습을 적용하는 것이 최선의 방법이다. 다음은 이러한 관습에 대한 몇가지 일반적인 규칙 또는 지침들을 나타낸다.

[list style=”star”]

  • 객체가 생성된 후에 내용을 자주 변경해야 한다면 가변형 객체를 사용한다.
  • 때로는 하나의 불변 객체를 다른 불변 객체로 교체하는 것이 좋다. 예를 들어, 문자열 값을 가지는 대부분의 인스턴스 변수들은 setter 메소드를 통해 교체될 수 있는 불변형 NSString 객체로 할당되어야 한다.
  • 가변성에 관한 표시는 리턴형에 의존해야 한다.
  • 객체가 가변 객체인지 또는 가변 객체가 되어야 하는지에 대해 불확실 하다면 불변 객체를 사용해야 한다.

[/list]

이번 섹션은 가변 객체를 사용할 때 결정해야 하는 일반적인 사항들에 대해 이야기하면서 위에서 언급한 지침에서 애매한 부분들을 살펴본다. 또한 파운데이션 프레임워크에서 가변 객체를 생성하고, 가변 객체와 불변 객체를 상호 변환하는 메소드에 대한 개요를 제공한다.

 

Creating and Converting Mutable Objects

다음 코드처럼 표준적인 alloc-init 중첩 메시지를 통해 가변 객체를 생성할 수 있다.

하지만, 다수의 가변 클래스들은 NSMutableArray 클래스의 arrayWithCapacity: 메소드와 같이 객체의 용량을 지정할 수 있도록 해주는 초기화 메소드와 팩토리 메소드를 제공한다.

용량에 대한 힌트는 가변 객체의 데이터에 대해 좀 더 효율적인 저장소를 사용할 수 있도록 해준다. (자동해제 된 인스턴스를 해제해야 하는 클래스 팩토리 메소드의 규약으로 인해, 코드에서 이 객체를 계속 유지하고 싶은 경우에는 반드시 객체를 유지(retain)해야 한다.)

일반적인 형식의 객체에 대한 가변 복사본을 생성하여 가변 객체를 만들수도 있다. 가변 클래스의 상위 클래스인 불변 클래스들이 구현하고 있는 mutableCopy 메소드를 호출하면 된다.

이것과 반대로, 가변 객체에 copy 메시지를 보내서 객체의 불변 복사본을 만들수도 있다.

가변형과 불변형을 가지는 다수의 파운데이션 클래스들은 다음과 같은 변환 메소드를 포함하고 있다.

[list style=”star”]

  • <type>With<Type>:—for example, arrayWithArray:
  • set<Type>:—for example, setString: (mutable classes only)
  • initWith<Type>:copyItems:—for example, initWithDictionary:copyItems:

[/list]

 

Storing and Returning Mutable Instance Variables

코코아 개발에서는 종종 인스턴스 변수의 가변성을 결정해야 한다. 딕셔너리나 문자열과 같이 값을 변경할 수 있는 인스턴스 변수의 경우 객체를 가변형으로 만드는 적절한 시점은 언제일까? 그리고 객체를 불변형으로 만들고 값이 변경될 때 다른 객체로 교체하는 것이 더 나은 시점은 언제일까?

일반적으로 값 전체가 변경되는 경우에는 불변형 객체를 사용하는 것이 더 좋다. 보통 문자열(NSString)과 데이터(NSData) 객체는 이러한 범주에 속한다. 객체가 변경될 것으로 예상되는 경우에는 가변 객체를 사용하는 것이 좋다. 배열이나 딕셔너리와 같은 컬렉션이 이러한 범주에 속한다. 하지만, 잦은 변경과  컬렉션의 크기는 이러한 선택에 있어서 중요한 요소가 된다. 예를 들어, 자주 변경되지 않는 작은 배열을 가지고 있다면, 불변형으로 만드는 것이 더 좋다.

컬렉션이 가지고 있는 인스턴스 변수의 가변성을 결정할 때 고려해야할 두가지 사항이 존재한다.

[list style=”star”]

  • 자주 변경되거나 접근자 메소드를 통해 자주 리턴되는 가변 컬렉션이 있다면, 이것을 사용하는 다른 객체에 의해 값이 변경될 수 있는 위험을 감수해야 한다. 이러한 위험성이 발생할 수 있다면 인스턴스 변수는 불변형이어야 한다.
  • 인스턴스의 값이 자주 변경되지만 접근자 메소드를 통해 자주 리턴되지 않는다면, 인스턴스 변수를 가변형으로 만들고 접근자 메소드에서 불변형 복사본을 리턴할 수 있다. 이러한 객체는 메모리 관리 프로그램에서 자동해제 될 것이다.(Listing 2-13)

[/list]

리턴되는 가변형 컬렉션을 처리하기 위해 사용할 수 있는 세련된 방식 중 하나는 객체의 가변성을 나타내는 플래그 값을 유지하는 것이다. 변경이 발생한 경우 객체를 가변형으로 만들고 변경사항을 적용한다. 컬렉션을 전달할 때는 필요에 따라서 객체를 불변형으로 만든다.

 

Receiving Mutable Objects

메소드의 호출자가 리턴되는 객체의 가변성에 관심을 가지는 이유는 두가지이다.

[list style=”star”]

  • 객체의 값을 변경할 수 있는지 확인하기 위해서
  • 객체의 값이 다른 객체에 의해 참조되고 있는동안 갑자기 변경될 수 있는가를 확인하기 위해서

[/list]

 

Use Return Type, Not Introspection

메시지 리시버는 리턴된 객체를 변경할 수 있는지 판단하기 위해서 반드시 리턴값의 형식에 의존해야 한다. 예를 들어, 불변형 배역 객체를 리턴 받는다면 변경을 시도하지 않아야 한다. 다음과 같이 객체의 클래스 멤버쉽에 기반해서 가변성을 확인하는 것은 허용되는 않는 방식이다.

구현과 관련된 이유로 인해 isKindOfClass: 메소드가 리턴하는 값은 정확하지 않을수도 있다. 하지만, 또 다른 이유 때문에 클래스 멤버쉽에 기반해서 객체의 가변성을 추측하지는 않아야 한다. 가변성에 대한 선택은 반드시 메소드의 시그니처를 통해서만 이루어져야 한다. 객체의 가변성을 정확히 판단하기 힘들다면, 이 객체는 불변형으로 생각해야 한다.

다음 두가지 예는 이러한 지침의 중요성을 좀 더 명확하게 보여준다.

[list style=”star”]

  • 파일에서 프로퍼티 리스트를 읽는다. 파운데이션 프레임워크는 이 리스트를 처리할 때, 여기에 포함된  프로퍼티 리스트의 하위 집합들이 동일하다는 것을 발견하고 하위 집합에서 공유되는 객체의 집합을 생성한다. 곧 이어 생성된 프로퍼티 리스트를 살펴보고 하나의 하위 집합을 변경하기로 한다. 이 경우, 당신은 아무것도 인지하지 못한 상황에서 다양한 부분에서 트리를 변경하게 된 것이다.
  • subviews 메소드를 통해 NSView에게 하위 뷰의 목록을 요청하고, 이 메소드는 내부적으로 NSMutableArray 형식을 가질수도 있는 NSArray 객체를 리턴한다. 그 후, introspection을 통해 이 배열의 가변성을 판단하기 위해서 다른 특정 코드로 전달한다. 코드는 이 배열을 변경함으로써 NSView 클래스의 내부적인 데이터 구조를 변형시키게 된다.

[/list]

그러므로 introspection에서 얻을 수 있는 정보를 통해 객체의 가변성을 추측하지는 않아야 한다. 객체의 리턴형에 따라서 객체를 가변형 또는 불변형으로 다루어야 한다. 객체를 전달할 때 가변성을 명백하게 표시해야 한다면, 가변성에 대한 정보를 객체와 함께 전달해야 한다.

 

Make Snapshots of Received Objects

만약 메소드로부터 리턴된 불변 객체가 알 수 없는 순간에 변경되지 않도록 하고 싶다면, 객체의 복사본을 지역적으로 생성하여 객체의 스냅샷을 만들 수 있다. 그런 다음 종종 저장된 스냅샷과 가장 최신 버전의  객체를 비교한다. 객체가 변경되었다면, 객체의 이전 버전에 의존하는 어떠한 것들도 조절할 수 있다. Listing 2-14는 이러한 기술을 사용하는 구현을 보여준다.

비교을 위해 스냅샷을 생성하는 것과 관련된 문제는 이러한 작업이 고비용이라는 것이다. 동일한 객체의 복사본을 여러개 생성해야 하기 때문이다. 좀 더 효율적인 대안은 key-value observing을 사용하는 것이다. 이 프로토콜에 관한 개요는 “Key-Value Observing”에서 제공한다.

 

Mutable Objects in Collections

컬렉션 객체에 가변 객체를 저장하는 것은 문제를 발생시킬 수 있다. 특정 컬렉션은 포함된 객체가 변경되는 경우 무효화 되거나 손상될 수도 있다. 이것은 변경을 통해 컬렉션에 포함되어 있는 객체가 컬렉션내에 위치하는 방식에 영향을 줄 수 있기 때문이다. 먼저, NSDictionary 객체나 NSSet 객체와 같은 해싱 컬렉션에서 키로 사용되는 객체의 속성이 변경되고, 이러한 변경이 객체의 hash 메소드나 isEqual: 메소드의 결과에 영향을 준다면 컬렉션이 손상될 것이다. (컬렉션에 포함된 객체의 hash 메소드가 내부 상태에 의존하지 않는다면 손상이 발생할 가능성은 낮아진다.) 두번째로 정렬된 컬렉션에 있는 객체가 자신의 속성을 변경한다면, 이것은 배열 내에서 하나의 객체를 다른 객체와 비교하는 방식에 영향을 줄 수 있기 때문에, 순서를 올바르게 표현하지 못한다.

 

Class Clusters

클래스 클러스터는 파운데이션 프레임워크에서 광범위하게 사용되는 설계 패턴이다. 클래스 클러스터는 다수의 구상 하위 클래스들을 하나의 공개 추상 클래스로 묶어준다. 이러한 방식으로 그룹화되는 클래스들은 객체지향 프레임워크의 기능적인 풍부함을 감소시키지 않고 공식적으로 공개되는 아키텍처를 단순화시켜준다. 클래스 클러스터는 “Cocoa Design Patterns”에서 설명하고 있는 Abstract Factory 설계 패턴을 기반으로 한다.

 

Without Class Clusters: Simple Concept but Complex Interface

클래스 클러스터의 설계와 장점에 대해 설명하기 위해 다양한 형식의 객체로 이루어진 클래스 계층구조를 구성할 때 발생하는 문제를 고려해보자. 서로 다른 자료형은 다수의 공통적인 특징을 가지고 있기 때문에, 하나의 클래스로 나타낼 수 있다. 하지만, 이들의 저장소 요구사항은 서로 다르기 때문에 이들을 동일한 클래스로 나타내는 것은 비효율적이다. 이런 사실을 고려해서, 문제를 해결할 수 있는 클래스 설계 중 한가지는 그림 2-10에서 보여주는 것과 같다.

Figure 2-10  A simple hierarchy for number classes

Figure 2-10 A simple hierarchy for number classes

Number는 하위 클래스에서 공통적으로 사용되는 메소드를 선언하고 있는 추상 클래스이다. 그러나 이 클래스는 숫자를 저장하는 인스턴스 변수를 선언하지는 않는다. 이런 인스턴스 변수는 하위 클래스에서 선언되고, 하위 클래스들은 Number에 선언되어 있는 프로그래밍 인터페이스를 공유한다.

이런 설계는 아직까지는 비교적 간단하다. 하지만, 공통적으로 사용되는 더 많은 C 자료형을 고려하면 클래스 계층구조가 그림 2-11에 더 가까울 것이다.

Figure 2-11  A more complete number class hierarchy

Figure 2-11 A more complete number class hierarchy

숫자 값을 저장하는 클래스를 생성한다는 단순한 개념은 수십개의 클래스로 쉽게 확장될 수 있다. 클래스 클러스터 아키텍처는 이러한 개념의 단순함을 반영하는 설계를 제공한다.

 

With Class Clusters: Simple Concept and Simple Interface

앞에서 설명한 문제에 클래스 클래스터 설계 패턴을 적용하면 클래스 계층구조는 그림 2-12와 같을 것이다. (비공개 클래스는 회색으로 표시되어 있다.)

Figure 2-12  Class cluster architecture applied to number classes

Figure 2-12 Class cluster architecture applied to number classes

이 계층구조의 사용자들은 Number라는 하나의 공개 클래스에 대해서만 알게 되는데, 어떻게 적절한 하위 클래스의 인스턴스를 할당할 수 있는 것일까? 이에 대한 해답은 추상 클래스가 인스턴스화를 처리하는 방식에 있다.

 

Creating Instances

클래스 클러스터에 있는 추상 클래스는 반드시 자신의 비공개 하위 클래스들의 인스턴스를 생성하는데 사용되는 메소드를 선언해야 한다. 호출하는 생성 메소드에 따라 적절한 하위 클래스를 선택하는 것은 상위 클래스의 몫이다. 따라서 인스턴스의 클래스를 직접 선택할 수는 없다.

파운데이션 프레임워크에서는 일반적으로 +<클래스이름>… 메소드나 alloc.., init… 메소드를 통해 객체를 생성한다. 파운데이션 프레임워크에 있는 NSNumber 클래스를 한가지 예로 살펴보면, 숫자 객체를 생성하기 위해서 다음과 같은 메시지들을 보낼 수 있다.

팩토리 메소드로부터 리턴되는 객체는 직접 해제할 필요가 없다. 다수의 클래스들은 객체를 생성하는데 사용되는 표준적인 alloc…, init… 메소드도 함께 제공한다. 이 경우에는 객체의 해제를 직접 관리해야 한다.

리턴되는 각 객체(위의 예제에서 aChar, anInt, aFloat, aDouble)는 서로 다른 비공개 하위 클래스에 속할 수도 있다(실제로도 그렇다). 비록 각 객체의 클래스 멤버쉽은 숨겨져 있지만, NSNumber 추상 클래스에 선언되어 있는 인터페이스는 공개되어 있다. 정확한 의미는 아니지만 aChar, anInt, aFloat, aDouble 객체가 NSNumber 클래스에 의해 생성되었고, 여기에 선언되어 있는 인스턴스 메소드를 통해 접근되기 때문에, NSNumber 클래스의 인스턴스로 생각하는 것이 편리하다.

 

Class Clusters with Multiple Public Superclasses

위의 예제에서 하나의 추상 공개 클래스는 다수의 비공개 하위클래스를 위한 인터페이스를 선언한다. 이것이 가장 정확한 의미의 클래스 클러스터이다. 또한 클러스의 인터페이스를 선언하는 두개 이상의 추상 공개 클래스를 가지는 것도 가능하다. 이러한 내용은 표 2-3에 나열되어 있는 클러스터들을 포함하고 있는 파운데이션 프레임워크에서 명확히 확인할 수 있다.

Table 2-3  Class clusters and their public superclasses

Table 2-3 Class clusters and their public superclasses

이 외에도 다른 클러스터들이 존재하지만, 이 목록에 나열되어 있는 클러스터들은 두개의 추상 노드가 클래스 클러스터의 프로그래밍 인스턴스를 선언하기 위해 어떻게 협력하는지를 명백하게 보여준다. 각 클러스터 중 하나는 모든 클러스터 객체가 응답할 수 있는 메소드를 선언하는 노드이고, 또 다른 노드는 내용을 변경할 수 있는 클러스터 객체에 적합한 메소드를 선언하는 노드이다.

이와 같은 클러스터 인터페이스의 분리는 객체지향 프레임워크의 프로그래밍 인터페이스를 좀 더 의미있게 만드는데 도움을 준다. 예를 들어, 다음과 같은 메소드를 선언하고 있는 책 객체를 생각해보자.

책 객체는 자신의 인스턴스 변수를 리턴하거나 새로운 문자열 객체를 생성한 다음 리턴할 수도 있다. 이런것은 문제가 되지 않는다. 이 선언에는 리턴되는 문자열을 변경할 수 없다는 것은 명백하다. 리턴되는 객체를 변경하고자 한다면 해당 위치에서 컴파일러 경고가 발생될 것이다.

 

Creating Subclasses Within a Class Cluster

클래스 클러스터 아키텍처는 단순함과 확장성 사이의 trade-off를 포함한다. 소수의 공개 클래스가 다수의 비공개 클래스를 대표하는 방식은 프레임워크에 있는 클래스를 쉽게 배우고 사용할 수 있도록 해주지만, 클러스터에서 하위 클래스를 생성하는 것은 더 어려워진다. 그러나 하위 클래스를 자주 생성할 필요가 없다면 클러스터 아키텍처가 분명히 도움이 된다. 클러스터는 파운데이션 프레임워크에서 이러한 상황에서 사용된다.

클러스터가 프로그램에서 필요로 하는 기능을 제공하지 않는다는 것을 알게 되었다면 클러스터를 서브클래싱 하는 것도 가능하다. 예를 들어, NSArray 클래스 클러스터 내에 파일 기반의 저장소를 사용하는 배열 객체를 만들기를 원한다고 가정해 보자. 이 클래스의 내부 저장 방식을 변경해야 하기 때문에 하위 클래스를 생성해야 한다.

반면에, 클러스터에 있는 객체에 포함되는 클래스를 정의하는 것만으로 충분한 경우가 있다. 어떤 데이터가 변경될 때마다 프로그램이 경고를 전달받아야 한다고 가정해보자. 이 경우에는 파운데이션 프레임워크에 정의되어 있는 데이터 객체를 감싸는 간단한 클래스를 생성하는 것이 가장 좋은 접근법이 될 수 있다. 이 클래스의 객체는 데이터 변경하는 메시지에 개입해서, 이 메시지를 가로채고, 이것을 자신이 포함하고 있는 데이터 객체로 전달할 수 있다.

요약하면, 객체의 저장소를 관리해야 한다면 실제 하위 클래스를 생성해야 한다. 그렇지 않으면, 표준 파운데이션 프레임워크 객체를 직접 정의한 객체에 포함하는 복합 객체를 생성해야 한다. 이어지는 섹션은  이러한 두가지 접근법에 대해 자세히 설명한다.

 

A True Subclass

클래스 클러스터에서 생성하는 새로운 클래스는 반드시 다음과 같은 내용에 부합해야 한다.

[list style=”star”]

  • 클러스터가 가진 추상 클래스의 하위 클래스여야 한다.
  • 자신만의 저장소를 선언해야 한다.
  • 상위 클래스의 모든 초기화 메소드를 재정의해야 한다.
  • 상위 클래스의 원시 메소드를 재정의해야 한다. (아래에 설명되어 있음)

[/list]

클러스터의 추상 클래스는 클러스터 계층구조에서 유일하게 공개되는 노드이기 때문에, 첫번째 내용은 명확하다. 이것은 새로운 하위 클래스가 클러스터의 인터페이스를 상속하지만, 추상 클래스는 어떠한 인스턴스 변수도 선언하고 있지 않기 때문에 이들은 상속하지 않는다는 것을 암시한다. 그러므로 두번째 항목에서 지적한 것과 같이 하위 클래스는 반드시 필요한 인스턴스 변수들을 선언해야 한다. 마지막으로 하위 클래스는 반드시 자신이 상속한 메소드 중에서 객체의 인스턴스 변수에 직접 접근하는 메소드들을 재정의해야 한다. 이러한 메소드를 원시 메소드(primitive method)라고 한다.

클래스의 원시 메소드는 인터페이스의 근간을 형성한다. 예를 들어, NSArray 클래스를 생각해보자. 개념적으로 배열은 인덱스를 통해 접근할 수 있는 다수의 데이터 항목을 저장한다. NSArray는 이러한 추상적인 개념을 두가지 원시 메소드(count, objectAtIndex:)를 통해 표현한다. 다른 메소드(derived method, 파생된 메소드)들은 이러한 원시 메소드를 기반으로 구현될 수 있다. 표 2-4는 파생된 메소드의 두가지 예를 보여준다.

Table 2-4  Derived methods and their possible implementations

Table 2-4 Derived methods and their possible implementations

원시 메소드와 파생 메소드 사이의 인터페이스 분리는 하위 클래스를 좀 더 쉽게 생성할 수 있도록 해준다. 하위 클래스는 반드시 상속된 원시 메소드들을 재정의해야 하지만, 이렇게 하는 것은 상속된 모든 파생 메소드들이 올바르게 동작할 것이라는 확신을 준다.

원시와 파생의 구분은 완전히 초기화된 객체의 인스턴스에 적용된다. 하위 클래스에서 init… 메소드들이 어떻게 처리되어야 하는지에 대한 것도 고려되어야 한다.

일반적으로 클러스터의 추상 클래스는 다수의 init… 메소드와 +<클래스이름> 메소드를 선언한다. 추상 클래스는 “Creating Instances” 에서 설명하고 있는 것처럼 호출된 메소드에 따라 어떤 하위 클래스의 인스턴스를 생성할지 결정한다. 추상 클래스는 하위 클래스의 편의를 위해서 이러한 메소드들을 선언하고 있다고 생각할 수 있다. 추상 클래스는 인스턴스 변수를 가지고 있지 않기 때문에 초기화 메소드를 필요로하지 않는다.

하위 클래스는 인스턴스 변수를 초기화해야 하는 경우 자신만의 init… 메소드와 +<클래스이름> 메소드를 구현해야 한다. 이들은 자신이 상속하는 초기화 메소드에 의존하지 않아야 한다. 하위 클래스는 초기화 체인에서 링크를 유지하기 위해서, 자신의 지정된 초기화 메소드에서 상위 클래스의 지정된 초기화 메소드를 호출해야 한다. 또한, 상속되는 모든 초기화 메소드를 재정의하고 적절한 방식으로 동작하도록 구현해야 한다. 클래스 클러스터에서 추상 클래스의 지정된 초기화 메소드는 항상 init이다.

 

True Subclasses: An Example

MonthArray라는 이름의 NSArray 하위 클래스를 생성하고 특정 인덱스에 있는 달(month)의 명칭을 리턴하기를 원한다고 가정해보자. 하지만 MonthArray 객체는 실제로 달 명칭 배열을 인스턴스 변수로 저장하지는 않을 것이다. 대신 특정 인덱스 위치에 있는 명칭을 리턴하는 메소드가 상수 문자열을 리턴할 것이다. 그러므로 어플리케이션에서 얼마나 많은 MonthArray 객체가 존재하는지에 관계없이 단지 12개의 문자열 객체만이 할당될 것이다.

MonthArray 클래스는 다음과 같이 선언되어 있다.

MonthArray에는 초기화 해야할 인스턴수 변수가 없기 때문에 init… 메소드를 선언하고 있지 않다는 것에 주목하자. count 메소드와 objectAtIndex: 메소드는 앞에서 설명한 것처럼 상속된 원시 메소드를 재정의하는 것이다.

MonthArray 클래스의 구현은 다음과 같다.

MonthArray는 상속된 원시 메소드를 재정의하기 때문에, 상속된 파생 메소드들은 재정의되지 않고도 올바르게 동작할 것이다. NSArray에 선언되어 있는 lastObject, containsObject:, sortedArrayUsingSelector:, objectEnumerator 등의 메소드들은 MonthArray 객체에서도 별다른 문제없이 동작한다.

 

A Composite Object

직접 설계한 객체에 비공개 클러스터 객체를 포함시키는 방식을 통해 복합 객체(composite object)를 생성한다. 복합 객체는 자신의 기본 기능을 위해 클러스터 객체 의존할 수 있으며, 특정한 방식으로 처리하고자 하는 메시지만을 가로챈다. 이러한 설계는 작성해야 하는 코드의 양을 줄여주며, 파운데이션 프레임워크에서 제공하는 검증된 코드의 이점을 활용할 수 있게 해준다. 그림 2-13은 이러한 설계를 보여준다.

Figure 2-13  An object that embeds a cluster object

Figure 2-13 An object that embeds a cluster object

복합 객체는 반드시 클러스터 추상 클래스의 하위 클래스로 선언되어야 한다. 그리고 상위 클래스의 원시 메소드를 반드시 재정의해야 한다. 파생 메소드를 재정의할 수도 있지만, 이들은 원시 메소드를 통해 동작하기 때문에 반드시 필요한 작업은 아니다.

NSArray 클래스의 count 메소드가 한가지 예이다. 복합 객체의 count 메소드는 다음과 같이 단순하게 구현될 수 있다.

 

A Composite Object: An Example

복합 객체의 사용에 대해 설명하기 위해서 배열의 내용을 변경하기 전에 몇가지 검증 조건을 통해 변경 사항을 확인하는 가변 배열 객체를 작성한다고 해보자. 아래의 예제는 표준 가변 배열 객체를 포함하고 있는 ValidatingArray 클래스를 보여준다. ValidatingArray는 자신의 상위 클래스인 NSArray와 NSMutableArray에 선언되어 있는 모든 원시 메소드를 재정의 한다. 또한 인스턴스를 생성하고 초기화 하는데 사용되는 array, validatingArray, init 메소드를 선언한다.

구현 파일은 ValidatingArray 클래스의 init 메소드 내에서 포함된 객체(embedded object)를 생성하고 embeddedArray 변수에 할당하는 방법을 보여준다. 배열에 접근하지만 내용을 변경하지 않는 메시지는 포함된 객체로 전달된다. 내용을 변경할 수 있는 메시지들은 정밀한 검사를 수행하고 가상의 유효성 시험을 통과한 경우에만 포함된 객체로 전달된다.

 

Toll-Free Bridging

코어 파운데이션 프레임워크와 파운데이션 프레임워크에는 서로 교환해서 사용할 수 있는 다양한 자료형이 존재한다. toll-free bridging이라고 하는 이러한 능력은 동일한 자료형을 코어 파운데이션 함수 호출에서 파라미터로 사용하거나 Objective-C 메시지의 리시버로 사용할 수 있다는 것을 의미한다. 예를 들어, NSLocale 클래스는 코어 파운데이션의 CFLocale과 교환해서 사용할 수 있다. 그러므로, 메소드에서 NSLocale * 파라미터를 사용하는 부분에 CFLocaleRef를 전달할 수 있고, CFLocaleRef 파라미터를 사용하는 함수에 NSLocale 인스턴스를 전달할 수 있다. 다음 예제에서 보여주는 것과 같이 컴파일러 경고를 감추기 위해서 하나의 자료형을 다른 자료형으로 형변환한다.

이 예제에서 메모리 관리 함수와 메소드 역시 교환해서 사용할 수 있다는 것에 주목하자. CFRelease 함수를 코코아 객체와 함께 사용할 수 있고, release 메소드와 autorelease 메소드는 코어 파운데이션 객체와 함께 사용할 수 있다.

[note color=”#fffae5″]Note: 가비지 컬렉션을 사용할 때, 코코아 객체와 코어 파운데이션 객체에서 메모리 관리가 동작하는 방식과 중요한 차이점을 가진다. 상세한 정보는 “Using Core Foundation with Garbage Collection” 에서 제공한다.[/note]

Toll-free bridging은 OS X 버전 10.0부터 사용할 수 있게 되었다. 표 2-5는 코어 파운데이션과 파운데이션 사이에서 교환해서 사용할 수 있는 자료형의 목록을 제공한다. 또한, 사용가능한 최소 버전도 함께 표시하고 있다.

Table 2-5  Data types that can be used interchangeably between Core Foundation and Foundation

Table 2-5 Data types that can be used interchangeably between Core Foundation and Foundation

 

[note color=”#fffae5″]Note: 모든 자료형에서 Toll-free bridging을 사용할 수 있는 것은 아니며, 자료형의 이름이 Toll-free bridging가 가능하다는 것을 암시하지만 실제로는 그렇지 못한 경우도 존재한다. 예를 들어, NSRunLoop와 CFRunLoop, NSBundle와 CFBundle, NSDateFormatter와 CFDateFormatter는 모두 Toll-free bridging을 통해 서로 교환해서 사용할 수 없다.[/note]

 

Creating a Singleton Instance

파운데이션과 AppKit 프레임워크의 일부 클래스들은 싱글톤 객체를 생성한다.싱글톤은 엄격한 구현에서는 현재 프로세스에서 허용되는 클래스의 유일한 인스턴스이다. 하지만 팩토리 메소드가 항상 동일한 객체를 리턴하도록 하는 좀 더 유연한 싱글톤을 구현할 수도 있고, 추가적인 인스턴스를 할당하고 초기화 할 수도 있다. NSFileManager 클래스는 뒤에서 설명한 패턴이 적합하고, UIApplication 클래스는 앞에서 설명한 패턴과 어울린다. UIApplication의 인스턴스를 요청할 때, 유일한 인스턴스에 대한 참조를 전달해 준다. 인스턴스가 존재하지 않는다면 전달하기 전에 새로운 인스턴스를 할당하고 초기화 한다.

싱글톤 객체는 일종의 제어 센터로 동작하면서 클래스의 서비스를 관리하거나 조정한다. 클래스는 개념상 단 하나의 인스턴스만이 존재해야 하는 경우에 싱글톤 인스턴스를 생성해야 한다. 얼마 지나지 않아 다수의 객체가 존재할 것으로 생각되는 경우에는 팩토리 메소드나 함수 대신 싱글톤 인스턴스를 사용해야 한다.

싱글톤 객체를 현재 프로세스에서 유일한 인스턴스로 생성하기 위해서 Listing 2-15와 유사한 코드를 구현해야 한다. 이 코드는 다음과 같은 작업을 수행한다.

[list style=”star”]

  • 싱글톤 객체의 정적 인스턴스 변수를 선언하고 nil로 초기화 한다.
  • 클래스의 팩토리 메소드(“sharedInstance” 또는 “sharedManager”와 같은 이름을 가짐)에서, 정적 인스턴스 변수가 nil인 경우에만 인스턴스를 생성한다.
  • 클래스의 팩토리 메소드를 사용하지 않고 직접 클래스의 인스턴스를 할당하고 초기화하는 경우, 또 다른 인스턴스가 할당되지 않도록 allocWithZone: 메소드를 재정의한다. 재정의한 메소드는 단순히 공유된 객체를 리턴한다.
  • 싱글톤 상태를 유지하는데 필요한 작업을 수행하기 위해 기본적인 프로토콜 메소드를 구현한다. 이러한 메소드에는 copyWithZone:, release, retain, retainCount, autorelease가 포함된다. (마지막 4개의 메소드는 메모리 관리 코드에서만 적용된다.)

[/list]

싱글톤 인스턴스을 원하지만 필요한 경우에는 할당과 초기화를 통해 또 다른 인스턴스를 생성할 수 있는 능력도 가지도록 하려면 Listing 2-15에 있는 allocWithZone: 메소드와 이 메소드 뒷부분에 있는 메소드들을 재정의하지 않아야 한다.

 

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

Filed under: Apple Developer Document, iOS