ProgrammingInObjectiveC:Chapter 03

From 흡혈양파의 번역工房
Jump to navigation Jump to search
3장
클래스,객체,메서드

3장 :: 클래스,객체,메서드

이 장에서는 객체지향 프로그래밍의 핵심 요소들을 배우고 Objective-C 의 클래스들을 다룰 것이다. 알아야 하는 용어가 몇 개 있는데, 알기 쉽게 설명하겠다. 또, 방대한 양에 질리지 않도록 기본 용어만 다룬다. 부록 A 「용어집」에서 이 용어들을 더 자세히 설명한다.


대체 객체가 뭔데?

'객체'는 어떤 것이다. 객체지향프로그래밍을 '어떤 것이 있고 당신이 그것에 취하고 싶은 행등을 정하는 활동' 이라고 생각해 보자. 이것은 C 같은 절차적 프로그래밍 언어와 대조된다. C 언어에서는 객체지향과 정반대로, 보통 하고 싶은 일이 무엇인지부터 생각하고, 객체에 대해 고민한다.

일상을 예로들어보자. 집에 있는 자동차는 분명히 객체이고,당신이 소유한 것이다. 아무차나 소유하는 것이 아니라, 디트로이트나 울산또는 다른 어딘가에 있는 어떤 공장에서 생산된 특정한 차를 소유한다. 또한 차는 등록번호가 부여되어 그 차를 다른차와 구별하여 인식할수 있다.

객체지향의 세계에서 당신의 차는 차의 '인스턴스'다. 더 자세히 설명하자면, car는 그 인스턴스가 생성된 클래스의 이름이다. 따라서 새로운 차가 생산될 때마다, 클래스 car 에서 새로운 인스턴스가 생성되고 car 의 각 인스턴스는 객체라고 부른다.

당신의 자동차는 회색 외부에 내장은 검은색이고, 컨버터블이나 하드 탑 같은 특정이 있을 수 있다. 게다가 당신이 소유한 자동차에 특정한 행등을 취할 수 있다. 예를 들어 자동차를 타거나, 기름을 채우거나, (바라건대) 세차도하고, 수리하는 일을 할 수 있다. 자동차에 취할 수 있는 행등을 표 3.1 에 나열했다.

표 3.1에 나온 행동은 당신이 당신 차에 하는 행동도 되고, 다른 사람들이 그들의 차에 취하는 행동도 된다. 예를 들어 당신 여동생이 자신의 차를 운전하거나 세차하고 또는 기름을 채우는 등의 일을 할 수있다.

객체 객체로 할 수 있는 일
당신의 차 운전한다
인스턴스 와메서드
기름을 채운다
세차한다
수리한다
표 3.1 객체에 취하는 행동

인스턴스와 메서드

클래스로부터 나온 것을 인스턴스라고 부르고, 이 인스턴스가 수행하는 행등을 '메서드'라고 한다. 메서드는 클래스 인스턴스나 클래스 자체에 적용된다. 예를 들어, 당신의 차를 세차한다면, 이는 인스턴스에 적용되는 행동이다(사실, 표 3.1 에 나온 모든 메서드는 인스턴스 메서드다). 반면,자동차 제조사가 만드는 차종 을알아내는 행동은 클래스에 적용되어야 하므로 클래스 메서드다.

조립 공정에서 완전히 똑같아 보이는 차가 두 대 나왔다고 해보자. 외장, 색상 등이 모두 동일하다. 이처럼 처음에는 동일할 수 있지만 각 차의 구매자가 차를 사용하면서 자신만의 특색이 생길 것이다. 예를 들어, 한 차는 옆에 흠집이 나고 다른차는 주행거리가 더 많을수있다. 각 인스턴스나 객체는 공장에서 만든 초기 특성뿐 아니라 현재 특성까지 포함한다. 이 특성들은 동적으로 변할수 있다. 차를 운전하면서 연료가 바닥나고 차가 더러워지고 타이어가 닳을 것이다.

객체에 메서드를 적용하면 객체의 '상태'에 영향을 미치게 된다. 만일 메서드가 '기름을 가득 채우다'라면 이 메서드가 수행된 후에 자동차 연료통이 다 채워질 것이다. 즉, 이 메서드는 자동차 연료통의 상태에 영향을 미친다.

여기서 핵심은 객체가 클래스의 유일무이한 표현이고 각 객체는보통 객체 자신만 접근 가능한(private) 정보(데이터)를 포함한다는 것이다. 메서드는 이 데이터들에 접근하고 수정하는 방법을 제공해 준다.


Objective-C 프로그래밍 언어는 클래스와 인스턴스에 메서드를 적용할 때 다음 문법(syntax)을 사용한다.

[ClassOrInstance method];


이 문법에서는 여는 대괄호([) 다음에 클래스나 그 클래스의 인스턴스 이름이 나오고 그 다음 공백이 나온 후, 수행할 메서드가 나온다. 마지막으로 대괄호를 닫아 주고(]), 명령문을 끝내는 세미콜론(;)을 단다. 클래스나 인스턴스에 행등을 수행하도록 할때는 '메시지' 를 보낸다 고 표현한다. 이 메시지를 받는 응답자를 '수신자'라고 한다. 따라서 메서드를 호출하는 일반적인 형태는 다음과 같다.

[ receiver message ];


