불변객체와 가변객체

코코아 프레임워크 객체는 크게 2가지로 분류할 수 있다.

  1. 초기화한 이후에는 내부 데이터를 변경할 수 없는 불변 객체
    • 문자열을 다루는 NSString 클래스는 문자열을 바꿀 수 있는 인터페이스가 없는 불변 객체다.
  2. 반대로, 초기화한 이후에도 내부 데이터를 변경할 수 있는 가변 객체

불변 객체

Immutable 객체들은 다음과 같은 특징을 갖는다.

  • 초기화 이후 객체 내부의 값이나 상태가 변하지 않는다.
  • 불변 속성 때문에 여러 객체에서, 여러 스레드에서 참조해도 안전하다.
    • 멀티 스레드 환경에서 같은 객체에 접근하여도 값/상태가 변경되지 않아, Concurrency 문제가 야기되지 않음
  • 값이 바뀌는 상황을 고민하지 않기 때문에, 설계가 쉽고 구현하기 수월하다.
  • 객체 내부에 모순된 상태가 줄어들어 부작용이 적다.

이런 불변 객체의 특징 때문에 FP에서는 불변 객체를 더 많이 쓴다.
심지어 불변 객체만 사용하라고 권장한다.
불변 객체의 사용은 의도하지 않은 부작용 의 가능성을 낮춘다.

불변 객체만을 사용하는 방식으로 구현하면 객체들 사이의 참조 관계가 비교적 단순해진다.
하지만 값이 다르면 새로운 객체를 만들어야한다는 단점이 있다.

불변 객체 인스턴스 개수가 많아지는 경우를 대비해 메모리를 효율적으로 사용하기 위한 최적화 과정을 두기도 한다.
불변 객체 중에 '정체성' 이 동일한 객체가 이미 존재하는지 확인하고, 중복된 불변 객체를 만들지 않도록 최적화한다.
특히, NSString 클래스 리터럴 문자열은 불변 객체로, 프로세스 메모리 영역에 문자열을 할당해서 중복 생성을 줄인다.

불변 객체 클래스

불변 객체는 초기화 메서드로 객체의 초기값을 지정한 이후에는 객체 상태를 변경할 수 있는 메서드를 제공하지 않는다.
대부분 객체 내부의 인스턴스 변수는 감춰지거나(private) 보이더라도 읽기만 가능하다.

코코아 프레임워크에 불변 객체 클래스가 많지만, 자주 사용하는 클래스는 다음과 같다.

  • 타입별 데이터 구조를 다루는 클래스 : NSNumber, NSValue, NSData
  • 규격에 맞춰 데이터를 다루는 클래스 : NSString, NSDate, NSURL
  • 다른 객체를 참조하는 클래스 : NSArray, NSDictionary
  • 다른 객체를 꾸며주는 클래스 : NSFont, NSColor

이 클래스 중 일부는 동일한 역할을 하면서 데이터를 변경할 수 있는 동등한 수준의 가변 객체 클래스(NSMutableString 등)가 존재하기도 한다.
반면에 가변 객체 없이 값만 저장하는 클래스도 있다.

Ex) NSString이나 NSData는 NSMutableString, NSMutableData가 있지만, NSNumber나 NSColors는 가변 객체 클래스가 없다.
만약 가변 객체 클래스가 존재한다면, 객체를 복사할 때 -> -mutableCopy로 가변 객체를 복사할 수 있는지도 확인해야 한다.

불변 객체 구현하기

고려해야 할 사항

  • 초기화 이후 내부 값이나 상태를 재정의하는 메서드가 없어야한다.
  • 내부 전용 인스턴스 변수는 감추고 접근하지 못하도록 한다.
  • 인스턴스 변수들은 상속이 불가능하도록 private 속성을 갖도록 하고, 읽기 전용 접근자만 허용한다.
  • 내부 데이터를 바꾸는 게 아니라 새로운 값을 반환하도록 구현한다.
  • 내부에서만 사용하는 가변 객체가 있다면, 외부에서 내부 가변 객체를 반환하거나 수정할 수 있는 인터페이스가 없어야 한다.

하지만 다음의 경우라면 불변 객체보다는 다른 방법을 고민해봐라.
다른 방법 : 가변 객체로 설계를 바꾸고 변동 가능성을 낮추며 구현하라

  1. 내부 데이터가 너무 커서 복사하기 부담스러운 경우
    • 복사하여서 새로운 값을 반환해야 하기 때문
  2. 초기 생성자에서 모든 값을 정할 수 없고, lazy 혹은 점진적으로 데이터를 정해야 하는 경우
  3. 클래스 내부에 구조체를 포함하고, 그 구조체 내부에 변경가능한 하위 요소가 있는 경우
  4. 상태를 공유하는 공용 컨테이너로 동작하는 경우

