Slug wendy lu header

Pinterest의 데이터 레이어 재설계에서 배우는 데이터 일관성 유지하기

최근 핀터레스트는 보다 빠르고 신선한 경험을 선사하기 위해 iOS 앱을 재설계했습니다. 재설계의 가장 큰 목표 중의 하나는 변경 불가능한 모델 계층(immutable model layer)을 구축하는 것이었습니다. 요즘 많은 사람들이 “변경 불가능한 모델(Immutable models)” 에 대해 이야기하고 하고 있으며, 많은 앱들이 변경 불가능한 모델을 갖는 구조로 설계를 변경하고 있습니다. 변경 불가능 구조로 설계할 경우, 복잡한 객체 그래프의 데이터 지속성을 어떻게 다룰 것인지에 대해 고민을 해야 합니다. Swift Language User Group 강연에서 Wendy Lu 는 핀터레스트에서 iOS 앱 마이그레이션을 추진하게 된 배경과 새로운 시스템이 API로부터 새로운 정보를 로딩하는 방법, 모델을 업데이트하는 방법 등을 소개하고, 데이터 무결성에 대해서도 설명하고 있습니다.


소개 (0:00)

제 이름은 웬디입니다. 저는 오늘 데이터, 그중에서도 하루하루 빠르게 변하고 있는 세상 속에서 데이터 지속성을 유지하는 방법에 대해 이야기하고자 합니다.

제 소개를 간단히 하자면 핀터레스트에서 iOS 개발자로 근무하고 있고요, 이번이 Realm에서의 첫 번째 강연입니다. 스위스 서밋에서도 이 주제와 관련해서 강연했기 때문에 스위스 서밋에 참여한 분이라면 이 주제가 익숙할 것으로 생각합니다.

먼저 이 자리에 초대해 주신 Realm에 감사드립니다. 강연자이자 참석자로 이 강연에 참여하여 뛰어난 iOS 개발자들을 만날 수 있어서 정말 기쁘게 생각합니다. 수많은 모임과 컨퍼런스에 참여하고 있지만, Realm 모임은 제가 가장 좋아하는 모임 중의 하나랍니다.

이모지콘 (0:49)

얼마전에 샌프란시스코에서 열렸던 이모지콘이라는 컨퍼런스에 참석했었습니다. 여러분이 이 컨퍼런스에 대해 들어보지 못했다면, 실망하지 마세요. 저도 못 들어봤었거든요. 이모지콘은 며칠 동안 이모지에 관한 모든 것에 대해 찬양하는 행사입니다.

이모지콘에 대해 처음 들었을 때는 무슨 행사인지 전혀 감이 오지 않았지만 멋진 행사라는 생각이 들었습니다. 그래서 저는 이모지콘 행사에 관한 정보를 알아보았습니다. 이모지콘은 사실 기술 컨퍼런스입니다. 이 행사에서는 “이모지, 딥 러닝”과 같은 주제를 다룹니다. 이 주제와 관련하여 AI 배경지식이 충분하다고 생각하지는 않았지만, 온라인 비디오를 보고 난 뒤 이모지콘에 참석하기로 했습니다.

이모지콘은 제너럴 일렉트릭, 판다 익스프레스 등 다양한 기업 스폰서를 받아 진행되는 행사입니다. 판다 익스프레스를 좋아하기 때문에 이 행사가 저와 잘 맞을 거라는 생각이 들었습니다.

새로운 이모지 (1:53)

이모지콘에서 가장 뜨거웠던 주제 중 하나는 iOS 10.2 에서 도입된 새로운 이모지에 관한 것이었습니다. 많은 분들이 10.2 에서 복숭아 이모지가 변경된 것을 안타까워했고 다행히도 iOS 10.3 에서는 원래의 모습으로 돌아갈 예정입니다. 가장 기본적인 데이터 셋인 유니코드에서도 지속적으로 변화가 발생하고 있으며 업데이트가 진행되고 있습니다.

오늘 강연에서는 이러한 변화를 관리하기 위한 전략과 지속성을 유지하는 방법에 관해 살펴보도록 하겠습니다.

핀터레스트의 성장 (2:33)

4년간 핀터레스트에서 일하는 동안, 4명으로 시작했던 iOS 개발팀은 현재 36명 이상으로 늘어났습니다. 저는 이러한 변화가 멋진 경험이라고 생각하며, 저희 팀의 성장을 지켜보는 것이 즐겁습니다.

