iOS
10살 여기어때 iOS앱의 모듈화 여정
Envy여기어때
2024년 3월 5일
원문에서 보기 ↗여기어때 iOS 앱은 2014년 국내 숙소 숙박 서비스로 시작하여 딱 10년째인 2024년 지금은 해외여행, 항공권, 공간대여, 티켓까지 다양한 서비스를 제공하는 복합 플랫폼으로 성장했습니다. 여기어때 프로젝트는 서비스의 성장과 함께 계속해서 덩치가 커져 현재 약 70만 라인의 소스코드로 이루어져 있습니다.

Copyright 2024. 여기어때컴퍼니 All Right Reserved. Graphic by 김제린(Riny).
대규모 프로젝트 여기어때 Tuist 도입
앱의 규모가 증가하고 하나의 프로젝트 위에서 많은 인력이 동시에 개발하는 상황이 되면서 iOS 개발자들은 다음과 같은 문제를 겪게 됐습니다.
- xcodeproj 파일의 컨플릭 문제(iOS 개발자라면 깊게 공감하실 것 같습니다.)
- 프로젝트의 빌드시간이 점점 오래 걸리기 시작함
우선, xcodeproj파일의 컨플릭 문제부터 해결해야 합니다.
iOS IDE인 Xcode가 다루는 xcodeproj파일은, 프로젝트에 속한 소스의 변경 사항이 저장되어 있는 형태여서, 소스코드 파일이 변경될 때 해당 변경 이력 부분에서 전부 컨플릭이 발생하는 구조입니다.

컨플릭이 발생하면 당연히 프로젝트 자체가 안열리게 되구요..

컨플릭이 많이 발생하게 되면 git Tool 자체가 종료되어버리기도 합니다…😇
머지해보기 전까진 프로젝트 파일 내에 컨플릭이 얼마나 많이 발생할지 예측할 수 없고, 이를 해결할 방법론도 없어서 개발자들이 프로젝트가 정상화될 때까지 손으로 컨플릭을 고쳐야 했습니다. 이는 대규모 앱 개발에 정말 크리티컬한 이슈이기 때문에, 일정 규모 이상의 프로젝트를 운용하는 iOS 개발 조직에서는 통상 Tuist와 같은 서드파티 프로젝트 관리 솔루션을 사용합니다. Tuist는 xcodeproj파일 기반으로 프로젝트 내 구성요소들을 관리하는 기존의 형태를 뒤집어서, Tuist 설정값 및 프로젝트 내 구성요소들을 기반으로 xcodeproj파일을 생성해 주는 형태로 동작하기 때문에, xcodeproj 파일 내 컨플릭에서 벗어날 수 있습니다.
여기어때 iOS 파트 역시 xcodeproj 파일 컨플릭 이슈를 해결하기 위해 약 3개월 동안 아래와 같은 과정을 거쳐 완전히 Tuist 기반으로 프로젝트가 관리되도록 변경했습니다.
- 프로젝트 구조 분석
- 폴더 구조 정리
- 외부 종속성 툴을 SPM(Swift Package Manager)으로 전환
- 앱에서 외부 라이브러리를 찾지 못해 생기는 런타임 앱 크래시 해결
- 라이브러리 중복 복사로 인한 빌드 에러와 앱 오류 해결
- 예측 못한 오류가 없는지 앱 전체 검증 진행
그 결과 브랜치를 머지할 때마다 발생하는 xcodeproj 파일 컨플릭 이슈가 해결되어 개발 생산성과 안정성이 크게 향상됐습니다. Tuist를 성공적으로 도입하면서 iOS 개발 환경에서 브랜치를 병렬로 개발 진행하는데 큰 걸림돌이였던 xcodeproj 컨플릭에서 벗어나게 되었고, 모듈을 자유롭게 추가/삭제 할 수 있는 환경이 만들어졌습니다.
다음으로는 빌드시간 단축을 위한 모듈 분리 작업을 알아보겠습니다. 이를 통해 모듈들의 역할을 명확하게 분담하고, 각 모듈간의 참조 구조를 명확하게 분리할 수 있습니다.

