모바일 개발자에게 scalability란 뭘까

페북 포스팅에서 복사해옴.

모바일 개발자에게 scalability란 뭘까.. 란 궁금증을 오래전부터 지니고 있었습니다. 커리어를 라인에서 시작했는데, 글로벌 수천만의 유저를 감당하는 경험을 해보고 싶었거든요. 그러나 알고보니 수 천만 명의 트래픽은 서버 개발자가 감당할 일이었던거죠. 약간 실망했어요.

서버 개발자는 얼만큼의 트래픽을 감당해봤느냐가 기술력의 척도가 되는데 모바일 개발자는 100명이 쓰는 앱을 개발하든, 100만 명의 유저가 쓰는 앱을 개발하든 큰 기술력의 차이도 없고 코드의 차이도 없다고 느꼈어요. 그럼 대체 모바일 앱에서 scalable한 코드나 시스템을 만든다는게 어떤걸까 싶었어요.

개발자의 기술력이 집약되는 부분은 병목이 발생하는 구간이겠죠. 서버는 사용자들이 동시에 한 곳으로 몰리기 때문에 그 곳에 부하가 발생하고, 이 부하를 해결하는 기술력이 값진 것이고요. 그런데 앱은 사용자가 많다고 앱에 부하가 걸리지 않아요. 사용자가 앱에 몰리는게 아니라 앱이 사용자의 문앞까지 배달되기 때문에요.👇

그럼 대체 모바일 개발자가 해결해야할 병목은 어디일까요. 역설적이게도 모바일 앱의 병목은 사용자가 아니라 개발자로 인해 발생한다는 생각이 들었습니다. 단일 프로그램으로 배포되는 앱에 수많은 개발자들이 코드를 밀어넣는 지점을 병목이라고 볼 수 있지 않을까 싶었어요.👇

유저가 천명일 때와 수천만 명일 때 서버의 구조와 코드가 달라야하듯이, 앱은 개발자가 3명일 때와 100명일 때 코드의 구조와 개발 환경이 완전히 달라야 하더라고요. 그렇지 않으면 병목의 폐해가 개발자 자신에게는 물론이고 사용자에게도 전가되기 시작해요. 앱 최초 실행 시간(startup time)이 증가하고 앱 용량이 계속 커지기만 하고요. 뿐만 아니라 크래시가 증가해서 사용자의 만족도가 줄어들거나, 계속해서 터지는 사이드이펙트 때문에 핫픽스를 밥 먹듯이 해야할 수도 있습니다. 또 빌드 시간이 늘어나서 개발자의 생산성이 저하되고 스트레스 때문에 삶의 만족도가 줄어듭니다. 게다가 100명이 마스터 브랜치에 푸시를 하는 상황에서는 QA를 하고 버그를 잡고 앱을 배포하는 것조차 간단한 일이 아닙니다. 누구는 버그를 고쳐서 체리픽을 할 때 누구는 기능 개발한 코드를 계속해서 푸시하고 있거든요. 100명까지 가지 않더라도 10명만 넘어가도 위같은 문제들이 스멀스멀 발생하지 않나 싶어요.

그래서 최근에 제 궁금증에 대한 실마리를 찾은거 같아요. 모바일 개발자에게 scalability란 회사가 성장하면서 모바일 팀이 점점 커져도 사용자 경험과 개발자 경험 둘다 악화되지 않게 하면서 앱은 계속해서 빠르고 자신있게 배포하는 것이 아닐까라는 생각을 해봤습니다.

Tags: scalability, mobile dev  

uber/RIBs 유닛 테스트 짜기

RIBs는 우버에서 개발하고 오픈소스로 공개한 모바일 아키텍처 프레임워크다. RIBs 프레임워크는 앱의 복잡한 상태 관리와 비즈니스 로직을 RIBs 덩어리(이하 Riblet)로 분리한 뒤 트리 구조로 연결시킨다. 하나의 Riblet 단위를 구성하는 객체와 각각의 역할은 아래와 같다.

RIBs와 객체지향 프로그래밍