앞 목록에 있던 모든 행등을 이 새로운 문법에 맞춰 작성해 보자. 맨 먼저 무엇이 필요할까? 당연히 무슨 행등을 취하기 위해 차가 필요하다. 공장에서 새로 차를 구하자.

yourCar = [Car new]; //새 차를 얻는다.


car 클래스(메시지의 수신자)에게 새 차를 요청한다. 결과로 받는 객체(당신의 유일무이한 차롤 나타낸다)는 yourCar 라는 변수에 저장된다. 이제부터 yourCar 를 사용하여 공장에서 받은당신의 자동차 인스턴스를 부를 수 있다.

공장에서 차를 얻어 오기 때문에 메서드 new 는 팩토리 혹은 클래스 메서드라고 부른다. 나머지 메서드들은 당신의 차에 적용되는 인스턴스 메서드다. 자동차에 적용되는 메시지 표현 예제를 살펴보자.

[yourCar prep]; //자동차를 처음으로 사용할 준비를 한다.
[yourCar drive]; //자동차를 운전한다.
[yourCar wash]; //세차한다.
[yourCar getGas]; //필요하면 기름을 채운다.
[yourCar service]; //차를 수리한다.

[yourCar topDown]; //컨버터블일 경우
[yourCar toUp];
currentMileage = [yourCar currentOdometer];


이 코드의 마지막 줄에는 현재 주행기록계에 나와있는 주행거리 정보를 반환하는 메서드가 있다. 이 정보를 프로그램 내의 currentMileage 변수에 저장한다.

여동생 수(Sue) 역시 동일한 메서드를 자신의 자동차 인스턴스에 사용할수 있다.

[suesCar drive];
[suesCar wash];
[suesCar getGas];


다른 객체에 동일한 메서드를 사용할 수 있는 것이 객체지향 프로그래밍의 핵심요소중 하나다. 이에 대해서는 나중에 상세히 다룬다.

프로그램에서 자동차를 가지고 작업할 일은 별로 없다. 여러분의 객체들은 창이나 사각형, 텍스트조각 혹은 계산기, 재생목록 처럼 컴퓨터와 관련된 것 이다. 자동차에 사용한 메서드와 유사하게 실제로 만들 메서드들도 다음과 같이 표현될 것이다.

[myWindow erase]; //창을 정리한다.
[myRect getArea]; //사각형 영역을 계산한다.
[userText spellCheck]; //텍스트의 철자률 검사한다.
[deskCalculator clearEntry]; //마지막 입력을 정리한다.
[favoritePlaylist showSongs]; //선호하는 곡 목록에서 곡을 보여 준다.
[phoneNumber dial]; //전화번호에 전화를 건다.


분수를 처리하는 Objective-C 클래스

실제로 Objective-C 클래스를 정의하고 클래스의 인스턴스를 다루는 방법을 배울 때가 되었다.

이번에도 절차부터 배운다. 그 결과 실제 프로그램은 별로 실용적이지 않게 보일지 모른다. 실용적인 프로그램을 만드는 내용은 나중에 나온다.

분수를 다루는 프로그램을 만들어 보자. 덧셈, 뺄셈, 곱셈 등을 처리할 수 있어야 한다. 클래스에 대해 모르는 상태였다면 다음과 같이 단순한 프로그램에서 시작했을것이다.


프로그램 3.1


// 분수를 다루는 간단한 프로그램
#import <Foundation/Foundation.h>

int main (int argc, char *argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  int numerator = 1;
  int denominator = 3;
  NSLog (@"The fraction is %i/%i", numerator, demoniator);

  [pool drain];
  return 0;
}

프로그램 3.1 의 출력결과


The fraction is 1/3

int numerator, denominator;

numerator = 1;
denominator = 3;


프로그램 3.1 에서 분수는 분자와 분모로 표시된다. 오토릴리스 풀이 생성된 다음, 두 줄로 된 명령문이 numerator 와 denominator 변수를 정수로 선언하고 초기값으로 각각 1과 3을 할당한다. 이것은 다음과 같이 표현할 수도 있다.

int numerator, denominator;
numerator = 1;
denominator = 3;


분수 1/3을 표시할 때 변수 numerator(분자)에 1 을 저장하고 denominator(분모)에 3을 저장하는 방식을 사용하였다. 만일 분수를 프로그램에 아주 많이 저장해야 한다면, 이런 방식은 상당히 귀찮다. 분수를 찾고 싶을 때마다 해당하는 분자와 분모를 찾아야 한다. 또한 이 분수들을 연산하는 일 역시 틀림없이 귀찮을 것이다.

분수를 단일 개체로 정의하여 분모와 분자를 myFraction 같은 하나의 이름으로 부를 수 있다면 더 좋을 것이다. Objective-C 에서는 클래스를 새로 정의하여 이렇게 할 수 있다.

프로그램 3.2는 프로그램 3.1의 기능을 Fraction 이라는 새 클래스를 이용해 구현한다. 먼저 프로그램 코드를 보고 어떻게 동작하는지 상세히 알아보자.


프로그램 3.2


// 분수를 다루는 프로그램 - 클래스 버전

#import <Foundation/Foundation.h>

//---- @interface 부분 ----

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

-(void) print;
-(void) setNumerator: (int) n;
-(void) setDenominator: (int) d;

@end

//---- @implementation 부분 ----

@implementation Fraction

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

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

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

@end

