iOS Modular Architecture 도입기

Gunter Kwon
MUSINSA tech
Published in
11 min readAug 4, 2024

안녕하세요, 무신사 모바일 개발팀 iOS 엔지니어 권태완입니다.

무신사와 같은 대규모 애플리케이션은 시간이 지남에 따라 코드베이스가 복잡해지고 유지보수가 어려워지는 문제에 직면하게 됩니다.

기존에는 하나의 Project와 하나의 앱 타켓만을 가진 단일 구조로 운영되고 있었지만, 이러한 구조는 시간이 지남에 따라 코드베이스가 복잡해지고 유지보수가 어려워집니다. 이는 유지보수가 점점 더 힘들어지며, 빌드 속도가 느려지고 생산성 저하로 이어질 수 있습니다.

이를 해결하기 위해 무신사는 iOS 앱에 Modular Architecture 를 도입하기로 결정했습니다.

Modular Architecture 란?

Modular Architecture 는 애플리케이션을 독립적인 모듈로 분리하여 개발, 테스트, 유지보수를 용이하게 하는 아키텍처 스타일입니다. 각 모듈은 특정 기능이나 도메인을 담당하며, 다른 모듈과의 의존성을 최소화합니다.

Modular Architecture 장점

모듈러 아키텍처의 장점은 모듈화된 코드는 여러 프로젝트에서 재사용이 가능하고, 각 모듈은 독립적으로 개발되고 테스트될 수 있습니다 또한 코드베이스의 복잡성을 줄이고 빌드속도를 향상시킵니다.

전체 코드베이스를 여러 독립적인 모듈로 분할하면, 다음과 같은 빌드 속도 향상의 이점을 제공합니다

  1. 병렬 빌드: 모듈화된 프로젝트는 각 모듈을 독립적으로 빌드할 수 있습니다. 이를 통해 여러 모듈을 동시에 빌드하는 병렬 빌드가 가능해져 전체 빌드 시간이 단축됩니다.
  2. 빌드 캐싱: 모듈별로 캐시를 사용할 수 있어, 변경되지 않은 모듈은 재빌드할 필요가 없습니다. 이는 빌드 시간을 크게 줄이는 데 기여합니다.
  3. 의존성 관리: 각 모듈은 독립적인 의존성 관리를 통해 필요한 부분만 빌드하게 되어, 불필요한 빌드를 방지합니다. 이를 통해 빌드 과정이 더욱 최적화됩니다.

Modular Architecture 도입 과정

저희 팀은 모듈러 아키텍처 도입을 위해 세 가지 Phase 로 나눠 계획하였습니다.

우선, XcodeGen에서 Tuist로 전환하였습니다. 이 과정에서 기존 프로젝트는 CocoaPods를 사용하여 외부 라이브러리를 관리하였지만, Tuist에서는 공식적으로 CocoaPods을 지원하지 않기 때문에 CocoaPods의 의존성을 하나씩 제거하고, Swift Package Manager(SPM)로 전환하는 작업이 필요했습니다.

추가로, Tuist 도입 후 CI/CD 빌드 머신이 정상 동작하도록 수정하였고, 1차적으로 Modular Architecture Layer를 정의하여 분리된 모듈로 파일들을 옮기고, 그 과정에서 레이어 분리에 병목이 되는 코드들을 함께 리팩토링하기로 계획하였습니다.

Modular Architecture Layer 정의

모듈러 아키텍처를 구현하기 위해서는 독립적인 모듈로 나누는 것이 중요합니다. 이를 위해 모듈의 기능과 책임을 명확하게 분리하고, 각 모듈 간의 의존성을 최소화하는 레이어 구조를 정의하였습니다.

1. App Core Layer

App Core Layer는 애플리케이션의 핵심 로직을 포함하는 레이어입니다. 이 레이어는 애플리케이션의 주요 기능을 구성하는 모듈들로 이루어져 있으며, 다음과 같은 모듈로 구성됩니다.

Entity: 애플리케이션에서 사용되는 데이터 모델을 정의합니다.

Networking: 네트워크 통신을 처리하는 모듈입니다.

Service: 네트워크 로직과 외부 시스템과의 상호작용을 담당하는 모듈입니다.

UI: 사용자 인터페이스를 구성하는 컴포넌트들을 포함합니다.

Analytics: 이벤트 로그 분석과 관련된 기능을 처리합니다.

