The Objective-C 2.0:메세징
OSXDEV
이 장에서는 메세지를 보내는 문법을 설명한다. 중첩된 메세지 전송 표현 또한 다루게 된다. 그리고 멤버변수의 가시영역, OOP의 다형성이라는 개념과 동적 바인딩에 대해서 설명한다.
목차:
- 메세지 문법
- nil에 메세지 보내기
- 수신객체의 멤버변수
- 다형성
- 동적바인딩
- 점(.)문법
[편집] 메세지 문법
객체에게 무언가를 지시하기 위해서는 메소드를 실행하라는 메세지를 보내야 한다. 오브젝티브 C에서 메세지 표현은 []로 기술한다.
[수신객체 메세지]
수신객체는 어떤 객체를 의미하고 메세지는 무엇을 해야할 지를 의미한다. 소스코드에서 메세지라 함은 메소드명과 그 메소드에 전달하는 매개변수이다. 메세지를 보내면 런타임 시스템이 수신객체에서 해당하는 메소드를 선택해서 호출해 준다.
예를 들어서, 아래의 메세지는 myRect라는 객체가 display메소드를 실행하라는 의미이다. diaplay메소드는 자신을 표시하는 메소드이다.
[myRect display];
메소드는 매개변수를 받을 수도 있다. 매개변수가 하나인 경우, 선별자명(selector name)(역자주:메소드명)의 뒤에 콜론(:)을 붙이고 그 뒤에 매개변수를 기술한다. 각각의 요소를 키워드라고 한다. 키워드는 항상 콜론으로 끝나고 그 뒤에 매개변수가 따라 붙는다.
[myRect setWidth:20.0];
선별자명은 콜론을 포함한 모든 키워드를 포함하지만 그 이외 것(리턴할 데이터형, 매개변수의 데이터형)은 포함하지 않는다. 아래의 메세지 예는 myRect객체에게 위치좌표를(30.0, 50.0)로 설정하라고 알린다.
[myRect setOrigin:30.0 :50.0]; // 복수의 매개변수를 전달하는 좀 나쁜 표현
콜론도 메소드명(역자주:선별자명에서 수신객체를 제외한 부분)의 일부이기 때문에 이 메소드의 메소드명은 setOrigin::이다. 2개의 콜론이 있다는 것은 2개의 매개변수를 받는다는 의미이다. 이 예에서는 메소드명에 매개변수명을 지정하지 않았기 때문에 각 매개변수가 어떤 의미를 가지는 지가 알기 어렵다.
하지만, 메소드명에는 각 매개변수명을 지정해서 메소드가 필요로 하는 매개변수가 무엇인지도 표현하는 것이 바른표현이다. 예를 들어서 Rectangle클래스는 setOrigin::대신에 setOriginX:y:라고 하는 쪽이 더 알기 쉬울 것이다.
[myRect setOriginX: 30.0 y: 50.0]; // 복수의 매개변수를 지정하는 바른 표현
| 중요:메소드명의 요소들(매개변수들)은 생략하거나 순서를 바꿀 수 없다. 이로 인해서 매개변수가 종종 런타임시에 바뀔 수도 있고 어떤 것은 디폴트값을 가질 수도 있고 순서도 달라지거나 추가적으로 매개변수가 있을 수도 있다는 문제점을 피할 수 있기 때문이다. (역자주: 파이썬등 매개변수를 생략가능한 언어들에서 나타나는 혼란들)
사실상, 오브젝티브 C에서의 메소드는 매개변수 앞에 특수한 2개의 매개변수를 추가한 C함수에 지나지 않는다.(메세지의 동작 원리를 참고) |
잘 쓰지는 않지만 가변 매개변수를 취하는 매소드를 작성하는 것도 가능하다. 추가적인 매개변수들은 ,로 분리해서 매소드명의 뒤에 표기한다. (콜론과는 다르게 ,는 매소드명의 일부로 인식되지 않는다.) 아래는 makeGroup이라는 메소드에게 group이라는 필수 매개변수와 3개의 가변 매개변수를 전달하는 예이다.
[receiver makeGroup:group, memberOne, memberTwo, memberThree];
C함수처럼 메소드도 값을 리턴할 수 있다. 아래의 예에서는 isFilled라는 변수에 myRect가 색이 칠해져 있으면 YES를 아니면 NO를 저장한다.
BOOL isFilled; isFilled = [myRect isFilled];
변수명과 매소드명이 같은 이름을 가질 수 있다는 점에도 주목한다.
메세지 표현은 중첩하는 것도 가능하다. 어떤 사각형 객체의 색에 다른 사각형 객체의 색을 저장하는 예이다.
[myRect setPrimaryColor:[otherRect primaryColor]];
오브젝티브 C는 점(.)연산자도 제공한다. 이를 사용하면 간편하게 객체의 엑세스 메소드(역자주: 멤버변수의 값을 취득하거나 저장하는 메소드)를 호출할 수 있다. 이는 "프로퍼티 정의"기능과 점(.)문법의 조합으로 사용한다.
[편집] nil에 메세지 보내기
오브젝티브 C에서는 nil에 메세지를 보내는 것이 가능하다. 물론 아무런 동작도 일어나지 않지만 말이다. 이를 이용하는 몇 가지 패턴이 있다. nil에 보낸 메세지의 리턴값은 다음과 같다.
- 객체를 리턴하는 메소드라면, nil에 메세지를 보내면 0(nil)을 리턴할 것이다.
Person *motherInLaw = [[aPerson spouse] mother];
- 만약 aPerson의 spouse가 nil이라면, nill로 mother이라는 메세지를 보내지고, 메소드의 리턴값은 nil이 될 것이다. (spouse: 배우자)
- 포인터 데이터형, 정수의 스칼라형(sizeof(void*)크기 이하의 모든 데이터형(ex>정수형, 롱형...)), 부동 소수점형, 더블형, 롱 더블형, 롱롱형을 리턴하는 메소드인 경우, nil로 보낸 메세지의 리턴값은 0가된다.
- 구조체를 리턴하는 메소드의 경우, "Mac OS X ABI Function Call Guide"에 설명되어 있듯이, 그 값이 레지스터를 통해서 리턴되는 경우에는 구조체의 모든 필드를 0으로 채운 값을 리턴한다. 그 이외의 경우에는 0로 채우지 않을 것이다.
- 위에 언급되지 않은 값을 리턴하는 메소드라면, 어떤 값이 리턴될지 알 수 없다.
아래에 nil로 메세지를 보내는 적절한 예가 있다.
id anObjectMaybeNil = nil;
// 요효한 표현이다.
if ([anObjectMaybeNil methodThatReturnsADouble] == 0.0)
{
// 적절한 코드들...
}
노트:nill로 메세지를 보내는 것은 Mac OS X 10.5에서 부터 바뀐 점이 좀 있다. 10.4 이전에는, 메세지의 리턴형이 객체, 포인터, void, 정수의 스칼라형일 때에만 리턴값이 nil(0)였다.그 외의 형을 리턴하는 메세지를 nil에 보낸 경우 리턴값은 정의 되지 않는다(어떤 값이 될지 알 수 없다). 그러므로 메소드의 리턴형이 객체, 포인터, 정수의 스칼라형이 아닌 경우는 nil로 메세지를 보낸 리턴값에 의존하지 않도록 주의한다.
[편집] 수신객체의 맴버변수
메소드는 수신객체의 멤머변수에 엑세스할 수 있다. 메소드에 명시적으로 그 객체를 매개변수로 전달할 필요가 없음을 의미한다. 위의 예에서 처럼 primaryColor메소드의 매개변수가 없음에도 불구하고 otherRect객체의 색이라는 맴버변수에 엑세스가 가능함을 알 수있다. 모든 메소드는 메개변수로 지정하지 않더라도 수신객체와 그의 맴버변수들에 대해서 알고 있다.
이런 규칙이 있기 때문에 오브젝티브 C가 좀 더 심플해 질 수 있었다. OOP 프로그래머가 객체와 메세지라는 관념으로 생각하는 것에도 도움이 된다. 메세지는 마치 편지가 집으로 배달되는 것처럼 수신객체로 배달된다. 메세지의 매개변수는 외부의 정보를 수신객체로 배달하는 매개체 역할을 한다. 반대로 내부에서 내부로는 배달할 필요는 없다.
메소드가 가지는 엑세스권은 수신객체의 멤버변수에 국한되어 있다. 다른 객체에 저장되어 있는 변수의 정보를 원할 경우에는 그 변수를 가진 객체에 알려달라고 부탁하는 메세지를 보내야 한다. 위의 예에서는 primaryColor와 isFilled 메소드가 바로 이러한 목적에 사용되는 메소드이다.
멤버 변수에 엑세스하는 것에 대한 더 자세한 정보는 클래스 정의하기를 참고한다.
[편집] 다형성(polymorphism)
위의 예들에서 보았듯이 오브젝티브 C에서의 메세지라 함은 C에서의 함수호출과 같은 문법적 위치를 가진다. 단, 메소드가 객체에 속해 있다는 점 때문에 메세지는 함수호출과는 조금은 다르게 동작한다.
특히, 객체는 객체를 위해서 만들어진 메소드를 통해서만 조작된다는 제한이 있다. 다른 종류의 객체에 대해서 동작하는 메소드와 혼돈하는 일은 있을 수 었다. 이름이 같다고 하더라도 혼돈될 수는 없다. 즉, 같은 메세지라고 하더라도 두 종류의 객체는 서로 다른 반응을 일으킬 것이다. 예를 들자면 display라는 메세지를 받았을 때 각 객체는 고유의 방식대로 표시할 것이다. 원과 사각형 객체는 동일한 명령에 서로 다른 반응을 하게된다.
이러한 기능을 OOP에서 아주 중요한 역할을 하는 다형성이라고 한다. 다형성과 동적바인딩의 이점과 함께해서, 코딩할 때 객체의 클래스형이 무엇일지 미리 정하지 않더라도 수 많은 종류의 객체에 대응할 수 있는 코드를 작성할 수 있게 해준다. 대상이 되는 객체는 심지어 다른 프로젝트에 다른 프로그래머가 만든 것을 나중에 붙이는 것도 가능한 얘기가 된다. id형의 객체에 display라는 메세지를 보낸다고 하면, display라는 메소드를 가진 어떤 객체라도 수신객체가 될 수 있는 것이다.
[편집] 동적바인딩
함수호출과 메세지의 결정적인 차이점 다음과 같다. 함수와 함수의 매개변수는 컴파일할 때 연결되지만 메세지와 수신객체의 연결은 프로그램이 실행되고 실제로 그 메세지가 보내질 때까지 연결되지 않는다는 점이다. 그러므로 메세지에 대응하는 메소드의 호출은 런타임시에 밖에 알 수 없다(컴파일시에는 알 수 없다).
메세지가 호출할 정확한 메소드는 수신객체가 무엇이냐에 달려있다. 서로 다른 수신객체는 서로 다른 (그러나 같은 이름의)메소드를 가지고 있을 것이다(다형성). 컴파일러에서 메세지에 대응하는 정확한 메소드를 찾으려고 한다면, 반드시 어떠한 객체가 수신객체인지를 어떤 클래스에 속해 있는지를 알아야만 할 것이다. 하지만 이러한 정보는 수신객체가 메세지를 받았을 때에 비로소 알 수 있다(동적 데이터형). 소스코드 상으로는 알 수 없는 것이다.
메소드의 선정은 런타임시에 일어난다. 메세지가 보내졌을 때 런타임의 메세징 루틴은 메세지 속의 수신객체와 메소드명을 참조해서 수신객체에 구현되어 있는 메소드명과 일치한는 메소드를 호출하게 된다. 이 때 수신객체의 맴버변수들의 구조체에 대한 포인터도 같이 넘겨준다. 이 루틴에 대한 더 자세한 정보는 메세징의 원리를 참고한다.
메세지에서 메소드명은 메소드 구현을 "선별"하는 역할을 한다. 이러한 연유로 메세지에서 메소드명은 종종 선별자라고도 불리어 진다.
메소드의 동적바인딩은 다형성과 협동해서 OOP를 더욱 유연하고 강력하게 해준다. 각 객체는 자신만의 버젼의 메소드를 가지고 있기 때문에, 프로그램은 다양한 결과를 얻어낼 수 있다. 메세지 자체를 다양하게 해서가 아니고 단지 메세지를 받을 객체를 다양하게 함으로써 얻어지는 것이다. 이러한 다양한 결과는 프로그램이 실행되면서 얻어진다. 수신객체는 그자리에서 결정되고 유저의 조작등 외부로부터의 요인에 의해서 다양한 결과를 만들어 낼 수가 있는 것이다.
어플리케이션 킷(Application Kit)을 기반으로 작성된 코드를 실행할 때, 예를 들면, 유저가 메뉴의 Cut, Copy, Paste같은 메뉴 명령을 보낼 수신객체를 결정한다. 이 메세지는 현제 선택되어 있는 객체로 보내어 진다. 텍스트를 표시하고 있는 객체는 Copy메세지에 대해서 스켄한 이미지를 표시하고 있는 객체와는 다른 결과를 만들어 낼 것이다. 도형들의 모임은 사각형하고는 다른 결과를 만들어 낼 것이다. 런타임시까지 메세지는 메소드를 선별하지 않기 때문에 이러한 결과의 차이는 각 수신객체의 메세지에 대응하는 메소드 안에서 만들어 지는 것이다. 메세지를 보내는 코드에서 보면 메소드 안에서 무엇을 하는지 알 필요도 없고 어떤 종류들이 있는지도 알 필요가 없다. 각 어플리케이션은 자신만의 객체를 만들고 copy메세지에 어떻게 반응할 지도 마음대로 만들 수 있다.
오브젝티브 C의 동적바인딩은 한 단계 더 나아가 보내지는 메세지도 실행중에 결정되는 변수로 하는 것도 가능하다. 여기에 대한 더 자세한 정보는 메세징의 원리를 참고한다.
[편집] 점(.)문법
오브젝티브 C는 엑세스 메소드를 호출하기 위해서 []의 대신에 약식으로 사용할 수 있는 점(.)연산자를 제공한다. 객체의 프로퍼티(멤버변수의 일종)값을 취득하거나 프로퍼티에 값을 저장할 때 특히 편리한다.
[편집] 점문법 사용하기
[편집] 개요
점문법를 사용해면 구조체의 요소에 엑세스하는 패턴과 동일하게 엑세스 메소드를 호출할 수 있다.
myInstance.value = 10;
printf("myInstance value: %d", myInstance.value);
점문법은 단지 "문법적 꿀"이다. 컴파일러가 점문법을 엑세스 메소드로 변환해 준다. 즉, 맴버변수를 직접 엑세스하고 있지는 않다는 뜻이다. 위의 예제코드는 아래의 예제와 완전히 동일하다.
[myInstance setValue:10];
printf("myInstance value: %d", [myInstance value]);
[편집] 일반적인 사용법
프로퍼티를 읽거나 쓰기위해서 점(.)연산자를 사용한다. 아래에 예가 나와 있다.
Graphic *graphic = [[Graphic alloc] init];
NSColor *color = graphic.color;
CGFloat xLoc = graphic.xLoc;
BOOL hidden = graphic.hidden;
int textCharacterLength = graphic.text.length;
if (graphic.textHidden != YES) {
graphic.text = @"Hello";
}
graphic.bounds = NSMakeRect(10.0, 10.0, 20.0, 120.0);
(@"Hello"는 NSString형의 상수이다. 제세한 내용은 “컴파일러 지시자”를 참고한다.)
property라는 프로퍼티에 엑세스할 때 그 프로퍼티에 대응되는 취득 메소드를 호출한다(디폴트: property(메소드명)). 저장할 때는 저장 메소드를 호출한다(디폴트:setProperty). 프로퍼티 선언 기능을 사용해서 호출되는 메소드를 변경하는 것도 가능하다(자세한 내용은 "프로퍼티 선언"참고). 소스코드에서 보여지는 것과는 다르게 점문법을 사용하더라도 캡슐화를 계속 유지한다. 즉 맴버변수를 직접 엑세스하는 것이 아닌 것이다.
아래의 []를 사용한 예제코드는 예제의 컴파일 결과와 완전히 동일한 결과를 나타낸다.
Graphic *graphic = [[Graphic alloc] init];
NSColor *color = [graphic color];
CGFloat xLoc = [graphic xLoc];
BOOL hidden = [graphic hidden];
int textCharacterLength = [[graphic text] length];
if ([graphic isTextHidden] != YES) {
[graphic setText:@"Hello"];
}
[graphic setBounds:NSMakeRect(10.0, 10.0, 20.0, 120.0)];
점문법을 사용해서 얻는 이점은 읽기전용의 프로퍼티에 쓰기를 하려고 할 때 컴파일러가 에러를 발생시킨다는 점이다. 점문법을 사용하지 않았다면 존재하지 않는 setProperty: 메소드 호출이라는 런타임때 경고 메세지가 발생할 것이다.
C언어의 데이터형인 프로퍼티에 대해서는 복합연산자의 사용도 가능하다. 예를 들어서 NSMutableDate의 인스턴스의 length프로퍼티를 복합연산자를 사용해서 값을 바꾸는 것이 가능하다.
NSMutableData *data = [NSMutableData dataWithLength:1024]; data.length += 1024; data.length *= 2; data.length /= 4;
이 예제코드는 아래와 동일한 코드이다.
[data setLength:[data length] + 1024]; [data setLength:[data length] * 2]; [data setLength:[data length] / 4];
프로퍼티가 사용될 수 없는 한 가지 경우가 있다. 아래의 코드를 보자.
id y; x = y.z; // z는 정의되지 않은 프로퍼티이다
y는 데이터형이 동적이고 z라는 프로퍼티는 정의되어 있지 않은 상태가 된다. 이 라인이 컴파일될 상황이 몇 가지 있다. 먼저 z에 대해서 알 수 없는 상황이기 때문에, 이 문장은 정의되지 않은 프로퍼티라는 에러를 발생시킨다. 만약에 z가 정의되어 있다면, z가 현재 컴파일 유닛의 단 한 곳에서만 정의되어 있다면 이 문장은 문제가 없다. 만약 여러 곳에 z라는 프로퍼티가 존재한다면 z의 데이터형이 모두 같은 경우, 문제없이 컴파일 된다. 만약 하나라도 데이터형이 다르면 에러를 일으킬 것이다. 또한 하나라도 읽기전용일 경우에도 에러를 일으킬 것이다.
[편집] nil값
중첩된 프로퍼티 엑세스에서 그 중간에 있는 프로퍼티가 nil일 경우에도 엑세스 메세지를 호출하는 것에는 변화가 없다. 예를 들어서 아래의 쌍들은 모두 동일한 코드이다.
// 각 프로퍼티들은 모두 객체이다. x = person.address.street.name; x = [[[person address] street] name]; // 점문법의 연결사이에 C구조체가 있을 경우 // window객체 또는 contentView가 nil일 경우 에러가 발생할 것이다. y = window.contentView.bounds.origin.y; y = [[window contentView] bounds].origin.y; // 저장 엑세스 메소드 예제 person.address.street.name = @"Oxford Road"; [[[person address] street] setName: @"Oxford Road"];
[편집] self
자기 자신의 프로퍼티를 엑세스 메소드를 사용해서 엑세스하고 싶은 경우, 아래와 같이 명시적으로 self라고 지정해야 한다.
self.age = 10;
만약 self.을 생략하면, 멤버변수에 직접 엑세스하는 결과를 낳게 될 것이다. 아래의 예에서는 age프로퍼티에 대한 저장 메소드가 호출되고 있지 않다.
age = 10;
[편집] 퍼포먼스와 쓰레드
점문법을 사용하더라도 표준적인 메소드 호출로 변환될 뿐이다. 결과적으로 점문법을 사용한 코드와 엑세스 메소드를 사용한 코드는 동일한 동작을 한다. 단지 메소드를 호출할 뿐이기 때문에 점문법을 사용하더라도 쓰레드 의존성을 발생 시키지 않는다.
[편집] 점문법과 키-값 코딩
키-값 코딩(KVC:Key-Value Coding)기능은 범용 프로퍼티 엑세스 메소드를 제공한다. valueForKey: 메소드와 setValue:forKey:가 그것이다. 이 기능은 프로퍼티명을 문자열로 매개변수를 통해서 지정 가능하게 한다. KVC가 있는 이유는 엑세스 메소드를 대체하기 위해서가 아니라 어쩔 수 없이 사용해야 할 경우를 위해서이다. 컴파일시에는 프로퍼티명을 알 수 없는 경우가 그 예이다.
키-값 코딩과 점문법을 서로 별도로 존재하지만 교차하기도 한다. 점문법 없이 KVC를 사용할 수도 있고 KVC없이 점문법만도 사용할 수도 있다. KVC의 경우 점문법은 키의 연결을 구분하기 위해서 사용된다. 점문법을 사용하면 엑세스 메소드를 호출로 변환됨을 염두에 두자(좀 더 강조하자면, 점문범을 사용하더라도 KVC메소드인 valueFor:나 setValue:forKey:를 호출하지는 않는다).
KVC메소드를 사용하면 프로퍼티에 엑세스할 수 있다. 아래와 같은 클래스가 있다고 하자.
@interface MyClass @property NSString *stringProperty; @property NSInteger integerProperty; @property MyClass *linkedInstance; @end
인스턴스의 프로퍼티에 엑세스하기 위해서 KVC를 아래와 같이 사용한다.
MyClass *myInstance = [[MyClass alloc] init]; NSString *string = [myInstance valueForKey:@"stringProperty"]; [myInstance setValue:[NSNumber numberWithInt:2] forKey:@"integerProperty"];
프로퍼티의 점문법과 KVC의 키연결을 비교하기 위해서 아래의 예제를 보자.
MyClass *anotherInstance = [[MyClass alloc] init]; myInstance.linkedInstance = anotherInstance; myInstance.linkedInstance.integerProperty = 2;
위의 예제는 아래와 동일한 결과를 가진다.
MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
[myInstance setValue:[NSNumber numberWithInt:2]
forKeyPath:@"linkedInstance.integerProperty"];
[편집] 사용법 요약
aVariable = anObject.aProperty;
aProperty라는 메소드를 호출하고 리턴값을 aVariable에 대입한다. aProperty라는 프로퍼티의 데이터형과 aVariable의 데이터형은 반드시 호환해야 한다. 그렇지 않을 경우, 컴파일시에 경고 메세지를 보게 될 것이다.
anObject.name = @"New Name";
anObject의 setName: 메소드를 @"New Name"을 매개변수로 호출한다.
만약 setName:메소드가 존재하지 않거나 name이라는 프로퍼티가 존재하지 않거나 setNmae: 메소드가 무엇인가 리턴하는 메소드인 경우 컴파일시에 경고가 발생할 것이다.
xOrigin = aView.bounds.origin.x;
bounds메소드를 호출하고, 그 리턴값(NSRect구조체형)의 요소 origin.x값을 xOrigin에 대입한다. (origin또한 구조체)
NSInteger i = 10; anObject.integerProperty = anotherObject.floatProperty = ++i;
11을 anObject.integerProperty와 anotherObject.floatProperty에 대입한다. 즉 대입문의 우변이 먼저 평가되고 그 결과가 setIntegerProperty:와 setFloatProperty:로 전달된다.
[편집] 잘못된 사용
다음과 같은 사용은 하지 않도록 한다.
anObject.retain;
컴파일시 경고를 발생시킨다.(경고: 프로퍼티의 리턴값이 사용되지 않고 있습니다)
/* 메소드 선언 */ - (BOOL) setFooIfYouCan: (MyClass *)newFoo; /* 사용하는 코드 */ anObject.fooIfYouCan = myInstance;
컴파일시 setFooIfYouCan: 매소드가 값을 리턴하고 있으므로(void가 아니므로) 경고를 발생시킨다.
flag = aView.lockFocusIfCanDraw;
lockFocusIfCanDraw메소드를 호출하고 리턴값을 flag에 대입한다. flag의 데이터형이 메소드의 리턴형과 틀리지 않는 이상 컴파일러 경고는 발생하지 않을 것이다.
/* 프로퍼티 정의 */ @property(readonly) NSInteger readonlyProperty; /* 메소드 정의 */ - (void) setReadonlyProperty: (NSInteger)newValue; /* 사용하는 코드 */ self.readonlyProperty = 5;
프로퍼티가 읽기전용(readonly)로 정의되어 있기 때문에 컴파일시 경고를 발생시킨다. 동작은 하겠지만, 저장 엑세스 메소드를 추가했다고 해서 일고쓰기가 가능해(readwrite)졌음을 의미하지는 않는다.
| 번역자 | 사용자:airless |
| 원본문서링크 | http://developer.apple.com/documentation/Cocoa/Conceptual/ObjectiveC/Articles/chapter_2_section_3.html (Last Updated - 2008-10-15) |




