ProgrammingInObjectiveC:Chapter 09

From 흡혈양파의 번역工房
Jump to navigation Jump to search
9장
다형성,동적 타이핑,동적 바인딩

9장 :: 다형성,동적 타이핑,동적 바인딩

이 장에서는 Objective-C 를 더욱 강력한 프로그래밍 언어로 만들어 주고, c++ 같은 다른 객체지향 언어와 구분 짓는 특징을 배운다. 이 특정에는 다음 세 가지 핵심요소가 포함된다. 바로 다형성 (Polymolphism), 동적 타이핑, 동적 바인딩 이다. '다형성' 은 다른 클래스의 객체들이 동일한 메서드 이름을 사용할 수 있도록 해준다. '동적 타이핑' 은 객체가 속한 클래스를 알아내는 단계를 프로그램이 실행될때로 미룬다. '동적 바인딩'은 객체에 호출되는 실제 메서드를 알아내는 시기를 프로그램 실행 중으로 미룬다.


다형성 - 동일한 이름, 다른 클래스

프로그램 9.1 은 복소수를 다루는 Complex 클래스의 인터페이스 파일이다.


프로그램 9.1 Complex.h 인터페이스 파일


// Complex 클래스의 인터페이스 파일

#import <Foundation/Foundation.h>

@interface Complex: NSObject
{
    double real;
    double imaginary;
}

@property double real, imaginary;
-(void) print;
-(void) setReal: (double) a andImaginary: (double) b;
-(Complex *) add: (Complex *) f;
@end


이 클래스를 구현한 부분은 7장 「클래스에 대해서」의 연습문제 6번과 7번에서 완성했을 것이다. 거기에 setReal:andImaginary: 메서드를 추가하여 메시지 하나로 실수부와 허수부를 한꺼번에 설정하였다. 또한 접근자 메서드를 자동으로 생성하였다. 다음의 구현 파일을 살펴보자.


프로그램 9.1 Complex.m 구현 파일


// Complex 클래스의 구현 파일

#import "Complex.h"

@implementation Complex

@synthesize real, imaginary;

-(void) print
{
    NSLog (@"%g + %gi ", real, imaginary);
}

-(void) setReal: (double) a andImaginary: (double) b
{
    real = a;
    imaginary = b;
}

-(Complex *) add: (Complex *) f
{
    Complex *result = [[Complex alloc] init];
    [result setReal: real + [f real]
    andImaginary: imaginary + [f imaginary]];
    return result;
}
@end



프로그램 9.1 main.m 테스트 프로그램


// 공유된 메서드 이름:다형성

#import "Fraction.h"
#import "Complex.h"

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

    Fraction *f1 = [[Fraction alloc] init];
    Fraction *f2 = [[Fraction alloc] init];
    Fraction *fracResult;
    Complex *c1 = [[Complex alloc] init];
    Complex *c2 = [[Complex alloc] init];
    Complex *compResult;

    [f1 setTo: 1 over: 10];
    [f2 setTo: 2 over: 15];

    [c1 setReal: 18.0 andImaginary: 2.5];
    [c2 setReal: -5.0 andImaginary: 3.2];

    // 두 개의 허수를 더하고 결과를 표시한다.

    [c1 print]; NSLog (@"          +"); [c2 print];
    NSLog (@"---------");
    compResult = [c1 add: c2];
    [compResult print];
    NSLog (@"\n");

    [c1 release];
    [c2 release];
    [compResult release];

    // 두 개의 분수를 더하고 결과를 표시한다.
    [f1 print]; NSLog (@"      +"); [f2 print];
    NSLog (@"----");
    fracResult = [f1 add: f2];
    [fracResult print];

    [f1 release];
    [f2 release];
    [fracResult release];

    [pool drain];
    return 0;
}



프로그램 9.1 의 출력결과


18 + 2.5i
+
-5 + 3.2i
---------
13 + 5.7i

1/10
+
2/15
----
7/30


Fraction 과 Complex 클래스 모두 add: 와 print 메서드를 포함한다. 그렇다면 다음 메시지 표현식을 실행할 때, 시스템은 어떤 메서드를 실행할지 어떻게 알아낼 수 있을까?

compResult = [c1 add: c2];
[compResult print];


사실 간단하다. Objective-C 런타임은 첫 메시지의 수신자 c1 이 complex 객체라는 것을 알고 있다. 그러므로 Complex 클래스에 정의된 add: 메서드를 선택한다.