성장과 함께, 앱이 미래 상황을 대비하기 위해서는 많은 일들을 해야 하며 앱 사용자와 팀 모두가 함께 성장할 수 있는 구조가 되어야 합니다.

네트워크 스피드 (2:55)

예를 들어, 저희 시스템을 국제적으로 확장하기 시작했을 때, 우리는 모든 사용자가 항상 최고의 폰, 최고의 네트워크 환경을 보유하고 있지 않다는 사실을 배울 수 있었습니다. 속도는 저희에게 정말 중요한 문제가 되었습니다.

여러분이 지하철이나 BART 열차에서 폰을 사용한 적이 있다면, 느린 네트워크 환경에서 앱을 이용하는 것이 얼마나 짜증 나는 일인지 알고 계실 것으로 생각합니다.

특정 지역 사용자들의 경우, 항상 열악한 네트워크 환경에서 서비스를 이용하고 있습니다.

어떻게 하면 사용자가 거주하는 국가, 기기 종류, 대역폭에 상관없이 우리 앱을 사용할 때 좋을 경험을 하게 할 수 있을까요?

네트워크 최적화 (3:41)

저희는 속도를 향상할 수 있는 모든 방안을 검토하였습니다. 네트워크 사용량이 최우선 검토 대상이었습니다. API 요청과 응답에 사용하는 데이터양을 최적화하는 것이지요.

앱을 시작하는데 걸리는 시간과 여러 작업을 동시에 수행하는 문제(concurrency 이슈)에 대해서도 살펴보았습니다. 앱을 시작하면 app delegate에서 application(_:didFinishLaunchingWithOptions:) 메서드가 호출됩니다. 개발자들은 자신들의 코드를 실행하기 위한 첫 번째 장소로 이 메서드를 많이 이용하고 있습니다.

여러분의 개발팀 규모가 상당히 크고, 출시 이후 지속적인 업데이트로 관리되고 있는 앱이라면 이 메서드는 아마 아래와 같은 모습일 것입니다.

이런 개발 뉴스를 더 만나보세요


AVPlayer.resetAVAudiOSessionCategoryToDefault()

FBSDKSettings.configureForUseForApplication(application,
withLaunchOptions:launchOptions)

GSDAppIndexing.sharedInstance().registerApp(kAppStoreID)

iRate.configureForUse()

Adjust.appDidLaunch(adjustConfig)

Stripe.configureForUse()

DDLog.addLogger(DDTTYLogger.sharedInstance())
let fileLogger = DDFileLogger()
fileLogger.logFileManager.maximumNumberOfLogFiles = 3
DDLog.addLogger(fileLogger)

PIDeadlockDetector.enable()

PICrash.sharedInstance().configureForUse()

PINRemoteImageManager.configureForUse()

CBLExperienceManager.configureForUse()

NSValueTransformer.setValueTransformer(PIDateValueTransformer(), forName:kPINModelDateValueTransformerKey)

CBLDeepLinkManager.sharedManager().configureServicesWithLaunchOptions(launchOptions)

위의 예제는 저희 팀 코드에서 가져온 것인데요, 이 메서드 안에서 몇 개의 초기화하는 작업을 수행하고 있는 것을 보실 수 있습니다. Stripe 와 Adjust 같은 외부 라이브러리도 있고, 디버그, 크래쉬 보고, manager 클래스, 그리고 cache 와 관련된 설정도 보이는군요.

이러한 작업들은 앱 시작속도를 떨어뜨리는 원인이 됩니다. 이 메서드에 수행되는 대부분의 작업들을 낮은 우선 순위를 갖는 백그라운드 큐로 보낸다면, 시작시점에 좀 더 중요한 작업들에 앱의 리소스를 할당할 수 있습니다. 예를 들면, 초기 피드 요청 보내기, 파싱된 응답 처리, 사용자에게 유용한 정보 안내 등을 시작 시점에 수행할 수 있습니다.

앱 시작시간 감소 (5:14)

현재 저희 앱은 앱 시작 시점에 초기 피드 요청에 대한 요청과 응답만을 초기화합니다.

다른 작업들은 우선순위가 낮은 concurrent 큐로 보내서 처리하였습니다. 이러한 구조 변화로 인해 앱 시작시간은 50% 감소했습니다. 특정 국가에서는 앱 시작시간을 1/3 수준으로 낮출 수 있었습니다.

