자동 메모리 관리

2장에서 살펴본 메모리 관리는 문제가 자주 발생하는 곳이라 모든 개발자에게 중요하다.
메모리에 만들어질 때부터 사라질 때까지, 객체의 내부 속성은 계속해서 변화한다.

이전에는 컴파일러가 실행시점에 발생할 메모리 관련 문제들을 정적 분석 기법으로 경고만 주었다.
하지만 요즘엔 정적 분석 기법을 발전시켜 자동으로 메모리 관리를 도와주는 'ARC' 라는 새로운 메모리 관리 방식을 제공한다.

ARC (자동 참조 계산)

애플은 2010년에 XCode4를 소개하며 LLVM 컴파일러 C언어 계열 프런트엔드 Clang을 공개했다.
Clang은 C언어와 Objective-C 소스코드를 컴파일하기 위해 만들어졌지만, 부가적으로 정적 분석 기능도 포함했다.
XCode에서 정적 분석을 실행하면, Clang에서 소스를 분석하고 XML 파일로 저장하고, 실행시점에 발생할 수 있는 이슈를 시각적으로 보여준다.

그리고 2011년 WWDC에서 Objective-C 객체에 대한 자동 메모리 관리 방식인 ARC를 소개했다.

수동 참조 계산 방식과 비교 (vs MRC)

ARC를 사용해서 자동으로 메모리를 관리한다고해서 2장에서 알아본 참조 계산 방식이 바뀐 것은 아니다.

ARC에서도 여전히 객체마다 참조 횟수(Reference Count)가 존재하고, 객체 소유권에 대한 동일한 규칙을 기준으로 참조 계산을 진행한다.
MRC는 객체를 생성하면서 소유권을 가지며, 특정 객체를 참조하기 전에 소유권을 요청하고, 참조한 이후에는 소유권을 반환한다.
ARC에서도 참조 계산을 위한 규칙과 방식을 그대로 적용한다.

ARC 규칙

ARC 기준으로 새로운 규칙을 알아보자.
ARC에서는 MRC에서 쓰는 retain, release 메서드를 보내는 코드가 필요없다.
컴파일러가 컴파일동안 객체 인스턴스 별로 생명주기를 분석해서 자동으로 retain, release 메세지를 보내는 코드를 채워주기 떄문이다.

규칙
  1. 메모리 관리 메서드를 구현하지 말라
  2. 객체 생성을 위한 메서드 이름 규칙을 따르라
  3. C 구조체 내부에 객체 포인터를 넣지말라
  4. id 와 void* 타입을 명시적으로 타입 변환하라
  5. NSAutoreleasePool 대신 @autoreleasepool 블록을 사용하라
  6. 메모리 지역(zone)을 사용하지 말라

메모리 관리 메서드를 구현하지 말라

ARC 에서는 retain, release, retainCount, autorelease, dealloc 메서드를 구현해서도 안되며, 호출해서도 안된다.
객체 인스턴스를 명시적으로 소유하거나 직접 해제할 필요가 없다는 얘기다.

dealloc 메서드에서 옵저버를 제거한다거나, 다른 동작을 해야한다면 -> super.dealloc() 코드를 넣지 말아야 한다.

객체 생성을 위한 메서드 이름 규칙을 따르라

ARC 기반으로 객체를 생성할 때, +alloc 메서드를 주로 사용한다.
init으로 시작하는 인스턴스 메서드는 특별하게 +alloc 메서드로 생성한 객체를 초기화해서 반환하는 용도로 사용해야만 한다.

C구조체 내부에 객체 포인터를 넣지마라

C 언어세ㅓ 사용하는 struct나 union 내부에 Objective-C 객체 포인터를 넣으면 ARC에서 메모리 관리가 불가능하여 컴파일 오류가 발생한다.

struct ArrayWrapper{
    NSMutableArray* array; // 구조체에서 Objective-C 객체 사용하기 오류
}

ARC 기반에서는 컴파일러가 객체 생명주기를 추적할 수 있어야 하는데, C 구조체 내부에 있는 객체 포인터는 컴파일러가 관리 불가능

id와 void* 타입을 명시적으로 타입 변환하라

기존의 Objective-C 에서는 id타입과 void* 타입을 내부에서 당연하게 같은 타입으로 인식하며 사용하였다.
ARC에서는 객체 생명주기를 관리하기 위해서 타입 변환할 때는 명시적으로 타입 변환 연산자를 사용해야만 한다.