Objective-C 런타임은 compResult 역시 complex 객체임을 확인하고, Complex 클래스에 정의된 print 메서드를 선택하여 덧셈의 결과를 표시한다. 다음 메시지 표현식 에서도 동일한 방식으로 동작한다.

fracResult = [f1 add: f2];
[fracResult print];


objc2_notice_01
13장 「하부 C 언어 기능」에서 더 상세히 다루겠지만, 시스템은 언제나 객체가 속한 클래스에 대한 정보를 가지고 있다. 그 덕분에 이런 핵심적인 결정을 내리는 시기를 컴파일할 때가 아닌 런타임 도중으로 옮길 수 있다.


이 메시지 표현식을 처리할 때 f1 , fracResult 의 클래스인 Fractio n의 메서드가 선택된다.

이미 언급했듯이, 다형성은 다른 클래스들 간에 동일한 메서드 이름을 사용할 수 있는 기능이다. 다형성을 이용하면 동일한 메서드 이름에 각각 응답할 수 있는 클래스 모음을 개발하는 것이 가능하다. 각 클래스 정의에서 특정한 메서드에 필요한 코드를 캡슐화하여 다른 클래스의 정의로부터 독립적으로 만드는 방식으로 말이다. 또한, 다형성을 통해 동일한 이름의 메서드에 응답할수 있는 새로운 클래스를 추가할 수도 있다.


objc2_notice_01
이 절을 마치기에 앞서 다시 한 번 주의할 점이 있다. 테스트 프로그램이 아닌 Fraction 과 Complex 클래스는 각각 자신의 add: 메서드에서 생성한 결과를 릴리스해줄 책임이 있다. 사실, 이 객체들은 오토릴리스 되어야 한다. 이에 대해서는 17장 「메모리 관리」에서 상세히 다룬다.


동적 바인딩과 id형

4장에서 잠시 id 데이터 형을 사용하며 일반 객체형 이라고 언급하였다. 그것은 id 형을 써서 어느 클래스의 객체든 저장할수 있다는의미다. id 형은 프로그램이 실행되는 동안 다양한 데이터 형 객체를 여기에 저장할때에야 비로소 진정한 강점을 맛보게 된다. 프로그램 9.2와 출력 결과를 살펴보자.


프로그램 9.2


// 동적 타이핑과 동적 바인딩의 예

#import "Fraction.h"
#import "Complex.h"

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

    id dataValue;
    Fraction *f1 = [[Fraction alloc] init];
    Complex *c1 = [[Complex alloc] init];

    [f1 setTo: 2 over: 5];
    [c1 setReal: 10.0 andImaginary: 2.5];

    // dataValue에 분수 대입

    dataValue = f1;
    [dataValue print];

    // dataValue에 복소수 대입

    dataValue = c1;
    [dataValue print];

    [c1 release];
    [f1 release];
    [pool drain];
    return 0;
}

프로그램 9.2 의 출력결과


2/5
10 + 2.5i


변수 dataValue 는 id 형 으로 선언되었다. 따라서 dataValue 는 프로그램 내에 있는 객체라면 어느 데이터 형이든 모두 담을수 있다. 선언부에 별표(*)가 사용되지 않았음에 주의하자.

id dataValue;


Fraction f1 은 2/5 로 설정되고 Complex c1 은 10 + 2.5i 로 설정되었다. 다음 할당문에서는 Fraction f1 을 dataValue 에 저장한다.

dataValue = f1;


이제 이 dataValue 로 무엇을 할 수 있을까? 벼록 dataValue 는 Fraction 형이 아니라 id 형이지만, Fraction 객체에 사용하는 모든 메서드를 dataValue 에 호출할 수 있다. 그런데 dataValue 가 어느 형이든 객체를 다 담을수 있다면, 시스템은 어떤 메서드를 호출할지 어떻게 알 수 있을까? 다시 말하자면, 다음 메시지 표현식이 실행될 때 어떤 print 메서드가 호출되어야 하는지 어떻게 알까?

[dataValue print] ;


Fraction 과 Complex 클래스에 모두 print 메서드를 정의했다. 이전에도 언급했지만, 정답은 Objective-C 시스템이 언제나 객체가 속한 클래스를 알고 있다는 사실에 숨어 있다. 또한,동적 타이핑과 동적 바인딩에도 답이 숨어 있다. 이 두 개념을 사용해서 시스템은 컴파일할 때가 아니라 런타임 시에 객체의 클래스가 무엇인지를 알아낸다. 그러므로 호출할 메서드가 무엇인지를 동적으로 알아내게 되는 것이다.