위 도표처럼 각 RIBs 객체는 역할이 뚜렷하게 나눠져 있다(Single Responsibility). 그리고 화살표로 표시돼 있는 각 객체의 input와 output은 프로토콜로 추상화 돼있어서(Dependency Inversion) 따로 떼어낸 후 mocking 기법을 통해 독립적으로 테스트하기 좋다. 또한 부모와 자식 Riblet은 트리 구조로 분리(decoupling) 돼있어서 부모 Riblet의 코드 수정을 최소화하면서도 복잡한 자식 Riblet을 새로 만들거나 수정할 수 있다(Open-Closed Principle). 두 달 정도 프로젝트를 해본 바 느낀점은 RIBs 아키텍처를 쓰면 SOLID에서 비중이 크고 꾸준히 지키기 어려운 SRP, OCP, DIP 세 가지 원칙을 반강제적으로 지키게 된다. 물론 어기기도 쉽다. 어떤 아키텍처를 쓰든 개발자가 코드를 짜기 나름이고 단지 아키텍처를 도입하는 것만으로 내 코드가 좋아지진 않는다.

무엇을 테스트 해야할까

RIBs 아키텍처를 구성하는 객체들은 단위 테스트하기 정말 용이하다. 역할이 명확하고 잘게 나뉘어있고 부모와 자식 Riblet이 약하게 커플링된 만큼, 높은 커버리지를 달성할 수 있다. Riblet당 최소 4+ 개의 테스트 클래스가 필요한데 Xcode의 코드생성 템플릿을 쓰면 매번 만들기 번거로운 유닛 테스트 클래스와 보일러플레이트 코드까지 자동으로 생성해준다. 그런데 처음 프로젝트를 할때는 이 많은 테스트 클래스에 뭘 채워 넣어야하는지 몰라 막막했다. 하지만 코드 리뷰도 받고 기존 코드도 살펴보면서 규칙을 발견했고 이를 통해 각 클래스의 역할과 목적에 따라 어떤 방식으로 테스트 해야하는지 터득했다.

Router Test: 자식 Riblet 라우팅

Router의 테스트는 비즈니스 로직에 맞춰 자식 Riblet을 뗐다(attachChild) 붙였다(detachChild) 하는 동작을 검사해야 한다. Router 객체 설명에 보면 ‘라우터가 자식 라우터를 만들때는 꼭 helper builder를 써야 한다.’(링크)는 주석이 있다. 자식 Riblet을 생성할 때 xxBuilder 클래스를 직접 쓰지말고 xxBuildable로 추상화하여 주입 받아야 한다는 말이다. 이렇게 해야 테스트 환경에서 xxBuildableMock을 주입해서 라우터가 자식 Riblet을 제대로 생성했는지 검사할 수 있다. Router는 interactor의 요청에 따라 라우팅을 대신해주는 역할이기 때문에 이것 외에 다른 로직은 없는게 바람직하다.

Interactor Test: 각종 비즈니스 로직

Interactor는 앱의 비즈니스 로직을 담당하는 부분이라 다른 클래스보다 복잡하고 개발자의 자유도가 높다. 그래서 다른 컴포넌트처럼 유형화하기가 쉽진 않지만 자주 사용되는 몇 가지가 있다.

  • Interactor는 자식 interactor에 정보를 전달하기 위해 리액티브 프로그래밍 방식을 사용한다(참고: 공식 문서). 리액티브 스트림을 활용하면 부모와 자식 interactor가 직접적인 의존 관계(direct coupling)를 맺지 않을 수 있다. 스트림은 어떻게 구현하든 개발자의 자유지만 보통은 RxSwift를 쓰면 좋다. RIBs 아키텍처도 Rx를 쓰고 있어서 이걸 쓰면 굳이 리액티브 라이브러리를 여러개 추가할 필요가 없다. 테스트 환경에서는 스트림을 mocking해서 interactor가 제대로 값을 내보내는지 확인할 수 있다. 반대로 부모 interactor한테서 주입 받은 스트림에 subscribe해서 작업을 처리해야하는 경우라면 마찬가지로 mock 스트림을 주입한 뒤 테스트 케이스에서 값을 바꿔가면서 데이터에 맞게 잘 처리하고 있는지 검사한다.

  • Interactor는 view로부터 사용자 입력을 전달받는다. 사용자 입력에 따라 적절한 작업이 실행됐는지 확인하기 위해서 interactor의 view listener 관련 메서드를 테스트 케이스에서 직접 호출해준 뒤 필요한 작업이 실행됐는지 mocking된 의존성 객체를 확인한다.

  • Interactor는 router와 presenter(혹은 view)를 자주 호출한다. 뷰를 뗐다 붙였다 하기도 하고 UI를 업데이트하기도 한다. 그래서 router와 presenter 메서드가 의도에 맞게 불리는지 확인한다. 이 경우에는 단순히 불렸는지 안불렸는지 확인하는 것보다는 불린 횟수를 정확히 검사하는게 좋다. UI 업데이트를 불필요하게 여러번 하는지 확인할 수도 있고 중복으로 뷰 라우팅을 하지 않는지도 확인한다. (참고: 공식 튜토리얼 Mock 객체)