//---- program 부분 ----

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

  // Fraction의 인스턴스를 생성한다.

  myFraction = [Fraction alloc];
  myFraction = [myFraction init];

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

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

  // print 메서드를 분수로 표시한다.

  NSLog (@"The value of myFraction is:");
  [myFraction print];
  [myFraction release];

  [pool drain];
  return 0;
}

프로그램 3.2 의 출력결과


The value of myFraction is:
1/3


프로그램 3.2의 주석에서 보듯이 이 프로그램은 논리적으로 세부분으로 나뉜다.

  • @interface 부분
  • @implementation 부분
  • progrnm 부분


@intertace 부분은 클래스, 데이터 요소, 메서드를 선언하고 @implementation 부분은 이 메서드들을 구현하는 실제 코드가 담겨 있다. 마지막으로 program 부분 에는 프로그램이 달성하려는 목적을 실행하는 프로그램 코드가 들어 있다.

비록 직접 각 부분을 작성하지 않더라도 모든 Objective-C 프로그램에는 이 세부분이 전부 포함되어 있다. 앞으로 보겠지만 각 부분은 보통 다른파일에 따로 저장된다. 그러나 지금은 한개의 파일에 이 세 부분이 몽땅 들어 있다.


@interface 부분

클래스를 새로 정의할 때 해야 할 일이 몇 가지 있다. 먼저, Objective-C 컴파일러에게 클래스가 어디서 왔는지 알려준다. 이것은 '부모.(parent)' 클래스를 알려주어야 한다는 뜻이다. 둘째로, 이 클래스의 객체 내에 저장될 데이터의 형 이 무엇인지 지정해준다. 이 말은 클래스의 멤버가 포함할 데이터를 설명해야 한다는 것이다. 이 멤버들은 '인스턴스변수' 라고 부른다. 마지막으로 이 클래스의 객체를 다룰때 사용하는 메서드를 정의한다. 이 모든작업은 프로그램의 @interface 라는 특별한 부분에서 수행된다. 이 부분은 보통 다음과 같은 형태다.

@interface NewClassName: ParentClassName
{
  memberDeclarations;
}

methodDeclarations;
@end


반드시 지켜야 하는 것은 아니지만, 클래스 이름은 대문자로 시작하는 것이 관례다. 이 관례를 지키면 프로그램의 소스코드를 볼 때, 첫 자 만으로도 클래스인지 변수인지 쉽게 구별할수 있다. Objective-C 에서 이름 짓기에 대해 잠깐 살펴보자.


이름 정하기

2장 「Objective-C 로 프로그래밍하기」에서 정수 값을 저장하는 변수를 몇 개 사용했다. 예를 들어 프로그램 2.4에서 sum을 정수 50과 25의 합을 저장하는 데 사용하였다.

Objective-C 언어에서는 프로그램에서 변수를 사용하기 전에 선언만 제대로 해준다면 정수가 아닌 데이터 형도 저장 가능하다. 변수는 부동소수점 수, 문자, 객체(좀더 정확히는 객체의 참조)를 저장할수 있다.

이름을 정하는 규칙은 꽤 단순하다. 문자나 언더스코어(_)로 시작하고 그 후에는 아무문자(대소문자 상관없음), 언더스코어, 숫자 0부터 9까지의 조합이 나오면 된다.다음은 사용 가능한 이름이다 .

  • sum
  • pieceFalg
  • i
  • myLocation
  • numberOfMoves
  • _sysFlag
  • ChessBoard


반면에 다음 이름은 사용하지 못한다

  • sum$value - $는 사용할수없다.
  • piece flag - 빈칸은 사용할 수 없다.
  • 3Spencer - 이름은 숫자로 시작할수 없다.
  • int - 예약어는 이름으로 쓸 수 없다


int는 Objective-C 컴파일러에서 특별한 의미로 사용되기 때문에 변수 이름으로 쓰지 못한다. 이런 단어들을 '예약어'라고 한다. 보통 Objective-C 컴파일러에게 특별한 의미가 있는 이름은 변수 이름으로 쓸 수 없다. 부록 B 「Objective-C 2.0 언어 요약」에 이런 예약어가 모두 나와있다.

Objective-C 는 대소문자를 구분한다는 점을 주의해야 한다. 따라서 변수 이름 sum, Sum, SUM 은 제각기 다른 변수를 지칭한다. 조금 전에 말했듯이 클래스 이름은 대문자로 시작한다. 반면 인스턴스 변수, 객체,메서드의 이름은 보통 소문자로 시작한다. 가독성을 높이기 위해, 이름 증가에 새로운 단어가 시작될 경우, 이 단어의 첫 자도 대문자로 표시한다.

  • AddressBook - 클래스 이름으로 사용
  • currententry - 객체 이름으로사용
  • current_entry - 어떤이들은언더스코어로단어를분리한다 *
  • addNewEntry - 메서드 이름으로사용


이름을 정할 때에는 늘 한 가지 조언을 염두에 두어야 한다. 바로 게으름 피우지 말아라. 변수나 객체가 어떻게 사용될지 생각하고 목적에 맞는 이름을 골라야 한다. 그 이유는 너무나 당연하다. 주석문과 마찬가지로 의미 있는 이름은 프로그램의 가독성을 엄청나게 높여 주고 디버그와 문서화 단계에서 그 진가를 발휘하게 된다. 실제로 프로그램 코드가 자명해지면 문서화 작업은 매우 쉬워진다.

다시 프로그램 3.2의 @intertace 부분을 보자.

//---- @interface 부분 ----

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

