[Clean Code] 클린 코드 법칙

개발자 누구라면 깔끔한 코드를 원할 것이다. 나는 평소에 리팩토링, 클린 코드등에 괜히 관심히 많았다. (그렇다고 내 코드가 깨끗하다고는 못하겠다.) 처음으로 HOWTO 같은 종류가 아닌 개발 서적을 처음으로 산 책이 로버트 마틴의 ‘클린 코드’이다. 몇주 전 사내에 로버트 아저씨(실재로 백발의 할아버지이다.)가 같은 주제로 자신이 만든 동영상 시리즈 링크가 돌길래 하루에 한개씩 1.5배 속도로 시청하고 있다. 책에 나온 이야기를 재미있게 설명하고 있어서 귀에 속속 들어왔다. 한번 보고 스쳐 지나간다면 또 까먹을지 모른다는 두려움으로 노트에 핵심 포인트만 작성하기 시작했고, 노트도 나중되서 없어지만 안되겠다는 생각에 블로그에 정리를 한번 해보려고 한다. 이 글을 읽고 클린 코드에 관심이 생긴다면 책도 한번 사보길 권한다. 팀 단위로 일하는 소프트웨어 엔진이어라면 꼭 권하고 싶다. 링크

네이밍 (Naming)

  • 좋은 고드란? 사람들이 예상한 대로 작동하는 코드이다.
  • 좋은 네이밍은 타인과 소통할 수 있는 기본적이고 가장 중요한 도구이다.
    • 네이밍에서 의도가 나타나야 한다.
    • 해결하고자 하는 문제가 묘사되어야 한다.
    • 잘못된 정보는 절대 넣지 마라.
    • 클래스명, 변수명은 명사를 사용하여 작명하라.
    • 함수명은 동사로 시작해야 한다.
    • enum은 형용사로 시작한다.
    • 발음이 가능한 단어를 사용하자.
    • 변수의 타입을 나타내는 Prefix은 이제 그만 사용하자. 발달한 IDE가 해결해준다.
  • 이름이 사용되는 범위에 맞게 작명해야한다.
    • 변수는 범위가 광범위 할 수록 의도와 정확한 정보로 비교적 길게 작명하여 어디에서든지 이해가기 쉽게 만드는 것이 좋다.
    • 변수의 범위가 작을수록 간단하고 짧게 만드는 것이 가독성을 높이는 것에 좋다.
    • 클래스와 함수는 변수와 반대 되는 법칙을 따른다. 사용되어지는 범위가 넓을 수록 이름을 짧게 짓고, 범위가 좁을 수록 길게 지어서 정확히 무슨 일을 하는 묘사하자.

함수 (Function)

  • 함수의 크기가 작으면 작을 수록 좋다. 얼마나? (4줄~6줄)
  • 작명을 잘해야 한다.
  • 한가지 일만을 하자. 한가지 일만 하는지 알기 위해서 코드를 추출 (extract)이 안될 때 까지 추출한다.
  • 작은 크기의 함수들이 많아지면 함수 호출 오버해드나 가독성이 떨어진다고 생각한다면 그것은 오해이다.
    • 작은 크기의 좋은 작명이 된 함수가 많다는 것은 복잡한 길에 가고자 하는 길을 안내해줄 표시판이 많은 것 처럼 좋은 일이다.
    • 함수 호출 오버해드 (function call overhead)가 걱정되는가? 현대시대의 개발장비 성능은 옛 장비와 다르게 빨라졌기 때문에 나노 초도 걸리지 않으며, 여러 사람과 함께 용이 작업에 있어서 높은 가독성이 가져다 주는 혜택이 더 크게 보여진다.
  • 큰 함수의 구현을 클래스로 추출하여 크기를 줄여보자. 호출하는 곳에서는 새 클래스의 invoke()를 호출하고 중복 코드를 하나로 묶고, 더 이상 줄일 수 없을 때 까지 줄여 작은 단위의 함수로 구현을 분배하자.

함수 구조 (Function Structure)

  • 함수의 인자(Argument)의 갯수는 적을 수록 좋다. 1~2개가 적당하다. 3개 이상이 되면 오프젝트로 묶어서 전달하자.
  • output 인자는 절대 사용하지 말자. 전달된 인수의 필드나 프로퍼티를 변경하여 함수 밖에서의 사용하지 말자.
  • 함수 인자로 Boolean 타입의 인자는 사용하지 말자. Boolean 타입의 인자를 사용한다는 것은 함수 내에서 2가지 이상을 하겠다는 것을 공식적으로 선언한 것이기 때문이다. 함수는 한가지 일만 하는 것을 원칙으로 하자.
  • Nullable 인자는 지향하자. 물론 Open Source과 같이 불특정 인원이 어떤 것을 전달할지 모를 때면 사용하여 방어적 코드를 짜야겠지만, 팀내에서는 합의하에 절Nullable 인자를 사용하지 않고 공격적인 코딩 스타일을 고수하자.
  • Step Down 법칙: Public 변수나 메소드를 위에 넣고, private 메서드를 아래에 넣는 코드 컨밴션은 유행이 한참 지난 스타일이다. 잡지나 신문을 보면 제목이 나오고 자세한 내용은 그 후에 따라 나온다. 이와 같이 하면, 비록 Public 메서드들이 한눈에 안들어 올 것이다. 하지만 코드의 가독성을 위해서 Step Down 법칙을 추천한다.

Step Down Rule

 

  • Switch-case 문: 사용을 지향한다. Switch 문을 사용하게 되면 코드가 의존도가 높아진다. 대신하여 Polymorphism 방식을 사용하자.

No Switch statement

  • Command & Query: 함수의 특성을 광범위적으로 보면 크게 두가지 종류로 구분하여 사용하는 것을 추천한다.
    • Command : 반환 값은 없어야 하며 어떤 행동, 혹은 명령을 실행하는 함수라고 할 수 있다. 예를 들어 authenticator.login()라는 함수는 로그인을 하는 행동을 하는 함수이다. 꽤 자주 많은 곳에서 login과 같은 메서드에 User 오브젝트를 반환하는 경우를 볼 수 있다. 명령 성질의 함수에 반환을 하게 되면 Query 종류의 함수도 아니고 Command 종류의 함수도 아닌 두 가지 일을 하는 함수가 되어 버린다. 물론 Nil을 반환하여 로그인 실패를 체크하려는 의도도 있을지 모른다. 하지만 이런 박쥐 같은 함수는 타 개발자에게 혼란을 가중시킨다.
    • Query: 값을 반환하는 함수를 말한다.
  • Null를 반환하지 말자. 만약 Null이 될 것 같으면 Exception을 던지자.
  • try문은 함수의 가장 위에 사용하자. 여기에 언급되는 함수 구조의 룰은 전부 함수가 한가지 일만을 명확하게 하고 짧은 함수들을 전재를 한다.

 

형식 (Form)

  • 모든 파일의 형식은 일관성이 중요하다. (인던테이션, 줄 바꿈, 주석 등)
  • 주석: 오래된 레가시 코드에는 업데이트를 하지 않은 주석이 많다. 주석은 Public API 문서에서는 불특정 다수에게 유용함을 주지만 코드 자체가 주석을 대신해야 좋은 코드이다.
  • 파일 크기는 최대한 작을 수록 좋다. 500 줄 이상이 되지 않게 조심하자.
  • 적절한 코드 가로 길이는 화면에 보이는 곳 까지이다. 줄 바꿈 없이 한 눈에 코드가 읽혀지는 것이 좋다.
  • 실제 구현에서 직접적으로 구현체 클래스 (concrete class)를 호출하기 보다는 추상 클래스를 호출함으로써 확장성 확보하자.

TDD (Test Driven Development)

  • 개발자는 현존하는 코드에 손을 대길 꺼려 한다. 그 이유는 잘 돌아가던 것이 깨질까 봐 그렇다. 하지만 코드가 깨지는 지 안깨 지는 보장 받을 수 있으면, 지저분한 코드를 더 깔끔한 코드로 바꾸려고 노력할 가능성이 높다. 그래서 테스트가 필요하다.
  • 그런데 왜 TDD인가? 이름에서 느낄 수 있는 것 처럼 Test code의 중요성은 아무리 강조해도 부족하다. 중요한 것을 먼저 짜는 것이 좋은 습관이다. Production code를 먼저 짜게 되면 Test case를 소흘하게 여길 것이다. 뿐만 아니라 좋은 설계를 갖게 될 것이다. 좋은 설계는 유연하고, 유지보수 가능해야하며, 확장 가능해야 한다. 그러기 위해서 코드가 깨지는 지 안깨지는 확신을 해야한다.
  • TDD의 세가지 법칙
    • 실패한 Test case를 패스하기 위해 만든 것 외에는 절대 Production code를 짜서는 안된다.
    • 실패할 만큼만 Test case를 짠다.
    • 실패한 Test case가 패스 할 만큼만 Production 코드를 짜라
  • 순서 (Red -> Green -> Refactor)
    • Red: 실패할 만큼만 Test case를 짠다.
    • Green: 실패한 Test case가 패스 할 만큼만 Production 코드를 짜라.
    • Refactor: Test case를 포함해서 코드를 정리하라.

설계 (Architecture)

  • 소프트웨어 개발에 있어 훌륭한 설계는 최대한 개발환경에 대한 결정을 안 할수 있는 설계이다. 개발을 시작하기 전에 우리는 Use case 보다 사용할 Tool과 Framework에 얽매이는 경우가 많다. 또한 이렇게 결정된 사항에 맞추어 설계가 진행되는 경우가 대부분이다. 하지만 이렇게 되면 각 tool이나 framework에 의존도가 높아져 유연한 설계를 할 수 없게 만든다.
  • 좋은 설계자란 tool과 framework와 같은 개발 환경에 대한 결정 사항들을 최대한 유연하게 하고, 미루고 미룰 수 있는 자이다. 예를 들어 데이터 저장을 어떤 방식으로 할지 결정을 나중에 해도 될 만큼 유연한 설계를 짜는 사람이 진정 좋은 설계자라고 할 수 있다. Sql를 사용할지 file system을 사용할지 웹 API를 사용할 지 등을 설계 초입에 안하고 설계할 수 있어야 한다.
  • 개발 환경 보단 Use case에 더 집중하도록 하자.
  • UI 작업을 Use case의 플러그인 정도로 생각을 하고 설계를 하자. 보통 UI 작업은 꽤 비싼 작업이다. UX/UI design/UI dev등 상당히 많은 노력과 시간이 필요한 부분이다. 하지만 Use case 입장에서 나중에 UI이가 Web page에서 Command line으로 바뀌거나 stand alone 어플리케이션으로 바뀐다 해도 큰 지장 없는 설계, 즉 UI 레이어를 Use case의 플로그인 정도로 생각하고 설계를 해야한다.
  • Use case 란?
    • 사용자와 시스템이 특정 목적을 달성하기 위한 설명서 이상, 이하도 아니다.
    • 유스케이스 주도적 개발 (Use case driven development)은 보다 더 나은 설계를 줄 것이다.
    • Use case와 전달 방식 (delivery mechanism)와 분리되어야 한다.

[iOS] 런타임에 언어 바꾸기

iOS 프로젝트에서 Localizable.string 파일을 만들어 앱의 다양한 언어를 지원할 수 있다. 기본적으로 유저가 사용하고 있는 OS 세팅에 적용되어 있는 언어를 앱은 받아드리고 보여주게 될 것이다. 하지만 필자 같이 영문 OS 버전을 사용하면서 특정 앱에 한에서 한국어를 사용하고 싶을 때가 있다. 하지만 CocoaTouch에 선언되어 있는 NSLocalizedString 매크로를 통해서 앱의 언어를 바꾸려면 앱을 메모리에서 한번 지우고 다시 앱을 열어야지만 새 언어를 적용할 수 있다는 특징이 있다. 꽨 번거로운 일이다. 그래서 필자는 런타임에 언어를 바꿀수 있는 방법을 찾아보다가 괜찮은 방법을 찾았기에 소개하고자 한다.

본 글은 stackoverflow에서 찾은 솔루션이다.

링크

기본적인 아이디어는 애플에서 적용되어 있는 NSLocalizedString 매크로를 재정의 하여 사용하는 것이다.

  1. NSBundle의 카테고리를 만든다.
  2. AppDelegate에 language 코드를 알려주는 인스턴스 메서드를 만든다.
  3. NSLocalizedString 매크로를 사용하는 파일에 카테코리 파일을 임포트 한다.

NSBundle의 카테고리 만들기

  1. //
  2. //  NSBundle+RunTimeLanguage.h
  3. //  Copyright © 2016 bartysways. All rights reserved.
  4. //
  5.  
  6. @import Foundation;
  7.  
  8. #undef NSLocalizedString
  9. #define NSLocalizedString(key, comment) [[NSBundle mainBundle] runTimeLocalizedStringForKey:(key) value:@"" table:nil]
  10.  
  11. @interface NSBundle (RunTimeLanguage)
  12.  
  13. - (NSString *)runTimeLocalizedStringForKey:(NSString *)key
  14.                                      value:(NSString *)value
  15.                                      table:(NSString *)tableName;
  16.  
  17. @end
  18.  
  19. //
  20. //  NSBundle+RunTimeLanguage.m
  21. //  Copyright © 2016 bartysways. All rights reserved.
  22. //
  23.  
  24. #import "NSBundle+RunTimeLanguage.h"
  25. #import "AppDelegate.h"
  26.  
  27. @implementation NSBundle (RunTimeLanguage)
  28.  
  29. - (NSString *)runTimeLocalizedStringForKey:(NSString *)key
  30.                                      value:(NSString *)value
  31.                                      table:(NSString *)tableName
  32. {
  33.     AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
  34.     NSString *path = [[NSBundle mainBundle] pathForResource:appDelegate.languageCode
  35.                                                      ofType:@"lproj"];
  36.     NSBundle *languageBundle = [NSBundle bundleWithPath:path];
  37.     NSString *localizedString= [languageBundle localizedStringForKey:key value:key table:nil];
  38.  
  39.     return localizedString;
  40. }
  41.  
  42. @end

