Devin.KR

메시지 보내기 - 대괄호 문법

개발자KR 조회 0

이 장에서 배우는 것

Objective-C 에서 객체에게 일을 시키는 방법은 하나다. 객체에 메시지(message)를 보내는 것이다. 이 장에서는 그 표기법인 대괄호 문법을 처음부터 끝까지 익힌다. 앞 장에서 C 타입과 객체 타입을 구분했으므로, 이제 객체 타입 변수가 가리키는 대상에게 무엇을 어떻게 요청하는지 다룬다. 예제는 사내 도서 대출 관리 도구의 일부, 곧 도서 번호를 만들고 대출 목록을 쌓고 회원을 조회하는 작업이다.

  • [수신자 메시지] 형태를 읽고 쓸 수 있다.
  • 콜론이 붙은 메시지 이름에서 인자의 개수와 순서를 알아낼 수 있다.
  • 메시지를 중첩해 쓰고, 안쪽부터 평가되는 순서를 설명할 수 있다.
  • 클래스 메서드와 인스턴스 메서드를 표기와 수신자로 구분할 수 있다.
  • nil 에게 보낸 메시지의 결과를 예측하고, @selector 와 respondsToSelector: 로 객체가 메시지를 받을 수 있는지 확인할 수 있다.

문제 상황

대출 관리 도구에 회원 조회 기능을 붙인다고 하자. 회원 번호로 이름을 찾고, 찾은 이름을 대문자로 바꾸거나 글자 수를 세어 화면에 보여 주는 단순한 작업이다. C 에서는 이름을 담은 포인터가 NULL 인지 먼저 확인하지 않으면 함수 안에서 프로그램이 곧바로 비정상 종료한다. 그래서 C 프로그래머는 호출 앞에 검사 코드를 습관처럼 넣는다.

Objective-C 는 호출 방식이 다르다. 함수를 부르는 것이 아니라 객체에 메시지를 보내고, 받는 쪽이 nil 이어도 프로그램이 멈추지 않는다. 이 성질은 편리하지만, 조회에 실패한 사실이 조용히 묻힐 수도 있다. 또 같은 대괄호 안에서 콜론이 어디에 붙는지, 클래스에 보내는 메시지와 객체에 보내는 메시지가 어떻게 다른지 모르면 코드를 읽는 것부터 막힌다. 이 장은 이런 지점을 하나씩 정리한다.

대괄호 문법의 기본 형태

수신자와 메시지

메시지 전송의 기본 모양은 [수신자 메시지] 다. 수신자(receiver)는 메시지를 받는 객체이고, 메시지는 그 객체에게 요청하는 동작의 이름이다. 아래 코드는 문자열 객체에 글자 수를 묻는다.

NSString *title = @"Objective-C 입문";
NSUInteger length = [title length];

title 이 수신자이고 length 가 메시지다. 이 메시지에 해당하는 코드는 수신자가 속한 클래스 안에 있으며, 어느 코드를 실행할지는 프로그램이 돌아가는 중에 정해진다. C 의 함수 호출처럼 컴파일할 때 호출 대상이 하나로 고정되지 않는다. 수신자가 메시지를 처리하는 방법을 모르면 실행 중에 오류가 난다. 이 점은 뒤에서 respondsToSelector: 와 함께 다시 본다.

이 책의 예제에서는 title.length 같은 점(dot) 표기를 쓰지 않는다. 점 표기는 프로퍼티라는 별도 기능과 묶여 있으므로 그 주제를 다루는 곳에서 설명한다. 이 장의 모든 호출은 대괄호로 쓴다.

인자가 있는 메시지와 콜론

메시지에 값을 함께 넘기려면 이름 뒤에 콜론(:)을 붙이고 그 뒤에 값을 쓴다. 콜론은 이름의 일부이며 인자가 하나 있다는 표시다.

BOOL ok = [title hasPrefix:@"Objective"];

인자가 둘 이상이면 이름이 여러 조각으로 나뉘고, 조각마다 콜론과 인자가 하나씩 붙는다. C 처럼 쉼표로 인자를 나열하지 않는다.