-(void) print;
-(void) setNumerator: (int) n;
-(void) setDenominator: (int) d;

@end


새 클래스의 이름(NewClassName)은 Fraction 이고, 부모 클래스는 NSObject 다(부모 클래스에 대해서는 8장 「상속」 에서 더 상세하게 다룬다). NSObject 클래스는 NSObject.h 파일에 정의되어 있고, 이 파일은 Foundation.h 을 임포트하면 자동으로 프로그램에 포함된다.


인스턴스변수

memberDeclarations 부분에서는 Fraction 에 저장된 데이터 형과 이름을 지정한다. 앞에 나온 코드에서 볼 수 있듯이, 이 부분은 중괄호로 묶여 있다. Fraction 클래스의 이런 선언은 Fraction 객체가 numerator 와 denominator 라는 두 정수 멤버를 가지고 있다는 의미다.

int numerator;
int denominator;


이 부분에서 선언된 멤버는 '인스턴수 변수'라고 한다. 새 객체를 생성할 때마다 이 인스턴스 변수들도 새로 생성된다. 따라서 Fraction 객체가 두 개인데, 하나는 fracA 이고 다른 하나는 fracB 라면, 이 두 객체는 각각 자신만의 인스턴스 변수 모음을 가지고 있을 것이다. 다시 말해서, fracA 와 fracB는 각기 자신만의 numerator 와 denominator 를 갖는다. Objective-C 시스템은 자동으로 이것들을 관리해 주기 때문에 프로그래머가 직접 관리할 필요가 없다. 이것도 객체를 다루는 작업이 지닌 장점 중 하나다.


클래스와 인스턴스 메서드

Fraction 객체를 다룰 메서드를 정의해야 한다. 분수의 값을 원하는 대로 설정할 수 있어야 하기 때문이다. 분수 객체의 내부표현에 직접 접근할수 없으므로(달리 말하면, 내부 변수에 직접 접근할 수 없으므로) 분자와 분모를 설정해 줄 print라는 메서드를 작성해야 한다. 인터페이스 파일 내에서 print 메서드를 다음과 같이 선언한다.

- (void) print;


맨 앞에 있는 빼기 부호(-)는 Objective-C 컴파일러에게 이 메서드가 인스턴스 메서드라고 알려 준다. 더하기 부호(+)는 클래스 메서드를 나타낸다. 클래스 메서드는 클래스의 새로운 인스턴스를 만드는 것과 같이 클래스 자체에 작업을 수행한다. 이는 차를 새로 생안하는 것과 비슷한데 , 차는 클래스이고 새 차를 만드는 것은 클래스 메서드가 되는 것이다.

인스턴스 메서드는 클래스의 특정 인스턴스에 작업을 수행한다. 예를 들어, 인스턴스 값을 설정하거나, 값을 받아 오거나 표시하는 등의 일이다. 다시 자동차를 예로 설명하면, 차를 생산한 뒤에 기름을 채워야 할 것이다. 기름을 채우는 일은 특정한 차에 하는 일이다. 인스턴스 메서드도 이와 유사하다.


반환값

새 메서드를 선언할 때 Objective-C 컴파일러에게 그 메서드가 값을 반환하는지, 또 반환한다면 반환하는 값의 종류가 무엇인지 알려 주어야 한다. 맨 처음 등장하는 더하기 기호나 빼기 부호 뒤에 나오는 괄호 안에 반환 형을 적어서 값의 종류를 표시한다. 따라서 다음 선언은 인스턴스 메서드 retrieveNumerator 가 정수 값을 반환한다는 지시다.

-(int) retrieveNumerator;


다음 선언도 비슷하게 2배의 정밀도를 갖는 값(double 값)을 반환하는 메서드를 선언한다(데이터 형에 대해서는 4장 「데이터 형과 표현식」에서 좀더 상세히 배울것이다).

- (double) retrieveDoubleValue;


앞에 나온 예제에서, main 에서 값을 반환할 때처 럼 Objective-C 의 return 명령문을 사용하여 메서드에서 값을 반환한다.

만일 메서드가 아무 값도 반환하지 않는다면 다음과 같이 void 형을 사용한다.

-(void) print;


이 선언은 인스턴스 메서드 print 가 아무런 값도 반환하지 않음을 나타낸다. 이런 경우, 메서드의 맨 마지막에 return 명령문을 실행할 필요가 없다. 혹은 return 에 아무값도 붙이지 않고 다음과 같이 실행해도 된다.

return;


메서드의 반환 형을 지정하는 편이 나은 습관이기는 하지만 지정하지 않을수도 있다. 형을 지정해 주지 않는다면 id 가 기본 형이 된다. id 데이터 형에 대해서는 9장 「다형성,동적 타이핑,동적 바인딩」에서 상세히 배울 것이다. 기본적인 부분만 설명하자면, id 형을 사용해서 어느종류의 객체든 참조할 수 있다.


메서드인수

프로그램 3.2의 @interface 부분에 다음 두가지 메서드도 선언되어 있다.

-(void) setNumerator: (int) n;
-(void) setDenominator: (int) d;


