Objective-C에서 웹서버로 POST 요청

안내

> 워낙 예전에 작성된 글이어서 얼마나 참고가 될지는 모르겠다. 2017년 6월 기준으로 NSURLConnection API자체에도 많은 변경이 있었고, 현재 애플은 NSURLConnection보다 NSURLSession을 이용하는 것을 권장하고 있다. (NSURLConnection 클래스 자체가 deprecated되었다.)  NSURLSession 사용하기 포스팅을 참고하시길 권장한다.

코코아 네트워킹

코코아에서 네트워크 연결을 통해 통신을 하는 기능을 추가하는 것은 사실 쉽지 않다. 그것은 여느 C/C++ 프로그램과 마찬가지로 상당히 번거로운 작업들을 수반한다. 하지만 이러한 과정들은 단지 작성해야 할 코드량이 파이썬과 같은 언어에 비해 많다는 것이지, 그 과정까지 번거롭다는 것은 아니다. 보통 HTTP 연결을 사용하는 프로그램은 다음과 같은 과정을 거쳐서 동작한다.

  1. HTTP 요청 객체를 생성한다. 이 요청객체에는 헤더 정보를 써서 HTTP 서버가 어떤 요청이며, 어떤 데이터가 전송되는지를 알아채어 올바른 응답을 줄 수 있도록 한다. 다행스러운 점은 일반적인 GET 요청의 경우에는 대부분의 정보는 [[URL로딩시스템]]을 통해 거의 공짜로 생성된다는 점이다. (여러분은 단지 URL만 제공하면 된다. )
  2. HTTP 연결 객체를 생성한다. 1에서 생성한 요청객체(NSRequest)로 HTTP 연결을 요청할 수 있다.

여기까지가 요청을 보내는 것의 전부이고, 파이썬과 비교하였을 때 별다른 차이는 없어보인다. 다만, 응답을 받는 부분이 다른데. 파이썬에서는 요청을 보낸다음, 응답을 읽어오는 함수를 다시 호출하지만, 코코아의 NSURLConnection 객체는 생성 직후에 자동으로 연결을 수립한다. 연결 객체가 만들어지는 시점에 [[델리게이트]]를 지정하게 되는데, 응답을 받고 전달되는 데이터를 처리하는 것은 모두 이 NSURLConnection Delegate 객체가 처리하게 된다.

이 차이는, 코코아의 네트워크 연결은 항상 ‘비동기’로 처리되기 때문이다. 메인 스레드는 요청 객체를 만들고나면 URL로딩시스템은 새로운 스레드에서 네트워크 요청을 처리하게 되고, 델리게이트 역시 새로운 스레드에서 동작하게 된다.

URL Loading System