AppDelegate에 인스턴스 메서드 만들기

  1. //
  2. //  AppDelegate.h
  3. //  Copyright © 2015 bartysways. All rights reserved.
  4. //
  5.  
  6. @import UIKit;
  7.  
  8. @interface AppDelegate : UIResponder
  9. <
  10. UIApplicationDelegate
  11. >
  12.  
  13. @property(nonatomic, strong) UIWindow *window;
  14. @property(nonatomic, strong, readonly) NSString *languageCode;
  15.  
  16. @end
  17.  
  18. //
  19. //  AppDelegate.m
  20. //  Copyright © 2015 bartysways. All rights reserved.
  21. //
  22.  
  23. @import UserNotifications;
  24.  
  25. #import "AppDelegate.h"
  26.  
  27. @implementation AppDelegate
  28.  
  29. - (NSString *)languageCode
  30. {
  31.     NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
  32.     NSString *languageCode = ((NSArray *)[userDefaults objectForKey:@"AppleLanguages"]).firstObject;
  33.     return languageCode != nil ? languageCode : @"en";
  34. }
  35. @end

카테코리 파일을 임포트하기

  1. //
  2. //  HeaderCollectionReusableView.m
  3. //  Copyright © 2016 bartysways. All rights reserved.
  4. //
  5.  
  6. #import "HeaderCollectionReusableView.h"
  7. #import "NSBundle+RunTimeLanguage.h"
  8.  
  9. @implementation HeaderCollectionReusableView
  10. @end

VIPER 패턴의 각 요소의 역할

2014-06-07-viper-wireframe-76305b6d

MVC, MVP, MVVM과 같은 패턴들이 난무하는 세상에 VIPER라는 또 다른 개념의 패턴이 나오게 된 배경은 방대하게 커지는 View Controller를 막기 위한 수단이 아닐까 생각해본다. 기본적으로 모바일 어플리케이션에서 View Controller가 커지는 이유는 간단하다. 사용자의 이벤트 액션이 빈번하게 일어나면서 보다 느낌 좋은(?!) UI, UX를 제공하기 위함이 아니겠는가? 나는 MVC, MVP, MVVM을 실제 프로젝트에 적용해가면서 경험한 느낌 중 VIPER가 가장 깔끔한 인상을 받았다. 물론 하나라도 제대로 사용한다면 격게 될 경험일지도 모른다. 개인적인 경험상 그렇다는 것이다. 그렇다면 VIPER 패턴이 무엇인지 요소 하나하나 살펴보자.

 

Interactor

비즈니스 로직이 들어가는 클래스 종류이다. 각 클래스는 하나의 Use case에 해당하는 비즈니스 로직이 들어가야한다. NSObject를 상속받아 설계하면, iOS 프로젝트와 macOS 프로젝트에서 공유해서 함께 사용할 수 있다. 어떠한 UI와도 연관이 없어야 하며 독립적으로 존재해야 한다. Data Store에서 전달 받은 Entity는 절대 그대로 Presenter 클래스에 전달되어서는 안된다. Presenter에 전달 될 모델 역할의 클래스는 Behavior가 없는 간단한 데이터로 구성되어 있는 모델이어야 한다. 그리고 네트워킹을 사용하는 앱이라면 Interactor에서 DataManager나 DataStore와 같은 직접적으로 네트워킹 구현이 있는 클래스를 사용하여, 네트워크를 초기화하고 시작해야할 것이다. Interactor는 여러 Remote 자원에서 받은 데이터를 종합해서 정보를 Presenter에게 넘져주는 역할을 해야 할 것이다.

  • Data Store로 부터 데이터를 받아온다.
  • Entity를 조작 혹은 조합한다.
  • 조작된 Entity를 다시 Data store에 넣어 주거나, Presenter에게 화면에 뿌려주기 좋은 모양의 모델을 넘겨준다.

Entity

모델 클래스이다. Interactor 클래스에서 다루어지는 객체로써, 절대 그대로 Presenter 레이어로 넘겨서는 안된다. 그래고 만약 Core Data를 사용하고 있다면, ManagedObject로 부터 분리해서 생성되어야 한다.

Presenter

크게 두가지 역할이 있다. UI를 그려주고, Interactor 클래스에게 데이터를 요구하는 역할이다. 뿐만 아니라 VIPER의 약자중 Router에 해당하는 개념의 일부분을 담당한다. Wireframe 클래스는 다음 화면이 어딘지 안다면, Presenter는 언제 다음 화면이 나와야하는지 알고 있는 클래스이다. Presenter는 View 레이어 즉 UIViewController와 같은 클래스를 상속 받은 클래스에게 알맞는 데이터를 할당해주던가 원하는 UI 모습을 만들수 있는 클래스이다. Presenter는 View의 UILabel 이 존재하는지 UIButton이 존재하는지 알아서는 안된다. Presenter는 그려질 컨텐츠와 언제 그것이 View에 전달되어 화면에 뿌려져야하는 지만 알고 있으면 된다.

View

View는 수동적이여야 한다. Presenter가 보여줄 컨텐츠를 줄 때까지 기다려야 한다. 절대 Presenter에게 데이터를 달라고 요구해서는 안된다.  InterfaceProtocol 같은 것을 사용하여 Presenter가 View에 컨텐츠를 넣어 줄수 있게 해야한다. 비록 컨텐츠는 Presenter가 가지고 있지만 화면에 어떤 모습으로 그려져야하는지는 View가 알고 있다. View와 View Controller가 커지는 이유는 이벤트를 받고 그것을 처리하기 가장 편한 곳이기 때문이다. 하지만 View 레이어를 가볍게 만들기 위해서는 View에 관심을 가지고 있는 객체들에게 사용자가 특정 액션을 했을 때 그것을 알려주면 된다. 예를 ViewController가 이벤트 핸들러라는 이름으로 Presenter 객체를 프로퍼티로 가지고 있다고 가정하면, ‘취소’ 버튼 같은 특정 UI의 이벤트가 발생했을 때 Presentor의 메서드를 호출 하면된다. 이때 리액티브 관련 프래임워크의 힘을 빌리면 조금더 깔끔하게 코드를 짤 수 있다.

Routing

VIPER 패턴의 Routing은 Xcode의 스토리보드와 같은 역할을 하는 레이어이다. 구제적으로는 Wireframe 클래스와 Presenter 클래스가 그 역할을 한다. 먼저 Wireframe 클래스는 UIWindow, UINavigationController, UIViewController 같은 것을 소유하고 있으며, 담당 역할은 View나 ViewController를 생성하고 window에 설치하는 것이다. Presenter는 View와 인터랙션을 하고 있기 때문에 언제 새로운 창을 만들어야 하고 언제 화면전환이 이루어지는 지를 알고 있다. 반면 Wireframe은 어디로 가야하는 지를 알고 있다. 이 두 클래스를 통해서 Routing의 레이어를 완성 할 수 있다.

etc.

Data store

Data store는 Entity를 Interator에게 주는 역할을 한다. Entity를 받은 Interactor는 모델 데이터를 조작하여 알맞은 모양으로 Presenter에게 던진다.

Data Manager

Data Manager는 Data store와 비슷한 역할도 하지만, 추가로 fetch Request를 만든다든가, Query를 만들거나 하는 일을 한다. 이렇게 하므로써 Interactor는 Application 레벨의 비즈니스 로직에 더 집중 할 수 있게 해준다.

 

추가 2016.11.04

Module 간의 통신

위에서 간단히 설명한 VIPER의 구조는 iOS 프로젝트로 따지자면, 앱 기능의 의미를 갖는 ViewController를 중심으로 모듈화하여서 사용할 수 있을 것이다. 하지만 중요한 것은 모듈과 모듈 사이에 연결되는 고리를 고민해야 할 때가 온다. 예를 들어 Setting 모듈에서 적용된 작업을 List 모듈 같은 것에 적용하고 싶다면 Notification과 같은 옵져버 패턴을 사용하여 데이터 전달 혹은 메서드 호출을 할 수 있을 것이다. 하지만 더 VIPER 패턴스러운 방법은 Presenter에다가 delegate을 두고 사용하는 것이 깔끔하다. 예를 들어 Setting 모듈의 Presenter에 delegate을 선언하고, List 모듈의 presenter가 이 것을 구연하게 되면 Notification과 같은 API에 의존하지 않고 데이터 전달 및 메서드 호출이 가능해 질 것이다.

[Xcode] LLDB 유용한 명령어 모음

PO

  • 객체의 description 메서드 호출
  • 오브젝트를 반환 해주는 expression 명령어와 함께 사용 할 수 있다.

P

  • Primitive 값을 출력할 수 있다.
  • Primitive 값을 반환 해주는 expression 명령어와 함께 사용 할 수 있다.

br

  1. 브레이크 포인트 리스트 보기: br list
  2. 브레이크 포인트 삭제 하기: br delete 1
  3. 특정 브레이크 포인트 활성화 시키기: br e 1
  4. 특정 브레이트 포인트 비활성화 시키기: br di 1
  5. 동일한 메서드명을 가지고 있는 곳에 브레이트 포인트 설정하기: br set -n viewDidLoad
  6. 특정 브레이크 포인트에 조건 넣기: br mod -c “totalValue > 1000″ 3
  7. 특정 브레이크 포인트에 command 심기: br com add 2

b

  1. 브레이크 포인트 새로 만들기: b MyViewController.m:30

continue

  • 실행 재생하기

n

  • 커서 한줄 실행 시키기 (step over)
  • 단추키 F6

s

  • 커서 메서드 안에 들어가기 (step in)
  • 단추키 F7

finish

  • 커서 메소드 빠져나오기 (step out)
  • 단추키 F8

 expr -part1-

  • 런타임으로 Objective-c 코드를 실행 시켜준다.
  • [예] expr self.view.hidden = YES
  • [예] expr (void) NSLog(@”hello world”)
  • [예] expr (BOOL) [self.myArray containObject @"carKeys"]
  • [예] expr — (CGRect)[self.view frame]
  • [예] expr [self prepareForSegue:@"mySegue" sender:nil]

expr -part2-

  • 런타임에서 변수를 생성 및 조작 할 수 있다.
  • 정의하기: expr int $meaningOfLife = 42
  • 사용하기: expr 100 + $meaningOfLife
  • 예재: JSON 스트링은 오브젝트로 바꾸기
  1. expr NSString *$json = [self fetchRemoteData];
  2. expr NSData *data = [$json dataUsingEncoding:4];
  3. expr NSDictionary *$parsedDic = [NSJSONSerialization JSONObjectWithData:$data option:0 error:NULL];
  4. po parsedDic

bt

  • back trace: 브레이크 포인트를 통해 멈춰진 곳이 어떤 경로를 통해 불렸는지 보여주고, 각 단계를 왔다 갔다 할 수 있는 기능
  • 모든 thread의 back-trace를 보고 싶을 때: bt all

thread

  • Thread 정보에 대해서 알려준다. 하지만 한번에 한 Thread에서 작업 할 수 있다.
  • 불린 값으로 메소드 반환하기: thread return YES (또는) NO
  • back-trace: thread backtrace
  • 다 보기: thread all
  • 목록 보기: thread list
  • 이동하기: thread select 24
  • 브레이크 포인트 처럼 작동하기: thread until 100

frame

  • 로컬 변수 살펴보기: frame variable self
  • 정보 보기: frame info
  • 직접 이동하기: frame select 2
  • 상대 이동하기: frame -r -1

watchpoint

  • 특정한 변수 값이 변할 때 마다 실행 멈추고 값 보기
  • 리스트 보기: watchpoint list
  • 삭제: watchpoint delete 1
  • 설정하기: watchpoint set variable _x
  • watchpoint set expression — my_point
  • 조건넣기: watchpoint modify — -c “_x < 0″ 1
  • 조건 없애기: watchpoint modify -c “” 1

script

  • LLDB에는 파이썬 언어 해석기가 포함되어 있다. 그리고 브레이크 포인트에서 스크립트를 실행 시킬수 있다.
  • 실시간 파이썬 실행하기: script
  • 브레이크 포인트에 파이썬 심기: br command add -s python  1
  • 브레이크 포인트에 command 삽입하기: breakpoint command add -f my.breakpoint_func

command

  • 현존하는 스크립트를 심을 수 있다.
  • 임포트하기: command script import ~/my_script.py
  • 추가하기: command script add -f my_script.python_function cmd_name
  • 히스토리 보기: command history
  • 디버깅 스크립트 임포트하기: command import ~/my_lldb.txt

Core Data 개론

Apple이 UI와 독립적으로 Model layer에 툴로 코어 데이터를 제공하고 있다는 것은 iOS 혹은 MacOSX 어플리케이션 개발자라면 다 알 것이다. 근데 많은 사람들이 코어 데이터를 O/RM (Object-relational mapping)으로 혹은 SQL Wrapper 정도로 오해하기도 한다. 물론 SQL를 기본으로 사용하고 있기는 하지만 더 높은 차원의 추상화되어 있는 개념으로 생각해야한다. 만약 O/RM이나 SQL Wrapper를 기대하고 코어 데이터를 생각한다면 다른 것을 사용해야 할 것이다. 코어 데이터를 코코아 어플리케이션 정보 저장에 사용되는 코코아용 데이터 베이스 API로 생각하는 것도 오해 중 하나라고 할 수 있다. 디스크에 있는 저장소뿐만 아니라 흔히 데이터 오브젝트라고 불리는 메모리상의 모든 객체를 포함한다. 자바나 C#, 혹은 다른 객체 지향 언어로 작업하면 데이터 객체를 만드는 데 많은 시간이 소요되며, 작업이 자주 반복되는 특징이 있다. 코어 데이터는 대부분의 반복되는 코드를 제거해 어플리케이션의 비즈니스 로직이나 컨트롤러 계층에 집중할 수 있게 해준다.

그렇다면 코어 데이터는 무엇인가?

코어 데이터는 모델 레이어 기술이다. 간단히 말하면 코어 데이터는 디스크에 저장될 수 있는 객체 그래프이다. 코어 데이터는 몇 가지 컴포넌트들로 구성이 되어 있다. 대부분의 사례에서 사용될 수 있으며, 세업도 비교적 쉬운 편이다. 각각의 컴포넌트들은 상호 간에 묶여 있다. 각각의 컴포넌트의 개념을 설명하기 전에 아래 그림을 먼저 잘 봐두기 바란다.

출처 https://www.objc.io/issues/4-core-data/core-data-overview/

출처 https://www.objc.io/issues/4-core-data/core-data-overview/

위 그림은 코어 데이터 스택을 나타낸 것이다. 여기에서 스택이라 하면 여러 층의 레이어를 갖추고 수직적으로 데이터를 전달하는 구조를 말한다. 수직적이라는 말은 상위 레이어에서 시작해 하위 레이어로 전달이 진행된다는 의미이다. 데이터는 바로 하위 레이어나 중간에 위치한 레이어로 전달되지 않기 때문에 상위 레이어부터 차례로 접근해야 한다.