이 두 메서드는 모두 아무 값도 반환하지 않는다. 그 대신 인수를 받는데, 인수이름 앞에 (int)라고 지시된 대로 정수형 인수를 받는다'. setNumerator 의 경우, 인수 이름이 n이다. 이 이름은 아무렇게나 지어줄 수 있으며 이 이름을 사용해서 메서드 내에서 해당 인수에 접근한다. 따라서, setNumerntor 선언에서는 n이라는 하나의 정수 인수가 메서드에 넘겨지고, 메서드는 아무 값도반환하지 않음을 지정한다. 이 것은 setDenominator 에서 인수 이름이 d인 것을 제외하고 거의 유사하다.

이 메서드들을 선언하는 문법에 주의하자. 각 메서드 이름은 콜론(:)으로 끝나는데, 이것은 Objective-C 컴파일러에게 메서드가 인수를 받는다는 것을 알라는 의미다. 그 다음에는 메서드의 반환값을 표시하는 것과 마찬가지로, 인수의 형이 괄호안에 나타난다. 마지막으로 메서드에서 사용할 인수를 나타내는 이름이 나온다. 이 전체 선언은 세미콜론으로 종료된다. 그림 3.1은 이 문법을 나타낸다.

그림 3.1 메서드선언


메서드가 인수를 받는다면, 그 메서드를 부를 때 콜론까지 붙여야 한다. 따라서, 인수를 하나만받는 이 두 메서드를 setNumerator: 와 setDenominator: 라고 지칭한다. 인수를 받지 않는 print 메서드의 경우, 콜론 없이 print라고 부른다. 7장 「클래스에 대해서」에서 인수를 여러 개 받는 메서드를 부르는 방법을 배운다.


@implementation 부분

앞에서 언급한 대로, @implementation 부분은 @interface 부분에서 선언한 메서드의 실제 코드를 담고 있다. 용어 측면에서 보면, 메서드를 @interface 부분에서 '선언'하고, @implementation 부분에서 '구현' 즉 실제 코드 작성을 하는 것이다.


@implementation 부분은 대개 다음과 같은 형태다.

@implementation NewClassName
    methodDefinitions;
@end


NewClassName 은 @interface 부분에서 사용한 클래스의 이름과 동일하다. @interface 와 동일하게 클래스 이름 뒤에 콜론을 붙이고 부모 클래스의 이름을 써줄수있다.

@implementation Fraction: NSObject


그러나 구현 파일에서 부모클래스를 적을지는 프로그래머 마옴대로선 택할 수 있으며, 보통 생략된다.

@implementation 의 methodDefinitions 부분은 @interface 부분에서 지정한 각 메서드의 코드를 담고 있다. @interface 부분과 유사하게 각 메서드의 정의는 메서드 종류(클래스 메서드인지 인스턴스 메서드인지)를 나타내는 것에서 시작하여, 반환 형, 인수 형과 인수까지 이어진다. 그러나 세미콜론으로 각 줄이 끝나는 것과 달리 메서드의 코드가 중괄호 안에 포함되어 있다.

프로그램 3.2의 @implementation 부분을 살펴보자.

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

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

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

@end


print 메서드는 NSLog 를 사용하여 인스턴스 변수 numerator 와 denominator 의 값을 표시한다. 그런데 대체 어떤분자와 분모를 참조하여 표시하는 것일까? print 메시지의 수신자인 객체에 포함된 인스턴스 변수를 참조한다. 이것은 중요한 개념이므로, 곧 다시 설명하겠다.

setNumerator: 메서드는 n이라고 부르는 정수 인수를 인스턴스 변수 numerator 에 저장한다. 이와 비슷하게 setNumerator: 메서드도 인수 d의 값을 인스턴스 변수 denominator 에 저장한다.


program 부분

program 부분은 당신의 독특한 문제를 해결할 코드를 (필요하다면 여러 파일에 나눠서) 담고 있다. 앞에서 설명했던 것처럼 어디엔가 main 루틴이 있어야 한다. 프로그램은 언제나 그곳에서 시작한다. 프로그램 3.2의 program 부분을 살펴보자.

//---- program 부분 ----

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

  Fraction *myFraction;

  // Fraction의 인스턴스를 생성한다.

  myFraction = [Fraction alloc];
  myFraction = [myFraction init];

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

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

  // print 메서드로 분수를 표시한다.

  NSLog (@"The value of myFraction is:");
  [myFraction print];

  [myFraction release];
  [pool drain];

  return 0;
}


main 내부에서 다음과 같이 myFraction 이라는 변수를 정의한다.

Fraction *myFraction;


이 줄은 myFraction 이 Fraction 형의 객체라는 의미다. 즉, myFraction 이 새 클래스인 Fraction 의 값을 저장하는 데 사용된다는 뜻이다. myFraction 앞에 별표(*)는 필수인데, 그 이유에는 일단 신경 쓰지 말자. 기술적으로는 myFraction 이 Fraction 의 참조(레퍼런스 혹은 포인터)라는 의미다.

이제, Fraction 을 저장할 객체가 생겼으니 자동차를 공장에서 새로 만드는 것처럼 실제로 만들 차례가 되었다. 이것은 다음 코드로 수행할 수 있다.

myFraction = [Fraction alloc];


alloc 은 allocate(할당하다)의 줄임말이다. 새로운 분수에 메모리 공간을 할당하는 것이다. 이 표현식은 새로 생성한 Fraction 클래스에 메시지를 보낸다.

[Fraction alloc];


Fraction 클래스에 alloc 메서드를 적용하도록 요청하는데, 직접 alloc 메서드를 구현한 적이 없다. 이 메서드는 어디서 온 것일까? 부모 클래스에서 이 메서드를 상속받은것이다. 8장 「상속」에서 이 주제에 대해 상세히 다룬다.