NSAutoreleasePool 대신 @autoreleasepool 블록을 사용하라

ARC 환경에서는 블록이 끝나고 범위를 벗어날 때, 해당 pool에 소유권이 있는 객체를 자동으로 해제한다.
만약 NSAutoreleasePool 객체를 사용하려고 하면 컴파일러가 오류를 표시할 것이다.

메모리 지역(zone)을 사용하지 말라

2장에서 설명한 것처럼 런타임 구조가 변경되면서 zone은 더이상 사용되지 않는다.

소유권 수식어

  • __strong
  • __weak
  • __unsafe_unretained
  • __autolereasing

__string

아무 수식어가 없을 때, 사용되는 Default 수식어이다.

해당 객체의 포인터를 (소유권을 갖고) 강하게 참조하고 있으므로 객체가 '살아있다'라는 뜻이다.
범위내에서 객체를 생성해서 소유권을 갖고 있다가도 범위를 벗어날 경우에는 release가 없어도 소유권을 반환한다.

{
    NSString* __strong str = [[NSString alloc] init];
}

__weak

객체가 살아있다는 것을 보장하지 않는 약한 참조
해당 객체를 참조하는 곳이 없으면 객체는 즉시 사라지고 포인터는 nil이 되어버린다.

__autoreleasing

함수에서 객체를 전달하는 경우에 객체가 사랒지ㅣ 않도록 하기 위해서, 약한 참조 대신 강한 참조를 써야만 하는가?
-> 코코아 프레임워크 내부에서 만든 객체를 넘겨받을 때는 이 지시어를 사용해서 자동 해제될 대상이라고 명시하여 전달한다.

__unsafe_unretained

weak과 마찬가지로 소유권을 갖지 않는 참조 관계는 비슷하다.
하지만 객체가 사라지면 nil로 바꿔주지 않고, 메모리 관리를 하지 않아 안전하지도 않다.
대부분 weak를 사용하는 것이 안전하다.

타입 연결

코코아 프레임워크 내부에는 C언어로 만들어진 코어 파운데이션 프레임워크가 있다.
NSArray나 NSString 같은 Objective-C 로 만든 객체도 내부 구현 코드는 코어 파운데이션 C 구조체를 사용한다.
그래서 코어 파운데이션에 있는 CFArrayRef나 CFString 구조체는 Objective-C 객체 포인터로 타입 연결할 수 있고, 반대로도 가능하다.
-> 이런 타입 연결은 추가적인 비용이 발생하지 않는다고 하여 '무비용 연결' 이라 부른다.

코어 그래픽스처럼 C 언어 수준 API를 사용하는 경우 -> C 구조체 포인터를 사용할 수 밖에 없다.
이 경우에는 메모리 관리가 자동으로 이뤄지지 않아, 개발자가 직접 CFRetain(), CFRelease()로 관리해야 한다.

Obejctive-C 객체 - 코어 파운데이션 구조체 연결 방법

  1. Objective-C 런타임에 구현되어 있는 객체 소유권 수식어 사용
  2. 코어 파운데이션 스타일의 매크로 사용
__bridge 방식

객체의 소유권을 넘기지 않고, 타입 연결만 하는 경우에 사용한다.
-> refCount 증감 X

__bridge_retained or CFBridgingRetain 방식

연결하면서 소유권도 주는 경우에 사용
소유권을 주기 때문에, refCount 증감
끝나면 소유권 반환해야 함

__bridge_transfer or CFBridgingRelease 방식

연결하면서 소유권을 넘긴다

무비용 연결 타입

무비용 가능 목록을 확인하여 연결이 가능하다.

프로퍼티와 인스턴스 변수

클래스의 프로퍼티를 선언할 때, 속성으로 지정하는 수식어와 ARC 소유권 수식어는 밀접한 관계를 가진다.
특히 인스턴스 변수를 미리 선언하는 경우, 인스턴스 변수의 소유권 수식어를 프로퍼티 속성과 동일하게 맞춰야만 한다.
그렇지 않으면 컴파일 오류가 발생한다.

요약

ARC를 사용하면 메모리를 자동으로 관리해주기 때문에 편리하다.
하지만 메모리 관리 코드를 개발자가 직접 작성하지 않을 뿐이지, 내부에서 동작하는 방식은 이해해야 한다.