위 그림을 참고로 설명하자면 크게 세가지 레이어로 나눌 수 있다.

  • Managed Object Context 레이어
  • Persistent Store Coordinator 레이어
  • Persistent Store 레이어

상위 두 레이어는 메모리에서 데이터를 관리한다고 보면 되고, Persistent Store 레이어로 내려가면 Disk 메모리 영역으로 생각해두면 쉽다.

코어 데이터 메인 컴포넌트

먼저 쉬운 설명을 위해서 상위 개념으로 부터 실제 파일 시스템 영역 차례로 기술해보겠다.

NSManagedObject

전에 코어 데이터를 사용해 보았다면 가장 친근한 클래스 명이라 생각 되어진다. 코어 데이터를 사용하는 개발자가 가장 많이 접하게 되는 객체이기 때문이다. NSManagedObject는 Managed object model의 Entity를 서브 클래싱 받아서 소스 코드에서 사용되어 지는 Value Object인데, 이 객체는 위 그림 과 같이 특정 Managed Object Context 안에서 관리되어 진다고 할 수 있다.

NSManagedObjectContext

Managed object context는 자신이 가지고 있는 Managed object들의 변동 여부를 추적하는 것이 주된 역할이다. 그래서 -save라는 메서드를 호출하면, 변동되어진 Managed object에 한에서만 로컬 데이터 베이스에 변경 요청을 한다. 기본 적으로 코어 데이터 템플릿은 하나의 Managed object context를 사용하고 있지만, 더 복잡한 상황에서는 하나 이상의 Context를 각기 다른 thread에서 사용할 수 있다. Managed object context는 PersistentStoreCoordinator와 통신을 하며 로컬 데이터 베이스에 데이터를 불러오고나, 저장 혹은 삭제를 요청한다.

NSPersistentStoreCoordinator

Persistent Store Coordinator는 스택의 중간에 중개자의 역할을 NSManagedObjectContext와 NSPersistentStore 객체 사이에서 한다. 자체적으로 Cache 영역이 존재하기 때문에 로컬 데이터 베이스에 자료를 요청하기 전에 cache 된 것이 있다면 디스크 메모리까지 접근하지 않고 cache 영역에서 데이터를 활용한다.

NSPersistentStore

Persistent Store은 실제 SQLite 데이터 베이스와 Coordinator 사이에서 데이터 베이스 쿼리 코드를 전환해주고 실 디비와 통신하는 Wrapper의 역할을 한다. 각 데이터 베이스 별로 존재하며 새로운 데이터 베이스를 사용하고자 한다면 NSPersistentStore를 상속 받아 상용해야 한다.

실제 데이터 가지고 오기

사용자가 로컬 데이터 베이스에 있는 데이터를 가지고 오기 위해서 여러가지 과정을 걸치게 된다.

  1. NSManagedObjectContext: 가장 context 안에 자기고 있는 데이터가 있는지 조사한다. 만약 없다면 한단계 아래 레이터에 데이터를 요청한다. 해당 레이어는 임시 메모리에 해당된다.
  2. NSPersistentStoreCoordinator: 위에서 잠깐 언급 했지만 NSPersistentStore를 통해 데이터를 요청하기 전에 자신이 가지고 있는 cache를 먼저 확안하는 작업을 걸친다.
  3. NSPersistentStore: 이 레이어는 직접 디비 쿼리로 데이터 베이스에 자료를 요청하는 작업을 하게 되므로 이 안계 부터는 Disk 메모리를 사용한다고 보면 된다.

위 과정은 NSFetchRequest를 사용하여 가지고 오는 경우가 아닌 objectIdentifier를 가지고 오는 경우를 말한다. NSFetchRequest를 사용하여 데이터를 요청하게 되면 항상 NSPersistentStore 레이어까지 내려가 디스크 메모리에 접근한다고 봐야한다.

XCode가 제공하는 기본 템플릿 말고 독립적으로 코드 사용하기

Xcode 프로젝트를 새로 생성할 때에 ‘코어 데이터 사용하기’라는 옵션을 체크하고 생성하게 되면 .xcdatamodeld라는 확장자를 가진 XML 파일과 함께 AppDelegate 파일에 코어 데이터 관련되 기본 템플릿이 추가되어 프로젝트가 생성이 될 것이다. 아마 많은 사용자들은 제공 해주는 코드를 그대로 사용 할 것으로 생각되어 진다. 하지만 위에서 배운 개념들을 생각하며 AppDelegate에서 분리된 PersistentStack이라는 파일을 통해 코어 데이터를 사용해보는 코드는 다음과 같다. (아래 방법으로 코어 데이터를 세팅 할 때에는 새로운 프로젝트를 생성할 때에 ‘코어 데이터 사용하기’ 체크를 푼다음에 직접 .xcdatamodelId 파일을 추가하며 아래 클래스를 사용해야 한다.)

 

  1. // 헤더 파일
  2. #import <Foundation/Foundation.h>
  3. @import CoreData;
  4.  
  5. #pragma mark – Interface
  6.  
  7. @interface SLKPersistentStack : NSObject
  8. @property(nonatomic, strong, readonly) NSManagedObjectContext *managedObjectContext;
  9. + (instancetype)sharedInstance;
  10. @end
  11.  
  12. // 구현 파일
  13. #import "SLKPersistentStack.h"
  14.  
  15. #pragma mark – Interface extension
  16.  
  17. @interface SLKPersistentStack ()
  18.  
  19. @property(nonatomic, strong, readwrite) NSManagedObjectContext *managedObjectContext;
  20. @property(nonatomic, strong) NSURL *modelURL;
  21. @property(nonatomic, strong) NSURL *storeURL;
  22.  
  23. @end
  24.  
  25. #pragma mark – Implementation
  26.  
  27. @implementation SLKPersistentStack
  28.  
  29. #pragma mark [Getters]
  30.  
  31. - (NSManagedObjectModel *)managedObjectModel{
  32.     return [[NSManagedObjectModel alloc] initWithContentsOfURL:self.modelURL];
  33. }
  34.  
  35. - (NSURL*)storeURL{
  36.     NSURL* documentsDirectory = nil;
  37.     documentsDirectory = [[NSFileManager defaultManager]
  38.                           URLForDirectory:NSDocumentDirectory
  39.                           inDomain:NSUserDomainMask
  40.                           appropriateForURL:nil
  41.                           create:YES
  42.                           error:NULL];
  43.  
  44.     return [documentsDirectory URLByAppendingPathComponent:@"db.sqlite"];
  45. }
  46.  
  47. - (NSURL*)modelURL{
  48.     return [[NSBundle mainBundle] URLForResource:@"SLKCodingExercise"
  49.                                    withExtension:@"momd"];
  50. }
  51.  
  52. #pragma mark [Constructors]
  53.  
  54. + (instancetype)sharedInstance{
  55.     static id sharedInstance = nil;
  56.     static dispatch_once_t onceToken;
  57.  
  58.     dispatch_once(&onceToken, ^{
  59.         sharedInstance = [[[self class] alloc] init];
  60.     });
  61.  
  62.     return sharedInstance;
  63. }
  64.  
  65. - (instancetype)init{
  66.     if (self = [super init]) {
  67.         [self p_setupManagedObjectContext];
  68.     }
  69.  
  70.     return self;
  71. }
  72.  
  73. - (void)p_setupManagedObjectContext{
  74.     self.managedObjectContext = [[NSManagedObjectContext alloc]
  75.                                  initWithConcurrencyType:NSMainQueueConcurrencyType];
  76.     self.managedObjectContext.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
  77.                                                             initWithManagedObjectModel:[self managedObjectModel]];
  78.  
  79.     NSError *error;
  80.     [self.managedObjectContext.persistentStoreCoordinator
  81.                                 addPersistentStoreWithType:NSSQLiteStoreType
  82.                                 configuration:nil
  83.                                 URL:self.storeURL
  84.                                 options:nil
  85.                                 error:&error];
  86.     if (error) {
  87.         NSLog(@"error: %@", error);
  88.     }
  89.  
  90.     self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];
  91. }
  92.  
  93. @end

 
위 클래스는 싱글톤 패턴을 사용하여 만들어 보았다. 그 이유는 프로젝트 소스 코드 여러 군데에서 NSManagedObjectContext를 필요로 할 것이기 때문이다. 하지만 하나이상의 Context를 사용하게 된다면 다른 방법을 생각해봐야 할 것이다. Managed Object Model은 위에서 언급한 .xcdatamodelId 파일에 해당되는 객체이다.

마치며…

해당 글은 코어 데이터의 기본적이 구조와 개념에 대한 내용만을 다루어 보았다. 멀티 스레드 환경에서의 코어 데이터 적용에 대한 부분과 쉽게 읽어지는 코드를 위해서는 꼭 필요한 과정일 것이다. iOS 환경이나 OS X 환경에서 기본적으로 로컬 디비를 사용하겠다고 한다면 코어 데이터를 꼭 한번 고려할텐데 좀 더 기본에 충실하게 무엇이 무엇인지는 알고 사용하자는 의미에서 글을 써보았다. 앞으로 여유가 된다면 코어 데이터의 성능관련 부분과 멀테 스레드 환경에서의 코어 데이터 사용하기 부분을 올려보고자 한다. 혹시 미흡하거나 잘못 된 부분이 있다면 댓글을 남겨 주시길.

참조
  1. https://www.objc.io/issues/4-core-data/core-data-overview/
  2. 코어 데이터 맥 OS X과 아이폰 개발자를 위한 -에이콘 출판사-

클라이언트 개발자로서 Data serialization에 대한 생각

이 글은 기술에 대한 깊이 있는 이야기를 하고자 한 것이 아니라 모바일 개발자로써 깊이 없이 개발해왔던 지난 날들을 생각하며 몇자 남기는 정도의 흔적이라고 생각해주면 좋겠다.

데이터 시리얼라이즈는 이기종 간에 통신을 위해 사용되거나, 객체를 저장할 때 사용한다. 특별히 iOS 클라이언트 개발자는 객체의 인스턴트를 저장할 때 사용할 것이다.

오늘날 iOS 프로젝트에서 보편적으로 JSON 데이터 포멧으로 http 프로토콜을 사용하여 통신을 하고 있을 것이다. http 프로토콜은 스트링 프로토콜로써 사용하기 비교적 쉬운 방면에 뛰어난 성능을 요구하는 데이터 통신에서는 무거운 성능 이슈로 비추하고 있는 것이 통념적이다. 보다 빠르고 쉽게 이기종 간의 환경에서 데이터를 주고 받을 때 데이터 시리얼라이즈를 사용하여 통신을 하는데 이에 대해서 간략하게 적어본다.

데이터 통신은 기본적으로 패킷이라는 단위로 송수신을 한다. 대부분의 클라이언트 개발자는 패킷 단위의 개념까지 고려해 가면서 통신 코드를 짜고 있지 않을 것으로 여겨진다. 적어도 나는 그랬다. 아무 개념과 고민 없이 서버에서 제공하는 API를 호출 할 때면 서버팀과 약속한 데이터 포멧에 맞추어 JSON 스트링을 만들었고 이것을 NSURLRequest 같은 객체에 실어서 보내기만 했기 때문에, 패킷이 어떻게 나눠져서 전달되게 되는지 혹은 그 패킷의 모습에는 관심이 없었다. 이 말은 현재 개발환경에서 클라이언트 개발자로써 데이터 통신부분에 대하여 큰 고민은 거의 하지 않아되 되는 정도의 진입장벽인 것이다.

하지만 서버 개발자들은 고민을 할 것으로 여겨진다. 회사는 비용절감의 측면에서 더욱 낮은 코스트로 같은 기능을 구현하기 원할 것이고, 이를 실현하기 위해 여러가지 것을 고민하고 있을 것이다. 데이터 시리얼라이즈는 그런 측면에서 중요한 부분을 차지한다. 효율적이고 넒리 사용되기 쉬운 데이터 시리얼라이즈를 위해서 여러 회사에서 자신들이 만든 프로토콜을 대중이 사용해주길 바라고 있다. ProtocolBuffer는 구글에서, Thrift는 페이스북에서 시작하여 지금은 오픈소스로 발전하여 많은 프로젝트에서 각광을 받고 있다.

객체를 데이터 시리얼라이즈를 하게 되면 바이트 배열에 원하는 데이터 스키마에 맞주어 결과물을 만들게 된다. 수신하는 쪽이나 송신하는 쪽에서 스키마에 대한 내용을 알고 있다면, decode하는 과정이 JSON과 같은 텍스트 기반의 인풋을 스트링 파싱하는 것보다 성능이 뛰어나다. 뿐만 아니라 패킷 사이즈가 몇배 이상으로 작아져 보다 뛰어난 성능을 가지고 올 수 있는 장점 때문에 데이터 시리얼라이즈에 대한 성능개선이 주목을 받고 있는 것이다.

분명 각자의 프로젝트 도메인에 가장 알맞는 데이터 타입과 통신 프로토콜을 사용하는 것이 정답일 것이다. 하지만 어떤 옵션이 있는지 알아둬어야 더 깊이 있는 개발자가 될 수 있다고 생각되어지며, 만약 자신의 프로젝트에서 HTTP 기반으로 JSON을 사용하고 있다면 왜 그 방식을 선택하게 되었는지 why에 대한 고민을 했을 때 비로써 멋진 개발자로 거듭날 수 있지 않을까 생각해본다.

[Question] isBalancedTree?

문제: 인자로 들어오는 Binary Tree는 Balanced인가 아닌가를 구분하는 함수를 만들어라.

문제를 풀기 앞서 몇 가지 개념 정리를 해야 할 것 같다. Binary 트리라 불리는 이진트리 데이터 구조는 무엇인가? 이진 트리는 0개 혹은 1개, 2개의 자식노드를 같는 트리를 말한다. 그럼 해당 데이터 구조가 균형잡히다, 라는 것은 무슨 뜻일까? Balanced하다 하는 것은 자신의 자식노드의 깊이의 차이가 1보다 많아서는 안될 때 균평 잡힌 이진트리이다. 라고 말한다.

Screen Shot 2016-01-07 at 11.35.58 AM