따라서, 프로그램이 실행되는 동안 시스템이 print 메시지를 dataValue 에 보내기 전에 dataValue 에 저장된 객체의 클래스부터 확인한다. 프로그램 9.2의 첫 번째 경우, 이 변수는 Fraction 객체를 담고 있으므로 Fraction 에 정의된 print 메서드가 사용된다. 프로그램을 출력한 결과에서 이를 확인할 수 있다.

두번째 경우, 동일한 과정이 발생한다. 먼저 Complex c1이 dataValue 에 대입된다. 그 후 다음의 메시지 표현식이 실행된다.

[dataValue print];


이번에는 dataValue 가 Complex 클래스에 속한 객체를 담고 있으므로, 실행 시 해당 클래스의 print 메서드가 선택된다.

매우 간단한 예제이지만, 이 개념을 좀더 복잡한 프로그램에도 적용할 수 있다. 다형성과 동적 바인딩, 동적 타이핑을 결합하면 다른 클래스의 객체에 동일한 메시지를 보내는 코드를 쉽게 작성할 수 있다.

예를 들어, 스크린에 그래픽 객체를 칠해 주는 draw 라는 메서드를 생각해 보자. 텍스트, 원, 사각형, 창 같은 그래픽 객체마다 다른 draw 메서드가 정의되어 있을 것이다. 만일 그려야 할 특정 객체가 currentObject 라는 id 형 변수에 저장되어 있다고 해보자. 다음처럼 draw 메시지를 보내 스크린에 그릴 수 있을 것이다.

[currentObject draw];


심지어 currentObject 에 저장된 객체가 draw 메서드에 실제로 응답하는지도 확인해볼수 있다. 이 장 후반부에 있는 '클래스에 대해 질문하기' 절에서 그 방법을 알수 있다.


컴파일 시기와 런타임 확인

id 변수에 저장된 객체의 데이터 형을 컴파일할 때는 정확히 알 수 없기 때문에, 몇몇 테스트는 런타임 시기 즉 프로그램을 실행할 때까지 미루게 된다.

다음 예제코드를 보자.

Fraction *f1 = [[Fraction alloc] init);
[f1 setReal: 10.0 andImaginary: 2.5);


setReal:andImaginary: 메서드가 분수가 아니라 복소수에 적용되기 때문에 이 코드를 포함하는 프로그램을 컴파일하면 다음과 같은 메시지가 뜰것이다.

prog3.m: In function 'main':
prog3.m:13: warning: 'Fraction' does not respond to 'setReal:andImaginary:'


Objective-C 컴파일러는 n 이 선언됨으로써 n 이 Fraction 객체임을 알게 된다. 또한 다음 메시지 표현식을 접하면서, Fraction 클래스가 setReal:andImaginary: 메서드를 보유하지 않았다는 사실도 (그리고 상속받지도 않았다는 사실까지) 알게 된다. 따라서, 컴파일러는 앞에서 본 경고 메시지를 표시할 것이다.

이제 다음코드를 살펴보자.

id dataValue = [[Fraction alloc] init];
[dataValue setReal: 10.0 andImaginary: 2.5];


이 코드를 컴파일하면 컴파일러가 소스파일을 처리할 때 dataValue 에 담긴 객체의 데이터형 을 모르기 때문에, 경고 메시지를 생성하지 않는다.

이 줄이 들어 있는 부분을 실행하기 전까지 오류 메시지는 발생하지 않는다. 이 부분이 실행되면 다음과 유사한 오류 메시지가 뜰것이다.

objc: Fraction: does not recognize selector -setReal:andImaginary:
dynamic3: received signal: Abort trap
When attempting to execute the expression
[dataValue setReal: 10.0 andImaginary: 2.5];


먼저 런타임 시스템은 dataValue 에 담긴 객체의 형을 검사한다. dataValue 가 Fraction 변수를 담고 있으므로, setReal:andImaginary: 메서드가 Fraction 에 정의되어 있는 메서드인지 확인한다. 당연히 정의되지 않았기 때문에 앞의 오류 메시지가 발생하고 프로그램은 종료된다.


id 데이터 형과 정적 타이핑

만일 id 데이터 형이 어느 객체든 담을 수 있다면, 왜 모든객체를 그냥 id 형으로 선언하지 않는 것일까? 이 일반 클래스 데이터 형을 남용하지 않는 데는 몇 가지 이유가 있다.

