안드로이드 프로젝트에서 MVC, MVP, MVVM을 위한 간략한 가이드

Model View Controller(MVC)
MVC 디자인 패턴은 세가지 양상으로 앱을 쪼갠다: Model, View, Controller. 일들을 강제로 분리시켜 도메인 model과 controller 로직을 유저 인터페이스(View)로부터 분리시켜준다. 결과적으로 앱을 유지보수하기 간편하고 테스트하기 쉽게 만들어준다.

Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

Controller
Controller는 들어온 요청을 처리하는 역할을 한다. Vie를 통해 사용자의 입력을 받으면 Model의 도움으로 사용자의 데이터를 처리하고 다시 그 결과를 View에 보내준다. 보통은 View와 Model 사이에 중재자 역할을 한다.

Model View Presenter(MVP)
이 패턴은 Controller 대신 Presenter가 들어간 MVC와 유사한 형태의 패턴이다. 이 디자인 패턴은 세가지 주 양상으로 앱을 쪼갠다: Model, View, Presenter


Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

Presenter
Presenter는 View의 도움으로 모든 UI 이벤트를 다루는 역할이다. view를 통해 사용자로부터  입력을 받고, Model의 도움으로 사용자의 데이터를 처리한 뒤, 다시 View에 결과물을 돌려준다. View와 Controller에서와는 다르게 View와 Presenter는 서로 완전히 분리되있고 인터페이스에의해 서로 소통하는 방식이다.
또한 Presenter는 Controller처럼 들어오는 요청 트래픽을 관리하지 않는다.

MVP 패턴의 요점
  • 사용자는 View에서 상호작용한다.
  • View와 Presenter는 one-to-one 관계를 가진다. 이 의미는 하나의 View는 하나의 Presenter에 맵핑된다.
  • View는 Presenter에 참조하고 있지만, Model에는 참조하지 않는다.
  • View와 Presenter 사이에 두 방향으로 소통할 수 있다.

Model View ViewModel(MVVM)
MVVM은 Model-View-ViewModel로 정의된다. 이 패턴은 View와 ViewModel 사이에서 두방향 데이터 바인딩을 지원한다. 이것은 ViewModel에서 View에게 자동으로 변화를 전달할 수 있다. 일반적으로 ViewModel에서 View로 변화를 알림받는 옵저서 패턴을 사용한다.


Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

ViewModel
ViewModel은 View의 상태를 유지, View의 액션 결과로 Model을 다루기, View 자체에서 이벤트를 트리거하는 그런 메소드나 명령, 다른 프로퍼티들을 노출시키는 역할을 한다.

MVVM 패턴의 요점
  • 사용자는 View에서 상호작용한다.
  • View와 ViewModel은 many-to-one 관계를 가진다. 그 의미는 여러 View는 하나의 ViewModel에 맴핑될 수 있다.
  • View는 ViewModel에 참조되지만 ViewModel은 View에대해 모른다.
  • View와 ViewModel의 사이에 두방향 데이터 바인딩을 제공한다.

안드로이드 구현




앞으로 "Controller"라는 용어를 앞에서 말한 Controller, Presenter, ViewModel과 같은 의미로 사용할 것이다.

일반적으로 안드로이드에서는 Activity 클래스가 Controller이고 Fragment 클래스가 View 영역이다. 그러나 이것은 코드의 재사용성을 줄인다. 또한 Fragment와 Activity는 제한적으로 화면 전환 애니메이션이 가능하다.


View 영역(layer)을 위한 UI 클래스(i.e. LinearLayout) 
View 영역은 LinearLayout이나 ViewGroup과 같은 View(UI) 엘리먼트를 상속하여 View를 구현할 수 있다.
  • Activity / 앱 플로우의 독립적인 기능을 재사용할 수 있다.
  • Activity 수를 줄인다.(앱 용량을 덜 잡아 먹는다)
  • Controller와 의존적인 부분을 줄인다.

(독립된) Controller 클래스
Controller 클래스는 어떤 안드로이드의 클래스를 상속받아서도 안된다. Activity와 Fragment로부터 독립적이게 해주어야 재사용이 가능할 것이다.
  • Controller를 가볍게 만들어라, View와 Model을 연결해주는 역할만 하면 된다.(단일 책임 원칙)
  • 이벤트를 다른 Controller에게 넘겨주어라(i.e. analytics)
  • 안드로이드 클래스로부터 분리해라 재사용을 위해

관련된 좋은 습관들
Activity 클래스의 의존성을 줄이기
  • Controller는 추상화에 의존한다(interface)
  • 시스템 구성과 의존성을 컨트롤하기위해 코드 중심에 위치시킨다.
  • 차후에 의존성 주입(dependency injection)을 옮길 수 있게 한다.

Analytics, A/B 테스트 등을 분리하기 위한 파사드(facade)
  • 우리는 여러 기록장치를 사용하기 때문에 파사드를 이용해 이 API들을 한데 모아둘 필요가 있다.
  • A/B 테스트나 다른 임시적인 기능들은 추상화돼있어야하고, 분리된 Controller나 파사드를 통해 접근가능해야한다.

이것들은 어떻게 생겼을까?
테스트에 용이
저렇게 잘 구현하여 Activity로부터 완전히 분리시킨다면, 테스트하기 아주 수훨해 질 것이다. 일반적으로 Activity는 수많은 것(디바이스의 시스템 구성, 네비게이션, 스타일, 액션바..)을 다루며 이것이 테스트의 범위를 너무 크게 만들어버린다.

위와같은 방법으로 Robelectric 테스트는 모든 의존성의 모의 객체(mock)를 만들어주고, 바깥에서 유닛 테스트 할 수 있게 해준다.

사용가능한 라이브러리&프레임워크
아래 안드로이드 프레임워크는 앞서 말한 것들을 구현할 수 있게 해준다. 우리는 지금 시점에서 저것들이 필요하지 않을 수 있으나, 나중에 코드베이스 전반에 걸쳐 적용시켜보려한다면 손쉽게 사용해볼 수 있을 것이다.

  • Square mortar: Activity 라이프 사이클의 행동으로부터 분리시켜, View를 가볍게 만들고 View를 Controller와 한 쌍으로 만들어주는 간편한 라이브러리
  • inloop AndroidViewModel: 엄청난 양의 코드 없이 Fragment나 Activity로부터 데이터와 상태를 분리시켜준다. 벙어리(dumb) View가 되는걸 줄인다.
  • sockeqwe mosby: 현대 안드로이드 앱을 위한 Model-View-Presenter 라이브러리.


'그 외' 카테고리의 다른 글

[번역] 리액티브 프로그래밍이란?  (0) 2017.03.03

WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

약 4달전, 우리팀(Marco, Arne, and Daniel)은 새 앱의 모델 레이어를 설계하기 시작했다. 우리는 테스트를 개발 과정에서 사용하고 싶었고, 회의를 거쳐 XCTest를 테스트 프레임워크로 정했다.

(테스트도 포함한) 우리의 코드베이스는 190개의 파일과 18,000 라인의 소스로 544KB까지 커져있었다. 우리 테스트에 들어가보면 우리가 테스트할 코드의 2배정도 되는  1,200KB 크기나 된다. 아직 프로젝트가 끝난 상황은 아니지만 거의 마무리 단계에 있다. 이 글을 통해 우리가 무엇을 배웠는지, 일반적인 테스트에 관하여나 XCTest에 관한 주제를 공유하고 싶다.

이 프로젝트는 아직 앱 스토어에 올라가지 않고 진행중이기 때문에 몇몇 클래스 이름이나 메소드 이름은 계속 바뀌어오고 있는 중임을 유의하라.

우리가 XCTest를 고른 이유는 간단하고 Xcode IDE와 잘 결합되기 때문이다. 이 글이 여러분의 XCTest를 고르거나 다른 것을 고를 때 결정을 도와줄 수 있길 바란다.

우리는 이 이슈와 비슷한 주장으로 이어가려 노력했다.

왜 테스트 해야하나
article about bad practices in testing에서 언급했듯, 많은 사람들이 "우리가 코드를 바꿀 때만 테스트 할 가치가 있다"고 생각한다. 이것에 대해 더 명확하게 짚고 싶으면 위 글을 읽어보면 된다. 그러나 사실 첫 버전의 코드를 작성할 때는 코드를 수정하는데 많은 시간이 들 수 밖에 없음을 인지해야한다.—프로젝트가 진행됨에 따라 더 많은 기능들이 추가되며, 그러면 코드 여기저기를 조금씩 수정해야 할 것이다. 따라서 1.1버전이나 2.0버전의 작업이 아니더라도 여전히 수많은 변경할 부분이 있을 것이고, 이때 테스트는 많은 도움을 줄 것이다.

우리는 아직도 최초버전의 프레임워크를 완성하는 과정에 있으며 최근데 10 man months 이상동안 1,000개의 테스트 케이스를 통과시켜 왔다. 우리 프로젝트 아키텍처가 명확한 버전을 가지고 있지만, 여전히 그 방법으로 코드를 수정하고 맞추고 있다. 계속 증가하는 테스트 케이스들은 이렇게 우리를 도와왔다.(원문: The ever-growing set of test cases have helped us do this.)

테스트는 우리 코드의 품질을 안정적으로 만들 수 있게 해주고, 코드를 부수지 않고 리팩토링이나 수정을 할 수 있는 능력을 가지게 해준다. 그리고 모든 코드가 합쳐지지 않아도 매일 코드를 실제 돌려볼 수 있게 해주었다.

XCTest는 어떻게 동작할까
애플은 XCTest 사용하기라는 문서를 제공한다. 테스트는 XCTestCase 클래스의 서브클래스 안에 그룹되어 만들어진다. test로 시작하는 각 메소드들이 실제 테스트이다.

테스트는 간단한 클래스나 메소드이기 때문에, 우리가 원하는 것에 맞춰 @property나 필요한 메소드를 테스트 클래스에 추가할 수 있다.

우리는 코드를 재사용하기 위해 모든 테스트 클래스의 수퍼클래스는 일반적으로 TestCase이다. 이 클래스(TestCase)는 XCTestCase의 서브클래스이다. 모든 테스트 클래스는 TestCase를 수퍼클래스로 한다.

또한 TestCase 안에 다 같이 사용하는 헬퍼 메소드도 하나 넣겠다. 그리고 각 테스트에 필요한 프로퍼티도 넣겠다.(원문: And we even have properties on it that get pre-populated for each test.)

네이밍
test라는 단어로 시작하는 메소드가 하나의 테스트이고, 일반적으로 테스트 메소드는 아래와 같이 생겼다:

우리 모든 테스트들은 "testThatIt"으로 시작한다. 테스트 네이밍에서 자주 쓰는 또 다른 방법은 testHTTPRequest처럼 테스트된 클래스나 메소드 이름을 사용하는 것이다. 그러나 이것은 가볍게 보기만해도 그 테스트의 의미를 바로 알 수 있을 것이다.

"testThatIt" 스타일은 우리가 원하는 결과에 초점이 쏠리고, 대부분의경우 한번에 이해하기 힘들다.

각 제품 코드 클래스의 테스트 클래스가 있고, 어떤 것은 Test가 접미에 붙기도 한다. HTTPRequestHTTPRequestTests클래스가 커지면 이것을 토픽에 따라 카테고리로 쪼개는 작업을 한다.

앞으로 영원히 테스트를 할 필요가 없으면 접두에 DISABLED를 붙인다:

이렇게하면 검색하기도 쉽고, 더이상 메소드 이름이 test로 시작하지도 않음으로 XCTest가 알아서 이 메소드를 생략한다.

Given/When/Then
우리는 모든 테스트를 Given-When-Then으로 나누어 만드는 패턴 구조를 사용한다.

given은 모델 오브젝트들을 만들거나 테스트를 위한 특정 시스템 상태로 만들어 테스트 환경을 셋업하는 영역이다. when은 테스트 하고 싶은 코드를 가지고 있는 영역이다. 대부분 테스트할 메소드 하나를 호출한다. then은 액션의 결과를 확인하는 역역이다. 우리가 기대하던 결과가 나왔는지, 오브젝트가 변경되었는지등을 확인한다. 이 영역은 assertion으로 구성되있다.

아래에 꽤 간단한 테스트가 있다:

이 기본 패턴을 따름으로서 더 짜기쉽고 이해하기 쉽게 해준다. 가독성을 높히기 위해 해당 영역의 상단에 "given", "when", "then"을 주석으로 달아놨다. 이 경우는 테스트된 메소드가 즉시 눈에 띈다.

재사용 가능한 코드
테스트를 여러번 하다보니, 테스트 코드 속에 자꾸 자꾸 재사용되는 코드를 발견했다. 비동기적 처리를 완료할때까지 기다리거나, CoreData 스택을 메모리에 옮기는 그런 코드들을 중복해서 사용하고 있었다. 우리가 최근에 사용하기 시작한 또다른 유용한 패턴은 XCTestCase 클래스에서 직접 프로토콜을 델리게이트하는 것을 구현하는 것이다. 이렇게 하면 엉성하게 델리게이트를 모의객체로 만들지 않고, 꽤 직접적인 방법으로 테스트 할 클래스와 소통할 수 있다.

It turned out that this is not only useful as a collection of utility methods. The test base class can run its own -setUp and -tearDown methods to set up the environment. We use this mostly to initialize Core Data stacks for testing, to reseed our deterministic NSUUID (which is one of those small things that makes debugging a lot easier), and to set up background magic to simplify asynchronous testing. 

Another useful pattern we started using recently is to implement delegate protocols directly in our XCTestCase classes. This way, we don’t have to awkwardly mock the delegate. Instead, we can interact with the tested class in a fairly direct way.

모의객체(Mocking)
우리가 쓰는 모의객체 프레임워크는 OCMock이다. 이 모의객체 주제의 아티클에서 이야기하듯, 모의객체는 메소드 호출에 준비된 결과를 반환하는 오브젝트이다.

우리는 모의객체를 한 오브젝트의 모든 의존성을 위해 사용한다. 이렇게 하면 타깃 클래스를 독립적으로 테스트할 수 있다. 단점이 있다면, 그 클래스에서 뭔가 바뀌게되면 그 클래스에 의존하는 다른 클래스의 유닛 테스트를 자동으로 실패로 만들지 않는다. 그러나 우리는 모든 클래스를 함께 테스트하는 통합 테스트를 하여 이 문제를 해결할 수 있다.

우리는 'over-mock'하지 않도록 주의해야하는데, 이것은 테스트할 하나를 제외한 나머지 모든 오브젝트를 모의객체로 만드는 것이다. 우리가 처음 시작할 때 이런 방식으로 테스트 했었고, 심지어 메소드에 입력하기위해 사용된 간단한 오브젝트까지도 모의객체로 만들었다. 이제는 많은 오브젝트를 모의객체 없이 사용하는 방법으로 테스트 하고 있다.

모든 테스트 클래스를 위한 우리 일반적인 슈퍼클래스의 일부이고, 한 메소드를 추가한다. 
이것은 메소드/테스트 마지막에서 검증하는 모의객체이다. 이것이 모의객체 사용을 더욱 편리하게 만든다. 우리가 만든 모의객체가 그 지점에 옳바르게 있는지 확인할 수 있다:

상태와 상태없음(State and Stateless)
지난 몇년동안 상태없는 코드를 많이 이야기해왔다. 그러나 결국 우리 앱은 상태를 필요로 했다. 상태가 없는 앱은 꽤 요점을 잃어버린다. 반대로 상태를 관리하면 그것이 굉장히 복잡하기 때문에 수많은 버그를 만들어 내기도 한다.

우리는 상태로부터 코드를 떼어내어 작업하기 쉽게 만들었다. 몇몇 클래스는 상태를 가지고 있으나 대부분의 클래스에는 상태가 없다. 또한 코드를 테스트하기도 아주 쉬워졌다.

예를들어 우리가 EventSync라는 클래스가 있는데, 이 클래스의 역할은 로컬의 변화를 서버에 보내는 것이다. 이것은 어떤 오브젝트가 서버에 갱신을 보내야하는지 현재 서버에 보내진 갱신들은 무엇인지 기억하고 있어야한다. 한번에 여러 갱신을 보낼 수 있지만 같은 갱신을 두번 보내서는 안된다.

또한 우리가 주시해야하는 오브젝트들 사이는 상호의존적이다. 만약 A가 B에 연관되있고 B에서 로컬 갱신이 일어나면, A 갱신을 보내기 전에 B 갱신을 먼저 서버에 보낼때까지 기다려 주어야 한다.

우리는 다음 요청을 만드는 -nextRequest 메소드를 가진 UserSyncStrategy를 가지고 있다. 이 요청은 로컬에서의 갱신을 서버로 보낼 것이다. 이 클래스 안에는 상태가 없으나 그 모든 상태는 UpstreamObjectSync 클래스 안에 캡슐화되어 들어 있는데, 이 클래스는 유저가 만든 모든 로컬 갱신에 대한 기록을 서버에 날린다. 이 클래스 바깥에는 상태가 없다.

이 경우 이 클래스가 관리하는 상태가 올바른지 체크한다. UserSyncStrategy의 경우 UserSyncStrategy를 모의객체로 만들어 UserSyncStrategy 내부의 상태에 더이상 신경 쓰지 않아도 된다. 이것이 테스트의 복잡도를 확 낮춰주는데, 수많은 다른 종류의 오브젝트를 동기화하고 있기 때문이다. 그러면 다른 클래스들은 상태가 없으며, UpstreamObjectSync 클래스를 재사용 할 수 있다.

Core Data
우리 코드는 굉장히 Core Data에 의존한다. 우리 테스트가 다른 하나로부터 독립되야하므로 각 테스트 케이스마다 명확한 Core Data 스택을 만들어야하고 그 후에 그것을 다시 원래대로 해야했다. 우리는 이 store를 한 테스트 케이스에만 사용하고 다음 테스트에는 다시 사용하면 안되었다.

우리 모든 코드는 다음 두가지 Managed Object Context 주변에 집중되있다: 하나는 유저 인터페이스가 사용하고 메인 큐에 묶여있는 것이고 다른 하나는 동기화를 위해 사용되며 자신의 개인 큐를 가지고 있다.

우리는 그들이 필요로하는 모든 테스트마다 Managed Object Context 자꾸자꾸 생성하길 원하지 않는다. 그러므로 공유된 TestCase 수퍼클래스의 -setUp 메소드에 두개의 Managed Object Context를 만들어둔다. 이것은 각 개별 테스트에서 가독성을 높혀준다.

Managed Object Context가 필요한 테스트는 간단하게 self.managedObjectContextself.syncManagedObjectContext를 호출하면 된다:

우리는 코드의 일관성을 만들기 위해 NSMainQueueConcurrencyTypeNSPrivateQueueConcurrencyType을 사용하고 있다. 그러나 독립적 문제 때문에 -performBlock: 상단에 우리만의 -performGroupedBlock:을 구현했다. 이것에 대핸 더 많은 자료는 비동기 코드를 테스팅하는 섹션에서 볼 수 있다. 