또한 UI 작업들에서도 개선할 점이 없는지 살펴보았고 그 결과, 뷰와 레이아웃 구성을 메인 스레드가 아닌 곳에서도 할 수 있는 Async Display Kit 프레임워크를 도입하기로 했습니다.

이러한 변화를 통해 최근에 출시된 iOS 기기들에 탑재된 멀티 코어를 활용할 수 있게 되었다는 점도 매우 기쁘게 생각하고 있습니다. Async Display Kit 프레임워크 도입으로 인해 레이아웃 코드에서 수행되는 계산 식들을 백그라운드 큐로 보낼 수 있게 되었습니다.

변경 불가 모델 계층 (6:07)

앱에서 동시성과 멀티 스레드 기능에 대한 요구가 증가할수록, 앱의 기본이 되는 모델 계층을 잘 구성해야 합니다. 저희는 변경 불가 모델 계층(immutable model layer)으로 완전히 전환했습니다. 모델이 한번 생성되고 나면 이후에는 변경되지 않습니다. 얼마나 멋진 일인가요!

기존 방식인 변경 가능한 시스템(mutable system)에서는, 같은 Pin을 참조하고 있는 두 개의 뷰 컨트롤러가 있으면, 첫 번째 뷰 컨트롤러에서 Pin을 변경했을 때, 두 번째 뷰 컨트롤러에서는 Pin이 변경되었다는 사실을 전혀 모르는 상황이 발생할 수도 있습니다.

더욱이 두 번째 뷰 컨트롤러가 Pin 을 읽는 동안 첫 번째 뷰 컨트롤러가 Pin에 쓰는 동작을 수행한다면, 두 번째 뷰 컨트롤러는 정확하지 않은 값을 읽어들일 가능성이 존재하며 이는 앱 크래쉬의 원인이 될 수도 있습니다.

예제 (6:53)

테일러 스위프트, 카니예 웨스트와 함께 채팅하고 있다고 가정해봅시다. 테일러는 저의 스위프트 언어 사용자 그룹 토크 행사를 무척 좋아하기 때문에 행사에 참여하고 싶다고 말할 것입니다. 하지만 카니예가 갑자기 끼어들어서 고양이 사진을 저희에게 보냈습니다.

테일러는 “카니예, 뭐라니?” 라고 답글을 달았지만 카니예는 계속 고양이 사진 스팸만 보내고 있습니다. 이런 상황이라면 저는 카니예를 채팅창에서 차단할 것입니다.

카니예를 차단하기 위해 blockUser(users[1]) 메서드를 호출할 건데요, 이 메서드는 users 배열에서 첫 번째 인자값에 해당하는 사용자를 차단합니다. 하지만, 메서드가 실행되기 전에 다른 스레드가 users 배열에 접근하여 배열에 저장된 값을 변경하면 어떻게 될까요?

서버로부터 받은 응답이 갱신되어 배열이 변경되거나 다른 스레드에서 이 배열을 변경하는 상황이 발생할 수 있습니다. 이로 인해, blockUser 이 호출되었을 때, 배열에서 첫 번째 인자값을 조회하면 카니예 대신 테일러가 리턴되는 상황이 발생할 수도 있습니다. 그 결과, 다른 사람이 차단되겠지요.

잘못된 값을 읽는 것은 제가 피하고 싶은 상황들을 발생시키는 원인이 됩니다.

변경 불가능 상태는 스레드 세이프입니다. (8:06)

변경 가능한 상태(mutability)와 관련하여 발생하는 대부분 문제는 상태 공유(shared state)로 인해 발생합니다. 변경 불가능 시스템에서는 한번 Pin이 초기화되면, 이후에 이것이 변하지 않을 것이라는 사실을 보장합니다. 이러한 방법을 통해 여러 명이 동시에 Pin의 정보를 안전하게 읽을 수 있습니다. 잘못된 상태나 완전하지 않은 값을 읽어드릴 가능성이 사라지게 되는 것이지요.

변경 불가능 모델은 태생적으로 스레드 세이프합니다. 이것이 저희가 변경 불가능 모델 시스템으로 전환하게 된 이유입니다.

모델 갱신하기 (8:36)

모델을 바꿀 수 없게 되었는데, 어떻게 모델을 갱신하거나 변경할 수 있을까요?

변경 불가능 시스템에서는 모델들은 한번 생성되고 나면 변경할 수 없으며, 모델을 갱신할 수 있는 유일한 방법은 모델 객체의 새로운 인스턴스를 생성하는 방법밖에 없습니다.