모듈화를 왜 할까?
최초의 여기어때 앱은 Obj-C로 개발되었습니다, MVC였죠 🙂
Swift로 100% 전환 및 여러 아키텍쳐 개념이 도입되어 왔지만, 그 근본은 하나의 프로젝트로 이루어진 싱글앱 구조였습니다. 책임과 역할에 따른 계층 구조 없이 객체간 상호 참조가 아주 복잡하게 얽혀 있어 코드 결합도가 높은 상태였습니다.
안녕하세요! -> 안녕하세요.
위와 같은 단순한 텍스트 변경사항을 확인하기 위해서도 프로젝트 전체를 다시 빌드해야 했습니다. 당시 저희가 사용하던 M1 MacBook Pro에서 클린빌드 기준 약 15분의 빌드타임이 필요했습니다. 서비스 초기에는 프로젝트 전체 코드의 양이 많지 않아서 빌드 타임도 딱히 오래 걸리지 않았고, 코드간의 높은 결합도 역시 큰 문제가 되지 않았습니다. 하지만 프로젝트 규모가 커짐에 따라 늘어나는 빌드타임, 코드 간의 높은 결합도로 인한 사이드 이팩트는 개발비용을 크게 증가시키는 이슈가 됐습니다.
이제 싱글앱 구조로는 한계가 온 것 같다. 모듈화를 시작하자.
iOS 파트 내에서 위의 이슈를 해결하기 위해 모듈화의 필요성을 느끼게 되었습니다. 모듈화를 통한 계층 구조화, 필요한 기능들만 빌드하여 사용하는
Micro Feature Architecture 구조 도입이 해결 방법으로 제시됐습니다.
계층구조를 나누는 작업은 아래와 같은 문제로 인하여 생각보다 쉽지 않았습니다.
- 기존 코드의 순환 참조 구조 해결 문제
- 명확하지 않은 데이터 Model 분리 문제
- 기존 비즈니스 로직 간의 높은 결합도를 해소 문제
- 계층구조를 정립하면서 생기는 기존 코드의 의존성 역전 문제
위에 언급한 작업은 출시 전의 서비스, 출시 초창기의 서비스에서 진행하면 되는 것이 아니었습니다. 지속해서 업데이트가 이루어지고 있는 서비스에서, 기존 서비스 운영 및 신규 개발에 대한 안정성을 유지하며, 앱 근본 구조의 개선이 이루어져야 하므로 난이도가 높은 작업이었습니다.

여기어때 iOS 프로젝트 계층 이미지
결론부터 말씀드리자면, 여기어때 프로젝트는 약 1년의 개발 과정을 통해 위의 사진처럼 책임과 역할에 따라 계층 구조를 가진 프로젝트로 다시 태어났습니다. 그로 인해 해당 계층에는 그에 맞는 필요한 모듈들이 존재하게 되었고, 하위 계층 모듈은 상위 계층의 모듈을 의존할 수 없는 형태로 변경됐습니다.
모듈화 진행과정
iOS 파트에서는 어떤 과정과 기준을 통해 모듈화 작업을 진행했는지 간단하게 이야기하려 합니다.

Entity Layer
Entity
처음에는 데이터영역과 실제 View에서 사용하는 DTO(Data Transfer Object)영역까지 분리하는 작업이 가능한지 확인해 보았습니다. 그 결과, 기존의 여기어때에서 사용하는 Model이 가진 아래와 같은 문제점으로 인해
- 명확하지 않은 Model 네이밍 문제
- Model간의 중복되는 네이밍 문제
- 해외 숙소 / 국내 숙소 등 각각의 타깃에서 만들어서 사용하던 Model의 분산 문제
모든 데이터클래스를 한 번에 변경하는 작업은 현재 개발되고 있는 피처에 큰 영향이 가고, 앱 안정성에도 문제가 있을 수 있다고 판단했습니다.
Entity 영역을 모듈화하는 단계에서는 기존의 Model에 대한 정리, 앱 전체적으로 사용되고 있는 extension의 정리, Rx 모듈 축소 및 제거를 목표로 작업이 이루어졌습니다.
기존에 사용하던 Model을 정리하면서 프로젝트 내에 그 Model을 참조하는 모든 코드를 빠짐없이 수정해야 했습니다. 그리고 수정한 코드를 다른 피처들과 머지하는 과정에서는 수많은 컨플릭 이슈가 발생했습니다. 이를 최소화 하기 위해 구조 개선하면서 동시에 추가 피처에 대한 개발 건의 출시 타이밍을 적절하게 짜맞추기는 정말 어려웠습니다.
그래서 위에 언급한 이슈를 해결하기 위한 git 전략을 수립하는 데 많은 리소스가 사용 됐습니다. 그럼에도 방법론적인 차원에서 완벽히 해소되지 않는 많은 이슈가 작업 과정에서 발생했습니다. 이런 이슈들은 파트 내 상시 긴밀한 커뮤니케이션과 반복 작업을 통해 개발자들이 열심히 잘 해결했습니다😇