코코아의 HTTP 요청, 연결, 응답 객체를 묶어서 URL 로딩 시스템이라 부른다. 이 시스템은 HTTP(http://, https://)연결 및 주요 인터넷 프로토콜(ftp://, file:///, data://)을 제어할 수 있는 일련의 프로토콜과 클래스들의 집합으로 생각하면 된다. 간단하게는 웹서버로의 요청과 응답을 처리하는데, 그 외에도 세션이나 인증, 캐시 등을 처리하는 기능을 모두 내장하고 있다. (그리고 기본적으로 자동으로 멀티스레드로 동작한다!)

여기서는 그 중 가장 기본이 되는 [NSURLConnection]에 대해 살펴보자.

<updated 06.2017> NSURLSession을

NSURLConnection 사용하기

연결 만들기

NSURLConnection 클래스는 세 가지 방식의 컨텐츠 수신 모드를 제공한다. 동기적으로 다운로드 받거나, 코드 블럭을 사용하여 비동기적으로 수신데이터를 처리하거나, 혹은 고전적인 방법으로 델리게이트를 사용하여 받은 데이터를 처리하도록 할 수 있다. 이 중 첫번째 방법을 제외한 나머지 후자의 두 방법은 고도로 비동기화된 처리 방법이다.

연결을 동기적으로 처리하기 위해서는 +sendSynchronousRequest:returningResponse:error: 메소드를 사용할 수 있다. 이는 배타적인 백그라운드 스레드에서 수행된다. 이 함수는 응답이 완료되거나, 에러가 나는 경우에 리턴한다.

(개발자 문서의 관련 항목)

비동기적으로 처리하는 대신 핸들러 블럭을 사용하기 위해서는 리퀘스트 객체에 +sendAsynchronousRequest:queue:completionHandler:를 사용하여 요청을 처리한다.

(개발자 문서의 관련 항목)

마지막으로 델리게이트 객체를 사용하는 방법은, 먼저 델리게이트 객체가 다음 중 하나의 메소드를 구현하도록 하고

  • connection: didRecieveResponse:
  • connection: didRecieveData:
  • connection: didFailWithError:
  • connection: didFinishLoading:

연결 객체를 생성한다. 통상 연결을 만들면 그 즉시 백그라운드 스레드를 사용해서 통신을 하게 된다.

요청 객체 만들기

네크워크 요청은 NSURLRequest 클래스를 이용해서 생성한다. 가장 기본적으로는 -initWithURL:을 사용해서 만들면 된다. 이 객체는 URL과 그외의 연결 정책 정보를 래핑하고 있는 객체이다. 주로 다음과 같은 프로퍼티들을 사용할 수 있다.

  • cachePolicy
  • HTTPShouldUsePipelining
  • mainDocumentURL
  • timeoutInterval
  • networkServiceType
  • URL

그리고 HTTP 요청의 경우에는 다음 프로퍼티들을 사용할 수 있다.

  • allHTTPHeaderFields
  • HTTPBody
  • HTTPBodyStream
  • HTTPMethod
  • HTTPShouldHandleCookies
  • valueForHTTPHeaderField:

w에 글을 전송하는 POST 요청을 만드는 예를 보자. 우선 다음과 같은 정보들이 있다.

  • 세 가지 폼필드를 전송한다. 각각의 이름은 title, keyword, content이다.
  • POST 방식으로 전송하며, 요청 주소는 "http://w/add"이다.
  • 각각의 폼필드는 title=…&keyword=….&content=…. 과 같은 방식으로 연결된 하나의 덩어리로 전달되며, 이 값들은 모두 URL인코드(%로 이스케이프된)문자열이다.

예시 코드는 다음과 같다.

#import <Foundation/Foundation.h>

void sendRequest(NSURLRequest *req, NSURLResponse *res) {
    NSError *error = nil;
    [NSURLConnect sendSynchronousReqeust:req returningResponse:&res error:nil];
    if(error) {
        NSLog(@"Error!");
    } else {
        NSInteger s = res.statusCode;
        if(s == 200) {
            NSLog(@"Success");
        } else {
            NSLog(@"Fail : %d", s);
        }
    }
}

NSString *urlencodedString(NSString *str) {
    return [NSString stringByReplacingPercentEscapesUsingEncoding:NSUTF8Encoding];
}

int main(int argc, const char* argv[]) {
    @autoreleasepool{
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:
            [NSURL URLWithString:@"http://w/add"]];
        // data
        NSString *keyword = @"test keyword";
        NSString *title = @"test keywrod";
        NSString *content = @"Post with NSURLConnect";
        NSString *bodyData = [NSString stringWithFormat:
            @"title=%@&keyword=%@&content=%@",
            urlencodedString(title),
            urlencodedString(keyword),
            urlencodedString(content)];
        [reqeust setHTTPMethod:@"POST"];
        [request setValue:@"application/x-www-form-urlencoded"
            forHTTPHeaderField:@"Content-Type"];
        [request setHTTPBody:[NSData dataWithBytes:[bodyData UTF8String] length:strlen([bodyData UTF8String])]];
        NSHTTPURLResponse *response;
        sendRequest(request, response);
    }
    return 0;
}

몇 가지 부연은 아래와 같다.

  • Cocoa의 NSString은 내부적으로 UTF16 유니코드 문자열을 사용한다. HTTP로 전송하는 문자열의 인코딩은 기본적으로 UTF8이 되므로, URL인코딩한 후에는 이 문자열의 UTF8String 값을 전송해야 한다.
  • 전송 자체는 문자열 객체를 보내는 것이 아니라 바이트 스트림 데이터를 보내는 것이므로, NSData 객체로 감싸서 보내야 한다. 이 때 길이를 명시해주어야 한다.
  • NSString의 stringByReplacePercentEscapesUsingEncoding:은 모든 문자를 적절히 이스케이핑하지는 않는다!!! 다만, GNUSTEP을 사용하는 경우, CFStringRef를 사용할 수가 없어서 그냥 이 메소드를 그대로 썼다. OSX나 iOS기반이라면 CFStringRef의 변환 함수를 사용하는 것이 더 좋다.

동기식 전송은 왜 필요한가?

명령줄 도구를 만드는 경우, 비동기식 방법을 사용하면, 네트워크 처리를 담당하는 스레드의 작업 중간에 메인스레드의 작업이 끝나게 된다. (보통 앱의 처리 프로세스에서 네트워크 통신은 디스크 I/O보다도 더 느린, 가장 느린 처리 종목에 해당한다.) 따라서 웹서버의 응답을 받지 못하고 프로그램이 종료된다. 이런 경우 런루프를 강제로 돌려서 응답을 기다리도록 할 수 있는데, 이것보다는 통신 처리 자체를 동기적으로 처리하여 통신이 끝난 이후에 후작업을 처리하고 프로그램을 종료하는 것이 더 낫다.

같은 이유로 GUI 애플리케이션에서 UI를 담당하는 메인 스레드에서는 네트워크 연결을 동기적으로 처리해서는 절대 안된다. (그 동안 프로그램은 멈춘 것 처럼 보인다.)