위 예제는 균형잡힌 이진트리이다. 왜냐하면 모든 노드의 자식노드들의 깊이 차이가 1을 넘어 서는 것이 없기 때문이다.

Screen Shot 2016-01-07 at 11.36.09 AM

위 예제는 균형잡힌 이진트리가 아니이다. 왜냐하면 1의 자식들의 깊이의 차이가 1보다 큰 2이기 때문이다.

그렇다면 균형잡힌 이진트리를 찾기 위해서 어떻게해야 할 것인가? 고민해보자.

주먹구구식으로 먼저 생각을 해보자. 모든 노드의 자식의 차이를 구하고 그의 차이가 1 보다 큰 것이 나오면 false를 그 이외의 것들은 True를 반환하면 된다.

아래 답을 보기 전에 먼저 손수 풀어보자. 효율적인 함수를 짜기 위해서는 고민을 좀 해봐야 할 것이다. 왜냐하면 노드의 깊이도 구해야 할 것이고 Bool 값도 반환하고 싶을 것이기 때문이다.

이 문제의 팁은 -1를 통해 깊이 계산과 동시에 불균형를 표시하는 것이다.

  1. - (BOOL)isBalanced:(Node *)rootNode{
  2.     if([self getNodeDepth:rootNode] == -1){
  3.         return NO;
  4.     }
  5.     return YES;
  6. }
  7.  
  8. - (NSInteger)getNodeDepth:(Node *)node{
  9.     if(node == nil){
  10.         return 0;
  11.     }
  12.  
  13.     NSInteger leftDepth = [self getNodeDepth:node.left];
  14.     if(leftDepth == -1){
  15.         return -1;
  16.     }
  17.  
  18.     NSInteger rightDepth = [self getNodeDepth:node.right];
  19.     if(rightDepth == -1){
  20.         return -1;
  21.     }
  22.  
  23.     NSInteger depthDiff = Abs(leftDepth – rightDepth);
  24.     if(depthDiff > 1){
  25.         return -1;
  26.     }
  27.  
  28.     return Max(leftDepth, rightDepth) + 1;
  29. }

[Recursive Questions] Print all combination of Parenthesis

Combination 문제는 Permutation 문제의 응용문제라고 할 수 있다. 재귀함수 호출에 의한 문제 풀이를 한번 알아보자.

문제: 괄호의 수를 의미하는 integer 값을 입력을 받아 가능한 괄호의 조합을 프린트해주는 함수를 짜보자. 예를 들어 3을 입력으로 넣었다면, ((())), (())(), ()(()), (()()), ()()() 문자열이 프린트 되어야 한다.

#Walk through
0. 함수의 인자 값으로 다음과 같이 받을 것이다. (leftRemain, rightRemain, str)
1. [Base condition] 만약 rightRemain의 수가 0이라면, str을 프린트한다.
2. 만약 leftRemain가 0보다 큰지 확인한다.
2-1-a. 크다면, leftRemain을 하나 줄인 다음 str에 “(“를 추가해서 재귀 함수를 호출하자.
2-1-b. 만약, leftRemain가 rightRemain 보자 작은지 확인하자.
2-1-b-i. 작다면, rightRemain을 하나 줄인 다음 str에 “)”를 추가해서 재귀 함수를 호출하자.
2-2. 작다면, rightRemain을 하나 줄인 다음 str에 “)”를 추가해서 재귀 함수를 호출하자.

  1. - (void)printAllCombinationOfParenthesisWrapper:(NSInteger)numberOfPar{
  2.     [self printAllCombinationWithLeftRemain:numberOfPar
  3.                                     rightRemain:numberOfPar
  4.                                          string: @""];
  5. }
  6.  
  7. - (void)printAllCombinationWithLeftRemain:(NSInteger)leftRemain
  8.                               rightRemain:(NSInteger)rightRemain
  9.                                    string:(NSString *)str{
  10.     if (rightRemain == 0){
  11.         NSLog(@"%@", str);
  12.         return;
  13.     }
  14.  
  15.     if(leftRemain != 0){
  16.         [self printAllCombinationWithLeftRemain:leftRemain-1
  17.                                     rightRemain:rightRemain
  18.                                          string: [str appendString:@"("]];
  19.         if(leftRemain < rightRemain){
  20.             [self printAllCombinationWithLeftRemain:leftRemain
  21.                                         rightRemain:rightRemain-1
  22.                                              string: [str appendString:@")"]];
  23.         }
  24.     }
  25.     else{
  26.         [self printAllCombinationWithLeftRemain:leftRemain
  27.                                     rightRemain:rightRemain-1
  28.                                          string: [str appendString:@")"]];
  29.     }
  30. }

[Summary] Effective Objective-C 2.0

A. Objective-C와 친해지자

 

1. Objective-C의 근본을 이해하라

- c 언어는 function calling, objective-c는 dynamic binding을 사용하는 messaging structure이다. 가장 큰 차이는 런타임 때에 어떤 객체의 함수를 호출할지 결정된다는 점이 다르다.
- Runtime은 무거운 집을 다 들고 있다고 보면 된다. 데이터 구조와 객체에 해당하는 함수에 대한 정보들이 이에 해당되며 메모리 관리 함수들도 포함되어 있다.
- Runtime은 모든 코드를 서로 연결해주며 dynamic library를 링크해준다. 그러므로 라이브러리가 업데이트되면 재 컴파일 없이 성능적 이점을 누릴수 있게 된다.
- objective-c의 객체는 항상 stack 메모리 영역이 아닌, Heap 메모리 영역에 쓰인다. (이를 가리키는 포인터 변수는 스택에 저장되자만 말이다.) 스택에 저장된 변수 포인터들은 32 비트 컴퓨터에서는 4 바이트를, 64 비트 컴퓨터에서는 8 바이트를 할당한다.
- Heap 메모리는 레퍼런스 카운팅이라는 매카니즘으로 Objective-C가 추상화 하여 관리한다. malloc이나 free 같은 함수를 사용하지 않아도 된다.

 

2. 헤더파일에 헤더파일 importing 최소화하기

- 헤더 파일에 import는 컴파일 타임 때에 해당 파일의 내용이 복사가 되어 첨부가 되어 진다. 그렇기 때문에 헤더 파일에 import 하는 행위는 최소화 해야한다.
- 헤더 파일에 @class 예약어를 사용하여 존재여부 정도만 알리고, 클래스의 자세한 내용을 감춘다. Implementation 파일에서는 import를 함으로써 클래스의 사용을 위해서 자세한 내용을 보여줘야 한다.
- @class를 하용하여 forward declaration을 사용하면 상호 참조하는 경우를 피할 수 있게 해주는 효과를 얻을 수 있다. 상호 참조를 할 때 #import를 #include 대신 사용하면 끝 없이 루프를 돌면서 헤더파일을 크게 만들 것이며 클래스 둘 중 하나는 제대로 컴파일이 안될 것이다.
- 하지만 헤더 파일에 꼭 import를 써야할 경우도 있다.

  1. 상속받을 때
  2. protocol를 구현해야 할 때

- Class continuation에는 Delegate같은 protocol를 선언 함으로, 헤더파일를 간편하게 할 수 있다.
- 위의 것들를 염두하여 개발하면, 문제를 만들 수 있고, 유지보수를 어렵게 하는 상호참조를 피할 수 있으며, 컴파일 타임을 줄일 수 있다.
 

3. Equivalent 방식 보다 Literal 문법 선호하기

- Literal 문법을 사용하여 소스코드의 양을 줄이고 가독성을 높여라.
- 하지만 모든 면에서 좋다고 할 수는 없다. 다음 코드를 참조하라.

  1. NSArray *arrayA = [NSArray arrayWithObjects:obj1, obj2, obj3, nil];
  2. NSArray *arrayB = @[obj1, obj2, obj3];

만약 위 코드에서 obj2, obj3가 nil이라고 하자, 그러면 arrayA는 @[obj1]가 생성이 될 것이지만 arrayB는 exception을 토해 낼 것이다.
 

4. 전처리 매크로 #define보다 타입 상수 선호하기

- 전처리 매크로인 #define은 전처리 때에 정의된 내용을 replace한다. 하지만 변수 타입이 의무화 되지 않기 때문에 각 가지 문제를 가지고 올 수 있다.
- 매크로 #define 대신에 타입형을 표시된 상수를 사용하라.
- 만약 글로벌 scope으로 상수를 선언해야 할 상황이 온다면 클래스 이름을 prefix으로 붙여서 사용해야 한다.
- 로컬 scope에서는 ‘k’를 prefix로 붙여서 네이밍을 하고, 글로벌 scope에서는 클래스 이름을 붙이는 코드 컨벤션을 지켜라.
- static과 const를 함께 사용하는 것이 중요하다. (static은 .m 파일을 오브젝트 파일로 컴파일할 때 로컬 변수의 영역을 할당하겠다는 뜻이다. const는 값을 한번 지정하고 변경하고자 할때 exception을 발생시킨다.) 만약 static을 붙이지 않으면 컴파일러는 external 지시어를 붙여 글로벌 scope으로 만들 것이다.
- NSNotification의 string 상수를 사용하고자 할때 쓰인다:

  1. //.h file
  2. extern NSString * EOCLoginManagerDidLoginNotification;
  3.  
  4. //.m file
  5. NSString *const EOCLoginManagerDidLoginNotification = @"EOCLoginManagerDidLoginNotification";

 

5. 상태, 옵션, status 코드는 enum을 사용하라

- NS_Enum, NS_Option 매크로를 사용하여 enum을 생성하자.

- switch 문을 사용하여 개발할 때 enum에 정의된 것을 빼놓고 빌드하면 경고 메세지가 뜬다.

  1. //Example of 'NS_Enum'
  2. typedef NS_ENUM(NSUInteger, EOCConnectionState){
  3.     EOCConnectionStateDisconnected,
  4.     EOCConnectionStateConnecting,
  5.     EOCConnectionStateConnected,
  6. };
  7. // is Equal To
  8. typedef enum EOCConnectionState:NSUInteger EOCConnectionState;
  9. enum EOCConnectionState:NSUInteger{
  10.     EOCConnectionStateDisconnected,
  11.     EOCConnectionStateConnecting,
  12.     EOCConnectionStateConnected,
  13. };
  14.  
  15. //Example of 'NS_Option'
  16. typedef NS_OPTIONS(NSUInteger, EOCPermittedDirection){
  17.     EOCPermittedDirectionUp = 1 << 0,
  18.     EOCPermittedDirectionDown = 1 << 1,
  19.     EOCPermittedDirectionLeft = 1 << 2,
  20.     EOCPermittedDirectionRight = 1 << 3,
  21. };
  22. // is Equal To
  23. typedef enum EOCPermittedDirection:int EOCPermittedDirection;
  24. enum EOCPermittedDirection:int{
  25.     EOCPermittedDirectionUp = 1 << 0,
  26.     EOCPermittedDirectionDown = 1 << 1,
  27.     EOCPermittedDirectionLeft = 1 << 2,
  28.     EOCPermittedDirectionRight = 1 << 3,
  29. };

 
 

B. Object, 메세징, 그리고 런타임

 

6. Property 이해하기

- @property 지시어를 사용하면 클래스의 인스턴트 변수의 접근자를 자동으로 만들어주게 된다. (Auto-synthesis)
- Property는 object의 데이터를 은닉하는 기능을 제공한다.
- Dot syntax를 하용할 수 있게 해준다.
- 인스턴트 변수를 자동으로 생성해준다. 생성시 ‘_’를 prefix하여 만들어준다.
- 속성:

  • Atomicity: 기본값은 locking을 사용하는 atomic이다. 만약 nonatomic을 사용하면 locking을 사용하지 않는다.
  • Read/Write: [readwrite/readonly]
  • Memory-Management Semantics: [assign/strong/weak/unsafe_unretained/copy]
  • Method Names: [getter=/setter=]

- 생성자 함수 안에서 자신의 접근자 함수를 사용해서는 안된다. 서브 클래스가 접근자 함수를 변동하여 사용 할 수 있기 때문이다.
 

7. 내부에서 인스턴스 변수는 접근자 함수말고 직접 접근하라

- 기본적으로는 클래스 내부에서 인스턴스 변수를 가지고 올 때는 직접 가지고 오고, 값을 set할 때는 접근자 함수를 사용하라. 값을 가지고 올 때 접근자 함수를 사용하지 않는 것은 method dispatch를 걸치지 않고 가지고 올 수 있기 때문에 빠르기 때문이다. 그리고 값을 set할 때는 접근자 함수를 사용하면 디버깅할 때에 이점이 있으며 메모리 관리 및 KVO 매커니즘을 사용할 수 있다는 장점이 있다.
- 내부에서 직접 접근시

  • 빠르다
  • 메모리 관리 옵션과 무관하게 값을 할당할 수 있다.
  • KVO를 사용할 수 없다.

- 내부에서 접근자 함수를 사용시:

  • messaging dispatch가 일어나기 때문에 비용이 크다.
  • 메모리관리 가능하다
  • 디버깅에 용이하다.
  • KVO를 사용할 수 있다.

- 하지만, 예외도 있다. 생성자 함수에서는 접근자 함수를  사용하지 않는 것은 서브 클래스에서 setter를 덮어 쓸 수 있기 때문이다.
 

8. 객체의 Equality를 이해하라

- 커스텀 클래스를 만들 때 isEqual 함수를 만들어 사용하라. deep comparison과 shallow comparison이 있다.
 

