2011년 4월 29일 금요일

[Objective-C] 가변 인자(Variable Arguments) 구현

Objective-C 스타일의 가변 인자(Variable Arguments)에 대한 소개.
굳이 가변인자 설명을 하지 않아도 될 것 같지만, 용어 설명 상 언급하자면 다음과 같은 형태이다.
NSString *str = [NSString stringWithFormat:@"%@ is %d", @"a", 1];
인자 갯수가 고정되지 않는 형태로써 보통 formatted string 을 생성할 때 가장 많이 쓰이는 예 같다. Objective-C의 경우 NSArray 등을 초기화 할 때도 가변인자 형태로 넘기는 예 이다.

그렇다면 이런 형태의 메서드를 구현하려면 어떻게 해야 하는지 정리해보자. 만약 C를 좀(?) 안다면 매우 익숙한 커맨드가 나올 것이다.

1. Nil Termination 이냐 아니냐

NSArray를 초기화 하면서 한번에 오브젝트를 집어 넣을 때는 꼭 끝에 nil을 넣어줘야 한다. 안그럼 컴파일에러가 뿅뿅... 이런 스타일을 Nil Termination 이라고 부른다.

반대로 끝에 nil이 오지 않는 NSString:stringWithFormat 같은 경우도 존재한다. 이 경우 끝에 nil이 없어도 컴파일 에러는 나지 않지만, 대신 인자 갯수가 포맺과 맞지 않으면 프로그램이 죽어버리는 치명적인 문제를 일으키기도 한다.

예제 코드.
// Definition
- (void)argListTest1:(NSString *)str1, ... NS_REQUIRES_NIL_TERMINATION;

// Implementation
- (void)argListTest1:(NSString *)str1, ... {
    va_list args;
    va_start(args, str1);
    
    for (NSString *arg = str1; arg != nil; arg = va_arg(args, NSString *)) {
        NSLog(@"Argument: %@", arg);
    }
    
    va_end(args);
}
편의 상 헤더(.h)와 구현코드(.m)을 한군데 명시했다.

이 코드는 nil termination이 정의된 가변인자를 지원하는 메서드의 예 이다. NS_REQUIRES_NIL_TERMINATION 라는 매크로가 컴파일 타임에 이를 컴파일러에 알려주는 역활을 한다.

이 메서드를 호출 시 몇 개의 인자냐에 따라 로그에 문자열이 지정된 인자 갯수 만큼 찍히는 것을 알 수 있다.

핵심은 C에서도 자주 보던 va_start, va_arg, 그리고 va_end로 이루어지는 va 3단 콤보. (va의 의미는 Variable Arguments, 즉 가변인자로 이해하면 될 듯)

기술적으로 설명하자면, 가변인자는 메모리 상에 차례대로 포인터가 저장되어서 전달되는 형태다. 순서대로 다음과 같은 형식이다.
  1. va_start에서 지정한 포인터가 가변인자의 시작점이 되기 때문에 str1의 포인터를 알려준다. 즉, 인자가 나열된 메모리 상의 시작점을 알려주는 것이다.
  2. for 루프에서 사용되는 것 처럼 va_arg를 통해 차례대로 각 인자의 포인터를 가져올 수 있다. 인자가 나열된 메모리에서 순서대로 포인터를 엑세스 한다는 의미다.
  3. 사용이 끝나면 va_end를 호출해서 알려줘야 한다.

for루프 탈출 조건인 nil이 오는건 왜 가변인자 끝에 nil이 들어가야 하는지를 알려주는 예이다. nil이 마지막 인자라는 것을 알려주기 위한 것이다. 만약 끝을 알지 못 한다면 계속해서 va_arg를 찾으려 할 테고 결국 엉뚱한 메모리 영역을 건드리게 되는 셈이다.

만약 nil termination이 아닌 형태로 만들었다면 (즉 정의에서 NS_REQUIRES_NIL_TERMINATION가 명시되지 않았다면) 무슨 차이가 있을까.

답은 컴파일 타임에 해당 메서드를 호출하는 인자의 끝에 nil이 없어도 에러가 발생하지 않는다.

대신 프로그램이 죽는 걸 원하지 않는다면 프로그래머가 직접 인자의 갯수가 몇 개 인지 확인할 수 있는 방법을 제공해야 한다.

예를 들어, NSString:stringWithFormat의 경우 첫 인자로 포맺 문자열을 명시하는데 여기 내부에 들어가야할 변수의 갯수가 명시되어 있는 셈이다. 그래서 끝에 nil을 붙이지 않아도 갯수 파악에 문제는 없다. 대신 갯수가 틀리면 깔끔하게(?) 죽어버리지만...

2. 가변인자 메서드에서 다른 가변인자 메서드를 호출해 보기

이번 예는 가변인자를 사용하는 메서드에 가변인자를 넘겨보기이다. 예를 들어 NSString:stringWithFormat을 호출하는 등의 행위(?)는 어떻게 하는 것일까.

그 전에, NSString:stringWithFormat의 경우는 좀 제약이 있기 때문에 init메서드인 NSString:initWithFormat:arguments을 호출한다는 것을 미리 알린다.

이 코드는 nil termination을 사용하지 않는다.
// Definition
- (void)argListTest2:(NSString *)str, ...;

// Implementation
- (void)argListTest2:(NSString *)str, ... {
    va_list args;
    va_start(args, str);
    
    NSString *output = [[NSString alloc] initWithFormat:str arguments:args];
    NSLog(@"output = %@", output);
    
    va_end(args);
}
nil termination에 대한 정의가 빠져 있다. va_start/va_end를 사용하지만 va_arg를 사용하지는 않는다.

argListTest2를 호출하는 방법은 stringWithFormat과 동일하다. 처음에 문자열로 포맺 스트링이 오고 그 다음부터 집어넣을 변수들을 알려주는 형태.

va_start로 얻게되는 args의 포인터, 즉 가변인자 시작 지점(포인터)을 arguments에 넘긴다. 이렇게 되면 NSString:initWithFormat을 직접 호출한 것과 동일한 결과를 얻을 수 있다.

뭐 굳이 이렇게 쓸 일은 별로 없을 것 같다. NSLog를 좀 더 편하게 쓰기 위해서 Custom Logging 메서드를 만드는 것 등에서는 유용하긴 하다.

댓글 4개 :

익명 :

도움됐어요 ㄱㅅ

간다 :

좋은글 잘봤습니다... 근데 무심코든 의문이 있어서...질문좀 해보겠습니다.

답변은 해주시면 고맙고 안해 주시면 어쩔수 없구요...

Nil Termination이라는게 NSMutableArray에도 당연히 적용이 되겠지만..

초반에 초기화를 한다면요.. 하지만.

[[NSMutable alloc]init]으로 초기화하고

addObject 메서드로 값을 넣게 되면.. 마지막에 addObject:nil로 nil값을 붙일 수가 없지 않나요?

시도 해봤지만. 에러가 나더군요... 그렇다고 NSNull로 만들어서 넣는거랑은
얘기가 달라지구요..

그럼 좋은 글 잘 읽어 습니다...

Seorenn :

간다//

Nil Termination을 사용하겠다고 하는 경우에 적용되는 법칙이지요.

NSMutableArray의 경우 addObjects 메시지가 Nil Termination을 사용하도록 정의되어 있습니다. 다수의 갯수의 정해지지 않은 오브젝트를 리스트에 추가하는 것이고 그러니 끝에 nil을 명시해야 합니다.

하지만 addObject는 그냥 단일 오브젝트 추가를 위한 것입니다.

간다 :

답변 너무나 감사합니다...꾸벅