dynamic-feature-module

1 개의 포스트

200MB 모듈을 팀 단위로 해결하기: 당근 숏폼팀의 On-demand Dynamic Feature Module 도입 (새 탭에서 열림)

당근 숏폼팀은 글로벌 사용자에게 불필요한 용량 부담을 주지 않기 위해 비디오 편집 기능의 핵심인 네이티브 라이브러리를 On-demand Dynamic Feature Module(DFM)로 분리했습니다. 기술적 순수성보다 운영의 안정성을 택하여 코드 영역은 베이스 모듈에 유지하고 무거운 SO 파일만 선택적으로 다운로드하는 구조를 도입했으며, 이를 통해 기능의 독립성을 확보하면서도 전체 앱 용량을 효율적으로 관리하는 성과를 거두었습니다. ### 편집 기능의 비대화와 DFM 도입 배경 * 당근 스토리의 비디오 편집 기능 추가로 인해 관련 모듈 용량이 200MB까지 증가했으며, 리소스를 CDN으로 전환한 후에도 40MB라는 적지 않은 용량이 남았습니다. * 편집 기능은 전체 사용자 중 일부만 사용하며 특히 기능이 제공되지 않는 글로벌 사용자에게는 불필요한 용량이었기에, 필요한 시점에만 설치하는 On-demand DFM 방식을 선택했습니다. * 단순히 용량을 줄이는 것을 넘어, 팀 단위의 실험적 기능이 전체 앱 사용자 경험(UX)에 영향을 주지 않는 구조를 만드는 것이 핵심 목표였습니다. ### 개발 과정에서의 기술적 제약과 해결 방안 * **Hilt/DI 제약:** DFM은 컴파일 타임에 베이스 모듈과 의존성 그래프를 합칠 수 없어 일반적인 Hilt 사용이 불가능합니다. 이를 해결하기 위해 `EntryPoint` 패턴으로 베이스의 의존성을 노출하고 DFM에서 Dagger 컴포넌트를 수동으로 구성했습니다. * **SplitCompat 설정:** 별도로 설치된 DFM의 클래스와 리소스에 접근하기 위해 `SplitCompat`을 적용하여 기본 ClassLoader가 나중에 설치된 모듈을 인식할 수 있도록 조치했습니다. * **R8 난독화 관리:** DFM에만 적용된 keep 규칙으로 인해 베이스의 타입이 난독화되어 런타임 오류가 발생하는 문제를 겪었으며, 모든 중요 규칙을 베이스 모듈에서 집중 관리하는 원칙을 세웠습니다. ### 네이티브 라이브러리(SO) 관리의 복잡성 * **로딩 경로 문제:** On-demand DFM의 SO 파일은 일반 경로와 다르기 때문에 `System.loadLibrary()` 대신 `SplitInstallHelper.loadLibrary()`를 사용해야 하며, 수정 불가능한 서드파티 SDK는 베이스로 옮기는 등 전략적 배치가 필요했습니다. * **STL 충돌과 메모리 이슈:** 여러 모듈이 `libc++_shared.so`를 공유할 때 발생하는 심볼 충돌과 메모리 오염을 방지하기 위해, 베이스 모듈에서 특정 SO를 제외하거나 `pickFirsts` 설정을 통해 버전 정합성을 맞추었습니다. * **ABI 필터 일치:** 베이스와 DFM 모듈 간의 지원 ABI 세트가 다르면 빌드 자체가 실패하므로 모든 모듈의 `ndk.abiFilters`를 동일하게 정렬했습니다. ### 실용적인 결론: SO 파일 중심의 DFM 구조 * 모든 코드를 분리하는 대신, 용량의 90% 이상을 차지하는 SO 파일만 DFM으로 옮기고 비즈니스 로직은 베이스 모듈에 남기는 실용적인 노선을 택했습니다. * 기능 코드가 베이스에 있으므로 DFM 로딩에 실패하더라도 앱이 강제 종료되지 않고 사용자에게 적절한 안내를 보여줄 수 있어 운영 안정성이 크게 향상되었습니다. * 로컬 개발 시 DFM 설치 여부를 신경 쓰지 않아도 되어 디버깅 효율이 높아졌으며, 결과적으로 대규모 앱에서 팀 단위 실험을 안전하게 진행할 수 있는 기반을 마련했습니다. **추천:** 대규모 앱에서 특정 국가나 특정 사용자에게만 제공되는 무거운 기능을 개발 중이라면, 전체 아키텍처를 분리하느라 고생하기보다 용량의 핵심이 되는 에셋이나 네이티브 라이브러리만 On-demand로 분리하는 방식을 우선적으로 고려해 보시기 바랍니다.