요약

불변 객체 클래스를 직접 만들거나 코코아 프레임워크에 있는 불변 객체를 사용하면 참조 관계가 단순해져서 복잡도를 낮출 수 있다.
굳이 가변 객체를 사용할 이유가 없다면, 불변 객체를 사용하자.
부작용이 발생할 수 있는 위험을 줄일 수 있다.
최근에는 블록을 활용한 핸들러 코드나 비동기 프로그래밍 방식에서도 불변 객체가 안전하다.
다중 스레드상에서도 동시에 접근해도 훨씬 안전하다.


가변 객체

가변 객체의 특징

  • 초기화 이후에도 객체 내부 값이나 상태를 추가, 삭제, 변경할 수 있다.
  • 여러 객체나 여러 스레드에서 참조하기 위해서는 Concurrency 문제에 대한 예외 처리가 필요하다
  • 성능 특성을 고려해야 한다
  • 어느 시점이든 값이 변경되는 것에 대한 부작용이 생길 수 있다.

가변 객체는 불변 객체의 정 반대의 특성을 갖고 있다.
내부 데이터를 변경할 수 있기 때문에, 변경하는 값이 유효한지 확인해야 한다.

가변 객체 클래스

불변 객체를 사용하는 것이 장점이 많기는 하지만, 가변 객체를 사용해야 하는 경우도 있다.
객체를 초기화하는 과정에서 1) 모든 데이터를 초기화할 수 없을 수도 있고, 2) 점진적으로 값을 변경해서 최종 값과 상태를 사용 하는 경우도 있다.

코코아 프레임워크가 제공하는 가변 객체 클래스 중 자주 사용하는 클래스는 다음과 같다.

  • 다른 객체를 참조하는 클래스 : NSMutableArray, NSMutableDictionary, NSMutableSet
  • 특정 타입을 집합으로 다루는 클래스 : NSMutableIndexSet, NSMutableCharacterSet
  • 문자열을 다루는 클래스 : NSMutableString, NSMutableAttributedString
  • 특정 데이터 구조를 클래스 : NSMutableData, NSMutableURLRequest

불변 객체와 달리 접두어 다음에 Mutable을 넣어서 불변 객체와 구분하고 있다.
내부에는 값을 추가, 수정, 삭제할 수 있는 메서드들이 존재한다.
ex) -insertObject:atIndex: 등

가변 객체 참조 사례1 : 가변 모델 객체와 뷰 객체

가변 객체를 참조하는 경우는 참조하는 객체 내용이 변경되면 그 변화에 따른 일련의 추가 작업이 필요하다.
코드처럼 가변 객체를 참조하는 경우를 살펴보자
ViewController에는 UITableView가 있어서 모델에 해당하는 PenHolder 내부 펜 목록을 테이블 뷰에 표시하는 코드이다.

@interface ViewController () <UITableViewDataSource>

@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (nonatomic) NSArray* tableItems;

@end

@implementation ViewController

- (void)viewDidLoad{
    [super viewDidLoad];

    PenHolder* penHolder = [[PenHolder alloc] init];
    //penHolder에 값을 넣었다고 가정
    self.tableItems = penHolder.pens;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger) section{
    return self.tableItems.count;
}

//이하 생략

@end
  1. PenHolder.pens에는 이미 20개의 Pen 객체가 가변 배열(NSMutableArray)에 포함되어 있다.
  2. 내부 인스턴스 변수인 tableItems 배열에 penHolder.pens 가변 배열을 참조한다.
  3. -tableView:numberOfRowsInSection: 메서드에서 tableItems.count를 리턴해서 테이블 뷰의 Row를 20개 그린다.

데이터 변경

If) 만약 테이블 뷰가 그려진 이후, penHolder.pens 가변 배열의 데이터가 삭제되거나 새로운 Pen 객체를 추가한다면
TableView는 DataSource가 바뀐 것을 모르고, 여전히 기존 데이터만 보여주고 있을 것이다.
이처럼 가변 객체를 참조하는 경우에는 TableView 갱신 문제처럼, 모델에 바뀐 데이터를 화면에 반영하기 위해 KVO/NSNotificationCenter 같은 옵저버 패턴을 활용할 것이다. 결국 가변 데이터의 흐름을 따라서 컨트롤러와 뷰까지 영향을 주는 코드가 이어지게 된다.

PenHolder 클래스의 pens 가변 배열을 외부에서 직접 바꿀 수 없도록 불변 객체로 만들더라도, 기술적으로 완벽한 불변 객체가 아니다.
왜냐하면 키-값 코딩(KVC)를 사용해서 -setValue:forKey: 같은 메서드로 우회적으로 pens 프로퍼티를 변경할 수 있기 때문이다.

