들어가며
안녕하세요, KT Experience Engineering 본부에서 모바일 앱 개발자로 근무하고 있는 최유태입니다.
소상공인 혁신 플랫폼 '사장이지' 앱을 개발하면서 모바일 아키텍처 관점에서 고민하고 여러 문제를 겪었던 경험을 공유하고자 합니다.
먼저, 사장이지 앱이란?
KT에서 소상공인을 위한 4대 서비스(통화비서, 기가아이즈, 로보케어, 하이오더)와 타사 제휴 서비스(AI 매장음악 등)를 통합한 소상공인 사업성장 풀케어 플랫폼입니다.
초기 아키텍처 검토
"어떻게 하면 대규모(4개의 앱+@) 프로젝트를 효율적으로 관리하면서 빠르게 개발할 수 있을까?"
모바일 Clean Architecture 기반의 도메인 중심 아키텍처를 논쟁 없이 채택했지만, High-Level 관점에서 Layer First vs Feature First에 대한 여러 논의가 있었습니다.
Layer First: 레이어(presentation/domain/data) 단위로 나누고, 그 안에 기능들을 배치하는 방식
TypeScript/lib /domain /featureA /featureB /presentation /featureA /featureB1234567
Feature First: 기능 단위로 나누고, 그 안에 관련 레이어를 모아두는 방식TypeScriptlib/features /featureA /data, /domain, /presentation /featureB /data, /domain, /presentation12345
장단점 비교
| Layer First |
Feature First |
|
|---|---|---|
| 장점 |
|
- 기능 단위 앱 실행 가능 - Modular Architecture 유리
|
| 단점 |
|
|
"초기 비용이 크더라도 구조상 이점이 많은 Feature First가 낫지 않을까?"
"현 상황에서 정말로 Feature First 유리할까? 오히려 보일러플레이트 코드가 더 많이 발생하지 않을까?"
검토 당시 Feature First의 구조적 이점은 충분히 인지했지만, 개발 일정상 초기 비용을 무시할 수 없었습니다. 엔지니어 관점에서 기술과 아키텍처링이 정말 중요하지만, 신규 프로젝트이기에 비즈니스 관점이 더 중요하다고 판단했습니다.
따라서 Time To Market을 위해 Layer First를 채택했습니다.
기존 방식 그대로 적용하기보다는 Layer First의 단점을 보완하고, 특정 모듈은 독립적으로 Feature First 형태로도 지원할 수 있도록 팀 내 Ground Rule 정하고 여러 장치를 마련했습니다.
-
Domain, Data 레이어를 모듈로 관리하고 내부적으로는 폴더 단위로 Feature 구분
-
공통 기능이 아닌 경우에는 Feature간 의존성(참조) X
-
규모가 큰 Feature는 각각 ‘모듈’ 단위로 관리 (기가아이즈, 로봇, 통화비서 등)
-
여러 Feature에서 공통으로 사용하는 것들은 common, util 모듈로 분리
-
모든 공통 로직을 하나의 common 모듈로 관리하지 않고, 규모가 있는 기능들은 별도 모듈로 관리
-
analytics, remote_config, design_system 등
-
< High-level 관점, Layer-First 아키텍처 다이어그램 >
출시 이후 고도화 업데이트 과정에서 Layer First의 한계 경험
1. 높은 결합도(Coupling)
다른 Feature에 변경이 생기더라도 data/domain 모듈 변경으로 인해 사실상 영향이 없는 기능이더라도 Side-effect 확인이 필요했습니다.
또한, 공통 도메인(auth, store 등)에 변경이 생기는 경우에도 자체적으로 보장이 어렵기 때문에 전체 Feature에 미치는 영향이 더 컸습니다.
2. Feature 단위의 독립적인 example 앱
개발 생산성을 위해 기능 단위로 병렬 형태로 개발 진행하였고 Integration 전까지는 각 모듈의 example 앱을 활용했습니다.
Layer First에서도 example 앱을 활용할 수는 있지만 불필요한 domain/data 소스코드까지 포함되기 때문에 항상 호환성을 고려해야 단점을 경험했습니다.
3. 신규 서비스(통화비서/AI 전화)에서 새로운 OS 플랫폼 web 지원
통화비서를 사장이지 App에 추가하는 작업뿐만 아니라, 상담사분들이 원활하게 고객 대응을 할 수 있도록 Back-office 형태의 Web 플랫폼도 지원이 필요했습니다.
즉, 하나의 모듈로 2개의 애플리케이션(사장이지 앱 / 상담사 웹)을 지원해야 하는 상황이었습니다.
Flutter Web 지원을 검토해보니, Web 플랫폼에 대응하지 않는 플러그인과 로직 등으로 인해 컴파일 자체가 되지 않았습니다.
이 과정에서 특히, 신규 서비스에 불필요한 코드들이 Layer First 구조이기 때문에 참조되었고 이 단점을 가장 크게 경험했습니다.
4. Minor한 단점들
이외에도 아래 단점들을 경험하였습니다.
- 도메인간의 의존 관계를 도출하기 어려움
- 단위테스트가 결국 통합 테스트가 되는 현상
- 폴더로 Depth 구분하더라도 파일명/클래스명 Unique 보장하기 위해 Prefix 등 필요
한계를 극복하기 위해 Feature First 도입
오히려 생산성(Productivity)이 높았다?
Feature-First의 가장 큰 단점인 개발 비용 이슈가 실제로 경험해보니 궁극적으로는 오히려 개발 생산성에 도움이 되었습니다.
빠른 Feasibility:
개발 초기 단계부터 다른 도메인으로부터 완전히 분리된 모듈을 활용해 Flutter Web 빌드를 직접 시도해봄으로써, Web 플랫폼 지원 가능 여부를 사전에 명확히 검증할 수 있었습니다.
이러한 사전 검증 과정은 프로젝트의 핵심 의사결정에 매우 중요한 역할을 했습니다.
적극적인 example 앱 활용:
독립 모듈로 다른 도메인과의 디펜던시 없이 example 앱 개발
-
공통(auth, store 등) 연동 없이 UI/UX 개발
-
화면 Flow와 상관없이 병렬적으로 개발 가능
-
암호화, 플레이어 등 PoC가 필요한 과제도 별도 의존성 없이 빠르게 검증
앞으로의 아키텍처 방향성
1. 공통 Domain(auth, store, user 등)부터 리팩토링
여러 도메인과의 디펜던시가 존재하는 공통 도메인을 먼저 리팩토링하여 전환함으로써, 기존 Feature와 새로운 Feature 모두 리팩토링 및 개발이 편리하도록 할 예정입니다.
2. 모듈 단위 기준의 아키텍처 검토
모든 도메인이 모듈로 구성된다면, High-level 관점으로 모듈 단위 기준으로의 아키텍처를 검토할 예정입니다.
-
High-level 관점으로 Layer 구성 및 단방향 참조
-
Feature Layer / Common Layer
-
-
Circular Dependency 방지
-
A모듈 ←→ B 모듈 상호 참조
-
-
Feature 간의 Dependency 제거
또한, Rule들을 시스템적으로 Detecting 하기 위해서 pubspec.yaml 기준으로 CI, git hook 등 활용할 예정입니다.
3. Feature First의 단점 보완
가장 큰 단점인 보일러플레이트 코드를 극복하기 위해 팀에서 여러 노력을 하고 있습니다.
AI Agent
-
AI Rules : Flutter 에서 제공하는 Rule(AI rules for Flutter and Dart)기반에 프로젝트 아키텍처 + 팀 내 Coding Convention 등을 Custom
-
신규 서비스(통화비서) 모듈을 Best Practice 지정
-
-
Clean Architecture에서의 Use Case, Data Source, Repository 등 기반 생성에 활용
-
안정성 확보를 위해 Test Code 작성에 기여
Template Generator
mason | Dart package 사용한 Template 방식도 활용
bricks/feature /lib /data /repository {{name.snakeCase()}}_repository_impl.dart /data_source {{name.snakeCase()}}_data_source.dart data.dart /domain /repository {{name.snakeCase()}}_repository.dart /use_case {{name.snakeCase()}}_use_case.dart domain.dart /presentation /bloc /widget {{name.snakeCase()}}.dart pubspec.yaml12345678910111213141516171819
해당 Template이 결국 Best Practice가 될 예정입니다.
Code Snippet
stl, stf 처럼 팀 내 자주 사용하는 코드 블록을 관리할 예정입니다.
-
freezed, usecase, …
// freezed import 'package:json_annotation/json_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part '$filename$.freezed.dart'; part '$filename$.g.dart'; @freezed class $model$ with _$$$model$ { factory $model$({ $END$ }) = _$model$; factory $model$.fromJson(Map<String, dynamic> json) => _$$$model$FromJson(json); }1234567891011121314151617
여기까지 Modular Architecture 주제의 아티클을 마무리하고, 다음 글에서 Feature First 형태로 개발된 모듈을 활용한 Back Office 통화비서 상담사 웹(Web)을 지원하면서 경험했던 Flutter Web 이슈 트러블슈팅을 공유해드리겠습니다.
🤗 함께한 사람들: P-App개발팀
모바일 앱 개발자로 구성되어 있으며 특정 플랫폼 제약 없이 iOS, Android, Flutter, Kotlin Multiplatform 등 내재화 개발 가능하며 프로젝트 특성을 고려하여 최적화된 플랫폼을 채택하고 있습니다.