먼저 정적 타이핑이라는 말은, 특정 클래스의 객체로 변수를 정의하는 것이다. '정적' 이라는 말은 변수가 언제나 그 특정클래스를 저장하는데만 사용된다는 의미다. 따라서 저장된 객체의 클래스는 언제나 이미 정해져 있다. 혹은 '정적이다' 라고 할 수도 있다. 정적 타이핑을 사용하면, 컴파일러는 프로그램에서 변수를 일관성 있게 사용하여 최고 성능을 내게 한다. 컴파일러는 객체에 적용되는 메서드가 그 클래스에 의해 정의되거나 상속되었는지를 확인할수 있고, 그렇지 않은 경우에는 경고 메시지를 표시한다. 따라서 Rectangle 형 변수 myRect 를 선언하면 컴파일러는 myRect 에 대해 호출되는 메서드가 Rectangle 클래스에 정의되었거나 수퍼클래스 에게서 상속 받았는지 확인한다.


objc2_notice_01
변수로 지정한 메서드를 호출하는 기법이 몇 가지 있는데, 그런 기법을 사용하면 메서드의 구현여부 등을 컴파일러가 확인해 주지 못한다.


그러나 런타임 도중에도 이렇게 확인된다면, 정적 타이핑이 뭔지 신경 쓸 필요가 있을까? 대답은 '신경 써야 한다'이다. 이유는 오류를 컴파일 단계 에서 잡아 내는 편이 실행 단계에서 잡는 것보다 낫기 때문이다. 런타임 시까지 미루면 여러분이 아닌 다른사람이 프로그램을 사용하다 오류를 보게 될지도 모른다. 프로그램이 판매되었다면 불쌍한 사용자가 프로그램을 실행하던 도중에 특정 객체가 어떤 메서드에 응답하지 않는다는 걸 알게 될수도 있다.[1]


또 정적 타이핑을 사용하는 이유는 프로그램의 가독성을 높여 주기 때문이다. 다음 선언을 살펴보자.

id f1 ;
Fraction *f1;


둘중 무엇이 더 이해하기 쉬운가? 어느쪽이 변수 f1 를 쓰는 목적을 좀 더 분명히 나타내느냐는 말이다. 이 예제에서는 일부러 사용하지 않았지만, 변수에 정적 타이핑과 의미를 파악하기 쉬운 이름을 붙이면 프로그램 코드 자체가 문서화되는 머나먼 길에 조금 더 가까워진다.


동적 타이핑과 인수, 반환 형

만일 동적 타이핑을 사용하여 메서드를 호출한다면 다음 규칙을 기억해 두자. 하나 이상의 클래스에 동일한 이름의 메서드가 존재한다면, 각 메서드는 인수와 반환 형을 자신이 선언하고 구현한 대로 사용해서 컴파일러가 메시지 표현식에 맞는 코드를 작성할 수 있도록 해줘야 한다.

컴파일러는 자신이 발견한 각 클래스의 선언이 일치하는지를 확인한다. 만일 메서드의 인수나 반환 형이 선언과 다르다면 컴파일러는 경고 메시지를 표시한다. 예를 들어 Fraction 과 Complex 클래스 모두 add: 메서드를 담고 있다. 그러나 Fraction 클래스의 add: 메서드는 인수와 반환 값으로 Fraction 객체를 사용하고 Complex 클래스는 Complex 객체를 쓴다. frac1 과 myFract 는 Fraction 객체이고, comp1 과 myComplex 가 Complex 객체일 때, 다음 명령문에는 아무런 문제가 발생하지 않을 것이다.

result = [myFract add: frac1];
result = [myComplex add: comp1];


두 경우 모두 메시지의 수신자가 정적으로 지정되었고 컴파일러는 수신자의 클래스에서 정의한 대로 메서드를 올바르게 사용하는지 확인할수 있다.

만일 dataValue1 과 dataValue2 가 id 형 변수라면, 다음 명령문은 컴파일러가 코드를 생성하여 add: 메서드에 인수를 넘기고 반환 값을 받는 과정을 추측하여 수행한다.

result = [dataValue1 add: dataValue2];


런타임 도중에 Objective-C 런타임 시스템은 dataValue1 에 실제로 포함된 객체 클래스를 확인하고, 올바른 클래스에서 적절한 메서드를 선택하여 실행한다. 하지만 일반적인 경우, 컴파일러는 메서드에 인수를 넘기는 코드나 반환 값을 처리하는 코드를 부정확하게 생성하기도 한다. 예컨대 한 메서드는 객체를 인수로 받고 다른 메서드는 부동소수점 값을 인수로 받는 경우라던가, 한 메서드는 객체를 반환하는데, 다른 메서드는 정수를 반환하는 경우에 이런 문제가 발생할수도 있다. 두 메서드가 일치하지 않는 부분이 그저 객체의 데이터 형이 다른 것뿐이라고 해보자(예를 들어, Fraction 의 add: 메서드는 Fraction 객체를 인수와 반환 값으로 사용하고, Complex의 add: 메서드는 Complex 객체를 사용한다). 이 경우에도, 컴파일러는 올바른 코드를 생성할수 있는데, 메서드에 넘겨지는 인수는 결국 객체를 참조하는 메모리 주소(포인터)이기 때문이다.