UseCase Layer
UseCase / Service
UseCase / CoreService 계층 구조 개선은 프로젝트 전체적으로 중요하게 사용되는 서버 통신 로직 / 비즈니스 로직에 대한 모듈화 작업입니다. 이미 프로젝트 전체적으로 사용되던 기능이기에 기존 코드와의 호환성과 개발 안정성이 최우선으로 요구됐습니다.
기존 프로젝트에서는 UseCase / Service 계층에 해당하는 Manager, Service, NetworkAdpater등의 기능들이 싱글톤으로 구현되어 제공됐습니다. 하지만 상/하위 계층의 명확한 개념 없이 만들어져있다 보니 Manager, Service 간의 순환 참조가 일어나기 쉬운 구조였고, 실제로도 많이 발생하고 있었습니다. 그래서 UseCase / Service 계층에 해당하는 모듈의 구조 개선과, 계층에 따라 재정리하는 작업을 아래와 같이 정리하여 진행했습니다.
- 내부에 구현되어 있는 순환 참조 코드 구조 개선 작업
- 완성된 코드를 직접적으로 제공하는 싱글톤 패턴이 아닌 DI Container를 통해 추상화된 객체를 전달하여 코드의 결합도를 낮추는 작업
- “Manager, Service” 등 제각각 정의된 이름에 규칙을 부여하고 그것에 맞게 구조 개선 작업
UseCase / Service 계층을 구조 개선 작업할 때 기존의 싱글톤 패턴처럼 클래스 전체를 제공하는 것이 아닌 추상화 작업을 사전 진행하여 상위 계층이 요청하는 기능을 Interface형태로 제공하였습니다. 그 결과, 구조 개선 작업이 피처 개발에 미치는 영향이 최소화되었습니다. 이로 인해 상위 계층의 작업을 담당한 팀원들은 모듈화 작업과 피처개발을 병행할 수 있게 됐습니다. UseCase / Service 계층 작업을 통해 팀 전체적으로 추상화 작업에 대한 이해도가 높아졌고 앞으로의 방향성에 대한 공감대가 형성됐습니다.

Micro Feature Architecture
Feature
UseCase / Service 계층 모듈화 작업이 끝난 후, 피처 계층의 모듈화 작업 시작을 위해 해당 계층을 어떤 구조로 변경해야 하는지에 대한 고민이 많았습니다. 피처 영역은 가장 많은 모듈을 포함하게 되고, 가장 많은 모듈을 생성하게 될 계층이므로 가장 효율적이고 생산적인 구조를 가져야 했습니다. 어떻게 하면 조금이라도 더 빨리 효율적으로 개발할 수 있을지에 대한 고민의 과정 끝에 Tuist에서 제시한 Micro Feature Architecture 형태를 채택하게 됐습니다.
Micro Feature Architecture의 특징은 다른 피처들 간의 상호작용을 도와줄 수 있는 BaseDependency 기반으로 각각의 피처 인터페이스를 만들고 해당 인터페이스에 다른 피처들과 상호 작용이 필요한 최소한의 인터페이스를 제공하게 됩니다. 이렇게 구조화된 Micro Feature Architecture는 피처 개발할 때 아주 유연한 구조로 되어 있어 개발의 생산성을 높여줍니다.
여러 건의 피처가 동시에 개발되고 있을 때, 개발이 진행되고 있는 다른 완성된 피처 모듈을 직접적으로 가져오지 않고, Mock 모듈을 참조하게 됩니다. 개발 중인 피처와 연관이 있는 피처 개발이 완성이 되지 않더라도, 그림과 같이 SampleApp에 다른 피처의 Mock 모듈을 참조하여 빌드 및 테스트를 진행 할 수 있고 이를 통해 전체적인 개발의 생산성을 높일 수 있었습니다.
“Micro Feature Architecture 도입하자!!”
Micro Feature Architecture 도입이 결정되자마자 바로 프로젝트 전환 작업이 한번에 완성된 건 아닙니다. 기존의 피처도 추가 기능 개발 건을 통해 구조 개선 과정을 거치고 있기 때문에 모든 피처를 단기간 내에 모듈화 한다는 건 사실상 불가능했습니다. 그로인해 꾸준히 전환 작업이 이루어지고 있습니다
모듈화가 진행되면서 새롭게 개발하는 피처들은 모듈화를 적용하며 추가 개발이 이뤄지는 데 모듈화가 되어 있지 않은 피처를 사용하기 위한 의존성 역전 현상이 생기게 되었습니다. 그리고 모듈화를 진행하며 개발을 진행하기 위해 의존성 역전 문제를 해결해야 할 필요가 있었습니다.
이를 해결하기 위해 공통 모듈 BaseDependency에 Interface Protocol을 추가하고 추후에 모듈화가 되어 있지 않은 피처를 모듈화 진행하는 방향으로 의존성 역전을 해결하며 모듈화 작업이 진행되었습니다.

