Devin.KR

Objective-C · 기본

Objective-C 문법과 Cocoa 기초

Objective-C 시작하기 - C 위에 얹은 메시지

Objective-C 가 아직 필요한 곳(기존 iOS·macOS 코드, Swift 연동), clang 으로 컴파일, #import, @autoreleasepool, NSLog 와 printf 출력 차이

개발자KR · 원고 갱신

이 장에서 배우는 것

Objective-C는 C 언어에 객체와 메시지 전달이라는 기능을 얹은 언어다. 이 책은 사내 도서 대출 관리 도구(도서·회원·대출 기록)를 장마다 조금씩 키우면서 이 언어의 문법과 Cocoa의 기초 클래스(Foundation)를 익힌다. 첫 장에서는 클래스를 만들지 않는다. 대신 한 파일짜리 프로그램을 컴파일하고 실행해서, 앞으로 모든 예제가 따를 뼈대와 출력 방식을 정한다.

  • Objective-C가 오늘날 어디에서 쓰이는지 설명할 수 있다.
  • clang 으로 main.m 을 컴파일하고 실행하며, 각 옵션이 하는 일을 말할 수 있다.
  • #import 와 @autoreleasepool 이 뼈대에서 맡는 역할을 설명할 수 있다.
  • printf 와 NSLog 의 출력 위치와 형식 차이를 구분해서 쓸 수 있다.

문제 상황

작은 회사의 개발자가 사내 도서 대출 도구를 넘겨받았다고 하자. 저장소를 열어 보니 확장자가 .m 인 파일이 가득하고, 코드에는 [book title] 같은 대괄호 호출과 @"문자열" 같은 낯선 표기가 있다. 이 도구는 몇 년 전에 만들어졌고 지금도 잘 돌아간다. 새 화면은 Swift로 만들지만 핵심 계산 코드는 Objective-C 그대로 두고 호출한다.

이 개발자가 간단한 확인용 프로그램을 만들어 결과를 파일과 비교하는 스크립트를 짰다고 하자. 그런데 비교가 계속 실패한다. 결과를 NSLog 로 찍었더니 줄마다 날짜와 프로세스 번호가 앞에 붙어 실행할 때마다 값이 달라졌기 때문이다. 게다가 이 출력은 표준 출력이 아니라 표준 오류로 나가서, 파이프로 넘긴 내용에는 아예 들어오지 않았다.

이 장은 두 가지 문제를 푼다. 하나는 낯선 코드를 읽고 직접 컴파일해 볼 수 있는 최소한의 바탕이다. 다른 하나는 결과 출력과 진단 출력을 구분하는 습관이다.

Objective-C가 아직 필요한 곳과 언어의 층

어디에서 쓰이는가

새 앱은 대부분 Swift로 시작한다. 그래도 Objective-C를 읽어야 하는 이유는 분명하다.

Objective-C를 만나게 되는 대표적인 상황
상황무엇을 하게 되는가필요한 능력
오래된 iOS·macOS 앱기존 .m 코드를 고치고 기능을 덧붙인다문법 읽기, 메모리 규칙 이해
Swift 프로젝트 안의 일부 코드Swift에서 Objective-C 클래스를 부른다헤더 읽기, 타입 대응 이해
Apple 프레임워크 문서와 예전 자료메서드 이름이 대괄호 호출 형태로 적힌 글을 읽는다메시지 이름 규칙 이해
C·C++ 라이브러리와의 결합C 함수를 그대로 부르는 래퍼를 둔다C 문법 이해

Swift 에서 Objective-C 코드를 부르는 구체적인 방법은 프로젝트 설정에 따라 다르다. 이 책은 그 설정을 다루지 않고, Objective-C 쪽 코드를 읽고 쓰는 일에 집중한다. 프레임워크 API 설명은 Foundation 공식 문서에서 확인할 수 있다.

C 위에 얹은 층

Objective-C는 C와 별개의 언어가 아니라 C의 문법을 그대로 포함한다. int, if, for, 함수, 포인터, printf 를 모두 그대로 쓸 수 있다. 여기에 객체를 만들고 객체에게 메시지(message)를 보내는 문법이 더해진다. 메시지는 "이 객체야, 이 일을 해 줘"라는 요청이다. 문법은 [객체 메시지] 처럼 대괄호로 쓴다. 자세한 규칙은 메시지 보내기를 다루는 장에서 배우므로, 지금은 [appName length] 가 "appName 객체에게 length 를 요청한다"는 뜻이라는 것만 알면 된다.

그 위에 Foundation 프레임워크가 놓인다. 프레임워크는 헤더와 라이브러리를 묶어 둔 꾸러미다. Foundation에는 문자열(NSString), 로그 함수(NSLog) 같은 기본 도구가 들어 있다. 이름 앞의 NS 는 이 라이브러리의 먼 옛 이름에서 온 접두어다. 접두어는 이름 충돌을 피하려고 붙인다.

