ProgrammingInObjectiveC:Chapter 08
- 8장
- 상속
8장 :: 상속
이 장에서는 객체지향 프로그래밍을 강력하게 만드는핵심 요소인 '상속'에 대해 배울 것이다. 상속이라는 개념을 이용하면 기존 클래스를 자신의 프로그램에 맞도록 수정하여 사용할 수 었다.
모든 것은 루트에서 시작된다
3장 「클래스, 객체, 메서드」 에서 부모 클래스라는 개념을 배웠다. 부모 클래스 역시 자신의 부모 클래스가 있을 수 있다. Objective-C 에서는 프로그래머가 자신의 '루트' 클래스를 만들 수 있지만, 보통은 직접 루트 클래스를 만들지 않는다. 그대신, 기존 클래스를 활용한다. 지금까지 정의한 클래스는 모두 NSObject 라는 루트 클래스의 자식들이다. 이를 인터페이스 파일에서 다음과 같이 지시해 주었다.
@interface Fraction: NSObject
...
@end
Fraction 클래스는 NSObject 클래스에서 파생되었다. 계층도를 보면 NSObject 가 최상위에 있기 때문에(즉, 그 위로 아무 클래스도 없기 때문에) 그림 8.1처럼 '루트 클래스(root class)'라고 부른다. Fraction 클래스는 '자식 클래스(child class)' 혹은 '서브클래스(sub class)'라고 부른다.
클래스, 자식 클래스, 부모 클래스라는 용어로 이야기할 수 있다. 또는 클래스, 서브클래스, 수퍼클래스 라는 용어로 이야기해도 된다. 이 두 가지 표현에 모두 익숙해져야 한다.
(루트 클래스를 새로 만들 때를 제외하면) 새 클래스를 정의할 때, 이 클래스는 어떤 프로퍼티를 상속받는다. 예를 들어, 부모의 인스턴스변수와 메서드는 모두 암묵적으로 새 클래스의 정의에 들어간다. 다시 말해서 서브클래스는 마치 자신이 이 메서드와 인스턴스변수들을 정의한 것처럼 이것들에 직접 접근할수있다. 간단한 예를보자. 좀 부자연스럽긴 하지만 상속의 핵심 요소를 설명한다. 다음은 initVar 이라는 메서드를 보유한 ClassA 를 선언한 것이다.
@interface ClassA: NSObject
{
int x;
}
-(void) initVar;
@end
initVar 메서드는 ClassA 에 있는 인스턴스변수의 값을 100 으로 설정한다.
@implementation ClassA
-(void) initVar
{
x = 100;
}
@end
이제 ClassB 를 정의하자.
@interface ClassB: ClassA
-(void) printVar;
@end
선언에서 맨 첫줄에 대한 내용이다.
이 줄은 ClassB 가 NSObject 가 아닌 ClassA 의 서브클래스임을 나타낸다. 따라서 ClassA 의 부모 클래스(혹은 수퍼클래스)는 NSObject 지만, ClassB 의 부모는 ClassA 다. 그림 8.2 에서 이를 설명한다. 그림 8.2 처럼 루트 클래스는 수퍼클래스가 없고, 계층도에서 맨 아래에 있는 ClassB 는 서브클래스가 없다. 따라서 ClassA 는 NSObject 의 서브클래스 이고, ClassB 는 ClassA 와 NSObject 의 서브클래스다(정확히 말하면 NSObject 의 서브-서브 클래스 혹은 손자 클래스다). 또한 NSObject 는 ClassB 의 수퍼클래스인 ClassA 의 수퍼클래스다. 계층도를 따라 올라가면 NSObject 는 ClassB 의 수퍼클래스 이기도 한것이다.
printVar 메서드만 정의하는 ClassB의 선언 전체이다.
@interface ClassB: ClassA
-(void) printVar;
@end
@implementation ClassB
-(void) printVar
{
NSLog (@"x = %i", x);
}
@end
ClassB 에 인스턴스 변수를 전혀 선언하지 않았음에도, printVar 메서드는 인스턴스 변수 x 의 값을 출력한다. ClassB 가 ClassA 의 서브클래스이기 때문에 ClassA 가 지닌 인스턴스 변수를 모두 상속받아서 가능한 일이다(이 경우는 ClassA 에서 단 하나만 상속받는다). 그림 8.3은 이를 나타낸다.
(물론 그림에서는 NSObjec.t 클래스에서 상속받는 인스턴스 변수 몇 개와 메서드를 표시하지 않았다.)
완전한 프로그램을 하나 보면서 상속이 어떻게 동작하는지 살펴보자. 예제를 간단하게 만들기 위해 클래스 선언과 정의를 모두 한 파일에 몰아넣었다(프로그램 8.1을보라).
프로그램 8.1
// 상속을 설명하는 간단한 예제
#import <Foundation/Foundation.h>
// ClassA 선언과 정의
@interface ClassA: NSObject
{
int x;
}
-(void) initVar;
@end
@implementation ClassA
-(void) initVar
{
x = 100;
}
@end
// ClassB 선언과 정의
@interface ClassB: ClassA
-(void) printVar;
@end
@implementation ClassB
-(void) printVar
{
NSLog (@"x = %i", x);
}
@end
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
ClassB *b = [[ClassB alloc] init];
[b initVar]; // 상속받은 메서드를 사용한다.
[b printVar]; // x의 값을 표시
[b release];
[pool drain];
return 0;
}
프로그램 8.1 의 출력결과
x = 100
먼저 b 를 ClassB 객체로 정의한다. b 를 생성하고 초기화한 뒤, b 에 메시지를 보내 initVar 메서드를 적용한다. 그런데 ClassB 가 정의된 부분을 돌아보면, 그런 메서드를 정의한 적이 없다. 사실 initVar 는 ClassA 에서 정의되었다. 그런데 ClassA 가 ClassB 의 수퍼클래스이므로 ClassB 는 ClassA 가 보유한 메서드를 모두 사용할 수 있는 것이다. 따라서 ClassB 에게 initVar 는 상속받은 메서드다.
alloc과 init 메서드는 지금까지 나온 모든 클래스에서 계속 다뤘지만, 직접 정의한 적이 없다. 그저 상속받은 메서드의 혜택을 누렸던 것이다. |
initVar 메시지를 b 에 보낸 다음, printVar 메서드를 호출하여 인스턴스 변수 x 의 값을 표시한다. 출력 결과로 'x = 100' 이 나왔는데, 이는 printVar 가 이 인스턴스 변수에 정상적으로 접근할 수 있었다는 의미다. 바로 initVar 메서드를 상속받았기 때문이다.
상속의 개념은 계층도를 따라 계속 이어진다는 점을 기억하자. 예컨대 ClassB 를 부모 클래스로 가지는 ClassC 를 다음과 같이 정의했다고 하자
@interface ClassC: ClassB;
...
@end
ClassC 는 ClassB 의 모든 메서드와 인스턴스 변수를 상속받고, ClassB 는 다시 ClassA 의 메서드와 인스턴스 변수를 모두 상속받는다. ClassA 는 NSObject 가 가지고있는 메서드와 인스턴스 변수를 모두 상속받는다.
한 가지 유념할 점이 있는데, 클래스 인스턴스는 상속받더라도 각각 자신의 인스턴스 변수를 보유한다. 따라서 ClassC 객체와 ClassB 객체는 각자 자신의 인스턴스 변수를 갖는다.
알맞은 메서드 찾기
객체에 메시지를 보낼 때, 어떻게 올바른 메서드를 선택하고 객체에 적용하게 되는지 궁금할 것이다. 사실 그 규칙은 매우 간단하다. 먼저 객체가 속한 클래스를 보고 그 클래스에 메시지와 이름이 동일한 메서드가 명시적으로 정의되어 있는지 확인한다. 만일 정의되어 있다면, 그 메서드가 사용된다. 메서드가 정의되지 않았다면, 부모클래스를 확인한다. 부모클래스에서 메서드가 정의되어 었다면 그 메서드가 사용된다. 그렇지 않다면 그 위 부모 클래스에서 찾는 식으로 계속 검색해나간다.
부모 클래스를 검색하는 것은 다음 두 조건 중 하나가 충족될 때까지 계속된다. 찾는 메서드를 보유하는 클래스를 발견하거나, 루트 클래스에 도달할 때까지 원하는 메서드를 찾지 못하는 경우다. 첫 번째 경우는 아무런 문제가 발생하지 않는다. 그러나 두 번째 경우, 다음과 같은 경고 메시지가나타난다.
warning: 'ClassB' may not respond to '-inity'
실수로 inity 라는 메시지를 ClassB 형 변수에 보낸 경우다. 컴파일러가 해당 클래스 형의 변수는 그 메서드에 어떻게 응답해야 하는지 모른다고 표현한 것이다. 다시 살펴보면, 이 것은 ClassB 의 메서드를 확인하고, 루트 클래스(이 경우 NSObject)까지 이르는 부모클래스의 메서드들을 확인해 본 다음에 결정된다.
어떤 경우에는 메서드가 발견되지 않더라도 경고 메시지가 생성되지 않는다. '포워딩'이라는 기법을 사용할 때 일어나는일이다. 9장 「다형성,동적 타이핑,동적 바인딩」 에서 간단히 설명한다.
상속으로 확장하기 - 새 메서드 추가
보통 상속을 사용하여 클래스를 확장한다. 예컨대, 직사각형, 원, 삼각형 같은 2D 그래픽 객체를 다루는 클래스를 개발하는 업무를 맡았다고 해보자. 지금은 직사각형만다룬다. 4장 「데이터 형과표현식」에 있는 연습문제 7번의 @intertace 부분에서 시작해보자.
@interface Rectangle: NSObject
{
int width;
int height;
}
@property int width, height;
-(int) area;
-(int) perimeter;
@end
자동 생성 (synthesized) 메서드를사용하여 직사각형의 너비와 높이를 설정하고 그 값들을 가져온다. 그리고 직접 작성한 메서드로 직사각형의 넓이와 둘레를 계산한다. 메서드를 추가하여 직사각형의 너비와 높이를 한번에 설정해 보자.
-(void) setWidth: (int) wandHeight: (int) h;
이 새 클래스를 정의하는 부분은 인터페이스 파일 Recrangle.h 에 입력하자. 구현파일 Rectangle.m 에는 다음 코드를 입력하면 된다.
#import "Rectangle.h"
@implementation Rectangle
@synthesize width, height;
-(void) setWidth: (int) w andHeight: (int) h
{
width = w;
height = h;
}
-(int) area
{
return width * height;
}
-(int) perimeter
{
return (width + height) * 2;
}
@end
코드만 봐도 각 메서드의 정의를 이해할 수 있을 것이다. 프로그램 8.2로 이 클래스를 테스트 해보자.
프로그램 8.2
#import "Rectangle.h"
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Rectangle *myRect = [[Rectangle alloc] init];
[myRect setWidth: 5 andHeight: 8];
NSLog (@"Rectangle: w = %i, h = %i",
myRect.width, myRect.height);
NSLog (@"Area = %i, Perimeter = %i",
[myRect area], [myRect perimeter]);
[myRect release];
[pool drain];
return 0;
}
프로그램 8.2 의 출력결과
Rectangle: w = 5, h = 8
Area = 40, Perimeter = 26
myRect를 생성하고 초기화한 다음, 너비와 높이를 각각 5와 8로 설정하였다. 첫 NSLog 호출로 제대로 값이 설정되었는지 확인할 수 있다. 그 다음부터는 적절한 메시지를 호출하여 직사각형의 넓이와 둘레가 계산되고 반환 값이 NSLog 로 표시된다.
이제 정사각형을 다뤄 보자. Square 라는 클래스를 새로 정의하고 Rectangle 클래스에서 했던 것처럼 유사한 메서드를 정의하는 방식도 있다. 혹은, 정사각형이 직사각형에서 너비와 높이가 똑같은 특별한 도형임을 인식해서 해결해도 된다.
좀더 쉬운 방법은 Square 라는 새 클래스를 정의하고 이 클래스를 Rectangle 의 서브클래스로 만드는 것이다. 그렇게 하면, Rectangle 이 보유한 메서드와 변수를 모두 사용할 수 있고, 필요하면 새 메서드나 변수를 정의할 수도 있다. 우선 정사각형에서 한변의 값을 설정하고 그 값을 받아오는 메서드를 추가해야 한다. 프로그램 8.3 은 새 Square 클래스의 인터페이스 파일과 구현 파일을 보여 준다.
프로그램 8.3 Square.h 인터페이스 파일
#import "Rectangle.h"
@interface Square: Rectangle
-(void) setSide: (int) s;
-(int) side;
@end
프로그램 8.3 Square.h 구현 파일
#import "Square.h"
@implementation Square: Rectangle
-(void) setSide: (int) s
{
[self setWidth: s andHeight: s];
}
-(int) side
{
return width;
}
@end
이제 무엇을 했는지 살펴보자. Square 클래스가 혜더파일 Rectangle.h 에 선언된 Rectangle 클래스의 서브클래스가 되도록 정의했다. 인스턴스 변수는 새로 만들지는 않았지만, setSide: 와 side 라는 메서드를 추가하였다.
정사각형은 변의 값을 하나만 갖지만 내부적으로는 두 숫자로 표현된다. 이렇게 해도 아무런 문제가 발생하지 않는다. 내부적으로 어떻게 표현되는지는 사용자에게 감추어져 있고, 필요하다면 Square 클래스를 언제든 다시 정의해도 된다. 앞에 설명한 데이터 캡슐화라논 개념 덕택에 클래스를사용하는 이가 내부에 어떤 식으로 표현되어 있는지까지 알 필요가 없어진 것이다.
setSide: 메서드는 Rectangle 클래스에서 이미 정의한 너비와 높이를 설정하는 메서드를 상속받아 활용한다. 따라서 setSide:는 매개변수 s 를 Rectangle 클래스의 메서드인 setWidth:andHeight: 의 너비와 높이 값으로 건네 준다. 그 외에 다른 어떤 작업도 수행할 필요가 없다. Square 객체를 사용하는 누군가는 setSide: 를 사용하여 정사각형의 치수를 지정해 주고, Rectangle 클래스의 메서드를 활용하여 정사각형의 넓이, 둘레 등을 계산할 수 있다. 프로그램 8.3 은 새 Square 클래스를 테스트한다.
프로그램 8.3 테스트 프로그램
#import "Square.h"
#import <Foundation/Foundation.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Square *mySquare = [[Square alloc] init];
[mySquare setSide: 5];
NSLog (@"Square s = %i", [mySquare side]);
NSLog (@"Area = %i, Perimeter = %i",
[mySquare are], [mySquare perimeter]);
[mySquare release];
[pool drain];
return 0;
}
프로그램 8.3 의 출력결과
Square s = 5
Area = 25, Perimeter = 20
Square 클래스를 정의한 방식이 바로 Objective-C 에서 클래스를 다루는 기본적인 기법이다. 프로그래머 자신이나 다른 사람이 이미 작성해 놓은 것을 필요에 맞게 확장해 쓰는 것이다. 더붙어 '카테고리' 기법을 써서 기존 클래스정의에서 메서드를 모듈 방식으로 추가할 수 있다. 이 방법을 쓰면 동일한 인터페이스 파일과 구현 파일에 새 메서드 정의를 계속 덧볼일 필요가 없다. 이 기법은 소스코드에 접근하지 못하는 클래스를 확장하고 싶을 때 유용하다. 카테고리에 대해서는 11장「카테고리와 프로토콜」에서 배운다.
포인터 클래스와 메모리 할당
Rectangle 클래스는 직사각형의 치수만 저장한다. 실제 그래픽 응용 프로그램 에서는 직사각형의 채움 색, 테두리 색, 창내 위치(원점, origin)같은 추가정보가 있어야 할 것이다. 클래스를 확장하여 이 작업을 손쉽게 처리할 수 있다. 일단, 직사각형의 원점만 처리해 보자. '원점'을 이차원 좌표계에서 직사각형의 왼쪽 아래 꼭짓점의 좌표 (x, y)라고 가정하자. 만일 그림 그리는 프로그램을 만들고 있었다면, 이 지점은 그림 8.4 처럼 창안쪽의 직사각형 위치를 나타낼 것이다.
그림 8.4에서 직사각형의 원점은 (x1, y1)으로 표시되었다.
Rectangle 클래스를 확장하여 직사각형의 원점을 x 좌표, y 좌표라는 두 값으로 저장할수 있다. 혹은, 응용프로그램을 개발하면서 앞으로 아주 많은 좌표를 다뤄야 하니, 아예 XYPoint 라는 클래스를 정의하기로 결정할 수도 있다(3장의 연습문제 7번에서 이 문제를 다뤘던 것을 기억하라).
#import <Foundation/Foundation.h>
@interface XYPoint: NSObject
{
int x;
int y;
}
@property int x, y;
-(void) setX: (int) xVal andY: (int) yVal;
@end
이제 Rectangle 클래스로 돌아가자. 직사각형의 원점을 저장할 origin 이라는 변수를 Rectangle 클래스 정의에 추가한다.
@interface Rectangle: NSObject
{
int width;
int height;
XYPoint *origin;
}
...
직사각형의 원점을 설정하고 값을 받아오는 메서드를 추가하는 편이 좋아 보인다. 요점을 살펴보기 위해 원점에 접근하는 접근자 메서드를 자동으로 생성하지 않을 것이다. 직접 코드를작성하자.
@class 지시어
이제 직사각형의 (그리고 정사각형의) 너비, 높이, 원점을 설정하도록 허용했다.
먼저 인터페이스파일 Rectangle.h를살펴보자.
#import <Foundation/Foundation.h>
@class XYPoint;
@interface Rectangle: NSObject
{
int width;
int height;
XYPoint *origin;
}
@property int width, height;
-(XYPoint *) origin;
-(void) setOrigin: (XYPoint *) pt;
-(void) setWidth: (int) w andHeight: (int) h
-(int) area;
-(int) perimeter;
@end
Rectangle.h 혜더파일에서 새로운 지시어를 사용했다.
@class XYPoint;
@class 지시어를 사용하면, Rectangle 에서 XYPoint 형식의 인스턴스 변수를 만나게 될 때, 컴파일러에게 그 클래스가 무엇인지 알려준다. setOrigin:과 origin 메서드에서도 클래스 이름을 인수와 반환 값을 선언하는 데 사용한다. @class 를 쓰는 대신 다음과 같이 헤더파일을 임포트하는 방법도 있다.
#import "XYPoint.h"
@class 지시어를 달면 컴파일러가 XYPoint.h 파일 전체를 처리할 필요가 없어지기 때문에 좀더 효율적이다(비록 이 파일이 매우 작지만). 컴파일러는 그저 XYPoint 가 클래스 이름이라는 것만 알고 있으며 된다. 만일 XYPoint 클래스의 메서드를 사용해야 한다면, 컴파일러에게 더 많은 정보가 필요하기 때문에 @class 지시어로는 충분치 않다. 이때 컴파일러는 메서드가 인수를 얼마나 많이 받는지, 인수가 어떤 형인지, 어떤 값을 반환하는지에 대한 정보를 알아야 한다.
이제 새 XYPoint 클래스와, Rectangle 메서드의 빈 공간을 채워 프로그램을 테스트해 보자. 먼저 프로그램 8.4는 XYPoint 클래스를 구현한 파일이다.
Recrangle 클래스의 새 메서드를 보자.
프로그램 8.4 메서드를 추가한 Rectangle.m
#import "XYPoint.h"
-(void) setOrigin: (XYPoint *) pt
{
origin = pt;
}
-(XYPoint *) origin
{
return origin;
}
@end
다음은 온전한 XYPoint 와 Rectangle 클래스를 정의한 부분이고, 그 뒤를 이은 것은 시도할 테스트 프로그램의 코드다.
프로그램 8.4 XYPoint.h 인터페이스 파일
#import <Foundation/Foundation.h>
@interface XYPoint: NSObject
{
int x;
int y;
}
@property int x, y;
-(void) setX: (int) xVal andY: (int) yVal;
@end
프로그램 8.4 XYPoint.m 구현 파일
#import "XYPoint.h"
@implementation XYPoint
@synthesize x, y;
-(void) setX: (int) xVal andY: (int) yVal
{
x = xVal;
y = yVal;
}
@end
프로그램 8.4 Rectangle.h 인터페이스 파일
#import <Foundation/Foundation.h>
@class XYPoint;
@interface Rectangle: NSObject
{
int width;
int height;
XYPoint *origin;
}
@property int width, height;
-(XYPoint *) origin;
-(void) setOrigin: (XYPoint *) pt;
-(void) setWidth: (int) w andHeight: (int) h;
-(int) area;
-(int) perimeter;
@end
프로그램 8.4 Rectangle.m 구현 파일
#import "Rectangle.h"
@implementation Rectangle
@synthesize width, height;
-(void) setWidth: (int) w andHeight: (int) h
{
width = w;
height = h;
}
-(void) setOrigin: (XYPoint *) pt
{
origin = pt;
}
-(int) area
{
return width * height;
}
-(int) perimeter
{
return (width + height) * 2;
}
-(XYPoint *) origin
{
return origin;
}
@end
프로그램 8.4 테스트 프로그램
#import "Rectangle.h"
#import "XYPoint.h"
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Rectangle *myRect = [[Rectangle alloc] init];
XYPoint *myPoint = [[XYPoing alloc] init];
[myPoint setX: 100 andY: 200];
[myRect setWidth: 5 andHeight: 8];
myRect.origin = myPoint;
NSLog (@"Rectangle w = %i, h = %i",
myRect.width, myRect.height);
NSLog (@"Origin at (%i, %i)",
myRect.origin.x, myRect.origin.y);
NSLog (@"Area = %i, Perimeter = %i",
[myRect area], [myRect perimeter]);
[myRect release];
[myPoint release];
[pool drain];
return 0;
}
프로그램 8.4 의 출력결과
Rectanble w = 5, h = 8
Origin at (100,200)
Area = 40, Perimeter = 26
main 루틴 내에서 myRect 는 직사각형과 myPoint 라는 점을 생성하고 초기화하였다. setX:andY: 메서드를 사용하여 myPoint를 (100, 200)으로 설정하였다. 직사각형의 너비와 높이를 각각 5와 8로 설정한 후, SetOrigin: 메서드를 사용하여 직사각형의 원점을 myPoint 가 나타내는 위치로 설정한다. 그 다음 NSLog 명령문을 세 개 호출하여 값을 받아온 뒤 이를 표시한다. 다음 표현식을 보자.
myRect.origin.x
이 식은 접근자 메서드인 origin 이 반환하는 XYPoint 객체를 받아 . 연산자를 써서 직사각형 원점의 x 좌표를가져온다. 비슷한 방식으로 다음 표현식은 직사각형 원점의 y 좌표를 가져온다.
myRect.origin.y
자신의 객체를 소유하는 클래스
프로그램 8.5 를 출력한 결과를 설명해 보자.
프로그램 8.5
#import "Rectangle.h"
#import "XYPoint.h"
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Rectangle *myRect = [[Rectangle alloc] init];
XYPoint *myPoint = [[XYPoint alloc] init];
[myPoint setX: 100 andY: 200];
[myRect setWidth: 5 andHeight: 8];
myRect.origin = myPoint;
NSLog (@"Origin at (%i, %i)",
myRect.origin.x, myRect.origin.y);
[myPoint setX: 50 andY: 50];
NSLog (@"Origin at (%i, %i)",
myRect.origin.x, myRect.origin.y);
[myRect release];
[myPoint release];
[pool drain];
return 0;
}
프로그램 8.5 의 출력결과
Origin at (100,200)
Origin at (50,50)
프로그램에서 XYPoint myPoint 를 (100, 200)에서 (50, 50)으로 변경하였다. 당연히, 직사각형의 원점도 이동하였다. 왜 그렇게 된 것일까? 명시적으로 직사각형의 원점을 다시 설정하지 않았는데 왜 직사각형의 원점이 옮겨진 것일까? setOrigin: 메서드가 정의된 부분에 그 이유가 숨어 있다.
-(void) setOrigin: (XYPoing *) pt
{
origin = pt;
}
다음표현식으로 setOrigin: 메서드를 호출해 보자.
myRect.origin = myPoint;
그러면 myPoint 의 값을 메서드의 인수로 넘기게 된다. 그림 8.5에서 볼 수 있듯이, 이 값은 XYPoint 객체가 저장된 메모리 공간을 가리킨다.
myPoint에 저장된 값은 메모리를 가리키는 포인터 이고, 메서드 내에 정의된 지역 변수 pt 에복사된다. 이제 myPoint 와 pt 모두 메모리에 저장된 동일한 데이터를 가리킨다. 그림 8.6은 이를 설명한다.
원점 변수가 메서드 내에서 pt 로 설정되면, pt 에 저장된 포인터는 그림 8.7처럼 인스턴스 변수 origin에 복사된다.
myPoint 와, myRect 에 저장된 원점 변수(그리고 지역 변수 pt)가 메모리에서 동일한 영역을 가리키기 때문에 myPoint 의 값을 (50, 5O)으로 변경하면 직사각형의 원점도 바뀌는 것이다.
이 문제를 피하려면 setOrigin: 메서드를 수정하여, 원점을 새로 생성하고 그 점으로 origin 을 설정하면 된다. 다음 코드를 보자.
-(void) setOrigin: (XYPoint *) pt
{
origin = [[XYPoint alloc] init];
[origin setX: pt.x andY: pt.y];
}
이 메서드는 먼저 새 XYPoint 를 생성하고 초기화한다. 다음 메시지 표현식을 보자.
[origin setX: pt.X andY: pt.y];
이는 새로 생성한 XYPoint 를 메서드로 넘어오는 인자의 x 좌표와 y 좌표로 설정한다. 이 메시지 표현을 완전히 이해할 때까지 거듭 살펴보자.
setOrigin: 메서드를 위와 같이 변경하면, 각 Rectangle 인스턴스가 자신만의 origin XYPoint 인스턴스를 소유한다는 뜻이다. 이제 XYPoint 의 메모리를 할당하고 해제시켜 줘야한다. 대개 클래스가 다른 객체를 포함하면, 이 객체들의 일부나 전부를 소유하는 편이 좋다. 직사각형의 경우, 원점은 직사각형이 갖는 기본 속성이므로, Rectangle 클래스가 자신의 원점을 소유하는 것이 합당하다.
그런데 origin 이 차지한 메모리는 어떻게 릴리스 할까? 직사각형이 차지한 메모리를 릴리스한다고 해서, origin 에 할당한 메모리가 릴리스되지는 않는다. 한 가지 방법은 다음 코드를 main 에 추가하는 것이다.
[[myRect origin] release];
이 코드는 origin 메서드가 반환하는 XYPoint 객체를 릴리스한다. Rectangle 객체의 메모리 공간을 릴리스하기 전에 이 작업을 해줘야 한다. 객체의 메모리 공간이 릴리스되면 그 안에 포함된 모든 변수는 더는 유효하지 않기 때문이다. 따라서 다음 순서로 해야된다.
[[myRect origin] release]; // origin에 할당된 메모리 릴리스
[myRect release]; // Rectangle에 할당된 메모리 릴리스
직접 원점의 메모리를 릴리스해 줘야 한다는사실을 기억하기가좀 번거로울 것이다. 게다가 원점을 생성한 장본인은 당신이 아니라 바로 Rectangle 클래스다. 다음 절 '메서드 재정의하기'에서 Rectangle 이 이러한 메모리를 릴리스해 주는 방법에 대해 배운다.
메서드를 수정한 뒤 프로그램 8.5 를 다시 컴파일하고 실행하면, 그림 8.8 과 같은 오류메시지가 뜬다.
프로그램 8.5B 의 출력결과
Origin at (100, 200)
Origin at (100, 200)
이런! 여기서는 수정된 메서드에서 XYPoint 클래스의 메서드를 사용한 것이 문제다. 따라서 이제 컴파일러는 @class 지시어가 제공하는 정보보다 많은 정보가 필요하다. 이 경우, 앞으로 돌아가서 이 지시어를 import 로 변경해 줘야 한다.
#import "XYPoint.h"
이제 잘 동작한다. 이번에는 Rectangle 에 있는 setOrigin: 메서드 내에 좌표 사본이 생성되기 때문에 main 내의 myPoint 를 (50, 50)으로 변경해도 직사각형의 원점에는 영향을 주지 않는다.
여기서 왜 origin 메서드를 자동 생성하지 않은 것일까? 자동 생성된 setOrigin: 메서드는 맨 처음에 직접 작성한 메서드와 동일하게 동작하기 때문이다. 기본적으로, 자동 생성된 세터는 객체 자체를 복사하지 않고 그저 객체의 포인터만 복사한다.
물론, 자동 생성된 세터 메서드도 객체 자체의 사본을 만들도록 할 수 있다. 그러나 이 작업을 하려거든 복사 메서드를 작성하는 방법을 배워야 한다. 이 주제는 17장 '메모리관리' 에서 다시 다룬다.
메서드 재정의하기
이 장의 앞부분에서 상속으로는 메서드를 제거하거나 삭제할 수 없다고 말했다. 그러나 상속받은 메서드를 '재정의' 하여 메서드 정의를 변경할 수는 있다.
앞서 본 클래스 ClassA 와 ClassB 로돌아가서 initVar 메서드를 ClassB 에 작성하고 싶다고 하자. 이미 ClassB 는 ClassA 에 정의된 initVar 메서드를 상속받는다는 것을 알 것이다. 이 상속받은 메서드를 제거하고 동일한 이름으로 새 메서드를 만들 수 있을까? 대답은 '그렇다'이다. 그저 동일한 이름으로 메서드를 새로 정의하기만 하면 된다. 부모 클래스에 있는 메서드와 동일한 이름으로 메서드를 정의하면, 새로운 내용이 상속받은 메서드를 대치하거나 재정의한다. 새 메서드는 반환 형, 인수개수, 데이터 형 이 재정의하는 메서드와 같아야 한다.
프로그램 8.6은 이 개념을 보여 주는간단한 예제다.
프로그램 8.6
// 메서드 재정의하기
#import <Foundation/Foundation.h>
// ClassA 선언 및 정의
@interface ClassA: NSObject
{
int x;
}
-(void) initVar;
@end
@implementation ClassA
-(void) initVar
{
x = 100;
}
@end
// ClassB 선언 및 정의
@interface ClassB: ClassA
-(void) initVar;
-(void) printVar;
@end
@implementation ClassB
-(void) initVar // 추가된 메서드
{
x = 200;
}
-(void) printVar
{
NSLog (@"x = %i", x);
}
@end
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
ClassB *b = [[ClassB alloc] init];
[b initVar]; // B에서 재정의한 메서드를 사용한다.
[b printVar]; // x의 값을 표시
[b release];
[pool drain];
return 0;
}
프로그램 8.6 의 출력결과
x = 200
다음 메시지를 보자.
[b initVar];
분명 이 메시지는 이전 예제와 달리 ClassA 가 아닌 ClassB 에 정의된 initVar 메서드를 사용하였다. 그림 8.9 는 이를 설명한다.
무슨 메서드가 선택되었을까?
시스템이 객체에 적용할 메서드를 계층도에서 어떻게 찾는지를 살펴봤다. 만일 이름이 동일한 메서드가 여러 클래스에 존재한다면, 메시지 수신자의 클래스에 해당하는 메서드가 선택된다. 프로그램 8.7 은 앞에서 본 ClassA 와 ClassB 의 정의를 사용한다.
프로그램 8.7
#import <Foundation/Foundation.h>
// ClassA와 ClassB의 정의를 여기에 추가한다.
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
ClassA *a = [[ClassA alloc] init];
ClassB *b = [[ClassB alloc] init];
[a initVar]; // ClassA의 메서드를 사용한다.
[a printVar]; // x의 값을 표시한다.
[b initVar]; // ClassB에서 재정의한 메서드를 사용한다.
[b printVar]; // x의 값을 표시한다.
[a release];
[b release];
[pool drain];
return 0;
}
이 프로그램을 빌드하면 다음과 같은 경고 메시지를 받을 것이다.
warning: 'ClassA' may not respond to '-printVar'
무슨 일이 일어난 것일까? 이 문제에 대해서는 앞 절에서 다뤘다. ClassA 의 선언부를 살펴보자.
// ClassA 선언 및 정의
@interface ClassA: NSObject
{
int x;
}
-(void) initVar;
@end
바로 printVar 메서드가 선언되지 않았던 것이다. 이 메서드는 ClassB 에서 선언되고 정의되었다. 따라서 ClassB 객체와 그 자식들은 이 메서드를 상속받아 사용할 수 있지만, ClassA 객체는 printVar 를 사용할 수 없다. 계층도에서 ClassA 보다 아래에서 정의한 메서드이기 때문이다.
이 메서드를 사용하도록 강제할 수는 있지만, 거기까지 다루지는 않는다. 게다가 좋은 프로그랩 습관도 아니다. |
예제로 돌아가자. ClassA에 printVar 메서드를 추가하여 인스턴스 변수의 값을 표시한다.
// ClassA 선언 및 정의
@interface ClassA: NSObject
{
int x;
}
-(void) initVar;
-(void) printVar;
@end
@implementation ClassA
-(void) initVar
{
x = 100;
}
-(void) printVar
{
NSLog (@"x = %i", x);
}
@end
ClassB 를 선언하고 또 정의한 부분은 아무것도 바꾸지 않았다. 이제 프로그램을 다시 컴파일하고 실행해 보자.
프로그램 8.7 의 출력결과
x = 100
x = 200
이제 예제 자체에 대해 얘기해 보자. 먼저 a 와 b 는 각각 ClassA 와 ClassB 객체로 정의했다. 이것들을 생성하고 초기화한 뒤, a 에 메시지를 보내 initVar 메서드를 적용했다. initVar 메서드가 ClassA에 정의되어 있으므로, ClassA에 구현된 이 메서드가 선택된다. initVar 메서드는 그저 인스턴스변수 x 의 값을 100 으로 설정한다. 방금 ClassA 에 추가한 printVar 메서드는 그 다음에 호출되어 x 의 값을 표시한다.
ClassA 객체와 마찬가지로 ClassB 객체인 b 도 생성되고 초기화된 다음, 인스턴스변수 x 의 값을 200 으로 설정하고 나서 마지막에 값이 표시된다.
a, b 가 어느 클래스에 속하는 객체인지에 따라 적절한 메서드가 선택된다는 점을 완벽히 이해하자. 이것은 Objective-C 로 객체지향 프로그래밍을 할 때 핵심이 되는 개념이다.
연습문제 삼아 ClassB 에서 printVar 메서드를 제거하는 경우를 생각해 보자. 제거해도 잘 동작할까? 그렇다면 이유는 무엇일까?
dealloc 메서드 재정의와 super 키워드
이제 메서드를 재정의하는 방법을 배웠으니,프로그램 8.5B 로 돌아가보자, origin 이 차지하는 메모리를 더 나은 방법으로 해제해 보겠다, setorigin: 메서드는 이제 자신의 XYPoint origin 객체를 할당하기 때문에 여러분이 이 메모리를 릴리스해 줘야만 한다. 프로그램 8.6 에서는 main 에서 다음 명령문으로 메모리를 릴리스해주는 방법을 썼다.
[[myRect origin] release];
이제 클래스에 있는 모든 멤버를 각각 릴리스해 줘야 한다는 것을 염려하지 않아도 된다. 상속받은 dealloc 메서드를 재정의하여(NSObject 에서 상속받았다) 그곳에서 origin 의 메모리를 릴리스해 주면 된다.
release 메서드 대신 dealloc 메서드를 재정의한다. 앞으로 나올 장에서 배우겠지만, release 는 객체가 사용한 메모리를 해제할 때도 있지만, 아닐 때도 있다. 즉, release 는 어느 누구도 그 객체를 참조하지 않을 때만 객체가 차지한 메모리를 해제한다. 또한 메모리를 해제할 때는 객체의 dealloc 메서드를 호출해서 수행한다. 실제로는 이 dealloc이 실제로 메모리를 해제한다. |
dealloc 을 재정의하기로 결정했다면, 자신이 소유한 인스턴스 변수가 차지하는 메모리 공간뿐 아니라, 상속받은 변수가 차지하는 공간도 해제해야 한다.
이를 위해 특별 키워드 super를 사용한다. super 는 메시지 수신자의 부모 클래스를 나타낸다. super 에 메시지를 보내서 재정의된 메서드(수퍼클래스의 메서드)를 실행시킨다. 이것이 이 키워드를 가장많이 쓰는 방식이다. 다음의 메시지 표현식을 보자.
[super release];
이 식은 어떤 메서드 안에서 사용되면 부모클래스에 정의된(혹은 부모클래스가 상속받은) release 메서드를 호출한다. 그 메서드는 메시지의 수신자, 다른말로 하면 self 에 대해 호출된다.
그러므로 Rectangle 클래스에서 dealloc 메서드를 사용하려면 먼저 origin 이 차지하는 메모리를 릴리스하고, 부모 클래스의 dealloc 메서드를 호출하여 작업을 마무리한다. 이를 통해 Rectangle 객체 자체가 차지하는 메모리를 릴리스한다. 새 메서드를 살펴보자.
-(void) dealloc
{
if (origin)
[origin release];
[super dealloc];
}
dealloc 은 값을 반환하지 않는다. NSObject.h 헤더파일에 있는 dealloc 메서드 선언부에서 이를 알 수 있다. dealloc 메서드 내에서, 릴리스 전에 origin 이 0 이 아닌지를 확인한다. 만일 기본값인 0 이라면,아마 직사각형의 원점은 설정된적이 없을 것이다. 그 후 만일 dealloc 메서드를 재정의하지 않았다면, Rectangle 클래스가 상속받은 메서드와 동일한, 부모 클래스의 dealloc 메서드를 호출한다.
dealloc 메서드를 다음과 같이 더 간결하게 작성할 수도 있다.
-(void) dealloc
{
[origin release];
[super dealloc];
}
nil 객체 메시지를 보내도 되기 때문에 위와 같이 검사를 생략해도 괜찮다. 또한 origin 을 dealloc 하는 것이 아니라 release 하고 있음에 주의하자. 아무도 origin 을 사용하지 않다면 release 는 결국 origin 에 대해 dealloc 을 호출하여 메모리를 해제할 것이다.
새 메서드로 당신이 생성한 직사각형을 릴리스해 주면, 포함된 XYPoint 에 대해서 더는 걱정할 필요가 없다. 프로그램 8.5 에서 사용했던 release 메시지 두 개를 이용해서 이제 setOrigin: 이 생성한 XYPoint 객체 동 프로그램에서 생성한 모든 객체를 릴리 스하게 된것이다.
[myRect release];
[myPoint release];
그런데 여전히 한 가지 문제가 남아 있다. 만일 프로그램이 실행되는 동안 Rectangle 객체 하나의 원점을 여러 번 설정해 주었다고하자. 새 원점을 생성하고 할당하기 전에 이전 원점이 차지했던 메모리 공간을 해제해 주어야 한다. 예를 들어 다음코드를 살펴보자.
myRect.origin = startPoint;
...
myRect.origin = endPoint;
...
[startPoint release];
[endPoint release];
[myRect release];
myRect 의 멤버인 origin 에 저장된 XYPoint startPoint 의 사본은 두 번째 원점(endPoint)으로 덮어쓰이기 때문에 릴리스되지 않을 것이다. 두 번째 원점은 직사각형 자신이 릴리스될 때에서야, 새 dealloc 메서드에 의해 해제될 것이다.
따라서, 직사각형에 새 원점을설정해 주기 전에 이전값을 릴리스해 줘야한다.이를 위해 setOrigin: 메서드를 다음과 같이 수정한다.
-(void) setOrigin: (XYPoint *) pt
{
if (origin)
[origin release];
origin = [[XYPoint alloc] init];
[origin setX: pt.x andY: pt.y];
}
다행스럽게도 접근자 메서드를 자동 생성할 때, 컴파일러가 이 문제를 알아서 해결하도록 만들 수 있다.
상속으로 확장하기 - 새 인스턴스 변수 추가
새 메서드를 추가하여 효과적으로 클래스 정의를 확장할 수 있을뿐 아니라 새 인스턴스 변수도 더할 수 있다. 두 경우 모두 효과는 누적된다. 그러나 상속을 통해 메서드나 인스턴스 변수를 제거할 수는 없다. 오로지 인스턴스 변수를 추가하거나, 메서드를 추가하거나 재정의하는 것만기능하다.
ClassA 와 ClassB 로 돌아가 몇 가지를 바꿔 보자. ClassB에 다음과 같이 변수 y 를 추가한다.
@interface ClassB: ClassA
{
int y;
}
-(void) printVar;
@end
ClassB 가 인스턴스변수를 y 하나만 가지는 것처럼 보이겠지만, 사실 이전에 선언한 것이 있어 인스턴스 변수 두개를 가지게 된다. ClassA 에서 변수 x 를 상속받고,자신의 인스턴스 변수 y 를 추가한 것이다.
물론 NSObject 에게서 상속받는 인스턴스 변수들도 있지만, 일단 이런 상세한 내용은 무시하기로 한다. |
프로그램 8.8 에서 이 개념을 설명한다.
프로그램 8.8
// 인스턴스 변수의 확장
#import <foundation/Foundation.h>
// Class A 선언 및 정의
@interface ClassA: NSObject
{
int x;
}
-(void) initVar;
@end
@implementation ClassA
-(void) initVar
{
x = 100;
}
@end
/ClassB 선언 및 정의
@interface ClassB: ClassA
{
int y;
}
-(void) initVar;
-(void) printVar;
@end
@implementation ClassB
-(void) initVar
{
x = 200;
y = 300;
}
-(void) printVar
{
NSLog (@"x = %i", x);
NSLog (@"y = %i", y);
}
@end
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
ClassB *b = [[ClassB alloc] init];
[b initVar]; // ClassB에서 재정의한 메서드 사용
[b printVar]; // x와 y의 값 표시
[b release];
[pool drain];
return 0;
}
프로그램 8.8 의 출력결과
x = 200
y = 300
ClassB 객체인 b 는 ClassB 에 정의된 initVar 메서드를 호출해 초기화되었다. 이 메서드가 ClassA 의 initVar 메서드를 재정의해 준다는 사실을 기억하자. 이 메서드는 (ClassA 에서 상속받은)x 의 값을 200 으로 설정하고 (ClassB 에서 정의한)y 의 값을 300 으로 설정한다. 그 후 printVar 메서드를 사용하여 이 두 인스턴스 변수의 값을 표시한다.
'어떤 메시지에 응답해 적절한 메시지를 선택하는 개념'은 특히 수신자가 여러 객체중 하나일 기능성이 있을 때 미묘한 점들이 더 많다. 이것은 '동적 바인딩' 이라는 강력한 개념으로, 다음장의 주제다.
추상클래스
'추상클래스'라는 용어를 설명하며 이 장을 마치는 방법보다 괜찮은 마무리는 없을 것이다. 이 용어를 소개하는 이유는 바로 상속과 직접 관계가 있기 때문이다.
이따금, 다른 사람이 좀더 쉽게 서브클래스를 만들 수 있도록 돕는 용도로만 사용되는 클래스가있다. 이런 클래스들은 '추상클래스' 혹은 '추상수퍼클래스' 라고 부른다. 이 클래스에 메서드와 인스턴스 변수가 선언되어 있기는 하지만, 그 클래스에서 인스턴스를 바로 만들어 사용해서는 안 된다. 예를 들어, 루트 객체 NSObject 를 생각해 보자. 이 클래스로 어떤 객체를 정의해 사용하는 상황을 떠올릴 수 있는가?
2부 「Foundation 프레임워크」 에서 다루는 Foundation 프레임워크는 이런 추상클래스를 몇 개 포함한다. 예를 들어, Foundation의 NSNumber 클래스는 추상 클래스다. 이 클래스는 숫자를 객체로 다루기 위해 만들었다. 정수와 부동소수점 수는 일반적으로 저장공간을 다르게 요구한다. NSNumber 의 서브클래스는 각 숫자형에 맞춰 별도로 마련되어 있다. 그러나 이 서브클래스들은 추상 수퍼클래스와 달리, 실제로 존재하므로 '구상(concrete)' 서브클래스라고 한다. 구상 서브클래스는 각각 NSNumber 클래스의 그늘아래 속하며, 이것들을 한데 묶어 '클러스터' 라고 부른다. NSNumber 클래스에 메시지를 보내 새 정수 객체를 만들려고 하면, 정수 객체에 적절한 서브클래스가 사용되어 필요한 메모리를 할당하고 값이 설정된다. 이 서브클래스들은 전부 내부에 감춰져(private) 있다. 직접 접근할 수 없고 추상 수퍼클래스를 통해서만 간접적으로 접근할 수 있다. 추상 수퍼클래스는 모든 종류의 숫자 객체를 다룰 공통 인터페이스를 제공한다. 그로 인해 프로그래머는 어느 형식의 숫자를 숫자객체에 저장했는지,어떻게 설정하고 값을 받아올지를 고민하지 않아도 된다.
분명 이 이야기는 좀 '추상적'일 것이다(미안하게 생각한다) 그러나 걱정할 필요는 없다. 여기서는 이렇게 기본 개념만 맛보는 것으로 충분하다.
연습문제
1. 프로그램 8.1 에 ClassB 의 서브클래스로 ClassC 를 생성하라. initVar 메서드가 인스턴스 변수 x 의 값을 300 으로 설정하도록 만들어라. ClassA, ClassB, ClassC 객체를 선언하고 해당하는 initVar 메서드를 호출하는 테스트 루틴을 작성하라.
2. 고해상도 장치를 다룰 때는, 단순한 정수가 아닌 부동소수 값으로 특정 위치를 표시하는 좌표계를 사용하기도 한다. 이 장의 XYPoint 와 Rectangle 클래스를 수정하여 부동소수점 수를 다루도록 만들어라. 직사각형의 너비, 높이, 넓이, 둘레도 모두 부동소수점 수를 다루도록 해야 한다.
3. 프로그램 8.1을 수정하여 ClassB처럼 ClassA의 서브클래스인 ClassB2 를 추가하라
- ClassB 와 ClassB2 의 관계를 뭐라고 부를 수 있을까?
- NSObject, ClassA, ClassB, ClassB2 사이의 계층관계를 설명해 보자 .
- ClassB 의 수퍼클래스는 무엇인가?
- ClassB2 의 수퍼클래스는 무엇인가?
- 한 클래스는 서브클래스를 얼마나 많이 가질 수 있는가? 또, 수퍼클래스를 얼마나많이 보유할수 있는가?
4. XYPoint(Xy, Yv)라는 벡터를 인수로 받는 translate: 메서드를 Rectangle 클래스에 추가한다. 이 메서드에서 주어진 벡터에 따라 직사각형의 원점을 옮겨 보자.
5. GraphicObject 라는 새 클래스를 정의하여 NSObject 의 서브클래스로 만들어라. 다음 인스턴스 변수를 새 클래스에 정의한다.
int fillColor; // 32비트 색상
BOOL filled; // 객체 내부가 칠해져 있는가?
int lineColor; // 32비트 선 색깔
이 변수들의 값을 설정하고 값을 받아오는 메서드를 작성하라.
Rectangle 클래스를 GraphicObect 의 서브클래스로 만들어라.
새 클래스 Circle 과 Triangle 을 작성하고 둘 다 GraphicObect 의 서브클래스로 만든다. 이 객체들의 다양한 매개변수를 설정하고 받아올 메서드를 작성한다. 또한 원의 둘레와 넓이를 계산하고, 삼각형의 둘레와 넓이를 계산할 메서드도 작성하라.
6. intersect: 라는 메서드를 Rectangle 클래스에 작성하라. 이 메서드는 직사각형들을 인수로 받아 두 직사각형의 겹쳐지는 직사각형 영역을 반환한다. 예를들어 그림 8.10 과 같은 두 직사각형이 있을 때, 이 메서드는 원점이 (400, 420)이고 너비가 50, 높이가 60 인 직사각형을 반환해야한다.
만일 직사각형이 겹치지 않는다면, 원점이 (0,0)이고 너비와 높이가 모두 0 인 직사각형을 반환한다.
7. 가로 선(-)과 세로 선 (|)을 사용하여 직사각형을 그라는 draw 메서드를 Rectangle 클래스에 작성하자. 다음 코드 조각을 사용하면 그 아래에 나온 그림처럼 표시되어야 한다.
Rectangle *myRect = [[Rectangle alloc] init];
[myRect setWidth: 10 andHeight: 3];
[myRect draw] ;
[myRect release];
----------
| |
| |
| |
----------
NSLog는 호출될 때마다 새 줄에 출력하으로, 문자를 그릴 때 NSLog 대신 printf를 사용해야 한다. |