ProgrammingInObjectiveC:Chapter 07

From 흡혈양파의 번역工房
Jump to navigation Jump to search
7장
클래스에 대해서

7장 :: 클래스에 대해서

이 장에서도 클래스를 사용하고 메서드를 작성하는 방법을 살펴본다. 또한 프로그램을 반복하기, 의사결정하기, 표현식 사용하기등 지금까지 배워온 개념을 적용한다. 먼저 더 큰 프로그램을 손쉽게 다룰수 있도록 프로그램을 여러 파일로 나눠보자.


인터페이스와 구현 파일 나누기

클래스선언과 정의를 다른 파일로 나누는 일에 익숙해져야할 때가왔다.

만일 xcode 를 사용한다면 Fraction 이라는 프로젝트를 새로 생성하자. 다음 프로그램 코드를 FractionTest.m 파일에 입력하자.


프로그램 7.1 fractionTest.m 메인 테스트 프로그램


#import "Fraction.h"

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

    // 분수를 1/3로 설정한다.

    [myFraction setNumerator: 1];
    [myFraction setDenominator: 3];

    // 분수를 표시한다.

    [myFraction print];
    [myFraction release];

    [pool drain];
    return 0;
}


이 파일에 Fraction 클래스의 정의가 포함되지 않았음에 주의하자. 그 대신에, Fraction.h 파일을 임포트한다.

보통 클래스 선언(@interface 부분)은 '정해 둔 클래스명'.h 라는 파일에 저장한다. 클래스 정의 (@implementation 부분)는 대개 동일한 이름에 확장자가 .m 인 파일에 저장한다. 따라서 Fraction 클래스는 Fraction.h 에서 선언하고, Fraction.m 에서 정의한다.

Xcode 에서 이를 수행하려면 File 메뉴에서 New File을 선택한다. 왼쪽에서 Cocoa 를 선택하고 오른쪽 상단에서 Objective-C class 를고른다(그림 7.1).

그림 7.1 Xcode 의 New File 메뉴


New 버튼을 누르자. 파일 이름에 Fraction.m 을 입력하고 Alsa create "Fraction.h" 체크박스를 선택한 채로 둔다. 이 설정은 파일을 FractionTest.m 이 담긴 폴더와 동일한 위치에 설정해준다. 그림 7.2 와같이 입력한다.

그림 7.2 프로젝트에 새로운 클래스 추가하기


이제 Finish 버튼을 누른다. Xcode 는 프로젝트에 Fraction.h 와 Fraction.m 이라는 두 파일을 추가할 것이다. 그림 7.3 은 파일이 추가된 프로젝트다.

그림 7.3 Xcode가 새로운 클래스의 파일을 추가했다.


지금은 코코아를 사용하지 않으므로 Fraction.h 의 다음 코드를 아래 코드와 같이 변경해주자.

#import <Cocoa/Cocoa.h>

#import <Foundation/Foundation.h>


동일한 파일(Fraction.h)에서 Fraction 클래스의 인터페이스 부분을 입력한다.

이 인터페이스 파일은 컴파일러(와 다른 프로그래머들)에게 Fraction 클래스가 어떻게 생겼는지를 알려 준다. 정수 인스턴스 변수 두 개 (numerator, denominator)를 포함하며, print, setNumerator:, setDenominator:, numerator, denominator, convertToNum 등 인스턴스 메서드롤 여섯 개 보유한다. 처음 세 메서드는 반환값이 없고, 그 다음 두 개는 int 를, 마지막 메서드는 double 을 반환한다. 메서드 setNumerator: 와 setDenominator: 는 각각 정수 인수를 받는다.


Fraction 클래스는 Fraction.m 파일에서 구현된다.


프로그램 7.1 Fraction.h 인터페이스 파일


//
// Fraction.h
// FractionTest
//
// Created by Steve Kochan on 7/5/08.
// Copyright 2008 __MyCompanyName__. All rights reserved.
//

#import <Foundation/Foundation.h>

// Fraction 클래스

@interface Fraction : NSObject
{
int numerator;
int denominator;
}
-(void) print;
-(void) setNumerator: (int) n;
-(void) setDenominator: (int) d;
-(int) numerator;
-(int) denominator;
-(double) convertToNum;

@end



프로그램 7.1 Fraction.m 구헌 파일


//
// Fraction.m
// FractionTest
//
// Created by Steve Kochan on 7/5/08.
// Copyright 2008 __MyCompanyName__. All rights reserved.
//

#import "Fraction.h"

@implementation Fraction
-(void) print
{
    NSLog (@"%i/%i", numerator, denominator);
}

-(void) setNumerator: (int) n
{
    numerator = n;
}

-(void) setDenominator: (int) d
{
    denominator = d;
}

-(int) numerator
{
    return numerator;
}

-(int) denominator
{
    return denominator;
}

-(double) convertToNum
{
    if (denominator != 0)
        return (double) numerator / denominator;
    else
        return 1.0;
}


다음 명령문으로 인터페이스 파일을 구현파일에 임포트한다는 사실을 잊지 말자.

#import "Fraction.h"