9. 클러스터 패턴을 사용하여 구현을 숨기라

  1. //—- Base-class —-
  2. typedef NS_ENUM(NSUInteger, EOCEmployeeType){
  3.     EOCEmployeeTypeDeveloper,
  4.     EOCEmployeeTypeDesigner,
  5.     EOCEmployeeTypeFinance,
  6. }
  7.  
  8. @interface EOCEmployee: NSObject
  9. @property(copy) NSString *name;
  10. @property NSUInteger salary;
  11.  
  12. + (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;
  13. - (void)doADayWork;
  14. @end
  15.  
  16. @implementation EOCEmployee
  17. + (EOCEmployee *)employeeWithType:(EOCEmployeeType)type{
  18.     switch(type){
  19.         case EOCEmployeeTypeDeveloper:
  20.         return  [EOCEmployeeDeveloper new];
  21.         break;
  22.         case EOCEmployeeTypeDesigner:
  23.         return [EOCEmployeeDesigner new];
  24.         break;
  25.         case EOCEmployeeTypeFinance:
  26.         return [EOCEmployeeFinance new];
  27.         break;
  28.     }
  29. }
  30.  
  31. - (void)doADayWork{
  32. }
  33. @end
  34.  
  35. //—- Sub-class —-
  36. @interface EOCEmployeeDeveloper: EOCEmployee
  37. @end
  38.  
  39. @implementation EOCEmployeeDeveloper
  40. - (void)doADayWork{
  41.     [self writeCode];
  42. }
  43. @end

 

10. Associated Object를 사용하여 클래스에 데이터 붙이기

- void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)를 통해서 객체에 데이터를 할당할 수 있다.
- void objc_getAssociatedObject(id object, void *key)를 통해서 담겨져 있는 데이터를 불러 올 수 있다.
- NSDictionary와 비슷한 방법으로 key를 넣어 데이터를 저장 붙일 수 있는데, 다른 점이 있다면 key는 set에 사용했던 key 포인터를 써야한다.
- objc_associatedObject는 다음과 같은 Policy를 사용하여 메모리 관리를 하게 된다.

  • OBJC_ASSOCIATION_ASSIGN: assign
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC: nonatomic, retain
  • OBJC_ASSOCIATION_COPY_NONATOMIC: nonatomic, copy
  • OBJC_ASSOCIATION_RETAIN: retain
  • OBJC_ASSOCIATION_COPY: copy