클래스에 대한 의문과 질문

다른 클래스의 객체에 담긴 변수들을 다루기 시작하면, 다음과 같은 질문을 해야할 것이다.

  • 이 객체가 사각형인가?
  • 이 객체가 print 메서드를 지원하는가?
  • 이 객체가 Graphics 클래스의 멤버 혹은 그 자식 클래스의 멤버인가?


이 질문들에 답을 함으로써 프로그램을 실행하면서 다른 코드들을 실행시키거나, 오류를 피하거나, 프로그램이 흠없이 동작하도록 만들수 있다.

표 9.1은 NSObject 클래스가 지원하는 기본 메서드 가운데 이런종류의 질문을 할 때 사용하는 것들을 요약해 놓았다. 이 표에서 class-object 는 클래스 객체(보통 class 메서드로 생성한다)이고 selector는 (보통 @selector 지시어로 만들 수 있는)SEL형의 값이다.

메서드 질문 혹은 액션
-(BOOL) isKindOfClass: class-object 객체가 class-object의 멤버이거나 자식클래스의 멤버인가?
-(BOOL) isMemberOfClass: class-object 객체가 class-object의 멤버인가?
-(BOOL) respondsToSelector: selector 객체가 selector로 지시한 메서드에 응답할수 있는가?
+(BOOL) instancesRespondToSelector: selector 지정한 클래스의 인스턴스가 selector에 응답할 수 있는가?
+(BOOL) isSubclassOfClass: class-object 객체가 지정한 클래스의 서브클래스인가?
-(id) performSelector: selector selector로 지정한 메서드를 적용한다.
-(id) performSelector: selector withObject: object selector로 지정한 메서드와 object 인수를 함께 적용한다.
-(id) performSelector: selector withObject: object withObject: object2 selector로 지정한 메서드와 object1, object2 인수를 함께 적용한다.
표 9.1 동적 형(dynamic type)을 다루는 메서드


여기서 다루지 않는 메서드들도 있다. 그 가운데에는 객체가 프로토콜을 따르는지를 알아내는 메서드가 있으며 (11장 「카테고리와 프로토콜」) , (이 책에서 다루지는 않지만)동적으로 메서드를 알아내는 메서드도 있다.

클래스 이름이나 다른 객체에게서 클래스 객체를 만들어 내려면 클래스 이름이나 객체에 class 메시지를 보낸다. 그러므로 Square 라는 클래스에서 클래스 객체를 얻어내려면 다음과 같이 한다.

[Square class]


만일 mySquare 가 Square 의 인스턴스라면 다음 코드로 클래스를 얻어을 수 있다.

[mySquare class]


변수 obj1 과 obj2 에 저장된 객체가 동일한 클래스의 인스턴스인지 비교하려면 다음과 같이 작성한다.

if ([obj1 class] == [obj2 class])


혹시 변수 myFract 가 Fraction 클래스의 인스턴스인지 알아내려면 다음과 같은 표현식의 결과를 보면 된다.