[loans insertObject:@"BK-000" atIndex:0];

이 메시지의 이름은 insertObject:atIndex: 전체다. insertObject: 와 atIndex: 가 따로 있는 두 메시지가 아니다. 인자가 이름 사이사이에 끼어 있어서 호출문을 영어 문장처럼 읽을 수 있고, 값이 무슨 역할인지 호출하는 쪽에서 바로 보인다. 아래 그림은 이 호출을 부분으로 나눈 것이다.

메시지 이름은 콜론으로 끝나는 조각들을 이어 붙인 하나의 이름이고 인자는 그 사이에 들어간다
메시지 표기와 이름, 인자 수의 대응
호출문메시지 이름인자 수콜론 개수
[title length]length00
[title hasPrefix:@"Obj"]hasPrefix:11
[loans insertObject:x atIndex:0]insertObject:atIndex:22
[title stringByReplacingOccurrencesOfString:a withString:b]stringByReplacingOccurrencesOfString:withString:22

표에서 보듯 이름에 든 콜론의 개수가 곧 인자의 개수다. 코드를 읽다가 낯선 메시지를 만나면 콜론부터 센다.

중첩 메시지

메시지의 반환값은 객체일 수 있으므로, 그 결과에 곧바로 다음 메시지를 보낼 수 있다. 대괄호 안에 대괄호가 들어가는 이 형태를 중첩(nesting)이라 한다.

NSString *label = [[title uppercaseString] stringByAppendingString:@" (대출 가능)"];

평가는 안쪽 대괄호부터 한다. 먼저 [title uppercaseString] 이 대문자로 바꾼 새 문자열을 돌려주고, 그 문자열이 바깥 메시지 stringByAppendingString: 의 수신자가 된다. 인자 자리에도 메시지를 넣을 수 있다. 읽을 때는 가장 안쪽 대괄호를 찾아 거기서 바깥쪽으로 나오면 된다. 세 단계 넘게 겹치면 읽기 어려우므로 임시 변수에 나누어 담는 편이 낫다.

클래스 메서드와 인스턴스 메서드

메시지를 받을 수 있는 대상은 객체뿐이 아니다. 클래스 자체도 메시지를 받는다. 클래스 이름을 수신자 자리에 쓰면 클래스에게 보내는 메시지가 된다.

NSMutableArray *loans = [NSMutableArray array];
NSString *code = [NSString stringWithFormat:@"BK-%03d", 7];

클래스에 보내는 메시지에 대응하는 메서드를 클래스 메서드(class method)라 하고, 객체(인스턴스)에 보내는 메시지에 대응하는 메서드를 인스턴스 메서드(instance method)라 한다. 문서나 헤더에서는 선언 맨 앞의 기호로 구분한다. 클래스 메서드는 +, 인스턴스 메서드는 - 로 시작한다. 예를 들어 + array 는 NSMutableArray 클래스에 보내는 메시지이고, - count 는 만들어진 배열 객체에 보내는 메시지다.

클래스 메서드와 인스턴스 메서드의 비교
구분선언 기호수신자이 장의 예
클래스 메서드+클래스 이름[NSString stringWithFormat:...]
인스턴스 메서드-객체를 가리키는 변수나 식[loans addObject:...]

클래스 메서드는 주로 객체를 만들어 돌려주거나 객체 없이 할 수 있는 일에 쓰인다. 객체를 만드는 정식 절차인 alloc 과 init 은 메모리를 다룬 곳에서 따로 설명하므로, 여기서는 array 처럼 완성된 객체를 돌려주는 클래스 메서드만 쓴다. 두 종류를 혼동하면 [NSMutableArray addObject:@"x"] 처럼 클래스에 인스턴스 메서드를 보내는 오류가 나는데, 대개 컴파일 단계에서 걸린다.

nil 과 셀렉터

nil 에게 보낸 메시지