이를 통해 컴파일러는 Fraction 클래스를 위해 선언된 클래스와 메서드를 알 수 있고, 두 파일 사이의 일관성을 보장할 수 있게 된다. 보통 구현 부분에서 클래스의 인스턴스 변수를 다시 선언하지 않기 때문에(원한다면다시 선언해도된다) 컴파일러는 Fraction.h 의 인터페이스 부분에서 선언 정보를 얻어 와야 한다.

이 파일에서 살펴봐야 할 사항이 또 있다. 바로 임포트한 파일이 <Foundation/Foundation.h> 처럼 꺾쇠(< >)가 아닌 큰따옴표(" ")로 둘러싸여 있다는 점이다. 큰따옴표는 시스템 파일이 아닌 로컬 파일(여러분이 만든 파일)에 사용되며 시스템에게 그 파일을 어디서 찾아야 하는지를 알려 준다. 큰따옴표를 쓰면 컴파일러는 먼저 현재 디렉터리에서 지시한 파일을 찾는다. 그 후 다른곳들에서 찾게 된다. 필요하다면 컴파일러가 검색할 실제 위치를 지정해줄 수 있다.


파일 FractionTest.m 에 입력한 프로그램으로 우리 예제를 테스트해 보자.


프로그램 7.1 FractionTest.m 메인 테스트 프로그램


#import "Fraction.h"

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

    // 분수를 1/3로 설정한다.

    [myFraction setNumerator: 1];
    [myFraction setDenominator: 3];

    // 분수를 표시한다.
    NSLog (@"The value of myFraction is:");
    [myFraction print];
    [myFraction release];

    [pool drain];
    return 0;
}


다시 한 번 주의하자. 테스트 프로그램 FractionTest.m 에는 구현 파일인 Fraction.m 이 아닌 인터페이스 파일 Fraction.h 가 임포트되어 있다. 이제 프로그램을 세 파일에 분리해서 저장했다. 규모가 작은 프로그램에서는 괜히 일만 늘어나는 듯하겠지만, 큰 프로그램을 다루고 다른 프로그래머와 클래스 선언을 공유하기 시작하면 이 방식이 유용함을 바로 깨닫게 될 것이다.

이제 이전과 동일한 방식으로 프로그램을 컴파일하고 실행하자. Build 메뉴에서 Build and Go 를 선택하거나 Xcode 메인 창에서 Build and Go 아이콘을 클릭하자.

커맨드라인에서 프로그램을 컴파일한다면 Objective-C 컴파일러에 '.m' 파일 두 개를 모두 알려야 한다. gcc를 사용하면 다음과 같이 작성한다.

localhost # gcc -framework Foundation Fraction.m FractionTest.m -o FractionTest


빌드 결과, FractionTest 라는 실행 파일이 생긴다.


프로그램 7.1의 frac1ionTest 출력 결과


The value of myFraction is:
1/3



자동 생성 접근자 메서드

Objective-C 2.0 부터는 세터와 게터 메서드(둘을 합쳐 '접근자 메서드'라고 한다)를 자동으로 생성할 수 있다. 지금까지는 이 방법을 일부러 설명하지 않았다. 일단 이 메서드들을 직접 작성하는 방법을 배우는 게 중요하기 때문이었다. 그러나 이는 프로그래밍 언어가 제공하는 매우 편리한 기능임이 분명하다. 자동 생성 방법을 배워보자.

첫 단계는 인터페이스 부분에서 @property 지시어를 사용해서 프로퍼티를 지정해 주는 것이다. 이 프로퍼티들은 보통 인스턴스 변수라고 부른다. Fraction 클래스의 경우, numerator 와 denominator 가 여기에 해당된다. 다음은 @property 지시어를 추가한 새로운 인터페이스다.

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

-(void) print;
-(double) convertToNum;
@end


이제 numerator, denominator, setNumerator:, setDenominator: 같은 게터와 세터 메서드에 대한 정의가 포함되지 않는다. 이 점에 유의하자 이것들은 Objective-C 컴파일러가 자동으로 생성(synthesize) 해 줄 것이다. 어떻게 자동 생성이 가능한걸까? 다음 코드처럼 구현 부분에 간단히 @synthsize 지시어를 사용해 주면 된다.

#import "Fraction.h"

@implementation Fraction

@synthesize numerator, denominator;

-(void) print
{
    NSLog (@"%i/%i", numerator, denominator);
}

-(double) convertToNum
{
    if (denominator != 0)
        return (double) numerator/ denominator;
    else
        return 1.0;
}


다음 줄은 Objective-C 컴파일러에게 언급한 두 인스턴스 변수, numerator 와 denominator 에 해당하는 게터와 세터 메서드를 생성하도록 지시한다.

@synthesize numerator, denominator;


일반적으로 x 라는 인스턴스 변수가 있을 때, 구현 부분에 다음 코드를 추가하면 컴파일러가 자동으로 x 라는 게터 메서드와 setX: 라는 세터 메서드를 생성한다.

@synthesize x


여기서는 이것이 대수롭지 않게 보일지 모른다. 그러나 컴파일러가 작성한 접근자 메서드는 효율적일 뿐아니라 다중스레드,다양한 시스템, 다중코어 환경에서 좀더 안전하게 돌아간다.