Objective-C 는 C 언어 위에 객체와 메시지 문법을 얹고, 그 위에 Foundation 라이브러리를 사용하는 3층 구조다.

컴파일과 뼈대 코드

clang 으로 컴파일하기

소스 코드는 기계가 바로 읽지 못한다. 컴파일러가 실행 파일로 바꿔 준다. macOS에서는 Xcode 명령줄 도구에 clang 이 들어 있다. 이 책의 모든 예제는 다음 한 줄로 만든다.

clang -fobjc-arc -Wall -framework Foundation *.m -o app
컴파일 명령의 각 부분이 하는 일
부분역할빠뜨리면
-fobjc-arc객체 메모리를 컴파일러가 관리하게 한다직접 메모리를 관리해야 하는 방식으로 컴파일된다
-Wall흔한 실수에 경고를 낸다실수가 조용히 지나간다
-framework FoundationFoundation 을 연결한다링크 단계에서 심볼을 찾지 못한다
*.m ... -o app모든 소스를 모아 app 이라는 실행 파일로 만든다실행 파일 이름이 a.out 이 된다

ARC(자동 참조 계수)는 객체가 더 쓰이지 않을 때 컴파일러가 해제 코드를 넣어 주는 방식이다. 원리는 메모리를 다루는 장에서 설명한다. 이 책에서는 항상 켠 상태로 쓰고, 경고가 하나라도 나오면 고친 뒤에 넘어간다. 컴파일 옵션은 clang의 ARC 문서에 정리되어 있다.

Linux에서는 GNUstep 같은 별도 환경이 필요하고 프레임워크 연결 방식도 다르다. -framework 옵션은 macOS 용이므로, 이 책의 명령은 macOS 를 기준으로 한다.

#import 와 @autoreleasepool

C에서는 헤더를 #include 로 가져온다. Objective-C에는 #import 가 있다. 하는 일은 같지만 같은 헤더를 한 번만 읽어 들인다는 점이 다르다. 그래서 헤더마다 중복 방지 장치를 직접 쓰지 않아도 된다. <Foundation/Foundation.h> 는 Foundation 의 주요 헤더를 한꺼번에 가져오는 대표 헤더다. 꺾쇠 괄호는 시스템에 설치된 헤더를 찾는다는 뜻이고, 직접 만든 헤더는 큰따옴표로 가져온다. 이 책은 클래스를 나누는 장부터 큰따옴표를 쓴다.

@autoreleasepool { ... } 는 중괄호 범위 안에서 만들어진 임시 객체를 범위가 끝날 때 한꺼번에 정리하는 블록이다. Foundation 의 많은 메서드는 "나중에 정리해도 되는 객체"를 돌려주는데, 그 객체가 쌓이는 곳이 이 풀(pool)이다. ARC를 쓰더라도 main 의 본문은 이 블록으로 감싸는 것이 관례이므로 이 책의 모든 예제가 이 형태를 따른다. 자세한 동작은 메모리를 다루는 장에서 다시 본다.

NSLog 와 printf 의 출력 차이

이 책에서 가장 먼저 정해 둘 약속은 결과를 어디로 내보내는가이다. 두 함수는 같아 보이지만 성격이 다르다.

printf 와 NSLog 의 차이
항목printfNSLog
출력 위치표준 출력(stdout)표준 오류(stderr)
덧붙는 내용없음, 쓴 그대로 나간다시간, 프로세스 이름과 번호
줄바꿈\n 을 직접 쓴다필요하면 자동으로 붙는다
객체 출력C 문자열만 받는다서식 %@ 로 객체를 출력한다

NSLog 가 찍는 한 줄은 대략 다음 모양이다. 날짜, 프로세스 이름과 번호는 실행할 때마다 달라지므로 값은 예시일 뿐이다.

2026-10-01 10:20:30.123 app[4821:90210] 진단: 도서 3권 확인

이 줄은 결과를 담는 용도가 아니라 개발자가 상태를 확인하는 진단용이다. 표준 출력과 표준 오류는 서로 다른 통로이므로, 다음 그림처럼 셸에서 따로 다룰 수 있다. 예를 들어 ./app > out.txt 는 표준 출력만 파일에 담고, 표준 오류는 터미널에 그대로 남긴다.

printf 는 표준 출력으로, NSLog 는 표준 오류로 나가므로 결과와 진단을 따로 다룰 수 있다.

