ProgrammingInObjectiveC:Chapter 11

From 흡혈양파의 번역工房
Jump to navigation Jump to search
11장
카테고리와 프로토콜

11장 :: 카테고리와 프로토콜

이 장에서는 카테고리를 사용하여 모듈방식 으로 클래스에 메서드를 추가하는 방법과 다른 사람들이 구현할 메서드의 표준화된 목록을 만드는 방법을 배운다.


카테고리

클래스 정의를 다루던 도중 새 메서드를 추가하고 싶을 때가 있을 것이다. 이를테면 Fraction 클래스에서 두 분수를 더하는 add: 메서드 외에도 뺄셈, 곱셈, 나눗셈 을하는 메서드가 필요할 수도 있다.

다른 예로, 큰 프로젝트를 작업하고 있는데, 당신이 속한 그룹에서 프로젝트의 일부로 다양한 메서드가 담긴 새 클래스를 정의한다고 해보자. 당신을 이 클래스에서 파일시스템을 다루는 메서드를 작성하는 업무를 맡았다. 다른 팀원들옹 클래스를 생성하고 초기화하며, 객체에 작업을 수행하고, 객체를 화면에 표시하는 메서드를 맡았다.

마지막 예로, 라이브러리에서 클래스(예컨대, Foundation 프레임워크의 배열 클래스인 NSArray) 를 사용하는 방법을 배웠고, 이 클래스에 메서드가 하나 이상 더 구현되기를 바라는 상황이다. 물론 NSArray 클래스의 서브클래스를 새로 작성하고 새 메서드를 구현할 수도 있지만, 더 쉬운 방법이 었다.

이 모든 상황에 실용적인 해결책이 있으니 바로 '카테고리'다. 카테고리는 클래스 정의를 그룹짓거나, 연관된 메서드를 카테고리로 쉽게 모듈러화할 수 있게 해준다. 또한 원본 소스코드에 접근하거나 서브클래스를 생성하지 않고도 현존하는 클래스의 정의를 쉽게 확장하는 방법도 제공한다. 카테고리는 강력하면서도 배우기 쉬운 기법이다.

자, 그럼 Fraction 클래스로 돌아가 보자. 이 클래스에 사칙 연산을 처리하는 카테고리를 추가하는 방법을 알아보자. 먼저 Fraction 클래스의 인터페이스를 보자.

#import <Foundation/Foundation.h>

// Fraction 클래스를 정의한다.

@interface Fraction : NSObject
{
    int numerator;
    int denominator;
}

@property int numerator, denominator;
-(void) setTo: (int) n over: (int) d;
-(Fraction *) add: (Fraction *) f;
-(void) reduce;
-(double) convertToNum;
-(void) print;
@end


이제 add: 메서드를 이 인터페이스 부분에서 제거하고, 다른 세 연산 메서드와 함께 구현할 카테고리로 옮기겠다. 새 카테고리인 MathOps 의 인터페이스 부분은 다음과 같다.

#import "Fraction.h"
@interface Fraction (MathOps)
-(Fraction *) add: (Fraction *) f;
-(Fraction *) mul: (Fraction *) f;
-(Fraction *) sub: (Fraction *) f;
-(Fraction *) div: (Fraction *) f;
@end


이는 인터페이스 부분을 정의한 코드인데, 이미 존재하는 인터페이스를 확장한 것이다. 따라서 (새 카테고리를 원래 클래스 헤더파일인 Fraction.h 에 추가하지 않는한) 원래 인터페이스를포함시켜야 컴파일러가 Fraction 클래스에 대해 알 수 있다.

예제에 있는 #import 문의 다음 줄을 보자.

@interface Fraction (MathOps)