여기어때 모듈화 작업 타임라인
이렇게 약 1년의 기간 동안 모듈화 구조 개선 작업을 통해, 약 10년간 하나의 프로젝트로 관리가 되던 여기어때 iOS 프로젝트는 명확한 계층구조를 가지게 될 수 있었습니다. 모듈의 계층구조가 명확하게 나누어지고, 추상화 작업을 통해 기능 수정 사항으로 인한 사이드 이펙트의 위험성은 줄어들게 됐습니다. 그리고 Micro Feature Architecture 구조화를 통해 팀원들은 더 이상 다른 Feature의 개발과 상관없이 본인의 Feature 개발에 더욱 더 집중할 수 있게 됐습니다.
앞서 말씀드린대로 아주 작은 부분이라도 변경을 하고 그 결과를 화면으로 확인하기 위해서는 전체 프로젝트를 빌드해야 했고, 그 시간은 15분 정도 소요되었습니다. 그러나 이번 모듈화 이후에는 70~85%를 단축하여 4분 내외로 빌드되었습니다.
현재 상용 서비스 중인 대규모 프로젝트에서 모듈화하는 일은 쉬운 작업이 아닙니다. 서 있는 자동차의 엔진을 교체하는 작업과 달리고 있는 자동차를 계속 달리게 하면서 엔진을 교체하는 작업의 난이도는 하늘과 땅 차이입니다. 여기어때는 2주 단위로 앱스토어 앱 업데이트가 진행되는 프로젝트입니다. 이렇게 치열하게 달리고 있는 프로젝트에서 모듈화를 병행하는 과정은 정말 많은 챌린지가 있었고 다음과 같이 난이도 높은 문제들에 대해서 고민해야 했습니다.
- 계층구조를 잡으면서 기존의 코드들이 가지는 결합도를 해결하는 문제
- 여러 모듈을 생성하면서 라이브러리들이 중복으로 복사되어 생기는 문제
- 하위 모듈이 참조하고 있는 라이브러리를 상위 모듈에서 찾지 못하는 문제
위와 같은 다양한 이슈들이 모듈화 과정에 존재하였지만 특히 그 중에서도 레거시 기능과 얽혀있거나 정책이 복잡한 부분의 구조 개선 작업은 난이도가 상당히 높았습니다. 하지만 여기어때 iOS 파트원들은 이런 고난도 작업들을 진행할 때 선뜻 나서서 적극적으로 참여해주셨습니다. 이러한 팀원들의 자발적인 참여가 모듈화 작업이 성공적으로 진행되는 데 가장 큰 힘이 되었습니다.
아직 30% 가량의 모듈화 작업이 완료되지 못한 채 저희에게 남아있습니다. MainApp에 남아있는 피처들의 모듈화와 CoreService 구조 정리 작업이 바로 이 부분입니다. 이 부분은 앞으로 파트원들과 함께 즐겁게 개선해 나갈 수 있겠다는 확신을 가질 수 있게 되었기 때문에 매우 즐겁게 진행할 수 있을 것 같습니다. 이 프로젝트를 통해 파트원들이 한마음으로 목표하는 방향성이 생겼고, 변화되는 프로젝트를 보며 기술적 성취감을 느껴서 더욱 보람이 있었습니다.
짧은 요약
- 프로젝트가 커지고 개발자 수가 늘어남에 따라 iOS 프로젝트 컨플릭 이슈를 해결해야만 했다
- Tuist를 활용하여 xcodeproj 파일의 컨플릭을 해소하였다.
- 빌드 시간 단축 및 프로젝트 내 모듈들의 역할 분담과 참조 구조를 명확하게 운영해야 한다.
- 이를 위해서는 단일 서비스에서 모듈화된 서비스 형태의 프로젝트로 변경이 필요했다.
- 약 1년의 기간 동안 여기어때는 Entity → UseCase → CoreService → Feature 순으로 모듈화를 이루었다.
- 여기어때 iOS 파트는 Feature Layer 개발의 효율성을 위하여 Micro feature Architecture를 도입했다.
- 모듈화 과정을 통해 개발 생산성, 안정성이 올라갔다.
- 모듈화가 진행되며 생기는 의존성 역전 현상을 Interface Protocol 제공으로 해결하였다.
- 모듈화 작업은 절대적인 작업량, 동시에 진행되고 있는 Feature 개발 작업들과의 컨플릭의 문제로 난이도 높은 과제였다.
- 하지만 iOS 팀원 모두 하나의 공통된 목표를 바라봄으로 성공적으로 진행할 수 있었다.
- 현재 iOS 프로젝트에서 트랜디한 모듈화라는 기술을 적용할 수 있어 매우 만족스러웠다.