저희 코드에서는 모델 갱신을 위해 두 가지 방식을 사용하고 있습니다.

JSON을 이용한 모델 갱신 (9:02)

첫 번째 방식은 JSON dictionary를 기반으로 갱신된 모델을 생성하는 것입니다. 반드시 JSON dictionary 일 필요는 없고요. JSON dictionary가 가장 많이 사용되고 있으므로 이것을 활용했을 뿐입니다.


{
"board" =  {
"created_at" = "Tue, 13 Aug 2013 16:38:36 +0000";
"id" = 418131215342691718;
"name" = "spaces";
};
"comment_count" =  0;
"description" = "At the top of my wish list for this fall is a giant chunky knit wool blanket.";
"id" = "AVpd31ttshLHlWbcG9g_Kt3uVzZHjfHNvzwT20p6YnO6qzvQnqs_Z5A";
"Image_square_url" = "https://s-media-cache-ak0.pinimg.com/b58cc94084407a39d62c83885ce4699e.jpg";
}

let Pin = Pin(dictionary:pinJSON)

간단한 코드입니다. 서버로부터 Pin에 해당하는 JSON 응답을 가져오는 코드입니다. Pin은 핀터레스트에서 가장 기본이 되는 모델이며 포스트와 유사하다고 생각하시면 됩니다. 위의 예제는 dictionary를 이용해 Pin 모델을 초기화하는 작업을 수행하고 있는 모습을 보여주고 있습니다.

builder 객체를 이용한 모델 갱신 (9:28)

모델을 업데이트하는 또 다른 방법은 builder 객체를 이용하는 것입니다. builder 객체는 모델의 필드에 저장된 것을 모두 가지고 있는 모델을 변경 가능한 모델로 표현하는 데 사용되는 일반적인 방식입니다.

이미지 URL, 타이틀, board 이렇게 3개의 속성(property)을 가지고 있는 Pin이 있다고 가정해봅시다. 물론 변하지 않는(immutable) Pin입니다. Pin builder는 이미지 URL, 타이틀, board에 저장된 값과 동일한 값을 갖게 됩니다.

Pin builder는 변경 가능하기 때문에 어떤 것이든 요구조건에 맞게 수정할 수 있습니다. 예를 들어, “세계 최고의 Pin” 라고 저장된 타이틀을 “Meow” 로 변경하고 새로운 builder 객체를 이용해 새로운 Pin을 초기화할 수 있습니다.


let pin = Pin(builder:pinBuilder)

로딩과 캐싱 (10:18)

이제, 서버에서 수신한 데이터에 대한 로딩과 캐싱에 대해 살펴볼까요? “컴퓨터 공학에서 제일 어려운 문제 두 가지는 캐시 무효화(cache invalidation), 이름 정하기, 그리고 off-by-one 에러이다.”라는 말을 들어보신 적이 있을 겁니다.

이미 off-by-one 에러에 관해서는 이야기 했으니, 캐싱에 대해서 이야기해보죠.

저희 API는 JSON 모델 중 일부분만을 서버에 요청할 수 있으며, 이는 모델의 전체 필드 중 일부 영역에 해당합니다. 예를 들어, Pin 피드 뷰의 경우, 이미지 URL, 설명, 사용자에 대한 필드 정보만 필요할 뿐 Pin에 대한 모든 정보가 필요하지는 않습니다. 사용자가 상세 화면을 보기 위해 화면을 탭 하기 전까지는요.

JSON 모델 중 일부분만 요청하는 것이 가능해짐에 따라 API를 통해 주고받는 데이터의 양이 줄어들었고, 서버로부터 수신한 데이터 중 불필요한 필드를 조회하는 데 소모되었던 시간들 역시 감소했습니다.

PINCache (11:22)

저희의 새로운 변경 불가능 시스템에서는 이 개념을 PINCache 에 기반을 둔 모델 캐시에 적용했습니다. PINCache 는 객체 캐시 집합이라고 이해하시면 되고요, 핀터레스트에서 개발했고, 오픈 소스로 관리하고 있습니다. 이 캐시들은 모델의 ID 값으로 구분합니다.

서버로부터 새로운 응답을 받으면 먼저, 캐시가 존재하는지 확인하기 위해 동일한 ID 를 가지고 있는 모델이 있는지 살펴봅니다. 만약에 기존 모델이 있다면, 새로운 응답을 기존 캐시 모델과 병합(merge)합니다.