이제 프로그램 7.1 로 돌아가서 인터페이스와 구현 부분을 변경하여 접근자 메서드를 자동으로 생성하자 FractionTest.m 파일은 손대지 않고, 프로그램이 정상적으로 돌아가는지 확인하자.


점 연산자(.)를 사용하여 프로퍼티에 접근하기

Objective-C 는 프로퍼티에 좀더 쉽게 접근할 수 있는 문법을 제공한다. myFraction 에 저장된 분모의 값을 가져오려면 다음과 같이 코드를 작성할 것이다.

[myFraction numerator]


이 코드는 myFraction 객체에 nurnemtor 메시지를 보내고, 그 결과로 원하는 값이 반환된다. Objective-C 2.0 부터는 다음 표현식을 사용해도 동일한 결과를 얻을 수 있다.

myFraction.numerator


일반적인 사용법은 다음과 같다.

instance.property


다음 문법을 사용하여 값을 할당해 줄 수도 있다.

instance.property = value


이것은 다음 표현식을 사용하는 것과 동일하다.

[instance setProperty: value]


프로그램 7.1 에서 분수의 값을 1/3로 설정하려면 다음 코드 두 줄을 사용한다.

[myFraction setNumerator: 1];
[myFraction setDenominator: 3];


이 표현식을 다음처럼 작성해도 동일하게 동작한다.

myFraction.numerator = 1;
myFraction.denominator = 3;


앞으로 메서드를 자동 생성하고 프로퍼티에 접근하는 이 새로운 기능을 자주 보게 될 것이다.


메서드에 여러 인수 넘져주기

Fraction 클래스에 몇 가지를 추가해 보자. 지금까지 여섯 메서드를 정의했다. 만일 한 메시지로 분자와 분모를 설정할 수 있다면 매우 편리할 것이다. 인수를 여러개 받는 메서드를 정의할 때는, 그저 콜론 뒤에 인수를 연속으로 나열하면 된다. 이 인수들이 메서드 이름을 구성한다. 예를 들어, 메서드 이름이 addEntryWithName:andEmail: 이라면 이 메서드는 이름과 전자우편 주소라는 두 가지 인수를 받는다. addEntryWithName:andEmail:andPhone: 메서드는 이름, 이메일 주소 전화번호까지 세 인수를 받는다.


따라서 분자와 분모를 동시에 설정하는 메서드의 이름은 setNumerator:andDenominator: 가 될 테고, 다음과 같이 사용할 것이다.

[myFraction setNumerator: 1 andDenominator: 3];


이렇게 사용하는 것도 그 나름대로 괜찮다. 사실, 메서드명으로 가장 먼저 떠오를 만한 이름이다. 그런데 좀더 읽기 편한 메서드 이름을 생각해 볼 수는 없을까? 예를 들어 setTo:over: 는 어떨까? 첫눈에는 매력적인 이름 같아 보이지 않을 수도 있다. 그러나 myFraction 을 1/3 로 설정하는 다음 메시지를 위 메시지와 비교하면 생각이 달라질지도 모른다.

[myFraction setTo: 1 over: 3];


내 생각에는 이 방식이 더 읽기 쉬운 것 같다. 선택은 당신에게 달려 있다(어떤이들에게는 첫 번째 이름이 더 좋게 여겨질 수도 있다. 첫 번째 이름은 클래스에 포함된 인스턴스 변수 이름을 사용하기 때문이다). 다시 말하지만, 프로그램의 가독성을 높이는 좋은 메서드 이름을 사용하는 것은 중요하다. 실제로 메시지 표현문을 작성해 보면 좋은 이름을 고르는 데 도움이 많이 된다.


이 새로운 메서드를 동작시켜 보자. 먼저 프로그램 7.2 처럼 인터페이스 파일에 setTo:over: 의 선언을 추가한다. 구현 파일에 새 메서드의 정의를추가하자.


프로그램 7.2 Fraction.h 인터페이스 파일


#import <Foundation/Foundation.h>

// Fraction 클래스를 선언한다.
@interface Fraction : NSObject
{
    int numerator;
    int denominator;
}

@property int numerator, denominator;

-(void) print;
-(void) setTo: (int) n over: (int) d;
-(double) convertToNum;
@end



프로그램 7.2 Fraction.m 구헌 파일


#import "Fraction.h"

@implementation Fraction

@synthesize numerator, denominator;

-(void) print
{
    NSLog (@"%i/%i", numerator, denominator);
}

-(double) convertToNum
{
    if (denominator != 0)
        return (double) numerator / denominator;
    else
        return 1.0;
}

-(void) setTo: (int) n over: (int) d
{
    numerator = n;
    denominator = d;
}


새로 추가한 setTo:over: 메서드는 그저 정수 인수 n 과 d 를 분수에서 해당하는 인스턴스 변수 numerator, denominator 에 대입할 뿐이다.

다음 테스트프로그램으로 새 메서드를 테스트해 보자.



프로그램 7.2 FractionTest.m 테스트 파일


#import "Fraction.h"

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

    [aFraction setTo: 100 over: 200];
    [aFraction print];

    [aFraction setTo: 1 over: 3];
    [aFraction print];
    [aFraction release];

    [pool drain];
    return 0;
}

프로그램 7.2 의 출력결과


100/200
1/3


인수 이름 없는 메서드