- objc_associatedObject는 최후의 수단으로 사용을 해야한다. 버그가 생기면 찾기 힘든 포인트가 되기 때문이다.

  1. //Objc_association의 사용 예제
  2. // Test.m
  3.  
  4. #import <objc/runtime.h>
  5.  
  6. static void *TESTMyAlertViewKey = "TESTMyAlertViewKey";
  7.  
  8. - (void)askUserAQuestion{
  9.     UIAlertView *alert = [[UIAlertView alloc]
  10.                             initWithTitle: @"Question"
  11.                             message: @"What do you want to do?"
  12.                             delegate: self
  13.                             cancelButtonTitle:@"Cancel"
  14.                             otherButtonTitles:@"Continue", nil];
  15.  
  16.     void (^block)(NSInteger) = ^(NSInteger buttonIndex){
  17.         if(buttonIndex == 0){
  18.             [self doCancel];
  19.         }else{
  20.             [self doContinue];
  21.         }
  22.     };
  23.  
  24.     objc_setAssociatedObject(alert, TESTMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
  25.     [self show];
  26. }
  27.  
  28. - (void)alertView:(UIAlertView *)alertView
  29.         clickedButtonAtIndex:(NSInteger)buttonIndex{
  30.  
  31.     void (^block) (NSInteger) = objc_getAssociatedObject(alertView, TESTMyAlertViewKey);
  32.  
  33.     block(buttonIndex)
  34. }

 

11. objc_msgSend의 역할 이해하기

- 메세지는 다음과 같은 것을 포함 하고 있다.

  • 이름
  • selector
  • 인수(arguments)
  • 반환타입(없을 수도 있다)

- c로 만들어진 objc_msgSend 함수는 해당 객체의 메서드 테이블에 가서 해당 메서드가 있는지 확인하고 있다면, 그리로 껑충 뛰어서 코드를 진행하게 된다. 만약 찾지 못했다면 상속관계도를 따라 올라가 메서드가 있는지 확인하게 된다. 만약 그래도 없다면 message forwarding이라는 메커니즘이 끼어들게 된다.
- 함수 호출 한번에 여러 단계가 있는 것 처럼 보인다. 하지만 각 클래스 마다 캐시를 사용하여 한번 호출 된 것은 저장하였다가 사용하기 때문에 같은 객체의 같은 이름의 함수를 사용하고자 한다면 처음보다 빠르게 실행할 수 있다.

  1. id returnValue = objc_msgSend(someObject,
  2.                               @selector(messageName:),
  3.                               parameter);

 

12. Message Forwarding 이해하기

- 메세징 디스패치는 런타임에서 이루어진다. 그렇기 때문에 때로는 존재하지 않는 메서드 메세징을 시도하는 실수를 범하기도 한다. 이를 핸들링하기 위해 message forwarding 매커니즘이 존재한다.
- 핸들링하는 세가지 스탭이 있다.

  1. Dynamic method resolution: resolveInstanceMethod: 사용
  2. Replacement Receiver: – (id)forwardingTargetForSelector:(SEL)selector 사용
  3. Full Forwarding 메카니즘: – (id)forwardInvocation:(NSInvocation *)invocation 사용

- 위 핸들링을 시도해도 정의되어 있는 조치가 없으면 예외처리를 던지게 된다.

 

13. Method Swizzling을 통해 메서드 구현을 바꿔치기 하자

- 클래스의 implementation은 ‘IMP’라고 하는 함수 포인터로 저장되어 있다. 오프젝티브 씨에서는 Dynamic messaging system에서 해당 함수 포인터를 참조하여 메세지를 보내는 방식을 취하고 있다.
- 클래스마다 메서드 명들이 기술되어 있는 테이블를 조작할 수 있는 몇몇 함수들이 존재한다. 추가, 기존 implementation의 함수 포인터 가지고 오기, 바뀌치기등 몇몇 조작이 가능한데, ‘void method_exchangeImplementations(Method m1, Method m2)’가 그중의 일부이다.
- Method Swizzling은 디버깅를 위한 의도로 사용되는 것이 가장 적합하며, 가장 유용하게 사용될 수 있다.

  1. @interface NSString (EOCMyAdditions)
  2. - (NSString *)eoc_myLowercaseString;
  3. @end
  4.  
  5. @implementation NSString (EOCMyAdditions)
  6. - (NSString *)eoc_myLowercaseString{
  7.     NSString *lowercase = [self eoc_myLowercaseString];
  8.     NSLog(@"%@", lowercase);
  9.     return lowercase;
  10. }
  11. @end
  12.  
  13. Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
  14. Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
  15.  
  16. method_exchangeImplementations(originalMethod, swappedMethod);

- 위 코드를 살펴 보면 재귀 호출을 하는 것 처럼 보이지만 사실은 메서드 스위즐링을 하고 나서 이 함수의 IMP는 기존의 lowercaseString를 가리키고 있다는 것을 기억하자.
 

14. Object 이해하기 (Introspection 메서드를 최대한 사용하자)

- 오브젝트의 타입은 컴파일 타임이 아닌 런타임에 결정이 된다.
- id 타입을 사용하여 컴파일 타임에 dynamic programming을 할 수 있는데, id는 컴파일 타임때 어떤 메서드들를 호출 가능하게 구현이 되어있다. 하지만 특정 메서드 호출의 시도는 런타임으로 가지고 간다.

  1. NSString *stringType = @"Some string";
  2. id genericType = @"Some string";

- 위 예제 코드에서 genericType는 의미상 NSString 포인터이다. 하지만 다는 점이 있다면 컴파일 타임에 NSString에 기술되어 있는 메서드가 있는지 없는지 알고 있다 없다의 정도의 차이이다.
- NSObject의 프로토콜에는 introspection이라고 불리는 런타임 때에 오브젝트 타입을 확인할 수 있는 메서드를 제공하고 있다.
  1. NSMutableDictionary *dict = [NSMutableDictionary new];
  2. [dict isMemberOfClass:[NSDictionary class]];//NO
  3. [dict isMemberOfClass:[NSMutableDictionary class]];//YES
  4. [dict isKindOfClass:[NSDictionary class]];//YES
  5. [dict isKindOfClass:[NSArray class]];//NO

 
 

C. API 디자인와 인터페이스

 

15. 클래스 이름 앞에 자신만의 Prefix를 붙이자

- Objective-C에서는 name space 지원을 하지 않는다.
- Open Source 라이브러리를 사용하면, dynamic loader를 통해 적재되기 때문에 개발시 이름 충돌을 모를 수 있다.
- Apple에서 두 글자 Prefix를 예약했기 때문에 세글자 prefix를 사용해야 한다.
 

16. Designated 생성자를 만들어라

- 다양한 모든 생성자는 designated 생성자를 통해 작동해야 한다.
- 부모의 designated 생성자는 꼭 사용에 맞게 override를 해서 사용해야 한다.

  1. @interface EOCRectangle: NSObject <NSCoding>
  2. @property (nonatomic, assign, readonly) float width;
  3. @property (nonatomic, assign, readonly) float height;
  4.  
  5. - (id)initWithWidth:(float)width andHeight:(float)height;
  6. @end
  7.  
  8. @implementation EOCRectangle
  9. - (id)initWithWidth:(float)width andHeight:(float)height{
  10.     if((self = [super init])){
  11.         _width = width;
  12.         _height = height;
  13.     }
  14.  
  15.     return self;
  16. }
  17.  
  18. - (id)init{
  19.     return [self initWithWidth:5.0f andHeight:10.0f];
  20. }
  21.  
  22. - (id)initWithCoder:(NSCoder *)decoder{
  23.     if((self = [super init])){
  24.         _width = [decoder decodeFloatForKey:@"width"];
  25.         _height = [decoder decodeFloatForKey:@"height"];
  26.     }
  27.     return self;
  28. }
  29. @end
  30.  
  31. //====================================================================
  32.  
  33. @interface EOCSquare : EOCRectangle
  34. - (id)initWithDimension:(float)dimension;
  35. @end
  36.  
  37. @implementation EOCSquare
  38. - (id)initWithDimension:(float)dimension{
  39.     return [super initWithWidth:dimension andHeight:dimension];
  40. }
  41.  
  42. - (id)iniWithWidth:(float)width andHeight:(float)height{
  43.     float dimension = MAX(width, height);
  44.     return [self initWithDimension:dimension];
  45. }
  46.  
  47. - (id)initWithCoder:(NSCoder *)decoder{
  48.     if((self = [super initWithCoder:decoder])){
  49.  
  50.     }
  51.     return self;
  52. }
  53. @end

 

17. description 메서드를 작성하라

- LLVM 디버깅 명령어 중 po(print object)를 사용할 때 description 메서드를 호출하기 때문에 커스텀 클래스는 description 메서드를 작성하는 것이 좋다.
 

18. Immutable Object를 선호하라

- @property의 readonly라 하여도 KVO를 통해서 변경이 가능하다. eg. [obj setValue:@"abc" forKey:@"identifier"];
- mutable 변수를 @property로 사용할 때 직접 값을 변경하게 말고, add, remove 함수를 사용해서 간접적으로 접근하도록 하라.
 

19. 명확하고 일관적인 네이밍을 사용하라

- 긴 이름의 함수명을 짓는 것을 꺼리지 마라.
- 하지만 쓸데 없이 긴 이름은 피하라.
- 네이밍 Tip (메서드)

  1. 초기화하는 팩토리 메서드의 이름은 객체 타입을 시작으로 함수명을 지어라.
  2. parameter는 명사로 지어라.
  3. 뭔가 실행하는 함수는 <동사 + 명사>, 이런식으로 이름을 지어라.
  4. 줄임말은 피하라.
  5. boolean 타입의 반환 타입의 메서드 명은 is나 has로 시작하는 이름을 지어라.
  6. get으로 시작하는 메서드 이름은 Accessor 메서드에게 양보하라.

 

20. private 메서드의 이름에 prefix를 붙여라

- public 메서드와 private 메서드를 나누어 표히하면 디버깅이 쉬어진다.
- 리팩토리시 메서드 이름이나 signature를 바꿀 때 더 쉽게 구별이 가능하다.
- 싱글 언더스코어 “_” 로 시작하는 prefix은 Apple에서 예약을 했다. 피하라.
 

21. Objective-C의 Error 처리 방식을 이해하라.

- ARC는 예외처리 안전 모드가 기본값이 아니다.
- 메모리에 할당된 resource가 @try로 인해서 release 타이밍을 놓치게 될 수도 있다.
- exception은 치명적인 에러에만 사용하라. (fatal error)
- 일반적인 에러 처리 방법

  1. delegate으로 error를 전달하라
  2. out 파라미터로 메서드에 전달하라. eg. – (BOOL)doSomething:(NSError **)error

 

22. NSCopying 프로토콜 이해하기

- -(id)copyWithZone:(NSZone *)zone을 사용하여 객체를 복사하라.
- 최근 방식은 zone은 하나만 사용되어 지고 있다.
- copy 메서드를 오버라이드 하지마라. 대신 copyWithZone:을 구현하라.
- deep copy / shallow copy를 구분하라.
 
 

D. 프로토컬과 카테고리

 

23. Object 내부간의 통신은 delegate이나 Data source 프로토콜을 이용하라

- delegate 패턴은 데이터를 비즈니스 로직으로 부터 분리해준다.
- delegate은 @property의 weak으로 선언하여서 non-owning 관계를 유지하는 것이 중요하다.
 

24. Category를 사용하여 방대한 클래스를 나누어라

 

25. 항상 서드 파티 라이브러리의 Category는 prefix을 붙여 사용하라

- Category는 마지막으로 추가된 것이 최종으로 override하게 된다. 그러므로 prefix을 하용하여 혼란을 방지해야한다. 그래서 메서드 이름 앞에 prefix를 붙이고 category 이름도 다르게 하는 것이 좋다.
 

26. Category에서는 @property를 피하라

- 모든 property는 메인 클래스에서 선언하여 사용하라.
- category는 functionality 확장으로만 봐야지, 데이터 확장은 피하는 것이 좋다.
- 때로는 readonly property는 category에 넣어도 좋다.
 

27. Class-Continuation을 통해 구현을 감추어라

 

28. Protocol을 통해 무명의 객체를 제공하라

- eg. @property id<BTBCompletionHandler> delegate;
 
 

E. 메모리 관리

 

29. Reference Counting 이해하기

 

30. ARC를 사용하라

 

31. Reference를 release하고 Observation을 해지하는 것은 dealloc에서만 하라

- dealloc에서 NSNotification, KVO등을 해지하라.
- OS 자원인 file description이나 DB관련 객체도 dealloc에서 해재하라.
- dealloc에서 메서드 호출을 피하라.
 

32. Exception 안전 코드와 함께 메모리 관리하기

- (수동 레퍼런스 카운트 관리) try문 안에 retain하는 객체는 코드 진행중 exception 발생으로 release 할 타이밍을 놓칠 경우가 생길 가능성이 있다. Finally 구문으로 객체 해제를 해야한다.
- ARC에서 기본 값으로 finally 구문에 retain 된 객체를 해지하지 않는다. -fobj-arc-exception 컴파일러 플래그로 컴파일 타임에 release코드를 포함시켜 주자.
 

 33. Retain cycle을 피하기 위해 Weak reference를 사용하라

- 만약 own하지 않는 것은 retain하지 말라.
 

34. Autorelease Pool Block 사용하기

- 반드시 꼭 자신만의 auto release pool를 만들어서 사용해야 하는 것은 아니다.
- @autoreleasepool 문법을 사용하여 Loop 구문에 들어 가있는 tmp 변수들을 정리해주자.
 

35. Zombie 기능을 사용하여 메모리 관리를 하자

- Zombie 객체를 알아보는 디버깅 툴을 사용하여 retain되어 release되지 않는 코드를 알아보고 최적해보자.
 

36. retainCount 메서드 사용을 피하자

 
 

F. 블럭과 GCD 이해하기

 

37. Block 이해하기

- 블록은 다른 값과 맞찬가지로 자신만의 타입을 가지고 있다. Primitive 타입 변수나 Reference 타입 변수과 같이 변수와 연계할 수 있고 다른 변수 처럼 사용될 수 있다.
- 문법: return_type (^block-Name) parameters
- 블럭 밖에서 선언된 변수를 블럭 안에서 수정하고자 한다면 ‘__block’수식어를 통해 표시해야 한다.
- 블럭도 reference 카운팅을 하여 memory 관리에 속하게 된다.
- 블럭 안에서 instance 변수를 ‘__block’ 수식어 없이 사용가능 한데, self와 함께 capture된다.
 

38. 자주 사용하는 블럭 타입을 typedefine을 사용하여 선언하자

- #typedefine은 다른 타입의 alias를 제공한다.

eg. typedef int(^EOBlockName)(BOOL flag, in val) //선언

EOBlockName block = ^(BOOL flag, int val){….} // 사용
 

39. 코드 분리를 줄이기 위해 핸드러 block을 사용하라

- delegate 패턴으로 처리하여 코드가 여러 곳으로 분리되는 것은 직관적이지 못하다. 그리고 또한 delegate 함수를 여러 경우에 사용할 경우 구분해줘야 하는 코드가 추가적으로 구현되어 있어야 한다. 이럴때 블럭을 사용하여 직관적이고 표현력을 높히는 방법을 사용하는 것이 좋다.
- 블럭 처리의 두 가지 종류:

  1. startWithCompletion:failure:
  2. startWithCompletion: (하나의 블럭안에 ^(data, error))

 

40. Block이 객체를 소유하고 있어 생기는 retain cycle를 피하라

 

41. dispatch queue를 사용하여 스레드 lock을 실행하라

- GCD 출연 전 통상적으로 사용하던 방식

  1. @synchronized(self)를 통해 critical section 구현하기
  2. [NSLock() lock] / [NSLock() unlock] 메서드 직접 사용하기

- concurrent queue와 barrier_block이 추천하는 조합이다.
 

42. performSelector보다 GCD를 선호하라

 

43. GCD와 Operation Queue를 구분하여 사용하라

- GCD 강점:

  1. GCD는 C library이다.
  2. synchronization (critical section)을 구현할 경우 좋다.
  3. dispatch_once를 통해 코드 한번만 실행하기
  4. lightweight한 작업만 사용할 것을 추천한다.

- NSOperation 장점:

  1. cancelling operations
  2. operation dependencies
  3. KVO를 적용된 Operation property를 사용할 때: NSOperation은 각 단계에 있어 flag를 제공한다. 이때 KVO를 사용해 flag의 변화에 귀를 기울이자.
  4. 우선순위: GCD에서는 Queue 자체에 우선순위를 줄 수 있지만, 각 Task에 우선순위를 줄 수 없다.
  5. 재사용성 용이

 

44. Dispatch_Group을 사용하라

 

45. Thread safe한 단한번의 실행을 위해 dispatch_Once를 사용하라

- token은 반드시 static이나 global scope의 변수여야 한다.

46. dispatch_get_current_queue의 사용을 피하라

- 현재 deprecated 되었다. 디버깅을 위해서만 사용하라.
- dispatch queue는 내부적으로 hierarchy 구조로 설계되어 있으므로 단순히 해당 메서드를 사용하여 현재 사용중인 큐를 알아내는 것은 무의미 하다.
 
 

G. 시스템 프래임워크

 

47. System 프래임워크와 친해져라

- toll-free bridging은 CoreFoundation와 Foundation 프래임워크를 이어주는 역할을 한다.
 

48. Loop 대신 enumeration을 선호하라

- 통상적으로 사용하는 loop 방식은 extra 변수를 할당해야 하며 처리해야한다. 대신 enumeration 메서드를 사용하면 보다 쉽고 메모리 관리에 용이한 방법을 사용할 수 있다.
- enumerateObjectsUsingBlock:
- enumerateKeyAndObjectsUsingBlock:
 

49. Collection 타입의 자원의 메모리 관리를 위해 Toll-Free Bridging을 사용하라

- Toll-free bridging 은 foundation에서 정의한 objective-C 클래스와 CoreFoundation에 정의한 C 데이터 구조를 형볌환해주는 중요한 역할을 해준다.
 

50. NSDictionary 대신 캐시 역할을 해주는 NSCache를 사용하라

- NSCache는 memory가 꽉차면 자동으로 저장되어 있는 데이터를 release해준다. (사용한지 오래된 데이터 순서로)
- NSCache는 copy하지 않고 retain한다.
- Thread safe하다.
- NSCache 대신 NSPurgeableData를 사용해도 된다.
 

51. initialize 함수와 load 함수 사용을 선호하라

- load 메서드 사용은 디버깅 목적으로만 사용하는 것이 좋다.
- initialize는 클래스가 사용되기 전 단 한번 불려진다.
 

52. NSTimer는 자신의 타겟을 retain한다는 것을 잊지 마라

 

[번역글] Concurrent Programming: APIs and Challenges

This posting is a Korean translation of “Objc.io”‘s contents. Please contact me if I need to remove it. Thank you.

Original article: Concurrent Programming: APIs and Challenges

 

동시성(Concurrency)라는 개념은 여러 과업(tasks)이 같은 시간에 처리되는 것을 뜻한다. 이것은 싱글 CPU 코어 환경에서는 시간 분할 처리 방식이나, 멀티 CPU 코어 환경에서 진정한 병렬 처리 방식과 같이 구현될 수 있다.

OSX와 iOS는 병행 프로그래밍(Concurrent programming)을 가능케 하는 다양한 API를 제공한다. 각각 API는 다 다른 능력과 한계를 가지고 있으며, 여러 종류의 과업에 알맞게 설계되었다. 그리고 각 API는 다 다른 추상화(Abstraction) 정도의 레벨로 구현되어 있다. 하위 레벨 정도의 조작이 가능하지만 그에 따른 책임도 따른다.

병렬 프로그래밍은 난해한 문제들과 상황들이 존재하는 매우 까다로운 주제이며, GCD나 NSOperationQueue와 같은 API를 사용하면서 쉽게 까먹을 수 있는 개념이다. 먼저 이 글에서는 OSX와 iOS의 병렬처리 관련된 API를 전체적으로 살펴 볼 것이며, 전부터 내려오던 병렬 프로그래밍의 어려운 과제들에 대해서 깊이 알아볼 것이다.

 

OS X와 iOS의 병렬 API

애플의 모바일과 데스크톱 OS는 병렬 프로그래밍을 위한 동일한 API를 제공한다. 이 글에서는 pthread와 NSThread, GCD, NSOperationQueue, 그리고 NSRunLoop을 살펴 볼 것이다. 기술적으로 run loop이 소개 된다는 것이 이상할지 모른다. 왜냐하면 진정한 병렬처리와 관계가 없기 때문이다. 하지만 이 주제와 함께 자세히 알아볼 필요가 있다고 생각한다.

우리는 먼저 하위 레벨 API를 시작으로 점점 상위 레벨 API를 살펴 볼 것이다. 이렇게 작성한 이유는 상위 API들은 하위 API들 위에 만들어진 것이기 때문이다. 아무튼 실제 개발에서는 지금과 같은 방식이 아닌 상위 API를 먼저 검토하고 사용해야 할 것이다. 왜냐하면 코드를 쉽게 만들고 병렬 처리 모델을 더 심플하게 유지 할 수 있기 때문이다.

만약 우리가 왜 상위 레벨 추상화와 매우 심플한 병렬 처리 코드를 계속적으로 추천하는지 궁금 할 것이다. 그렇다면 이글의 두번째 부분과 Peter Steinberger가 작성한 쓰레드 세이프관련 포스트를 읽어보길 추천한다.

 

쓰레드

쓰레드는 운영체재의 스케쥴러에 의해서 독립적으로 스케쥴이 잡히는 프로세스의 하위 유닛이다. 가상으로 GCD와 NSOperationQueue의 모든 병렬 처리 API는 쓰레드 위해서 만들어 졌다.

하나 이상의 쓰레드는 싱글 CPU 코어에서도 실행될 수 있다. (최소한 느끼기에 동시에 실행되고 있다고 느끼게 해준다.) 운영체제는 각 쓰레드에게 짧은 시간을 할당해 주며 계산을 하는데, 사용자는 여러 쓰레드가 동시에 실행 되는 처럼 느낄 것이다. 만약 하나 이상의 CPU 코어가 탑재 되어 있다면 실제로 병렬 처리를 하여 특정 과업의 전체 시간을 줄여 줄 것이다.

멀티 CPU 코어에서 당신의 코드와 당신이 사용하고 있는 프레임워크 코드가 실행 스케쥴을 얼마나 잡는지 확인하고 싶다면, XCode의 Instruments의 CPU strategy를 사용하된다.

마음에 염두하고 있어야 할 것은 우리에게는 언제 어디서 작성한 코드가 스케쥴 잡힐지 모른다는 것이다. 그리고 또한 언제 얼마나 길게 할당을 위해 기달려야 자신의 차례가 오는지도 알 수 없다. 이런 종류의 쓰레드 스케쥴링은 매우 강력한 테크닉이다. 하지만 매우 난이도 높은 작업이다. 우리는 앞으로 이것을 조사해 볼 것이다.

복잡도에 대한 내용은 잠시 뒤로하고, POSIX 쓰레드 API를 사용하든지 Objective-C로 랩핑되어 있는 NSThread를 사용하여 자신만의 쓰레드를 만들수 있다. 다음은 pthread를 사용하여, 100만의 숫자 가운데서 가장 작은 수와 가장 큰수를 찾는 간단한 샘플 코드이다. 4개의 쓰레드가 병렬 처리하면서 찾을 것이다. 코드를 살펴 보면 왜 pthread를 직접 쓰지 말아야 할 지 알게 될 것이다.

 

  1. #import <pthread.h>
  2.  
  3. struct threadInfo {
  4.     uint32_t * inputValues;
  5.     size_t count;
  6. };
  7.  
  8. struct threadResult {
  9.     uint32_t min;
  10.     uint32_t max;
  11. };
  12.  
  13. void * findMinAndMax(void *arg)
  14. {
  15.     struct threadInfo const * const info = (struct threadInfo *) arg;
  16.     uint32_t min = UINT32_MAX;
  17.     uint32_t max = 0;
  18.     for (size_t i = 0; i < info->count; ++i) {
  19.         uint32_t v = info->inputValues[i];
  20.         min = MIN(min, v);
  21.         max = MAX(max, v);
  22.     }
  23.     free(arg);
  24.     struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));
  25.     result->min = min;
  26.     result->max = max;
  27.     return result;
  28. }
  29.  
  30. int main(int argc, const char * argv[])
  31. {
  32.     size_t const count = 1000000;
  33.     uint32_t inputValues[count];
  34.  
  35.     // Fill input values with random numbers:
  36.     for (size_t i = 0; i < count; ++i) {
  37.         inputValues[i] = arc4random();
  38.     }
  39.  
  40.     // Spawn 4 threads to find the minimum and maximum:
  41.     size_t const threadCount = 4;
  42.     pthread_t tid[threadCount];
  43.     for (size_t i = 0; i < threadCount; ++i) {
  44.         struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info));
  45.         size_t offset = (count / threadCount) * i;
  46.         info->inputValues = inputValues + offset;
  47.         info->count = MIN(count – offset, count / threadCount);
  48.         int err = pthread_create(tid + i, NULL, &findMinAndMax, info);
  49.         NSCAssert(err == 0, @"pthread_create() failed: %d", err);
  50.     }
  51.     // Wait for the threads to exit:
  52.     struct threadResult * results[threadCount];
  53.     for (size_t i = 0; i < threadCount; ++i) {
  54.         int err = pthread_join(tid[i], (void **) &(results[i]));
  55.         NSCAssert(err == 0, @"pthread_join() failed: %d", err);
  56.     }
  57.     // Find the min and max:
  58.     uint32_t min = UINT32_MAX;
  59.     uint32_t max = 0;
  60.     for (size_t i = 0; i < threadCount; ++i) {
  61.         min = MIN(min, results[i]->min);
  62.         max = MAX(max, results[i]->max);
  63.         free(results[i]);
  64.         results[i] = NULL;
  65.     }
  66.  
  67.     NSLog(@"min = %u", min);
  68.     NSLog(@"max = %u", max);
  69.     return 0;
  70. }

 

NSThread는 pthread를 랩핑한 Objective-C 클래스이다. 이렇게 랩핑을 한 것이 코코아 환경에서 더 익숙해 보인다. 예를 들어, 백그라운드에서 은닉화하고 싶은 코드를 쓰레드로 작성하고 싶다면, NSThread를 상속 받아서 작성하면 된다.

&nbsp

  1. @interface FindMinMaxThread : NSThread
  2. @property (nonatomic) NSUInteger min;
  3. @property (nonatomic) NSUInteger max;
  4. - (instancetype)initWithNumbers:(NSArray *)numbers;
  5. @end
  6.  
  7. @implementation FindMinMaxThread {
  8.     NSArray *_numbers;
  9. }
  10.  
  11. - (instancetype)initWithNumbers:(NSArray *)numbers
  12. {
  13.     self = [super init];
  14.     if (self) {
  15.         _numbers = numbers;
  16.     }
  17.     return self;
  18. }
  19.  
  20. - (void)main
  21. {
  22.     NSUInteger min;
  23.     NSUInteger max;
  24.     // process the data
  25.     self.min = min;
  26.     self.max = max;
  27. }
  28. @end

 

