개인과 팀이 성장하는 모바일 개발 환경

어제 페북에 들어갔다가 어느 회사 iOS팀의 채용 공고를 보게 됐다. 보통 채용 공고는 훑어보면 복지, 기술 스택 등의 키워드 위주로만 몇 개 눈에 띄는데 이 채용 공고는 기술 블로그처럼 내용이 술술 읽혔다. 예전 팀에 있을때 도입하고 싶었던 많은 것들이 이미 구축되어 있었다. 혼자서 팀원들을 설득해가면서 조금씩 도입해봤기 때문에 이만큼의 모바일 개발 환경을 구축하는데에 많은 노력과 시간과 시행착오가 들어갔을 거라는걸 알았다. 고개를 끄덕이면서 읽다가 문득, 내가 쪼렙일때 이 글을 읽었다면 어땠을까란 생각이 들었다. 경험이 부족하고 아는게 별로 없으니 뭐가 좋은건지, 이게 나한테 어떤 영향을 주는지 잘 몰랐을거 같다. 그동안 이런 개발 환경이 있을 때와 없을 때를 다 경험해 봤기 때문에 이제는 안다. 이런 크고 작은 것들이 모여서 내가 회사에서 보내는 시간의 질을 결정한다. 단순하고 반복적인 일이 적고, 개발 도중 흐름을 끊는 사건이 적어야 한다. 그래서 개발에 집중할 수 있는 시간이 많이 확보되는게 업무 시간의 질이고 이 시간이 개발자의 성장과 연결된다.

배포 자동화

“매 커밋마다 모든 코드가 테스트되며, 하루에도 수십 번 구성원들에게 개발 중인 빌드가 공유됩니다.”

배포 자동화는 클릭 한번(혹은 0번)으로 앱을 빌드하고 내부 사용자나 외부 스토어까지 바로 전달할 수 있는 환경을 의미한다. 배포는 단순하고 반복적이면서 자잘하게 시간이 오래걸린다. 예전 팀에 처음 갔을때는 반만 자동화되어있었다. 앱을 배포하려면 먼저 앱 버전이나 빌드 넘버를 올려줘야하는데 이걸 누군가가 수동으로 커밋을 하고, PR을 만들고 규칙에 따라 굳이 테스트도 다 통과해야하고, 마지막으로 또다른 개발자가 PR을 승인 해줘야 비로소 버전을 올릴 수 있었다. 프로세스가 이러하다 보니 구성원들에게 앱을 전달하려고 할때마다 무려 개발자 두 명의 업무 흐름이 끊어졌다. 이런 방법에 문제를 느껴서 꾸준히 개선을 해나갔고 결국에는 시리로 배포를 할 수 있을 정도로 자동화를 해서 배포에 소요되는 노력과 시간을 엄청 줄였다.

개발에 몰입하려면 방해받지 않는 연속된 시간이 정말 중요하다. 특히 개발자에게 30분 두 번은 1시간과 결코 같지 않다. 하루에 수십 번 빌드가 공유된다는건 배포가 완전히 자동화돼있어서 개발자는 업무 중에 배포에 신경쓸 일이 없다는 뜻이고 구성원들도 다른 팀원을 방해할 필요없이 가장 최신의 앱을 설치해볼 수 있다는 뜻이다. CI/CD가 구축되어 있어서 이런 종류의 자잘한 방해 업무가 최대한 없는게 좋은 환경이다.

피쳐 플래그

“피쳐 플래그를 도입하여 사용자에게 매주 배포할 수 있는 환경을 만들었습니다.”

피쳐 플래그란 백엔드가 내려주는 불리언 값으로 앱의 기능을 껐다 켰다 할 수 있는 장치다. 피쳐 플래그가 없으면 어떤 기능이 개발되는 동안에는 마스터 브랜치에 코드를 밀어 넣을수 없어서 별도로 피쳐 브랜치를 관리해야 한다. 기능이 규모가 크고 개발이 오래 걸릴수록 점차 마스터와 피쳐 브랜치가 멀어져서 나중에 합칠때 많은 고생을 한다. 심지어 머지 컨플릭을 잘못 수정해서 코드가 유실되고 기능이 망가지는 일도 겪어봤다. 이런 일을 겪고나면 마스터 브랜치에 코드를 병합하는 일이 중대한 일이 돼서 걱정과 두려움도 생긴다. 열심히 개발해서 테스트까지 잘 마쳤는데 코드 머지하는 마지막 단계에서 문제가 발생할 수 있다는건 개발자를 위축시킨다.