메서드 이름을 지을 때 인수 이름은 보통 옵션이다. 예를 들어, 다음과 같은 메서드도 선언할 수 있다.

- (int) set: (int) n: (int) d;


이전의 예제와 달리 두 번째 인수의 이름이 주어지지 않았다. 이 메서드 이름은 set:: 이다. 비록 인수에 이름은 붙어 있지 않지만 콜론 두개는 이 메서드가 인수를 두 개 받는다고 알려 준다.

set:: 메서드를 부르려면 다음과 같이 콜론을 인수 구분자로 사용한다.

[aFraction set: 1 : 3];


인수 이름을 생략하는 것은 대개 좋은 프로그래밍 스타일이 아니다. 프로그램을 이해하기 어려워지고 메서드가 보유한 실제 매개변수의 목적이 덜 직관적으로 보이기 때문이다.


분수 계산하기

Fraction 클래스에 작업을 계속해 보자. 먼저, 분수를 더하는 메서드를 작성해 보자. 이 메서드에 add: 라는 이름을 붙이고, 분수를 인수로 받게 만든다. 여기 새 메서드를 선언한 부분이 있다.

-(void) add: (Fraction *) f;


변수 f 의선언을보자.

(Fraction *) f //success case


이 부분은 add: 메서드의 인수가 Fraction 형임을 나타낸다. 여기서 별표는 반드시 있어야 하기 때문에 다음 선언은 제대로 동작하지 않는다.

(Fraction) f //fail case


add: 메서드에 분수 하나를 인수로 넘기고, 메서드가 이 메시지의 수신자에게 그 인수를 더하도록 할 것이다. 다음 표현식은 Fraction bFraction 을 Fraction aFraction 에 더한다.

[aFraction add: bFraction];


어릴적 배웠던 수학을 돌아보자는 의미로, 분수 a / b 와 c / d 의 덧셈을 다음과 같이 계산해 본다.

분수 계산식


@implementation 부분에 다음 코드로 새 메서드를 추가하자.

// 수신자에 분수를 더한다.

-(void) add: (Fraction *) f
{
    // 두 분수를 더하려면:
    // a/b + c/d = ((a*d) + (b*c)) / (b * d)

    numerator = numerator * f.denominator
                + denominator * f.numerator;
    denominator = denominator * f.denominator;
}


이 메시지의 수신자인 Fraction 객체는 자신의 필드 numerator 와 denominator 를 이름으로 바로 참조할 수 있음을 잊지 말라. 그러나 인수 f 의 인스턴스 변수는 이런식으로 참조할 수 없다. 그대신, f 에 .(dot) 연산자를 사용해야 변수를 얻을 수 있다(혹은, f 를 보내 적절한 메시지를 얻을 수 있다).

인터페이스 파일과 구현 파일에 add: 메서드의 선언과 정의를 추가하고 테스트 프로그램 7.3 을 작성하여 출력 결과를 보자.



프로그램 7.3 FractionTest.m 테스트 파일


#import "Fraction.h"

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

    Fraction *aFraction = [[Fraction alloc] init];
    Fraction *bFraction = [[Fraction alloc] init];

    // 두 분수를 1/4과 1/2로 설정하고 둘을 더한다.

    [aFraction setTo: 1 over: 4];
    [bFraction setTo: 1 over: 2];

    // 결과를 표시한다.

    [aFraction print];
    NSLog (@"+");
    [bFraction print];
    NSLog (@"=");

    [aFraction add: bFraction];
    [aFraction print];
    [aFraction release];
    [bFraction release];

    [pool drain];
    return 0;
}

프로그램 7.3 의 출력결과


1/4
+
1/2
=
6/8


테스트 프로그램은 의도한 대로 작동한다. Fraction 객체 두 개, 즉 aFraction 과 bFraction 이 생성되고 초기화된다. 그 후 각각 1/4, 1/2 로 값이 설정된다. 그 다음 Fraction bFraction 이 Fraction aFraction 에 더해지고, 이 덧셈의 결과가 표시된다. add: 메서드는 인수를 메시지의 객체에 더하므로, 값이 변하는 것은 메시지의 수신자임을 다시 한 번 기억하자. main 의 마지막에 aFraction 의 값을 출력하여 이 사실을 확인할 수 있다. aFraction 의 값을 add: 메서드가 호출되기 전에 출력해 줘야 값이 변하기 전의 값을 표시해 줄 수 있다. 이 장의 후반부에 add: 메서드를 재정의하여 메서드가 수신자의 값에 영향을 미치지 않도록 할 것이다.


지역변수

앞의 출력 결과를 보면 1/4 과 1/2 을 더한 결과가 3/4 이 아니라 6/8 로 나왔다. 덧셈 루틴에서 덧셈만 해주고 결과를 약분하지 않았기 때문이다. 분수를 아주 간단히 약분해 주는 reduce 메서드를 추가해 보자.

다시 어릴 적에 배운 수학으로 돌아가 보자. 분수를 약분하려면 분자와 분모를 나누어 맞아 떨어지는 가장 큰 수를 찾고 그 수로 분자와 분모를 나누어야 한다. 전문용어를 쓰자면, 분자와 분모의 최대공약수(gcd) 를 찾아야 하는 것이다. 이미 프로그램 5.7 에서 최대공약수를 찾는 방법을 알아보았다. 잘 기억 나지 않는다면 앞으로 가서 예제를 살펴보자.