DeepLink: 딥링크 처리를 담당합니다.

FeatureFlag: 기능 플래그를 관리하여, 특정 기능을 활성화하거나 비활성화할 수 있습니다.

2. Third Party Layer

Third Party Layer는 외부 라이브러리의 구현 클래스를 담고 있는 모듈로 구성된 레이어입니다. 이 레이어는 애플리케이션이 외부 서비스와 통합될 수 있도록 도와주며, 외부 라이브러리의 구체적인 구현을 포함합니다. 다음과 같은 모듈로 구성됩니다.

Amplitude, Appsflyer, Braze, Experiment, NTracker : 각 SDK 관련된 구현 클래스를 담고 있는 클래스

Firebase: Firebase 관련 Protocol 를 정의한 모듈

FirebaseImpl: Firebase 관련 Protocol 구현부 모듈

3. Shared Layer

Shared Layer는 여러 모듈에서 공유되는 기능이나 로직을 제공합니다. 이는 공통적으로 사용되는 유틸리티와 같은 기능을 포함하며, 다음과 같은 모듈로 구성됩니다.

Foundation: 기본적인 기능과 유틸리티를 제공합니다.

Resources: 리소스(Asset) 관리 기능을 포함합니다.

Logging: 콘솔 로깅 기능을 제공합니다.

ReactiveX: 반응형 프로그래밍을 위한 라이브러리 및 유틸리티를 제공합니다.

이렇게 Layer로 분리하면 모듈 간의 의존 관계를 명확하게 정의할 수 있습니다. 하위 레이어가 상위 레이어를 참조하는 것은 불가능하며, 동일 레벨의 모듈 간에는 참조가 가능합니다. 그림으로 봤을 때, AppCore Layer와 Third Party Layer는 모듈 간 참조가 가능하고, Shared Layer를 참조할 수 있습니다. 반면, Shared Layer는 AppCore Layer와 Third Party Layer를 참조할 수 없습니다.

이처럼 명확하게 Layer를 정의하면 모듈 간의 의존성을 최소화하고, 모듈 간 순환 의존성(circular dependency)이 발생하지 않도록 도와줍니다

도입 과정에서의 문제점

모듈러 아키텍처를 도입하는 과정에서 몇 가지 문제점이 발생했습니다. 특히 Firebase와 관련된 외부 라이브러리 모듈화를 진행할 때 문제가 있었습니다.

Dulicated Symbols 에러

Firebase 라이브러리는 Analytics, Crashlytics, DynamicLinks, Messaging, RemoteConfig, GoogleSignIn, GoogleTagManager 등의 다양한 기능을 제공하는데, 이들을 Native SPM을 통해 static library로 제공하려다 보니 중복 복사 문제가 발생했습니다. 각 모듈에서 Firebase의 일부 라이브러리를 사용하면서 동일한 라이브러리가 여러 모듈에 복사되는 문제가 발생했습니다.

처음에는 AppCore_RemoteConfig라는 모듈을 정의하여 Firebase RemoteConfig 라이브러리 관련 로직들을 관리하고 있었습니다. 하지만 ThirdParty_Firebase 모듈을 생성하면서 동일한 static library가 두 곳에 복사되어 빌드할 때 에러가 발생했습니다.

Dulicated Symbols 에러

이러한 문제가 발생한 이유는 RemoteConfig 라이브러리가 Firebase Analytics static library 를 의존하고 있어, Analytics static library 가 RemoteConfig와 Firebase 모듈 둘 다에 복사되는 문제가 있었기 때문입니다. static library가 여러 번 링크되면서 동일한 이름(symbol)이 여러 번 정의되면 링커가 이를 허용하지 않기 때문에 에러가 발생합니다.

이 문제를 해결하기 위해 하나의 모듈(ThirdParty_Firebase)에서 모든 Firebase 관련 코드를 관리하고, 다른 모듈에서는 이 모듈을 참조하도록 설계했습니다. 이를 통해 Dulicated Symbols 에러를 해결할 수 있었습니다.

Firebase 구현부 모듈과 인터테이스 모듈 분리