그래서 읽기 전용 프로퍼티로 객체 외부에 노출하기보다 감추는 것이 좋다.
객체를 감추는 방법은 구현부에서 클래스 확장 카테고리로 확장하거나, 내부를 변경하는 인터페이스를 제공하고 인터페이스에서 데이터 흐름에 따라 다른 코드로 진행되게 만들기를 권장한다.

가변 객체 참조 사례 : NSMutableSet와 가변 객체

NSMutableSet은 내부에 여러 타입의 객체를 담을 수 있지만, 동일한 객체 인스턴스를 중복해서 추가하지 못한다.
하지만 가변 객체를 참조할 때, 일시적으로 동일한 객체 인스턴스를 포함할 수 있다.

  1. 가변 집합 set을 만들어서 "unique-key" 문자열 추가
  2. 동일한 내요 문자열을 추가할 수 없다.
  3. 다시 "unique"라는 가변 문자열 객체를 추가하면 값이 동일하지 않기 때문에 정상적으로 추가됨
  4. 3에서 추가한 가변 문자열 객체에 "-key"를 덧붙이면, 실제로는 동일한 내용의 문자열이 set에 존재한다.
  5. 이런 조건에서 가변 집합 set을 복사하면, 복사본을 만들면서 집합 내부 객체를 다시 비교하기 때문에, 동일한 값을 가진 객체는 중복해서 만들어지지 않는다.

-> 4번 집합 ["unique-key", "unique-key"]
-> 5번 집합 ["unique-key"]

결국 5번 집합은 4번집합을 복사한 것이지만, 5번 집합 요소를 보면, 4번 집합과는 다른 집합이 된다.

객체 중복성 검사
NSSet이나 NSDictionary처럼 키 값을 사용하는 컬렉션은 내부적으로 객체 중복성을 검사할 때, 객체의 -hash와 -isEqual 메서드가 중요하다
반면에 정렬한 배열처럼 순서가 중요한 컬렉션은, 순서를 정하기 이ㅜ한 비교 메서드가 중요하다.

요약

가변 객체를 사용하는 경우, 가변 객체의 내부 값이 바뀌기 때문에 생기는 부작용에 대비해야 한다.
그러기 위해서 가변 객체를 변경하는 메서드가 배타적으로 동작해야 한다.
데이터 내용이 바뀌는 시점에 따라 데이터 흐름을 처리하는 코드가 있다면, 변화를 감지하기 위한 디자인 패턴(옵저버)을 적용하는 게 좋다
가변 객체를 참조하는 경우는 의도치 않은 변화에 대비해 동작 방식을 정확하게 이해하고 있어야 한다.

객체 복사

2장 메모리 관리에서 객체 인스턴스를 메모리에 만들고, 객체를 포인터 변수로 참조하면서 소유권을 갖거나, 소유권 없이 참조하는 방식에 대해 설명했다.
3장 자동 메모리 관리에서는 ARC 방식을 기준으로 자동 참조 계산과 참조 방식에 따라 다른 구현 방식에 대해 설명했다.

이번에는 이미 만들어진 객체 인스턴스를 참조하는 경우와 달리, 객체 인스턴스 데이터를 새로운 객체 인스턴스로 복사해야하는 경우 필요한 프로토콜을 살펴본다.
그리고 얕은 복사가 아닌 깊은 복사를 위한 아카이브 방식에 대해 알아보자

NSCopying 계열 프로토콜

코코아 프레임워크에서는 객체를 복사하기 위한 방법으로 NSCopying / NSMutableCopying 프로토콜을 지정해서 구현하는 방법을 권장한다.
NSCopying 프로토콜은 객체를 복사하기 위해 클래스에 미리 구현해야 하는 복사용 메서드 목록을 지정해놓은 프로토콜이다.
애플이 만든 코코아 객체들은 이미 NSCopying 프로토콜을 기반으로 만들어져있어서 객체를 복사하기 쉽다.

복사만 가능한 객체

NSCopying 프로토콜은 구현해야 할 메서드가 딱 하나이다.
내가 만든 객체가 복사가능한 객체여야 한다면, NSCopying 프로토콜 내에 있는 해당 메서드를 구현하면 된다.

-(id)copyWithZone: (NSZone*)zone;

앞서 2장에서 설명했듯 현재는 메모리 영역을 zone으로 나누지 않기때문에, 인자는 nil을 넘겨도 된다.
-> 현재 모든 앱은 단일 존(기본 존)을 갖기 때문

final class AObject: NSObject, NSCopying {
    var num1: Int
    let num2: Int

    init(_ num1: Int, _ num2: Int) {
        self.num1 = num1
        self.num2 = num2
        super.init()
    }

    func copy(with zone: NSZone? = nil) -> Any {
        let newObj: CopyTest = .init(num1, num2)
        return newObj
    }
}