이 알고리즘을 사용하여 reduce 메서드를 작성하면 된다.

-(void) reduce
{
    int u = numerator;
    int v = denominator;
    int temp;

    while (v != 0) {
        temp = u % v;
        u = v;
        v = temp;
    }

    numerator /= u;
    denominator /= u;
}


이 reduce 메서드에는 이전에 보지 못했던 새로운 형태가 나와 있다. 메서드 내에서 정수 변수 u, v, temp 를 선언하는 것이다. 이 변수들은 '지역' 변수라고 부르며, 이것들의 값은 reduce 메서드를 실행하는 동안에만 유효하다. 즉, '자신이 정의된 메서드 안에서만 접근 가능' 하다. 이런 면은 main 루틴에서 정의한 변수와 유사하다. 이 변수들도 main 안에서만 유효하며 main 루틴 안에서만 직접 접근할 수 있다. 여러분이 만들어 낸 어떤 메서드도 main 에 적용된 이 변수들에는 직접 접근하지 못한다.

지역 변수는 기본 초기값이 없으므로, 사용하기 전에 특정한 값을 설정해 주어야만 한다. reduce 메서드에 있는 지역변수 세 개는 사용하기 전에 값을 할당받으므로 여기서는 아무런 문제가 발생하지 않는다. 또한 메서드 호출로 값을 리테인 하는 인스턴스 변수와 달리, 지역 변수는 메모리가 없다. 따라서 이 메서드가 변수들을 반환하고 나면(즉, 끝난 뒤라면), 이 변수의 값은 사라진다. 메서드가 호출될 때마다, 메서드 안에서 정의된 지역 변수는 선언할때 지정했던 값으로 초기화된다.


메서드 인자

메서드 인자를 호출하는 이름 역시 지역 변수다. 메서드가 실행될 때 건네지는 인자가 이 변수에 복사된다. 메서드 인자의 사본을 다루기 때문에, '메서드에 건네진 원래 값을 변경할 수는 없다'. 이것은 매우 중요한 개념이다. 다음과 같은 calculate: 라는 메서드가 정의되어 있다고 하자.

-(void) calculate: (double) x
{
    x *= 2;
    ...
}


이 메서드를 다음 표현식으로 호출한다고 하자.

[myData calculate: ptVal];


calculate: 메서드 실행될 때 변수 ptVal 외의 값은지역 변수 x 에 복사된다. calculate: 내에서 x 의 값을 바꾸더라도 ptVal 의 값에는 아무런 영향을 미치지 않는다.

덧붙여 말하자면, 인수가 객체일 때는 객체 내에 저장된 인스턴스 변수를 바꿀 수도 있다. 이에 대해서는 다음 장에서 배운다.


static 키워드

메서드내에서 지역 변수를선언할때 앞에 static 키워드를 적어 두면, 메서드를 여러 번 호출하는 동안 그 값을 유지할 수 있다. 예를 들어, 다음은 정수 hitCount 를 정적 변수로 선언한다.

static int hitCount = 0;


일반적인 지역 변수와 다르게, 정적 변수는 초기값이 0 이다. 따라서,앞서 보여준 선언에서의 초기화 과정은 중복이다. 게다가, 정적 변수는 프로그램을 실행하는 동안 단 한번만 초기화 되고, 메서드가 여러번 호출되는동안 자신의 값을 계속 유지한다.

showPage 메서드에 있는 다음 코드는 메서드가 몇 번 호출되었는지 알아낸다. (혹은 이 경우, 출력된 페이지 수를 알아낼 것이다).

-(void) showPage
{
    static int pageCount =0;
    ...
    ++pageCount;
    ...
}


정적 지역 변수는 프로그램이 시작되면 한번 0 으로 설정되고 showPage 메서드가 호출되는 동안 값을 유지한다.

pageCount를 정적 지역 변수로 만드는 것과 인스턴스 변수로 만드는 것 사이의 차이점에 유의하자. 정적 지역 변수 pageCount 는 showPage 메서드를 실행시키는 모든 객체가 인쇄한 페이지 수를 센다. 후자, 즉 인스턴스 변수의 경우에는 각 객체가 자신만의 pageCount 사본이 있으므로 각 객체가 인쇄한 페이지 수를 센다.

정적 변수나 지역 변수는 자신이 정의된 메서드 내에서만 접근할 수 있다는 사실을 기억하라. 심지어 정적 변수 pageCount 도showPage 내에서만 접근할 수 있다. 만일, 변수를 선언한 부분을 메서드 선언 바깥으로 (보통 구현 파일 앞부분에)옮기면 다른 메서드에서도 접근할수 있다.

#import "Printer.h"
static int pageCount;

@implementation Printer
...
@end


이제 파일에 있는 어떤 인스턴스나 클래스 메서드도 pageCount 변수에 접근할수 있다. 10장 「변수와 데이터 형에 대하여」에서 이 변수의 범위에 대해 더 상세히 다룬다.