Builder Test: Concrete 클래스 생성과 주입

Builder는 RIBs 객체를 생성하여 참조점을 연결해주고, 의존성 주입에 필요한 concrete 클래스를 생성하는 역할이다. 따라서 isas? 를 써서 올바른 클래스 타입이 생성됐는지 확인해주는 방식으로 동작을 검사한다.

Presenter Test: 뷰 모델 생성 로직

데이터 모델이 뷰 모델로 잘 변환됐는지 확인한다. 데이터 모델은 UIKit 클래스를 쓰지 않도록 만들고, 뷰를 구성할때 필요한 UIKit, Core Graphics 타입 등은 뷰 모델에서 가지고 있는게 좋다. 예를 들어 데이터 모델은 hex 문자열로 색상 값을 가지고 있고 presenter에서 이를 UIColor로 변환을 한다. 또는 데이터 모델이 Date 타입으로 날짜를 가지고 있고 presenter에서 DateFormatter를 통해 문자열로 변환해서 뷰 모델에 저장한다. 이같은 변환 로직을 검사해야 한다.

View Test: 뷰의 렌더링

View는 코드만으로는 유의미한 테스트를 하기 어렵다. 스냅샷 테스트 라이브러리를 쓰면 뷰를 렌더링해서 이미지 파일로 저장해놓고, 테스트를 돌릴때 뷰를 새로 렌더링해서 기존의 이미지 파일과 비교하는 방식으로 뷰 클래스(UIView, UIViewController)를 검사할수 있다. 만약에 코드 수정으로 인해 뷰가 바뀐다면 테스트 케이스가 실패하기 때문에 뷰가 잘못된 걸 미리 알아낼 수 있다. 스냅샷 테스트도 유닛 테스트기 때문에 커버리지에 포함되는게 장점이다. 스냅샷 테스트를 추가해서 Riblet의 커버리지를 대략 12~15% 정도 증가시킬 수 있었다.

Tags: uber/ribs, unit tests  

XCTest 소요시간 단축하기

얼마전까지 XCTest를 유닛 테스트와 통합 테스트로만 구분하고 있었는데, 유닛 테스트 또한 런타임 작동 방식에 따라 application test와 library test로 나눌 수 있다는걸 알게 됐다. 둘의 차이점과 쓰임새를 정리했다.

Application Test vs Library Test

유닛 테스트 타켓의 Host Application 항목에서 앱 executable을 선택하면 application test가 되고 None을 선택하면 library test가 된다. Xcode에서 유닛 테스트 타겟을 만들 때 초기 설정을 변경하지 않았다면 기본값으로 Host Application이 선택된다. 그래서 지금까지는 별 생각없이 대부분의 테스트를 application test로 짜고 있었다. 그러나 목적에 따라 이 둘을 구분해서 사용하면 분명한 이점이 있다.

Application Test

iOS 앱과 관련된 부분(UIViewController, UIWindow, UIView 등)을 검사하는 유닛 테스트는 application test 번들에 포함시켜야 한다. Application test 번들은 테스트를 실행하기 위한 호스트 앱이 필요하고, 호스트 앱을 설치할 iOS 시뮬레이터도 필요하다.