final class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let obj1: CopyTest = .init(0, 1)
        let obj2 = obj1.copy()
        //obj1 과 obj2 는 같은 num1과 num2를 갖고, 서로 다른 주소값을 갖는다 -> 등가성은 갖지만, 동일성은 갖지 않는다.
        obj1.num1 = 100
        //obj1 과 obj2 는 같은 num2를 갖고, 서로 다른 num1 값과 주소값을 갖는다
        print(obj1.num1)
        print((obj2 as? CopyTest)?.num1 ?? 0)
    }
}

위의 코드는 obj1과 obj2가 복사하여도 서로 다른 주소값을 갖는 객체를 가르키고 있기때문에, 얕은 복사이다.

복사와 수정이 가능한 객체

  • 코코아 프레임워크에서 -copy 메서드로 복사하는 객체는 불변(immutable) 객체라고 가정한다.
    • NSString 객체 인스턴스를 복사하면 내부 문자열 데이터를 복사해서 새로운 NSString 객체 인스턴스를 만들지만, 이 문자열 객체 내용은 못 바꿈
  • 만약 수정가능한 가변(mutable) 객체로 복사하려면 -mutableCopy 메서드로 복사해야 한다.
    • 해당 메서드는 NSMutableCopying 프로토콜 내에 존재한다.
  • 객체를 복사하는 과정에서 복사할 원래 객체가 가변인지 불변인지는 상관없다.
    • 새로 만들 객체가 가변인지 불변인지만 중요함

요약

애플 프레임워크에 포함된 클래스는 대부분 NSCopying과 NSMutableCopying 프로토콜을 구현하고 있다.
직접 개발한 클래스도 복사 가능한 객체라면, 애플이 만든 클래스와 마찬가지로 직접 구현을 하면 된다.


얕은 복사(shallow copy) vs 깊은 복사(deep copy)

NSArray처럼 내부에 다른 객체를 포함하는 경우에는 객체를 복사할 때, 주의해야 한다.
참조 포인터 변수를 복사해서 포인터에 있는 힙공간 주소값을 복사한다고해서, 참조하던 객체와 동일한 복사본이 하나 더 생기진 않는다.
참조하는 객체를 가르키는 포인터 변수만 하나 더 생길 뿐이다.
이처럼 주소값만 복사하는 방식을 '얕은 복사' 라고한다.

얕은 복사

@interface PenHolder : NSObject <NSCopying> {
  NSMutableArray *_pens;
}
-(void) addPen:(Pen*)pen;
-(void) removePen:(Pen*)pen;

@end

@implementation PenHolder

- (id)copyWithZone:(NSZone *)zone {
  PenHolder *copiedHolder = [[[self class] alloc] init];
  copiedHolder->_pens = [_pens mutableCopy];
  return copiedHolder;
}

@end

PenHolder 클래스는 Pen 객체를 참조하는 가변 배열 컬렉션 NSMutableArray 객체를 포함한다.
PenHolder 객체를 복사하는 -copyWithZone: 메서드는 새로운 PenHolder 객체 인스턴스 copiedHolder를 만들고, 자신의 _pens 배열을 복사해서 copiedHolder 객체 _pens 변수에 설정한다.

해당 복사는 새로운 객체를 만드는 것이 아닌, 기존의 _pens 배열을 참조하는 copiedHolder를 참조하는 포인터가 생기는 것이다.

하지만 Foundation 프레임워크 내에 있는 모든 클래스는 얕은 복사로 구현되어 있다.

이 상태에서 copiedHolder 인스턴스 내 _pens 집합에 있는 Pen 객체를 수정하면, 기존의 penHolder 인스턴스 내 _pens 집합의 Pen 객체로 같이 변경된다.

깊은 복사

앞서 말한 것처럼 NSArray 계열 컬렉션 클래스의 경우 -initWithArray: CopyItems: 초기화 메서드를 활용해서 생성할 때만 깊은 복사가 가능하다.

만약 객체에서 객체를 포함하고, 또 그 객체에서 다른 객체를 포함한다면 -> DFS 방식으로 하위 노드들부터 탐색해서 새로운 객체를 만들어서 복사하고, 이어서 다른 노드를 탐색하다보면 모든 객체를 복사할 수 있다.

하지만 DFS 방식으로 깊은 복사를 하더라도 객체 참조 관계가 기존 객체와 항상 완벽하게 동일하다고는 할 수 없다.
-> 탐색을 하다 보면 어떤 객체는 중간에 여러 객체에서 여러번 참조될 수도 있고, 특정 객체들은 순환 참조 문제가 있을 수도 있다.
-> 약한 참조를 갖고 있으면 해당 객체를 복사하고 약한 참조로 지정해주어야 한다.

따라서 복잡한 참조 관계를 가진 객체를 복사하는 경우에는 객체 참조 그래프를 활용하는 것이 좋다

요약

코코아 프레임워크의 클래스는 객체를 복사할 경우, 얕은 복사 형태로 참조 관계를 유지한다.
특히 NSArray, NSSet 같은 컬렉션 객체나 다른 객체를 참조하는 객체를 복사하는 경우에는 깊은 복사를 고민해봐야 한다.

자동 메모리 관리

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 환경에서도 객체 인스턴스에 대한 메모리 관리는 신경 써야만 한다.

메모리 관리

현대 컴퓨터 구조에서는 프로그램이 메모리에 올라간 상태로 명령어가 하나씩 실행된다.
모든 객체 인스턴스는 메모리에 만들어진다.
그 중에서 객체에 대한 메모리 관리는 필수적이면서, 귀찮은 숙제이다.

메모리 사용을 최적화하는 과정은 CPU 사용률에 직간접적으로 영향을 주고, 배터리 소모량에도 영향을 미친다.

메모리와 객체

OS가 관리하는 프로세스는 이론적으로

  1. 32bit -> 4GB : (2^32)
  2. 64bit -> 18EB : (2^64)macOS는 메인 메모리상의 사용하지 않는 공간을 '페이지' 단위로 나눠서 하드디스크에 백업(Swapping)하는 기능을 제공한다.
    반면에 iOS는 하드디스크가 없고, 대신 플래시 메모리를 사용하기 때문에, 늘 메모리가 부족하기 마련이다.
  3. 크기를 가지는 가상 주소 공간에 접근할 수 있다.

iOS에서 읽고-쓰는 데이터는 프로그램을 실행하는 동안 사라지지 않지만, 사용하지 않는 읽기전용 데이터는 페이지를 저장하고 메모리상에서 지운다.
더 나아가, 읽고쓰는 데이터들의 총합이 일정 수준 이상 많아지면, 메모리 부족 경고를 보내고, 그래도 부족하면 앱을 강제 종료시켜서 메모리를 확보한다.

  • applicationDidReceiveMemoryWarning(_:)
    메소드를 구현하여서, app이 죽지 않게 메모리를 확보하는 작업을 할 수 있다.

macOS와 iOS를 비롯한 OS는 프로세스 주소 공간보다 물리 메모리(실제 메모리)가 부족하기 떄문에, 가상 메모리 방식을 사용한다.
물리 메모리보다 더 큰 가상 메모리를 다루기 이ㅜ해서, CPU와 MMU에서 일정한 크기를 가신 '페이지' 단위로 나눠 메모리를 관리한다.
기본적으로 크기는 4KB를 사용하며, Page Fault가 발생하면 -> 디스크에서 4KB 단위씩 새 페이지를 읽는다.

객체 인스턴스 생성

객체를 구현할 때, 우선적으로 고민해야 하는 것은 '객체 생명주기' 를 예측 가능하게 만드는 것이다.
보통 클래스 코드를 작성할 때, 생명주기와 관련해서 생성자 메서드를 가장 먼저 구현한다.

Pen *aPen = [[Pen alloc] init];

Pen 클래스에 +alloc 메세지를 보내면 힙 공간에 객체 인스턴스가 만들어진다.
+alloc 메서드 구현 내용을 수도코드로 보면 이렇다.

+alloc{
    id newObject = malloc(self->clsSizeInstance, 0);
    newObject->isa = self;
    return newObject;
}

해당 클래스의 메타 클래스에 명시된 속성 데이터 타입 크기를 확인해서 clsSizeInstance 크기만큼 힙 메모리를 할당한다.
메모리를 할당하는 단위를 16Byte이다.
If) 4Byte를 요청하면 -> 16Byte 할당, 20Byte 요청 -> 32Byte 할당
64bit 커널을 기준으로 994Byte ~ 128KB -> 512Byte 단위로 할당, 그 이상 -> 4KB를 할당

+alloc 메서드는 _allocWithZone: 을 호출하고, class_createInstance() 런타임 API를 호출하는데,
이 함수 내부에서 calloc() 함수를 호출한다.
calloc() 함수 : malloc() 과는 달리 객체 크기만큼 메모리를 할당한 다음, 할당된 메모리 공간을 0으로 채워준다.

메모리 할당 단위

메모리에 TINY, SMALL, LARGE 단위의 메모리 조각들이 구분없이 만들어지면, 외부 파편화가 발생할 것이다.
따라서 힙 메모리 공간을 영역별로 구분한다.

-??> JAVA GC에서는 Compact 작업이 있는데, 없는건가?

메모리 영역과 가상메모리

UIImage 클래스의 +imageNamed: 메서드로 이미지를 생성할 경우, 의도하지 않아도 시스템 내부에 이미지를 캐싱한다.

객체 인스턴스 소멸

소멸자 메서드는 생성과 달리 객체가 소멸되기 직전에 호출된다.
객체 내부에서 생성한 객체 인스턴스가 있다면, 먼저 해제해주도록 도와준다.
-> 그래야 불필요한 객체 인스턴스들이 남지 않기 떄문이다.

요약

요즘 단말기기의 대부분은 커널에서 관리하는 가상 메모리 크기도 커지고, Zone 관리도 필요가 없어졌다.
따라서 프로그램 데이터 구조를 강제로 줄이거나 조정해야 하는 제약 사항이 사라졌다.
그럼에도 객체 인스턴스를 메모리에 생성해서 소멸할 때까지의 과정을 메모리 관리 측면에서 이해하고 있으면 효율적인 것은 변함없다.
객체 인스턴스가 아니더라도 이미지나 DB Cache를 위해 내부적으로 할당하는 메모리 공간에 대해 종합적으로 고려해야 한다.


참조 계산

객체 인스턴스가 메모리에 생성되고 소멸되기까지를 '객체의 생명주기' 라고 부른다
어떤 객체는 생명주기가 짧고, 어떤 객체는 App과 같이 긴 경우도 있다.
이렇게 만들어진 객체는 처음에 필요해서 만든 객체가 아니라, 전혀 다른 객체가 메세지를 보내기 위해 참조하기도 한다.

특정 객체가 다른 객체를 참조하는 경우, 참조할 객체가 메모리에 존재하는지 아니면 사라졌는지 판단할 필요가 있다.
Apple은 객체 인스턴스를 확인하기 위해 "참조 계산" 방식을 제공한다.
-> Reference Counting 이 ARC가 동작할 수 있는 바탕이다.

코코아 프레임워크가 제공하는 모든 객체는 "참조 카운터" 공간이 있다.
해당 객체의 참조 횟수를 계산한 값을 기록하는 공간이다.
객체가 만들어질 때, 해당 객체를 참조하는 포인터로 인해서 1로 설정
다른 객체들이 추가로 강한 참조(retain)할 때, +1
참조하고 있던 객체들이 끊으면(release)할 때, -1 로 동작한다.

Apple에서도 Java JVM처럼 GC를 지원했었다.
사라진 원인은 알아보쟈...

객체 소유권

다른 객체를 참조한다는 것을 C 스타일로 표현하면, "다른 객체의 힙 메모리 주소를 포인터 변수에 담고 있는 것" 을 의미한다

    Pen *aPen = [[Pen alloc] init];
    Pen *bPen = aPen;
    [aPen release];
    bPen.color = [UIColor yellowColor]; -> bPen이 객체 인스턴스를 retain하지 않았기 떄문에 nil 크래시

객체 소유권 규칙

코코아에서 사용하는 일반적인 객체 소유권 규칙은 다음과 같다.

  • 특정 객체를 새로 만드는 경우는 소유권을 갖는다.
  • 다른 객체가 생성한 객체를 참조하기 전에, 소유권을 요청해서 받아야 한다.
  • 소유권을 얻는 객체를 더 이상 참조하지 않으면, 소유권을 반환한다.
  • 소유권을 갖고 있지 않는 객체를 반환하면 안된다.

이 규칙을 코코아 프레임워크 용어로 다시 설명해보자.

  • '소유권을 갖는다' -> 참조 횟수를 1 증가한다.
  • '소유권을 반환한다' -> 참조 횟수를 1 감소한다.
    Pen *aPen = [[Pen alloc] init];
    Pen *bPen = [aPen retain];
    Pen *cPen = [bPen copy];
    [aPen release];
    bPen.color = ...
    cPen.color = ...
    [cPen release];
    [bPen release];

객체 메서드와 소유권 규칙

  1. alloc, new, copy, mutableCopy 계열 메서드로 특정 객체를 생성하거나 복사하는 경우 새로운 객체 인스턴스를 만든다
    • 그리고 참조 횟수를 1로 설정하고 소유권을 갖는다.
  2. 다른 객체가 이미 만들어 놓은 객체 인스턴스를 참조하는 경우에는 retain 메서드를 사용해서 객체 소유권을 요청한다.
    • 그리고 참조 횟수를 1 증가한다.
  3. 1, 2에서 소유권을 얻는 객체를 더 이상 참조하지 않는 경우, release 또는 autorelease 메서드를 사용해서 객체 소유권을 반환한다.
    • 참조 횟수를 1 감소한다.
  4. 소유권을 갖고 있지 않은 객체는 반환하면 안된다.
    • 1, 2에서 설명한 메서드로 소유권을 요청한 적이 없거나, 이미 소유권을 반환한 경우에는 release/autorelease 메세지를 보내면 안됨

자동 반환 목록(autorelease list)