분수로 돌아가자. reduce 메서드를 구성하는 코드를 구현 파일 Fraction.m 에 추가하자. 인터페이스 파일 Fraction.h 에도 reduce 메서드를 선언해야 한다. 절대로 잊지 말자. 그 다음에, 프로그램 7.4 에서 새 메서드를 테스트해 보자.


프로그램 7.4 FractionTest.m 테스트 파일


#import "Fraction.h"

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

    Fraction *aFraction = [[Fraction alloc] init];
    Fraction *bFraction = [[Fraction alloc] init];

    [aFraction setTo: 1 over: 4]; // 첫 번째 분수를 1/4로 설정한다.
    [bFraction setTo: 1 over: 2]; // 두 번째 분수를 1/2로 설정한다.

    [aFraction print];
    NSLog (@"+");
    [bFraction print];
    NSLog (@"=");
    [aFraction add: bFraction];

    // 덧셈 결과를 약분하고 표시한다.

    [aFraction reduce];
    [aFraction print];

    [aFraction release];
    [bFraction release];

    [pool drain];
    return 0;
}

프로그램 7.4 의 출력결과


1/4
+
1/2
=
3/4

훨씬 나아진걸 확인할 수 있다!

self 키워드

프로그램 7.4에서는 add: 메서드 바깥에서 함수를 약분하기로 하였다. add: 에서 약분할 수도 었다. 어디서 약분할지는 마음대로 결정할 수 있다. 그런데 reduce 메서드로 약분할 분수를 어떻게 불러야 할까? 메서드 내에서는 인스턴스 변수를 이름으로 직접 부를 수 있다. 그러나 메시지의 수신자를 직접 호출하는 방법은 아직 배우지 않았다.

현재 메서드의 수신자인 객체는 self 키워드로 부르면 된다. 예컨대 add: 메서드에서 다음과 같이 작성해 보자.

[self reduce];


우리가 원했던 대로, add: 메서드의 수신자였던 Fraction 객체에 reduce 메서드가 적용된다. 이 책을 읽는 내내 self 키워드가 얼마나 유용한지를 보게 될 것이다. 일단은 add: 메서드에서 사용하자. 수정된 메서드는 다음과 같을 것이다.

-(void) add: (Fraction *) f
{
    // 두 분수를 더하려면:
    // a/b + c/d = ((a*d) + (b*c)) / (b * d)

    numerator = (numerator * [f denominator]) +
    (denominator * [f numerator]);
    denominator = denominator * [f denominator];

    [self reduce];
}


메서드를 수정하고 나면 드디어 분수가 약분된다.


메서드에서 객체를 생성하고 반환하기

add: 메서드가 메시지 수신자인 객체의 값을 바꾼다는 것을 살펴보았다. 이제 새로운 버전으로 만들어서 덧셈의 결과값을 저장할 분수를 새로 생성하자. 이 경우 메시지를 호출한 객체 (sender)에게 새 Fraction 를 반환한다. 다음은 add: 메서드를 새로 정의한 것이다.

-(Fraction *) add: (Fraction *) f
{
    // 두 분수를 더하려면:
    // a/b + c/d = ((a*d) + (b*c)) / (b * d)

    // result는 덧셈 결과를 저장할 것이다.
    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 *) add: (Fraction *) f;


이 코드는 add: 메서드가 Fraction 객체를 반환하고 인수를 하나 받는다는 것을 나타낸다. 인수는 다른 Fraction 객체인 메시지의 수신자에 더해질 것이다.

이 메서드는 새로운 Fraction 객체인 result 를 생성하고 초기화한다. 그 다음 resuleNum, resultDenom 이라는 지역 변수를 정의한다. 이 변수들을 사용하여 덧셈 계산을 했고, 그 결과로 나온 분자와 분모를 저장할 것 이다.

이전과 같이 덧셈을 수행하고 결과를 분자 분모의 지역 변수에 할당한후, result 를 다음 표현식으로 설정해줄 수 있다.

[result setTo: resultNum over: resultDenom];


결과 값을 약분한 다음 return 명령문을 써서 메시지의 호출자에게 반환한다. Fraction result 가 차지하는 메모리는 add: 메서드 내에서 할당되지만, 반환된 후에 릴리스(메모리 해제)되지 않는다. 메서드의 호출자는 이 객체가 필요하기 때문에, add: 메서드 내에서는 릴리스할 수 없다. 따라서, 이 메서드를 쓸 때는 반환되는 객체가 새 인스턴스이며 반드시 릴리스되어야 함을 알고 있어야 한다. 클래스를 사용하는 이에게 적절한 문서를 제공하여 이를 알려줄 수 있다.


프로그램 7.5로 새로운 add: 메서드를 테스트하자.


프로그램 7.5 main.m 테스트 파일


#import "Fraction.h"
int main (int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    Fraction *aFraction = [[Fraction alloc] init];
    Fraction *bFraction = [[Fraction alloc] init];

    Fraction *resultFraction;
    [aFraction setTo: 1 over: 4]; // 첫 번째 분수를 1/4로 설정한다.
    [bFraction setTo: 1 over: 2]; // 첫 번째 분수를 1/2로 설정한다.

    [aFraction print];
    NSLog (@"+");
    [bFraction print];
    NSLog (@"=");

    resultFraction = [aFraction add: bFraction];
    [resultFraction print];

    // 이번에는 결과를 바로 출력한다.
    // 여기서 메모리 누수가 발생한다.

    [[aFraction add: bFraction] print];
    [aFraction release];
    [bFraction release];
    [resultFraction release];

    [pool drain];
    return 0;
}