이 때문에 결과 출력은 printf 로, 진단은 NSLog 로 나눈다. 시간이 붙는 NSLog 는 출력을 비교하는 스크립트에서 쓸 수 없다. 또 표준 출력이 파이프나 파일로 연결되면 내용을 모아 두었다가 종료할 때 내보내고, 표준 오류는 바로 내보낸다. 두 통로를 같은 파일에 합치면 줄 순서가 코드의 순서와 다를 수 있다. 이 책의 예제는 결과를 표준 출력만으로 확인한다.

printf 는 NSString 객체를 직접 받지 못한다. 객체에게 C 문자열(UTF-8 인코딩 바이트 열)을 달라고 UTF8String 메시지를 보내고, 그 결과를 %s 에 넘긴다. 이 책의 모든 결과 출력은 printf("%s\n", [문자열 UTF8String]) 형태를 쓴다.

완성 코드

사내 도서 대출 관리 도구의 첫걸음으로, 프로그램 이름과 등록 도서 수를 출력하는 코드다. 파일 이름은 main.m 이다.

#import <Foundation/Foundation.h>

int main(void) {
    @autoreleasepool {
        NSString *appName = @"사내 도서 대출 관리";
        int bookCount = 3;

        printf("프로그램: %s\n", [appName UTF8String]);
        printf("등록 도서: %d권\n", bookCount);
        printf("이름 글자 수: %lu\n", (unsigned long)[appName length]);

        NSString *summary = [NSString stringWithFormat:@"%@ - %d권", appName, bookCount];
        printf("요약: %s\n", [summary UTF8String]);

        NSLog(@"진단: 도서 %d권 확인", bookCount);
    }
    return 0;
}

줄별 해설

1행 Foundation 의 대표 헤더를 가져온다. 이 줄이 있어야 NSString 과 NSLog 가 무엇인지 컴파일러가 안다.

3행 C 와 똑같은 main 함수다. 인자를 쓰지 않으므로 void 로 둔다. 프로그램은 여기서 시작한다.

4행과 16행 임시 객체를 정리하는 범위를 연다. 4행의 여는 중괄호부터 16행의 닫는 중괄호까지가 한 풀이다.

5행 @"..." 는 NSString 객체를 만드는 문자열 리터럴이다. 앞의 @ 가 없으면 C 문자열이 되어 타입이 달라진다. NSString * 의 별표는 객체를 가리키는 포인터라는 뜻으로, C의 포인터와 같은 모양이다. 객체는 항상 포인터로 다룬다.

6행 일반 C의 int 변수다. Objective-C에서는 C 타입과 객체를 한 함수 안에서 섞어 쓴다. 두 부류의 차이는 다음 장에서 정리한다.

8행 [appName UTF8String] 은 appName 객체에게 C 문자열을 요청하는 메시지다. 받은 값을 %s 에 넣어 표준 출력에 찍는다. 한글은 UTF-8 로 나가므로 터미널이 UTF-8 이면 정상으로 보인다.

9행 %d 는 int 를 찍는 C의 서식이다. bookCount 가 int 이므로 맞는다.

10행 [appName length] 는 글자 수를 돌려준다. 돌려주는 타입은 부호 없는 큰 정수(NSUInteger)이고, 플랫폼에 따라 크기가 다르다. 서식 %lu 에 맞추려고 (unsigned long) 으로 바꾼다. "사내 도서 대출 관리"는 한글 8자와 공백 3개로 11이다.

12행 stringWithFormat: 은 서식에 값을 끼워 새 문자열 객체를 만든다. %@ 는 객체를 문자열로 바꿔 끼우는 서식이고, 여기서는 appName 이 들어간다.

13행 만들어진 문자열을 8행과 같은 방식으로 출력한다.

15행 진단 줄이다. 표준 오류로 나가고 시간과 프로세스 정보가 붙는다. 서식 문자열 앞에 @ 가 붙는 것에 주의한다. NSLog 의 첫 인자는 C 문자열이 아닌 NSString 이다.

17행 0을 돌려주어 정상 종료를 알린다.

실행 결과

컴파일한 뒤, 표준 오류를 버리고 표준 출력만 보도록 실행한다.

$ clang -fobjc-arc -Wall -framework Foundation *.m -o app
$ ./app 2>/dev/null
프로그램: 사내 도서 대출 관리
등록 도서: 3권
이름 글자 수: 11
요약: 사내 도서 대출 관리 - 3권

컴파일 명령은 경고 없이 끝나야 한다. 리다이렉션 없이 ./app 만 실행하면 터미널에 NSLog 줄이 하나 더 보이지만, 시간과 프로세스 번호가 매번 다르므로 위 결과에는 넣지 않았다.

실무에서 자주 틀리는 것

1. Foundation 을 연결하지 않는다

헤더만 가져오고 연결 옵션을 빠뜨리면 컴파일은 되어도 링크 단계에서 실패한다.