nil 은 어떤 객체도 가리키지 않는 객체 포인터 값이다. C 의 NULL 과 비슷하지만, nil 에게 메시지를 보내도 프로그램이 종료되지 않는다. 메시지는 아무 일도 하지 않고 지나가며, 값을 돌려받는 자리에는 0에 해당하는 값이 들어온다. 반환형이 객체면 nil, 숫자형이나 BOOL 이면 0 이다(NO 도 0 이다). 구조체를 돌려주는 메서드는 이 규칙에 기대지 않는 것이 좋다.

수신자가 nil 이면 메서드는 실행되지 않고 결과는 nil, 0, NO 로 돌아온다

회원 조회에 실패한 경우가 전형적이다. 딕셔너리에서 없는 키를 찾으면 nil 이 나오고, 그 위에 length 를 보내면 0 이 나온다. 널 검사 없이도 안전하다는 점이 장점이다. 반대로 실패가 오류로 드러나지 않으므로, 조회 결과가 정말 있어야 하는 곳에서는 if (name == nil) 로 직접 확인해야 한다.

한 가지 경계가 있다. 메시지를 받는 쪽이 nil 인 것은 괜찮지만, nil 을 인자로 넘기는 것은 메서드마다 다르다. 배열의 addObject: 는 nil 인자를 받으면 예외를 일으켜 프로그램이 종료된다. 이 부분은 뒤의 틀리기 쉬운 사례에서 다시 다룬다.

셀렉터와 respondsToSelector:

메시지 이름은 실행 중에 문자열이 아닌 별도 타입 SEL 로도 다룰 수 있다. 이름을 @selector( ) 안에 쓰면 그 이름의 셀렉터(selector) 값이 만들어진다. 괄호 안에는 콜론까지 그대로 적는다.

SEL s = @selector(insertObject:atIndex:);
NSString *name = NSStringFromSelector(s);

셀렉터는 호출이 아니라 이름이다. 이 값만으로 메서드가 실행되지는 않는다. 셀렉터를 쓰는 가장 흔한 곳이 respondsToSelector: 다. 객체에게 이 이름의 메시지를 처리할 수 있는지 묻고 BOOL 로 답을 받는다.

if ([loans respondsToSelector:@selector(addObject:)]) {
    [loans addObject:@"BK-003"];
}

수신자가 처리하지 못하는 메시지를 실행 중에 받으면 "unrecognized selector" 라는 예외가 나고 프로그램이 종료된다. 호출하기 전에 확인하면 이를 피할 수 있다. 수신자가 nil 이면 respondsToSelector: 자체도 nil 규칙에 따라 NO 를 돌려준다. 또 sortedArrayUsingSelector: 처럼 셀렉터를 인자로 받는 메서드도 있다. 이 장의 예제에서는 키를 정렬하는 데 @selector(compare:) 를 쓴다.

셀렉터로 메시지를 직접 보내는 performSelector: 도 있지만, 셀렉터가 변수이면 ARC 컴파일러가 반환형을 알 수 없어 경고를 낸다. 이 장에서는 쓰지 않는다. 메시지 이름을 확인하는 용도만 익히면 충분하다.

완성 코드

아래 프로그램은 앞에서 본 내용을 한 파일에 모았다. 클래스를 직접 정의하지 않고 Foundation 이 제공하는 NSString, NSMutableArray, NSDictionary 만 쓴다. 결과는 printf 로 표준 출력에 내보낸다. NSLog 는 앞에 시각과 프로세스 정보를 붙이고 표준 오류로 출력하므로, 결과를 파일로 저장하거나 다른 명령에 넘길 때 섞여 들어가 비교가 어려워진다. 그래서 이 책에서는 결과 출력에 쓰지 않는다. 컴파일은 clang -fobjc-arc -Wall -framework Foundation main.m -o app 로 한다.

main.m

#import <Foundation/Foundation.h>

static void show(NSString *label, NSString *value) {
    printf("%s: %s\n", [label UTF8String], value ? [value UTF8String] : "(nil)");
}

static NSString *yesNo(BOOL flag) {
    return flag ? @"YES" : @"NO";
}