여기 예제에서는, 서버로부터 받은 응답과 기존의 캐시 모델이 123이라는 동일한 ID 를 가지고 있으므로 ID 123 을 가지고 있는 모델에 대한 병합작업을 수행할 것입니다.

PinJSON 과 캐시 모두 image_url 을 가지고 있지만 image_url 필드에 저장된 값이 서로 다르므로, 두 개 중에서 서버로부터 최근에 응답받은 데이터를 사용합니다.

캐시 모델에만 board 필드가 있으므로 병합된 모델은 board에 대한 값을 갖게 될 것입니다. 또한, 서버에서 응답받은 데이터에만 recipe 필드가 있으므로 병합된 모델은 recipe에 대한 값을 갖게 될 것입니다.

마지막 단계에서는 병합된 모델을 캐시에 삽입하는 절차를 수행합니다. 이런 절차를 거치게 되면 캐시는 항상 최신 정보를 유지할 수 있게 됩니다.

초기화 이후의 병합 (12:55)

ditionary 또는 builder를 이용한 모델 초기화를 마치고 나면 항상 앞에서 언급한 병합 절차를 수행하고 있습니다.

사실 저희가 모델 초기화를 위해 사용하는 방식이 하나 더 있는데 그것은 바로 initWithCoder를 이용하는 것입니다. 하지만 이것은 NS-secure 코딩 호환이 필요한 경우에만 사용하고 그 외에는 dictionary 또는 builder 를 사용합니다.

초기화를 마치면 항상 병합 작업을 수행하기 때문에 이 캐시를 데이터 모델의 기준으로 삼고 있으며, 이 캐시가 항상 최신 필드 정보를 가지고 있다는 가정하에 관련 작업을 수행합니다.

변경 사항 관찰(observe) (13:28)

API로부터 데이터를 로드하는 방법과 데이터를 업데이트하는 방법을 익혔으니, 관심 있는 모델에서 변경사항이 발생했을 때 앱의 뷰 또는 뷰 컨트롤러에게 알려주는 방법에 대해 알아보도록 하겠습니다. 뷰 또는 뷰 컨트롤러는 데이터를 시각적으로 나타내고, 그들과 연관된 모델이 변경되면 화면을 업데이트해야 합니다.

저희는 그동안 모델 변경 사항을 알리려는 방법으로 Key-Value Observing을 사용해왔습니다. 하지만, KVO는 변경 불가능 시스템에서는 사용할 수 없습니다. 왜냐하면 변경 불가능 시스템에서는 변하지 않는 모델 객체에 대한 1개의 인스턴스만 관찰할 수 있으며, 이 모델은 변하지 않기 때문입니다.

NSNotification (14:05)

모델 변경 사항을 알리기 위해 저희가 선택한 것은 NSNotification 기반의 시스템입니다.

프로필 뷰 컨트롤러를 예로 들어보겠습니다. 여기서는 유저 객체를 관찰할 필요가 있습니다. 왜냐하면 사용자 객체가 갱신되었을 때 Pin count, 또는 follower count와 같은 UI의 업데이트 작업이 함께 필요한 경우가 있기 때문입니다.

저희 개발자들은 NotificationManager 라고 불리는 헬퍼 클래스를 통해 이러한 작업을 처리하고 있습니다. NotificationManager 에서는 모델 갱신 여부를 관찰하기 위해 addObserverForUpdatedModel 메서드를 제공하며, 이 메서드는 NotificationCenteraddObserver 메서드를 호출하는 역할을 담당합니다.


notificationManager.addObserverForUpdatedModel(user, block:{ (NSNotification) in 
// 여기서 프로파일 뷰를 갱신합니다!
})

내부구조를 살펴보자면, 이것은 NotificationCenter 블럭 기반의 API를 사용하고 있습니다. 여러분이 블럭 기반의 API에 대해서 들어보신 적이 없다면 표준 selector 기반 API와 유사하다고 생각하시면 되겠습니다. selector 대신에 블럭 안에 전달한다는 점만 제외하구요. 이 블럭은 notification이 발생하면 실행될 것입니다.

블럭 기반의 API를 선택하게 된 이유와 관련해서는 상당히 긴 스토리가 있습니다. 기본적으로 iOS 9 이전의 NotificationCenter 에서는 레이스 컨디션 관련 이슈가 있었습니다. 이러한 현상은 일반적으로 멀티 쓰레드 환경에서 NotificationCenter 를 사용할 때 발생합니다. 다만, 개발 경험상 블럭 기반 API를 사용하게 되면 레이스 컨디션 관련 이슈가 거의 발생하지 않았습니다.