코어 파운데이션 C 구조체를 사용하지 않고 Objective-C 객체만으로 개발할 수 있는 부분이 많아졌지만, 소유권과 타입 연결에 대한 문제는 개발 과정에 이슈가 있다.
특히 Swift와 C/C++ 코드를 연결하기 위해서는 Objective-C 객체로 포장해야 하는 경우가 있다.


ARC 구현 방식

ARC와 관련된 런타임 함수는 새로운 OS 버전이 나올때마다 구조가 바뀌고 성능이 개선된다.

강한 참조

NSString __strong *aString = [[NSString alloc] init]; 

위처럼 strong 변수를 선언했을 때, 컴파일러가 변환한 코드는 아래와 같다.

id tmp = objc_msgSend(NSString, @selector(alloc)); 
objc_msgSend(tmp, @selector(init));
NSString* aString;
objc_storeStrong(&aString, tmp);

alloc과 init을 처리하기 위해 objc_msgSend를 한번씩 호출한다.
마지막 부분에서 앞서 만든 객체 인스턴스(tmp)를 aString 포인터에 강한 참조로 저장하기 위해서 objc_storeStrong() 함수를 호출한다.

objc_storeString()의 구현을 아래와 같다.

void objc_storeStrong(id *location, id obj) { 
    id prev = *location; 
    if (obj == prev) { 
        return; 
    } 
    objc_retain(obj); 
    *location = obj; 
    objc_release(prev); 
}

자동 반환용 리턴 값

objective-C 에서는 '두단계 초기화 패턴'으로 객체 인스턴스를 만든다.

  1. 객체 인스턴스를 힙 공간에 생성 -> alloc
  2. 할당된 메모리 공간을 초기화를 통해 값을 채워넣음 -> init

객체 생성 메서드 중에 '두 단계 초기화 패턴'을 한번에 처리해주는 간편한 메서드로 객체를 만드는 경우에는 만들어진 객체가 자동 해제 대상이다.

NSDictionary __strong *dictionary = [NSDictionary dictionary]; 

위처럼 간편한 메서드로 객체 생성할 경우

id tmp = objc_msgSend(NSDictionary, @selector(dictionary)); 
objc_retainAutoreleasedReturnValue(tmp);
NSDictionary *dictionary;
objc_storeStrong(&dictionary, tmp); 

이와 같이 변환된다.

objc_retainAutoreleasedReturnValue() 함수를 사용해서 객체를 AutoReleasePool에 등록하고, 등록된 객체를 반환받아 그 객체에 대해 소유권을 갖는다.

항상 retain을 사용하지는 않고, 해당 객체가 생성됐는지 확인하기 위해서 쓰레드 TLS 영역에 정보를 저장하는 최적화 루틴을 포함한다.

약한 참조

{
    NSString __weak *aString = [[NSString alloc] init]; 
}

위와 같은 코드를 컴파일러가 변환하면

d tmp = objc_msgSend(NSString, @selector(alloc)); 
objc_msgSend(tmp, @selector(init));
NSString* aString;
objc_initWeak(&aString, tmp); 
objc_release(tmp);
objc_destroyWeak(&aString); 

로 변환된다.

objc_initWeak()는 다음과 같이 구현되어 있다.

id objc_initWeak(id *addr, id val) { 
    *addr = 0;    
    if (!val) return nil;    
    return objc_storeWeak(addr, val); 
}

objc_storeWeak(addr, val)은 약한 참조 목록을 저장하는 일종의 해시 테이블을 구현하고 있는데, 이곳에 ㅁddr 포인터에 있던 이적 객체에 대한 약한 참조는 해지하고 val 객체에 대한 약한 참조를 등록한다.

objc_destroyWeak는 다음과 같이 구현되어 있다.

void objc_destroyWeak(id *addr) { 
    if (!*addr) return; 
    return objc_destroyWeak_slow(addr); 
}

objc_destroyWeak_slow() 함수는 objc_storeWeak() 함수로 등록한 약한 참조 목록에 대한 해시 테이블에서 해당 객체의 약한 참조를 해지한다.

약한 참조 불가능한 객체

allowsWeakReference 메서드(objc_storeWeak() 함수 내부에서 사용됨)의 리턴값이 NO이면, 메모리가 중복 해제됐다고 가정하여 에러를 표시

retainWeakReference 메서드(objc_loadWeak() 함수 내버에서 사용됨)가 구현되어 있지 않거나, NO를 반환하면 마찬가지고 약한 참조가 불가능