int main(void) {
    @autoreleasepool {
        NSString *title = @"Objective-C 입문";

        // 인자 없는 메시지와 인자 있는 메시지
        NSUInteger length = [title length];
        show(@"길이", [NSString stringWithFormat:@"%lu", (unsigned long)length]);
        show(@"접두사 일치", yesNo([title hasPrefix:@"Objective"]));
        show(@"교체", [title stringByReplacingOccurrencesOfString:@"입문" withString:@"기초"]);

        // 중첩 메시지
        show(@"중첩", [[title uppercaseString] stringByAppendingString:@" (대출 가능)"]);

        // 클래스 메서드와 인스턴스 메서드
        show(@"도서 번호", [NSString stringWithFormat:@"BK-%03d", 7]);
        NSMutableArray *loans = [NSMutableArray array];
        [loans addObject:@"BK-001"];
        [loans addObject:@"BK-002"];
        [loans insertObject:@"BK-000" atIndex:0];
        show(@"대출 목록", [loans componentsJoinedByString:@", "]);
        show(@"대출 건수", [NSString stringWithFormat:@"%lu", (unsigned long)[loans count]]);

        // 딕셔너리는 키를 정렬해 출력
        NSDictionary *members = @{@"m1": @"김하늘", @"m2": @"박도윤"};
        NSArray *keys = [[members allKeys] sortedArrayUsingSelector:@selector(compare:)];
        for (NSString *key in keys) {
            show(key, [members objectForKey:key]);
        }

        // nil 에게 보낸 메시지
        NSString *name = [members objectForKey:@"m9"];
        show(@"m9 회원", name);
        show(@"nil 길이", [NSString stringWithFormat:@"%lu", (unsigned long)[name length]]);
        show(@"nil 대문자", [name uppercaseString]);
        show(@"nil 접두사", yesNo([name hasPrefix:@"김"]));

        // 셀렉터와 respondsToSelector:
        show(@"셀렉터 이름", NSStringFromSelector(@selector(insertObject:atIndex:)));
        SEL checks[3] = {@selector(count), @selector(addObject:), @selector(uppercaseString)};
        for (int i = 0; i < 3; i++) {
            printf("%s -> %s\n",
                   [NSStringFromSelector(checks[i]) UTF8String],
                   [yesNo([loans respondsToSelector:checks[i]]) UTF8String]);
        }
        printf("nil의 length -> %s\n", [yesNo([name respondsToSelector:@selector(length)]) UTF8String]);
    }
    return 0;
}

줄별 해설

show 함수. 레이블과 값을 레이블: 값 형태로 한 줄 출력하는 C 함수다. UTF8String 은 문자열 객체에서 C 문자열을 꺼내는 메시지다. 값이 nil 이면 UTF8String 이 널 포인터를 돌려주는데, 이를 %s 에 넘기면 안 되므로 삼항 연산자로 "(nil)" 을 대신 출력한다. 메시지는 nil 에게 안전하지만 C 함수 printf 는 그렇지 않다는 점을 보여 주는 부분이다.

yesNo 함수. BOOL 값을 YES, NO 문자열 객체로 바꾼다. 아래 출력 줄들이 모두 show 를 거치게 하려는 보조 함수다.

길이, 접두사, 교체. [title length] 는 인자 없는 메시지, hasPrefix: 는 인자 하나, stringByReplacingOccurrencesOfString:withString: 은 인자 둘이다. 글자 수 14 는 영문 11자(Objective-C), 공백 1자, 한글 2자의 합이다. %lu 에는 NSUInteger 를 unsigned long 으로 바꿔 넘겨 경고를 피한다.

중첩. 안쪽 uppercaseString 이 OBJECTIVE-C 입문 을 만들고, 바깥 stringByAppendingString: 이 뒤에 문구를 붙인다. 한글은 대소문자가 없어 그대로다.

클래스 메서드. [NSString stringWithFormat:...] 과 [NSMutableArray array] 는 클래스 이름을 수신자로 쓴다. %03d 는 정수 7 을 세 자리 0 채움으로 찍어 BK-007 을 만든다. 이어지는 addObject:, insertObject:atIndex:, componentsJoinedByString:, count 는 만들어진 배열 객체에 보내는 인스턴스 메서드다. 인덱스 0 에 끼워 넣은 BK-000 이 목록 맨 앞에 온다.