Firebase에서 사용하는 라이브러리들은 정적 라이브러리이기 때문에, 이러한 특성을 고려하여 모듈 구조를 설계했습니다.
정적 라이브러리는 컴파일 시 해당 라이브러리의 코드가 실제로 바이너리에 복사되므로, 동일한 정적 라이브러리가 여러 모듈에 포함되면 각 모듈에 동일한 코드가 중복되어 포함된다는 의미입니다. 이러한 중복은 빌드 시간 증가와 불필요한 메모리 사용을 초래할 수 있습니다.

그래서 저희는 Firebase 모듈을 구현부 모듈과 인터페이스 모듈로 분리하여 정의하였습니다.

ThirdParty_FirebaseImpl 모듈은 여러 Firebase 관련 정적 라이브러리를 포함하고 있으며, ThirdParty_Firebase 모듈에는 추상화된 인터페이스만 존재합니다. ThirdParty_FirebaseImpl은 ThirdParty_Firebase 모듈을 의존하지만, ThirdParty_Firebase는 다른 모듈에 의존하지 않는 구조입니다.
이를 통해 다른 모듈이 ThirdParty_Firebase를 사용하더라도 Firebase 관련 정적 라이브러리는 ThirdParty_FirebaseImpl 모듈에만 복사됩니다.

예를 들어, ThirdParty_FirebaseImpl 모듈에 Firebase Analytics, Crashlytics, RemoteConfig 등의 정적 라이브러리가 포함되어 있고, ThirdParty_Firebase 모듈에는 이들의 추상화된 인터페이스가 정의되어 있습니다. ThirdParty_FirebaseImpl 모듈이 ThirdParty_Firebase를 의존하면서, 실제 Firebase 구현체는 ThirdParty_FirebaseImpl 모듈에서 관리되고, 다른 모듈은 ThirdParty_Firebase를 통해 Firebase 기능을 사용할 수 있습니다.
이렇게 하면 Firebase 라이브러리가 ThirdParty_FirebaseImpl 모듈에만 존재하게 되어 코드 중복 복사를 방지할 수 있습니다.

Modular Architecture 도입 후 효과

Modular Architecture를 도입한 결과, 기존의 0개 모듈에서 19개 모듈로 분리하였고, 이를 통해 빌드 속도 면에서 큰 개선을 이루었습니다. 클린 빌드 속도는 172.3초에서 120.3초로 39.87% 감소하였고, 증분 빌드 속도는 57.3초에서 30.4초로 31.24% 감소하였습니다.

마치며

Modular Architecture 를 도입하여 독립적인 모듈로 개발과 테스트를 용이하게 하고, 확장 가능한 프로젝트를 구축하였습니다. 이로 인해 코드베이스의 복잡성을 줄이고, 빌드 속도를 향상시켜 개발 생산성을 크게 높일 수 있었습니다.

앞으로도 저희 팀은 Modular Architecture 아키텍처를 지속적으로 개선할 예정입니다. 특히, Micro Feature Architecture를 도입하여 Feature Layer를 정의하고 모듈을 분리함으로써 피처별로 데모 앱을 실행할 수 있도록 개선할 계획입니다. 이를 통해 개발 과정의 효율성을 더욱 높이고, 더 나은 품질의 무신사 앱을 제공할 것입니다.

Modular Architecture를 도입한 경험이 도움이 되기를 바라며, 앞으로도 저희 팀은 지속적으로 생산성을 개선하여 사용자들에게 다양하고 좋은 기능을 제공하기 위해 노력하겠습니다.

Musinsa CAREER

함께할 동료를 찾습니다.
무신사 모바일 개발팀은 앱을 통해 무신사의 서비스의 가치를 고객에게 전달하는 역할을 맡고 있습니다. 이를 위해 모바일 엔지니어들은 각 담당 도메인의 영역에서 완성도 높은 서비스를 구축하기 위해 큰 책임감과 자율성을 가지고 있습니다.

전국민이 사용하는 1위 패션 플랫폼 무신사에서 기술로 비즈니스를 성장시키는 경험을 함께하고 싶으시다면 아래 채용 페이지를 통해 지원해 주세요!

🚀 팀 무신사 채용 페이지 (무신사/29CM 전체 포지션 확인이 가능해요)

🚀 팀 무신사 테크 소식을 받아보는 링크드인

🚀 무신사 테크 블로그

🚀 29CM 테크 블로그

🚀 무신사 테크 유튜브 채널

채용이 완료되면 공고가 닫힐 수 있으니 빠르게 지원해 주세요!

--

--