앞에서 객체 소유권을 반환할 때, release나 autorelease 메서드를 사용한다고 했다.
특정 객체에게 release 메세지를 보내면 참조 횟수가 1 감소하고, 0이 되면 그 즉시 dealloc 메서드를 호출하고 메모리를 반환한다

어떤 객체는 생성하고 소유권이 없는 상태에서 다른 객체가 사용할 때까지, 일정 시간동안 메모리를 반환하지 않고 남아있어야 하는 경우가 있다.
이런 경우를 대비해 '자동 반환 목록 동작'에 대해 알아보자.
자동 반환 목록은 일정 시간 뒤에, 반환할 객체 목록을 만들어서 관리해준다.

Objectice-C 객체 인스턴스는 힙 메모리에 만들어지지만, 함수 범위나 문법적으로 특정 범위가 정해진 변수들은 C 언어처럼 자동 변수 스택에 생겼다가 사라진다.

temp 객체는 alloc, init 메서드로 만들어졌고, 소유권 규칙에 따라 소유권을 갖는다.
당연히 temp 객체 메모리를 해제할 때, release 메세지를 보내야 한다.
하지만 release 메세지를 보내서 소유권을 즉시 반환하는 대신, autorelease 메세지를 보내면, "자동 반환 목록" 에 객체를 등록할 수 있다.
그리고 주석 부분에 해당하는 다른 작업을 처리하고, autoreleasePool 객체가 drain 메서드를 처리하면서 "자동 반환 대상"인 객체를 차례대로 release 시킨다.

간편한 메서드와 자동 반환 대상

코코아 프레임워크 객체 중에는 객체를 생성하면서, "자동 반환 목록"에 추가하는 객체가 존재한다.
객체 팩토리 메서드 중에서 객체를 생성하기에 편리하도록 준비된 특별한 메서드는 객체를 '생성'하고 '초기화'한 다음 "자동 반환 목록"에 등록까지 해준다.
간편한 메서드(convenience methods)의 클래스 이름 형태는 코코아 클래스 이름에서 NS 접두어가 없고 소문자로 시작한다.
ex) NSString 클래스는 +string~ 형태로 시작하는 +stringWithFormat:, +stringWithString: 같은 메서드가 바로 간편한 메서드다.
NSNumber에서는 +numberWith~ , NSArray에서는 +array~ 메서드가 간편한 메서드이다.

NSString* aString = [[NSString alloc] initWithFormat: @"%08d", myNumber];

위 코드는 다음과 같이 풀어쓸 수 있다.
동작은 동일하나 소유권이 aString 객체를 생성한 객체에 있는 것이 아니라, "자동 반환 목록"으로 넘어간다.

NSString* aString = [[[NSString alloc] initWithFormat: @"%08d", myNumber] autorelease];

자동 반환 대상을 관리하는 AutoreleasePool 클래스에 대한 객체는 스레드마다 하나씩 생성해서 소유권을 갖고 있다가, 스레드가 끝날 때, 같이 소멸되도록 권장한다.
main() 함수 내에는 AuroreleasePool 객체가 이미 들어있다. -> 메인 스레드에는 별도로 생성할 필요 X

자동 반환 목록 사용 시 주의 사항

AutoreleasePool 객체는 대부분 NSRunLoop 클래스와 함께 동작하는데, 코드 흐름상 반복해서 객체를 생성하는 경우에는 자동 반환 목록에 있는 객체를 반환하는 시점이 되기도 전에 너무 많이 쌓이는 현상 이 생길 수 있다.

객체 그래프

OOP로 프로그래밍을 하다보면 여러 객체 인스턴스가 만들어지고, 객체들끼리 참조 관계가 생긴다.
이런 객체들끼리 관계를 네트워크 그래프로 표현하면 -> "객체 그래프"이다.

순환 참조 문제

객체 그래프를 확인해야 하는 이유는 객체끼리 순환 참조가 있는지 여부를 확인하기 위해서다.

요약

특정 객체에 대한 참조 관계를 관리하기 위해, 참조 횟수를 가감하여 계산하는 방식은 객체 포인터 변수를 참조할 때, 위험 요소를 줄이는 방법이다.
메모리 관리를 위해 참조 계산을 실수없이 처리하려면, 객체 소유권 개념을 정확히 파악해야 한다.
즉시 반환할 객체가 아니라면, auroreleasePool을 사용하는게 좋다.


객체 초기화

객체 인스턴스를 메모리에 할당한 직후, 객체 내부 변수를 초기 값으로 지정하기 위해 init 메서드를 사용한다.
NSObject에 구현된 -init 기본 초기화 메서드는 상속받아 만들어진 모든 객체에서 기본 초기화 메서드로 사용한다.