이 코드는 컴파일러에게 Fraction 클래스의 새 카테고리로 MathOps 를 정의한다고 알린다. 카테고리 이름은 클래스 이름 다음에 괄호로 감싸서 적어 준다. 여기서 Fraction 의 부모 클래스를 언급하지 않음에 주의하자. 컴파일러는 Fraction.h 에서 이미 이 정보를 알게 되었다. 앞에서 정의한 인터페이스 부분과 달리, 인스턴스 변수에 대해서도 따로 언급하지 않는다. 사실, 부모 클래스나 인스턴스 변수를 나열하면 컴파일러가 구문 오류를 표시할 것이다.

이 인터페이스 부분은 컴파일러에게 MathOps 카테고리를 Fraction 이라는 클래스에 추가하여 클래스를 확장할 것이라고 알려 준다. MathOps 카테고리는 add:, mul:, sub:, div: 이라는 네 가지 메서드를 포함한다. 각 메서드는 분수를 인수와 반환값으로 사용한다.

하나의 구현부에 모든 메서드 정의를 넣을 수 있다. 이것은 Fraction.h 의 인터페이스 부분에 있는 모든 메서드와 MathOps 카테고리에 든 메서드를 몽땅 단일한 구현 부분에서 정의할 수 있다는 의미다. 혹은 카테고리의 메서드를 별도의 구현부에 정의해도 된다. 이런 경우, 이 메서드들을 구현한 부분은 메서드가 속한 카테고리가 어딘지를 언급해 주어야한다. 인터페이스 부분에서와 마찬가지로 클래스이름 뒤쪽에 카테고리 이름을 괄호로감싸면 된다.

@implementation Fraction (MathOps)

    // 카테고리 메서드의 코드
    ...
@end


프로그램 11.1 을 보면, 새로 만든 카테고리인 MathOps 의 인터페이스와 구현 부분이 테스트 루틴과 함께 단일 파일에 작성되었다.


프로그램 11.1 MathOps 카테고리와 테스트 프로그램


#import "Fraction.h"

@interface Fraction (MathOps)
-(Fraction *) add: (Fraction *) f;
-(Fraction *) mul: (Fraction *) f;
-(Fraction *) sub: (Fraction *) f;
-(Fraction *) div: (Fraction *) f;
@end

@implementation Fraction (MathOps)
-(Fraction *) add: (Fraction *) f

{
    // 두 분수를 더하는 방법:
    // a/b + c/d = ((a*d) + (b*c)) / (b * d)

    Fraction *result = [[Fraction alloc] init];
    int resultNum, resultDenom;

    resultNum = (numerator * f.denominator) +
            (denominator * f.numerator);
    resultDenom = denominator * f.denominator;

    [result setTo: resultNum over: resultDenom];
    [result reduce];

    return result;
}

-(Fraction *) sub: (Fraction *) f
{
    // 두 분수를 빼는 방법:
    // a/b - c/d = ((a*d) - (b*c)) / (b * d)

    Fraction *result = [[Fraction alloc] init];
    int resultNum, resultDenom;

    resultNum = (numerator * f.denominator) -
            (denominator * f.numerator);
    resultDenom = denominator * f.denominator;

    [result setTo: resultNum over: resultDenom];
    [result reduce];

    return result;
}

-(Fraction *) mul : (Fraction *) f
{
    Fraction *result = [[Fraction alloc] init];

    [result setTo: numerator * f.numerator
            over: denominator * f.denominator];
    [result reduce];

    return result;
}

-(Fraction *) div: (Fraction *) f
{
    Fraction *result = [[Fraction alloc] init];

    [result setTo: numerator * f.denominator
            over : denominator * f.numerator];
    [result reduce];

    return result;
}
@end