여러 컨텍스트를 합치기
우리 코드에는 두 컨텍스트를 가지고 있다. 프로덕션에는 -mergeChangesFromContextDidSaveNotification:의 의미로서 한 컨텍스트가 다른 컨텍스트와 합쳐지는 것에 굉장히 의존적이다. 우리는 동시에 각 컨텍스트 별로 독립된 퍼시스턴스 store coordinator를 사용하고 있다. 그러면 두 컨텍스트 모두 최소의 명령으로 한 SQLite store에 접근할 수 있기 때문이다.

그러나 테스트를 위해 약간 바꾸어서 메모리 store를 사용할 것이다.

테스트시 SQLite store를 사용하여 디스크에 두는 것은 디스크 store에서 삭제시 경쟁상태(race condition)를 만들기 때문에 동작하지 않는다. 이것은 테스트간의 독립성을 해칠 것이다. 반면 메모리에 store하면 매우 빠르게 동작하며 테스트하기도 좋다.

우리는 모든 NSManagedObjectContext 객체를 만들기 위해 팩토리 메소드를 사용한다. 기본 테스트 클래스는 이 팩토리 클래스를 약간 고쳐 모든 컨텍스트가 같은 NSPersistentStoreCoordinator를 공유한다. 각 테스트의 마지막에는 다음 테스트가 사용할 새 것이나 새 store가 있는지 확인하기 위해 공유하고 있던 퍼시스턴트 store coordinator를 버린다.

비동기적 코드를 테스트하기
비동기적인 코드는 조금 까다로울 수 있다. 그러나 대부분 테스트 프레임워크는 비동기적 코드를 위한 기본 기능을 지원한다.

NSString에 비동기적인 메시지를 가지고 있다고 해보자:

XCTest에서는 아래와 같이 테스트할 수 있다:
대부분 테스트 프레임워크가 이런식으로 되었다.

그러나 비동기 테스트의 주된 문제는 독립적으로 테스트 하기 힘들다는 것이다. 테스트 습관에 관한 글에서 말했듯, 독립(Isolation)의 첫 글자는 "I" 이다.(원문: Isolation is the “I“ in FIRST, as mentioned by the article about testing practices.)

비동기 코드에서 다음 테스트가 시작하기 전에, 현재 테스트의 모든 스레드와 큐가 완전히 멈추는 것을 확신하기 까다로울 수 있다.

이 문제에 대해 우리가 찾는 최고의 해결책은 dispatch_group_t라는 이름의 그룹을 사용하는 것이다.

혼자 두지 말고 그룹에 넣자
몇 우리 클래스들은 내부적으로 dispatch_queue_t를 사용할 필요가 있다. 몇 우리 클래스들은 NSManagedObjectContext의 private 큐에 블럭들을 넣는다.

모든 비동기 작업이 끝날때까지 -tearDown 메소드에서 기다린다. 이것을 하기위해 우리는 아래 보이는 것처럼 여러 일들을 한다.

테스트 클래스는 이런 프로퍼티를 가진다:

우리는 이것을 일반적인 수퍼클래스에 한번만 선언해 두었다.

다음으로 dispatch_queue나 그 비슷한 것을 사용하는 모든 클래스 안에 이 그룹을 넣었다. 예를들어 dispatch_async()를 호출하는 대신, dispatch_group_async()를 호출하였다.

우리 코드는 CoreData에 의존적이므로 NSManagedObjectContext에 호출하는 메소드도 추가하고
모든 Managed Object Context에 새 dispatchGroup 프로퍼티를 추가했다. 그래서 우리는 독립적으로 -performGroupedBlock:을 사용했다.

이렇게하여 모든 비동기 처리가 끝날때까지 tearDown 메소드에서 기다릴 수 있었다.

메인 루프에서 -tearDown이 호출된다. 메인루프에서 큐에 들어간 어떤 코드가 실행되었는지 확인하기 위해 메인루프를 끈다. 위 코드는 그룹이 비지 않는한 영원히 돌고 있다. 우리의 경우 타임아웃을 넣어 살짝 바꾸었다.

모든 작업이 끝날때까지 기다리기
이렇게 하면 수많은 다른 테스트들도 쉬워진다. 아래와 같이 사용할 WaitForAllGroupsToBeEmpty()를 만들었다:

마지막 라인은 모든 비동기 작업이 완료될때까지 기다리는 코드이다. 즉, 이 테스트는 추가적인 비동기 처리를 큐에 넣은 비동기 블럭들까지도 모두 끝나고 어떠한 것도 거절된 메소드를 호출하지 않는다.

이것을 만든한 메크로로 만들었고:
나중에는 공유된 TestCase 수퍼클래스에 메소드를 정의했다:

커스텀 예외
이 섹션의 초반부에서, 어떻게 이것을 하는지 이야기 했었고

비동기 테스트를 위해나 블럭을 만드는 기본이다.

XCTest는 NSNotification과 key-value observing을 위한 몇가지 약속이 존재하는데, 이 둘다 블럭을 만드는 최상단에서 구현될 것이다.

그러나 종종 여러 곳에서 이 패턴을 사용하고 있다는 것을 발견하였다. 예를들어 Managed Object Context가 비동기적으로 저장될거라 예상할 때, 우리 코드는 이렇게 생길 것이다:

이 코드를 공유된 한 메소드만을 호출하게하여 가볍게 만들었다:

그리고 테스트에서 사용할 때이다:

이렇게하면 가독성이 더 좋아진다. 이 패턴은 사용하면 다른 상황에서도 자신만의 커스텀 메소드를 추가할 수 있다.

The Ol’ Switcheroo — Faking the Transport Layer
앱을 테스트하는데 중요한 줄문 중 하나는, 어떻게 서버와 연동하여 테스트할 것인지 이다. 가장 이상적인 솔루션은 실서버를 로컬에 빨리 복사하고, 가짜 데이터를 제공하여 http를 통해 직접 테스트를 돌려보는 것이다.

사실 우리는 이러한 솔루션을 이미 사용하고 있다. 이 솔루션은 굉장히 실제와 유사한 테스트 환경을 제공한다. 그러나 현실적으로 너무 느리게 환경설정이 된다. 각 테스트마다 서버의 DB를 정리하는 것이 너무 느리다. 우리는 1000여개의 테스트를 가지고 있다. 실서버에 의존하는 30개의 테스트가 있는데, 만약 DB를 정리하고 서버 인스턴스를 깨끗히 만드는데 5초가 걸린다치면 적어도 2분 30초를 테스트를위해 기다려야 한다는 것이다. 그리고 또한 서버 API가 구현되기 전에 서버 API를 테스트할 수 있는 것도 필요했다. 우리는 뭔가 다른 것이 필요했다.

이 대안의 솔루션은 '가짜 서버(fake server)'이다. 우리는 서버와 통신하는 모든 클래스를 TransprotSession이라는 한 클래스와 통신하도록 구조를 짜고, 이 클래스는 NSURLSession과 비슷한 스타일이지만 JSON 변환까지도 처리해준다.

우리는 UI에 제공할 API 테스트들을 가지고, 서버와 통신하는 모든 것들은 TransportSession이라는 가짜 서버로 우회하여 두었다. 이 transport session은 실제 TransportSession과 서버 모두의 행동을 따라한다. 이 가짜 session은 TransportSession의 모든 프로토콜을 구현하여 그것의상태를 설정할 수 있게 해주는 몇 메소드를 추가한다.

OCMock를 사용하여 각 테스트에 커스텀 클래스를 가지는 것은 모의 서버(mocking the server)를 넘어 여러 이점을 가진다. 그중 하나는, 실질적으로 모의 서버를 사용하여 더 복잡한 시나리오를 만들어 테스트해볼 수 있다. 실제 서버에서는 시도해보기 어려운 극한의 상황을 시뮬레이트 해볼 수 있다.

또한 가짜 서버는 그 스스로 테스트를 가지므로 그 결과가 좀 더 정밀하게 정의되어 있다. 만약 요청에 대한 서버의 응답이 항상 바뀌어야 한다면 한 장소에서 오직 그렇게 한다. 이것은 가짜 서버를 사용하는 모든 테스트를 보다 더 튼튼하게 만들며, 우리 코드에서 새 기능이 잘 동작하지 않는 부분을 좀 더 쉽게 찾아낼 수 있다.

FakeTransportSession 구현은 간단하다. HTTPRequest 객체를 요청에 관한 URL, 메소드, 패이로드(payload)에 관련하여 캡슐화 시키면 된다. FakeTransportSession은 내부 메소드에 모든 끝부분을 매핑시키고 응답을 발생시킨다. 이것이 알고있는 오브젝트의 기록을 가지고 있기 위해 메모리에 담은 CoreData 스택까지도 가지고 있는다. 이렇게하여 PUT으로 추가된 이전 오퍼레이션의 리소스를 GET으로 반환할 수 있다.

이 모든것을 하기에 시간이 부족하다고 생각할 수도 있겠지만 사실 가짜 서버는 꽤 간단하다. 실제 서버가 아니며, 많은 부분을 떼어냈다. 가짜 서버는 오직 한 클라이언트에만 기능을 제공하기 때문에 퍼포먼스나 스케일리비티는 전혀 신경쓰지 않는다. 또한 한번에 큰 노력을 들여 모든것을 구현할 필요가 없다. 우리가 개발이나 테스트에 필요한 부분만 만들면 된다.

그러나 우리 상황의 경우, 우리 테스트를 시작할쯤엔 서버 API가 꽤 안정적이고 잘 정의되있었다.

커스텀 Assert 메크로
Xcode 테스트 프레임워크에선, 실제 확인을 위해 XCTAssert 메크로를 사용한다:
애플의 “Writing Test Classes and Methods” 글에 "카테고리로 정의된 Assertion의 모든 목록이 있다.

그러나 우리는 아래와 같은 특정 도메인을 체크하는 Assertion을 자주 사용했다:
이렇게하면 가독성이 너무 떨어지고, 코드의 중복을 피하기 위해 간단한 assert 메크로를 만들었다:

테스트 할때는 아래와같이 간단하게 사용하면 된다:

이 방법으로 테스트의 가독성이 굉장히 좋아졌다.

한단계 더
그러나 우리 모두가 알듯 C의 전처리기 메크로는 굉장히 난잡하다(a beast to dance with).

몇몇은 이것을 피할 수 없으며 그 고통을 줄이고싶은 것에 대한 이야기이다. 어디라인 어디파일에 assertion 실패가 생겼는지 알기 위해 테스트 프레임워크를 정렬하는 경우 메크로가 필요하다.(We need to use macros in this case in order for the test framework to know on which line and in which file the assertion failed.) XCTFail()은 메크로이고 __FILE____LINE__이 설정되는 것에 의존하고 있다.

좀 더 복잡한 assert와 체크를 위해 FailureRecorder라 불리는 간단한 클래스를 만들었다:

우리 코드에는 두 딕셔너리가 서로 일치하는지 확인해야하는 부분이 곳곳에 있는데, XCTAssertEqualObject()가 그것을 체크한다. 이것이 실패했을때 내뱉는 결과가 아주 유용하다.

우리는 이런식으로 하길 원했다:

결과에는

그래서 이렇게 메소드를 만들었다.

FailureRecord가  __FILE__, __LINE__, 테스트 케이스를 잡아내는 방법을 썼다. -recordFailure: 메소드는 그냥 간단하게 문자열을 테스트 케이스로 전달한다:

Xcode, Xcode 서버와 통합
XCTest의 최고 장점은 놀라울 정도로 Xcode IDE와 통합하기 좋다는 것이다. Xcode6과 Xcode6 서버와 함게 작업하면 더욱 빛을 발한다. 이 강력한 결합력은 생산성을 증진시키는데에도 큰 몫을 한다.

초점
테스트 클래스에서 한 테스트나 여러 테스트를 하고 있을 동안, 왼편 라인 넘버 옆에 있는 작은 다이아몬드는 특정 테스트나 테스트 집합을 실행시켜준다.



테스트에 실패하면 빨간색으로 되고:



성공하면 초록색이 된다:




^⌥⌘G 단축키는 마지막 테스트를 다시 돌려볼 수 있게 해주는데, 자주 사용하게 될 것이다. 다이아몬드를 클릭하고, 우리가 테스트를 변경하면, 키보드에 손 델 필요없이 간편하게 다시 테스트를 돌려볼 수 있다. 디버깅 테스트시 아주 유용하다.

네비게이터
(Xcode 왼쪽 창에 있는) 네비게이터는 Test Navigator라는 것인데, 클래스별로 모든 테스트를 묶어 보여준다:



그룹 테스트나 개별 테스트는 이 UI로부터 시작할 수도 있다. 더 유용한 점은 네비게이터 하단의 세번째 아이콘을 활성화시켜 실패한 테스트만 보여주게도 할 수 있다:




이어지는 통합
OSX 서버는 Xcode 서버라 불리기도 한다. 이것은 Xcode를 기반으로 이어지는 통합(continuous integration) 서버이다. 우리는 이렇게 사용해 왔다.

우리의 Xcode 서버는 github의 새 커밋이 들어올때 자동적으로 프로젝트를 체크한다. 우리는 스태틱 어널라이저를 실행하고 iPod touch나 다른 iOS 시뮬레이터에서 모든 테스트를 돌린 뒤 마지막으로 다운받을 수 있는 Xcode 아키브(archive)를 생성하도록 했다.

Xcode6에서는 Xcode 서버의 이 기능들이 복잡한 프로젝트에까지도 꽤 유용하게 쓰인다. 우리는 커스텀 트리거를 가지고 있는데, 이것은 배포 브런치에서 Xcode 서버의 빌드 부분을 실행한다. 이 트리거 스크립트는 생성된 Xcode 아키브를 파일 서버에다 올려둔다. 이렇게 함으로서 버전별 아키브를 관리할 수 있다. UI팀은 파일 서버로부터 미리 컴파일된 특정 버전의  프레임워크를 내려받을 수 있다.


BDD와 XCTest
당신이 만약 BDD(behavior-driven development)에 익숙하다면, 우리의 네이밍 스타일이 이 방식(BDD)에 영감을 받았다는 것을 알 수 있을 것이다. 우리 중 몇명은 Kiwi라는 테스트 라이브러리를 사용해 보았고 자연스럽게 클래스나 메소드의 동작에 집중함을 느꼈을 것이다. 그러면 XCTest가 그 좋은 BDD 라이브러리를 대체할 수 있을까? 대답은 아니오 이다.

XCTest가 간편하다는 것에는 장단점이 분명 존재한다. 당신이 클래스를 생성하고 "test"라는 단어를 접두에 붙인 테스트 메소드를 만들어서 그렇게 해도 된다. 게다가 Xcode와 XCTest는 최고의 통합을 자랑한다. 한 테스트를 실행하기 위해 왼편의 다이아몬드를 누르면 되고, 실패한 테스트들을 ㅅ ㅟㅂ게 걸러볼 수 있으며, 또한 테스트의 리스트 중에 원하는 테스트로 쉽게 이동할 수도 있다.

불행히도 당신에게 이런것들을 새로 배우기에 꽤 부담스러울 수 있는 양이다. 우리는 XCTest와 개발/테스트하면서 어떠한 장애물도 만나지 않았으나 종종 더 편하게 사용해왔다. XCTest 클래스는 일반 클래스처럼 보이지만, BDD 테스트 구조는 nested context가 있다. 이것은 테스트시 nested context를 만드는 것을 잊어버릴 수도 있다. nested context는 개별 테스트를 간단하게 하면 더 많은 특정 시나리오를 만들어내야한다. 물론 XCTest에서도 그렇긴하다. 예를들어 몇 테스트를 위해 커스텀된 초기화 메소드를 호출함우로써 말이다. 이것의 단지 편리함 때문만은 아니다.

BDD 프레임워크의 추가적인 기능이 얼마나 중요한지는 프로젝트의 크기에 따라 알게될 것이다. 우리의 결로은 다음과 같다. XCTest는 작은 사이즈나 중간 사이즈의 프로젝트에 적합하나, 큰 사이즈의 프로젝트에는 KiwiSpecta 같은 BDD 프레임워크를 사용하는 것이 더 낫다.

요약

XCTest가 옳바른 선택일까? 당신의 프로젝트에 따라 판단해야한다. 우리는 KISS의 부분으로 XCTest를 선택했고—다르게 해보고 싶었던 위시리스트를 가진다. XCTest는 우리가 어느정도 절충해야 하지만, 그 역할을 잘 한다. 그러나 다른 프레임워크에서는 다른 것들도 절충해야할 것이다. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

원문https://medium.com/@kazmiekr/what-every-ios-developer-should-be-doing-with-instruments-d1661eeaf64f

Introduction

iOS 프로젝트를 마무리할때, 디바이스에 올려 테스트하여 크래쉬가 없이 잘 돌아가면 만족하고 끝낸적이 있을것이다. 그게 과연 올바른 마무리일까? Instruments를 사용하여 프로파일링(profiling)을 하지 않고 끝냈다면 잘 마무리 했다고 할 수 없다고 생각한다. 그 이유는 개발자 디바이스에서 잘 동작한다해서 실유저에서 크래쉬가 안난다는 보장은 없기 때문이다.

Xcode에는 Instruments라는 퍼포먼스 튜닝 어플이 포함 되 있다. 이 프로그램은 다양한 기준으로 개발자의 앱을 프로파일 할 수 있게 해준다. Instruments는 CPU 사용량, 메모리 사용량, 메모리 누수, 파일/네트워크 활성, 베터리 사용량 등을 측정할 수 있는 툴을 포함한다. 이것들은 Xcode에서 바로 시작할 수 있어서 쉽게 켤 수 있다. 그러나 지금 보고있는 프로파일 자료가 뭘 프로파일링 한건지 잘 모를수도 있으며 이런 상황은 개발자들이 이 깊은 잠재력을 가진 툴 사용을 저해할 수 있다.

이 많은 프로파일 툴 중에 어떤걸 선택해야할까? 먼저, 느려진 네트워크 요청이나 버벅거리는 스크롤과 같은 눈어띄는 퍼포먼스 이슈 해결을 위한 선택지들이 있을 수 있다. 그러나 나는 당신이 생각하기에 모든것이 정상적으로 돌아가는것으로 보이는 부분에서 CPU나 메모리 사용량을 체크해보는걸 추천한다.

프로파일을

이 이야기를 하기 전에 한가지 우리가 염두할 것은, 모든 개발자들이 항상 그들의 앱에 프로파일을 하지는 못한다는 것이다. 대부분은 데드라인과 예상 결과물이 존재한다. 어떨땐 앱을 프로파일링이고뭐고 마감을 빠듯하게 지키는것도 힘들 때가 있지만, 우리는 이런 상황까지 고려하여 '어느 시점에 프로파일링을 하면 좋을까'에 대해 얘기해보고자한다.

최소한 만든 앱을 앱스토어에 제출하기 전에 한번은 프로파일링을 해줘야한다. 당신은 앱스토어의 심사를 통과 한 후 유저들이 앱을 사용하면서 안 좋은 일들이 일어나길 바라지는 않을것이다. 프로파일링을 하지 않았다가 문제가 생긴다면, 좋지 않은 리뷰들이 많아질 것이고 다운로드 수는 감소할 것이다.