클래스에 alloc 메시지를 보내, 그 클래스의 새로운 인스턴스를 받는다. 프로그램 3.2에서 반환된 값은 변수 myFraction 에 저장된다.alloc 메서드는 객체의 모든 인스턴스 변수를 0으로 초기화한다. 그러나 이것으로 객체가 사용하기에 충분히 초기화되었다고 볼 수는 없다. 따라서 객체를 생성 (allocate, 메모리에 할당)한 후, 초기화(initialize)해 주어야 한다.

이것은 프로그램 3.2의 다음 명령문으로 수행할 수 있다.

myFraction = [myFraction init];


이번에도 직접 작성하지 않은 메서드를 사용하였다. init 메서드는 클래스의 인스턴스를 초기화한다. myFraction 변수에 init 메시지를 보내는 것에 주목하자. 이것은 클래스가 아니라 특정한 Fraction 객체를 초기화하는 것을 뜻한다. 초기화 메시지를 클래스의 인스턴스에게 보내는 것이다. 진도를 더 나가기 전에 이 점을 명확히 이해하자.

init 메서드는 초기화된 객체를 반환한다. 이 반환 값을 Fraction 형의 변수인 myFraction 에 저장한다. 새 인스턴스를 생성하고, 생성된 객체를 초기화하는 이 연속적인 명령은 Objective-C 에서 자주 사용되는데, 보통은 다음과 같이 결합하여 한줄로 쓴다.

myFraction = [[Fraction alloc] init];


안쪽의 메시지 표현이 먼저 실행된다.

[Fraction alloc]


앞서 말한 대로 이 메시지 표현의 결과 값은 생성된 실제 Fraction 이다. 이 생성된 객체를 변수에 저장하는 대신, 바로 init 메서드를 적용한다. 다시 정리하면, 새 Fraction 객체를 생성하고 그 후 초기화하는 것이다. 초기화한 결과가 myFraction 변수에 할당된다.

다음과 같이 줄여쓰기 기법을 사용하여 선언과 동시에 생성과 초기화를 할 수 도있다.

Fraction *myFraction = [[Fraction alloc] init];


이 책에서 이런 코딩 스타일을 자주 사용하므로 잘 이해하자. 지금까지 등장한 모든 프로그램에서 오토릴리스 풀을 생성하는 것을 보았을 것이다.

NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];


여기서 alloc 메시지는 NSAutoreleasePool 클래스에 보내져 새 인스턴스가 생성되도록 요청한다. 그 다음 init 메시지가 보내져 새로 생성된 객체를 초기화한다.

프로그램 3.2로 돌아가서 분수의 값을 설정하자 다음 코드에서 분수의 값을 설정한다.

// 분수를 1/3로 설정한다.
[myFraction setNumerator: 1];
[myFraction setDenominator: 3];


첫 명령문은 myFraction 에 setNumerator: 메시지를보낸다. 인수로 넘어가는 값은 1 이다. 그 후 컨트롤은 Fraction 클래스에 정의한 setNumerator: 로 넘어간다. Objective-C 시스템은 myFraction 이 Fraction 의 객체이기 때문에 이 클래스의 메서드인 setNumerator: 를 사용해야 한다는 것을 안다.

setNumerator: 메서드로 넘기 값 1은 변수 n에 저장된다. 이 메서드 안에 있는 단 한 줄이 이 값을 인스턴스 변수 numerator 에 저장한다. 따라서 myFraction 의 분자는 1이 된다.

그 다음으로 myFraction 의 setDenominator: 를 호출하는 메시지가 이어진다. 남겨진 인수 3은 setDenominator: 내 변수 d에 저장되고, 결국값 1/3이 myFraction 에 할당된다. 이제 분수의 값을 표시할 준비가 다 되었으므로 프로그램3.2 의 다음 코드로 넘어가자.

NSLog (@"The value of myFraction is:");
[myFraction print];


NSLog 를 호출하여 다음 텍스트를 표시한다.

The value of myFraction is:


다음 메시지는 print 메서드를호출한다.

[myFraction print];


print 메서드 내에서 인스턴스 변수 numerator, 슬래시 문자(/), 인스턴스 변수 denominator 가 표시된다.

프로그램 내의 다음 메시지는 Fraction 객체가 사용했던 메모리를 해제해 준다.

[myFraction release];


좋은 프로그래밍 스타일을 유지하려면 언제나 메모리를 해제하는 습관을 들여야 한다. 객체를 생성할 때마다 그 객체에 할당될 메모리를 요청하는데, 객체를 다 사용하고 나서 이 메모리를 해제해야 한다. 프로그램이 종료되면 할당된 메모리가 모두 해제되긴 하겠지만, 복잡한 프로그램을 개발하다 보면 객체를 수백 수천개 다루게 될 테고 메모리 소비량도 엄청나게 증가할 것이다. 그럴 때 사용하지 않는 메모리의 해제를 프로그램을 종료할 때까지 미룬다면, 응용 프로그램의 동작이 느려질 수 있다. 좋지 않은 프로그래밍 스타일이다. 그러므로 메모리는 해제가 가능해지는 순간에 즉시 해제하는 습관을 들이자.