int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    Fraction *a = [[Fraction alloc] init];
    Fraction *b = [[Fraction alloc] init];
    Fraction *result;

    [a setTo: 1 over: 3];
    [b setTo: 2 over: 5];

    [a print]; NSLog (@" +"); [b print]; NSLog (@"-----");
    result = [a add: b];

    [result print];
    NSLog (@"\n");
    [result release];

    [a print]; NSLog (@" -"); [b print]; NSLog (@"-----");
    result = [a sub: b];
    [result print];
    NSLog (@"\n");
    [result release];

    [a print]; NSLog (@" *"); [b print]; NSLog (@"-----");
    result = [a mul: b];
    [result print];
    NSLog (@"\n");
    [result release];

    [a print]; NSLog (@" /"); [b print]; NSLog (@"-----");
    result = [a div: b];
    [result print];
    NSLog (@"\n");
    [result release];
    [a release];
    [b release];

    [pool drain];
    return 0;
}

프로그램 11.1 의 출력결과


1/3
+
2/5
-----
11/15

1/3
-
2/5
-----
-1/15

1/3
*
2/5
-----
2/15

1/3
/
2/5
-----
5/6


다시 한 번 말하지만, Objective-C 에서 다음과 같은 명령문을 사용해도 아무런 문제가 없다.

[[a div: b] print];


이 코드는 Fraction a 를 b 로 나눈 결과를 바로 출력해 준다. 프로그램 11.1 처럼 변수 result 에 값을 할당하는 증가 과정을 생략한 것이다. 그러나 결과로 나온 Fraction 을 잡아 메모리를 릴리스해 주려면 이 증가 할당 과정이 필요하다. 이렇게하지 않으면, 분수 연산을 수행할 때마다 프로그램에서 메모리 누수가 발생할 것이다.

프로그램 11.1 은 새 카테고리의 인터페이스와 구현 부분을 테스트 프로그램과 함께 한 파일에 담았다. 앞에서 언급했듯이, 이 카테고리의 인터페이스 부분은 원본 Fraction.h 혜더파일에 포함되어, 모든 메서드를 한 장소에서 선언하거나, 자신만의 헤더파일을 따로 가질 수도 었다.

만일 카테고리를 마스터 클래스 정의 파일에 넣어 두면, 해당 클래스의 사용자는 모두 그 카테고리의 메서드를 사용할 수 있다. 만일 원본 헤더파일을 직접 수정할 수 없다면(2부 「Foundation 프레임워크」에서처럼 라이브러리에 이미 있는 클래스에 카테고리를추가하는 경우를 생각해 보라), 다른 파일에 담는 방법뿐이다.


카테고리에 대한 부연

카테고리에 대해 몇가지 더 언급할부분이 었다. 먼저,카테고리는 원래 클래스의 인스턴스 변수에 접근할 수는 있지만 새 인스턴스 변수를 추가할 수는 없다. 인스턴스 변수를 추가해야 한다면, 서브클래스를만드는 것을 고려해야 한다.

또한, 카테고리는 클래스에 있는 메서드를 재정의할 수 있는데, 이 방법은 보통 나쁜 프로그래밍 습관으로 여겨진다. 한 가지 이유를 대자면, 메서드를 재정의한 후에 원래 메서드에 접근할 방법이 없어지기 때문이다. 따라서 재정의를 선택할경우, 원래 메서드의 기능을 모두추가해 줘야 한다. 만일 메서드를 재정의해야 한다면, 서브클래스를 만드는 것이 올바른 선택일 것이다. 서브클래스에서 메서드를 재정의하면 super 에 메시지를 보내 부모 클래스의 메서드를 계속 사용할 수 있다. 따라서 재정의하는 메서드의 복잡한 내부 사정을 이해할 필요가 없다. 서브클래스의 메서드에서 부모 메서드를 호출하고 새 기능을 추가해 주기만 하면 된다.

여기서 설명한 규칙만 지키면 원하는 수만큼 카테고리를 만들 수 있다. 만일 한 메서드가 여러 카테고리에 정의되어 있다면 Objective-C 는 어느 메서드를 사용하게 될지 지정해 주지 않는다.

일반적인 인터페이스 부분과 달리 카테고리의 모든 메서드를 구현할 필요가 없다. 일단 모든 메서드를 카테고리에 정의하고, 추후에 점차 구현해 나가면 되므로, 점진적 개발을 할 때 카테고리가 매우 유용하다.