딕셔너리. @{...} 는 딕셔너리 리터럴이다. 딕셔너리는 순서가 정해져 있지 않으므로 allKeys 로 키를 꺼내 sortedArrayUsingSelector: 에 @selector(compare:) 를 넘겨 정렬한 뒤 출력한다. 이렇게 해야 실행할 때마다 출력이 같다. 딕셔너리 자체는 뒤에서 별도로 다룬다.

nil 부분. 키 m9 는 없으므로 objectForKey: 가 nil 을 돌려준다. 그 nil 에게 length 를 보내면 0, uppercaseString 을 보내면 nil, hasPrefix: 를 보내면 NO 가 나온다. 프로그램은 멈추지 않는다.

셀렉터 부분. SEL 은 셀렉터를 담는 타입이고, 배열에 셀렉터 세 개를 담았다. 반복문에서 respondsToSelector: 로 대출 목록 배열에게 묻는다. 배열은 count 와 addObject: 에 응답하고 문자열용 uppercaseString 에는 응답하지 않는다. 마지막 줄은 nil 에게 물은 결과가 NO 임을 보여 준다.

실행 결과

$ clang -fobjc-arc -Wall -framework Foundation main.m -o app
$ ./app
길이: 14
접두사 일치: YES
교체: Objective-C 기초
중첩: OBJECTIVE-C 입문 (대출 가능)
도서 번호: BK-007
대출 목록: BK-000, BK-001, BK-002
대출 건수: 3
m1: 김하늘
m2: 박도윤
m9 회원: (nil)
nil 길이: 0
nil 대문자: (nil)
nil 접두사: NO
셀렉터 이름: insertObject:atIndex:
count -> YES
addObject: -> YES
uppercaseString -> NO
nil의 length -> NO

실무에서 자주 틀리는 것

1. 인자를 쉼표로 나열한다

C 함수 호출에 익숙하면 인자를 쉼표로 이어 쓰기 쉽다. 메시지에서는 쉼표가 인자 사이를 잇지 못한다.

// 틀린 코드
[loans insertObject:@"BK-000", 0];

// 고친 코드
[loans insertObject:@"BK-000" atIndex:0];

메시지 이름은 insertObject:atIndex: 로 정해져 있으므로, 이름 조각을 모두 쓰고 조각마다 콜론 뒤에 인자를 붙인다. 이름에 든 콜론 개수를 세어 인자 개수와 맞추는 습관이 도움이 된다.

2. nil 결과를 C 함수에 바로 넘긴다

메시지는 nil 에게 안전하지만, 그 결과를 받은 C 함수까지 안전한 것은 아니다.

// 틀린 코드
NSString *name = [members objectForKey:@"m9"];
printf("%s\n", [name UTF8String]);   // name 이 nil 이면 널 포인터가 %s 로 넘어간다

// 고친 코드
NSString *name = [members objectForKey:@"m9"];
if (name == nil) {
    printf("회원 없음\n");
} else {
    printf("%s\n", [name UTF8String]);
}

메시지 체인 중간에서 nil 이 생겨도 끝까지 조용히 지나가므로, 마지막에 C 함수나 다른 API 로 넘기는 곳에서 문제가 터진다. 조회 결과가 필수인 지점에서는 nil 을 직접 검사한다.

3. nil 을 컬렉션에 넣는다

수신자가 nil 인 것과 인자가 nil 인 것은 다르다.

// 틀린 코드
NSString *name = [members objectForKey:@"m9"];
[loans addObject:name];   // name 이 nil 이면 예외로 종료된다

// 고친 코드
NSString *name = [members objectForKey:@"m9"];
if (name != nil) {
    [loans addObject:name];
}

배열과 딕셔너리는 nil 을 원소로 가질 수 없다. 컬렉션에 넣기 전에 값이 있는지 확인한다. 값이 없음을 표현해야 한다면 컬렉션 전용 널 객체를 쓰는데, 이는 컬렉션을 다루는 곳에서 설명한다.