자동 반환 방식

객체 참조 변수에 autoreleasing 소유권 수식어를 명시적으로 지정하는 자동 반환 방식을 사용할 수 있다.

@autoreleasepool {
    NSDictionary __autoreleasing *dictionary = [[NSDictionary alloc] init];
}

해당 코드를 컴파일러가 변환하면

id pool = objc_autoreleasePoolPush();
id tmp = objc_msgSend(NSDictionary, @selector(alloc)); 
objc_msgSend(tmp, @selector(init));
NSDictionary *dictionary = tmp; 
objc_autorelease(dictionary); 
objc_autoreleasePoolPop(pool);

요약

ARC 구현 방식을 알아보기 위해 Objective-C 런타임 API 동작 방식까지 살펴봤다.
강한 참조로 객체 소유권을 갖고 처리하는 방식부터 약한 참조나 자동 반환 참조 구현 방식까지 이해하면, ARC 환경에서 나타나는 메모리 문제를 해결하는 데에 도움을 줄 것이다.

ARC 환경에서도 객체 인스턴스에 대한 메모리 관리는 신경 써야만 한다.

Objectibe-C 기반으로 만든 맥용 Framework : Cocoa Framework

이후에 iOS용으로 나온 Framework는 터치 이벤트가 추가되었다고 하여 Cocoa Touch Framework라고 한다.

 

Objective-C를 포함한, 모든 OOP에서는 프로그램을 구성하는 단위를 Object 단위로 구분한다.

그렇다면 '객체'라는 단어는 "어떤 의미인지", "왜 주체는 없고 객체만 있는지" 에 대한 궁금증에서 시작하자.

1장에서는 Class와 인스턴스의 개념을 설명하고, 객체의 정체성, 등가성, 예외성을 확인한다.

그리고 객체 사이의 관계를 살펴보고, 마지막으로 Objective-C 런타임 구조에 대해 설명한다.

 

클래스와 객체 인스턴스

OOP는 절차 중심 프로그래밍과 대비되며, 객체 안에 속성 메서드 형태로 변수와 함수를 구현하고 프로그램을 구성하는 객체들끼리 메세지를 주고 받아 협력하는 형태로 동작한다.

 

첫번째 OOP 언어인 '시뮬라' 이후로, 대부분의 OOP에서는 객체, 클래스 개념을 사용한다.

문제 해결을 위해 추상화한 코드 = 클래스

실제로 프로그램을 실행하는 시점에, 메모리에 구체화된 실체 = 인스턴스

 

 

객체에 대한 철학

객체라는 단어는 주체(나)를 중심으로 하는 1인칭 시점/서양 철학에서 비롯됐다.

객체를 다루는 방식 : "자연에 실존하는 객체를 그에 대응되는 형체가 없는 개념과 언어로 추상화시켜 생각하는 방식"

 

객체 중심 프로그래밍(OOP)

랑그(langue) : 같은 언어를 사용하는 사람들끼리 생각하는 방식에 대한 원칙(단어의 의미, 문법)

파롤(parole) : 실제 대화나 상황에 따라서 표현이나 발음이 달라지는 것

 

랑그의 예 : 스택(Stack) - 위로 쌓고, 위에서부터 차례대로 꺼내는 원칙/방식

파롤의 예 : 이런 '스택' 개념을 각각 다른 언어로 구현

 

이전에 '프로그램 코드가 동작하는 순서'를 중요하게 생각했던 절차 중심 프로그래밍 패러다임과

'실생활에서 사고하는 방식과 비슷한 객체 개념을 차용하여, 객체의 역할과 책임 그리고 관계를 생각하는' 객체 중심 프로그래밍 패러다임을 생각하는 방식에서부터 차이가 있다.

 

프로그래밍 언어로 코드를 작성하는 방법과 객체 중심 패러다임을 알더라도, 클래스 단위로 코드를 표현하기에는 여러 어려움이 있다.

-> 이러한 어려움을 해결하고자 하는 원칙이 바로 "SOLID"

 

SOLID

  • 변경에 유연
  • 이해하기 쉬움
    • SOLID에 대한 지식이 있어야 -> 어렵고 사람마다 알고있는 개념이 약간씩 다르다
  • 명확/깔끔한 책임 구조, 높은 응집력, 낮은 의존성, 유지보수성 등의 장점

 

