레고처럼 조립하는 토스 앱
100만 줄.
이게 뭐냐고요?
바로, 토스 iOS 앱의 코드량입니다.
토스팀은 사용자에게 가치를 전달하기 위해 끊임없이 서비스를 개발해왔어요. 지금 토스 앱 안에는 수백 개의 서비스가 들어있습니다. 그렇게 성장해오는 동안, 토스 iOS 앱도 Swift 100만 줄이 넘는 거대한 프로젝트로 자라났습니다.
이 글을 읽고 계신 iOS 개발자분들에게 질문을 드려볼게요.
이렇게 프로젝트가 크고 복잡해지면 뭘 해야 할까요?
바로 모듈 분리입니다!
앱을 하나의 큰 Xcode 프로젝트로 관리하는 대신, 여러 개의 작은 모듈로 나눕니다. 그리고 모듈 간의 적절한 구조를 설계하는 거죠.
코드 베이스가 커지면, 모듈 분리도 점점 더 중요해지죠. 그래서 토스 iOS 챕터도 모듈화에 대한 많은 고민을 했는데요.
이 글에서는, 저희가 어떻게 슈퍼 앱 토스의 모듈을 관리하고 있는지 살펴볼게요.
먼저 기존 토스 앱의 구조를 알아봐야겠죠?
기존 토스 앱은 가장 일반적인 계층 구조로 이루어져 있었어요. 책임과 역할에 따라 계층을 나눠 해당 계층에는 그에 맞는 모듈들이 위치하고, 하위 계층에 있는 모듈은 상위 계층에 있는 모듈을 의존할 수 없는 형태죠.
공통적으로 쓰이는 유틸리티 모듈들이 모여있는 Foundation 계층.
수백 개의 서비스 모듈이 위치한 Feature 계층.
최종적으로 사용자에게 제공될 앱이 위치한 App 계층.
이렇게 계층적으로만 모듈을 관리하고 있었어요.
일반적인 계층 구조도 꽤 오랜 시간 잘 작동했습니다. 계층 안에서의 모듈 분리도 되어있었고요. 하위 계층의 모듈이라면 필요에 따라 가져다 활용할 수 있었죠.
하지만 수년간 토스에는 정말 많은 서비스가 생겼습니다. 다시 말해 Feature 계층에 모듈들이 엄청나게 늘어났죠.
그러다 보니 문제가 발생했습니다.
단순히 모듈 수가 많아서는 아니었어요. 많은 서비스가 생겨나면서, 모듈 간의 의존성이 폭발적으로 늘어나는 게 문제였죠.
하나의 서비스가 Feature 레이어의 여러 모듈을 활용하는 경우가 많이 생겼습니다. 결과적으로 같은 계층 내에 의존 관계가 복잡해졌고요.
그러자 순환 참조와 같은 문제가 발생하기도 했어요. 전반적인 모듈 구조도 이해하기에 너무 어려워졌습니다.
처음에는 계층 구조를 유지하면서 대응을 해봤어요. 공통 기능을 다시 묶어서 하위 계층의 모듈로 분리하거나, 새로운 계층을 추가하여 공통 모듈을 추가로 분리하는 방식도 시도했고요.
그런데 이렇게 하다 보니 지나치게 많은 계층이 생겨났어요. 적절한 계층 및 모듈 분리의 기준을 세우기도 애매해지더라고요.
이대로는 안 되겠다. 다른 방법이 필요하다.
iOS 개발자 모두가 느끼기 시작했습니다.
Microfeatures 아키텍처는 Tuist가 소개한 모듈 구조예요. (Tuist는 iOS 프로젝트 관리 툴입니다.)
크게 보면, Microfeatures 아키텍처는 아래와 같이 5개의 요소로 구성되어 있어요.
Feature(Source)
Feature의 실제 기능이 구현된 코드가 위치한 모듈이에요.
Interface
Feature에서 제공하는 기능에 대한 외부 인터페이스와 모델을 제공하는 모듈이에요.
Testing
단위 테스트나 Example 앱에서 사용될 코드와 Mock 데이터 등을 제공하는 모듈이에요.
Tests
단위 테스트, UI 테스트 등이 위치한 모듈이에요.
Example
Feature의 기능을 간단히 체험해 볼 수 있는 작은 앱이에요.
그래서 이 요소들을 가지고 하나의 서비스를 담당하던 모듈을 쪼개는 거예요.
홈 서비스를 예로 들어볼까요?
기존에 하나였던 Home 모듈을 이렇게 5개의 모듈로 나누게 되는 거예요.
- Home (Feature)
- HomeInterface (Interface)
- HomeTesting (Testing)
- HomeTests (Tests)
- HomeExample (Example)
이것을 여러 서비스로 확장해 보면 이렇게 돼요.
Interface 모듈에 있는 인터페이스를, Feature, Testing 모듈이 구현합니다.
Tests 모듈에 테스트를 작성하고요.
Example 앱을 구성할 때는 Feature 혹은 Testing 모듈 중 필요한 것을 골라서 사용해요.
이때 자체적으로 개발한 DI(Dependency Injection) Container를 사용해요. Interface 모듈에 대한 Feature 모듈의 구현을 주입하는 역할을 맡고 있죠.
Microfeatures 아키텍처 구조로 바뀌고 난 이후에는 뭐가 달라졌을까요?
다른 서비스에서 Home 서비스의 코드를 필요로 할 때 Home 모듈을 직접 사용하지 않아요. 대신 HomeInterface 모듈을 사용하죠. HomeInterface에는 외부에 제공되는 인터페이스가 있어요. 이 Home의 기능을 사용할 때는 이 인터페이스를 씁니다.
이렇게 하면 한 서비스가 다른 서비스의 코드를 사용하더라도, 같은 계층 (Feature ↔ Feature, Interface ↔ Interface) 내의 의존 관계가 생기지 않게 돼요.
같은 계층 내에 의존 관계가 복잡해지는 문제를 해결했습니다.
쉽고 멋있게 해결한 척했지만… 사실 Microfeatures 아키텍처를 도입하는 것은 꽤나 험난한 길이었어요.
일단 작업량이 만만치 않았죠. 기존에 하나였던 모듈을 5개의 모듈로 분리해야 하는 작업인데, 기존의 모듈도 이미 수백 개였거든요.
수많은 컨플릭이 발목을 잡았어요. 공통 모듈을 자주 건드릴 수밖에 없었고. 현재 작업 중인 모듈이 있고, 그 모듈이 의존하는 다른 모듈 분리 작업이 동시에 진행되었어요. 컨플릭이 자주 발생할 수밖에 없는 상황이었죠.
현실적인 리소스의 문제도 있었고요. 토스에선 빠른 실험을 위해 ‘매주’ 앱 배포를 진행하는데요. 이런 환경에서 사일로 업무도 하고, 구조 개선을 위한 모듈 분리도 하는 건 쉽지 않은 일이었죠.
iOS 챕터에서는 작업량을 줄이기 위해서, Tuist에서 제공하는 Stencil 템플릿과 Tuist Scaffold 기능을 최대한 활용했어요. 토스 앱의 모듈 구조에 알맞게 Tuist extension 을 적절히 구현했습니다. 다양한 Template 을 만들어서 단 1줄의 코드로 새로운 모듈을 생성할 수 있도록 했죠.
하지만 무엇보다도 iOS 챕터 전체가 모두 적극적으로 참여했던 게 도입을 해낼 수 있었던 가장 큰 이유예요. 기존 구조의 문제를 겪고 있던 분들이 도입의 필요성에 많이 공감해 주셨고요. 너 나 할 것 없이 담당하고 있던 기능들에 Microfeatures 아키텍처를 적용해 나갔어요. 담당자가 퇴사를 했다든지, 공통적으로 쓰는 모듈이라든지 그런 애-매한 모듈들도 있었는데요. 다들 본인 것처럼 적극적으로 나서주셨죠.
Microfeature 아키텍처의 장점은 이게 끝이 아닙니다.
Example 앱이 있기 때문이죠!
Example 앱은 필요한 기능만을 담은 별도의 미니 앱이라고 보시면 돼요.
Microfeature 아키텍처가 도입된 후로, 개발할 때 Example 앱을 적극 활용하고 있어요. 실제로 송금 Example 앱, 자동이체 Example 앱, 본인확인 Example 앱 등등 다양한 Example 앱이 있습니다.
Microfeature 아키텍처가 있다면 Example 앱을 만드는 건 어렵지 않아요. 구현을 확인해 보고 싶은 기능의 Feature 모듈을 사용하고요. 나머지 기능의 경우 Testing 모듈에 있는 Mock을 연결하면 되죠.
아래 그림은 송금 Example 앱인데요. 실제 송금 결과 케이스들을 바로바로 볼 수 있도록 만들어놓았어요.
특히 UI 개발을 할 때 무척 편리합니다. 아까 말씀드린 것처럼, 토스 앱은 어마어마하게 큰 앱이거든요. Swift만 100만 줄이 넘고요. 모듈은 약 700개나 있고요. 그니까 빌드 시간이 엄청 길 수밖에 없어요.
만약 토스 앱 전체를 빌드하고 나서, 수정한 화면이 어떻게 보이는지 확인해야 된다면? UI 수정 확인 한번 하려고 한참을 기다려야 합니다.
빌드 할 때마다 수십 초씩 걸린다는 건 상당히 불편한 일이잖아요. 해보신 분들은 이게 얼마나 생산성을 떨어뜨리는지 아실 거예요.
Example 앱은 이럴 때 빛을 발합니다. Example 앱은 토스 전체 앱 빌드보다 훨씬 빠르거든요. (무려 5배) 전체 앱이 아닌 일부분만을 빌드하기 때문이죠.
Example을 하다 보면, 협업을 하는데도 굉장히 편리해요.
토스 디자인 시스템(TDS)을 만드는 디자인 플랫폼 팀의 예시를 들어드릴게요.
TDS에는 AnimateTop라는 컴포넌트가 있습니다. 말 그대로 애니메이션 효과가 들어있는 제목이에요. 그런데 이걸 코드로만 보면, 실제 느낌은 알기 어렵잖아요.
그럴 때 ShowCaseExample 앱을 사용해요. AnimateTop이 다양한 속성을 적용했을 때 어떻게 보이는지 실제로 확인해 볼 수 있죠.
개발을 하고 나면, Example 앱을 사내에서 배포할 수 있어요.
복잡한 화면이나 애니메이션을 개발하다 보면 디자이너 분과 빌드 된 앱을 같이 확인하는 경우가 꽤 많아요. 중간중간 실제 화면을 확인해보기 위해 다른 팀원에게 앱을 빌드하여 보여주는 경우도 흔히 있고요.
그럴 때 Example 앱을 사용하면, “00님, 이거 와서 봐주세요, 어때요?” 매번 여쭤볼 필요가 없어요. Example 앱 배포했습니다! 알려드리면 직접 받아서 확인하면 되거든요. 직접 사용하시는 PO나 디자이너 분들도 정말 편하다고 하시더라고요.
디자인 플랫폼 팀에서는 새로운 인터랙션, 컴포넌트 등을 만드는 일이 많은데요. 이 Example 앱을 적극 활용해서 개발부터 QA까지 하고 있습니다.
🔔 요약 정리
- 앱의 코드 베이스가 커지고 복잡해질수록, 모듈 분리와 관리가 중요해진다.
- 기존 토스 iOS 앱은 일반적인 계층 구조로 나눠서 모듈을 관리했다.
- 하지만 앱 내 서비스가 계속 많아지면서, 같은 계층 내의 의존 관계가 너무 복잡해졌다.
- 토스 iOS 챕터는 이 문제를 해결하기 위해 Microfeature 아키텍처를 도입했다.
- Microfeature 아키텍처 도입은 작업량, 컨플릭, 리소스의 문제로 쉽지 않은 과제였지만, iOS 챕터 모두가 합심하여 결국 성공!
- Microfeature 아키텍처를 도입하면서 Example 앱도 적극적으로 쓰게 되었다.
- Example 앱은 전체 앱보다 빌드가 5배 빨라서 개발 생산성이 올라가고, 사내 배포가 가능해서 협업에도 도움이 된다.