[북리뷰] Test-Driven iOS Development

린 소프트 개발 실천법인 테스트 주도 개발(Test-Driven Development, TDD)을 iOS 개발에 적용해보기 위해 도서를 검색하던 중, 구글을 통해 무료로 eb00k을 다운로드 할 수 있었다. (다운로드 링크 , Amazon 링크)

번역서는 현재 국내에 없으며, iOS 앱 개발 중심의 TDD 도서 역시 국내에 아직 없다. TDD는 개발론으로 iOS 예시 도서가 학습에 꼭 필요하지 않지만, 본인처럼 iOS 및 TDD 초보자로서 iOS 앱개발과 TDD를 동시에 학습하고 싶은 사람에게 이 책을 추천한다. (영문이 부담되나 이해가 어려운 문장들은 예제 코드를 통해 어느 정도 이해할 수 있다.)

이 책에서 다루는 내용은 다음과 같다.

  • StackOverflow API 를 이용한 앱 개발 과정을 다룬다. 네트워크로부터 데이터(JSON)를 획득하여 Model을 만들고 화면에 표시하는 앱이다. 차후에 비슷한 구조의 앱 개발 시 템플릿으로 활용할 수 있을 것 같다.
  • Model Test -> Application Logic Test -> ViewController Test -> 통합테스트 순인 Bottom-Up 방식으로 진행된다. 대부분의 iOS 앱 개발도서들은 Interface Builder 를 이용한 UI 개발을 주로 다룬다. 하위 구조부터 설명이 잘 되어 있어 실제 앱 개발에 가까운 과정을 학습할 수 있다.
  • StackOverflow API 를 이용하므로 NSURLConnection 사용법을 학습할 수 있다.
  • 네트워크 응답을  delegate 를 이용하여 화면에 전달하는 구조를 학습할 수 있다. 예외 처리도 포함된다.
  • NSNotificationCenter , Introspection , Category 를 사용한다.
  • Mock객체와 delegate 중심의 테스트 케이스 작성을 학습한다.
  • 하나의 TableView 와 두 개의 TableDataSource 로 구성된 화면을 학습한다.
  • 소스 코드가 github에 공개되어 있다.

현재 간단한 앱 개발을 통해 TDD를 학습하고 있다. TDD도 익숙치 않고 iOS 앱 개발도 숙련되지 않아 테스트케이스 작성에 들어가는 수고가 낭비는 아닐까 생각이 들기도 한다. 더욱이 처음부터 완벽한 코드를 작성해야 한다는 집착을 여전히(?) 버리지 못하여 테스트 작성에 망설이는 것도 이런 생각을 부추기는데 한 몫 한다.

불필요한 SW Engineering이 개발자가 제품을 빠르게 만들지 못하는 원인을 제공한다는 글을 본 적이 있다. 본인도 개발자의 으뜸 능력은 안정적인 제품을 빠르게 만드는 것이라 생각한다. 익숙치 않은 개발법이 이를 어렵게 할 수 있지만, 익숙해진다면 TDD가 이를 도울 수 있다는 생각이다.

“클래스 하나를 추가하는데 주저하지 마라” 는 책의 메시지가 기억에 남는다.

 

Advertisements

[iOS] Practical Memory Management

iOS  프로그램 개발에 있어서 메모리 관리를 쉽게 하고, 안정성을 높이는 방법을 알아보자.

iOS 메모리 관리는 객체의 소유권을 바탕으로 한다. 객체의 소유권 정책(ownership policy)은 참조 카운트(reference counting)에 의해 이루어 지는데 다음과 같은 방식으로 계산된다.

  • 객체를 생성하면 retain count는 1이다.
  • retain 메시지를 전송하면 retain count는 1 증가한다. (소유권을 갖는다)
  • release 메시지를 전송하면 retain count는 1 감소한다.(소유권을 반환한다)
  • autorelease 메시지를 전송하면 autorelease pool block 종료시점에 retain count는 1 감소한다.
  • 객체의 retain count 가 0이 되면 객체는 해제(deallocated)된다.
  •  NSObject의 retainCount 메소드는 개발자가 의도한 숫자를 반환하지 않을 수 있으니 디버그 이외에는 사용하지 말자.

iOS Application을 위한 Apple의 권장 메모리 관리 방법은 아래와 같다.

1. 접근자 메소드(Accessor Method) 사용

클래스의 인스턴스 변수는 사용되는 동안 해제되어서는 안되다. 따라서 변수에 객체를 할당할 때 객체의 소유권을 요청해야 하며 사용 후에는 객체의 소유권을 반환해야 한다.

접근자(Accessor) 메소드는 이를 쉽게 함으로써 빈번하게 개발자가 객체 소유권을 획득,반환하는데서 발생할 수 있는 문제들을 줄인다.