프로그램 7.5 의 출력결과


1/4
+
1/2
=
3/4
3/4


여기서 몇 가지 설명해야 할 내용이 었다. 먼저 aFraction 과 bFraction 이라는 두 Fraction 객체를 정의하고, 두 객체의 값을 각각 1/4 과 1/2 로 설정했다. 또한 resultFraction 이라는 Fraction 객체를 정의했다(왜 이 객체는 할당하지 않고, 또 초기화하지 않았을까?). 이 변수는 이후에 나을 덧셈 연산의 결과를 저장하는 데 사용할 것이다.

다음 코드에서는 먼저 add: 메시지를 aFraction에 보내면서, Fraction bFraction 을 인수로 넘긴다.

resultFraction = [aFraction add: bFraction];
[resultFraction print];


그 결과로 메서드가 반환하는 Fraction 은 resultFraction 에 저장되고, print 메시지를 보내 화면에 출력된다. main 에서 resultFraction 을 직접 생성하지는 않았지만, 프로그램 마지막에 이것을 잊지 말고 릴리스하자. add: 메서드에서 생성된 이 객 체를 말끔히 제거하는 일은 당신의 몫이다.

다음 메시지 표현식은 깔끔해 보이긴 하지만, 문제를 발생시키게 된다.

[[aFraction add: bFraction] print];


add: 메서드가 반환하는 Fraction 객체에 print 메시지를 보냈기 때문에, add: 메시지가 생성한 Fraction 객체를 릴리스해 줄 방법이 없다. 이것은 바로 '메모리 누수' 의 예다. 만일 프로그램에서 메시지를 이런식으로 중첩해서 여러번 사용한다면, 메모리가 해제되지 않는 분수를 담기 위해 공간을 잔뜩 쌓아 놓은 꼴이 된다. 매번 직접 복구할 수 없는 메모리 공간을 추가(혹은 '누수' )하게 된다.

이 문제는 여러 가지 방법으로 해결할수 있다. 우선 print 메서드가 수신자를 반환하도록 하여, 반환된 객체를 릴리스하는 방법이 있다. 그렇지만 이는 길을 돌아가는 것이다. 나은 방법이 있다. 바로 앞의 프로그램에서 했듯이 이 중첩 메시지를 메시지 두개로 나누는 것이다.

그런데 add: 메서드에서는 아예 임시 변수인 resultNum, resultDenom 은 사용하지 않아도 된다. 다음 메시지 하나로 대치하면 된다.

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


이렇게 간결한 코드로 작성할 것을 추천하지는 않는다. 그러나 다른 프로그래머가 작성한 코드를 볼때를 대비하여, 이런 강력한 표현식을 어떻게 읽고 이해할지 정도는 알아두는 펀이 좋다.

이 장에서 한 번만 더 분수를 다뤄 보자. 이번에는, 예제로 다음 등비 수열을 계산해야 하는 경우를 생각해보자.

적분 계산식


시그마 기호는 합계를 줄여 쓰는 것이다. 여기서는 i 가 1 에서 n 까지의 정수일 때 1/2(i승) 의 값을 전부 더한다는 의미다. 즉, 112 + 114 + 118 + ... 이다. 만일 n의 값을 충분히 크게 하면 이 수열의 합은 1에 가까워질 것이다. n의 값을 달리 해서 1 에 얼마나 가까워질 수 있는지 알아보자.


프로그램 7.6은 n 의 값을 입력받아 위 계산을 수행한다.


프로그램 7.6 FractionTest.m


#import "Fraction.h"

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

    Fraction *aFraction = [[Fraction alloc] init];
    Fraction *sum = [[Fraction alloc] init], *sum2;
    int i, n, pow2;

    [sum setTo: 0 over: 1]; // 첫 번째 분수를 0으로 설정한다.

    NSLog (@"Enter your value for n:");
    scanf ("%i", &n);

    pow2 = 2;
    for (i = 1; i <= n; ++i) {
        [aFraction setTo: 1 over: pow2];
        sum2 = [sum add: aFraction];
        [sum release]; // 이전의 합계를 릴리스한다.
        sum = sum2;
        pow2 *= 2;
    }

    NSLog (@"After %i iterations, the sum is %g", n, [sum convertToNum]);
    [Afraction release];
    [sum release];

    [pool drain];
    return 0;
}

프로그램 7.6 의 출력결과


Enter your value for n:
5
After 5 iterations, the sum is 0.96875

프로그램 7.6 의 출력결과(재실행)


Enter your value for n:
10
After 10 iterations, the sum is 0.999023

프로그램 7.6 의 출력결과(재실행)


Enter your value for n:
15
After 15 iterations, the sum is 0.999969


Fraction sum 의 값은 분자를 0, 분모를 1 로 설정하여 0 으로 설정된다(만일 분자와 분모를 모두 0 으로하면 어떻게 될까?). 그 후 프로그램은 사용자에게 n의 값을 입력해 달라고 요구하고 scanf 로 그 값을 읽어 들인다. 그 다음 for 문으로 들어가 수열의 합을 계산한다. 먼저 변수 pow2 의 값을 2 로 초기화한다. 이 변수는 2(i승)의 값을 저장하는 데 사용된다. 반복문을 돌 때마다 이 값에 2 가 곱해진다.