새로운 쓰레드를 시작하려면 쓰레드 객체를 만들어서 다음과 같이 start 메서드를 호출해야 한다.

  1. NSMutableSet *threads = [NSMutableSet set];
  2. NSUInteger numberCount = self.numbers.count;
  3. NSUInteger threadCount = 4;
  4. for (NSUInteger i = 0; i < threadCount; i++) {
  5.     NSUInteger offset = (numberCount / threadCount) * i;
  6.     NSUInteger count = MIN(numberCount – offset, numberCount / threadCount);
  7.     NSRange range = NSMakeRange(offset, count);
  8.     NSArray *subset = [self.numbers subarrayWithRange:range];
  9.     FindMinMaxThread *thread = [[FindMinMaxThread alloc] initWithNumbers:subset];
  10.     [threads addObject:thread];
  11.     [thread start];
  12. }

 

먼저 결과를 평가하기 전에 우리가 새로 작성한 쓰레드의 모든 코드가 끝나고 나서 thread 객체의 isFinished 프로퍼티를 알고 싶다면, 관심이 있는 독자들은 한번 직접 짜보길 권한다. 여기에서 진짜 말하고 싶었던 부분은 쓰레드를 직접 사용해 본다는 부분이다. pthread를 사용하든지, NSThread의 API를 사용하는 것은 우리 멘탈 모델에 적합하지 않는 비교적 시대에 뒤떨어지는 경험일지 모르지만 말이다.

마음에 직접적으로 떠오르는 한 가지 문제는, 만약 당신의 코드나 프레임워크 코드가 자신들의 쓰레드를 만들어 작동 한다면, 활성화 되는 쓰레드의 숫자가 기하급수적으로 늘어난다는 것이다. 해당 이슈는 큰 프로젝트에서 자주 발생하는 문제이다. 예들 들어, CPU 코어가 8개가 있는 환경에서 이점을 챙기기 위해 쓰레드 8개를 생성해서 사용한다고 치자, 각 쓰레드에서 위 로직이 들어 있는 프레임워크를 사용하게 되면, 결국 짧은 시간내에 수 백개나 되는 쓰레드를 만들어 낼 것이다. 각 코드들은 자신들의 책임을 다하여 작동할 것인데, 그럼에도 불구하고 결과는 좋은 모습이 아니다. 쓰레드를 잘 사용하기는 쉽지 않다. 각 쓰레드는 메모리와 커널 자원과 연결 되어 있기 때문이다.

다음으로, 우리는 큐 기반의 병령처리 관련 API를 살펴 볼 것이다. GCD와 NSOperationQueue인데, 이것들은 집중적으로 쓰레드 풀를 관리하여 해당 문제점들을 완화해 준다.

Grand Central Dispatch

늘어나는 사용자 디바이스의 CPU 코어 갯수의 이점을 개발자들이 누릴 수 있도록 애플은 Grand Central Dispatch를 OSX 10.6과 iOS4에서 부터 제공하기 시작했다. 하위 레벨의 병렬처리 API인 GCD에 대해서 이 글에서 더 자세히 알아 볼 것이다.

GCD를 사용하면 이제 더 이상 쓰레드를 직접적으로 상대하지 않아도 된다. 대신에 코드 블럭을 큐에 넣으면 밑 물에서 GCD가 쓰레드 풀을 관리 한다. GCD는 어떤 특정 코드 블럭의 쓰레드를 실행할지 정하고, 시스템의 가용 리소스에 따라서 쓰레드들을 관리한다. 쓰레드는 중앙 통제적으로 관리 되어지기 때문에 쓰레드 때문에 생기는 많은 문제점들을 줄어들게 된다.

또 다른 이점으로는 어플리케이션 개발자 입장에서는 복잡한 쓰레드 개념보다 큐 개념을 가지고 개발할 수 있게 되었다는 점이다. 새로운 병렬처리 개념은 기존 방식에 비해 쉽게 작업할 수 있다.

GCD는 다섯 종류의 큐를 제공한다. 메인 쓰레드위에서 도는 메인 큐, 세 개의 우선순위가 다른 백그라운드 큐, 그리고 I/O 처리같은 작업을 할 수 있는 보다 더 낮은 우선순위를 가지고 있는 백그라운드 큐이다. 더 나아가, 순차적 혹은 비동기식 커스텀 큐를 만들 수도 있다. 이렇게 강력한 추상화를 지닌 커스텀 큐는 특정한 하나의 글로벌 큐에 끼어 들어가게 되어 해당 쓰레드 풀에서 연관되게 된다.

Thread Pool

처음 다른 우선순위로 여러가지 큐를 만들어 사용한다는 것은 직관적으로 보일지 모른다. 하지만 우리는 어떤 경우에든지 여러분이 디폴트 우선순위가 지정되어 있는 큐를 최대한 사용하라고 권하고 싶다. 공유자원을 접근하는 과업을 작업하다 보면, 실행고자 하는 과업을 각 다른 우선순위와 함께 큐에 스케쥴링한다는 것이 예상하지 못한 작동을 불러이르킨다는 것을 알게 될 것이다. 또한 낮은 우선순위 과업이 높은 우선순위 과업을 가로막는 일이 잦아져, 프로그램 전체가 점점 느려지다가 멈추게 만들기도 한다. 이런 현상을 우선순위 전도(Priority Inversion)라고 부른다. 이 글 뒷부분에서 더 자세히 다룰 것이다.

비록 GCD는 하위 레벨 C API이지만, 사용하는데 있어서 직관적이다. 디스페칭 블럭을 GCD의 큐에 적용하므로써 병렬처리 프로그램에서 발생하는 어려움들과 알고 있는 문제점들을 생각나지 않게 된다. 병렬 프로그래밍에서 발생 가능한 문제들에 대한 내용을 서술한, 해당 글의 병렬처리 프로그래밍이 처한 난관들을 꼭 읽어보기 바란다. 더 나아가 다음 포스팅에서는 GCD를 통해 이러한 어려운 난관들을 어떻게 해결할 수 있는지 알아 볼 것이다.

 

Operation Queue

Operation 큐는 GCD API 기반으로 만들어진 추상화 된 코코아의 큐 모델이다. GCD는 Operation 큐에 비해 낮은 레벨에서 컨트롤을 할 수 있게 해주지만, Operation 큐는 어플리케이션 개발자들에게 보다 안전하고 편리한 기능을 제공한다.

NSOperationQueue는 메인 큐(main queue)와 커스텀 큐(custom queue)로 구분 할 수 있다. 메인 큐는 메인 쓰레드 위에서 돌며. 커스텀 큐는 백그라운드에서 진행이 된다. 어떤 경우든, 해당 큐에서 진행되는 과업들은 NSOperation을 상속받은 서브 클래스들이다.

You can define your own operations in two ways: either by overriding main, or by overriding start. The former is very simple to do, but gives you less flexibility. In return, the state properties like isExecuting and isFinished are managed for you, simply by assuming that the operation is finished when main returns.

사용자는 main 메서드를 오버라이드 한다든지, start 메서드를 오버라이드하는 방식으로 두 가지 방법으로 자신만의 operation을 정의할 수 있다. 첫 번째 방식(main 메서드를 오버리이드 하는 방식)은 간단하지만 사용자가 할 수 있는 것이 제한 되어있다.

  1. @implementation YourOperation
  2.     – (void)main
  3.     {
  4.         // do your work here …
  5.     }
  6. @end

만약 좀 더 많은 컨트롤을 원하거나, 비동기식 작업을 해당 operation에서 실행하기 원한다면 start 메서드를 오버라이드하면 된다.

  1. @implementation YourOperation
  2.     – (void)start
  3.     {
  4.         self.isExecuting = YES;
  5.         self.isFinished = NO;
  6.         // start your work, which calls finished once it's done …
  7.     }
  8.  
  9.     – (void)finished
  10.     {
  11.         self.isExecuting = NO;
  12.         self.isFinished = YES;
  13.     }
  14. @end

만약 start 메서드를 오버라이드 해서 사용하고자 할때 주의 할 점은 operation의 상태를 수동으로 관리해야한다. Operation 큐가 큐 안에 있는 Operation을 선택하여 실행하기 위해서는 KVO 방식을 사용하여 상태 프로퍼티 값을 설정해주어야 한다. 그러므로 만약 디폴트 접근자를 사용하지 않고 있을 경우에는, 올바르게 해당 KVO 메세지를 보내고 있는지 주의해야 할 것이다.

Operation queue에서 긴 작업을 위해 제공하는 작업 취소하기 기능을 제대로 사용하기 위해서는 ‘isCancelled’ 프로퍼티를 주기적으로 확인해야 한다.

  1. - (void)main
  2. {
  3.     while (notDone && !self.isCancelled) {
  4.         // do your processing
  5.     }
  6. }

사용자가 Operation 큐를 정의만 해놓으면 큐에 넣고 사용하는 것은 매우 쉽다.

  1. NSOperationQueue *queue = [[NSOperationQueue alloc] init];
  2. YourOperation *operation = [[YourOperation alloc] init];
  3. [queue  addOperation:operation];

반면, 클래스를 정의해서 사용하지 않아도 블럭을 큐에 삽입해서 작업 할 수 있다. 이 방식은 간편하다. 예를 들어 사용자가 한번만 사용할 작업이라면 다음 코드와 같이 큐에 넣어 사용하면 된다.

  1. [[NSOperationQueue mainQueue] addOperationWithBlock:^{
  2.     // do something…
  3. }];

비록 위 코드와 같이 블록을 사용하여 스케쥴링하면 매우 편리하지만, NSOperation을 상속 받아서 작업하는 방식은 디버깅에 매우 유용하다. 사용자가 만약 description 메서드를 오버라이드하면 현재 특정 큐 안에 있는 operations들을 쉽게 알수 있기 때문이다.

operation이나 블록을 스케쥴링하는데 있어서, GCD에서 제대로하기 쉽지 않은 몇가지 작업을 operation 큐에서 제공해주고 있다. 예를 들어 maxConcurrentOperationCount 프로퍼티를 사용하여 특정 큐에서 병렬처리 할 과업의 수를 관리하기에 용의하다. 시리얼 큐에 설정을 할 시 다른 작업과 격리시키기 위한 목적으로는 사용할 수 있다.

또 다른 편리한 기능으로는 우선순위에 따라 특정 큐의 작업들의 정렬하는 기능이다. 이 기능은 GCD의 큐의 우선순위와 다른 기능이다. 오로지 하나의 큐에 스케쥴 잡힌 과업들의 실행에 영향을 주는 기능이다. 만약 다섯 가지 우선순위를 넘어서, 실행 순서가 정해져 있는 작업을 해야 한다면, 작업 간에 의존도를 부여해 줌으로써 원하는 순서에 실행을 시킬수 있다.

  1. [intermediateOperation addDependency:operation1];
  2. [intermediateOperation addDependency:operation2];
  3. [finishedOperation addDependency:intermediateOperation];

이 코드는 intermediateOperation를 operation1과 operation2가 끝내고서 실행이 되도록 의존도를 설정해 주고 있으며, finishedOperation은 intermediateOperation이 끝나면 실행이 될 것이다. Operation 의존도 실행 순서를 잘 정의해 주는 대단히 강력한 매카니즘이다. 이것은 사용자가 의존하고 있는 operation이 끝나야지만 실행 할 수 있도록 보장해 주는 operation 그룹 같은 것을 만들수 있게 해준다.

추상화의 기본적 특성 때문에 operation 큐는 GCD API와 비교하면 성능이 약간 떨어진다는 점이 있다. 하지만 대부분의 경우에는 임팩트가 약하며, operation 큐를 선택 사용하는 것이 좋은 선택이라고 생각한다.

Run Loops

런 루프는 과업들을 병렬실행 해주지 않기 때문에 기술적으로 GCD나 Operation 큐와 같은 병렬처리 매카니즘은 아니다. 하지만 런 루프는 직접적으로 메인 operation 큐와 메인 dispatch에서 실행되는 과업들과 연관이 있으며, 비동기식 코드 실행에 관한 매카니즘을 제공한다.

런 루프는 GCD나 Operation 큐를 사용하는 것 보다 헐씬 더 쉽게 사용할 수 있다. 왜냐하면 사용자는 병렬처리 대한 복잡한 것을 고려하지 않아도 비동기식으로 일을 해낼수 있기 때문이다.

런 루프는 언제나 하나의 특정한 쓰레드에서 작동된다. 메인 쓰레드와 연관되어 있는 메인 런 루프는 UI, 이벤트, 타이머, 그리고 커널 이벤트를 처리하기 때문에 코코아와 코코아 터치 어플리케이션에 중심적인 역할을 하고 있다. 사용자가 타이머를 사용하여 스케쥴를 잡거나, NSURLConnection을 사용하거나, 혹은 performSelector:withObject:afterDelay: 메서드를 호출 하면, 비동기식 작업을 처리하는 런 루프가 사용된다.

런 루프를 사용할 때마다 기억해야 할 것은 런 루프는 여러가지 모드로 실행 할 수 있다는 것이다. 각 모드는 런 루프가 반응하게 될 이벤트들을 정의한다. 이 방식은 메인 런 루프에 있는 과업들을 넘어 임시적으로 특정 과업들에게 우선순위를 정할 수 있는 좋은 방식이다.

이것에 대한 iOS의 예로 스크롤링을 될 수 있다. 사용자가 스크롤링을 하는 동안 런 루프는 디폴트 모드로 작동하지 않는다. 그렇기 때문에 예를 들어 타이머는 반응하지 않을 것이다. 스크롤링이 끝나면 런 루프는 디폴트 모드에 반환하고 큐에 적소된 다음 이벤트를 실행하게 될 것이다. 만약 스크롤링 중에 타이머가 작동하기 원한다면 런 루프의 NSRunLoopCommonModes에 추가해야 할 것이다.

메인 쓰레드는 항상 메인 런 루프를 설정하고 실행한다. 다른 쓰레드는 기본적으로 런 루프가 설정되어 있지 않는다. 사용자는 다른 쓰레드에도 런 루프를 설정할 수 있지만, 이런 경우는 거의 드물 것이다. 대부분은 메인 런 루프를 사용하는 것이 쉽다. 만약 메인 쓰레드에서 오래 걸리는 과업을 실행하고 싶지 않다면, 메인 런 루프로 부터 다른 큐에 넣어서 작동하게 하면 될 것이다. 크리스가 쓴 블로그 포스트에서 좋은 예재를 소개할 것이다.