클래스에 카테고리로 새 메서드를 추가하면, 해당 클래스뿐 아니라 서브클래스에도 영향을 준다. 그 때문에 예상치 못한 상황이 발생할 수도 있다. 만일, 루트 객체인 NSObject 에 새 메서드를 추가했다면, 모든 클래스가 새 메서드를 상속받게 된다.

개인적인 용도라면 이미 존재하는 클래스에 카테고리로 메서드를 추가해도 괜찮겠지만, 해당 클래스의 의도나 원래 디자인과는 맞지 않을 수도 있다. 예를 들어, (과장해서 말하는 것이지만)카테고리를 새로 만들어 클래스에 몇몇 메서드를 추가해서 Square 를 Circle 로 만들었다고 해보자. 이는 클래스정의를 더럽히며, 좋은 프로그래밍 습관이라고 할 수 없다.

또한, 특정 객체에 대한 카테고리 이름은 반드시 유일무이해야 한다. 주어진 Objective-C 이름 공간에서 NSString (Private) 은 단 하나만 존재할 수 있다. 이는 Objective-C 이름 공간이 프로그램 코드와 모든 라이브러리, 프레임워크, 플러그인에서 공유되기 때문에 문제될 여지가 있다. 특히 화면보호기, 환경설정 구획 (Preferences Pane), 그 외 플러그인을 작성하는 Objective-C 프로그래머들에게는 더 중요한 문제다. 자신이 조절할 수 없는 응용 프로그램이나 프레임워크 코드에 자신의 코드가 주입되기 때문이다.


프로토콜

'프로토콜'은 클래스 사이에서 공유되는 메서드 목록이다. 프로토콜에 나열된 메서드들은 해당하는 구현 부분이 없다. (여러분과 같은) 다른 사람이 구현하도록 되어 있다. 프로토콜은 특정 이름과 관련된 메서드 모음을 정의하는 방법을 제공한다. 이 메서드들은 보통 문서화되어 어떤식으로 동작하는지 알려 준다. 그덕에 프로그래머가 원한다면 자신의 클래스정의에서 이것들을 구현할 수도 있다.

만일 특정 프로토콜에서 요구하는 메서드를 모두 구현하기로 했다면, 이는 프로토콜을 '따른다' 혹은 '받아들인다'고 말한다.

프로토콜을 정의하기는 쉽다. 그저 프로토콜 이름 앞에 @protocol 지시어를 붙이면 된다. 그다음에 인터페이스 부분에서 한 것과 동일하게 메서드를 선언하면 된다. 그러면 @end 지시어가 나오기 전까지 선언되는 메서드는 모두 프로토콜의 일부가 된다.

만일 Foundation 프레임워크를 사용한다면 이미 정의된 몇몇 프로토콜을 발견할 수 있다. 그중 하나인 NSCopying 은 클래스에서 copy(혹은 copyWithZone:) 메서드로 객체 복사를 지원할 때 구현해야할 메서드를 선언한다(18장 「객체 복사하기」에서 객체 복사를 상세히 다룬다).

표준 Foundation 혜더파일 NSObject.h 에서 NSCopying 프로토콜이 어떻게 정의되었는지 살펴보자.

@protocol NSCopying
- (id)copywithZone: (NSZone *)zone;
@end


만일 클래스에서 NSCopying 프로토콜을 받아들이려면 copyWithZone: 이라는 메서드를 구현해야 한다. 컴파일러에게 프로토콜을 받아들인다고 알려주려면 @interface 줄에 프로토콜의 이름을 꺾쇠(< >)로 감싸야 한다. 프로토콜 이름은 클래스 이름과 부모클래스 이름다음에 자리 잡는다.

@interface AddressBook: NSObject <NSCopying>