[myFract isMemberOfClass: [Fraction class]


표 9.1 에 나온 셀렉터를 만들려면 @selector 지시어를 메서드 이름에 적용하면 된다. 예를 들어 다음 코드는 NSObject 클래스에서 상속받는 alloc 이라는 메서드의 SEL형 값을 생성해 준다.

@selector (alloc)


다음과 같이 표현식을 작성하면 Fraction 클래스에서 구현했던 setTo:over: 메서드에 대한 셀렉터가 생성된다(메서드 이름에 콜론을불이는 걸 잊지 말자).

@selector (setTo:over:)


Fraction 클래스의 인스턴스가 setTo:over: 메서드에 응답하는지 알고 싶을 때는 다음과 같은 표현식의 반환값을 확인해 보면 된다.

[Fraction instancesRespondToSelector: @selector (setTo :over:)]


이 테스트는 클래스가 정의하는 메서드뿐 아니라 상속받은 메서드까지 확인한다. performSelector: 메서드와 그 메서드를 변형해서 쓰면(표 9.1 에는 없다) 셀렉터를 변수에 담아 객체에 메시지를 보낼 수 있다. 예를 들어 다음 코드를 살펴보자.

SEL action;
id  graphicObject;
...
action = @selector (draw);
...
[graphicObject performSelector: action];


이 예제에서는 SEL 변수 action 으로 지정한 메서드를 graphicalObject 에 저장된 그래픽 객체에게 보낸다. 여기서는 액션을 draw 라고 적어 놓기는 했지만 프로그램을 실행하는 도중에 (사용자의 입력에 따라) 액션이 달라질 수도 있다. 먼저, 객체가 액션에 응답할 수 있는지를 확인하기 위해 다음과같이 코드를 작성한다.

if ([graphicObject respondsToSelector: action] == YES)
    [graphicObject perform: action]
else
    // 오류 처리 코드


objc2_notice_01
메서드를 재정의하여 오류를 잡아낼 수도 있다. 이 메서드는 클래스가 알지 못하는 메시지가 날라올 때마다 호출되고, 인식하지 못한 셀렉터와 인수를 넘긴다.


다른 방법을 쓸 수도 있다. forward:: 메서드를 사용하여 메시지를 처리할 다른 사람에게 보내거나, 혹은 그냥 일단 보내고 오류가 발생하면 예외를 처리하도록 할 수 있다. 이 기법은 곧 살펴볼 것이다.

프로그램 9.3 은 8장 「상속」 에서 정의한 Square 와 Rectangle 클래스에 대해 몇 가지 질문을한다. 실제 출력된 결과를 보기 전에 이 프로그램의 결과가 어떻게 나을지 예상해 보자(답을 베껴서는 안 된다!).


프로그램 9.3


#import "Square.h"

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

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

    // isMemberOf:
    if ( [mySquare isMemberOfClass: [Square class]] == YES )
        NSLog (@"mySquare is a member of Square class");

    if ( [mySquare isMemberOfClass: [Rectangle class]] == YES )
        NSLog (@"mySquare is a member of Rectangle class");

    if ( [mySquare isMemberOfClass: [NSObject class]] == YES )
        NSLog (@"mySquare is member of NSObject class");


    // isKindOf:
    if ( [mySquare isKindOfClass: [Square class]] == YES )
        NSLog (@"mySquare is a kind of Square");

    if ( [mySquare isKindOfClass: [Rectangle class]] == YES )
        NSLog (@"mySquare is a kind of Rectangle");

    if ( [mySquare isKindOfClass: [NSObject class]] == YES )
        NSLog (@"mySquare is a kind of NSObject");


    // respondsTo:
    if ( [mySquare respondsToSelector: @selector (setSide:)] == YES )
        NSLog (@"mySquare responds to setSide: method");

    if ( [mySquare respondsToSelector: @selector (setWidth:andHeight:)] == YES )
        NSLog (@"mySquare responds to setWidth:andHeight: method");

    if ( [Square respondsToSelector: @selector (alloc)] == YES )
        NSLog (@"Square class responds to alloc method");


    // instancesRespondTo:
    if ([Rectangle instancesRespondToSelector: @selector (setSide:)] == YES )
        NSLog (@"Instances of Rectangle respond to setSide: method");

    if ([Square instancesRespondToSelector: @selector (setSide:)] == YES )
        NSLog (@"Instances of Square respond to setSide: method");

    if ([Square isSubclassOfClass: [Rectangle class]] == YES )
        NSLog (@"Square is a subclass of a rectangle");

    [mySquare release];
    [pool drain];
    return 0;
}


이 프로그램을 빌드할 때는 8장 「상속」 에 나온 Square, Rectangle, XYPoint 의 파일들을 모두 포함시키자.


프로그램 9.3 의 출력결과


mySquare is a member of Square class
mySquare is a kind of Square
mySquare is a kind of Rectangle
mySquare is a kind of NSObject
mySquare responds to setSide: method
mySquare responds to setWidth:andHeight: method
Square class responds to alloc method
Instances of Square respond to setSide: method
Square is a subclass of a rectangle


프로그램 9.3을 출력한 결괴논 명백하다. isMembetOfClass: 가 지정한 클래스에 바로 속하는지를 확인하고, isKindOfClass 가 상속 계층 내에 속하는지를 확인한다는 점을 기억하자. 그러므로 mySquare 는 Square 클래스의 멤버이지만, 상속 계층에 속하므로 Square, Rectangle, NSObject 의 일종(kind)이기도 하다(당연히, 루트객체를 새로 정의하지 않았다면 모든 객체는 isKindOf: 메서드로 NSObject 에 대해 테스트하면 YES 를 반환할 것이다).


