2014년 4월 26일 토요일

[Objective-C] 블럭 문법 (Blocks Programming)

최근 iOS나 OS X SDK Framework 를 보고 있다면 블럭에 기반한 메소드들이 점점 늘어나는 것 같다. 상황에 따라서 들여쓰기 레벨이 높아지거나 좀 불안한(?) 코드 모양이 나오는 듯 코드 리딩에 안좋은 모양새를 나타낼 때도 있지만, 그래도 여러 면에서 유용한 기능(?)이니 이 참에 메모를 남겨본다.

블럭(Block)은 C 함수이다. 정확히 표현하자면 런타임에 생성되는 다이나믹 함수(Dynamic Function) 이다. 여기서 런타임(Runtime)과 다이나믹(Dynamic) 이란 의미에 중점을 두면 '병렬처리'나 '비동기처리(Asyncronous Processing)', 혹은 '함수형언어(Functional Language)'의 특징과 비슷하다.

근데 말이 너무 어렵다 -_-

어쨌든 그래서 어떻게 쓰이는지 한번 보자. 참고로 코드 예제는 애플의 블럭 프로그래밍 문서의 것을 그대로 활용한다.
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};
이 코드가 실행되면 myBlock 이라는 함수 변수(?)가 생성된다. 대충 C의 함수 포인터와 비슷하게 생각하자. 그래서 이런 식으로 실행시키는 것이 가능하다.
int result = myBlock(10);
// result 결과는 70
​윗쪽꺽쇠(^)로 시작되는 것이 블럭 문법의 중요한 점이다. 즉 myBlock은 함수 블럭 변수이고 그 다음에 등장하는 '^(int num)'은 블럭 타입의 함수를 의미한다.

이번에는 블럭의 제한에 대해 보자.
int x = 123;
 
void (^printXAndY)(int) = ^(int y) {
    x = x + y; // error
    printf("%d %d\n", x, y);
};
이 코드에서 에러가 발생한다는 주석이 달려있다. 이유는 블럭 외부의 변수에 뭔가를 쓰려고 해서이다. 즉 다른게 이야기 하자면, 블럭 내부에서는 블럭 외부의 변수를 읽기 전용(read only)으로만 참조 할 수 있다는 말이다.

이 문제를 해결하려면 블럭 내부에서 사용한다고 표현을 고치면 된다. x 선언부를 아래와 같이 수정한다.
__block int x = 123;
언더스코프가 두 개나 붙어있어서 개인적으로 좀 싫어하는 표현이지만 그래도 Objective-C가 좋으니 어쩔 수가 없다. -_-;;


일반적으로 많이 쓰이는 구조

이론적인 예제는 이제 집어치우고 실제로 많이 쓰이는 방식을 살펴보자. 주로 비동기(Asynchronous) 방식으로 처리되어야 하는 경우 기존에는 Delegate 스타일이 많이 쓰였지만 요즘엔 한 번의 결과만을 받는 경우 이 블럭 방식을 많이 사용한다.

아래 예제는 블럭 타입과 블럭을 사용하는 메서드의 선언(Definition, 즉 헤더파일) 예제이다.
typedef void (^ResponseBlock)(NSData *data, NSError *error);

...

@property (nonatomic, strong) ResponseBlock responseBlock;

...

- (void)startRequestWithCompletion:(ResponseBlock)responseBlock;
꺽쇠가 붙어있는 이름(ResponseBlock)이 블럭 타입 이름이 되었다. 프로퍼티(property)로 선언된 내용은 실제 블럭 함수를 보관해 놓기 위한 용도이고 strong 으로 정의함으로써 소유권을 이 클래스가 가지게 된다. 만약 strong이 아니라면 이 메서드를 호출한 쪽이 먼저 메모리에서 해제될 경우 문제가 발생 할 수도 있기 때문이다.

이 메서드의 구현부(Implementation, 즉 .m 파일)는 몇 가지 가정이 있어야 할 것이다. 비동기로 처리되어 responseBlock이 호출되어야 한다는 점이다. 아래 코드는 특정 클래스의 메서드를 실행시키고 자신은 delegate로써 이 결과물을 따로 호출받는 식으로 동작한다는 가정하의 예제이다.
// Method
- (void)startRequestWithCompletion:(ResponseBlock)responseBlock
{
    self.responseBlock = responseBlock;

    SomeClass *someObject = [[SomeClass alloc] init];
    [someObject startRequestWithDelegate:self];
}

...

// Delegate
- (void)finishWithData:(NSData *)data error:(NSError *)error
{
    if (self.responseBlock) {
        self.responseBlock(data, error);
        self.responseBlock = nil;
    }
}
많이 축약하긴 했지만 앞서 이야기한 가정 하에 구현했을 경우 위와 같은 모양이 될 것이다. startRequestWithCompletion: 메서드가 responseBlock을 보관하고 실제 필요한 작업을 시작한다. 그리고 작업이 끝나면 finishWithData:error: 라는 메서드로 호출이 들어오는데 여기서 저장해 놓은 responseBlock(블럭 함수)를 호출히서 결과가 끝났음을 알려준다.

그리고 위 처럼 strong property 에 블럭을 묶어두고 있는 경우 처리가 끝나고 이를 nil로 초기화 해 줌으로써 해당 블럭을 계속 가지지 않도록 해야된다. 메모리가 남아돌아서 해제 안되도 문제 없다는 경우를 빼고 말이다. -_-;;

이 메서드를 사용하는 곳은 아래와 같은 식으로 블럭을 만들어 줄 수 있을 것이다.
[someObject startRequestWithCompletion:^(NSData *data, NSError *error) {
    if (error) { ... }
    // process data
}];
심하게 축약하긴 했지만 어쨌든 블럭을 넘겨 줄 때 윗쪽 꺽쇠(^)로 시작하는 함수 형태를 기억해 놓자.

이런 블럭 문법은 위 처럼 귀찮은 delegate 스타일을 간편하게 해결 할 수 있어서도 좋지만, GCD 등 여러 분야에서도 활용되는 중요한 문법이다. 더구나 GCD와 연동시키면 백그라운드/패러럴 프로세싱이 정말 즐거워 질 것이다.

[관련글] [Objective-C] Block Syntax 초간단정리
[관련글] [iOS] 특정 코드를 비동기로 실행시키기

댓글 1개 :

jusung :

좋은 포스트 감사합니다.! 머리에 쏙쏙 들어오네요.