나는 당신이 주요 기능들을 끝낸 후에, 재빨리 프로파일을하여 모든것이 정상적으로 동작하는지 확인해보는 것을 제안한다. 만일 재빨리 하지 못하면, 잠재적인 이슈들이 점점 드러날테고, 그것이 한참 쌓이면 완전히 새로운 기획으로 바뀔 수도 있으며 이것은 런칭을 심각하게 미루게 될것이다. 팀으로써 당신은 프로파일하는 시간을 개발 플랜의 일부로 살짝 넣도록 해야한다.

익숙하지 않은 프레임워크를 쓰게되면 Instruments 켜는걸 제안한다. 또한 대부분의 iOS 프레임워크와 라이브러리는 끊임없이 바뀌며, 이런 변화는 당신이 익축하지 않은 프레임워크와 작업하는것과 비슷하다. 대부분 개발자들은 바뀌는게 언제인지 느낌이 올때가 있고, 만약 당신이 그런 느낌을 받았다면 얼른 당신이 만든 작업물들에서도 안정적으로 동작하는지 확인하기위해 프로파일링을 돌려야한다. 당신이 추가한 라이브러리에서 당신의 통제를 범위를 벗어나 발생될 있는 메모리 이슈가 있는지 확인하기 위한 프로파일에는 3rd party 라이브러리를 가져다 쓰는것 또한 좋은 방법이다.


Xcdoe에서 프로파일링

Xcode 이전엔 Instruments 안에 묻어둔 많은 정보를 ‘Debug Navigator(Xcode 기능의 일부)’ 포함하기위해 위해 Instruments 밖으로 확장해왔다. 만일 당신이 6 단축키를 누르면, 앱에 대한 퍼포먼스 정보를 있다. 여기서 CPU/Memory/Energy/Disk/Network 엑티비티의 빠른 요약 정보를 있고 즉각적인 이슈를 수도 있다. 여기서 상단에 보이는 ‘Profile in Instruments’ 버튼을 눌러서 Instruments 실행할 수도 있는데, Instruments를 누르면 디버그 세션으로 이동할지 아니면 새것을 새로 시작할지 물어볼 것이다.

CPU 프로파일링

제일 먼저 우리가 프로파일해볼 것은 CPU 사용량이다. CPU 프로파일링을 시작하기위해 우리는 ‘Profile’ 이라는 프러덕트를 선택하고 타겟은 디바이스 선택해야한다. CPU 프로파일링을 할때, CPU 사용량에 대한 정확한 정확한 정보를 얻기 위해 실제 디바이스를 사용하기를 원할것이다. 만약 시뮬레이터로 타겟을 정하면 실제 디바이스 환경과는 많이 다른 머신의 CPU 정보를 얻게된다. 이상적인 테스트로는 당신의 작업물이 가장 느린 디바이스에서 동작하는지 확인한 뒤에 빠른 환경에서 테스트 하는 것일 것이다.

CPU 프로파일링은 인터벌을 줘서 프로세스들이 동작하는 샘플을 얻어냄으로써 측정한다. 디폴트로 샘플은 1ms 단위로 얻어지지만, 원하면 바꿀 수도 있다. 스냅샷들 사이에서 어떤 프로세스들이 아직 돌고있는지 봄으로써, 그것들이 얼마나 길게 작동되고있는지 측정할 있다.

프로파일 빌드가 끝이나면, Instruments 켜지면서 어떤 템플릿으로 프로파일링을 할지 물어본다. 우리는 CPU 사용량 측정을 위해 “Time Profiler” 것이다.

이것은 Timer Profiler 셋업을 위한 초기 Instruments 화면이다. 이제 실제로 프로파일링을 하기위해 녹화 버튼을 누른다. 가끔 몇몇 이유로 녹화 버튼이 비활성되있는 경우가 있다. 그럴때는 오른쪽 상단에 비활성된 이유가 나타나 있을 것이다. 나는 ‘device is offline’이라는 상태에서 멈춘적이 있는데, 보통 디바이스를 재부팅하면 해결된다. 이제 녹화 버튼을 누르면 당신 앱에서 무슨 일이 일어나는지 정보를 보여주기 시작한다.

상단에는 녹화가 진행되는동안 시간이 지남에 따라 당신의 CPU 사용량 그래프가 보이며 하단에는 동작하는 동안의 프로세스들의 Call Tree 보일것이다. 프로세스의 초기 덤프(dump) Call Tree에서 상단에 보이는 자료들은 매우 쓸때없이 많아 보인다. 또한 이것은 모든 시스템 라이브러리의 활성 상태를 보여줘버린다. 당신은 아마 당신이 작성한 코드에만 집중하고 싶을것이다. 다행히 중요한 정보만 빨리 찾도록 쉽게 설정하는 방법이 있다.

오른쪽에 톱니모양처럼생긴 ‘Display Settings’(2) 누르면 ‘Call Tree’를 위한 옵션들이 나온다. 디폴트로는 대부분 옵션이 오프되 있을 것이다. 우리는 우리가 원하는 것만 켜면 된다. 이제 이 옵션들이 뭘 하는 놈들인지 보자.

  • Seperate by Thread — 많이 사용된 스레드 순서로 프로세스를 보여준다.
  • Invert Call Tree — 스택을 뒤집어서 보여준다. 가끔 유용하게 쓰인다.
  • Hide System Libraries — 시스템 라이브러리 프로세스는 숨기고 당신의 코드만 보여준다.
  • Flatten Recursion — 하나의 개체에서 재귀로 호출된 콜들을 보여준다.
  • Top Functions — 함수 호출이 함수로부터 불려진 함수에 의해 추가적인 시간이 쓰이는지 시간순으로 보여준다. 기능은 무거운 메소드가 어떤건지 찾는데  도움을 준다.

필자는 프로파일링할때 대체로 모두 체크하고 정보를 얻는데, 이게 유용하다. 필터링 옵션선택한 Call Tree 보면, 당신의 앱에 메소드가 CPU 얼마나 사용하는지 쉽게 확인할 수 있다.

이제 CPU 입장에서 무거운 메소드들 리스트를 모니터링 하면서 앱의 정확한 지점을 최적화 시켜볼 있다. 몇몇은 건드리기 힘들 수도 있지만 최적화 가능한 것들도 많이 있을 것이다. 필자는 최적화를 어떻게 하는지에대해서는 깊게 보지는 않을 것이지만 여기 몇몇 생각해볼만한 것들이 있다:

  • 무거운 작업은 UI 프로세스가 아닌 것은 다른 쓰레드로 옮긴다.
  • 항상 다시불러올 필요가 없는 이미지, 데이터 등등의 것들은 캐싱한다.
  • 당신은 필요하지 않는 UI 업데이트를 지도 모른다. UI 업데이트를 줄인다.

한가지 팁은 ‘Call Tree’ 섹션의 오른편에 존재하는 ‘Extended Detail’ 섹션이다. 이것은 당신이 선택한 스택 흔적(trace) 있고 라인을 더블클릭하면 정확히 당신 코드를 찾아 보여줄 것이다. 작은 Xcode아이콘을 클릭하면 동작중인 메소드를 Xcode에서 보여줄 것이다.

퍼포먼스 이슈를 해결하여 업데이트를 시킨 후에는 같은 방식으로 profiler 돌리면서 이전 퍼포먼스보다 나아졌는지 체크한다. 과정을 당신이 만족할 까지 반복하면 된다.

메모리 프로파일링

다음으로 프로파일링 것은 메모리 프로파일링이다. 이것은 직접적인 이슈가 아닐때가 많아서 iOS 개발에서는 그냥 지나쳐버리기 쉬운 것중 하나이다. 만일 당신 앱에 메모리 누수가 발생하고 있는데 유저가 지속적으로 사용한다면 메모리는 점점 꽉 찬 뒤, 결국 아웃 오브 메모리 상황이 발생하여 앱이 꺼지게 것이다. 이런 상황은 당신의 앱한테만 좋은게 아니라 유저의 디바이스 입장에서 메모리 부족으로 다른 앱들에게도 안 좋은 영향을 끼치게 될것이다. 그럼 이제 Instruments 이용하여 어떻게 메모리 누수를 방지를 있는지, 그런 상황에서 어떻게 고칠 있는지 알아보자.

시간이 지남에 따라 메모리 사용량이 점점 증가하는걸 원치 않을 것이다.

아래와 같은 상황은 원치 않는다.

아래와 같은 상황을 원한다.

가장 쉽게 메모리 사용량을 확인하는 방법은 Xcode 안에 있는 ‘Debug Navigator’에서 보는것이다. ‘Memory’ 패널을 선택하고 실시간 메모리 사용량을 있다. 여기서는 메모리 사용량이 계속 증가하거나 한번도 낮아지지 않는 문제와 같은 문제들을 바로 확인하는데 도움을 준다.

메모리 사용량을 자세히 보기위해 ‘Profile in Instruments’ 누르면 세션에서 transfer할건지 새로 하나 만들면서 (Instruments)restart할건지 물어볼 것이다. restart 누른다. 필자는 transfer 눌러서 뭔가 정보를 손실하거나 그런 좋지않은 경험이 많다. 그러면 Allocations and Leaks 템플릿을 가지는 Instruments 열리고 이것은 모든 메모리 할당과 모든 발생하는 잠재적인 누수를 눈으로 있다.

이번에도 누수 부분을 깊게 들어가진 않겠지만, 메모리가 할당은 됬으나 해제가 되지않는 것들을 보기 위해 특정 시간 간격 안에서 스냅샷을 찍어볼 것이다. 메모리 누수는 Objective-C C 라이브러리 같은 것을 사용하면 종종 나타난다. 웬만하면 ‘Analyze’ 빌드 옵션을 사용하여 찾을 있으나, 간혹 Analyze 툴이 아무것도 찾지 못할때도 있다. 반면 Swift에서 작업하고 있으면 Objective-C 사용할때보다 누수를 적게 겪을 것이다.

메모리 프로파일러를 쓰면서 내가 찾은 유용한 기능은 두가지이다. 첫째로 순서가 이후에 동시에 이벤트의 순서를 수행한다는 것. 둘째로 메모리 마킹을 생성하는 것이다. 이걸 사용하면 당신은 스냅샷 사이에 메모리의 변화를 분석할 있다. 오른편 ‘Display Settings’ 섹션 위에 있는 ’Mark Generation’ 버튼을 누르면 바로 메모리 스냅샷을 찍을 수 있다. 진행되는 동안 마크를 만드는걸 까먹었어도 언제든지 마크를 추가한 뒤에 단지 클릭하고 표시를 상단에 놓고 움직이면 원하는 지점으로 마크를 이동시킬 있다. 그러고나서 ‘Allocation Type’ ‘All Heap Allocations’으로 바꾸면, 우리가 손대기 힘든 시스템의 정보들은 숨기고 증가량에 따라서 정렬해준다. 이제 당신은 범위동안의 메모리 사용량을 보기 쉽게 확인할 있을것이나, 이것은 실제 당신이 생성한 오브젝트 자체를 확인하기는 어려울 것이다.

이제 메모리 할당에 따른 리스트는 가지고 있으니, 이것이 제대로 나타나있는지 확인하야한다. 사실 초기에 표시되는 데이터들은 쓸모가 없다. 만약 Swift 사용하고 있다면, 모든 Swift 객체들의 이름 앞에 이름이 붙을것이고 거기서 이름으로 필터링하면 당신의 객체를 찾을 있다. 반면 Objective-C 사용하고 있다면 객체를 찾아내는데 특이한 방법을 사용한다. 당신이 찾고 싶은 것의 이름을 알고 있을것이다. 당신의 파일이나 객체 이름에 접두로 어떤 특정 이름을 붙인다면 찾을 있다. 예를들어 당신의 모든 view controller들이 *ViewController 같은 이름을 가진다면 ‘ViewController’ 검색해도 찾을 있을것이다.

여기 필자가 만든 예제에는 4개의 스냅샷이 있고 ViewController라는 이름으로 객체들을 필터링 했다. 그리고 스냅샷 사이에 ServiceViewContoller에서 누수가 일어났다는걸 있다.

여기 글의 목적을 위해 ViewController에다 뭔가 문제가 있는 retain cycle 집어넣으나, 원래는 코드를 뒤져보면서 어떤것이 객체의 누수를 만드는지 일일히 살펴봐야한다. retain cycle 객체가 서로 강한 참조를 하고있어서 서로 할당 해제되는걸 막을때 일어난다. 필자는 retain cycle에대해 깊게 말하진 않을것이나, 이런 경우에 delegate blocks/closures 먼저 확인하는 것을 추천한다. 당신의 delegates (weak)하고 blocks/closures에서 약한 참조를 사용한다는걸 확인하면 좋을것이다.

당신의 객체에서 다른 객체를 참조하고있어서 그 다른 객체가 메모리 해제되지 못하는지 일일히 확인하기 좀 힘들다면, Instruments 사용하여 많은 객체들의 자료를 얻을 있을 것이다. Instruments에서 객체 옆에 작은 화살표를 누르면, 객체의 모든 할당된 객체들을 보여줄 것이며 누가 생성하였는지까지도 나온다. 그다음 작은 화살표를 누르면 retain release count 정보까지 얻을 있다. 만일 count 0 되지 않는다면 메모리 해제가 되지 않았다는 뜻이다. 여기서 팁을 주자면 ‘Responsible Library’ 당신 코드이니 유심히 보고 ‘libsystem_blocks’ 유심히 봐라. 반대로 ‘UIKit’ 스킵해도 된다. 필터링하기위해 아이템들을 검색 박스에 쳐볼 있다. 그러고나면 ‘ Extended Detail’ 보이고 이것들이 어떻게 돌아가고있는지 스택 trace 보이게 될것이다.

결론

글은 Instruments 아주 일부만 보여준 것이다. 그래도 Instruments 시작하는 사람들에게 도움이 되길 바라며 처음부터 바로 잘하지 못해도 괜찮다. 툴을 사용하는데 점점 익숙해지다보면, 당신의 코드가 나은 코드가 될것이다. 당신은 문제를 만든 이슈를 찾는것에 주도적이게 것이며, 프로파일링이 당신의 익숙한 개발 플랜 하나의 플랜으로 자리잡게 될 것이다.

Instruments에대해 배우고 싶다면 필자는 2014 WWDC 비디오(Improving your app with Instruments) 보는것을 적극 추천한다. 여기서는 추가적인 팁엔 트릭과 함께 살펴볼 있다. Instruments에대해 읽고싶으면 애플이 만든 유저 가이드 확인해보길 바란다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

MVC, MVP, MVVM, VIPER에대해 확실하게 잡기

원문https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.wtcp3gqzw

UPD: NSLondon에대해 내가 발표한 슬라이드 자료가 이 링크 있다.

iOS에서 MVC 사용한다는게 다소 이상하게 느껴질 있다. 당신은 MV모데VM으로 바꾸려고 생각해본 적이 있는가? VIPER 적용시켜볼 생각을 적은 있으나, 그게 의미있는 것인지 확신이 들지 않는가?

글을 읽어 내려가면 것들에 대한 답을 찾을 있을 것이다. 또한 자유롭게 댓글로 의견을 제기할 있다.

당신은 iOS 환경에서 아키텍처 패턴에 대한 지식을 정리하고 싶을 것이다. 우리는 유명한 것들을 골라 한번 보고, 이론과 비교한 , 작은 예제들과 함께 연습해 것이다. 아래 링크는 당신이 특별히 관심있는 것을 연습할 있다.

디자인 패턴을 마스터하는것은 중독될 있으므로 조심해야한다: 전보다 많은 질문들이 생겨날 것이기 때문에.

- 누가 네트워크 리퀘스트를 소유하여야하나: 모델이냐 컨트롤러냐?
- 새 뷰의
어떻게 모델을 넘겨주나?
- 누가 새로 생긴 VIPER 모듈을 생성해야하나: Router Presenter?

아키텍처를 고르는데 신중해야하는가?

당신이 만약 개발을 하다가 디버깅을 해야하는데 엄청난 양의 클래스와 엄청난 양의 다른 것을 비교해야 하며, 이게 아키텍처가 없는 상황이라면, 당신 클래스의 어떠한 버그를 찾지도 고치지도 못하는 상황을 맞이하게 것이다. 우리는 클래스의 모든 속성을 머릿속에 담아두고 있을 없다. 만약 그짓을 하다보면 중요한 세부적인 요소를 놓힐 수가 있다. 만약 개발하면서 이런 경험을 이미 해보았다면 아래와 같은 것을 겪어봤을 것이다.

  • 클래스가 UIViewController 자식클래스이다.
  • 당신의 데이터들이 UIViewController에서 바로 저장된다.
  • UIView들이 거의 아무 일도 하지않는다.
  • Model 데이터 구조이다.
  • 유닛 테스트로 아무것도 하지 않는다.

그래도 애플의 가이드라인이나 애플의 MVC(링크) 따랐다해도 이러한 상황은 생길 있으니 너무 낙담하지는 마라. 애플의 MVC 뭔가 잘못되었고, 우리는 그걸 바로잡을 것이다.

좋은 아키텍처의 특징 정의해보자:

  • 엄격한 룰에 따라 개체들간의 책임 분리(Distribution) 균형있게 해야한다.
  • 첫번째 말한 특징으로부터 나올 있는 테스트들이 가능(Testability)해야한다. (그리고 걱정마라: 적절한 아키텍처를 고른다면 어렵지 않을것이다.)
  • 사용하기 편해야(Ease of use)하고 유지보수하기 쉬워야한다.

분리해야하나?
분배는 우리가 이게 어떻게 동작하는지 알아낼려고 노력하는 동안 우리의 뇌에서 균등하게 생각하도록 해준다. 만약 당신이 천재라 생각되면 그냥 하던대로 해라. 그러나 능력은 선형적으로 커지니 않을 뿐더러 광장히 빨리 한계에 도달해버린다. 그러므로 가장 빨리 복잡한 것을 극복하는 방법은 단일 책임 원칙으로 수많은 개체들의 책임을 쪼개는 것이다.

테스트 가능해야하나?
이미 유닛테스트에대한 중요성을 알고 있는 사람에게 던지는 질문이 아니라, 기능을 추가한 일때나, 클래스의 몇몇 복잡성을 리팩토링을 하기 위해서 테스트에 실패하는 사람들이 하는 의문이기도하다. 이것은 테스트가 런타임 내에서의 이슈를 찾는데 도와주며, 반대로 실유저에게 이슈가 발생한다면 그걸 고친 앱을 다시 실유저가 다시 사용하기까지 일주일씩이나 걸린다.

사용하기 쉬워야하나?
가장 좋은 코드가 뭔지는 한번 언급해 가치가 있다: 하나도 작성하지 않은 코드이다. 따라서 적은 양의 코드는 버그가 적다. 게으른 개발자 말을 빌려 적은 코드를 작성 하기를 갈망하며 이것은 코드를 설명해야하면 안된다. 또한 당신이 눈을 감고 허우적대며 유지보수하는 솔루션을 원치도 않을 것이다.