property는 두 가지(getter/setter) 접근자 메소드를 제공한다. @synthesize 는 컴파일러가 자동으로 접근자 메소드를 생성하게 하지만, 자동으로 생성되는 접근자 메소드의 내부 구현 원리를 확인할 필요는 있다.

@interface Counter : NSObject
@property (nonatomic, retain) NSNumber * count;
@end

@implementation Counter

// getter
-(NSNumber *)count{
    return _count;
}

// setter
-(void) setCount : (NSNumber*)newCount{

    [newCount retain];
    [_count release];

    // make the new assignment
    _count = newCount;
}
@end

get 메소드에서는 retain, release 를 호출하지 않는다. 반면 set 메소드에서는 [newCount retain] 을 호출하여 언제든 해제될 수 있는 newCount 객체의 소유권을 확보한다.

[_count release]는 [newCount retain] 이후에 와야한다. 그 이유는 newCount 객체와 _count 가 동일할 경우,  _count 객체 release를 먼저 수행하면 _count 객체와 동일한 newCount 객체도 해제될 수 있기 때문이다. (참고로 objective-c 에서는 [_count release] 대신에 _count = nil  사용할 수 있다. 이는 _count 에 할당된 객체가 없을 때에도 코드가 동작하도록 한다.)

프로퍼티에 새로운 값이 할당되면 이전 값은 release 됨을 기억하자. 프로퍼티 값을 다른 변수에 retain 없이 할당하고 프로퍼티에 새로운 값을 설정하는 경우, 프로퍼티의 이전 값은 release 되므로 이전 값이 할당된 객체를 이용한 코드는 문제 발생 소지가 있다.(Automatic Reference Counting 의 경우에는 프로퍼티의 이전 값을 retain 없이 할당해도 객체 메모리가 해제되지 않음을 보장한다.)

2. 초기화 메소드 / dealloc 메소드에서는 접근자 메소드(Accessor Methods) 사용 금지

초기화 메소드(initializer method) 및 dealloc 메소드 내에선 접근자를 이용해서 변수를 할당해서는 안된다. 초기화 메소드에서 변수의 초기 값을 할당하려면 접근자가 아닌 인스턴스 변수를 이용해야 한다. 또한 dealloc 메소드에서 소유권 포기를 위한 release 도 아래 코드와 같이 인스턴스 변수를 이용해야 한다.

// 초기화 메소드
- init{

    self = [super init];
    if(self){
        _counter = [[NSNumber alloc] initWithInteger : 0];
    }
    return self;

}
// dealloc 메소드
-(void)dealloc{
    [_counter release];
    [super release];
}

3. Retain Cycle 발생하지 않도록 약한 참조(Weak Reference) 사용

retain은 객체 간 강한 참조(strong reference) 관계를 형성한다. 객체는 자신과 강한 참조 관계를 가진 객체가 하나도 없을 때까지 해제되지 않는다.

Retain Cycle 문제는 두 객체가 서로 강한 참조일 경우 발생한다. (두 객체 간 뿐만 아니라 다수의 객체가 강한참조로 원형으로 물려 있어도 발생한다.) 아래 그림을 살펴보자.

< From iOS Developer Library >

Document 객체는 여러 개의 Page 객체를 가지고 있고, 각각의 Page 객체는 자신이 어떤 Document 에 속해있는지 추적하기 위해 Document를 프로퍼티로 가지고 있다. 만약에 Document 객체가 Page 객체를 강한 참조로 소유하고, Page 객체가 Document 객체를 강한 참조로 소유하고 있다면 두 객체는 해제되지 않는다.(Retain Cycle 문제)

서로 강한 참조를 하고 있으므로 Document 객체의 참조 카운트는 Page 객체가 해제될 때까지 0이 되지 않고 Page 객체는 Document 객체가 해제될 때까지 참조 카운트가 0이 되지 않는다. 즉, 서로 객체가 해제될 때까지 대기하므로 두 객체는 해제되지 않는다.

Retain Cycle 문제 해결 방법은 약한 참조(weak reference)를 사용하는 것이다. 약한 참조는 객체의 소유권을 갖지 않는 객체 참조로 소스 객체의 참조 카운트가 증가하지 않는다. Apple은 부모 객체- Document 객체 -는 자식 객체- Page 객체 -를 강한 참조로 소유하고, 자식 객체는 부모 객체를 약한 참조로 소유하도록 권고하고 있다.

약한 참조는 소스 객체의 참조 카운트를 증가시키지 않으므로 언제든 객체가 해제될 수 있다. 따라서 약한 참조로 소유하고 있는 객체에 메시지를 전송할 때는 주의해야 한다. 만약 해제된 객체로 메시지를 전송한다면 프로그램은 문제를 일으킬 것이다.