SRP : https://wlgusdn700.tistory.com/95?category=930441

  • 하나의 모듈은 하나의 일(책임)을 맡아야 한다.

OCP : https://wlgusdn700.tistory.com/96?category=930441 

  • 확장에는 열려있어야 하고, 변경에는 닫혀있어야 한다

LSP : https://wlgusdn700.tistory.com/97?category=930441

  • Sub Class, Struct는 Super Class 혹은 Protocol 로 교체할 수 있어야 한다.

ISP : https://wlgusdn700.tistory.com/98?category=930441 

  • 인터페이스(프로토콜)을 통해 기능/책임을 더 세분화, 사용하지 않는 책임에 의존관계 X

DIP : https://wlgusdn700.tistory.com/99?category=930441 

  • 고차원 모듈은 저차원 모듈에 의존하면 안된다.(추상적인 것은 구체적인 것에 의존하면 안된다)
  • 자주 변경되는 클래스에 의존하지 말자
  • OCP와 비슷한데, 인터페이스(프로토콜)을 통함

Objective-C 객체

+ 로 시작하는 메서드 : 클래스 메서드

- 로 시작하는 메서드 : 인스턴스 메서드

 

헤더 파일 내부 인터페이스(@interface) 영역에 선언한 메서드만 객체 외부에서 접근 가능

구현 영역(@implementation)에 선언한 메서드는 내부에서만 접근할 수 있고, 외부로는 감춰짐(캡슐화)

 

클래스 명세와 객체 인스턴스

 

출처 : Cocoa Internals

  1. aPen이라는 포인터는 aPen 고유의 객체 인스턴스를 가르킨다.
  2. aPen 고유의 객체 인스턴스는 Pen 클래스의 코드를 공유한다.

 

Objective-C 2.0 이후 변화

Class는 내부적으로 objc_Class 라는 구조체의 포인터이다

 

OS X 10.5 와 iOS 2.0 이후에 적용된 Objective-C 2.0부터는 최신 런타임 구조를 따른다

레거시 런타임 : 클래스의 구조가 바뀌면, 무조건 새로 컴파일

최신 런타임 : 인스턴스 변수와 메서드 변경해도, 재컴파일 X

최신 런타임 적용 이후, isa 포인터 사라짐

따라서 객체의 클래스를 알아내기 위해서, NSObject에 선언된 -(Class)class 메세지를 보낸다

 

Swift Native Object

스위프트에서는 Objective-C와 호환되는 객체를 쓸수도 있고, 네이티브 객체를 쓸수도 있다.

스위프트 언어는 SIL이라는 스위프트 중간 언어를 거쳐가면서 기계코드로 변형된다.

 

 

Swift ~ Objective-C 호환 객체

NSObject를 최상위 클래스로 지정해야 한다.

NSObject를 상속받은 Class로 생성한 객체는 내부에서 Objective-C로 자동으로 변환되고, Objective-C 런타임에서 동작한다.

 

 

요약

객체를 표현하기 위해서는 객체 중심으로 생각하는 과정이 중요

객체는 늘 객관적이어야 함

객체를 표현한 코드는 '나 혼자만의 것이 아님'

 

 


 

객체 정체성과 등가성

객체들은 필요한 시점에 객체 인스턴스가 만들어진다

어떤 클래는 인스턴스 없이 자체로 존재한다

 

Objective-C 객체와 메모리 구조

Text Segment : 프로그램 코드

Data Segment : 고정 값이 정해진 전역변수

BSS Segment : 초기값을 0으로 할당하는 전역변수

 

Objective-C에서 객체인스턴스는 항상 HEAP 에 만들어지며, 해당 힙 메모리 주소를 Stack 영역에 할당한 포인터로 참조해서 접근한다.

 

"포인터 변수에 담긴 메모리 주소"와 "해당 주소의 객체 인스턴스가 실제로 유효한지"를 포인터 주소만으로는 판단할 수 없다.

객체 포인터 변수는 이미 해제된 객체 주소를 저장하고 있는 Dangling Pointer 일 수도 있기 때문

Objective-C에서의 객체 생성 방법

  1. +new 하나만 호출하는 방법
    1. new = alloc + init
  2. +alloc 메서드와 -init 메서드를 두 단계에 걸쳐서 호출하는 방법

객체 정체성

객체 인스턴스가 HEAP 영역에 만들어지면서, 객체는 각각 고유한 메모리 영역을 차지한다.

Pen *aPen = [Pen new];

Pen *bPen = [Pen new];

생성한 2개의 펜은 서로 다른 객체이며, 고유한 정체성(메모리 구조)를 갖는다

동일한 객체 정체성

만약, bPen = aPen; 을 했다고 상상해보자.

메모리 그림은 변경될 것이다

또한, 같은 메모리 주소를 가르키기 때문에, aPen == bPen 도 true다

이때, bPen의 인스턴스를 차지하는 메모리 주소는 어떻게 될까 -> 2장 '메모리 관리'에서 살펴본다

 

객체 등가성

객체 인스턴스는 각각 고유한 메모리 영역을 차지하기 때문에, 동일한 속성에 대해서도 각자의 메모리 영역에 데이터를 보관한다.

 

만약 cPen이라는 인스턴스를 생성하고 aPen과 같은 color, position을 설정했다고 가정하자.

-> aPen과 cPen은 내부 속성이 모두 같기 때문에, 객체 등가성(Equality)를 갖는다

-> 등가성을 비교할 때는 aPen == cPen 비교문은 성립하지 않는다.

-> 대신 해당 Class에 -isEqual: 메서드를 오버라이드해서 모든 속성이 동일한지를 비교한다.

-> 특히 NSString 계열 클래스는 -isEqualToString: 메서드를 사용해서 동일한 문자열인지 비교하기를 권장한다.

 

객체 예외성

모든 코코아 객체 인스턴스가 Heap 영역에 생성되는 것은 아니다

특이하게 Heap 이 아니라 Text와 Data 영역에 생기는 경우가 있다.

 

NSString* aPenName = @"BluePen";

NSString* bPenName = @"BluePen";

 

NSString 클래스는 NSObject를 상속받는 코코아 클래스 중 유일하게 전역 변수로 선언할 수 있다

Heap 영역이 아닌, Text 영역에 "BluePen" 값을 저장하고, aPenName 변수는 전역 변수 형태로 Data 영역에 만들어진다.

 

더 특이한 점은 bPenName 객체처럼 aPenName 변수와 동일한 문자열 "BluePen"을 반복해서 사용하는 경우,

같은 Text 영역을 사용하고 bPenName 객체 인스턴스를 전역 변수 형태로 할당한다는 것이다.

다시 말해 aPenName 과 bPenName 은 동일한 정체성(같은 메모리 주소)을 갖게 된다. 

-> 이런 방식을 문자열 인터닝(string interning)이라 한다

 

Swift 문자열

스위프트에서 String 객체는 네이티브 문자열 객체를 만들 수 있고, NSString을 연결해서 쓸 수도 있다.

네이티브 문자열 객체는 내부적으로 인터닝을 시키는 NSString 객체와는 다르게, Text 영역에 있는 문자열을 OpaquePointer 형태 포인터 그대로 연결하는 방식을 사용한다.

-> 따라서, 네이티브 문자열이 NSString보다 조금 더 가볍다고 할 수 있다.

 

-hash 메서드

앞에서 말한 것처럼, -isEqual: 메서드를 재구현한 경우라면, 반드시 -hash 메서드도 다시 구현해야 한다.

왜냐하면 NSDictionary 같은 Collection 객체는 -isEqual: 메서드 대신 -hash를 사용하기 때문이다

최상위 객체인 NSObject에 기본적으로 구현된 -hash 메서드는 객체 정체성 기준이 되는 self 메모리 포인터 값을 NSUInteger 타입 숫자로 바꿔줄 뿐이다.

따라서, 객체 정체성이 다르지만 등가성이 성립하는 경우를 위해, -hash 메서드 결과 값도 고유한 값이어야만 한다.

 

Swift Hashable Protocol

스위프트에서는 모든 타입에 Hashable 프로토콜을 구현해야 한다.

Hashable Protocol은 앞서 설명한 -hash와 -isEqual 메서드에 해당하는 hashValue() 함수와 ==() 함수가 필수적으로 구현해야 하는 함수로 지정되어 있다.

 

요약

모든 객체는 메모리에 자리를 잡으면 고유한 정체성을 갖게 된다.

고유한 객체 중, 모든 속성이 같게 되는 등가성 관계도 생길 수 있다.

등가성, 정체성을 비교하기 위해서는 위에서 말한 메서드들을 재정의 해야 한다.

 