필수 MV(X)

요즘은 아키텍처 설계를 할때 수많은 선택지가 있다:

위에서 세개(MVC, MVP, MVVM) 아래 3 카테고리중 하나는 들어가있다:

  • Models데이터나 데이터 접근 레이어(Person 클래스나 PersonDataProvider 클래스와 같이 데이터를 다루고있는) 소유를 책임지는 부분
  • Views레이어에 표현되있는 것을 책임지는 부분(GUI), iOS 환경에서는 'UI' 접두로 붙는다(역자주: UILabel, UIView 등등..).
  • Controller/Presenter/ViewModelModel View 붙여준다. 보통 유저가 View에서 어떤 액션을 취할때 Model 변경하거나 Model 변경되었을 , View 갱신하는 책임을 가지는 부분

개체들을 나눌때 이점:

  • 이전보다 이해할 있다(이미 알고 있다 하더라도).
  • 재사용 가능하다(대부분 View Model 적용 가능하다).
  • 독립적으로 테스트 가능하다.

어서 MC(X) 패턴을 시작하고 나중에는 VIPER까지 해보도록 하자.

MVC

이전에는 어떻게 사용해왔을까?

애플의 MVC 살펴보기 전에 전통적인 MVC 어떻게 사용되었는지 보자.

Traditional MVCTraditional MVC

경우는 View 범위가 정확하지 않다. Model 바뀌고나서 Controller에의해 한번 랜더링(rendering) 된다. 웹페이지에서 다른 페이지로  수 있는 링크를 누른 , 다시 로딩되는 것을 생각해봐라. iOS 앱에서 전통적인 MVC 구현하는것은 가능할지라도 구조적인 문제때문에 효과적으로 처리할 없으며 당신 앱이 그러기도 원치 않는다.— 모든 개체가 둘씩 묶여있고, 개체는 다른 두개에 대해 알고있다. 이것은 각기 그들이 재사용성을 심각하게 줄여버린다. 이러한 이유로 우리는 흔히 쓰는 MVC 작성하는 또한 스킵 하겠다.

전통적인 MVC 최신 iOS 개발에 적합해 보이지 않는다


Apple’s MVC

기대한것..

Cocoa MVCCocoa MVC

원래 Controller Model View 연결시켜주는 역할을 하므로 서로에 대해 알필요가 없다. 그중에 가장 재사용 불가능한 것이 Controller이며, 우리도 그걸 알고있다. 따라서 우리는 모든 특이한 로직을 Model 아닌 Controller 넣어야한다.

이론적으로는 굉장이 전략적으로 보이지만 뭔가 문제가 있다. 당신은 MVC 컨트롤러 덩어리(Massive View Controller) 불리는걸 들은적이 있을지도 모른다. 나아가 View Controller offloading iOS 개발자들에게 중요한 토픽이다. 애플은 전통적인 MVC 조금 개선하여 사용하여서 이런 일이 일어나버린건가?

Apple’s MVC

실체는..

Realistic Cocoa MVCRealistic Cocoa MVC

Cocoa MVC View Controller 덩어리 작성하도록 만들어버린다. 이유는 View들의 라이프 사이클 안에서 뒤엉키는데 그것들을 분리해내기가 어렵기 때문이라고 말한다. 너가 Model*비지니스 로직이나 데이터 변환같은 것을 없애는 능력을 가졌을 지라도 대부분의 View에서 반응하면 액션을 Controller로 보내게 될것이다. 뷰 컨트롤러는 결국 모든 것의 델리게이트(delegate)나 데이터소스(data source)가 될테고, 종종 네트워크 요청과같은 처리도 하고 있을지 모른다. 

이런 종류의 코드를 얼마나 많이 보았는가:

Model 함께 직접적으로 구현된 View cell MVC 가이드라인을 위반한다. 그러나 항상 그렇게 사용하며 사람들은 이게 문제가 아니라고 느낄때가 많다. 좀더 MVC 따르고자 한다면 cell Controller에서 구성하고 View 안에 Model 거치지 않아햔다. 그러나 그렇게해버리면 Controller 커져버리게 될것이다.

Cocoa MVC View Controller 덩어리의 이유이기도하다.

문제는 유닛 테스트(여러분 프로젝트에 있기를 바란다)에까지 나타날 거라는걸 확신할수 없다. 당신의 View Controller View 붙어있고, 이렇게하면 그들의 View 라이프 사이클이나 테스트를 위한 View 만들기가 어려워지기 때문에 테스트가 힘들어진다. 반면 View Controller 코드를 작성하고 있으면 당신 비지니스 로직은 가능한 View 레이아웃 코드로부터 분리될것이다.

간단한 예제를 보자:

MVC 분리하면 현재 View Controller안에서 동작되게 있다.

테스트하기 좋아보이지는 않다. 우리는 greeting 생성을 GreetingModel 클래스에 옮겨 넣을 있다. 그러나 GreetingViewController안에서 UIView 연관되어있는 메소드(viewDidLoad, didTapButton) 호출하지 않은체 상연 로직(예제에는 로직이 많이 없지만) 테스트를 수가 없다.

사실, 로딩테스트는 디바이스를 바꿔가며(iPhone4S, iPad 등등으로) 확인해보는 것에대한 이점이 없다. 그래서 Unit Test target configuration에서 “Host Application” 지우고 시뮬레이터 없이 테스트 해보는것을 추천한다.

View Controller 사이의 상호작용은 Unit Test로써 테스트하기에 좋지 않다.

위에서 말한건, Cocoa MVC 사용하는것은 별로 좋지 않은 선택인것 같아 보인다는 것이다. 그러나 글의 서두에 언급했단 특징들의 용어를 정의했었다.

  • Distribution사실 뷰와 모델은 분리되 있지만, View Controller 붙어있다.
  • Testability거지같은(?)분리 때문에 아마 Model 테스트 가능할 것이다.
  • Ease of use다른 패턴에 비해 코드가 적게 든다. 추가로 많은 사람들이 친숙하게 사용하기도하며 경험해보지 못했던 개발자도 쉽게 접근할 있다.

Cocoa MVC 아키텍처 쪽에 시간을 투자할 시간이 별로 없을때 선택하는 패턴이며, 작은 프로젝트에는 지나친 유지보수 비용이 들어간다는 것을 느낄 있을 것이다.

Cocoa MVC 개발 속도면에서는 최고의 아키텍처 패턴이다.


MVP

전달될거라 약속한 Cocoa MVC(Cocoa MVC’s promises delivered)

Passive View variant of MVPPassive View variant of MVP

사진이 애플의 MVC 굉장히 비슷하지 않는가? 이것의 이름은 MVP(Passive View Variant)이다. 그럼 애플의 MVC MVP 같다는 걸까? 그렇지 않다. MVC에서는 View Controller 서로 붙어있지만 MVP에서 중간다리 역할을 하는 Presenter View Controller의 라이프 사이클에 아무런 영향을 끼치지도 않으며, View 쉽게 테스트가능한 복사본(moked) 만들 있다. 그러므로 Presenter에는 레이아웃 관련 코드가 없고 오직 View 데이터와 상태를 갱신하는 역할만 가진다.

만약 UIViewController View라고 말했으면 어떨까.

사실 MVP 입장에서는, UIViewController 자식클래스에 Presenter 아닌 View들이 있다. 이러한 구분은 좋은 테스트 용이함을 제공하지만, 수작업의 데이터나 이벤트 **바인딩 따로 만들어야하기때문에 개발 속도에대한 비용도 따라 온다. 아래 예제에서 확인할 있다:

Important note regarding assembly(중요 요약 모음)

MVP 세개의 다른 레이어를 가짐으로써 이런 문제 집합이 처음으로 나타난 패턴이다. 그러므로 뷰가 Model에대해 알기를 원치 않기 때문에, 현재 View Controller(View 것이다) 모아서 동작시키는건 옳지 않으므로 다른곳에서 동작시켜야한다. 예를들어, 우리는 앱에서 범용적인 모아서 수행하거나 View-to-View 보여주기위한 Router 돌릴 있다. 이슈는 MVP 뿐만아니라 아래 모든 패턴들에게도 나타나는 문제이기도하다.

이제 MVP 특징 보자.

  • DistributionPresenter Model 책임을 거의 분리했고 View 빈껍데기가 셈이다( 예제에서는 Model 빈껍데기 같았지만..)
  • Testability최고로 좋다. View 재사용가능 덕분에 대부분의 비지니스 로직을 테스트 있다.
  • Easy of use위에서 비현실적인 예제에서는 MVC에비해 코드의 양이 2배정도 많이 들지만 MVP 아이디어는 굉장히 명료하다.

iOS에서 MVP 테스트하기엔 좋지만 코드가 길어진다.


MVP

With Bindings and Hooters

MVP 다른 버전(MVP Supervising Controller) 있다. 이러한 다양한 MVP들은 Presenter(Supervising Controller) View로부터 액션을 처리하고 View 적합하게 변경하는 동안 View Model 직접 바인딩을 포함한다(?).

Supervising Presenter variant of the MVPSupervising Presenter variant of the MVP

그러나 우리가 이미 이전에 배웠듯, 막연하게 책임을 나누는건 좋지않은데다, View Model 합쳐버린다. 이것은 Cocoa 데스크탑 개발에서 어떻게 동작하는지와 비슷하다.

전통적인 MVC와같이, 결함이 있는 아키텍쳐의 예제를 찾기 힘들었다.

MVVM

마지막이자 MV(X) 종류의 최고 종류

MVVM은 최근에 나온 MV(X) 종류이다. 그러므로 이전의 MV(X) 문제들을 해결하여 나오기를 기대해보자.

이론적으로는 Model-View-ViewModel이 굉장히 좋아보인다. ViewModel은 이미 우리에게 친숙할테고, View Model 이라불리는 중계자 또한 마찬가지일 것이다.

MVVMMVVM

MVP 비슷하다:

- MVVM View Controller View라고 일컫는다.
- View Model 서로 연결 되어있지 않다.

추가로 MVP Supervising버전에서 처럼 binding 있다; 그러나 여기서는 View Model 관계가 아닌 View View Model 사이의 관계이다.

그래서 실제 iOS에서 View Model 뭘 의미할까? 그것은 기본적으로 UIKit인데 그로부터 View 독립된 표현이거나 상태이다. View ModelModel에서 변경을 호출하고 Model 자체를 갱신한다. 따라서 View나 View Model 사이에서 바인딩을 하며, 적절히 처음것이 갱신된다.

Bindings(바인딩)

MVP 파트에서 간당하게 언급한적이 있다. 그러나 여기서 좀 더 이야기 해보자. 바인딩은 OS X 개발을 위한 박스(역자주: 프레임워크나 툴을 말하는듯 합니다)에서 나왔으나 iOS 툴박스에서는 보지못한다. 물론 KVO나 notification을 가지고 있긴 하지만 그것이 바인딩만큼 편리하지는 않다.

그러므로 

- 바인딩 기반 라이브러리인 KVO에는 RZDataBinding 혹은 SwiftBond 이런게 있다.
- The full scale functional reactive programming beasts like ReactiveCocoa, RxSwift or PromiseKit. (번역하지 못했습니다ㅠ)

사실 요즘엔 MVVM을 들으면 바로 ReactiveCocoa를 말하기도하며, 반대도 그렇다(역자주: 뭐라고??????). 비록 간단한 바인딩으로 MVVM을 만드는게 가능하기는 하나 ReactiveCocoa (혹은 siblings)으로는 최고의 MVVM을 만들수 있게 해준다.

Reactive 프레임워크에는 쓰디쓴 진실이 하나 있다: 큰 책임엔 큰 에너지가 필요하다. Reactive를 사용하게되면 굉장히 혼잡해지기 쉬워진다. 다른말로 설명하자면, 문제가 하나 생기면 앱을 디버깅하는데 시간이 굉장히 많이 걸리며, 아래와 같은 콜 스택을 보게 될것이다.

Reactive DebuggingReactive Debugging

우리의 예제에서는 FRF 프레임워크나 KVO까지도 배보다 배꼽이 식이다. 대신에 showGreeting 메소드를 이용하여 갱신하기 위한 View Model 명백하게 물어 것이고 greetingDidChange 콜백 함수를 위해 작은 프로퍼티를 사용할것이다.

이제 돌아와서 특징들을 나열해보겠다:

  • Distribution우리의 작은 예제에서는 명료하게 나타나지 않았지만, 사실 MVVM View MVP View보다 책임이 많다. 왜냐면 두번째 것이 Presenter 포워드(forward)하고 자신를 갱신 하지는 않은 그 때, 바인딩을 세팅함으로써 View Model에서 처음 것의 상태를 갱신한다.
  • TestabilityView Model View에대해 전혀 모르며, 이것이 테스트하기 쉽게 해준다. View 또한 테스트 가능하지만 UIKit 의존이면 그러고 싶지 않게 원하게 될것이다.
  • Easy of use우리 예제에서는 MVP 비슷한 양의 코드나 나왔으나 View에서 Presenter으로 모든 이벤트를 포워드하고 View 갱신하는 실제 앱에선 바인딩을 사용했다면 MVVM 코드 양이 적을 이다.


MVVM 앞에서 말한 장점들을 합쳐놓은것 같아서 굉장히 매력적이다. 그리고 View입장에서 바인딩을 하기 때문에 View 갱신하는데 추가적인 코드를 필요로 하지도 않는다. 그럼에도불구하고 테스트에도 굉장히 좋은 수준이다. (역자주: 완전 극찬이군요)


VIPER

iOS 설계에 레고 조립 경험을 적용하다

VIPER 마지막 지원자다. 이것이 특별히 흥미로운 이유는 MV(X) 카테고리로 부터 나온 녀석이 아니기 때문이다.

이제부터 당신은 책임의 단위가 매우 좋다고 인정하게 될것이다. VIPER 분리된 책임이라는 아이디어에서 생겨난 다른 iteration 만드며, 이번 시간에는 다섯 레이어를 것이다.

VIPERVIPER

  • Interactor데이터 개체나 네트워킹과 연관되어있는 비지니스 로직을 가지고, 서버로부터 그들을 받아오거나 개체 인스턴스를 만드는것을 좋아한다. 이러한 목적으로을 위해서 당신은 VIPER 모듈의 일부로써 몇몇 Services Managers 사용해야 것이나, 다소 외부 의존도가 있을것이다.
  • Presenter—Interactor에서 발생되고 비지니스 로직과 관련있는 (그러나 UIKit과는 관련없는) UI 가진다.
  • Entities일반적인 데이터 객체이다. (데이터 접근 레이어(data access layer) Interactor 책임이기 때문에 Entities 아니다.)
  • Router—VIPER 모듈 사이의 연결고리(seques) 책임을 가진다.

기본적으로 VIPER 모듈은 스크린(screen)이나 당신 어플리케이션의 모든 ***사용자 스토리(user story) 있다인증을 생각해보면 스크린이나 여러개가 하나에 연관되어 있을 있다. 얼마나 작은 “LEGO” 블럭어여야 할까?—전적으로 당신에게 달려있다.

MV(X) 종류와 비교하면, 우리는 책임의 분리가 다르다는걸 확인할 있다:

  • Model(data interation) 로직은 데이터 구조로써 Entities 함께 Interactor 이동된다.
  • 오직 Controller/Presenter/ViewModel Presenter 이동하는 UI 표시 책임을 갖지만, 데이터를 변경할 능력은 없다.
  • VIPER 명시적으로 Router에의해 결정된 네비게이션 책임을 해결한 패턴이다

iOS 어플리케이션 입장에서는 각기 방법으로 라우팅 하는게 도전이라고 수있다. MV(X) 패턴들은 이러한 이슈가 발생하지 않는다.

토픽이 MV(X) 패턴을 반영하지 못했으므로, 예제 또한 라우팅이나 모듈간의 interaction 반영하지 않았다

이제 다시 돌아와 특징들을 살펴보자:

  • Distribution틀림없이 VIPER 책임 분배의 최고봉이다.
  • Testability분리가 잘되있는만큼 테스트에도 좋다.
  • Easy of use마지막으로 여러분이 이미 추측한것처럼 두배 정도의 유지보수 비용이 들것이다. 매우 작은 책임을 위해 수많은 클래스 인터페이스를 작성해야하는 점이다.

그래서 레고는 뭐였나?

VIPER 사용하는 동안 레고로 엠파이어 스테이트 빌딩(위키:엠파이어 스테이트 빌딩은 1931년부터 1972년까지 세계 최고층 건물이었다.) 쌓는 기분이 들것이자, 이것이 유일한 문제이기도하다. 아마 당신 앱에 VIPER 적용시키기에 이를수도 있고 좀더 간편한것으로 고려해도 좋다. 몇몇 사람들은 이걸 아예 무시하고 대포에다가 화살을 쏘아대는 경우도 있다. 지금은 비록 엄청나게 높은 유지보수 비용이 들지만, 그들이 미래에는 그들의 앱에 VIPER 필요할지도 모른다는걸 알고있을거라 생각한다. 만일 당신도 생각이 같다면 Generamba(VIPER 골격을 제공해주는 ) 한번 사용해보길 바란다. 개인적으로는 이건 새총 대신에 자동 대포 조준 시스템을 사용하는 느낌이긴하다.


결론

우리는 몇몇 다른 아키텍처 패턴을 살펴보았고, 무엇이 당신을 괴롭히는지 찾아냈기를 바란다. 그러나 여기에 완벽한 해답은 없고 아키텍처를 선택하는게 당신의 특별한 상황에서 문제의 비중을 등가교환하게 된다는걸 알게되었음을 의심하지 않는다

그러므로 앱에 다른 아키텍처를 섞어 사용하는것은 자연스러운 일이다. 예를들어 MVC 시작했지만 어떤 화면에서만 MVC 관리하기 어려워지는 상황이 생기면 부분만 MVVM으로 바꿀 있다. 이런 아키텍처들은 서로 공존할 있기때문에, 다른 화면이 MVC 골격으로 동작하면 바꿀 필요가 없다



Make everything as simple as possible, but not simpler 
이론은 가능한 간단해야하지만지나치게 간단해서는 안된다
— Albert Einstein




*비지니스 로직 (business logic)

**바인딩 (binding)

***사용자 스토리 (user story)



iOS 아키텍처 관련 번역글



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

원문 : medium.com/@mandrigin/ios-app-performance-instruments-beyond-48fe7b7cdf2?source=userActivityShare-d07a45aa48c6-1455001286


iOS 퍼포먼스: Instrument 이상

유저들은 기다리는걸 굉장히 싫어한다. 그들은 앱이 뭔가 초기화 한다는것을 전혀 모른 상태에서 자신들의 업무 처리를 최대한 빠르게 하고싶어한다. 그러므로 앱이 모두 즉각적으로 시작할 있다면 인터페이스가 흐르듯 부드럽게 넘어갈 있다. 퍼포먼스가 나오는 앱은 소프트웨어 마켓에서 경쟁력있는 특징중 하나이다. 개발자로서 앱이 퍼포먼스있게 동작하게 하는것을 자랑스러워하는 것을 원하는것도 이유이다.