그래도 만약 다른 쓰레드에 런 루프를 설정하여 사용하고 싶다면, 하나 이상의 입력 소스를 추가하는 것을 잊지 말아야 할 것이다. 만약 런 루프가 입력 소스가 설정되어 있지 않으면, 매번 실행하고 할 때 갑자기 종류될 것이다.

병렬처리 프로그램이 직면한 어려움들

병렬처리 프로그래밍을 작성하는 데는 많은 어려움이 따른다. 아무리 기본적인 작업을 하려고 해도 상호작용하는 여러 과업들이 병렬처리 될때 각각의 다른 상태를 감독하는 것이 어려워진다. 문제들은 병렬처리 코드를 디버깅하기 어려운 결정되지 않은 방식으로 나타다는 것이다.

지금부터 소개할 것은 병렬처리 프로그래밍의 예견치 못한 작동에 대한 예이다. 1995년 NASA는 화성에 정찰기를 보냈다. 탐사선이 화성에 성공적으로 착륙한지 얼마 지나지 않아 미션은 거의 실패로 돌아갈 위기에 처했다. 화성 탐사선은 원인불명으로 계속적으로 재 부팅이 되기 시작한 것이다. 낮은 우선순위 작업이 높은 우선순위 작업을 지속적으로 블록킹하여 나타나는 ‘우선순위 역전’ 현상으로 어려움을 겪고 있는 것이었다. 우리는 더 자세한 ‘우선순위 역전’ 현상에 대해서는 알아 볼 것이다. 그러나 이 예가 말해주고 있는 것은 엄청난 자원과 뛰어난 엔지니어들에게도 생긴 똑같은 어려움들이 당신에게 여러가지 방식으로 다가올 것이라는 것을 말해주고 있다.

공유자원

멀티 쓰레드에서 발생하는 수 많은 문제들의 근본지는 공유자원에 접근이다. 여기에서 말하는 자원은 클래스의 프로퍼티나 객체, 혹은 보통 경우 메모리, 네트워크 기기, 파일 등이 될 수 있다. 멀티 쓰레드 사이에서 공유하는 모든 것이 잠재적인 문제점이라고 할 수 있다. 그리고 반드시 안전한 측정을 취함으로 이러한 문제점들을 피해야 할 것이다.

소개한 문제를 재현해보기 위해 카운터로 사용하는 정수 프로퍼티 방식으로 되어 있는 자원을 예로 들어 보자. A와 B라고 하는 두 개의 쓰레드가 카운터를 동시에 평행하게 돌아간다고 가정해 보자. 문제는 C나 오브젝티브 C의 작성된 코드는 그냥 하나의 CPU의 기계식 명렁어 아니라는 것이다. 카운터의 숫자를 하나 올리기 위해서는, 현재 카운터의 값을 메모리에서 부터 불러와야 할 것이고, 해당 값을 하나 올린 다음에 다시 메모리에 써야할 것이다.

두 쓰레드가 동시에 처리하려고 시도하는 것을 상상해보라. 예를 들어, 쓰레드 A와 쓰레드 B가 17인 카운터의 값을 메모리로 부터 읽었다고 가정해보자. 쓰레드 A가 카우터의 값을 하나 올린 다음 18를 메모리에 쓰고, 쓰레드 B도 값을 하나 올린 다음 18이라는 값을 메모리에 썼다고 생각해 보라. 이 해당되는 데이터는 두번 값을 올렸음에도 실질적으로 1 밖에 안올라가는 오류가 생긴 것이다.

Screen Shot 2015-10-06 at 11.15.10 PM

이런 문제를 경함 조건(race condition)이라고 부르며, 멀티 쓰레드 환경에서 특정 쓰레드가 작업이 끝나기도 전에 다른 쓰레드가 동시에 자원에 접근할 때 항상 발생한다. 이런 상황을 막기 위해서는 멀티 쓰레드는 상호 베타적인 방식으로 공유자원에 접근해야 한다.

현실에서는 현존하는 CPU들이 최적화라는 이유로 읽기-쓰기 순서를 계속적으로 바꾸기(Out of order execution) 때문에 예재 보다 헐씬 복잡하다.

상호 베타

상호 베타적 접근이라는 뜻은 특정 자원에 한번에 하나의 쓰레드만 접근한다는 것을 말한다. 이것을 보장하기 위해서는 공유 자원에 접근하고자 하는 쓰레드는 먼저 뮤텍스 락을 획득하는 것이 필요하다. 자신의 작업이 끝나면 락을 풀어줌으로써 다른 쓰레드가 접근할 수 있는 기회를 얻어야 한다.

Screen Shot 2015-10-06 at 11.26.27 PM

상호 베타적 접근을 보장하기 위해서 추가적으로, 락은 반드시 순서 바꾸기 실행(out-of-order execution)을 통해 생기는 문제점들을 처리해야 한다. 프로그램 명령에 의해 정의된 순서에 따라 CPU가 메모리에 접근하는 것만 의지 해서는 부족하다. 사이드 이팩트를 피하는 방법은 메모리 방벽(barrier)을 사용하는 것이다. 메모리 방벽을 설정하면 순서 바꾸기 실행이 방벽에서는 생기지 않는다.(Setting a memory barrier makes sure that no out-of-order execution takes place across the barrier.)

물론 뮤텍스 락를 구현하는 것 자체로 경합 조건을 피할수 있다. 간단한 일이 아니며 현존하는 CPU에 대한 특별조취를 사용하는 것이 요구된다. 더 자세한 내용을 알고 싶다면 데니엘의 낮은 수준의 병렬처리 테크닉에 대한 글를 읽어보기 바란다.

Objective-C는 프로퍼티 선언시 atomic이라는 키워드 사용을 통해 언어적 레벨의 락 기능을 지원하고 있다. 프로퍼티를 atomic으로 선언하는 것은 공유자원의 매 접근시도 시 락과 언락을 해주는 결과를 가져다 준다. 만약을 위해 모든 프로퍼티를 atomic으로 선언해 줄수도 있지만 항상 비싼 비용이 든다는 것을 생각해야 한다.

자원에 락을 건다는 것은 언제난 성능적 비용든다. 락을 걸고 푸는 것은 멀티코어 시스템에서 구현하기 쉽지 않은 경함조건을 없는 것이 필요하다. 또한 락을 걸면 쓰레드는 먼저 자원은 사용하고 있는 다른 쓰레드 때문에 기달려야 한다. 이런 경우에는 sleep에 빠지게 되고 사용하던 쓰레드의 작업이 끝나면 노티를 받게 된다. 이모든 처리는 비용이 비싸고 어렵다.

또 다른 종류의 락이 있다. 어떤 락은 매우 저렴하지만 자원 경쟁에 약하다. 어떤 락은 기본적으로 비용이 비싸지만 자원 경쟁에서 강력하다.

락을 걸고 푸는 것에는 장단점이 있다. 그러므로 critical 섹션에 자주 들락거리는 것은 삼가해야 할 것이다. 동시에, 긴 실행 코드로 인해 락을 오래 잡는 것은 자원을 쓰고자 하여 락을 걸길 원하는 다른 쓰레드의 일을 하지 못하게 할 수도 있다. 참 어려운 문제이다.

동시에 실행하도록 짜여진 코드는 쉽게 접할 수 있지만, 공유 자원을 락하는 방식이 정해져 있어서 실제로 결과적으로는 하나의 쓰레드만 활성화 되는 결과를 낳는다. 사용자는 CPU 코어가 그들의 코드를 언제 스케쥴 잡아 사용할지를 판단하기 쉽지 않다. XCode의 Instrument의 CPU strategy 뷰를 통해 CPU 코어 할당을 효율적으로 사용하고 있는지 알아 볼수는 있을 것이다.

Dead Locks

뮤텍스 락은 경합조건을 해결해주지만 동시에 데드락이라는 새로운 문제도 만들어 낸다. 데드락은 하나 이상의 쓰레드가 다른 쓰레드의 작업이 끝나길 서로 기다리면서 생기는 현상이다.

Screen Shot 2015-10-13 at 1.22.50 PM

두 값을 스와핑하는 다음 예제를 한번 살펴보자.

  1. void swap(A, B)
  2. {
  3.     lock(lockA);
  4.     lock(lockB);
  5.     int a = A;
  6.     int b = B;
  7.     A = b;
  8.     B = a;
  9.     unlock(lockB);
  10.     unlock(lockA);
  11. }

위 코드는 거의 항상 잘 돌 것이다. 하지만 쓰레드가 동시에 상대방의 값으로 호출할 때는 발생하게 된다.

  1. swap(X, Y); // thread 1
  2. swap(Y, X); // thread 2

위와 같이 샐행하면 데드락에 빠질 것이다. 쓰레드 1은 x에 락을 걸것이고, 쓰레드 2는 y에 락을 것 것이다. 이렇게 된다면 두 쓰레드는 서로의 락이 풀리길 기다릴 것이며, 영원히 끝나지 않을 것이다.

다시한번 말하지만, 공유자원을 많은 쓰레드에 공유하거나, 락을 더 많이 걸 수록, 데드락에 빠질 위험을 더 커진다. 그렇기 때문에 최대한 간단하게 코딩을 해야 할 것이며, 쓰레드 사이에 공유자원을 최소화 해야한다. 로우 레벨의 병렬처리 API에 관한 글도 꼭 읽어 보길 바란다.

Starvation

고려해야 하는 상황이 많다고 생각할지 모르지만 새로운 난제를 하나 더 소개하고자 한다. 공유자원을 락하는 것으로 인해 읽기-쓰기 문제를 초래할 수있다. 많은 경우에 자원을 읽기 위한 접근을 한번에 하나씩 하는 것은 낭비일지도 모른다. 그러므로 리소스에 쓰기 락이 없는 한 읽기 락을 허용하는 경우가 많다. 이럴 경우에는 한 쓰레드에서 쓰기위한 락을 걸기위해 읽기위한 락들이 풀리길 기다리다가 굶줄이는 경우가 생긴다.

이 문제점을 해결하기 위해서는 읽기-쓰기 락과 같은 간단한 방법보다는 현명한 솔루션이 필요하다. 예를 들어 writer preference를 주거나 읽기-복사하기-갱신하기 알고리즘(read-copy-update algorithm)을 사용한다. 데니엘이 그가 쓴 로우 레벨 병렬처리 기법에 관한 글을 통해 GCD를 사용한 독자/싱글 writer 패턴을 어떻게 구현하는지 알려 줄것 이다.

우선순위 역전현상

우리는 이 글을 초반부에 NASA의 화성 탐색선이 겪은 병렬처리 관련 문제로 시작하였다. 지금부터 우리는 왜 탐색선이 거의 실패로 돌아 갈 뻔했는지 알아 볼것이며, 왜 우선순위 역전현상이라 불리는 이 어려움을 우리도 똑같이 당할수 있는지 알아 볼 것이다.

우선순위 역전현상은 낮은 우선순위 과업이 높은 우선순위 과업의 실행을 막는 상황를 말하는데, 효율적 진행을 위해 우선순위가 역전되는 것을 말한다. GCD가 파일 I/O를 위한 큐를 포함해 백그라운드 큐를 다양한 우선순위를 가지고 제공되고 있는 지금 꼭 알아두어야 할 중요한 내용이라고 생각한다.

해당 이슈는 높은 우선순위 과업과 낮은 순위 과업이 공유자원을 함께 사용할 때 생길 수 있다. 낮은 우선순위 과업이 공유자원을 사용하기 위해 락을 걸었다면, 높은 우선순위의 과업이 공유자원을 사용하고자 할 때 아무런 거리낌 없이 사용할 수 있도록 락을 일찍 풀어야 한다. 낮은 우선순위 과업이 자원을 점유하고 있으므로 높은 우선순위 과업이 블럭당하면, 그 사이에 큐 안에서 실행 가능한 과업 중 가장 높은 중간정도 되는 우선순위 과업이 높은 우선순위 과업 보다 앞서 실행될 가능성을 열어주게 된다. 이때 중간 우선순위 과업이 낮은 우선순위 과업의 락을 풀는 것을 방해하여 높은 우선순위 과업은 계속 공유자원의 락이 풀리길 기다리게 되여 실행하지 못하게 된다.

Screen Shot 2015-10-13 at 1.35.50 PM

당신의 코드에서는 화성의 탐사선이 재부팅을 하는 것 만큼 심각한 상황을 초래하지 않을 수 있지만 우선순위 역전현상은 꽤 자주 발생하는 현상이다.

특별한 경우가 아니면 서로 다른 우선순위 큐를 사용하지 않길 권유한다. 가끔 높은 우선순위 과업이 낮은 우선순위 과업이 끝날 때 까지 기다리는 현상이 이럴날 수 있기 때문이다. GCD를 사용할 때, 디폴트 우선순위 큐를 사용하길 권한다. 만약 다른 우선순위 규를 사용하게 되면, 상황을 더 악화시킬 수 있다.

하나 이상의 큐를 사용할 때 각기 다른 우선순위를 설정하여 사용하는 것은 좋아 보이지만, 병렬처리 프로그래밍에서 ‘우선순위 역전현상’과 같은 과거의 경험을 통해 보면, 복잡성을 높이게 되며 예상할 수 없는 상황을 만들게 된다. 만약 앞으로 개발을 하면서 높은 우선순위 과업이 예상한 타이밍에 실행되지 않을 때 NASA의 전문 엔지니어도 경험한 ‘우선순위 역전현상’을 떠올리게 될 것이다.

마치며…

우리는 API가 아무리 사용하기 쉽게 보일지라도 해당 글을 통해 병렬처리 프로그래밍의 복잡성을 알려주길 원했고 그에 수반한 문제점을 알아 보길 원했다. 이러한 문제들은 가지고 오는 결과들은 어디가 문제인지 쉽게 파악하기 어렵고, 디버깅하기 조차 매우 어렵다.

반면에 병렬처리 기술은 멀티 코어 컴퓨팅의 이점을 사용할 수 있는 매우 강력한 도구이다. 성공의 열쇠는 병렬처리 모델을 최대한 간단하게 유지하여 락을 필요할 때만 걸라고 하는 것이다.

우리가 추천하는 안전한 방법은 메인 쓰레드에서 데이터를 뽑아내서 백그라운드 큐에서 실제 처리를 하고 결과를 다시 메인 쓰레드에 전달하라는 것이다. 이 방식은 락을 걸지 않게 되하여 실수를 줄일 수 있을 것이다.