불변객체와 가변객체

코코아 프레임워크 객체는 크게 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 메서드가 중요하다
반면에 정렬한 배열처럼 순서가 중요한 컬렉션은, 순서를 정하기 이ㅜ한 비교 메서드가 중요하다.

요약

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

+ Recent posts