애플 런타임 시스템은 메모리 관리를 자동드로 해주는 '가비지 컬렉션' 기법을 제공한다. 그러나 이런 자동화 기법에 의존하기보다 직접 메모리를 관리하는 방법을 배우는 편이 낫다.사실, 아이폰처럼 가비지 컬렉션이 지원되지 않는 특정 플랫폼에서는 이 기법을 사용할 수도 없다. 이 책에서는 후반부에 가서야 가비지 컬렉션에 대해 다룰 것이다.

아마 지금은 프로그램 3.2에서 프로그램 3.1과 동일한 기능을 수행하기 위해 더 많은 코드를 중복 작성하는 것처럼 보일 것이다. 이 단순한 예제에서 코드를 중복 작성하고 있는 것은 사실이다. 그러나 객체를 다루는 궁극적인 목적은 프로그램을 쉽게 작성,관리, 확장 하도록 만드는 것이다. 나중에 객체의 이런 장점을 이해하게 될 것이다.

이 장의 마지막 예제를 통해 프로그램에서 분수를 하나 이상 다루는 방법을 배울 것이다. 프로그램 3.3에서는 분수 하나는 2/3 로,다른 하나는 frac1 으로설정하고 두 분수를 모두 표시한다.


프로그램 3.3


// 분수를 다루는 프로그램 - 계속

#import <Foundation/Foundation.h>

//---- @interface 부분 ----

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

-(void) print;
-(void) setNumerator: (int) n;
-(void) setDenominator: (int) d;

@end

//---- @implementation 부분 ----

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

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

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

//---- program 부분 ----

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

  Fraction *frac1 = [[Fraction alloc] init];
  Fraction *frac2 = [[Fraction alloc] init];

  // 첫 번째 분수를 2/3로 설정한다.

  [frac1 setNumerator: 2];
  [frac1 setDenominator: 3];

  // 두 번째 분수를 3/7으로 설정한다.

  [frac2 setNumerator: 3];
  [frac2 setDenominator: 7];

  // 분수를 표시한다.

  NSLog (@"First fraction is:");
  [frac1 print];

  NSLog (@"Second fraction is:");
  [frac2 print];

  [frac1 release];
  [frac2 release];

  [pool drain];
  return 0;
}

프로그램 3.3 의 출력결과


First fraction is:
2/3
Second fraction is:
3/7

[frac1 setNumerator: 2];


@intetface 와 @implementation 부분은 프로그램 3.2 와 동일하다. 프로그램은 Fraction 객체를 두 개 생성하고각각 frac1 과 frac2 라고 이름 지은 다음, 첫 번째 분수에는 2/3 를, 두 번째에는 3/7 을 대입한다. setNumerator: 메서드가 frac1에 적용되어 분자의 값을 2로 설정하면, 인스턴스 변수 frac1은 자신의 인스턴스 변수 numerator 의 값을 2로 설정하게 된다. 마찬가지로 frac2 도 동일한 메서드를 사용하여 자신의 분자 값을 3으로 설정하면, 인스턴스 변수 numerator 의 값이 3으로 설정된다. 새 객체를 생성 할 때마다 그 객체는 자신의 인스턴스 변수를 갖는다. 그림 3.2 는 이것을 나타낸다.

어떤 객체가 메시지를 받는지에 따라 그에 맞는 정확한 인스턴스 변수가 참조된다. 따라서, 다음 코드에서는 setNumerator: 메서드 내부에서 numerator 라는 이름을 사용할 때마다 frac1 의 numerator 가 참조된다.

[frac1 setNumerator: 2];


이것은 frac1이 메시지의 수신자이기 때문이다.

그림 3.2 객체에 따라 구동되는 인스턴스 번수


인스턴스 변수 접근과 데이터 캡슐화

우리는 앞서 분수를 처리하는 메서드가 두 인스턴스 변수 numerator 와 denominator 에 이름으로 직접 접근하는 것을 보았다. 사실, 인스턴스 메서드는 언제나 자신의 인스턴스 변수에 직접 접근할 수 있다. 그러나 클래스 메서드는 클래스의 인스턴스가 아닌 클래스 자신만 다루기 때문에(이 점에 대해 잠시 생각해 보자) 이 변수들에 직접 접근하지 못한다. 만일 다른 곳, 예를 들어 main 루틴 안에서 인스턴스 변수에 접근하고싶다면 어떻게 해야 할까? 변수는 숨겨져 있기 때문에 직접 접근할 수 는없다. 변수가숨겨져 있다는개념은 '데이터 캡슐화'의 핵심요소다. 클래스 정의를 작성하는 사람은 데이터를 캡슐화해 프로그래머(클래스의 사용자)가 클래스 내부 정보를 수정할지 말지를 걱정하지 않고 클래스 정의를 확장하거나 수정하도록 해줄 수 있다. 데이터 캡슐화를 함으로써 프로그래머와 클래스 개발자 사이에는 적절한 분리 층이 생기는 것이다.

인스턴스 변수의 값을 가져오는 특별한 메서드를 작성한다면 깔끔한 방식으로 인스턴스 변수에 접근할 수 있다. 한 예로, numerator 와 denominator 는 메서드를 생성하여 메시지 수신자인 Fraction 의 각 인스턴스 변수에 접근한다.[1] 반환 값 은 해당하는 정수 값 이다. 새로운 두 메서드는 다음과 같이 선언한다.

-(int) numerator;
-(int) denominator;


메서드의 정의는 다음과 같다.

-(int) numerator;
-(int) denominator;

-(int) numerator
{
    return numerator;
}

-(int) denominator
{
    return denominator;
}


메서드 이름이 접근하는 인스턴스 변수의 이름과 동일하다는 점에 주목하자. 이렇게 해도 아무 문제 없으며 오히려 널리 사용되는 방식이다 [2] 프로그램 3.4는 이 두 메서드를 테스트한다.


프로그램 3.4


// 인스턴스 변수에 접근하는 프로그램

#import <Foundation/Foundation.h>
//---- @interface 부분 ----

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

-(void) print;
-(void) setNumerator: (int) n;
-(void) setDenominator: (int) d;
-(int) numerator;
-(int) denominator;

@end

//---- @implementation 부분 ----

@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;
}

@end

//---- program 부분 ----

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: %i/%i",
	 [myFraction numerator], [myFraction denominator]);
  [myFraction release];
  [pool drain];

  return 0;
}

프로그램 3.4 의 출력결과


The value of myFraction is 1/3


다음 NSLog 명령문은 myFraction 객체에 메시지를 두 개 보내 numerator 와 denominator 의 값을 받아 표시한다.

NSLog (@"The value of myFraction is: %i/%i" ,
      [myFraction numerator] , [myFraction denominator]);


덧붙여 말하자면, 인스턴스 변수의 값을 설정하는 메서드는 보통 '세터 (setter)' 라고하고, 값을 받아오는 메서드는 '게터 (getter) ' 라고 부른다. Fraction 클래스의 경우 setNumerator: 와 setDenominator: 는 세터 이며 numerator 와 denominator 는 게터이다.


objc2_notice_01
곧 Objective-C 2.0의 편리한 기능을 사용하여 자동으로 게터와 세터 메서드를 생성하는 법을 배우게 된다.


또한 alloc과 init을 결합한 new라는 메서드가 있는데, 이 메서드는 이 두 가지 일을 한 번에 수행한다. 따라서 새 Fraction 객체를 생성(메모리 할당)하고 초기화하는 일을 다음 한 줄로 처리할 수 있다.

Fraction *myFraction = [Fraction new];


일반적으로는 생성과 초기화, 이 두단계로나눠 접근하여 두가지 처리를 개념적으로 구별해 이해하는 편이 좋다. 즉, 먼저 새 객체를 생성한 후에 초기화 하는 것이다.

요약

이 장에서는 자신만의 클래스를 정의하고, 그 클래스의 객체 혹은 인스턴스를 생성하여, 메시지를 보내는 방법을 배웠다. Fraction 클래스는 앞으로 다시 다루게 된다. 이제 여러 인수를 메서드에 넘기고, 클래스 정의를 여러 파일로 나누고, 상속과 동적 바인딩 같은 핵심요소를 사용하는 방법을 배울것이다. 그러나 그에 앞서 Objective-C 데이터 형과 명령문을 작성하는 법부터 더 배워야한다. 먼저 다음 연습문제를 풀며 이 장에서 다룬요점을 제대로 이해했는지 테스트해 보자.


연습문제

1. 다음중 사용할 수 없는 이름은 무엇인가? 그 이유는 무엇인가?

Int playNextSong 6_05
_calloc Xx alphaBetaRoutine
clearScreen _1312 z
ReInitializ _ A$


2. 이 장의 자동차 예제를 기반으로 해서 매일 사용하는 객체를 하나 생각해 보라. 그 객체의 클래스를 정의하고 그 객체로 하는일을 다섯가지 적어보자.

3. 연습문제 2의 목록을 다음 문법으로다시 작성해 보자.

[instance method];


4. 자동차 외에도 보트와 오토바이를 소유하고 있다. 보트와 오토바이로 할 수 있는 일을나열해 보자. 이 가운데 겹쳐지는부분이 있는가?

5. 연습문제 4를 기초로 하여 Vehicle 클래스와 Car, Motorcycle, Boat 가운데 하나가 될 수 있는 myVehicle 이라는 객체가 있다고 가정한다. 다음을 작성했다고 해보자.

[myVehicle prep];
[myVehicle getGas];
[myVehicle service];

몇몇 클래스 중 하나일 가능성이 있는 어떤 객체에 동일한 액션을 적용하면 어떤장점이 있을까?


6. C 와 같은 절차적 언어에서는 액션을 생각한 후, 그 액션을 다양한 객체가 수행하도록 코드를 작성한다. 자동차 예제의 경우, C 언어에서는 교통수단을 청소하는 프로시저를 만들고,그 프로시저 안에서 차를 세차하고, 배를 청소하고, 오토바이를 닦는 등의 코드를 작성한다. 이 접근법을 사용해 나중에 새로운 교통수단을 추가하려 한다고 해보자(앞 연습문제를 참고하자) 이때 객체지향 접근법과 비교해서 절차적 접근법에는 어떤 장단점이 있는지 설명하라.

7. x와 y가 정수일 때, 데카르트 좌표 (x, y)를 담는 XYPoint라는 클래스를 정의하라. 한 지점의 x와 y 좌표를 각각 설정하는 메서드와 그 값을 받아오는 메서드를 정의하라. 당신의 새 클래스를 구현하고 테스트하는 Objective-C 프로그램을 작성해 보라.


Notes

  1. (옮긴이) 즉, 두개 메서드의 이름과 동일한 인스턴스 변수에 접근한다는 뜻이다.
  2. (옮긴이) 애플은 이런 메서드 이름을 사용할 것 을 권하고 있다.