4. @selector 에서 콜론을 빼먹는다

콜론은 이름의 일부이므로 빠지면 다른 이름이 된다.

// 틀린 코드
if ([loans respondsToSelector:@selector(addObject)]) {   // 항상 NO
    [loans addObject:@"BK-003"];
}

// 고친 코드
if ([loans respondsToSelector:@selector(addObject:)]) {
    [loans addObject:@"BK-003"];
}

addObject 와 addObject: 는 서로 다른 셀렉터다. 앞의 것은 배열에 없으므로 조건이 거짓이 되고, 추가가 실행되지 않아도 오류가 나오지 않는다. 컴파일러가 이런 실수를 늘 잡아 주지는 않는다. -Wundeclared-selector 경고를 켜면 선언되지 않은 셀렉터를 알려 주므로 도움이 된다.

한눈에 보기

이 장의 개념과 주의점 요약
개념표기 또는 규칙주의할 점
메시지 전송[수신자 메시지]실행할 코드는 실행 중에 정해진다
인자 있는 메시지[수신자 이름조각:값 이름조각:값]콜론 개수가 인자 개수이고 쉼표는 쓰지 않는다
중첩 메시지[[a b] c]안쪽부터 평가되며 깊어지면 변수로 나눈다
클래스 메서드[클래스 메시지], 선언은 +객체에 보내지 않고 클래스에 보낸다
인스턴스 메서드[객체 메시지], 선언은 -클래스 이름에 보내면 오류다
nil 수신자결과는 nil, 0, NO인자가 nil 인 경우와 C 함수 전달은 별개다
셀렉터@selector(이름:), 타입은 SEL콜론까지 이름의 일부다
응답 확인[객체 respondsToSelector:셀렉터]nil 에게 물으면 NO 다

더 자세한 내용은 Apple 의 Programming with Objective-C 의 객체 다루기 문서에서 확인할 수 있다.

연습 문제

  1. [title stringByReplacingOccurrencesOfString:@"a" withString:@"b"] 에서 수신자, 메시지 이름, 인자 개수를 각각 쓰시오.
  2. NSUInteger n = [[title uppercaseString] length]; 를 중첩 없이 두 문장으로 나누어 쓰고, title 이 @"Objective-C 입문" 일 때 n 의 값을 쓰시오.
  3. 다음 코드가 실행되면 무엇이 출력되는지, 그리고 프로그램이 종료되는지 쓰시오.
    NSString *s = nil;
    printf("%lu\n", (unsigned long)[s length]);
  4. 대출 목록 배열 loans 가 removeAllObjects 메시지에 응답할 때만 그 메시지를 보내는 코드를 작성하시오. 셀렉터를 쓸 때 주의할 점도 한 줄로 쓰시오.

정답과 해설

  1. 수신자는 title, 메시지 이름은 stringByReplacingOccurrencesOfString:withString:, 인자는 2개다. 이름에 콜론이 두 개 있고 각 콜론 뒤에 인자가 하나씩 온다.
  2. NSString *upper = [title uppercaseString];
    NSUInteger n = [upper length];

    n 은 14 다. 대문자로 바꿔도 글자 수는 달라지지 않는다. 안쪽 메시지의 결과를 변수에 담아 바깥 메시지의 수신자로 쓴 것이 중첩과 같은 동작이다.

  3. 0 이 출력되고 프로그램은 종료되지 않는다. 수신자가 nil 이므로 length 는 실행되지 않고 반환형이 숫자라서 0 이 돌아온다. 이 값을 받는 쪽에서 0 이 빈 문자열의 길이인지 nil 때문인지 구분할 수 없다는 점도 기억해 둔다.
  4. if ([loans respondsToSelector:@selector(removeAllObjects)]) {
        [loans removeAllObjects];
    }

    이 메시지는 인자가 없으므로 이름에 콜론을 붙이지 않는다. 인자가 있는 메시지였다면 @selector(addObject:) 처럼 콜론까지 적어야 한다. NSMutableArray 는 이 메시지에 응답하므로 실제로는 조건이 참이다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.