그러나 많은 이들이 겪듯 퍼포먼스 최적화는 다루기 어려운 문제이다. 대부분 문제를 직관적으로 접근하기 어렵다. 각기 정리된 측정법 없이는 앱이 느려지는 이유를 알아내는건 매우 어렵다.

당신의 퍼포먼스를 최적화하기 위해서는 데이터에 기반한 결론을 내려야한다. 장에서는 당신의 앱에 서로 다른 부분에서 퍼포먼스 측정 데이터를 어떻게 얻어낼 있는지 보여줄것이다.

파트에서는 아래의 것들을 다룰 것이다.

  • 앱에서의 CPU, GPU, 메모리&베터리 사용량
  • 반응성(리스폰시브니스)
  • 앱시작동안의 시간
  • 유저로부터 퍼포먼스 데이터를 얻는

바로 시작해보자!

CPU, GPU, 메모리&베터리 사용량

첫번째 할일은 CPU, GPU, 메모리를 과하게 사용하는 비효율적인 코드를 찾아내는 일이다. 애플은 일을 하기위한 좋은 (Instruments) 제공한다.

우리가 주로 측정해야할 부분은 아래 4 정도이다.

  • CPU ("Time Profiler" 툴 이용)
  • GPU (“Core Animation” 툴 이용)
  • 메모리 사용량 (“Allocations” 툴 이용)
  • 베터리 소모량 (“Energy diagnostics” 툴 이용)

WWDC 비디오는 당신 앱을 분석하기위한 최고의 정보를 제공한다.
아래는 시작하면서 몇 개 골라보았다:


반응성(Responsiveness)

퍼포먼스 측정(이해 "측정"으로 줄여서 말하겠습니다)에 있어서 다음으로 중요한 것은 UI 반응성이다. 터치 헨들링은 메인 쓰레드에서 발생한다. 메인쓰레드에서 시간이 걸리는 작업을 하면, 앱은 버벅거리게 될것이다.

몇몇 동작은 CPU 사용하지 않는 주제에 시간을 잡아먹기도 한다. 만약 메인쓰레드에서 동기화 콜을 불렀다면, 콜이 얼마나 시간이 걸리는지 알아내는것이 문제를 해결하는 방법일것이다.

시간을 측정하기 위해 로그를 찍어볼 수도 있다.

한가지 다른 방법은 Viber 개발자들이 만든 솔루션으로 나타내는 것이다. 솔루션은 메인 쓰레드 하나를 400ms보다 많이 멈추지 않게 지켜보고, 체크하는 특별한 스레드를 가지고있다.

Testing Responsiveness (from Viber’s presentation at NSSpain)


Testing Responsiveness (from Viber’s presentation at NSSpain)


많은 정보는 발표자료(PDF, 7MB)에서 확인할 있다.

데이터를 이용하여 너무 많은 시간이 걸린 (메인싸레드가 멈추는데에는 400ms정도가 적당한 최대치이며, 보면 많은 정보를 얻을 있다.) 찾아내고, 이것을 최적화 시킬지 메인쓰레드 밖으로 보내던지 해야한다.

시작 시간

다음으로 중요한 측정은 앱의 시작하는데 걸리는 시간이다. 전형적인 유저는 당신의 앱을 오직 몇분만 사용한다. 시작시간은 앱의 이미지에 좋지않은 영향을 준다.
여기 시작의 2가지 경우가 있다.

  • Cold 시작 : 당신의 프로세스가 동작하지 않고, OS 의해 실행된다.
  • Warm 시작 : 당신의 앱은 최소화되나 죽지않는다. 이것은 백그라운드로부터 다시 불러온다.


색션에서는 리소스를 많이 잡아먹는 Cold 시작에 초점을 맞출것이다
아래에 iOS 앱의 시작 순서가 나와있다.


The Application Startup Phases (from the documentation)

1. 시작하는데 걸리는 시간을 측정한다.

우리는 main()에서부터 applicationDidBecomeActive:까지 시간이 얼마나 걸리는지 측정해야한다.

앱의 기능을 보여주면서 시간을 잡아먹게 하지 말아야한다. cold 시작 시간을 1 미만으로 떨어뜨리려고 노력하는 것이 좋다.

2. 시작하는 순서에서 부분별로 측정한다.

보통 시작시간의 전체만 아는 것은 충분하지않다. 어떤 부분에서 시작시간을 느리게 만들었는지 아는것 또한 중요하다.
밑에 보이는 것들이 가장 중요한 부분들이다

  • -[AppDelegate application:didFinishLaunchingWithOptions:] - 콜백은 런칭이미지(혹은 스토리보드) 보여질때 호출된다. 곧바로 메소드로부터 return되면 실제 UI 로딩을 시작한다.
  • -[UIViewController loadView] - 앱에 커스텀 뷰를 불러와야한다면, 여기서 뷰를 초기화하게된다.
  • -[UIViewController viewDidLoad] - 뷰가 불러와졌고, 마지막 초기화의 시간이다.
  • -[AppDelegate applicationDidBecomeActivate:] - UI 이미 초기화되어있지만, 콜백이 끝날때까지 블럭되있다. 메소드는 백그라운드로부터 restore될때 호출된다.

몇몇 매소드가 너무 시간이 많이 걸린다면, 최적화시켜야한다.

3. “under pressure” 시작 시간을 측정한다.

전형적으로 현실과 테스트 환경은 다르다는걸 알아야한다.

당신의 앱은 안타깝게도 "현실세계"에서 작동된다. 유저는 종종 다른 앱에서 당신의 앱을 열기도 한다. “다른 굉장히 무거운 앱일 수도 있다. 당신의 앱이 시작될때 다른 무거운 앱은 백그라운드로 가면서 데이터를 저장하려고 있으며, 그런 상황에서의 시간 측정은 굉장히 중요하다.

그런 테스팅(무거운 다른 앱에서 내 앱을 열어보는)을 통해 예측하지 못한 결과를 만들 수도 있기 때문에, 이전에는 코드가 완벽하게 안정적이다가도, 저런 상황에서는 느려질 수도 있다는걸 명심해야한다.

4. 앱이 이미 시작되었지만 여전히 소용없다.(무슨 의미인지 모르겠습니다)

만약 당신 앱이 곧바로 UI 불러오는것이 무의미한 일이라면, 런칭화면이 끝나지 않을 것이다. 비록 UI 불러와지고 반응이 왔었어도, 불러오는데 준비를 위한 데이터가 필요하며, 또한 역시 시작 시간을 재어봐라.

당신의 유저로부터 측정값을 수집해야한다.

모든 측정은 테스트 환경에서 가능할 것이다. 그것은 반드시 필요하지만 너무 완벽하게까지 할필욘 없다. 만약 당신 앱이 인기있는 앱이라면, 만약 유저들이 해외시장을 기반으로 한다면, 몇몇 유저들은 당신이 예상하지 못한 아주다른 환경을 사용할 수도 있다.

추측하기에 다를 있는 환경들이다.

  • 네트워크 상태
  • 하드웨어
  • 소프트웨어(OS 버전, Jailbreak(탈옥...)
  • 기기의 남은 용량
  • 기타 등등

분명 당신 개발실에서 측정한 것은 모두 안정적인 상태일지라도, 별점하나로 컴플레인("앱이 너무 느림ㅡㅡ") 리뷰를 받을 수도 있다. 어떻게 대처하면 될까?
퍼포먼스 측정의 집합을 정의하고(혹은 KPI) 실유저로부터 얻어와야한다. 이것은 대부분 통계 패키지와 함께 사용할 있다.

아래는 당신의 유저로부터 얻을 있는 KPI 예시들이다.

  1. cold 시작시간
  2. warm 시작시간
  3. 마디별 시작시간
  4. 반드시 서버로부터 다운받아야할 것들의 소요시간
  5. 메인쓰레드가 400ms보다 오래 블럭되는
  6. 메모리 워닝이 일어나는
  7. FOOMS(링크)
  8. UI 블럭되거나 쓰잘때기없는 동작의 길이 

결론

쉽게 설명하자면 퍼포먼스 측정은 Instruments.app 프로그램을 열면서 시작하며, 앞에서 본 것 말고도 다양한게 있다. 몇몇 소개된 방법은 구현하기 쉽지만 어떤건 시간과 노력이 필요할 것이다. 어쨋든 솔루션들은 당신의 퍼포먼스 이슈를 찾고 해결을 위한 모니터링하는데 도움을 줌과 동시에 즐겁게 만들것이다.

별점5 리뷰를 받기를 바란다!


+) 이해하면서 의역을 부분이 많이 있습니다. 잘못 이해한 부분이 있다면 지적해주시면 감사합니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

원문 : https://blog.risingstack.com/fundamental-node-js-design-patterns/

Translated by canapio
Help by soojin

디자인패턴에 대해 이야기할 때 당신은 singleton, observer, factory들을 생각할 것이다. 이 글은 단지 그것들에 대해서만 이야기하는것은 아니고 dependency injection이나 middleware와 같은 다른 일반적인 패턴과 함께 다룰것이다.

디자인 패턴이란?

디자인 패턴은 흔히 발생하는 문제를 재사용가능하게 일반화하여 해결한다.

Singleton

singleton 패턴들은 해당 "클래스"의 인스턴스 갯수를 한개로 한정한다. Node.js에서는 require을 사용함으로써 꽤 쉽게 싱글톤을 만들 수 있다.

//area.js
var PI = Math.PI;

function circle (radius) { 
  return radius * radius * PI;
}

module.exports.circle = circle; 

당신의 응용프로그램에서 싱글턴 객체를 얼마나 사용하든 상관없이; 오직 하나의 객체로 존재하게 될 것이다.

var areaCalc = require('./area');

console.log(areaCalc.circle(5));

require의 동작 덕분에, 싱글톤들은 NPM모듈들 사이에서 가장 일반적인 Node.ja 디자인 패턴들일 것이다

Observer

한 객체는 상태가 바뀔때 dependents나 observer의 리스트를 자동으로 유지하고 그것들을 알린다. Observer 패턴을 구현하기 위해서는 EventEmitter를 끌어 사용해야한다.

// MyFancyObservable.js
var util = require('util');  
var EventEmitter = require('events').EventEmitter;

function MyFancyObservable() {  
  EventEmitter.call(this);
}

util.inherits(MyFancyObservable, EventEmitter);  

이것이 그 방법이다; 우리는 단지 옵저버가 가능한 객체를 만들었다! 이것을 유용하게 만들기 위해서는 몇가지 기능을 추가하면 된다.

MyFancyObservable.prototype.hello = function (name) { 
  this.emit('hello', name);
};

잘 했다. 이제 우리의 observable은 이벤트를 발생시킬 수 있다. 이제 사용해보자!

MyFancyObservable.prototype.hello = function (name) { 
  this.emit('hello', name);
};

Factory

팩토리 패턴은 생성자 대신 제네릭한 인터페이스를 만들어야 하는 creational pattern이다.
이 패턴은 만들려는 프로세스가 복잡할 때 굉장히 유용하게 쓰인다.

function MyClass (options) {  
  this.options = options;
}

function create(options) {  
  // modify the options here if you want
  return new MyClass(options);
}

module.exports.create = create;  

펙토리는 테스팅 또한 쉽게 만든다. 가령 이 패턴을 이용해 모듈에 dependency를 넣을 수 있다.

Dependency Injection

Dependency injection은 의존객체에 하나 이상의 dependency를 주입하거나 참조로 전달하는 소프트웨어 디지인 패턴이다.     

예를들어 데이터베이스에 의존적인 UserModel을 생성해보자.

function userModel (options) { 
  var db;

  if (!options.db) {
    throw new Error('Options.db is required');
  }

  db = options.db;

  return {
    create: function (done) {
      db.query('INSERT ...', done);
    }
  }
}

module.exports = userModel;

이제 이걸 이용해서 인스턴스를 만들 수 있다.

var db = require('./db');

var userModel = require('User')({  
  db: db
});

왜 이게 유용한가? 이것은 테스팅을 엄청나게 쉽게 만들어준다 -당신이 유닛테스트를 만들 때, 이 모델에 가짜 db 인스턴스를 쉽게 넣어줄 수 있다.

Middleware / pipeline

Middleware는 강력하지만 아주 심플한 컨셉이다: 한 유닛이나 한 함수의 결과값은 다음을 위한 인풋이다. 만약 당신이 이미 ExpressKoa를 사용했다면 이 컨셉을 이미 사용해보았다.

Koa가 어떻게 그것을 하는지 확인해보자:

app.use = function(fn){  
  this.middleware.push(fn);
  return this;
};

기본적으로 이 코드는 middleware를 추가하면 단순히 middleware 배열에 추가한다. 지금까지는 잘 되고 있다. 그러나 서버에 요청을 하면 어떨까?

var i = middleware.length; 
while (i--) { 
  next = middleware[i].call(this, next);
}

마법이 아니다 - 당신의 middleware는 줄줄이 호출된다. 

Streams

stream은 특별한 pipeline으로 생각할 수 있다. 이것은 객체가 아닌 bytes이지만 많은 양의 데이터 흐름을 처리하는 데 좋다.

process.stdin.on('readable', function () {  
    var buf = process.stdin.read(3);
    console.dir(buf);
    process.stdin.read(0);
});

$ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume2.js 
<Buffer 61 62 63>  
<Buffer 0a 64 65>  
<Buffer 66 0a 67>  
<Buffer 68 69 0a>  

Example by substack

stream에 대해 더 공부하고 싶으면 substack의 Stream Handbook을 확인해보자.

Further reading



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

원본 : http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1

위 링크의 강좌를 2포스트에 걸쳐서 번역을 진행할 것이다. 본 포스트는 위 원문 포스팅의 절반을 번역한 것이다.


Update note : Swift를 사용한 iOS8 기반의 Grand Central Dispatch tutorial 최신 튜토리얼이 있다!

비록 Grand Central Dispatch(혹은 줄여서 GCD)가 얼마동안 주변에 있어 왔다고 해서, 모두가 그것을 잘 아는건 아니다. 이것은 이렇게 이해될 수 있다; 동시에 하는것은 편법이고, C기반 API의 GCD는 Objective-C의 세계로 자연스럽게 뽀죡한 끝으로 푹 찌르는 것처럼 보인다. 깊이있게 두 파트의 시리즈를 가진 Grand Central Dispatch 강좌를 배워보자.

이 두 파트에서, 첫번째 강좌에서는 GCD가 뭘하는지와 GCD 기능 기반의 몇몇 예시를 보여줄것이다. 두번째 파트는 GCD가 가진 더 많은 기능적인 이점을 배울 것이다. 



GCD란?
GCD는 libdispatch를 위한 마케팅적인 이름이다, 애플 라이브러리는 iOS와 OSX의 멀티코어 하드웨어에서 코드 동시 실행을 위한 기능을 제공한다. 이것은 아래 이점들을 따른다:
  • GCD는 무거운 처리를 살짝 뒤로 미루고 백그라운드에서 처리할 수 있게 도와주므로써  당신 앱의 대응력을 높혀준다.
  • GCD는 쓰레드를 락 거는것 보다 더 쉬운 동시 실행 모델을 제공하고 동시 실행시 생기는 버그들을 피할 수 있게 도와준다.
  • GCD는 싱글톤으로써 일반적인 패턴의 높은 퍼포먼스와 함께 당신의 코드를 아름답게 만들어 줄 것이다.
이 강좌는 블록코딩과 GCD가 어떻게 동작하는지 잘 알고있다는 가정하에 진행한다. 만약 GCD를 처음 접한다면 Multithreading and Grand Central Dispatch on iOS for Beginners를 먼저 필수로 보고 오면 좋겠다.


GCD 용어
GCD를 이해하기위해, 여러분은 쓰레딩과 동시실행어대한 몇몇 개념을 명확하게 할 필요가 있다. 이것들은 둘다 모호하고 헷갈리는 것이지만 잠시 다시 GCD의 관점에서 그것들을 가볍게 생기시켜 봐야한다.
(역자 주 : 원문에서는 GCD 용어라고 해놓았지만, 구글에 "쓰레드 용어"로 검색하면 더 많은 정보를 얻을 수 있다)

Serial vs. Concurrent
이 용어는 작업들이 서로관에 연관이되어 실행되는 경우를 설명하는 용어이다. 연속적으로 실행되는 작업들은 항상 한번에 실행된다. 작업들은 한번에 동시에 실행되어야한다.

비록 이 용어가 넓은 응용프로그램이지만, 이 강의의 목적상 당신은 Objective-C block 작업 수행에만 초점을 맞출 수 있다. block이 뭔지 모른다고? 여기 이 강좌(How to Use Blocks in iOS 5 Tutorial)를 참고해라. 사실 당신은 또한 함수포인터와 함께 GCD를 사용할 수 있지만, 대부분의 케이스에서는 이것이 사용에 실질적이고 편법적이다. Block은 매우 편하다!



Synchronous vs. Asynchronous
GCD에서, 이 용어는 함수가 연관된 다른 작업을 끝났을때 함수는 실행하기위해 GCD에게 물어보는 것을 뜻한다. 동기화(synchronous) 함수는 명령된 일이 모두 끝난 후에 리턴한다.

반면 비동기화(asynchronous) 함수는 즉시 리턴하고, 일이 다 끝났다고 알려주지만 실제 일이 끝날때까지 기다리지는 않는다. 그러므로 비동기화 함수는 다음 함수에서 처리되는 실행의 현재 쓰레드 block이 아니다.

조심하자 - 현재 쓰레드의 동기화 함수 “blocks”을 읽을때나 함수가 “blocking”함수 이거나 blocking operation인경우 혼동하지 말자! blocks라는 동사는 어떻게 한 함수가 현재 쓰레드에 영향을 미치는지이고 명사 block과는 아무 연관이 없다. 명사 block는 Objective-C에서 이름없는 함수의 용어이고 GCD에 보내는 일들을 정의한다.

Critical Section
이것은 한번에 두 쓰레드가 도는 상황에서 반드시 동시에 실행되지 않는 코드 조각이다. 동시 프로세스에 의해 접근된다면, 이것은 코드가 (변수와 같은) 공유된 자원을 동시에 건드릴 수 있기 때문에 잘못된 값이 들어가거나 할 수 있다.

Race Condition
---

Deadlock
만약 그 두가지(혹은 그 이상)의 일이 서로의 처리가 끝나기를 기다리고 있을때 이것을 deadlock 되었다고 부른다(대부분의 쓰레드에서 나타날 수 있다). 첫번째 처리는 두번째 처리가 끝나기를 기다리기 때문에 끝날 수 없다. 그러나 두번째 처리 또한 첫번째 처리가 끝나기를 기다리기 때문에 두번째 처리도 끝날 수가 없을 것이다.

