이 항목은 작성 중입니다.
이 항목은 작성 중인 항목입니다. 내용이 불완전할 수 있습니다. 충돌의 위험이 있으니 history를 확인한 후 작성하는 것을 권장합니다. 이 문서가 오랫동안 편집되지 않았다면 이 틀을 제거해 주세요.
오브젝티브 씨라고 읽으면 된다. 애칭은 옵씨. 브래드 콕스(Brad Cox)와 톰 러브(Tom Love)가 만든, 20세기의 걸작 언어 Jr.라고 생각하면 당신은 멋쟁이.
안시(ANSI) C에 객체지향 층을 얹어 만들었다. 따라서 실행 시 반드시 런타임(libobjc.dylib 또는 objc.dll)의 도움을 받아야 작동 가능하다. 그렇다고 Java처럼 가상 기계(VM) 위에서 돌아가는 건 아니고, 객체 간의 통신을 담당하는 작은 라이브러리에 링크되어야 한다. 물론 이 런타임 라이브러리는 애플 오픈 소스 사이트에 공개돼있으므로 능력이 있다면 얼마든지 정적 라이브러리로 만들어 붙여버려도 된다.
1. 역사 ¶
초기의 옵씨는 C에서 객체지향을 지원하도록 문법을 확장한, 일종의 문법적 편의사항(syntactic sugar)으로 구현됐다. 옵씨 컴파일러는 옵씨 코드를 읽어 C 코드를 뱉었고, C 컴파일러가 그걸 바이너리로 어셈블했다. 하지만 넥스트(NeXT Inc.)가 옵씨를 집어삼킨 이후, 옵씨 코드에서 바로 바이너리로 직행하는 컴파일러가 나왔다. 이를 위해, 예전에는 GCC를 이용했지만 요즘은 애플 주도의 LLVM/Clang을 이용해 컴파일한다. 물론 오픈 소스 LLVM/Clang 3와 Apple LLVM/Clang 4 모두 옵씨 코드를 컴파일할 수 있지만, 닫힌 가지인 Apple LLVM/Clang 4가 훨씬 월등한 어셈블 능력을 보여준다고. 애초에 버전부터 차이나니까.
여담으로 FreeBSD 재단에서 오픈 소스 LLVM/Clang 3를 다루던 사람이 말하길, 참고삼아 GCC 4.2의 옵씨 담당 부분 소스 코드를 봤다가 기절할 뻔했다고. 무려 일만 줄이 넘어가는 단일 C 파일 하나로 옵씨의 모든 걸 처리했단다. 이 GCC 확장에 대해서는 여러 루머가 있는데, 가장 유명한 걸 고려한다면 잡스가 빡쳐서 엿먹어라 하는 수준으로 GCC에 똥을 던진 듯.
2. 문법 ¶
C 소스 코드는 곧 옵씨 소스 코드이다. 앞서 말했듯, 옵씨는 C에 확장을 얇게 바른 언어에 가깝다. 하지만 그렇다고 옵씨 소스 코드가 곧 C 소스 코드인 건 아니다.
이 단에서는 옵씨만의 고유한 문법과 기호에 대해 설명하며, 이후 모든 설명에서는 ARC 사용을 기본적으로 가정한다.
이 단에서는 옵씨만의 고유한 문법과 기호에 대해 설명하며, 이후 모든 설명에서는 ARC 사용을 기본적으로 가정한다.
2.1.1. 클래스 선언 ¶
클래스 선언은 외부로 노출 가능한 클래스 명세를 의미한다. 즉, 외부의 프로그래머들에게 '이 클래스는 이러이러한 기능을 가지고 있다'고 알리는 목적이다.
클래스 선언에 쓰이는 옵씨 고유의 문법은
클래스 선언에 쓰이는 옵씨 고유의 문법은
@interface
명령과 @end
명령이다.@interface
명령은 선언하는 클래스의 이름과 상속하는 부모 클래스 이름, 그리고 쉼표로 구분된 준수할 프로토콜 이름의 목록을 인자로 받는다. 형식은 다음과 같다:@interface 클래스 이름 : 부모 클래스 이름 <준수할 프로토콜 이름 목록>여기서 부모 클래스 이름과 준수할 프로토콜 이름 목록은 선택적이다. 즉, 어떠한 클래스도 상속하지 않아 그 자신이 최상위 뿌리(root) 클래스가 될 수도 있고 어떠한 프로토콜도 준수하지 않을 수 있다.
@end
명령은 @interface
명령의 영역을 닫는다.아래에 몇 가지 예제가 있다:
// 뿌리 클래스 @interface ParentClass1 @end // 뿌리 클래스를 상속한 클래스 @interface ChildClass1: ParentClass1 @end // 또 다른 뿌리 클래스이면서 동시에 Protocol1을 준수 @interface ParentClass2 <Protocol1> @end // 또 다른 뿌리 클래스를 상속하면서 동시에 (상속받은) Protocol1과 Protocol2, Protocol3를 준수 @interface ChildClass2: ParentClass2 <Protocol2, Protocol3> @end
2.1.2. 메서드 선언 ¶
메서드 선언 역시 선언의 일부이므로 클래스 선언 안에 들어간다. 메서드 선언문의 형식은 다음과 같다:
// 클래스 메서드 선언문 + (반환형) 메서드_이름 ; // 인스턴스 메서드 선언문 - (반환형) 메서드_이름 ;선언문에서
(반환형)
부분은 생략 가능하다. 생략하면 기본 반환형인 id
가 사용된다.메서드 이름 부분은 다음과 같은 형식을 가진다:
// 알림: 읽기 쉽도록 메서드 이름에 언더스코어(_)를 포함시켰다. // 즉, 여기서 메서드 이름의 언더스코어는 필수요소가 아닌, 메서드 이름의 일부이다. // 인자를 받지 않는 경우. 메서드_이름 // 인자를 하나 받는 경우. // 이 경우, 메서드 이름은 '메서드_이름과_인자:'가 된다. 메서드_이름과_인자:(인자형)인자_이름 // 인자를 두 개 받는 경우. // 이 경우, 메서드 이름은 '메서드_이름과_인자1:인자2:'가 된다. 메서드_이름과_인자1:(인자형)인자_이름1 인자2:(인자형)인자_이름2 // 인자를 세 개 받는 경우. // 이 경우, 메서드 이름은 '메서드_이름과_인자1:인자2:인자3:'이 된다. 메서드_이름과_인자1:(인자형)인자_이름1 인자2:(인자형)인자_이름2 인자3:(인자형)인자_이름3 // 이 정도면 이해했겠지.
시범삼아 메서드 몇 개를 선언해보겠다:
// 클래스 메서드. 반환형은 char *. // 메서드 이름은 'iTakeNoParameter'. +(char *)iTakeNoParameter; // 클래스 메서드. 반환형은 id. // 메서드 이름은 'allocWithZone:'. // 하나의 인자를 받으며, 그 형은 NSZone *, 이름은 aZone. +allocWithZone:(NSZone *)aZone; +(id)allocWithZone:(NSZone *)aZone; // 같은 표현 // 인스턴스 메서드. 반환형은 NSInteger. // 메서드 이름은 'numberOfCharacter:inString:'. // 인자는 두 개를 받는다. 첫번째는 unichar형의 ch, 두번째는 NSString *형의 str. -(NSInteger)numberOfCharacter:(unichar)ch inString:(NSString *)str; // 인스턴스 메서드. 반환형은 없음. // 메서드 이름은 'drawRectangleOfWidth:height:upperLeftX:upperLeftY:'. // 인자는 네 개를 받는다. 모든 인자는 CGFloat형이고 그 이름은 각각 w, h, x, y이다. -(void)drawRectangleOfWidth:(CGFloat)w height:(CGFloat)h upperLeftX:(CGFloat)x upperLeftY:(CGFloat)y;콜론 역시 메서드 이름의 일부임을 유의하자.
옵씨는 메서드 오버로딩을 지원하지 않는다. 따라서 중복된 이름의 메서드를 선언하면 컴파일 오류가 발생한다.
또한, 클래스 선언에 동봉된 모든 메서드는 공개(public) 가시성을 갖는다. 비공개(private) 메서드는 추후 설명하겠다.
또한, 클래스 선언에 동봉된 모든 메서드는 공개(public) 가시성을 갖는다. 비공개(private) 메서드는 추후 설명하겠다.
2.1.3. 인스턴스 변수 선언: 제1부 ¶
인스턴스 변수는 두 곳에 선언할 수 있다. 첫번째는
@interface
명령 바로 다음이다:@interface ExampleClass: ExampleSuperClass <ExampleProtocol> { // 인스턴스 변수 선언 시작 int exampleInteger; // struct를 만들 때처럼 변수를 선언해주면 된다. char exampleCharacter; NSString *exampleString; // 기타 인스턴스 변수를 선언한다. } // 인스턴스 변수 선언 끝 // 메서드 선언부 @end
추가적으로, (C++처럼)
@private
, @protected
, @public
명령을 사용해서 인스턴스 변수의 가시성을 조절할 수 있다:@interface ExampleClass: ExampleSuperClass <ExampleProtocol> { double iAmProtectedByDefault; // 기본 가시성은 @protected이다. @private int privateInteger; // @private 변수는 이 클래스 소속의 메서드에서만 접근 가능하다. char privateCharacter; @protected // 이렇게 다른 가시성 명령을 만나기 전까지는 이전 가시성 명령이 계속 적용된다. int protectedInteger; // @protected 변수는 이 클래스와 상속받은 하위 클래스 소속의 메서드에서만 접근 가능하다. char protectedCharacter; @public int publicInteger; // @public 변수는 개나 소나 다 접근 가능하다. char publicCharacter; @private int privateInteger2; @public char publicCharacter2; @protected float protectedFloat; int iAmProtectedToo; } // 메서드 선언부 @end가시성 명령은 어디까지나 선택적이다. 명시적인 가시성 명령이 없으면 인스턴스 변수에는 기본적으로
@protected
가 적용된다.하지만 옵씨 고수들은 가시성을 제한하더라도 쓸데없이 내부 구조를 외부에 노출시킨다는 문제 때문에
@interface
내부에 인스턴스 변수 선언을 넣는 것에 반대하는 입장이다. 따라서 이렇게 할 수도 있다는 것만 알고 다음으로 넘어가도록 하자. 다른 방법은 클래스 정의 부분에서 알아볼 것이다.2.1.4. 프로퍼티 선언: 제1부 ¶
종래의 자바나 C++ 등의 언어에서는 클래스 외부에서 보호된(protected 또는 private) 인스턴스 변수를 다루려면 '접근자(accessor 또는 getter)'와 '변경자(mutator 또는 setter)'라는 메서드를 통해야만 한다. (C++의 friend 개념은 일단 제외하자.) 물론 옵씨도 예외는 아니라서, 모든 인스턴스 변수를
하지만 문제는 언제나 귀차니즘. 언제나
그래서 옵씨 2.0부터 새로이 도입된 개념이 바로 '프로퍼티(property)'.
프로퍼티 선언문의 구조는 다음과 같다:
@public
으로 놓는 미친 짓을 하는 게 아니라면 접근자와 변경자를 써야 한다.하지만 문제는 언제나 귀차니즘. 언제나
-(int)getInt { return anInteger;
}나 -(void)setInt:(int)newInt { anInteger = newInt;
} 같은 중복 코드를 작성하는 건 귀찮고 따분한 일이 아닐 수 없다.그래서 옵씨 2.0부터 새로이 도입된 개념이 바로 '프로퍼티(property)'.
@property
명령을 사용하면- 인자로 요청된 인스턴스 변수가 선언되지 않았을 경우 선언. (이 경우 해당 인스턴스 변수의 가시성은 정의되지 않는다. 그냥 이 변수는 존재하지 않는다고 생각하라.)
- 접근성 설정.
- (인스턴스 변수가 객체를 담는 경우) 메모리 관리 정책 설정.
- 접근자와 변경자 자동 선언 및 정의(구현).
프로퍼티 선언문의 구조는 다음과 같다:
@property( 쉼표로 구분된 프로퍼티 설정 목록 ) 변수형 변수이름 ;그리고 프로퍼티 선언문은 메서드 선언부에 같이 들어간다.
쉼표로 구분된 프로퍼티 설정 목록에는 다음 다섯 종류의 선택자가 들어갈 수 있다:
- 접근자 메서드 이름
- 형식:
getter = accessorName
- 이 선택자가 없으면 기본 접근자 이름은 인스턴스 변수 이름과 똑같다.
- 예를 들어, 인스턴스 변수 이름이 각각
name
과age
인 경우, 기본 접근자 이름 역시name
과age
이다.
- 예를 들어, 인스턴스 변수 이름이 각각
- 형식:
- 변형자 메서드 이름
- 형식:
setter = mutatorName
- 이 선택자가 없으면 기본 변형자 이름은
set변수명
의 형태가 된다.
- 예를 들어, 인스턴스 변수 이름이 각각
name
과age
일 경우, 기본 변형자 이름은setName
,setAge
가 된다. 자동 캐멀 케이싱에 주의할 것.
- 예를 들어, 인스턴스 변수 이름이 각각
- 형식:
- 접근성
readonly
: 접근자만 자동으로 생성되고 변형자는 생성되지 않는다. 이 경우, 아래 메모리 관리 정책은 쓸모가 없다.
readwrite
: 접근자와 변형자가 자동으로 생성된다.
- 위 두 선택자는 상호배타적(mutually exclusive)으로, 반드시 둘 중 하나만 들어가야 한다. 생략하면 기본적으로
readwrite
이다.
- 메모리 관리 정책
assign
: 변형자는 인자로 받은 값을 인스턴스 변수에 단순 대입한다. 인자로 원시형을 받는 경우에는 반드시 이 선택자를 써야 한다. 인자로 객체를 받는 경우, 이 객체의 생명주기에 전혀 간섭할 수 없다.
retain
,strong
: 변형자의 인자로 받은 객체에 대해 강한 소유권을 행사한다. 원시형에는 이 선택자를 적용할 수 없다. 이 변수가 어떠한 객체를 들고있는 동안 해당 객체는 절대 소멸되지 않는다.
copy
: 변형자의 인자로 받은 객체를 복제하여 사본 객체를 인스턴스 변수에 넣는다. 원시형에는 이 선택자를 적용할 수 없다. 사본 객체에 대해서는 강한 소유권을 행사한다. 원본 객체의 생명주기에는 간섭하지 않는다.
weak
: 변형자의 인자로 받은 객체에 대해 약한 소유권을 행사한다. 원시형에는 이 선택자를 적용할 수 없다. 객체는 강한 소유권을 행사하는 주체가 없어지는 즉시 소멸되는데, 이 때 약한 소유권을 행사하는 주체는 모두nil
로 재설정된다.
- 위 세 선택자는 상호배타적(mutually exclusive)으로, 반드시 셋 중 하나만 들어가야 한다. 생략하면 기본적으로
assign
이다.
- 원자성(atomicity)
nonatomic
: 접근자/변형자가 인스턴스 변수에 접근할 때 잠금(lock)을 사용하지 않도록 한다. 다중 스레드 환경에서 사용할 때는 주의하여야 한다. 다만, 읽기 전용(readonly) 프로퍼티에 적용하면 소소하게나마 성능 향상을 꾀할 수 있다.
- 주의:
atomic
선택자는 없다. 그냥nonatomic
을 쓰지 않으면 원자적으로(== lock 사용) 동작한다.
@interface Developer: Employee <CoffeeConsuming> { @private NSInteger employeeSince; BOOL fired; } // NSString *형의 name. // 생성되는 접근자: -(NSString *)name;. // 생성되는 변형자: 생성 안 됨. // 인스턴스 변수 자동 생성 여부: 생성됨. // 원자성: 원자적이지 않음. @property(readonly, nonatomic) NSString *name; // NSInteger형의 employeeSince. // 생성되는 접근자: -(NSInteger)employeeSince;. // 생성되는 변형자: 생성 안 됨. // 인스턴스 변수 자동 생성 여부: 생성 안 됨. // 원자성: 원자적임. @property(readonly) NSInteger employeeSince; // BOOL형의 fired. // 생성되는 접근자: -(BOOL)isFired;. // 생성되는 변형자: -(void)setFired:(BOOL)fireHim;. // 인스턴스 변수 자동 생성 여부: 생성 안 됨. // 메모리 관리 정책: 단순 대입. // 원자성: 원자적이지 않음. @property(readwrite, nonatomic, getter = isFired) BOOL fired; // NSArray *형의 thingsToDo. // 생성되는 접근자: -(NSArray *)thingsToDo;. // 생성되는 변형자: -(void)assignThingsToDo:(NSArray *)jobs;. // 인스턴스 변수 자동 생성 여부: 생성됨. // 메모리 관리 정책: 복제. // 원자성: 원자적임. @property(readwrite, copy, setter = assignThingsToDo) NSArray *thingsToDo; @end
2.2.1. 클래스 정의 ¶
클래스를 선언했으면 정의도 해야 할 것이다. 다음과 같이 정의한다:
@implementation 클래스 이름역시나
@end
명령으로 @implementation
의 영역을 닫는다.@implementation EmptyClass // The Beauty of Emptiness. @end
2.2.2. 메서드 정의 ¶
메서드 정의는 클래스 정의 내부에 들어간다. 정의할 때는 선언할 때처럼 메서드 이름과 인자 전체를 적고 세미콜론으로 마감하는 것이 아니라 중괄호를 열고 닫아 메서드 본체를 정의한다. 이해를 위해 예제로 설명하겠다. 윗부분 메서드 선언의 예제를 재활용하겠다:
@implementation ExampleClass +(char *)iTakeNoParameter { return "asdf"; } +(id)allocWithZone:(NSZone *)aZone { // Vendor specific implementation. } -(NSInteger)numberOfCharacter:(unichar)ch inString:(NSString *)str { NSUInteger j = [str length], k = 0; unichar *asdf = (unichar *)malloc(sizeof(unichar) * j); if(NULL == asdf) return -1; [str getCharacters:asdf range:NSRangeMake(0, j)]; for(NSUInteger i = 0; i < j; ++i) { if(ch == asdf[i]) ++k; } return k; } -(void)drawRectangleOfWidth:(CGFloat)w height:(CGFloat)h upperLeftX:(CGFloat)x upperLeftY:(CGFloat)y { // implementation may vary. } @end
2.2.3. 인스턴스 변수 선언: 제2부 ¶
아까 위에서 말했듯, 클래스 선언부에 인스턴스 변수 선언을 같이 적는 것은 쓸데없는 상세 구현을 외부로 노출시키는 것이라 옵씨 고수들은 싫어한다. 따라서 애플은 옵씨 2.1 이후의 어떤 업데이트에서 클래스 구현에 인스턴스 변수 선언을 할 수 있도록 허용했다. 다음과 같이 한다:
@implementation ExampleClass { // 인스턴스 변수 선언 시작 int protected; // 역시 기본 가시성은 protected이다. 하지만....... @public char inaccessibleFromOutside; // @public 변수가 구현부에 숨어있어서, 외부에서 절대 접근이 불가능하다. @protected long protectedButInaccessibleFromChilds; // @protected 변수가 구현부에 숨어있어서, 상속하는 클래스는 접근할 수 없다. @private unsigned long long private; } // 인스턴스 변수 선언 끝 @end