// 틀린 명령
clang -fobjc-arc -Wall *.m -o app
// 링크 오류: NSLog, NSString 등의 심볼을 찾지 못한다

// 고친 명령
clang -fobjc-arc -Wall -framework Foundation *.m -o app

헤더는 이름의 선언만 알려 줄 뿐이고 실제 코드는 라이브러리에 있다. 둘을 모두 챙겨야 한다.

2. printf 에 NSString 을 그대로 넘긴다

// 틀린 코드: %s 는 C 문자열을 기대하는데 객체 포인터가 들어간다
printf("%s\n", appName);

// 고친 코드
printf("%s\n", [appName UTF8String]);

-Wall 을 켜 두면 서식과 인자 타입이 맞지 않는다는 경고가 나온다. 경고를 무시하고 실행하면 엉뚱한 글자가 나오거나 비정상 종료할 수 있다.

3. NSUInteger 를 %d 로 찍는다

// 틀린 코드: length 는 부호 없는 큰 정수를 돌려준다
printf("%d\n", [appName length]);

// 고친 코드
printf("%lu\n", (unsigned long)[appName length]);

64비트 환경에서 %d 는 4바이트 정수를 읽는데 값은 8바이트다. 경고가 나오므로 서식과 형 변환을 맞춘다.

4. 문자열 앞의 @ 를 빠뜨린다

// 틀린 코드: C 문자열을 NSString 포인터에 넣고 있다
NSString *appName = "사내 도서 대출 관리";

// 고친 코드
NSString *appName = @"사내 도서 대출 관리";

따옴표 문자열은 C 문자열이고 @ 가 붙은 것이 NSString 이다. 겉모양이 비슷해 가장 자주 하는 실수이며, 컴파일러가 포인터 타입이 맞지 않는다고 알려 준다.

한눈에 보기

이 장에서 다룬 요소 정리
요소역할이 책의 약속
clang ... -o app소스를 실행 파일로 만든다옵션은 항상 -fobjc-arc -Wall -framework Foundation
#import <Foundation/Foundation.h>Foundation 헤더를 한 번만 가져온다모든 .m 파일 맨 위에 둔다
@autoreleasepool { }임시 객체를 정리하는 범위main 의 본문을 감싼다
@"..."NSString 리터럴따옴표만 쓰면 C 문자열이다
printf("%s\n", [s UTF8String])결과를 표준 출력으로 내보낸다결과는 항상 이 방식으로 찍는다
NSLog(@"...")진단을 표준 오류로 내보낸다결과 출력에는 쓰지 않는다

연습 문제

  1. 완성 코드에 전체 도서 3권 중 1권이 대출 중이라는 정보를 더해, "대출 중: 1권"과 "대출 가능: 2권"을 표준 출력으로 찍는 코드를 작성하라. 대출 가능 수는 뺄셈으로 계산한다.
  2. 완성 코드를 컴파일한 뒤 ./app 2>/dev/null 과 ./app 1>/dev/null 을 각각 실행했을 때 무엇이 보이는지 설명하라.
  3. 다음 코드에서 문제를 찾아 고쳐라.
    NSString *name = @"홍길동";
    printf("회원: %s\n", name);
  4. 다음 코드가 경고를 내는 이유를 말하고 고쳐라.
    NSString *name = @"홍길동";
    printf("글자 수: %d\n", [name length]);

정답과 해설

1번. C의 정수 연산을 그대로 쓴다. 6행 아래에 변수를 추가하고 출력 줄을 더한다.

int onLoan = 1;
printf("대출 중: %d권\n", onLoan);
printf("대출 가능: %d권\n", bookCount - onLoan);

출력은 대출 중: 1권 과 대출 가능: 2권 이다. 이 값들은 모두 int 이므로 %d 로 찍는다.

2번. 2>/dev/null 은 표준 오류를 버리므로 printf 로 찍은 네 줄만 보인다. 1>/dev/null 은 표준 출력을 버리므로 NSLog 가 찍은 진단 한 줄만 보인다. 이때 줄 앞에 시간과 프로세스 정보가 붙는다. 출력 통로가 둘로 나뉘어 있음을 확인하는 문제다.

3번. %s 에 NSString 객체를 넘긴 것이 문제다. 다음과 같이 고친다.

printf("회원: %s\n", [name UTF8String]);

4번. length 의 반환 타입이 부호 없는 큰 정수(NSUInteger)라서 %d 와 맞지 않는다. 다음과 같이 서식과 형 변환을 맞춘다.

printf("글자 수: %lu\n", (unsigned long)[name length]);

"홍길동"은 세 글자이므로 출력은 글자 수: 3 이다.

오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

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

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