이 코드에서는 AddressBook 객체는 그 부모가 NSObject 이고 NSCopying 프로토콜을 따른다는 것을 보여준다. 프로토콜에 정의된 메서드에 대해 시스템이 이미 알고 있기 때문에(이 경우 NSObject.h 헤더파일에서 알아낸다) 인터페이스 부분에서 메서드를 선언하지는 않는다. 그러나 구현 부분에서는 메서드를 정의해 줘야한다. 이 예제를 보면 AddressBook 의 구현 부분에서 컴파일러는 copyWithZone: 메서드의 정의가 있다고 예상한다.

만일 클래스가 프로토콜을 하나이상 받아들인다면 꺾쇠(< >) 안에 쉼표(,)로 구분해서 나열해 주면 된다.

@interface AddressBook: NSObject <NSCopying, NSCoding>


이 코드는 컴파일러에게 AddressBook 클래스가 NSCopying 과 NSCoding 프로토콜을 따른다고 알려준다. 다시 말하지만, 컴파일러는 AddressBook에 이 두 프로토콜의 메서드를 모두 구현하도록 요구한다.

만일 프로토콜을 직접 정의하더라도 구현할 필요는 없다. 그러나 그 프로토콜을 받아들이려면 메서드를 구현해야 한다는 점을 다른프로그래머들에게 알려주어야 한다. 이 메서드들은 수퍼클래스에서 상속받을수도 있다. 따라서 한 클래스가 NSCopying 프로토콜을 따른다면, 서브클래스도 이 프로토콜을 따른다(물론,이것이 메서드가 서브클래스에 맞게 구현된다는 의미는 아니다).

프로토콜을 사용하면, 당신이 만든 클래스의 서브클래스를 만드는사람들이 구현하고자 하는 메서드를 정의할 수 있다. 예를 들어 GraphicObject 클래스를 위한 Drawing 프로토콜을 정의할 수 있다. 그 안에 다음과 같이 그리기, 지우기, 테두리 그리기 등의 메서드를 정의하는 것이다.

@protocol Drawing
-(void) paint;
-(void) erase;
@optional
-(void) outline;
@end


GraphicObject 클래스를 여러분이 만들었더라도, 이 그리기 메서드들을 반드시 직접 구현해야 하는 것은 아니다. 그러나 GraphicObject 클래스의 서브클래스를 만드는 사람이 있다면 그가 만들고자하는 그림 객체의 표준을 따르기 위해 구현해야하는 메서드를 지시해줄 수 있다.


objc2_notice_01
@optional 지시어를 사용하였다. 이 지시어 다음에 위치하는 메서드들은 선택 사항이다. 즉, Drawing 프로토콜을 구현하는 이가 메서드를 구현하지 않아도 프로토콜을 따를 수 있다는 얘기다(또한, 프로토콜 정의에서 @required 를 다시 사용하면 필수 메서드 목록을다시 작성할 수 있다).


따라서 GraphicObject 의 서브클래스로 Rectangle 을 만들고 Rectangle 클래스가 Drawing 프로토콜을 따른다고 알린다면(문서화한다면), 클래스 사용자는 이 클래스의 인스턴스에 paint, erase 메시지를, (아마도) outline 메시지를 보낼 수 있음을 알게된다.


objc2_notice_01
물론, 이 이야기는 다 이론적이다. 컴파일러는 프로토콜에 정의한다고 하고, 메서드를 구현하지 않았을 때만 경고 메시지를 표시한다.


여기서 이 프로토콜은 어느 클래스도 참조하지 않는다. 즉, 클래스가 없는 프로토콜이다. 따라서 GraphicObject 의 서브클래스뿐 아니라 어느 객체든 Drawing 프로토콜을 따를 수 있다.


객체가 프로토콜에 따르는지를 알아보려면 conformsToProtocol: 메서드를 사용한다. 예를 들어, currentObject 라는 객체를 소유했고, 이 객체가 Drawing 프로토콜을 따르는지 보려면 다음과 같은 코드를 작성한다.

id currentObject;
    ...