for 문은 1번에서 시작해서 n번까지 계속된다. 반복문을 돌 때마다 aFraction 을 1/pow2 혹은 1/2(i승) 로 설정한다. 그 후 이 값은 이전에 정의한 add: 메서드를 사용하여, 합계 sum 에 더해진다. add: 의 반환값을 sum 이 아닌 sum2 에 할당하여 메모리 누수 문제를 피한다(만일 sum에 바로 대입하면 어떻게 될까?). 그 후 이전 sum 은 릴리스된다. 그리고 새로 더한 결과 값인 sum2 가 sum 에 대입되어 다음 반복을 대비한다. 코드에서 분수가 릴리스되는 방식을 고민해 보고 메모리 누수를 피하기위해 사용한 전략에 친숙해지자. 만일 이 for 문이 수백수천 번 실행 되어야 하는데, 분수를 현명하게 릴리스해 주지 않는다면 낭비되는 메모리 공간이 곧 엄청나게 증가할 것이다.

for 문이 종료되면 convertToNum 메서드를 사용하여 소수 값으로 최종 결과를 표시한다. 이제 두 객체만 릴리스해 주면 된다. 바로aFraction 과, sum 에 저장된 마지막 Fraction 객체다. 릴리스하고 나면 프로그램 실행은 종료된다.

최종 결과는 이 프로그램을 맥북 에어에서 세 번 실행해서 나온 것이다. 첫 번째는 수열이 계산되어 결과값 0.96875 가 표시되었고,세 번째는 n 의값을 15 로주고 프로그램을 실행하여 1 에 매우 가까운값이 결과로 나왔다.


클래스 정의 확장과 인터페이스 파일

지금까지 분수를 다루는 메서드의 작은 라이브러리를 개발했다. 이 클래스를 통해 지금껏 이루어낸 것을 다음의 인터페이스 파일에서 모두 볼수 있다.

#import <Foundation/Foundation.h>

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

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

@property int numerator, denominator;

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


앞으로 분수를 다룰 일이 없을 수도 었다. 하지만 이 예제들을 다루면서, 새로운 메서드를 추가하는 방식을 통해 지속적으로 클래스를 다듬고 확장하는 방법을 보았다. 분수를 다루는 사람이 었다면, 그 사람에게 이 인터페이스 파일만 주어도 분수를 다루는 프로그램을 작성하는 데 충분할 것이다. 만일 그 사람이 새 메서드를 추가해야 한다면 클래스에 직접 추가하거나 자신만의 서브클래스를 정의하여 그곳에 새 메서드를 정의해도 된다. 서브클래스를 이용한 방법은 다음 장에서 배울것이다.


연습문제

1. 다음 메서드를 Fraction 클래스에 추가하여 분수에서 행할 연산을 다 추가하자. 각 메서드의 결과는 약분한다.

// 수신자에서 인수를 뺀다.
-(Fraction *) subtract: (Fraction *) f;
// 수신자에 인수를 곱한다.
-(Fraction *) multiply: (Fraction *) f;
// 수신자를 인수로 나눈다.
-(Fraction *) divide: (Fraction *) f;


2. Fraction 클래스에 있는 print 메서드를 수정해서, 선택적인 BOOL 인수를 받아 분수를 표시할 때 약분 여부를 고를 수 있게 해보자. 만일 약분된다면, 분수 자체의 값에는 변화를 주지 말자.


3. 프로그램 7.6을 수정하여 결과 합도 분수로 표시하자.


4. Fraction 클래스가 음의 분수도 제대로 다룰까? 예를 들어, -1/4 과 -1/2 을 더하면 정확한 값을 얻을 수 있을까? 해법을 알아냈다면 프로그램을 작성해 보자.


5. Fraction의 print 메서드를 수정하여 1보다 큰 분수는 대분수로 표시하자. 예를 들어 분수 5/3는 1 2/3 으로 표시되어야 한다.


6. 4장 「데이터형과 표현식」의 연습문제 6번에서 복소수를 다루는 Complex 클래스를 정의했다. 이 클래스에 add: 라는 메서드를 추가하여 두 복소수를 더할 수 있도록하자. 두 복소수를 더하려면 다음과 같이 실수와 허수부를 각각 더하면 된다.

(5.3 + 7i) + (2.7 + 4i) = 8 + 11i

add: 메서드 선언은 다음에 나와 있다. 이 메서드가 새 Complex 수로 결과를 저장하고 반환하도록 하자.

-(Complex *) add: (Complex *) complexNum;


이 테스트 프로그램에서 발생할 수 있는 잠재된 메모리 누수문제를 해결하자.


7. 4장의 연습문제 6번에서 개발한 Complex 클래스와 이 장의 연습문제 6번에서 확장한 클래스에서 인터페이스 파일 Complex.h 와 구현 파일 Complex.m 을 따로 생성하도록 하자. 분리된 테스트 프로그램 파일을 만들어서 모든 것을 테스트하자.


Notes