객체 사이 관계

메타 클래스

앞서 객체 인스턴스마다 isa Pointer가 있어서 해당 객체의 Class를 알 수 있다.

Pen Class는 인스턴스 메서드 목록과 코드를 갖고 있으며, Pen Class의 Meta Class는 클래스 메서드 목록과 코드를 갖고 있다.

 

상속

OOP 언어가 갖는 특징은 

  1. 추상화한 클래스 명세
  2. 객체 인스턴스의 활용
  3. 캡슐화
  4. 상속
  5. 다형성

으로 요약할 수 있다.

이 중, 객체 사이 관계와 가장 밀접한 특징을 상속이다.

상속받은 모든 Class들은 super를 따라가다 보면 상위 Class가 나온다.

Root Class의 super는 nil 이다

보다시피 Meta Class도 super 관계가 존재한다.

 

 

is-a, has-a 관계

aPen is-a Pen

Pen is-a NSObject

표현들은 true이다.

 

is-a : 인스턴스와 클래스의 관계(클래스와 Meta 클래스의 관계 포함) -> isa 사용

has-a : Sub/Super 클래스의 관계 -> super 사용

 

has-a 관계는 강한 참조 결합성을 갖는 구성 관계(composition)

약한 참조 결합설을 갖는 집합 관계(aggregation)으로 구분한다

 

  • 구성 관계
    • 참조하는 객체(referrer)가 사라질 때, 하위 객체들도 같이 사라짐
    • 참조하는 객체와 하위 객체가 동일한 생명주기를 갖는다
  • 집합 관계
    • 참조하는 객체가 사라지더라도, 하위 객체는 사라지지 않는다

자세한 것은 3.1 (ARC) 장에서 알아본다.

 

요약

상속 관계는 객체 사이 관계를 바꾸기 위한 유지보수나 리팩터링이 어려운 밀결합 형태이다.

따라서, 객체를 상속해서 확장하는 방식보다는 카테고리로 객체를 확장하도록 권하고 있다

자세한 내용은 7.3.1 팩토리 추상화 패턴 장을 참조

 

 


Objective-C 런타임

앞서 설명한 클래스와 메타 클래스를 메모리에 로딩하는 것은 Objective-C 런타임이 담당한다.

런타임 : 1. 실행 중, 객체에게 보내는 메세지를 처리한 메서드를 찾거나, 2. 객체 메모리 관리, 동적 타입 변환 등을 수행하는 C 함수 라이브러리

 

기존 런타임과 최신 런타임

기존 32bit 방식에서 동작하는 레거시 런타임

62bit 방식에서 동작하는 최신 런타임 이 존재한다.

 

최신 런타임에서는 Objective-C 2.0에 추가한 프로퍼티, 빠른 탐색, ARC, 블록 기능 을 위한 개선이 추가됐다

 

메세지 디스패치

Objective-C는 객체의 메서드를 직접 호출하지 않고, 객체에 메세지를 보내는 방식으로 동작한다

 

객체에 메세지는 보내는 과정

  1. 클래스에 메서드를 선언
    1. 리턴 값, 메서드명, 인자값 타입, 변수명 을 순서대로 명시
    2. - (void) replacePen:(Pen *) pen1 withPen:(Pen *) pen2;
  2. 객체 인스턴스에서 메서드를 호출하고 싶다면, 인스턴스에게 메세지를 보낸다
    1. [aPenHolder replacePen:apen withPen:bPen];
  3. 컴파일러는 이 코드를 보고 메서드 이름을 replacePen:withPen: 이라고 판단한다.
  4. 컴파일을 하면서 objc_msgSend() 런타임 API를 사용하는 코드로 변경
    1. objc_msgSend(aPenHolder, @selector(replacePen:withPen:), aPen, bPen);
  5. 실행 중 런타임은 objc_msgSend()를 실행하면서 메세지로 어떤 메서드를 실행할 지, 메세지 디스패치 과정을 통해 찾는다

어떤 클래스의 메서드를 실행할 지, Selector를 선택한 후에는 해당 메서드의 메모리 주소를 내부 캐시에 저장

 

 

요약

Objective-C가 가지는 장접들은 모두 런타임에서 발현된다.

또한 런타임 API를 사용하면, 실행 중 클래스나 객체의 구조, 함수를 바꾸는 동작이 가능하다 -> 리플렉션(reflection)

 

+ Recent posts