if ([currentObject conformsToProtocol: @protocol (Drawing)] == YES)
{
    // currentObject에 paint, erase, outline 메시지를 보낸다.
    ...
}


여기서 사용된 @protocol 지시어는 프로토콜 이름을 받아 conformsToProtocol: 메서드가 인수로받는 Protocol 객체를 생성한다.

변수를선언할 때,다음과같이 꺾쇠(<>) 안에 프로토콜 이름을 적어 컴파일러가 프로토콜을 준수하는지 검사하는 것을 도울 수 있다.

id <Drawing> currentObject;


이 코드는 컴파일러에게 currentObject 가 Drawing 프로토콜을 따르는 객체를 담는다고 알려 준다. 만일 Drawing 프로토콜을 따르지 않는 정적 객체를 할당하면(Square 클래스가 이 프로토콜을 따르지 않는다고 하면) 컴파일러는 다음과 같은 경고 메시지를 표시할 것이다.

warning: class 'Square' does not implement the 'Drawing' protocol


이것은 컴파일러가 체크한 결과이므로, currentObject 에 id 변수를 대입해 준다면 id 변수에 저장된 객체가 Drawing 프로토콜에 따르는지를 컴파일러가 알 수 없기 때문에 위와같은 경고를 표시하지 않을것이다.


프로토콜을 하나 이상 따르는 변수의 경우 다음과 같이 여러 프로토콜을 나열할 수도 있다.

id <NSCopying, NSCoding> myDocument;


프로토콜을 정의할 때, 이미 존재하는 프로토콜을 확장해도 된다. 다음 프로토콜 선언은 Drawing3D 프로토콜이 Drawing 프로토콜을 따르고 있음을 나타낸다.

@protocol Drawing3D <Drawing>


따라서 Drawing3D 프로토콜을 따르는 클래스는 이 프로토콜에 나열된 메서드를 구현해야할 뿐 아니라 Drawing 프로토콜의 메서드도 구현해야 한다.

마지막으로 카테고리도 프로토콜을 받아들일 수 있다.

@interface Fraction (Stuff) <NSCopying, NSCoding>


여기서 Fraction은 (비록 멋진 이름은 아니지만)Stuff 라는 카테고리를 갖고, 이 카테고리는 NSCopying, NSCoding 프로토콜을 받아들인다.

클래스 이름과 마찬가지로 프로토콜 이름도 유일무이 해야한다.


비공식 프로토콜

이 책을 읽는 도중에 '비공식 프로토콜'의 개념을 머릿속에 떠올렸을 수도 있다. 비공식 프로토콜은 메서드들을 나열하지만 구현하지 않는 카테고리다. 모든 객체는(혹은 거의 모든 객체는) 동일한 루트 객체에서 상속받기 때문에, 비공식 프로토콜은 보통 루트 클래스에 정의된다. 이따금 비공식 프로토콜은 '추상 프로토콜' 로 부르기도 한다.

NSScriptWhoseTests.h 헤더파일을 보면 다음과 같은 메서드 선언이 보일 것이다.

@interface NSObject (NSComparisonMethods)
- (BOOL)isEqualTo:(id)object;
- (BOOL)isLessThanOrEqualTo:(id)object;
- (BOOL)isLessThan:(id)object;
- (BOOL)isGreaterThanOrEqualTo:(id)object;
- (BOOL)isGreaterThan:(id)object;
- (BOOL)isNotEqualTo:(id)object;
- (BOOL)doesContain:(id)object;
- (BOOL)isLike:(NSString *)object;
- (BOOL)isCaseInsensitiveLike:(NSString *)object;
@end


이 코드는 NSObject 클래스를 위한 NSComparisonMethos 카테고리를 정의한다. 이 비공식 프로토콜은 프로토콜의 일부로 구현될 메서드들을 나열한다(여기서는 아홉 개가 나열되어 있다). 비공식 프로토콜은 사실 한 이름 안에 묶인 메서드 목록에 불과하다. 이 방법을 사용하면 메서드를 문서화하고 모듈화하는 데 도움이된다.