개발 인원이 적어서 한번에 기능 하나만 개발할 수 있고 그 사이 앱을 배포할 일이 없다면 이런 환경의 필요성을 느끼지 못할 수 있다. 그러나 피쳐 플래그가 구축되어 있으면 여러 개발자가 동시에 각자 다른 기능 개발을 하면서도 마스터 브랜치 하나만 유지하면 되기 때문에 브랜치 관리가 훨씬 쉽고 그 와중에 실배포도 무리없이 할 수 있다. 개발이 덜 된 코드가 있더라도 백엔드 값을 통해 미완성 기능을 유저한테 숨길 수 있기 때문이다. 이렇게 되면 개발자는 브랜치 관리나 머지 컨플릭 등에 신경을 덜 쓰고 온전히 개발에 더 집중할 수 있다. 피쳐 플래그의 또 다른 장점은 출시된 기능에 문제가 발견됐을때 임시 방편으로 기능을 꺼버려서 유저한테 미치는 영향을 줄일 수 있다는 점도 있다.

코드 리뷰와 짝 프로그래밍

“코드 리뷰는 iOS 팀의 핵심 문화입니다. (중략) 페어 프로그래밍을 종종 활용합니다.”

이민석 이노베이션아카데미 학장님은 ‘리뷰 없이 만들어진 코드는 사채다’라는 말을 하셨다. 정말 공감가는 말이었다. 업계에서 기술 부채라는 용어를 쓰는데, 개발자 한명이 혼자서 만든 코드는 고리대금 악성 사채처럼 기술 부채를 많이 발생시킨다는 뜻이다. 자신이 짠 코드를 자신과 동일시 하거나 지나친 애착을 가지고 변화로부터 보호하려는건 정말 좋지 않다. 코드는 팀원들 모두와 공동명의로 된 자산이라는 생각으로 다뤄야한다. 코드 리뷰를 하면 기술 공유가 되고 팀이 전체적으로 같이 성장한다. 개발하다 보면 가끔씩 자만감에 ‘이건 진짜 완결성 있게 만든거같다. 리뷰에서도 별다른 의견이 나올만한 부분이 없겠지’ 싶은데도 다른 동료의 눈에서 개선점이 발견되고 새로운 아이디어가 생기고 몰랐던 기술을 배울수 있다. 서로의 코드를 보여주지 않고 지적하지 않으면 개인으로나 팀으로나 성장할 수 없다.

짝 프로그래밍은 코드 리뷰의 상위 호환이다. 개발 실력이 비슷한 두 명이 하는것보다는 비대칭적인 실력을 가진 두 명이 하는게 효과적이다. 구글에서는 짝 프로그래밍을 거쳐 만들어진 코드는 코드 리뷰 승인을 받은 것으로 간주한다고 한다. 짝 프로그래밍까지는 아니지만 내가 신입일 때 사수님이 옆자리에 앉혀놓고 코딩을 하고 디버깅을 하는 모습을 보여준 경험에 대해서는 예전 글에서도 언급한 적이 있는데 정말 큰 도움이 됐고 아직까지 기억에 남는다. 얼마 전에는 내가 알려주는 입장에서 신입 개발자와 짝 프로그래밍을 했는데 반응도 좋았고, 나도 생각을 정리하고 설계를 더 탄탄하게 할 수 있는 좋은 계기였다. 시니어 개발자와 하는 짝 프로그래밍은 주니어 개발자에게 정말 값진 시간이다. 나보다 잘하는 사람으로부터 기술과 노하우를 1대1로 전수받고 질문도 마음껏 할 수 있는 기회는 어디가서 돈 주고도 못 산다.

단순하고 반복적인 일이 적고, 온전히 개발에 오래 집중할 수 있는 시간이 많은게 좋은 환경이다.

그러기 위해서는 반복적인 업무를 자동화하는게 필수적이다. 또한 코드 리뷰 문화가 자리 잡혀 있으면 개인은 물론 팀이 전체적으로 같이 성장할 수 있다. 위 기준와 사례를 통해서 어떤 회사를 가야할지, 어떤 팀에 가야할지, 혹은 어떻게 자신의 주변 상황을 개선해야 더 성장할 수 있을지 고민인 주니어 개발자분들에게 도움이 됐으면 한다.

Tags: mobile platform, growth, mobile ci/cd  

How to search files in Xcode

Some of the most useful “Power Xcode Tricks” from the try! Swift World online workshop by @ericasadun

Search Scope

Create custom scopes
You can create custom search scope to limit your search within a subset of your code. For instance, create a scope to search only within your small team's scope.
Exclude certain files ⭐️
Xcode doesn't provide out-of-the-box filter functionality for this. But you can use regex to exclude certain files to create useful search scopes. For example, you can exclude test files from your search.
Other handy search scopes
Tests Only, Swift Only, Objective-C Only, headers, etc.
Search history
Related items

Navigation Pane

Find files with keyword
Find files with keyword X or Y
Wildcard
Recenty opened files
Modified file
Open sibling folders ⭐️
This functionality applies to everywhere with the similar triangle (e.g. Find tab in Navigator Pane, etc)

Open subfolders ⭐️
This functionality applies to everywhere with the similar triangle (e.g. Find tab in Navigator Pane, etc)

Others

File history
Reveal in Project Navigator ⭐️
Edit all in scope

Tags: xcode  

모바일 개발자에게 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