특정 객체가 포함하는 하위 객체는 상위 객체 인스턴스가 만들어지면서 동시에 만들어진다.
이런 소유권을 갖는 하위 객체는 대부분 초기화 메서드에서 만들어진다.
객체 내부 자원에 해당하는 인스턴스 변수를 준비한다는 측면에서 초기화 메서드는 중요하다.
If 상속시) Objective-C 에서 -init 초기화 메서드를 명시적으로 구현하지 않으면, 상속받은 상위 객체에 구현된 초기화 메서드를 호출한다.

여러 초기화 메서드

NSObject 클래스에 선언되어 있는 기본 초기화 메서드는 -(id)init 형태이다.
상속받아 만드는 클래스는 init 메서드를 다음과 같은 형태로 오버라이드해서 구현한다.

-(id)init{
    self = [super init];
    if(self != nil){
        //인스턴스 변수 초기화
    }
    return self;
}

기본 동작이 상속받은 부모 객체의 인스턴스 내부 변수와 리소스를 초기화하고, 자신의 내부 리소스를 초기화하는 순서를 권장하기 때문이다.
초기화 메서드가 기본값이 아닌 외부 데이터에 의존해서 초기값을 설저앻야 한다면, 추가적으로 초기화 메서드를 추가해야 한다.
메서드 명은 -init으로 시작하면 된다.

인스턴스타입(instanceType)
id 타입은 Objective-C에서 모든 객체를 표현할 수 있는 다이내믹 타입이다 -> AnyObject와 비슷
id 타입으로 리턴받은 객체는 타입 정보가 부족해서 특정 메세지를 보낼 수 있는지 없는지 컴파일러가 판단하기 어려움
실제로 메세지를 보내면 리턴받은 객체가 해당 메세지를 받아서 처리할 수 없어 앱이 죽기도 한다
최신 런타임 구조에서는 이런 생성/초기화 관련 메서드 리턴 타입을 id -> instanceType으로 변경했다.

Example

@interface AObject: NSObject
+ (instancetype)factoryMethodA;
+ (id)factoryMethodB;
@end

@implementation AObject
+ (instancetype)factoryMethodA {return [[[self class] alloc] init];}
+ (id)factoryMethodB {return [[[self class] alloc] init];}
@end

void aa(){
    NSUInteger x, y;

    //AObject instancetype
    x = [[AObject factoryMethodA] count]; // Warning -> AObject may not respond to 'count'
    // id
    y = [[AObject factoryMethodB] count]; // Not Warning
}

초기화 메서드 구현하기

코코아 프레임워크에서 -init 계열 메서드를 구현하기 위해 권장하는 가이드이다.

  1. 상속받은 Super Class의 초기화 메서드를 먼저 호출한다.
  2. Super Class 초기화 메서드 리턴 값을 확인해서 nil이면, 내부 리소스 초기화를 하지않고 그대로 nil을 리턴한다.
  3. 내부 리소스를 초기화하면서 객체느 copy나 retain 메서드를 호출해서 소유권을 갖는다.
  4. 인스턴스 변수들을 적정한 값으로 초기화하고 나면 self를 리턴한다.
  5. 인스턴스 변수들 초기화 과정에서 오류가 발생한 경우에는 self를 해제하고 nil 리턴
  6. self가 아닌 객체 인스턴스를 리턴하는 경우라 하더라고 self를 해제해야 한다.

Failable init in Swift

class A{
    var a: Int
    //Swift의 실패가능한 init은 실패 시에는 nil을 반환하지만, 성공시에는 값을 반환하지 않는다.
    //사용하는 이유 : 실수로 만들어지는 객체들로 인한 메모리 낭비를 방지, 예상하지 못한 상황 방지
    init?(value: Int){
        if value < 0{
            return nil
        }
        self.a = value
    }
}

객체 초기화 관련한 문제

한번 -init 계열 메서드로 객체 인스턴스를 초기화한 후, 또다시 -init 계열 메서드를 호출하면 안된다. -> exception 발생

초기화 하는 객체가 +alloc 메서드를 통해 정삭적으로 메모리에 생선한 객체가 아닌 경우도 조심해야 한다.
해당 객체 인스턴스가 하나만 존재해야 하는 싱글톤 인스턴스인 경우도 있고, 내부 인스턴스 변수 객체 중 싱글톤 형태로 존재하는 경우도 있다.

싱글톤 프로퍼티를 갖고 있거나, 클래스 자체가 싱글톤으로 사용된다면 주의

마지막으로 초기화 메서드가 실패한 경우를 대비해야만 한다.
-initWithString: 메서드에 인자 값 문자열이 nil인 경우가 있을 수 있고,
-initWithArray: 메서드에 인자값이 NSArray 대신 NSDictionary일 수도 있다.
이렇게 인자 값이 nil이거나 원하지 않는 객체가 들어오면, 새로 할당한 객체는 반환하고 nil을 리턴해야 한다.

+ Recent posts