Thread Safe
쓰레드 세이프 코드는 여러 문제(데이터 오염, 크래쉬 등등)를 피하면서 멀티쓰레딩이나 동시처리로부터 안전하게 콜이 가능하다. 쓰레드 세이프가 되지 못한 코드는 한번에 한 콘텍스 안에서 동작한다. 쓰레드 세이프 코드 중 하나는 NSDictionary이다. 당신은 멀티 쓰레드 이슈 없이 저것을 이용할 수 있다. 반면 NSMutableDictionary는 쓰레드 세이프가 아니다. 이것은 오직 한번에 한 쓰레드안에서 접근할 수 있다.

Context Switch
context switch는 당신이 한 싱글 프로세스에서 다른 쓰레드를 실행하여 전환할 때의 저장 및 복구 실행 상태의 프로세스이다. 이 프로세스는 멀티쓰레딩하는 앱을 만들때 아주 일반적인 방법이지만, 이것은 추가적인 비용이 따른다.

Concurrency vs Parallelism

참고 한글 자료 : http://skyul.tistory.com/263
Concurrency와 parallelism은 종종 함께 거론된다. 그래서 짧게 그 두개를 비교하여 설명할 것이다.

concurrent 코드의 나눠진 부분은 “동시에” 실행될 수 있다. 그러나 그것이 어떻게 처리되는지는 시스템이 정하기 나름이다.

멀티코어 디바이스는 병렬적으로 같은 시간에 멀티 쓰레딩을 실행시키지만, 싱글코어 디바이스는 이것을 수행하기 위해 쓰레드를 실행시키고, 컨텍스 스위칭을 동작하며, 다른 쓰레드나 프로세스를 작동시킨다. 이것은 아래 그림처럼 마치 병렬적으로 수행되게 보이는데 충분히 빨리 수행된다.

비록 당신이 GCD에서 동시수행을 코드에 사용했었어도, 얼마나 병렬수행이 필요한지 GCD가 정하기 나름이다. 병력수행은 동시에 일어나는 것을 요구한다. 그러나 ‘동시’는 보장된 병렬을 제공하지 않는다.

중요한 포인트는 ‘동시’는 사실 구조에 관한 것이다. 당신이 GCD를 코드에 사용할 생각이 있을 때, 당신은 동시에 일어날 수 있는 일의 부분을 당신 코드 구조체에 노출한다. 게다가 한개는 반드시 동시에 일어나지 않는다. 만약 당신이 이 주제에대해 더 깊게 알고싶으면 this excellent talk by Rob Pike를 확인해보아라.

Queues

GCD는 코드의 블럭들을 다룰 수 있게
dispatch queues를 제공한다; 큐(queue)들은 당신이 GCD에 제공한 테스크를 관리하거나 FIFO 명령에 의한 테스크를 실행시킨다. 첫 테스크를 큐에 추가하는데 이점은 첫번째 테스크가 큐에서 시작되고 두번째로 추가된 테스크가 두번째에서 시작할것이며, 차례로 될 것이다. 

모든 dispatch queues는 당신이 멀티쓰레드에서 동시에 접근하려해도 스스로 쓰레드 세이프하다. 이러한 GCD의 이점은 어떻게해서 dispatch queues가 당신의 코드의 부분에서 쓰레드-세이프한 것을 제공하는지 이해할때 나타날 것이다. 이것의 핵심은 옳은 dispatch queue 종류와 옳은 dispatching function을 골라서 당신의 일의 queue에 보내기 위함이다.

이 섹션에서 특별한 GCD queue가 제공하는 두가지 종류의 dispatch queue를 보게 될 것이다, 그리고 GCD dispatching function과 함께
큐에 어떻게 추가하는지 알려주는 형상화한 예제를 돌려볼 것이다.

Serial Queues

serial queues에서 데스크는 한번에 한회만 실행된다, 각 테스크는 이전 실행되는 테스크가 끝나고나야지만이 시작된다. 뿐만아니라, 아래 그림과같이 우리는 한 블럭의 끝나는 점과 다음 블럭의 시작하는 점의 시간차이를 알지도 못한다.

이 테스크의 동작 타이밍은 GCD의 컨트롤 아래에 이루어진다; 당신이 알고 있는 GCD의 이점은 한번에 한번만 수행한다는것과 queue에 추가되어서 명령을 받으면 테스크를 실행한다는 것이다.

Seiral queue에서는 절때 동시에 두 테스크가 실행될 수 없으나, 동시실행에서 같은 섹션을 접근할 위험은 없다; 이 테스크를 오직 수행한다는 점에서 race condition으로부터 섹션이 위험해지는것을 막는다. 그러므로 위험한 섹션에 접근 할 수 있는 유일한 방법은 dispatch queue에 테스크를 담아서 보내는 방법이다, 그리고 위험한 섹션이 안전하다는 것을 검증받을 수 있다.



Concurrent Queues

concurrent queues에 테스크들은 추가하라는 명령에서 시작하는 보장을 가지고... 그것은 당신이 완전히 보장됬다는 것을 의미한다! 요소들은 어떤 명령들도 끝낼 수 있고 당신이 다음 블럭이 언제 시작될건지 모르거나 많은 블럭이 얼마동안 시간을 잡아먹으면서 동작하는지 몰라도된다. 이것이 GCD의 모든것이다.

아래 그래프는 GCD에서 4개의 동시수행을 동작하는 테스트 샘플이다.


Block 1, 2, 3 모두가 어떻게하면 빨리 연이어서 실행될지 주목하라, Block0이 시작된 후 블럭1이 시작되는 동안 일어날 것이다. 또한 Block3은 Block2 다음에 시작되지만 Block2보다 빨리 끝난다.

block이 언제 시작되는지의 결정은 완전히 GCD에서 한다. 만약 block의 실행 시간이 다른것과 겹칠때, 다른 코어에서 실행시킬지, 한개만 사용할것인지 혹은 코드를 다른 block에 콘텍스 스위칭을 할지 정하는건 GCD가 하기 나름이다.

단지 흥미로운 점은 GCD가 각자 다른 queue 타입으로부터 정해서 적어도 5가지의 각 queue들을 제공한다는 점이다.


Queue Types

처음으로, 이 시스템은 당신이 main queue라고 알고있는 특별한 종류의 큐를 제공한다. 다른 queue들과 같이, 이 큐에서도 한번에 테스크를 수행한다. 그러나 이것은 모든 테스크를 메인 스레드에서 수행할 것이라는 뜻이며, 당신의 UI를 유일하게 업데이트 할 수 있는 스레드이기도하다. 이 queue는 UIView에 메세지를 보내거나 노티피케이션을 보내는 것에 사용되어야하는 유일한 queue이다.

시스템은 또한 여러 동시 queues를 제공한다. 이것은 우리가 알고있는 Global Dispatch Queues라고 불리는 놈이다. 이것은 다른 우선순위를 가진 4개의 global queue가 있다: backgroundlowdefault, and high

마지막으로, 당신은 커스텀화된 종류나 동시 queues 또한 생성할 수 있다. 이 말은 당신이 적어도 5개의 queue들을 마음대로 생성 소멸시킬 수 있다는 뜻이다: main queue, 4개의 global dispatch queues, 추가로 당신이 커스텀화시켜 만든 queues 까지!

그리고 dispatch queue들의 큰 그림이 있다!

GCD의 예술적인 면은 queue에 당신의 일을 적당한 queue dispatching function을 골라 보내는 역할을 한다는 것이다. 이것을 경험하기 가장 좋은 방법은 권장해놓은 길을 따라서 예제를 실행해보는 것이다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,


종종 좋은 것들은 한곳에 머무르지 않고, 그래서 모카(Mocha)와 Superagent를 이용한 테스트와 Mongoskin과 Express.js를 사용하여 Node.js와 MongoDB를 이용한 JSON REST API 서버를 만드는 튜토리얼을 Express.js 4버전이 출시됨과 함께 만들게 되었다. 최신의 Express.js 4, Node.js, MongoDB를 다루는 튜토리얼을 만나보자.
새 튜토리얼을 위한 소스코드는 github.com/azat-co/rest-api-express (master branch)에서 확인할 수 있다. 이전의 Express 3.x버전을 위한 튜토리얼 코드는  아직 작동하고 express3 branch에 있다.



Express.js4와 MongoDB REST API Tutorial은 아래 파트로 나뉘어 구성되어있다.
1. Node.js와 MongoDB REST API 개요
2. 모카와 Superagent를 이용한 REST API 테스트
3. NPM-ing Node.js Server Dependencies
4. Express.js 4.x Middleware Caveat
5. Express.js와 MongoDB (Mongoskin) 구현
6. Express.js 4 랩을 실행하고 모카를 이용한 MongoDB 테스팅
7. Express.js와 Node.js의 결론 및 확장성

만약 당신이 자장소와 그것이 무엇을 동작하는지로부터 코드의 동작에 관심이 있다면, 여기 REST API server가 어떻게 다운로드되고 동작하는지 간단한 설명이 여기있다.
$ git clone git@github.com:azat-co/rest-api-express.git
$ npm install
$ node express.js


$ mongod 와 함께 MongoDB를 시작한다. 그다음, 새 터미널창을 띄워서 모카 테스트를 실행한다. :

$ mocha express.test.js


아니면, 모카를 전역으로 설치하지 않은 경우. :

$ ./node_modules/mocha/bin/mocha express.test.js



1. Node.js와 MongoDB REST API 개요
Node.js, Express.js, MongoDB(Mongoskin) 튜토리얼은 모카와 SuperAgent를 사용해 테스트를 해나갈것이다. 이것은 Node.js의 JSON REST API 서버를 만들면서 테스트 주도 개발(Test-Driven Development)을 필요로 한다. 
서버 응용프로그램은 Express.js 4버전대 프레임워크와 MongoDB를 위한 Mongoskin 라이브러리를 필요로한다. 이 REST API 서버에서 우리는 CRUD(create, read, update and delete)기능을 실행하고 app.param( ), app.use( )와 같은 Express.js middleware 방식의 메소드를 실행할 것이다.

가장 처음에 할 것은, MongoDB를 설치하는 것이다. 이 링크를 따라가면 할 수 있을 것이다.
우리는 아래의 버전의 라이브러리를 사용하게 될 것이다.
  • express: ~4.1.1
  • body-parser: ~1.0.2
  • mongoskin: ~1.4.1
  • expect.js: ~0.3.1
  • mocha: ~1.18.2
  • superagent: ~0.17.0

만약 버전이 맞지 않다면 코드가 동작하지 않을 수도 있다 :-(



2. 모카와 Superagent를 이용한 REST API 테스트
시작하기 전에, 우리가 만들게될 REST API 서버에 HTTP 요청을 만드는 기능 테스트를 한번 적어보자. 만약 모카를 사용할 줄 알거나, 바로 Express.js 앱 구현을 해보고 싶은 사람들은 마음대로 해도 된다. 당신은 물론 터미널에서 CRUL테스트를 할 수도 있다.
우리는 이미 Node.js, npm, MongoDB를 설치했다고 가정하고, 새 폴더를 만들어보자.
$ mkdir rest-api
$ cd rest-api


우리는 모카, Expect.js, SuperAgent 라이브러리를 사용하게 될 것이다. 그것들을 설치하고, 프로젝트폴더에 들어가서 이 명령을 실행해라.

$ npm install mocha@1.18.2 --save-dev
$ npm install expect.js@0.3.1 --save-dev 
$ npm install superagent@0.17.0 --save-dev


Note: 당신은 물론 (명령에 -g를 넣고) 모카를 전역으로 설치할 수도 있다. 
이제 아까 그 폴더에 express.test.js파일을 만들자. 아래 6가지를 진행할 것이다.
  • 새 객체를 생성한다.
  • 객체의 ID를 가져온다.
  • 객체의 모든 정보를 가져온다.
  • ID를 이용해 객체를 업데이트한다.
  • ID를 이용해 객체를 제거한다.
HTTP 요청은 Super Agent’s와 연관된 (단지 테스트케이스를 넣기만 하면되는) 함수들을 사용하면 굉장히 식은죽먹기이다. 
이 강좌는 Express.js 4, MongoDB, Mocha를 사용하여 REST API를 만드는것이 목적이기 때문에 테스트케이스(test suits)에 대해 깊게 들어가지는 않겠다. 코드를 복붙하시오!

아래 코드는 express.test.js파일이다.
var superagent = require('superagent')
var expect = require('expect.js')

describe('express rest api server', function(){
  var id

  it('post object', function(done){
    superagent.post('http://localhost:3000/collections/test')
      .send({ name: 'John'
        , email: 'john@rpjs.co'
      })
      .end(function(e,res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(res.body.length).to.eql(1)
        expect(res.body[0]._id.length).to.eql(24)
        id = res.body[0]._id
        done()
      })    
  })

  it('retrieves an object', function(done){
    superagent.get('http://localhost:3000/collections/test/'+id)
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body._id.length).to.eql(24)        
        expect(res.body._id).to.eql(id)        
        done()
      })
  })

  it('retrieves a collection', function(done){
    superagent.get('http://localhost:3000/collections/test')
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(res.body.length).to.be.above(0)
        expect(res.body.map(function (item){return item._id})).to.contain(id)        
        done()
      })
  })

  it('updates an object', function(done){
    superagent.put('http://localhost:3000/collections/test/'+id)
      .send({name: 'Peter'
        , email: 'peter@yahoo.com'})
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body.msg).to.eql('success')        
        done()
      })
  })
  it('checks an updated object', function(done){
    superagent.get('http://localhost:3000/collections/test/'+id)
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body._id.length).to.eql(24)        
        expect(res.body._id).to.eql(id)        
        expect(res.body.name).to.eql('Peter')        
        done()
      })
  })    
  
  it('removes an object', function(done){
    superagent.del('http://localhost:3000/collections/test/'+id)
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body.msg).to.eql('success')    
        done()
      })
  })      
})

테스트를 하기위해, $ mocha express.test.js 커맨드를 날릴 것이다. (모카를 전역으로 설치하지 않은 경우 $ ./node_modules/mocha/bin/mocha espress.test.js)


3. NPM-ing Node.js Server Dependencies
이 튜토리얼에서 우리는 Mongoskin을 사용할 것이다. 또한, Mongoskin은 Mongoose나 shema-less보다 훨씬 가볍다. 자세한 내용은 Mongoskin comparison blurb를 확인해보기 바란다.
Express.js는 Node.js HTTP module 핵심 객체로 감싸져있다. Express.js 프레임워크는 Connect middleware의 상위층을 기반으로 만들어져있고, 어마어마하게 많은 편리함을 제공한다. 몇몇 사람들은 ...

만약 당신이 이전 섹션(Text Converage)에서 rest-api 폴더를 만들었다면, 어플리케이션 모듈을 설치하기 위해 아래 명령만 입력하면 된다. :
$ npm install express@4.1.1 --save
$ npm install mongoskin@1.4.1 --save


4. Express.js 4.x Middleware Caveat

슬프게도 NPM express만 하는 것으로는 Express.js와 함께 최소한의 REST API 서버를 구축하는게 불가능하다. 왜냐하면 4.버전대의 middlewares은 번들이 아니기 때문이다!(the middlewares are not bundled) 개발자들은 Express.js 4.x.왼쪽에 있는 express.static를 제외한 분리된 모듈들을 설치해야한다. 그리고 들어오는 정보를 파싱하기위해 body-parser를 추가해야한다:
$ npm install body-parser@1.0.2 --save


5. Express.js와 MongoDB (Mongoskin) 구현

제일 처음에 우리의 express.js 안에 우리의 dependencies를 정의해야한다. :
var express = require('express'),
  mongoskin = require('mongoskin'),
  bodyParser = require('body-parser')


3.x대 이후 버전(물론 v4도 마찬가지), Express.js은 앱 인스턴스의 객체를 간소화해서 가져온다, 아래 라인은 서버객체를 우리에게 제공할 것이다.(번역자:그냥 import정도로 생각하면 될듯):

var app = express()


요청의 바디로부터 파람들을 추출하기위해, 우리는 아래와같은 모양의 bodyParser() middleware를 사용할 것이다.:

app.use(bodyParser())


Middleware(여기, 다른 포럼)는 Express.js에서 강력하고 편리한 패턴이고 구성요소를 연결하며 코드의 재사용을 증진시킨다.

HTTP 요청의 바디객체 파싱의 넘사벽으로부터 구해주는 bodyParser()메소드와 같이, Mongoskin은 딱 한줄의 코드로 MongoDB 데이터베이스에 접속하는게 가능하다.:
var db = mongoskin.db('mongodb://@localhost:27017/test', {safe:true})


Note: 만약 당신이 원격으로 데이터베이스에 접속하고 싶다면(MongoHQ와 같은 것들..), 당신의 username, password, host and port의 값들을 스트링으로 치환하라. 여기 URI 스트링의 포맷이 있다:
mongodb://[username:password@]host1[:port1][, host2[:port2], …[, hostN[:portN]]][/[database][?options]].

app.param() 메소드는 또다른 Express.js middleware이다. 이것은 기본적으로 “요청 핸들러의 URL페턴에 어떤 값이 있으니 매 시간마다 뭔갈 처리해라”는 것을 말하고 있다. 우리의 경우 요청 패턴이 collectionName 스트링에 콜론이 점두사로 있을 때, 우리는 특정 콜랙션을 선택한다. 그러면 다음 요청 핸들러에서 사용할 수 있는 요청 객체(widespreadreq)의 프로퍼티(콜랙션이나 다른것일 수도 있다.)로써 콜랙션을 저장한다. (번역자:뭔소린지 모르겠다)
app.param('collectionName', function(req, res, next, collectionName){
  req.collection = db.collection(collectionName)
  return next()
})


단지 유저지향적으로, 메시지와 함께 루트 라우트를 넣자.:

app.get('/', function(req, res) {
  res.send('please select a collection, e.g., /collections/messages')
})


이제 진짜 할 일을 시작한다. 다수의 요소들중에 리스트를 어떻게 가져오는지 있다 (첫번째 파라메터는 빈 오브젝트{}이고 임의의 라는 뜻이다). 이 결과는 _id에(두번째 파라메터) 의해 정렬된 10개 제한으로 낼 것이다. find()메소드는 커서를 반환하고 우리는 toArray()를 불러 JavaScript/Node.js용 배렬로 만든다. :

app.get('/collections/:collectionName', function(req, res, next) {
  req.collection.find({} ,{limit:10, sort: [['_id',-1]]}).toArray(function(e, results){
    if (e) return next(e)
    res.send(results)
  })
})


URL 패턴 파라메너에서 :collectionName 스트링에대해 언급한적이 있나? 이것과 이전 app.param() middleware는  req.collection 객체를 우리에게 준다. 이 객체는 우리의 데이터베이스에서 특정 콜랙션을 가르키고 있다.

우리는 단지 MongoDB에서 전체적인 페이로드를 지나온 이후로 마지막 시점에 만들어진 이 객체는 조금 이해하기 쉽게 해준다. 이 메소드는 서버나 다른 것들의 데이터베스가 어떠한 데이터 스트럭쳐도 받아드릴수 있기 때문에 종종 free JSON REST API라 불린다. Parse.com과 다른 백엔드 서버 제공자는 free JSON 접근을 만들어낸다. 우리 Express.js 앱에서는 이것을 위해 req.body를 사용할 것이다.
app.get('/collections/:collectionName', function(req, res, next) {
  req.collection.find({} ,{limit:10, sort: [['_id',-1]]}).toArray(function(e, results){
    if (e) return next(e)
    res.send(results)
  })
})