블럭 기반의 API (15:18)

NotificationCenter 의 add​Observer(for​Name:​object:​queue:​using:​) 메서드 선언을 살펴보면, NSOjectProtocol 을 준수하는 객체를 반환하고 있음을 알 수 있습니다.


NotificationCenter.default.addObserver(forName: "name", object: nil, queue: nil) { note in // …
}

public func addObserver(forName name: NSNotification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NSObjectProtocol

instruments 도구를 이용해 좀 더 자세히 살펴보면 이 객체는 사실 __NSObserver 라는 프라이빗 클래스라는 사실을 알 수 있습니다. 이러한 NSObserver 객체들은 실제로는 NotificationCenter 가 사용하는 등록된(registered) 리스너들입니다.

옵져버 등록 해제하기 (15:43)

일반적인 API에서 옵져버 등록을 해제할 때 NotificationCenter 의 removeObserver 메서드에 self 를 전달하는 방식을 사용합니다.


NotificationCenter.default.removeObserver(self)

블럭 기반의 API의 경우 add​Observer(for​Name:​object:​queue:​using:​) 메서드에서 리턴된 observer 객체를 removeObserver에 대한 인자로 전달합니다.


NotificationCenter.default.removeObserver(observer)

NotificationManager 헬퍼 클래스 (15:57)

나중에 notification에 대한 등록을 해제하기 위해, addObserverForUpdatedModel(_:block:) 메서드에서 observer 객체들을 추적할 필요가 있습니다. 이것은 NotificationManager 헬퍼 클래스를 통해 쉽게 구현할 수 있습니다. NotificationManager 헬퍼 클래스의 addObserverForUpdatedModel(_:block:) 메서드에서는 모든 observer 들에 대한 강한 참조를 유지하고 있습니다.


Class NotificationManager: NSObject {
    private var observerTokens: [String: AnyObject] = [:]

    deinit {
        unregisterAll()
    }
    
    func unregisterAll() {
        for token in observerTokens.values {
            NotificationCenter.default.removeObserver(token)
        }
    }

    func addObserverForUpdatedModel(_ object: Any?, block: (NSNotification -> Void)) {
        let uniqueName = "uniqueName" // class name + unique server specified ID
        let newToken = NotificationCenter.default.addObserver(forName: "uniqueName", object: object, queue: nil, using: block)

        observerTokens[uniqueName] = newToken
    }
}

NotificationManager 헬퍼 클래스에는 observerTokens 라는 dictionary 가 있는데 우리가 등록한 notification 이름과 addObserverForUpdatedModel(_:block:) 메서드 내부에서 리턴된 옵저버 객체를 연결해주는 역할을 담당합니다.

옵져빙을 수행할 뷰 또는 뷰 컨트롤러의 속성(property)으로 NotificationManager 객체를 사용할 것이기 때문에, 뷰 컨트롤러의 deinit 이 호출되고 나면 NotificationManager 클래스의 deinit 이 호출되어야 할 것입니다.

NotificationManager 객체에 대한 참조를 뷰 컨트롤러가 유일하게 가지고 있으므로 뷰 컨트롤러가 deinit 되고 나면 이 객체 역시 할당이 해제(deallocated)될 것입니다. NotificationManager 의 deinit 메서드에서는 observerTokens dictionary 에 저장된 모든 옵저버들에 대해 등록 해제(unregist)를 수행합니다.

이런 방식은 개발자들이 옵져버들을 하나씩 등록 해제하지 않아도 되기 때문에 매우 유용합니다. 또한, 개발팀 규모가 늘어났을 때 어떤 개발자가 등록 해제를 깜빡 잊었더라도 앱이 크래쉬되지 않게 해주는 장점도 있습니다.

iOS 9 이후에는 수동으로 등록 해제를 해주지 않아도 되지만, 저희 팀은 iOS 7 버전까지 지원하고 있으므로 옵저버에 대한 등록 해제 작업을 항상 해오고 있습니다.

저는 이러한 아이디어를 More Indirection 블로그에서 얻었습니다. 추가적인 정보를 원하신다면 해당 블로그 포스트를 참조하시기 바랍니다.

복잡하지 않습니다. (17:40)

저희 옵저버 시스템이 어떻게 동작하는지 살펴보았습니다. 대부분 개발자들은 옵저버 시스템의 세부적인 사항에 대해 크게 신경 쓰지 않아도 됩니다. 여러분이 관심 있게 봐야 하는 메서드는 notificationManager.addObserverForUpdatedModel 입니다.


notificationManager.addObserverForUpdatedModel(user, block:{ (NSNotification) in
// Update profile view here!
})

새로운 모델 Notification (17:57)

새로운 모델이 생성되거나 갱신되면 notificationManager.postModelUpdatedNotificationWithObject 메서드를 호출하여 notification을 포스트 할 수 있습니다.


let newUser = PIUser(builder:builder)
notificationManager.postModelUpdatedNotificationWithObject(newUser)

이 메서드는 우선, 동일한 ID의 객체가 캐시에 있는지 확인할 것입니다. 이 캐시 객체에는 서버에서 수신한 가장 최신 정보가 저장되어 있을 것입니다. 만약 옵저버가 있다면 해당 옵저버로 이것을 포스트 할 것입니다.

UI 업데이트하기(18:28)

갱신된 객체를 이용해 UI 업데이트를 할 수 있습니다.

새로운 모델 객체는 NSNotification의 object 필드에 전달되기 때문에, object 필드로부터 새로운 모델 객체를 추출하여 관련 UI를 업데이트할 수 있습니다.


notificationManager.addObserverForUpdatedModel(user, block: { [weak self] 
notification in 
    if let user = notification.object as? PIUser {
        self?.user = user 

        // Update profile view here!
        self?.titleLabel.text = user.name 
        self?.imageView.setImageWithURL(user.imageURL)
    }
})

위의 예제 코드에서는 title label text 와 imageView URL 을 업데이트했습니다. UI 요소에 설정하기 전에 이 값들이 실제로 변경되었는지 확인하는 로직을 추가할 수도 있습니다.

강연을 마치며.. (18:59)

핀터레스트의 데이터 모델 계층이 어떻게 구성되어 있는지 살펴보았고, 데이터 지속성을 유지하는 방법에 대해서도 알아보았습니다. 변경 불가능(immutability)과 관련된 일반적인 내용에 관심이 있으신 분들 또는 앱을 변경 불가 모델로 전환하고 싶어 하시는 분들을 위해 참고할 만한 자료들을 알려드리겠습니다.

핀터레스트 엔지니어 블로그에 오늘 말씀드린 주제와 관련하여 좀 더 자세한 블로그 포스팅을 남겼습니다. 저희 iOS 앱 재설계와 관련된 연재 글과 변경 불가 모델과 관련된 몇 가지 글들이 블로그에 소개되어 있으니 참고하시기 바랍니다.

페이스북에서도 변경 불가능 모델로 전환하는 것과 관련된 멋진 글들을 보실 수 있으며, 글에서는 전환작업으로 인해 그들의 앱이 얼마나 빨라졌는지에 대해서도 소개하고 있습니다.

마지막으로 링크드인에서는 변경 불가 모델과 관련된 데이터 지속성 솔루션을 오픈 소스로 제공하고 있습니다. 오픈 소스 구현에 관심이 있는 분들이라면 꼭 참고하시길 바랍니다.

질의응답(19:56)

Q: 모델 병합과 관련된 질문입니다. 부분적인 핀 정보를 요청할 때 상당히 많은 옵셔널을 처리해야 할 것 같은데 어떻게 데이터를 처리하나요?

A: 현재 저희 코드 베이스는 Objective-C로 작성되어 있습니다. 부분적인 요청을 처리하기 위해 코드 작업을 진행하고 있습니다. 데이터를 가지고 있지 않은 많은 속성이 존재합니다. 저희 뷰와 뷰 콘트롤러들은 이러한 개념 위에서 구축되었으며 몇몇 필드는 값이 없기도 합니다.

Q: observation에 사용한 기술에 관한 질문입니다. 객체에 변화를 관찰하기 위해 서버의 ID 키를 사용하나요?

A: 예 맞습니다. 고유한 서버 ID를 사용합니다. notification 이름은 클래스 이름과 ID를 결합한 것입니다.

Q: 해당 ID는 백 앤드 엔지니어와 함께 사용하는 건가요? 이 ID는 재사용하지 말라든가, 리셋하지 말라든가 하는 약속이 있나요?

A: 네. 그렇습니다. 대부분의 백 앤드 모델은 그들이 저장된 고유한 ID를 가지고 있습니다.

Q: 메모리 캐시를 디스크에 저장하나요? 만약 그렇다면 어떻게 복구하나요? invalidation 처리는 어떻게 하죠?

A: 디스크에 저장할 수 있지만, 현재는 어떠한 것도 디스크에 저장하고 있지 않습니다. PINCache 를 이용하면 setObject(_:forKey:) 메서드를 통해 메모리에 캐시를 쉽게 구성할 수 있습니다. 여러분이 직접 그것이 메모리에 있는지 확인하고 디스크에 쓸 필요가 없습니다. PINCache 가 이 모든 것을 대신 해주니까요. 만약 메모리 크래쉬가 발생하거나 공간이 부족하거나 하면 디스크에 쓰겠지요. 저희는 이미 그렇게 할 수 있지만, 현재는 그렇게 하고 있지 않습니다. 영구 저장과 오프라인 사용과 관련해서는 지속적으로 관심을 가지고 있으니 지켜봐 주시길 바랍니다.

Q: 데이터 observation과 관련해 ReactiveCocoa, RxSwift 같은 솔루션 사용을 고려해 보셨나요?

A: ReactiveCocoa 와 RxSwift 사용을 고려했었지만, 36명 이상으로 구성된 저희 개발팀이 이들의 사용법을 익히려면 상당한 시간이 소요될 것으로 판단했습니다. 언젠가는 이들을 사용할 수도 있을 것으로 생각합니다.

Q: builder 와 관련된 질문입니다. 각각의 객체에 대해 이와 관련된 builder가 따로 있는 건가요? 아니면 CodeGen 을 사용한 건가요??

A: 저희는 CodeGen 을 이용했습니다. 저희 엔지니어 블로그에 이와 관련된 포스트가 있습니다. 저희가 사용하는 모든 모델 클래스는 자동으로 생성되며 JSON 스키마를 받아서 Objective-C 모델로 변환해주는 Swift 스크립트를 이용해 이러한 변환 작업을 수행합니다.

Q: 당신 팀의 코드 베이스가 Objective-C로 알고 있는데 예제는 Swift로 작성되었군요. 객체 자체는 let으로 선언하고 속성들은 variable 로 선언하는 방법도 있는데 이런 방법을 생각해 보셨나요? 이렇게 구성하면 builder 없이도 객체를 복사할 수 있다고 생각하는데요

A: 좋은 생각이네요. 저희 팀이 Swift로 코드를 옮길 때 당신에게 연락해야겠군요.

Q: 객체 생성과 관련해서 사용하고 있는 라이브러리가 있나요? 예를 들자면, JSON 스키마와 관련된 객체 같은 것을 만들 때 말이죠. 이러한 모델은 데이터 매핑에만 사용되는 건가요, UI 계층에서는 사용하지 않는 건가요? 아니면 앱 전체적으로 사용하고 있나요?

A: 저희가 직접 만든 라이브러리를 이용합니다. JSON 스키마와 관련된 Objective-C 타입을 매핑시키는 것은 상당히 쉬운 일입니다. 구현 파일에서는 initWithDictionary, initWithBuilder, initWithCoder 같은 것을 이용합니다. 모든 모델에서 비슷한 코드들이 필요하므로 이러한 작업을 자동화하기로 결정했습니다. 페이스북도 모델 파일을 자동으로 생성해주는 Remodel이라는 것을 개발한 것으로 알고 있습니다. 오픈 소스에서 구현이 어떻게 되어있는지 관심 있다면 한번 살펴보시는 것도 좋을 것 같네요.

Q: 이 모델을 앱 전체적으로 사용하나요? 네트워크 데이터 매핑에만 사용하는 것인가요?

A: 네, 이 모델들을 UI에 사용하고 있습니다. 예를 들어, 그리드에 있는 모든 Pin cell의 경우, cell을 구성할 수 있는 setPin이라는 메서드를 가지고 있습니다. 어떤 곳에서는 view model을 사용하고 있지만, 대부분의 경우 모델을 직접 사용하고 있습니다.

다음: Realm Obj-C와 Realm Swift의 새로운 기능을 소개합니다.

General link arrow white

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.

Wendy Lu

지난 4년간 Pinterest에서 일해온 iOS 엔지니어인 Wendy는 Pinterest의 모바일 상거래 제품의 출시를 주도했으며 데이터 레이어에서부터 광고 제품에 이르기까지 모든 것을 다뤘습니다. Apple Pay의 Swift Summit에서의 강연 경험과 Grace Hopper의 모바일 개발 패널 평가 경험이 있습니다.

4 design patterns for a RESTless mobile integration »

close