Search
🏗️

아키텍쳐 설계 - 무너지지 않는 제품을 위해

안녕하세요 하입타운의 공태현입니다.
하입타운의 누적 사용자가 어느새 15,000명을 넘어섰습니다.
이 많은 사용자가 매일같이 서비스를 이용하다 보니, 더 이상 기술부채를 방치할 수 없다는 생각이 들었습니다. 새로운 기능 하나를 적용할 때마다 예기치 않게 다른 부분에서 문제가 발생하는 경우도 잦아졌습니다.
예컨대 티켓 예매 로직을 일부 수정했더니 결제 처리나 환불 기능이 제대로 동작하지 않는 식입니다.
이런 상황을 그대로 두면 하반기에는 더 큰 이슈가 불거질 수 있다고 판단했습니다. 그래서 상반기 동안 주요 문제들을 차례대로 해결하고, 코드 구조 자체를 안정적으로 재정비하기로 했습니다.
앞서 진행한 솔리드 원칙 시리즈를 통해 코드 품질의 기초를 다졌다면,
이번에는 전체 구조를 체계적으로 다루며 유지보수성과 확장성을 한층 높이고자 합니다.

2. 왜 MVVM + GetX인가

기존 하입타운 프로젝트에서는 ViewModel에 UI 상태 관리, 비즈니스 로직, 에러 처리(예: Alert, Toast) 등 모든 책임이 한 데 모여 있었습니다.
이로 인해 다음과 같은 문제가 발생했습니다.
스파게티 코드
하나의 ViewModel 파일 안에 다양한 로직이 뒤섞여 있어 수정·확장이 어려워졌습니다.
테스트 곤란
UI 코드와 비즈니스 로직이 분리되지 않아 단위 테스트가 거의 불가능했습니다.
의존성 꼬임
상태 관리 코드가 곧바로 API 호출·데이터 가공 로직과 연결되며, 작은 변경에도 대규모 리팩터링이 필요해졌습니다.
이러한 문제를 해결하기 위해, MVVM 패턴과 GetX의 경량 상태 관리·의존성 주입 기능을 결합하여
전체 프로젝트를 클린 아키텍처 형태로 재구성하기로 결정했습니다.
그 결과, 다음과 같이 명확한 5개 계층을 도입했습니다.
1.
View (presentation)
2.
ViewModel (presentation)
3.
UseCase (domain)
4.
Repository (data)
5.
Service (data)
이 구조를 통해 각 계층이 자신에게 주어진 역할만 담당하도록 분리함으로써,
코드 가독성과 유지보수성을 대폭 향상시키고, 테스트 커버리지 확보도 용이해졌습니다.

3. 각 레이어 역할 상세 분석

3.1 View (Page)

화면 렌더링: Flutter 위젯을 사용해 UI를 그립니다.
사용자 입력 수집: 버튼 클릭, 스크롤, 폼 입력 등 이벤트를 받아서 ViewModel에 전달합니다.
상태 구독: ViewModel이 제공하는 Rx/Observable 상태를 구독하고, 변화에 따라 UI를 자동 갱신합니다.

3.2 ViewModel (GetXController)

화면 상태 관리: 로딩·성공·실패 등 UI에 필요한 상태를 보관하고 관리합니다.
사용자 이벤트 처리: View로부터 전달된 입력을 받아 필요한 비즈니스 동작(UseCase 호출)을 트리거합니다.
의존성 주입: GetX를 통해 UseCase나 Repository를 주입받아 재사용성과 테스트 용이성을 높입니다.

3.3 UseCase

단일 비즈니스 로직: “한 가지 기능”에 집중해 데이터를 처리하거나 조합합니다.
Repository 호출: 필요한 데이터를 Repository에서 가져오거나 저장 명령을 위임합니다.
결과 반환: ViewModel이 바로 사용할 수 있는 형태의 결과를 돌려줍니다.

비즈니스 로직이 아닌 것

UI 로직: 버튼 클릭, 로딩 스피너 표시, 라우팅 등 화면 제어
데이터 엑세스 로직: HTTP 호출, DB CRUD, JSON 직렬화/역직렬화
인프라 로직: 캐시, 로깅, 인증 토큰 갱신 등

3.4 Repository

데이터 가공: Service로부터 받아 온 raw JSON/DTO를 앱 내부 모델로 변환(fromJson 호출 지점).
데이터 소싱 추상화: 로컬 캐시, 원격 API 등 데이터를 어떤 경로로든 일관되게 제공하는 역할을 합니다.
UseCase와 Service 사이 완충지대: 비즈니스 로직과 네트워크·DB 코드를 분리해 결합도를 낮춥니다.

3.5 Service

통신 전담: HTTP 클라이언트, Firebase, GraphQL 등 외부 API나 DB와 직접 통신합니다.
원시 데이터 반환: 네트워크 응답을 가공 없이 Repository에 전달합니다.
에러·예외 처리 최소화: 통신 오류를 그대로 던지거나 간단히 래핑해 상위 계층에서 처리하도록 합니다.
이처럼 View → ViewModel → UseCase → Repository → Service  각 계층이 오직 자신의 역할만 담당하도록 분리하면,
코드 가독성이 높아지고
변경 범위가 명확해지며
단위 테스트가 수월해집니다.