다음 테스트를 보자. 이는 Square 클래스가 클래스 메서드 alloc 에 응답하는지 확인한다.

if ( [Square respondsTo: @selector (alloc)] == YES )


결과는 당연히 YES 다. 루트 객체 NSObject 를 상속받기 때문이다. 메시지 표현식의 수신자로 클래스 이름을 직접 쓸 수 있으니, 주의하자. 앞에서 사용한 표현식을 사용하지 않아도 된다(물론, 원한다면 앞의 긴 표현식을 써도 된다).

[Square class]


단, 클래스의 이름을 수신자로 쓸 때만 위 표현식을 적지 않아도 된다. 다른 위치에서 클래스 객체를 얻으려면 class 메서드를 적용해 주어야만한다.


@try를 사용해 예외 처리하기

좋은 프로그래밍 습관은 프로그램에서 발생할 수 있는 문제들을 미리 생각해 보고 그에 대처하는 것이다. 프로그램을 비정상 종료시키는 조건을 테스트하여 메시지를 표시하고, 프로그램을 종료시키거나 다른 액션을 취하는 식으로 문제를 해결할 수 있다. 예를들어, 이 장 앞부분에서 객체가 특정 메시지에 응답할 수 있는지를 테스트하는 방법을 배웠다. 오류를 피하기 위해, 프로그램이 실행되는 동안 이런 테스트를 수행해 객체가 알지 못하는 메시지를 보내지 않도록 할 수 있다. 객체가 알지 못하는 메시지를 보내려 하면, 프로그램은 보통 '예외' 를 날린 후 곧 종료될 것이다.

프로그램 9.4 를 살펴보자.Fraction 클래스에 noSuchMethod 라는 메서드를 정의한적이 없다. 따라서 이 프로그램을 컴파일 하려하면, 경고 메시지를 받는다.


프로그램 9.4


#import "Fraction.h"

int main( int argc, char *argv [])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    Fraction *f = [[Fraction alloc] init];
    [f noSuchMethod];
    NSLog (@"Execution continues!");
    [f release];
    [pool drain];
    return 0;
}

프로그램 9.4 의 출력결과


-[Fraction noSuchMethod]: unrecognized selector sent to instance 0x103280
*** Terminating app due to uncaught exception
'NSInvalidAgumentException',
    reason: '*** -[Fraction noSuchMethod]: unrecognized selector sent
        to instance 0x103280
Stack: (
    2482717003,
    2498756859,
    2482746186,
    2482739532,
    2482797730
)
Trace/BPT trap


경고 메시지를 받더라도 프로그램을 빌드하고 실행할 수 있다. 그러나 경고 메시지를 무시하고 실행하면 프로그램은 비정상 종료하게 되고 위와 비슷한 오류를 보고 말것이다.

이렇게 프로그램이 비정상 종료되는 것을막으려면 어떻게 해야 할까? 다음과 같이 특별한 명령문 블록 안에 명령문들을 넣어 주면 된다.

@try {
    statement
    statement
    ...
}
@catch (NSException *exception) {
    statement
    statement
    ...
}


@try 블록에 있는 각 statement 역시 일반적인 명령문과 동일하게 실행된다. 그러나 블록 안에 있는 이 명령문 가운데 하나가 예외를 날리면, 실행은 종료되지 않고 @catch 블록으로 넘어간다. 그리고 catch 블록 안에서 날라온 예외를 처리하게 된다. 이때 취할수 있는 방법은 오류 메시지를 기록하고 정리한 후 실행을 종료하는 것이다.

프로그램 9.5는 예외를 처리하는 법에 대해 설명한다. 프로그램의 출력 결과를 함께 보자.


프로그램 9.5 예외 처리하기


#import "Fraction.h"

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

    @try {
        [f noSuchMethod];
    }
    @catch (NSException *exception) {
        NSLog (@"Caught %@%@", [exception name], [exception reason]);
    }

    NSLog (@"Execution continues!");

    [f release];
    [pool drain];
    return 0;
}

프로그램 9.5 의 출력결과


*** -[Fraction noSuchMethod]: unrecognized selector sent to instance 0x103280
Caught NSInvalidArgumentException: *** -[Fraction noSuchMethod]:
unrecognized selector sent to instance 0x103280
Execution continues!