비공식 프로토콜을 선언하는 클래스는 프로토콜의 메서드를 직접 구현하지 않고, 서브클래스가 필요에 따라 구현할 메서드들을 선택하여 인터페이스 부분에서 다시 선언하고 메서드를하나 이상 구현한다. 공식 프로토콜과 달리, 컴파일러는 비공식 프로토콜에 아무런 조언도 해주지 않는다. 즉, 컴파일러에 의한 프로토콜 적합성(conformance)이나 테스트 같은 개념이 아예 존재하지 않는다.

객체가 공식 프로토콜을 받아들인다면 객체는 반드시 프로토콜이 요구하는 필수 메시지들을 모두 받아들여야 한다. 런타임 시에만이 아니라 컴파일시에도 이 규칙을 따라야 한다. 만일 객체가 비공식 프로토콜을 받아들인다면, 프로토콜의 모든 메서드를 받아들이지 않아도 된다. 비공식 프로토콜의 적합성은 런타임 시에 (respondsToSelector: 를 사용하여) 강제할 수 있지만 컴파일할 때는 강제할 수 없다.


objc2_notice_01
앞에서 다룬 @optional 지시어는 비공식 프로토콜을 대체하고자 Objective-C 2.0 에 추가되었다. UIKit 클래스에서 이 지시어가 사용되는 경우를 발견할 수 있다(UIKit은 Cocoa Touch 프레임워크의 일부다).


복합객체

지금껏 서브클래스, 카테고리 사용, 프로토콜 등으로 클래스 정의를 확장하는 기법들을 배웠다. 또다른 기법이 있다. 다른 클래스에서 객체를 하나 이상 받아와서 클래스를 정의하는 것이다. 이 새 클래스의 객체는 다른 객체들로 구성되어 있어 '복합 객체' 라고 부른다.

예를들어, 8장 「상속」에서 정의한 Square 클래스를 생각해 보자. 정사각형은 각변의 길이가 같은 직사각형의 일종이므로 이 클래스를 Rectangle 의 서브클래스로 정의하였다. 서브클래스로 정의되면 부모 클래스에서 메서드와 인스턴스 변수를 모두 상속받는다. 어떤 경우에는 이렇게 상속받는 것을 원치 않을 때도 있다. 예를들어, 부모 클래스에 정의된 메서st 서브클래스에서 사용하기에 적합하지 않은 경우가 있다. Rectangle 의 setWidth:andHeight: 메서드는 Square 클래스에 상속되지만, 정사각형에 적용되지는 않는다(물론 코드는 정상으로 동작한다). 게다가 서브클래스를 생성할 때 클래스의 사용자가 접근하게 되므로, 상속받은 메서드들이 모두 정상동작하도록 해야한다.

서브클래스를 만드는 대신, 확장하고싶은 클래스의 객체 하나를 인스턴스 변수로 포함하는 새 클래스를 정의해도 된다. 그후 새 클래스에만 적절한 메서드를 정의하면 된다. Square 의 예로 돌아가서 이런 식으로 정의해 보자.

@interface Square: NSObject
{
    Rectangle *rect;
}
-(int) setSide: (int) s;
-(int) side;
-(int) area;
-(int) perimeter;
@end


여기서 정의한 Square 클래스는 메서드를 네 개 갖고 있다. Rectangle의 메서드(setWidth:, seHeight:, setWidth:setHeight:, width, height) 에 바로 접근할 수 있었던 서브클래스 버전과 달리 이 버전에 나오는 메서드들은 Square 의 메서드가 아니다. 이 메서드들이 정사각형을 다루기에 적합하지 않으므로 이런 식으로 접근하는것이 적절하다.

Square 를 이런 식으로 정의할 때, 포함된 사각형의 메모리 할당을 책임져야 한다. 예를 들어, 메서드를 재정의하지 않고 다음 명령문을 작성하였다고 해보자.