Application test의 단점은 아래와 같다.

  • 앱 라이프사이클이 돌게 되는데 이때 타이밍 이슈가 발생해 테스트 결과가 달라질 수 있다.
  • 시뮬레이터는 동시에 하나의 호스트 앱만 실행할 수 있기 때문에 application test 타겟은 하나의 시뮬레이터에서 병렬(parallel) 테스팅이 작동하지 않는다.

Library Test

Library test는 앱과 상관없는 로직을 테스트하기 위해 사용한다. Library test는 호스트 앱을 시뮬레이터에 설치할 필요가 없어서 테스트가 더 빠르고 이따금씩 동작이 불안정한 시뮬레이터의 영향을 덜 받는다.

반면 library test에서는 작동하지 않는 iOS API가 좀 있다. 그런 경우에는 application test에서 검사해야 한다.

  • 유닛 테스트에서 유저의 버튼 탭을 흉내내기 위해 사용하는 UIControlfunc sendActions(for controlEvents: UIControl.Event)는 동작하지 않는다.
  • 임의의 UIWindow를 keyWindow로 만들 수 없다. 호스트 앱도 없기 때문에 UIWindow 자체가 없다. 그래서 UIView를 렌더링한 다음 검사하는 snapshot test가 불가능하다.
  • Keychain 관련 API

(등등 더 있을수 있다.)

Library Test를 쓰면 좋은 이유

Application test는 호스트 앱의 프로세스에 주입되기 때문에 시뮬레이터에 앱이 설치되고 실행(app launch)까지 된 후에야 테스트 코드가 돌기 시작한다. 반면에 library test는 앱에 의존하지 않아서 앱 설치 없이 iOS 시뮬레이터 내 xctest executable($DEVELOPER_DIR/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Agents/xctest)이나 macOS의 xctest 커맨드라인의 프로세스(/usr/bin/xctest/)에서 실행된다. 따라서 application test를 library test로 전환하면 전환한 타겟 갯수 * 앱 설치 및 실행 소요시간 만큼의 테스트 시간을 줄일 수 있다. 또한 호스트 앱을 기다릴 필요 없어 병렬로 빠르게 테스팅 할 수 있다.

Library Test로 전환하는 방법

먼저 테스트 타겟의 General 탭에 가서 Host Application을 None으로 변경하고 테스트를 실행해본다. 문제가 발생하면 다음과 같이 해결한다.

1. 테스트 번들이 실행되지 않을 때

아래와 같은 런타임 에러를 낸다.

Library not loaded. 
(중략)
Reason: image not found

Application test는 호스트 앱에 주입이 되기 때문에 앱 타겟에 링크된 라이브러리를 사용할 수 있다. 하지만 library test는 독립적인 프로세스에서 실행이 되기 때문에 테스트 타겟에 외부 의존성을 직접 링크해줘야한다. 코코아팟을 사용하고 있다면 Podfile을 열어 앱이 사용하고 있는 외부 의존성을 테스트 타겟에도 추가해준다.

2. 테스트 케이스가 실패할 때

실행은 되지만 테스트케이스가 실패한다면 iOS 앱에 의존성 있는 API를 사용했을 확률이 크다. 따라서 타겟을 분리하고 library test에서 실패하는 테스트는 application test로 옮긴다. 즉 iOS 앱이 필요한 테스트만 application test에서 실행하고 나머지는 library test로 실행하면 테스트 소요시간을 줄일 수 있다

이 글의 초안을 읽어준 류성두 님에게 고마움을 전합니다.

Tags: xctest, application test, library test, ios testing  

재택근무 한달

한국의 코로나바이러스 전파 상황이 악화될 무렵 싱가폴에 들어온 바람에 입사하자마자 의무로 2주간 재택 근무를 해야했고 현지 상황을 고려해 현재는 자발적으로 재택 근무를 하고 있다. 그 사이 스프린트를 하나 끝마쳤다. 새로운 나라에서 새로운 팀에 합류하자마자 원격 근무를 하게 돼서 어려운 상황이 될 수 있었지만 나름 성공적으로 새 회사에 적응하고 업무를 익힐 수 있었다. 화상회의 덕분이다.

코로나 때문에 시작된 강제 재택근무