예외가 발생하면 @catch 블록이 실행된다. 이 예외에 대한 정보를 담고 있는 NSException 객체가 이 블록의 인수로 넘겨진다. 예제에서 볼 수 있듯이 name 메서드는 예외의 이름을 가져오고, reason 메서드는 예외의 원인을 가져온다(예외원인은 앞에서 본 것처럼 시스템이 자동으로 출력한다). @catch 블록에 있는 마지막 명령문이 실행되고 나면, 프로그램은 그 블록 뒤의 명령문을 이어서 실행한다. 이때 NSLog 를 호출하여 실행이 종료되지 않고 정상적으로 이어졌는지 확인한다.

지금까지 프로그램에서 예외를 처리하는 매우 간단한 예제를 살펴보았다. @finally 블록을 사용하면 @try 블록에서 예외를 던지든 던지지 않든 실행할 코드를 추가할 수도 있다.

@throw 지시어를 쓰면 직접 만든 예외를 던질 수 있다. 이 지시어를 사용하여 특정 예외를 던지거나 다음과 같이 @catch 블록으로 들어오게 만든 예외를 다시한번 던지는 일도 가능하다.

@throw;


예외를 직접 처리하고 난 뒤에(예를들어, 정리 작업을 한후) 예외를 다시 던져 시스템이 나머지 작업을 처리하도록 할 수 있다. @catch 블록을 여러 개 연속사용하여 각각 다른종류의 예외를 처리하도록 할수 있다.


연습문제

1. 프로그램 9.1 에서 덧셈을 하고 compResult 가 릴리스 되기 전에 다음 메시지 표현식을 추가하면 어떻게 될까? 시도해 보고 결과를 살펴보라.

[compResult reduce];


2. 프로그램 9.2에서 정의한 id 변수인 dataValue 에 8장에서 정의한 Rectangle 객체를 대입할 수 있을까? 다시 말해서, 다음 명령문이 유효할까? 가능하다면 이유는 무엇일까?

dataValue = [[Rectangle alloc] init];


3. 8장에 정의한 XYPoint 클래스에 print 메서드를 추가해 보라. (x, y)의 형태로 표시하게 만들자. 그리고 프로그램 9.2 를 수정하여 XYPoint 객체를 추가해 값을 설정하고 id형 변수인 dataValue에 대입한후, 값을 표시하라.


4. 이 장에서 인수와 반환 형에 대해서 논의했던 비를 기반으로 해서 Fraction, Complex 클래스의 add: 메서드를 수정한다. 그리하여 id형 객체를 인수로 받고 반환하도록 만들자. 그후, 다음 코드를 사용하는 프로그램을 작성하라.

result = [dataValue1 add: dataValue2];
[result print];


여기서 Result 와 dataValue1, dataValue2 는 모두 id형 객체다. dataValue1 과 dataValue2 를 적절한 값으로 설정해 주고 프로그램이 종료되기 전에 모든 객체를 릴리스해 주는 것을 잊지 말자.


objc2_notice_01
NSObjectController 클래스 역시 add: 메서드를 가지고 있기 때문에, 메서드 이름을 add:가 아닌 다른 무언가로 바꿔야 한다. 앞서 언급한 것처럼 동일한 이름의 메서드가 다른 클래스에 퍼져 존재하고, 컴파일 시에 메시지 수신자의 형식을 알 수 없다면, 컴파일러는 동일한 이름을 갖는 메서드 간에 인자와 반환형이 일치하는지 확인할 것이다.


5. 이 책에서사용한 Fraction, Complex 클래스에 다음 정의가 있다고 해보자.

Fraction *fraction = [[Fraction alloc] init];
Complex *complex = [[Complex alloc] init];
id number = [[Complex alloc] init];


다음 메시지 표현식의 결과값이 어떻게 될지 알아보라. 그 후 프로그램을 작성해서 결과값을 확인하라.

[fraction isMemberOfClass: [Complex class]];
[complex isMemberOfClass: [NSObject class]];
[complex isKindOfClass: [NSObject class]];
[fraction isKindOfClass: [Fraction class]];
[fraction respondsToSelector: @selector (print)];
[complex respondsToSelector: @selector (print)];
[Fraction instancesRespondToSelector: @selector (print)];
[number respondsToSelector: @selector (print)];
[number isKindOfClass: [Complex class]];
[number respondsToSelector: @selector (release)];
[[number class] respondsToSelector: @selector (alloc)];


Notes

  1. (옮긴이) 아마 사용자는 그저 여러분이 만든 프로그램이 갑자기 죽어버렸다며 짜증을 내고, 다시는 여러분이 만든 프로그램을 구입하지 않을겁니다.