함수들을 구하는 findByIdfindOne과같이 생긴 단일 객체는 find()보다 빠르다. 그러나 그것들은 조금 다른 인터페이스를 사용한다 (그것들은 커서 대신에 진짜 오브젝트를 반환한다). 그러므로 그것을 기억하고 있어라. 추가적으로, 우리는 Express.js 마법에 의해 req.params.id 경로의 :id 부분으로부터 ID를 가져올 것이다.

app.get('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.findById(req.params.id, function(e, result){
    if (e) return next(e)
    res.send(result)
  })
})


PUT 요청 핸들러는 update()가 증가된 객체를 반환하기때문에  더 흥미로운 것을 가져온다.
또한 {$set:req.body}는 값을 저장하는 기능을 가진 특별한 MongoDB 기능이 (보통 달러표시로 시작한다).

두번째 {safe:true, multi:false} 파라메터는 MongoDB에 callback 함수가 실행되기 전까지 동작을 멈추고 오직 한가지(첫번째) 아이탬만 처리하라고 알리는 옵션을 가진 객체이다.
app.put('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.updateById(req.params.id, {$set:req.body}, {safe:true, multi:false}, function(e, result){
    if (e) return next(e)
    res.send((result===1)?{msg:'success'}:{msg:'error'})
  })
})


마지막으로 DELETE HTTP 함수는 app.del()에 의해 실행된다. 요청 핸들러에서, 우리는 그것이 그 동작을 하는 것처럼 보이는 removeById()를 사용한다. 그리고 커스텀 JSON success 메시지를 제거과정에서 내보낼것이다.:

app.del('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.remove({_id: req.collection.id(req.params.id)}, function(e, result){
    if (e) return next(e)
    res.send((result===1)?{msg:'success'}:{msg:'error'})
  })
})


Note: delete는 JavaScript의 연산자이고 대신에 Express.js는 app.del을 사용한다.

아래의 경우 서버 3000포트를 시작하는 마지막라인이다.
app.listen(3000)


단지 이 경우 뭔가 잘 실행되지 않을 수 있다. 여기 express.js 파일의 풀 소스가 있다.

var express = require('express') , mongoskin = require('mongoskin') , bodyParser = require('body-parser') var app = express() app.use(bodyParser()) var db = mongoskin.db('mongodb://@localhost:27017/test', {safe:true}) app.param('collectionName', function(req, res, next, collectionName){ req.collection = db.collection(collectionName) return next() }) app.get('/', function(req, res, next) { res.send('please select a collection, e.g., /collections/messages') }) app.get('/collections/:collectionName', function(req, res, next) { req.collection.find({} ,{limit:10, sort: [['_id',-1]]}).toArray(function(e, results){ if (e) return next(e) res.send(results) }) }) app.post('/collections/:collectionName', function(req, res, next) { req.collection.insert(req.body, {}, function(e, results){ if (e) return next(e) res.send(results) }) }) app.get('/collections/:collectionName/:id', function(req, res, next) { req.collection.findById(req.params.id, function(e, result){ if (e) return next(e) res.send(result) }) }) app.put('/collections/:collectionName/:id', function(req, res, next) { req.collection.updateById(req.params.id, {$set:req.body}, {safe:true, multi:false}, function(e, result){ if (e) return next(e) res.send((result===1)?{msg:'success'}:{msg:'error'}) }) }) app.del('/collections/:collectionName/:id', function(req, res, next) { req.collection.removeById(req.params.id, function(e, result){ if (e) return next(e) res.send((result===1)?{msg:'success'}:{msg:'error'}) }) }) app.listen(3000)

코드를 저장하고 당신의 에디터를 닫아라, 우리의 소박한 Express.js REST API 서버가 완성되었다.


6. Express.js 4 랩을 실행하고 모카를 이용한 MongoDB 테스팅

이제 MongoDB가 설치되고 실행됬다는 가정($ mongod)하에 터미널에서 실행시켜볼 수 있다(modgod와 다른 창을 띄워라).
$ node express.js


그리고 다른 창에서 아래 명령을 쳐라(처음 창은 닫으면 안된다):

$ mocha express.test.js

혹은 모카를 전역으로 설치 하지 않은 경우.:

$ ./node_modules/mocha/bin/mocha express.test.js


만약 모카나 BDD 사용을 원치 않다면, CURL는 언제나 당신을 위해 있다. :-)

예를들어 POST 요청을 만들기 위해 CURL데이터이다. :
$ curl -X POST -d "name=azat" http://localhost:3000/collections/test13


그리고 결과는 아래처럼 나오게 될 것이다.:

{"name":"azat","_id":"535e180dad6d2d2e797830a5"}]


우리는 REST API 서버를 사용하기때문에 쉽게 이 객체를 확인할 수 있다.:

$ curl http://localhost:3000/collections/test13
Using CURL with Express 4 and MongoDB REST API


GET요청 또한 브라우저에서 동작할 수 있다. 예를들어, http://localhost:3000/collections/test13 링크를 당신의 로컬 서버가 포트 3000에서 돌아가고 있을때 열 수 있다.
혹은 서버의 결과를 신뢰하지 못한다면, MongoDB($ mongo)를 이용하여 데이터베이스를 확인할 수도 있다.:
> db.test13.find()


Note: 만약 데이터베이스의 이름을 test라는 이름 대신에 다른 이름으로 바꾸고 싶으면, 명령 앞에 > use your_database_name 을 써라.

이 튜토리얼에서, 우리 테스트들은 실제 동작하는 어플리케이션 코드보다 길다. 몇몇에게는 테스트-기반-개발(Test-Driven Development)을 포기하고싶게 만들지도 모르겠지만, 당신이 어떠한 크고 복잡한 응용프로그램을 개발할 때에도 좋은 TDD 습관이 당신의 개발 시간을 줄여줄것이라 믿는다. 



7. Express.js와 Node.js의 결론 및 확장성
Express.js 4와 MongoDB/Mongoskin 라이브러리는 간단하게 몇 줄 만에 REST API 서버를 구축하기에 정말 좋다. 후에, 당신이 라이브러리를 확장하고자 한다면, 그것들은 또한 당신의 코드를 구성하는 방법을 제공할 것이다.
NoSQL 데이터베이스는 MongoDB와같이 스키마를 정의할 필요 없고 어떠한 다양한 형태의 데이터를 넘기거나 저장할 수 있는 좋은 free-REST APIs이다.
express.test.js, express.js and package.json의 풀소스코드는 github.com/azat-co/rest-api-exrpess에 있다.
Express.js나 다른 자바스크립트 라이브러리에대해 더 공부하고 싶다면, 아래 Azat의 책들을 보라.
  • Practical Node.js: Building Real-world Scalable Web Apps
  • Express.js Guide: The Comprehensive Book On Express.js
..
..
..

연관 글

[Node.js, MongoDB] Node.js 설치 및 실행

[Node.js, MongoDB] MongoDB 리눅스에 설치 및 실행

[Node.js, MongoDB] MongoDB 돌리기 (+백그라운드에서 돌리기)


> (번역) Express.js 4, Node.js and MongoDB REST API 강좌


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

원문 : http://www.dspdimension.com/admin/dft-a-pied/
Posted by Bernsee on September 21, 1999


Step 6 : DFT(Discrete Fourier Transform)

사인 변환에서 푸리에 변환까지 과정을 더 '일반화' 시킴으로써 간단하다. 사인 변환에 측정된 각 진동수를 위한 사인 파형을 사용하는 반면, 푸리에 변환에서는 사인 코사인 파형을 둘다 사용했다. That is, for any frequency we are looking at we ‘compare’ (or ‘resonate’) our measured signal with both a cosine and a sine wave of the same frequency. 만약 우리 신호가 사인 파형과 굉장히 닮았다면, 우리 변환의 사인 부분은 큰 진폭을 가질것이다. 만약 코사인 파형과 닮았다면, 코사인 부분이 커지게 될 것이다. 사인 파형과 정반대라면, 이것은 영에서 시작하여 1로 올라가는게 아니라 -1로 떨어지르것이다, 사인 부분은 음수의 값이 큰 진폭을 가질 것이다. 사인 코사인 위상은 받은 진동수에서 임의의 사인 형태로 보여질 수 있는것과 같이 +와 - 둘이 함께 보여질 수 있다.

//
// Listing 1.2: The direct realization of the Discrete Fourier Transform***:
//

#define M_PI 3.14159265358979323846

long bin, k;
double arg, sign = -1.; /* sign = -1 -> FFT, 1 -> iFFT */

for (bin = 0; bin <= transformLength/2; bin++) {
    cosPart[bin] = (sinPart[bin] = 0.);
    for (k = 0; k < transformLength; k++) {

        arg = 2.*(float)bin*M_PI*(float)k / (float)transformLength;
        sinPart[bin] += inputData[k] * sign * sin(arg);
        cosPart[bin] += inputData[k] * cos(arg);   

     }
}

우리는 여전히 푸리에 변환에 의해 뭔가 유용한 것을 얻어내야하는 과제를 남겨놓고 있다. I have claimed that the benefit of the Fourier transform over the Sine and Cosine transform is that we are working with sinusoids. 그러나 우리는 어떠한 정령 파도 보지 않았다, 오로지 싸인과 코싸인만 있다. 으음 이것은 추가적인 처리를 요구한다.

//
// Listing 1.3: Getting sinusoid frequency, magnitude and phase from 
// the Discrete Fourier Transform:
//

#define M_PI 3.14159265358979323846

long bin;
for (bin = 0; bin <= transformLength/2; bin++) {

    /* frequency */
    frequency[bin] = (float)bin * sampleRate / (float)transformLength;
    /* magnitude */
    magnitude[bin] = 20. * log10( 2. * sqrt( sinPart[bin]*sinPart[bin] + cosPart[bin]*cosPart[bin]) / (float)transformLength);
    
    /* phase */
    phase[bin] = 180.*atan2(sinPart[bin], cosPart[bin]) / M_PI - 90.;

}

DFT 아웃풋의 위 코드를 실행한 한 뒤에, 우리는 정형파의 합으로써 인풋 신호의 계산을 끝날 것이다. k-th 정형파는 frequency[k], magnitude[k] 그리고 phase[k]에 의해 설명된다. 단위는 Hz (Hertz, periods per seconds), dB (Decibel) 그리고 ° (Degree)이다. 이전에 계산한 1.3 Listing의 싱글 정형파의 사인 코사인 부분을 변환한뒤, 이제 항상 양수를 가지는 k-th 정형파의 DFT bin "magnitude" 진폭이라 부를것이고 이것을 기억하고 있어라. 우리는 진폭이 -1.0이든 1.0이든 위상이 +혹은 -180도 다른 magnitude가 1.0이라고 말 할 수 있다. 관련 문헌에서는, magnitude[ ] 배열을 측정된 신호의 Magnitude Spectrum라 부르고, phase[ ] 배열을 프리에 변환에 의해 계산된 측정된 신호의 Phase Spectrum이라 부른다.

데시벨에 bin magnitude를 측정하기 위해 참조함으로써, 우리의 인풋 파형은 DFS(digital full scale)의 0dB 크기를 갖는 [-1.0, 1.0] 범위의 값을 가질 것으로 예상 할 수 있다. DFT의 어플리케이션에 흥미를 보임으로써, 예를 들어 listing 1.3은 DFT를 기반으로한 스펙트럼 분석을 도출하는데 사용될 수 있다.





WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

원문 : http://www.dspdimension.com/admin/dft-a-pied/
Posted by Bernsee on September 21, 1999


Step 5 : Apple과 Oranges에 대해

만약 아직 당신이 따라오고 있다면, 푸리에 변환에 대한 여행이 거의 끝나간다. 우리는 얼마나 많은 사인 파형이 필요로 하는지에 대해 배웠고, 필요로 하는 수는 우리가 보고있는 샘플 수와 밀접한 관게가 있다는것, 낮은 주파수와 높은 진동수의 경계에 있는 것과 어쨋든 우리의 레시피를 완성시키기 위해서는 각 부분의 파형의 진폭을 결정할 필요가 있다는 것을 배웠다. 아직 완벽하게 해결한 것은 아니지만 샘플들을 어떻게 조리할지 결정할 수는 있을 것이다. 쉽게말하면, 우리가 측정한 샘플들과 우리가 알고 있는 진동수의 사인파형을 비교함으로써 사인파형의 진폭을 알아내고, 그것들이 어떻게 '같은지'에 대해 알아내보았다. 만약 그것들이 완벽히 같다면 그 사인파형은 반드시 같은 진폭이고 만약 참고하고있는 사인파형과 우리의 신호가 맞지 않다면 아닐 같은 진폭이 아닐 것이다. 그런데 어떻게 우리가 알고 있는 사인파형과 샘플 신호를 효과적으로 비교할 것인가? 다행히도 DSPer은 이 부분을 미리 만들어 놓았다. 사실 숫자를 곱하거나 더하는 아주 쉬운 부분이다. - 우리는 알고있는 진동수와 유닛진폭(이것은 계산기나 우리 컴퓨터의 sin( )함수로부터 나온 오직 한개의 진폭을 의미한다.)의 '참조하는' 사인파형을 계산하고, 우리의 신호 샘플들을 곱한다. 곱해진 결과값을 더하고나서, 우리가 다루고있는 진동수로부터 사인파형의 진폭을 얻어낸다.

이것을 C로 나타내면 아래와 같이 된다.

#define M_PI 3.14159265358979323846

long bin, k;
double arg;
for (bin = 0; bin < transformLength; bin++) {
    transformData[bin] = 0.;
    for (k = 0; k<transformLength; k++) {

        arg = (float)bin * M_PI * (float)k / (float)transformLength;
        transformData[bin] += inputData[k] * sin(arg);
    }
}

이 코드는 inputData[0...transformLength-1] 에 저장된 샘플 포인트를 transformData[0...transformLength-1]에 사인 파형의 진폭들의 배열 형태로 변환한다. According to common terminology, we call the frequency steps of our reference sine wave bins, which means that they can be thought of as being ‘containers’ in which we put the amplitude of any of the partial waves we evaluate. 일반적으로 쓰이는 용어를 빌리지면, 우리가 계산한 부분 파형의 어떤 진폭을 담은 'containers' 로써 생각될 수 있음을 의미하며, 참조하는 사인 파형 bin들의 진동수 단계라고 부른다
DST(Discrete Sine Transform)는 사인파형 부분들의 진폭을 구하기 위해 우리의 신호가 어떻게 생겼는지 모르거나 아니면 우리가 효과적인 방법을 사용할 수 있다고 가정하는 그러한 일반적인 흐름을 따른다. (예를들어 우리가 미리 우리의 신호가 알고있던 진동수의 한 ㅏ인 파형이라고 안다면, 우리는 즉시 사인파형의 큰 범위를 계산함 없이 진폭을 구할 수 있을 것이다. 푸리에이론에 기반한 이 효과적인 접근은 “Goertzel” 알고리즘을 찾을 수 있게 해주었다.)




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

원문 : http://www.dspdimension.com/admin/dft-a-pied/
Posted by Bernsee on September 21, 1999


Step 4 : 요리법(cooking recipes)에 대해

이전 단락에서는 컴퓨터의 어떤 신호가 혼합된 사인파형으로부터 만들어 질 수 있다는 것을 확인했다. 우리는 진동수에 대해 생각해보았고 어떤 최대, 최소 진동수의 사인파형이 우리가 분석하는 신호를 완벽하게 복원하기 위해 필요로하는지도 생각해보았다. 우리가 봐온 샘플의 수가 가장 낮은 사인 파형이 필요로하는 가장 최하 부분을 결정하는데 얼마나 중요한지 봐왔지만, 우리는 아직 실제 사인파형이 최대 몇개의 결과물을 만들어내는지 논의되지 않았다. 사인 파형에의해 만들어진 신호를 구성하기 위해, 우리는 그 중 한 부분을 확인해볼 필요가 있다. 사실은, 주파수는 우리가 알아야할 유일한 것이 아니다. 우리는 또한 사인 파형들의 진폭을 알 필요가 있다. 즉. 인풋 신호를 다시 만들기위한 각 사인 파형의 갯수. 진폭은 사인파횽의 최대 높이이다. 그러니까 0으로부터 최댓값 사이의 거리를 맗나다. 높은 진폭은 음량이 클것이고, 우리도 그렇게 들을 것이다. 그러므로 신호에 저음이 많다면 높은 주파수의 사인파형 보다 낮은 사인파형들이 많이 합쳐져 있을 것으로 예상할 수 있다. 일반적으로 저음의 낮은 주파수의 사인파형은 높은 사인 파형들보다 큰 진폭을 가지고 있다. 우리의 분석에서, 우리는 레시피(recipe)를 완성하기위해 각 사인파형의 진폭을 정해야 할 것이다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
원문 : Mastering The Fourier Transform in One Day
Posted by Bernsee on September 21, 1999

Step 3 : “많다”는게 얼만큼인가?

위에서 확인했듯이, 복잡한 모양의 파형은 사인 파형들의 조합으로 만들어 질 수 있다. 우리는 이렇게 질문을 던질 수 있다. “컴퓨터가 신호를 만드는데 얼마나 많이 필요로 하나?” 흠.. 물론 이것은 하나의 사인파형일 수도 있다. 그 주어진 사인파형으로 우리가 다룰 그 시그널이 어떻게 만들어지는지 알고있다. 대부분 우리는 복잡한 구조의 현실세계 신호를 다룬다. 따라서 우리는 현재 파형의 얼만큼 나누어져있는지 미리 알지 못한다. 이 경우, 원본 신호를 구성하는 사인파형이 얼마나 많은 상한선을 필요로하는지 모르는 것은 얼마나 다행스런 일인지 모른다. 그치만 여전히 "얼마나 많이”에 대한 질문은 해결되지 않았다. 그럼 좀 직관적으로 접근해보자: 신호 중 1000개의 샘플이 있다고 가정해보자. 이 짧은 주기(신호 안에 대부분 최댓값과 최솟값이 있는)에 있을 수 있는 사인 파형은 모든 샘플마다 최댓값과 최솟값을 왓다갓다 했다. 그러므로 모든 샘플이 피크가 있다고 가정할 때, 가장 높은 주파수의 사인파형은 1000개의 샘플에서 500개의 최댓값과 500개의 최솟값을 가진다. 아래 그래프의 검정색 점을 말하는 것이다. 따라서 가장 진동수가 높은 사인 파형은 아래와 같이 생겼다. 


이제 가장 낮은 진동수의 사인 파형이 어떻게 생겼는지 확인해 보자. 만약 우리가 오직 하나의 샘플 점을 얻었다면, 이 점 하나가지고 어떻게 사인파형의 최댓값과 최솟값을 구하겠는가? 할 수 없다, 그러므로 이 한 점으로부터 나올 수 있는 파형의 주기는 무수히 많다.