약한 참조의 경우 참조된 소스 객체가 해제되면 참조한 객체에서 파악할 수 있도록 프로그램을 설계해야 한다. 즉, 약한 참조가 해제될 때 자신과 연결된 다른 객체에게 이를 알려줄 의무가 있도록 설계해야한다.

Notification Center에 객체를 등록하게 되면 Notification Center는 약한 참조로 객체를 저장하고 사전 정의된 컨디션이 되면 저장된 객체에 이를 알려준다. 만약 Notification Center 에 등록한 객체가 해제될 경우, 개발자는 Notification Center에 등록된 객체를 등록 해제 해야한다.

이와 마찬가지로 delegate에 할당된 객체가 해제되면 개발자는 delegate 링크를 해지시킬 필요가 있다. 링크 해지는 보통 dealloc 메소드에서 setDelegate : nil 을 통해 구현한다.

Cocoa Framework에서 약한 참조를 사용하는 경우는 Table data source,  Outline view item, Notification observer, Target 그리고 delegates 가 있다.

4. 사용하는 객체가 해제되지 않도록 주의

객체가 사용되는 동안에는 객체가 릴리즈되지 않음을 보장하도록 프로그램을 작성해야한다.

아래 case 1, case 2 와 같은 방법으로 다른 객체를 할당 받은 변수의 사용은 문제를 일으킬 수 있다. 이를 방지하기 위해 case 3처럼 retain을 통하여 객체를 소유하여야 한다.


// case 1
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject could now be invalid
// case 2
id parent = <#create a parent object#>;
// ...
heisenObject = [parent child];
[parent release]; // or self.parent = nil;
// heisenObject could now be invalid

// case 3
heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// use heisenObject
[heisenObject release];

콜렉션은 객체를 추가할 때 객체의 소유권을 갖고, 객체를 삭제할 때 소유권을 반환한다 .따라서 case 1의 경우, 콜렉션에서 삭제된 객체의 소유자가 유일하게 콜렉션 자신일 경우 hesienObject 사용에 있어 문제가 생길 수 있다.

case 2의 경우 [parent release] 라인이 parent 객체의 참조 카운트를 0으로 만들고, child 객체의 소유자가 유일하게 parent 이며, parent 가 해제될 때 child 가 release (not autorelease)된다면 hesienObject 사용에 문제가 생긴다.

5. dealloc 메소드에서 한정된 자원(Scarce Resource) 관리 금지

File, Network connections, Buffer 그리고 Cache 와 같은 자원을 dealloc 메소드 내에서 관리하면 안된다. 즉, dealloc 메소드 호출 시점을 개발자가 예상할 수 있다는 가정 하에 프로그램을 설계하면 안된다. dealloc 메소드 호출은 버그 등의 문제로 지연되거나 건너뛸 수 있다.

위에 언급한 종류와 같이 한정된 자원을 다룰 때는 자원이 더 이상 사용되지 않는 시점에, 정확히 자원이 해지되도록 디자인 해야 한다. 그렇지 않고 자원의 해지를 dealloc 메소드에서 한다면 다음과 같은 문제가 발생한다.

  • 객체 그래프(object graph)가 해체되는 메카니즘은 본질적으로 불규칙(non-ordered)적이다. 비록 개발자가 특정 순서를 예상할 수 있다 하더라도 항상 그렇지 않다. 만약에 객체가 autoreleased 되었다고 한다면, 해체 순서는 변경될 것이고 이는 예상치 못한 결과를 야기할 수 있다.
  • 위에 언급한 자원들이 원하는 시점에 릴리즈 되지 못한다면 메모리 문제가 발생할 수 있다. 이는 파일시스템에서 동작하는 어플리케이션의 경우 데이터를 파일에 저장시키지 못할 수도 있다.
  • 만약 객체가 autoreleased 된다면 그 객체는 어떤 스레드에 의해서도 해제될 수 있다. 이는 하나의 스레드에서만 처리 되어야 할 자원인 경우 문제를 일으킬 수 있다.

6. 콜렉션은 내부객체의 소유권을 획득

객체가 콜렉션(array, dictionary, set) 에 추가되면 콜렉션은 객체의 소유권을 갖는다. 콜렉션은 객체가 콜렉션에서 제거될 때와 콜렉션 자체가 릴리즈될 때 객체의 소유권을 반환한다. 따라서 콜렉션을 이용할 때 다음을 따라야한다.

// case 1
for(NSUInteger i=0; i<10 ; i++){

    NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];
    [array addObject : convenienceNumber];

}

// case 2

for(NSUInteger i=0; i<10 ; i++){

    NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
    [array addObject : convenienceNumber];
    [allocedNumber release];
}

alloc 을 사용해서 생성된 객체라면 콜렉션에 추가후 release 해야하며, 그렇지 않고 간편생성자(convenience initializer)를 통해 생성된 객체라면 release 가 불필요하다.

* Reference : Practical Memory Management

%d bloggers like this: