feature 기반 Clean Architecture 전환 경험 정리

Clean Code Big Poo·2026년 8월 7일

Flutter

목록 보기
42/44

overview

레이어드 구조로 짜여 있던 다트 게임 앱을 feature 기반 clean architecture로 전환한 경험을 정리한다. 왜 전환이 필요했는지, 왜 이 구조를 택했는지, 전환 과정에서 발견하고 해소한 이슈들을 순서대로 다룬다.

왜 전환해야 했나

기존 구조는 vm/, screens/, usecase/, api/, repository/처럼 레이어 기준으로 최상위 폴더를 나눴다. 도메인이 늘면서 문제가 생겼다.

  • 도메인 하나를 바꾸려면 폴더 5개를 오가야 함
  • 어디에 코드를 넣어야 할지 기준이 모호해 shared/global 성격 폴더에 이것저것 쌓임
  • 도메인 경계가 코드로 강제되지 않아서 A 도메인이 B 도메인 내부 타입을 그냥 import

레이어 기준 폴더는 파일이 늘어날수록 "이 코드가 어느 도메인 것인지"를 사람이 기억해서 판단해야 하는 구조다. 그 판단 비용이 누적됐다.

왜 이 구조를 택했나

app / core / shared / feature 4단 구조로 재편했다.

  • app: entrypoint, DI, flavor 설정
  • core: 도메인 무관 인프라 (network, error, utils)
  • shared: 2개 이상 도메인이 같이 쓰는 상태/모델/위젯
  • feature/{domain}: 도메인 하나가 자기 데이터/도메인/프레젠테이션을 전부 소유
# before
lib/
  vm/
  screens/
  usecase/
  api/
  repository/

# after
lib/
  app/
  core/
  shared/
  feature/
    club/
      data/
      domain/
      presentation/
    home/
    auth/
    ...

각 feature 내부는 Presentation(View) → ViewModel → UseCase → Repository → API Client 흐름으로 고정. 도메인 하나를 레퍼런스 구현으로 먼저 완성하고, 나머지 도메인은 그 패턴을 그대로 따라가게 해서 이후 이동은 기계적으로 처리했다.

구조 전환과 다른 변경(상태관리 라이브러리 교체, 라우터 교체 등)은 분리했다. 한 번에 여러 축을 바꾸면 리뷰도 회귀 원인 추적도 어려워진다.

전환하며 발견/해소된 이슈

구조를 도메인 경계로 재편하는 과정에서 기존 코드를 다시 훑게 됐고, 그 과정에서 아래 문제들이 같이 드러났다.

core 내부 순환 참조
상수 정의 파일이 네트워크 설정 파일을 참조하고, 그 네트워크 설정 파일이 다시 상수 파일을 참조하는 구조였다. 상수 쪽에서 flavor 설정을 직접 읽도록 바꿔서 순환을 끊었다.

Repository에 도메인 로직이 섞여 있던 문제
로컬 저장소 Repository가 "로그아웃 시 지울 키 목록" 같은 정책을 들고 있었다. Repository는 순수 CRUD만 담당하도록 남기고, 정책 판단은 해당 도메인의 UseCase로 옮겼다.

전역 Provider인데 실제로는 화면별 로컬 상태였던 경우
어떤 ViewModel이 앱 전체 singleton Provider로 등록돼 있었는데, 실제 호출부는 전부 자기 화면에서 로컬 인스턴스를 새로 만들어 쓰고 있었다. 죽은 등록이었을 뿐 아니라, 다른 화면에서 남아있던 전역 상태를 이어받아 저장된 값을 덮어쓰는 실제 버그로 이어지고 있었다. 전역 등록을 제거하고 로컬 위젯 하위로 옮기면서 구조 문제와 버그를 같이 해결했다.

"global"이라는 이름 하나에 책임 3개가 섞여 있던 파일
네트워크 재시도 다이얼로그, 전역 로딩 UI, Future 확장 함수가 파일 하나에 들어있었다. 셋 다 도메인 무관 코드라 shared에 있을 이유가 없었다. 책임별로 쪼개서 core/utils로 이동했다.

도메인 간 직접 참조
A 도메인이 B 도메인 데이터 레이어의 타입을 그대로 import하는 경우, domain 레이어가 presentation 레이어의 enum을 참조하는 역방향 의존 경우가 있었다. 구조 전체를 훑는 감사 단계에서 걸러서 정리했다.

세션 상태가 여러 군데로 흩어져 있던 문제
인증, 프로필, 특정 도메인이 각자 세션 관련 상태를 따로 들고 있었다. 하나로 통합했다.

죽은 코드
안 쓰는 API 호출, 안 쓰는 화면, 엉뚱한 폴더에 있던 클라이언트 파일 등이 감사 과정에서 같이 발견돼 삭제됐다.

의도적으로 손대지 않은 것도 있다. View가 ViewModel을 건너뛰고 UseCase를 직접 참조하는 패턴이 20여 곳 남아있었는데, 대부분 단순 값 읽기라 고치는 리스크 대비 효과가 낮아서 그대로 뒀다. 대신 새 코드에서는 반복하지 않기로 했다.

마무리

구조를 도메인 경계에 맞추니 코드 위치를 판단하는 비용이 줄었다. 부수적으로 순환 참조, 죽은 전역 상태, 잘못된 레이어에 있던 정책 로직 같은 문제들이 감사 과정에서 같이 드러나고 정리됐다. 구조 전환 자체가 목적이 아니라, 실제 버그와 죽은 코드를 찾아내는 계기였다.

0개의 댓글