Square *mySquare = [[Square alloc] init];


이 명령문은 새 Square 객체를 생성한다. 그러나 그 안에 담긴 인스턴스 변수 rect 에 저장될 Rectangle 객체는 생성하지 않는다.

해결책은 init을 재정의하거나 initWithSides: 와 같은 새 메서드를 추가하여 메모리를 할당하는 것이다. 이 메서드에서 Rectangle rect 를 생성하고 변을 적절히 설정해 주면 된다. 또한 dealloc 메서드를 재정의하여(8장의 Rectangle 클래스에서 어떻게 하는지 설명하였다) Square 자신이 메모리에서 해제될 때, Rectangle rect 가 차지하던 메모리를 릴리스해 주어야 한다.

Square 클래스에서 메서드를 정의할 때, 여전히 Rectangle 클래스의 메서드를 사용할수있다. 예를들어, 다음 area 메서드의 구현 부분을 보자.

- (int) area
{
    return [rect area];
}


나머지 메서드를 구현하는 작업은 연습문제로 남겨 두었다(연습문제 5를 보라).


연습문제

1. 프로그램 11.1 의 mathOps 카테고리를 확장하여 invert 메서드를 작성하라. 이 메서드에서 반환하는 Fraction 은 수신자를 뒤집는 것이다.


2. Fraction 클래스에 Comparison 이라는 카테고리를 추가하라. 이 카테고리에서 다음 두 메서드를 추가하라.

-(BOOL) isEqualTo: (Fraction *) f;
-(int) compare: (Fraction *) f;


첫 메서드는 두 분수가 동일할 때 YES를 반환하고, 그 외에는 NO를 반환한다. 분수를 비교할 때는 실수하지 않도록 주의를 기울이자(예를 들어, 3/4 과 6/8 을 비교하면 YES를 반환해야 한다).

두 번째 메서드는 수신자가 인자로 넘어오는 분수보다 작을 때 -1 을 반환하고, 동일할 때는 0 을, 수신자가 더 클 때는 1 을 반환한다.


3. 이 장에서 언급한 비공식 프로토콜 NSComparisonMethods 에 따르는 메서드를 추가하여 Fraction 클래스를 확장하라. 프로토콜의 메서드를 여섯 개 구현하고(isEqllalTo:, isLessThanOrEqllalTo:, isLessThan:, isGreatOrThanOrEqllalTo:, isGreaterThan:, isNotEqllalTo:) 테스트해 보라.


4. 함수 sin(), cos(), tan() 은 (scanf 와 마찬가지로) 표준 라이브러리에 포함되어 있다. 이 함수들은 헤더파일 math.h 에 선언되어 있으며, 다음과 같이 프로그램에 불러들인다.

#import <math.h>


이 함수들을 사용하여 라디안(mdian) 값으로 표현된 double 인자의 사인, 코사인, 탄젠트를 계산할 수 있다. 함수의 반환 값도 배정밀도 부동소수점 수(dooble)다. 따라서 다음 코드를 사용하여 라디안으로 표현한 각도 d 의 사인값을 구할 수 있다.

result = sin(d);


6장 「의사결정하기」 에서 구현한 Calculator 클래스에 Trig 라는 카테고리를 추가하라. 이 카테고리에, 다음 선언에 따라 사인, 코사인, 탄젠트를 계산하는 메서드를 추가하라.

-(double) sin;
-(double) cos;
-(double) tan;


5. 이 장에서 다룬 복합 객체와 다음 인터페이스를 가지고 Square의 구현부분과 메서드를 테스트할 테스트 프로그램을 작성하라.

@interface Square: NSObject
{
    Rectangle *rect;
}
-(Square*) initWithSide: (int) s;
-(void) setSide: (int) s;
-(int) side;
-(int) area;
-(int) perimeter;
-(void) dealloc; // Rectangle 객체의 메모리를 릴리스하기 위해 재정의한다.
@end


Notes