그러므로 한개의 데이터 점은 전동수를 표현하기에는 충분하지 않다는 것이다. 이제 우리는 두개의 샘플이 지정되있을 경우, 이 두개의 점을 가지고 가장 낮은 진동수의 사인파형을 만들 수 있을까? 이 경우는 굉장히 단순하다. 여기서 두 점으로 부터 얻은 가장 낮은 진동수의 파형은 오직 하나밖에 없다. 아래 그림을 보자.



한뼘 정도 되는 두 못 사이에 줄이 연결되어 있는 상황을 상상해 보아라 (위 그래프는 사인 파형이 주기를 갖는다는 것을 세개의 점으로 표현한 것이다. 그러나 우리는 진동 수를 알아내기 위해 단지 제일 왼쪽 두 점만 있으면 된다). 사인 파형이 두 점 사이에서 왼쪽으로 가는 것과 같이, 우리가 볼 수 있는 가장 낮은 진동수는 두 못 사이에서 앞 뒤로 흔들리는 줄이다. 1000개의 샘플을 가지고 있다면, 두 ‘못’은 첫번째 샘플과 마지막 샘플이 될 수 있을 것이다, 예를들어 샘플 1번과 샘플 1000번. 우리는 우리의 경험으로 악기의 현 길이가 길어질수록 진동수가 낮아진다는 것을 안다. 그래서 우리는 못을 멀리 떨어뜨릴수록 사인파형의 주파수가 낮아짐을 예상할 수 있다. If we choose 2000 samples, for instance, the lowest sine wave will be much lower since our ‘nails’ are now sample number 1 and sample number 2000. 사실 1000 샘플에서 못을 두배 멀리 떨어뜨리면, 두배로 낮아질 것이다. 그러므로 우리가 더 많은 샘플을 가지고 있으면 0을 교차하는 점('못')을 멀리 떨어뜨려 더 낮은 주파수를 구할 수 있다. 이것은 설명을 이해하는데 굉장히 중요한 부분이다. 

또한 두 ‘못’과 함께 파형은 오르막 경사와 함께 반복된 후에 확인 할 수 있을 것이다(처음과 세번째 못은 동일하다). This means that any two adjacent nails embrace exactly one half of the complete sine wave, 다른말로는 한 최대점 혹은 한 최솟점 혹은 1/2 주기이다.


방금 무엇을 배웠는지 요약하자면, we see that the upper frequency of a sampled sine wave is every other sample being a peak and a valley and the lower frequency bound is half a period of the sine wave which is just fitting in the number of samples we are looking at. But wait – wouldn’t this mean that while the upper frequency remains fixed, the lowest frequency would drop when we have more samples? 정확하다! 우리는 낮은 주파수에서 시작하기 때문에, 이 결과는 알 수 없는 컨텐트의 더 긴 신호를 합치길 원할 때 더 많은 사인 파형을 필요로 하게 될 것이다.

모든것이 순조롭다. 그러나 여전히 결과적으로 얼마만큼의 사인 파형이 필요한지는 알 지 못했다. 이제 낮은 주파수나 높은 주파수의 사인 파형의 일부분을 알 수 있음으로써, 우리는 이 두 한계치 사이에서 얼만큼이 맞는지 계산할 수 있다. 우리는 극좌에서 극우까지 사이에서 가장 낮은 사인파형을 넣었어야 하기 때문에, 다른 모든 사인 파형 뿐만 아니라 nails까지 필요로 한다(왜 저렇게 다르게 다루고 있는가? 모든 사인파형은 같게 만들어지기 때문!). 단지 기타에서 두 고정된 점이 연결되 있는 현을 사인파형이라고 상상해보아라. 현은 두 고정된 점 사이에서만 진동할 것이다(부서지지 않는한), 우리의 사인 파형과 닮았다. 우리가 다루고 있는 1000 샘플에서 가장 낮은 1/2 주기를 가진 부분, 1주기를 가진 두번째 부분, 1과 1/2(1.5)주기를 가진 세번째 부분의 관계를 도출해낸다.

그림으로 그려보면 아래와 같다.


이제 1000샘플에서 얼마나 많은 사인파형이 적당한가를 물어본다면, 정확히 1000개의 사인 파형이 필요하다고 답해주면 된다. 사실 우리는 항상 가지고 있는 샘플의 수에 따라 사인파형이 필요하다는 것을 발견할 것이다.




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

※ 번역 작업 처음 해봅니다.

※ 발번역 죄송합니다.. 틀린점 댓글로 달아주시면 수정하겠습니다.

※ 블로그에 글 올리는 것도 처음 해봅니다. 눈에 거슬리는 점이 있어도 너그럽게 이해해주시길 바라겠습니다.




원문 : Streaming Audio to Multiple Listeners via iOS' Multipeer Connectivity

 Tony DiPasquale  November 20, 2013 

Translate by canapio




음악은 아이폰 혹은 모든 애플 제품에서 굉장히 중요한 부분이다. iOS7이 출현하면서, 애플은 NSOutputStreamNSInputStream를 이용하여 데이터 스트림에 접근이 가능한 "Multipeer Connectivity"이라는 새로운 기술을 선보였다. 그러나, NSOutputStream를 이용해 재생하는 것은 쉬운일이 아니었고, 나는 CoreAudio를 이용해 사용할 수 있게 만드는 모험을 시작했다.

개요
Multipeer Connectivity는 NSOutputStream를 이용하여 연결된 요소(connected peer)에 데이터를 스트림한다. 이것은 오디오 데이터를 전송하는데 사용할 것이다. 전송이 끝나고, Multipeer Connectivity는 우리가 incoming data를 얻는데 사용할 NSInputStream 를 쓴다. 애플에서 제공한 Audio Queue Services을 사용하여, 이 데이터를 디바이스 시스템에 보낼 것이다. Audio Queue Services는 버퍼를 치우고 재생까지 할 수 있게 해준다. 이것은 오디오 데이터 열(audio data raw)을 재생시킬 수 있지만, MP3나 AAC와 같은 대부분의 오디오 파일은 크기를 줄이기 위해 인코딩작업이 되어 있다. 애플은 인코딩된 오디오 포맷을 처리하고 오디오 데이터 열을 반환해주는 Audio File Stream Services를 제공했다. 아래 그림은 데이터의 플로우이면서 계획된 솔루션이 실행되기 직전의 모습이다.



첫째로, 오디오 스트림이 시작되고 데이터를 받으면, 디코딩을 해주는 스트림 파서에 넣는다. 이 파서는 우리가 필요로하는 오디오 데이터 열(audio data raw)를 보내준다. 파서로부터 하나씩 받은 세개의 오디오 버퍼 데이터가 오디오 큐(audio queue)안에 있다. 버퍼가 다 차면 시스템으로 보내진다. 그리고 시스템에서 소리를 다 냈으면 다시 돌아와 다시 채워지고, 소리내고 비우고 채우는 과정을 더이상 플레이할 것이 없을 때까지 반복한다. 아래 GIF는 시스템 하드웨어에서 오디오 데이터가 코드에서 어떻게 흘러가는지 보여주는 애니메이션이다. 빨간 박스는 빈 버퍼, 초록 박스는 가득찬 버퍼를 의미한다.




오디오 데이터 보내기
이제 우리는 스티리밍이 백그라운드에서 어떻게 동작하는지 좀 안다. 아이튠즈 라이브러리에서 노래 한곡을 재생해보자. MPMediaPickerController을 사용하여 유저는 노래를 고를 수 있다. 우리는 피커컨트롤러의 델리게이트 메소드(mediaPicker:didPickMediaItems:)를 이용하여 MPMediaItem들을 담은 배열을 얻을 것이다. 
MPMediaItem는 곡의 타이틀, 작사, 작곡 등.. 수많은 프로퍼티를 가지고 있지만, MPMediaItemPropertyAssetURL 프로퍼티에 초점을 둘것이다. AVAssetReader 와 AVAssetReaderTrackOutput 을 사용해서 데이터 파일의 위치를 알아낸 다음 저것(MPMediaItemPropertyAssetURL)을 사용하여 AVURLAsset 를 만들어낸다. 


NSURL *url = [myMediaItem valueForProperty:MPMediaItemPropertyAssetURL];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:asset error:nil];
AVAssetReaderTrackOutput *assetOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:asset.tracks[0] outputSettings:nil];
[self.assetReader addOutput:self.assetOutput];
[self.assetReader startReading];
이제, 미디어 아이템으로부터 AVURLAsset를 뽑아냈다. 우리는 이걸 사용해서 AVAssetReader 와 AVAssetReaderTrackOutput를 만들것이다. 마지막으로 우리는 읽'어주는 놈(reader)'한테 output을 던저주고 읽게 할 것이다. startReading 이라는 메소드는 읽어주는 놈(reader)을 열고 데이터를 요청했을때를 위해 준비하는 일밖에 하지 않는다.

다음으로 NSOutputStream 을 열고 해당 델리게이트 메서드는 NSStreamEventHasSpaceAvailable 이벤트가 호출 될때까지 읽어주는 놈(reader)의 데이터를 보냅니다.


CMSampleBufferRef sampleBuffer = [assetOutput copyNextSampleBuffer];

CMBlockBufferRef blockBuffer;
AudioBufferList audioBufferList;

CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(AudioBufferList), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer);

for (NSUInteger i = 0; i < audioBufferList.mNumberBuffers; i++) {
    AudioBuffer audioBuffer = audioBufferList.mBuffers[i];  
    [audioStream writeData:audioBuffer.mData maxLength:audioBuffer.mDataByteSize];
}

CFRelease(blockBuffer);
CFRelease(sampleBuffer);
먼저, 리더 아웃풋에서 나온 셈플 버퍼를 가져온다. 그리고나서 오디오 버퍼의 리스트를 얻기 위해 CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer 함수를 호출한다. 마지막으로 아웃풋 스트림의 각 오디오 버퍼를 쓴다(write).

이것이 우리는 처음에 하고자 했던 아이튠즈 라이브러리에 있는 음악을 스트리밍 한 것이다.(재생한건 아님) 이제 이 스트림 데이터를 어떻게 받아내는지 그리고 오디오를 어떻게 재생하는지 보자.


데이터 스트림
Multipeer Connectivity를 사용할 때 부터 NSInputStream 는 이미 만들어져 있었다. 먼저 우리는 데이터를 받기 위해 스트림을 해야한다.
// Start receiving data
// Start receiving data
inputStream.delegate = self;
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open]; 
이 클래스는  NSStreamDelegate 를 씌울 것이고, 우리는 이제 NSInputStream 으로부터 이벤트를 받을 수 있다.
@interface MyCustomClass () <NSStreamDelegate[CDATA[]]>
//...
@end

@implementation MyCustomClass
//...

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    if (eventCode == NSStreamEventHasBytesAvailable) {
        // handle incoming data
    } elseif (eventCode == NSStreamEventEndEncountered) {
        // notify application that stream has ended
    } elseif (eventCode == NSStreamEventErrorOccurred) {
        // notify application that stream has encountered and error
    }
}

//...
@end

위의 코드를 보면 델리게이트 메소드를 이용해서 스트림으로부터 이벤트를 받는다. 스트림이 끝나거나 에러가 나왔을 때, 앱에 알려야한다. 그래서 다음에 어떤 행동을 취할건지 정해야한다. 이제 우리는 이 이벤트로부터 얻은 스트림이 처리하여 가지고 있는 데이터에만 초점을 두면 된다. 우리는 이 데이터를 가져다가 사용하고, 그다음 Audio File Stream Services에 보내주는 작업이 필요하다.


스트림 파서
스트림 파서는 인코딩된 오디오 데이터를 넣고 디코딩된 오디오 데이터를 얻어오는 AudioFileStream 클래스이다. 먼저 AudioFileStream을 만들어보자.
AudioFileStreamID audioFileStreamID;
AudioFileStreamOpen((__bridge void *)self, AudioFileStreamPropertyListener, AudioFileStreamPacketsListener, 0, &audioFileStreamID);
우리는 클래스에 참조된 파서를 보내고, 프로퍼티는 콜백함수에 의해 바뀌고, 콜백함수에 의해 패킷들을 받는다. 우리는 이러한 기능을 하고 그 클래스에 참조되어 사용될 수 있는 콜백함수가 이 클래스 안에 필요하다.
void AudioFileStreamPropertyListener(void *inClientData, AudioFileStreamID inAudioFileStreamID, AudioFileStreamPropertyID inPropertyID, UInt32 *ioFlags)
{
    MyCustomClass *myClass = (__bridge MyCustomClass *)inClientData;
    [myClass didChangeProperty:inPropertyID flags:ioFlags];
}

void AudioFileStreamPacketsListener(void *inClientData, UInt32 inNumberBytes, UInt32 inNumberPackets, constvoid *inInputData, AudioStreamPacketDescription *inPacketDescriptions)
{
    MyCustomClass *myClass = (__bridge MyCustomClass *)inClientData;
    [myClass didReceivePackets:inInputData packetDescriptions:inPacketDescriptions numberOfPackets:inNumberPackets numberOfBytes:inNumberBytes];
}

didChangeProperty:flags: 메소드 안에서 다른 모든 프로퍼티가 준비됫다고 말해주는 kAudioFileStreamProperty_ReadyToProducePackets 프로퍼티를 찾고 있다. 이제 파서로부터 AudioStreamBasicDescription 를 가져올 수 있다. AudioStreamBasicDescription 는 오디오의 샘플비율(sample rate), 채널, 패킷당 바이트수 등등의 정보를 담겨있고 이것은 오디오 큐(audio queue)를 만드는데 꼭 필요한 요소이다.
AudioStreamBasicDescription basicDescription;
UInt32 basicDescriptionSize = sizeof(basicDescription);
AudioFileStreamGetProperty(audioFileStreamID, kAudioFileStreamProperty_DataFormat, &basicDescriptionSize, &basicDescription);
콜백으로 부터 받은 패킷에 사용될 다른 함수들은 나중에 오디오 큐 버퍼에 쌓일 디코딩된 오디오 데이터를 반환해줄것이다.

이제 스트림의 NSStreamEventHasBytesAvailable 이벤트로부터 인코딩된 데이터를 파서에 넣을 차례이다.
uint8_t bytes[512];
UInt32 length = [audioStream readData:bytes maxLength:512];
AudioFileStreamParseBytes(audioFileStreamID, length, data, 0);
파일 스트림은 파일의 타입을 알기게 충분한 바이트를 가질 때 까지 파싱을 할 것이다. At this point, it invokes its property changed callback with the property kAudioFileStreamProperty_ReadyToProducePackets. 그다음 이것은 우리가 사용하기 좋게 잘 포장된 디코딩된 데이터와 함께 해당 패킷이 받을 콜백을 호출한다.


오디오 큐
오디오 큐는 우리에게 오디오 버퍼를 생성하고, 채우고, 큐에 더할 수 있는것을 허락해주는 AudioQueue 클래스이다. 이것은 또한 재생, 일시정지, 멈춤등과 같은 오디오 컨트롤을 제공한다. 이제 큐와 버퍼를 생성해보자.
AudioQueueRef audioQueue;
AudioQueueNewOutput(&basicDescription, AudioQueueOutputCallback, (__bridge void *)self, NULL, NULL, 0, &audioQueue);

AudioQueueBufferRef audioQueueBuffer;
AudioQueueAllocateBuffer(audioQueue, 2048, &audioQueueBuffer);


오디오 큐를 만들기 위해서는 파서로부터 받은 theAudioStreamBasicDescription를 AudioQueueNewOutput 함수에 전달해 줘야하고, 시스템으로부터 호출된 콜백함수가 버퍼와 클래스 참조를 끝낸다. 다음으로 AudioQueueAllocateBuffer 함수를 호출하여 오디오 큐에 넘겨줄 수 있는 오디오 버퍼 한개를 만들고 잠시 멈추기 위한 버퍼의 사이즈도 함께 넘겨준다.


이제 파서가 패킷을 담은 콜백함수를 호출할 때까지 기다린다. 그리고 빈 버퍼를 패킷으로 채운다. 파서로부터 받을 수 있는 포멧은 VBR과 CBR 두가지 종류가 있는데, Variable Bitrate (VBR)는 비트율이 패킷이 어디있는지 따라 변할 수 있다는 것이고 Constant Bitrate (CBR)은 변하지 않는다(constant)는 것이다.

VBR의 경우,  많은 바이트를 가진 전체 패킷만을 버퍼에 채울 스 있다. 이것은 시스템에서 패킷을 보내주기 전까지는 버퍼가 차지 않는다는걸 의미한다. CBR의 경우, 패킷이 전송되는 도중에 버퍼를 가득 채울 수 있다.


CBR
AudioQueueBufferRef audioQueueBuffer = [self aFreeBuffer];
memcpy((char *)audioQueueBuffer->mAudioData, (constchar *)data,
또한 우리는 버퍼가 오버플로우가 되지 못하게 하거나 이것이 가득 차지 않았을 경우 기다리는 로직이 필요하다.

VBR
AudioQueueBufferRef audioQueueBuffer = [self aFreeBuffer];
memcpy((char *)audioQueueBuffer->mAudioData, (constchar *)(data + packetDescription.mStartOffset), packetDescription.mDataByteSize);

패킷이 버퍼에 넘처 남게되는 것을 체크하는 코드가 있다. 만약 다른 패킷의 mDataByteSize에 맞지 않는다면 우리는 다른 버퍼를 가져와야한다. 또한 패킷 디스크립션(packet descriptions)이 큐 되는동안 기다려야 한다.
바퍼가 차면, AudioQueueEnqueueBuffer 와 함께 시스템에 큐를 날린다.

AudioQueueEnqueueBuffer(audioQueue, audioQueueBuffer, numberOfPacketDescriptions, packetDescriptions);

이제 오디오를 재생할 준비가 끝났다. 모든 버퍼가 채워지고 큐 되면 AudioQueuePrime과 AudioQueueStart
를 사용하여 소리를 재생할 수 있다.
AudioQueueBufferRef audioQueueBuffer = [self aFreeBuffer];
memcpy((char *)audioQueueBuffer->mAudioData, (constchar *)(data + packetDescription.mStartOffset), packetDescription.mDataByteSize);

AudioQueueStart는 두번째 파라메터에 언제 재생될지에대한 시간을 나타내는 값을 NULL대신에 넣을 수 있다. 지금은 별로 중요하지 않으니 넘어가지만, 나중에 오디오 동기화(audio synchronization)을 하는데 꼭 필요한 것이니 기억해두면 좋다.

끝으로
이 글은 Multipeer Connectivity를 이용한 오디오 스트리밍에 대한 기초적인 글이다. 글을 마치면서 나는 조금더 복잡하고 잘 정리된 오픈소스 라이브러리를 민들었다. 좀더 자세한 내용을 알고싶으면, Github에 올라가있는 tonyd256/TDAudioStreamer 다듬어진 코드를 볼 수 있다.



 Tony DiPasquale  Developer

translate by canapio



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,