Grab 입사 첫날에 반나절 정도 오리엔테이션을 받고 집에 돌아갔다. 다음날 아침에 출근 준비를 하는데 HR에서 연락이 왔다. ‘혹시 싱가폴 들어오기 전에 어느 나라에서 왔냐.’ ‘미국에 쭉 있다가 한국에서 하룻밤 자고 들어왔다.’ ‘알았다, 잠깐만 기다려라.’

(10분 뒤)

‘방금 회의에서 정해진 내용인데 한국에서 입국한 직원들은 사무실 출근하지 말고 2주 동안 재택근무하기로 했다. 오늘부터 절대 사무실 오지 마라.’

아니, 나 아직 매니저 얼굴도 못봤고 팀원들도 못만나봤는데 2주나 재택근무를 하라고? 인사는 고사하고 업무를 제대로 할수나 있을지 걱정이 됐다. 원격으로 제대로 근무를 해본 적이 없었기 때문에 의사소통이 제대로 될지도 의구심이 들었고 아직 회사 업무 방식이나 코드도 잘 모르는데 원격으로 답답하게 어떻게 헤쳐나가야하나 싶었다.

모든 미팅은 줌Zoom으로 동시 진행된다

첫 주는 사실상 팀이 프로젝트를 시작하기도 전이라 공식 업무가 없었다. 개발에 필요한 거 설치하고 코드 실행해보고 RIBs 튜토리얼 보고 ‘그랩버디’한테 이것저것 물어보면서 보냈다. 그리고 다음주 월요일이 되어 정식으로 킥오프를 했다. 미팅은 줌으로 진행돼서 나도 집에서 참석할 수 있었다. 이어진 스프린트 계획 미팅도 줌으로 참여했다. 그런데 나만 원격으로 미팅에 참여한건 아니었다. 사무실에 있는 사람들은 한 회의실에 모여 컴퓨터 한 대를 대표로 연결하고, 출장 가있거나 재택 근무를 하는 사람들은 각자 위치에서 접속했다. 이렇게 본격적인 화상회의는 처음이었는데 은근히 매끄럽게 진행돼서 놀랐다. 미팅을 주도하는 사람이 화면 공유 기능으로 자신의 컴퓨터 화면을 공유하고 이걸 다같이 보면서 돌아가며 발언을 했다.

Grab은 줌이 사내 시스템과 연동이 되어있어서 회의 일정을 잡으면 회의실만 예약되는게 아니라 줌 링크도 자동으로 생성이 되어 참석자들에게 공유된다. 그리고 모든 회의실에는 마이크가 설치돼있고 회의를 시작할때는 항상 줌 미팅을 연결한다. 심지어 15분짜리 데일리 스탠드업도 무조건 줌 미팅을 생성한다. 평균적으로 2 ~ 3명 정도는 원격으로 참석하는 느낌이다. 어느날은 매니저가 느즈막히 출근하면서 오는 길에 폰으로 줌에 접속해서 데일리 스탠드업에 참여하기도 했다.

원격 근무를 가능하게 해주는 일상화된 화상회의

모든 회의를 화상으로 병행하니 회사에 안가도 필요한 회의에 참석할 수 있어서 업무에 지장이 없었다. 코드 구조나 사내 개발 프로세스, 툴 사용하는 방법 등도 동료가 줌으로 친절히 설명해주었다. 화상회의가 생활화되니 다른 나라 오피스에서 일하는 동료들과 협업하는 것도 어색하지 않다. 다같이 비대면 회의에 익숙해지니 가능한 것 같다. 무엇보다, 언제든 개인의 필요에 따라 원격 근무를 할 수 있는 여건이 마련되어 있는 것이 좋다. 그래서 사람들은 3 ~ 4주 자리를 비우면서 모국으로 휴가를 가고 그 곳에서 일주일 이상 원격 근무를 하기도 한다.

강제 재택근무 기간이 끝나고 이틀 출근했었는데 다음날 회사 직원이 코로나 확진을 받아서 그 이후로는 팀이 자발적 재택 근무까지 하고 있는 중이다. 화상회의가 일상화되어 있으니 팀 전체가 재택 근무를 하더라도 큰 불편없이 평소처럼 일할 수 있는 것 같다.

#줌_주식사러_갑니다

Tags: work from home, zoom