3기 김태영
57분 전•

10월 28일 일지 작성

10월 28일

오늘 공부에 투자한 순수한 시간: 4시간

학습한 개념 : 구독관리앱 처음부터 끝까지 제대로 이해하기 1 단계 (1단계~3단계 까지 있음)

1. 시스템 아키텍처

설계와 현재 프로젝트 소스 코드에서 확인한 구현을 함께 분석한다. 작업계획서는 1~9단계의 최종 구조를 설명하지만, 아래 구조도와 실행 흐름은 현재 코드에 존재하는 구성 요소를 기준으로 한다. 코드 분석은 정적 검토이며, 실제 기기 실행 결과까지 확인했다는 뜻은 아니다.

이 앱의 핵심 구조는 기기에서 사실을 수집하고 → Dart에서 후보를 판단하고 → 사용자가 확정한 구독만 DB에 저장하고 → 저장된 구독을 바탕으로 여러 화면의 결과를 계산하는 것이다.

1-1 전체 아키텍처 구조도

flowchart LR

JSON["내장 catalog.json"] --> SEED["CatalogSeeder"]

SEED --> DB[("SQLite 파일 1개 서비스·패키지·버전·구독 4테이블")]

DB <--> REPOSITORY["CatalogRepository, SubscriptionRepository"]

REPOSITORY --> PROVIDERS["Riverpod Provider"]

UI["Flutter 화면"] -->|"등록·수정·삭제 요청"| REPO

PROVIDERS -->|"조회 결과·화면 상태"| UI

ANDROID["Android 설치 앱·사용 기록 API"] <--> KOTLIN["Kotlin DeviceScanImpl"]

KOTLIN <--> PIGEON["Pigeon 생성 연결 코드"]

PIGEON <--> SCANREPOSITORY["DeviceScanRepository"]

SCANREPO --> PROVIDERS

PROVIDERS -->|"구독·스캔·카탈로그 입력"| CALCULATE ["Dart 순수 계산 함수"]

CALCULATE -->|"후보·대시보드·달력·통계 결과"| PROVIDERS

PROVIDERS --> PREFERENCE["PreferencesRepository<br/>알림 설정"]

PREFERENCE <--> SETTINGS[("SharedPreferences")]

PROVIDERS --> NOTIFY["NotificationScheduler"]

NOTIFY --> OSNOTIFY["Android 로컬 알림"]

앱이 시작되면 main.dart가 AppDatabase 객체를 만들고 CatalogSeeder.seedIfNeeded()로 내장 카탈로그를 준비한 후 화면을 띄운다. AppDatabase는 SQLite 데이터베이스 네 개가 아니라, SQLite 파일 하나와 그 안의 네 테이블을 정의한다. 테이블은 services, service_packages, catalog_meta, subscriptions이다(tables.dart).

기기 스캔에서는 scanResultProvider가 카탈로그의 패키지 이름을 DeviceScanRepository에 전달한다. 이 저장소는 Pigeon 계약을 통해 Kotlin 구현에 설치 여부와 사용 기록을 요청한다. Kotlin이 반환한 정보는 아직 구독 사실이 아니다. buildCandidateGroups()가 카탈로그, 스캔 결과, 이미 등록된 구독을 결합하여 화면에 제시할 후보를 만든다.

사용자가 후보를 선택하고 저장하면 구독 정보가 subscriptions 테이블에 기록된다. 이후 그 같은 구독 원본에서 홈 목록, 대시보드, 달력, 통계가 각자 필요한 결과를 만든다. 예를 들어 HomeContent는 구독 목록을 받아 dashboardProvider와 calendarPaymentsProvider에 입력으로 전달한다. 통계 화면도 구독 목록을 받아 별도의 통계 계산 결과를 표시한다(statistics_page.dart).

이 구조에서 Riverpod은 영구 저장소가 아니다. DB와 기기 기능에서 나온 데이터, 일시적인 화면 상태, 계산 결과를 화면에 연결하는 역할을 한다. 영구 구독 데이터는 SQLite에 있고, 스캔 결과와 대시보드 계산 결과는 필요할 때 메모리에서 사용한다.

1-2 코드 구조

기능을 먼저 나누고, 기능 안에 필요한 층을 둔다

feature-first 구조를 지정하고, 각 기능에 data, domain, presentation 중 필요한 층만 두도록 정했다. 실제 코드도 add_subscription, scan, dashboard, calendar, statistics, subscriptions처럼 기능을 기준으로 묶여 있다.

이 구조의 효과는 한 기능을 이해할 때 관련 화면·계산·저장 요청을 함께 따라가기 쉽다는 것이다. 이것은 작업계획서의 구조와 현재 코드에서 읽히는 설계 효과이며, 계획서에 직접 적힌 선택 이유로 단정하지 않는다.

파일별 (app, feature, core)로 어떤 역할을 하는지 간단하게 정리해보자면

lib/main.dart는 앱 시작, DB·스캔 저장소 준비,

lib/app/는 화면 조립, 라우팅, provider 연결,

lib/core/는 기능들이 공유하는 모델·저장소·계산과 DB·Android 경계,

lib/features/*/data/는 기능 입력을 저장 요청 등으로 연결,

lib/features/*/domain/는 기능의 계산 규칙,

lib/features/*/presentation/는 기능의 화면과 사용자 입력의 책임을 가진다고 볼 수 있고, 각각의 서로 다른 책임이 부여되고 있다.

core는 “순수한 비즈니스 계산만 있는 곳”이 아니다.

실제로 공통 위젯, DB 저장소, Pigeon 연결 코드, 알림 예약기까지 포함한다. 이 프로젝트에서 core의 기준은 여러 기능에서 공유하거나 외부 시스템과 만나는 경계인가이다.

모든 feature가 세 층을 전부 갖지도 않는다. 예를 들어 scan의 후보 판단은 domain에 있고, Android 스캔 연결은 core/platform에 있다.

-코드가 참조하는 방향-

flowchart TB

APP["app/ : 기능 조립"] --> FEATURES["features/ : 각 기능"]

APP --> CORE["core/ : 공유 코드·외부 경계"]

FEATURES --> CORE

이 화살표는 데이터 흐름이 아니라 import를 통한 코드 의존 방향이다.

APP -> FEATURE -> CORE 순으로 연결은 가능해도 반대로인 CORE -> FEATURE -> APP 즉, CORE가 FEATURE를 알고, FEATURE가 CORE를 아는 것은 불가능하게 설계했다는 의미이다.

실제로 HomeContent가 대시보드·달력·구독 목록 기능의 화면을 조립하고, 각 기능의 계산 파일은 공통 모델과 계산을 core에서 가져온다.

예를 들어 달력과 대시보드 모두 다음 결제일이 필요하다.

calendar_builder.dart와 dashboard_calculator.dart는 서로를 import하지 않고, core/billing/next_billing_date.dart를 각각 사용한다. 두 기능은 같은 계산 규칙에 의존하지만 서로의 내부 구조는 몰라도 된다.

다만 공통 함수의 규칙이 바뀌면 이를 사용하는 기능 모두가 영향을 받으므로 변경 시 함께 검토해야 한다.

1.3 주요 디자인 패턴과 상태관리

1) Repository: 저장소와 기기 기능으로 통하는 창구

CatalogRepository와 SubscriptionRepository는 서로 다른 DB를 소유하는 객체가 아니다. 둘 다 같은 AppDatabase 객체를 전달받는다.

카탈로그 저장소는 서비스와 패키지 목록의 조회를, 구독 저장소는 사용자 구독의 조회·등록·수정·삭제를 맡는다. 두 저장소가 기술적으로 네 테이블에 접근할 수는 있으므로, 담당 테이블의 구분은 DB의 접근 제한이 아니라 코드의 책임 분리다.

DeviceScanRepository도 외부 정보에 접근하는 창구다. 이것은 SQLite 대신 Pigeon을 통한 Android 호출을 감싸고, 중복 호출과 캐시를 관리한다(device_scan_repository.dart). 후보 점수는 이 저장소가 계산하지 않는다.

2) 의존성 주입: 누가 DB를 만들고 누가 사용하는가

main.dart가 AppDatabase 객체 생성

→ appDatabaseProvider에 제공

→ catalogRepositoryProvider와 subscriptionRepositoryProvider가 같은 객체를 전달받음

→ 각 Repository가 자신의 작업에 그 DB 객체를 사용

main.dart가 DB 객체를 만들고 ProviderScope의 overrideWithValue(database)로 제공한다.

catalogRepositoryProvider와 subscriptionRepositoryProvider는 ref.watch(appDatabaseProvider)로 데이터 목록이 아니라 DB에 접근하는 객체를 받아 각각 Repository를 만든다. 이를 의존성 주입이라 한다.

Provider가 실행 중 여러 DB를 스스로 선택하는 것은 아니다.

앱 시작 코드가 사용할 DB 객체를 정하고 Provider가 이를 전달한다. DB가 제공되지 않으면 appDatabaseProvider는 오류를 내도록 되어 있어, 시작 시 연결이 빠진 상태를 드러낸다.

3) 변경을 구독하는 흐름: DB 행에서 화면까지

SubscriptionRepository.watchAll()은 구독 목록을 한 번 조회하는 함수가 아니라 Stream<List<Subscription>>을 제공한다. 실제 코드의 핵심은 다음과 같다(subscription_repository.dart).

return query.watch().map((rows) => List.unmodifiable(rows.map(_fromRow)));

query.watch()는 처음의 조회 결과와 이후 관련 DB 변경을 반영한 행 목록을 내보낸다.

Repository의 _fromRow()는 각 행을 Dart의 Subscription 객체로 변환한다. 따라서 List<Subscription>은 카탈로그 전체나 DB 테이블 자체가 아니라 사용자가 등록한 구독을 표현하는 메모리상의 Dart 목록이다.

subscriptionsProvider가 이 스트림을 받고, 홈은 ref.watch(subscriptionsProvider)로 새 목록에 반응한다.

구독 등록 뒤의 실제 연결은 다음과 같다.

폼 입력

→ persistFormValues()

→ SubscriptionRepository.create()

→ SQLite subscriptions 행 추가

→ drift query.watch()가 새 행 목록 발행

→ Repository가 행을 List<Subscription>으로 변환

→ subscriptionsProvider가 새 목록 수신

├─ HomeContent → dashboardProvider → calculateDashboard() → 대시보드 표시

└─ currentCandidateGroupsProvider → buildCandidateGroups() → Scan 후보 갱신

4) 순수 계산과 라이브러리의 역할

calculateDashboard(), buildCandidateGroups(), nextBillingDate()는 필요한 데이터와 기준 날짜를 입력받아 결과를 만든다.

DB 조회나 Android API 호출을 함수 안에서 수행하지 않는다. 화면과 저장 기술에서 계산 규칙을 분리하므로 같은 입력으로 결과를 확인하고 여러 기능에서 재사용할 수 있다.

별도의 UseCase 클래스를 만들지 않고 이런 계산 함수를 사용하도록 정했다.

Flutter -> 화면과 사용자 입력

flutter_riverpod -> 객체 제공, 비동기 결과·스트림·화면 상태 연결

Pigeon -> Dart와 Kotlin 사이의 타입이 정해진 스캔 호출 코드 생성

drift·SQLite -> 구독과 카탈로그의 로컬 저장, query.watch()

Kotlin·Android API -> 설치 앱과 사용 기록 조회

shared_preferences -> 알림 설정 저장

flutter_local_notifications -> Android 로컬 알림 예약

table_calendar·fl_chart -> 달력과 통계 차트의 화면 표시

이 표의 Repository, 의존성 주입, 순수 계산은 설계 방식이고, Riverpod, drift, Pigeon 등은 이를 구현하는 데 사용된 도구다.

1..4 시스템 경계

1) Flutter와 Android의 경계

Flutter는 어떤 패키지를 확인할지 요청하고, Kotlin은 Android API로 설치·사용 기록을 조회한다.

Pigeon 인터페이스가 두 언어 사이에 오갈 함수와 데이터 형태를 정하고, Kotlin 구현은 기기 정보를 반환하지만 “이 서비스가 결제 중일 가능성이 높은가”를 결정하지 않는다. 그 판단은 Dart의 후보·점수 계산에 있다.

이 경계는 Android만 제공할 수 있는 사실과 앱이 정한 판단 규칙을 분리한다.

알림은 별도의 플랫폼 연결이고, 현재 코드는 Pigeon이 아니라 NotificationScheduler를 통해 flutter_local_notifications 플러그인으로 Android 알림을 예약한다.

2) 화면과 계산의 경계

HomeContent는 구독 목록과 화면 상태를 받아 dashboardProvider에 구독 목록·기준 날짜를 전달한다.

dashboardProvider가 calculateDashboard()를 호출하고, HomeContent는 반환된 요약값으로 카드를 그린다.

HomeContent가 대시보드 계산식을 직접 구현하거나 calculateDashboard()를 직접 호출하지는 않는다.

화면에 판단이 전혀 없다는 뜻이 아니다. 현재 등록 폼은 이름과 금액 입력이 저장 가능한지 확인한다.

그러나 금액 해석, 후보 점수, 다음 결제일, 대시보드 집계 같은 주요 계산 규칙은 별도 함수에 있다. 따라서 이 경계는 화면의 모든 조건문을 없앤다는 규칙보다, 구독에 관한 핵심 계산을 화면에서 분리한다는 원칙으로 이해해야 한다.

2장의 핵심 결론: 현재 앱은 하나의 DB에 확정 구독을 저장하고, Repository가 DB 행을 Dart 모델로 바꾸며, Riverpod이 그 모델과 일시적인 상태를 화면에 연결한다.

Android 경계는 기기 사실을 수집하고, 순수 계산 함수는 그 사실과 구독 데이터를 후보·금액·날짜·통계 결과로 해석한다. 따라서 한 구독의 등록이나 수정은 저장된 원본을 바꾸고, 여러 화면은 새 원본으로 필요한 결과를 다시 만든다.

키워드

ㄴ꼬리질문1 설치된 앱이 발견됐을 때 왜 곧바로 Subscriptions에 저장하지 않는거야?

ㄴ꼬리질문2 구독을 등록하면 화면은 Repository에게 등록 요청을 보내고. Repository가 SQLite에 저장을 하고, 등록 했을떄의 상태값을 Riverpod Provider에 보내고, 이 Riverpod Provider가 화면 상태를 갱신해서 Scan 시트에 등록한 서비스를 제외하고, 대시보드는 Riverpod Provider가 화면에서 받은 상태값을 받고 이를 입력값으로 다시 순수 계산함수에게 줘서 순수함수가 받은 입력값을 통해 게산을 하괴, 계산 결과를 다시 Riverpod Provider에게 주고 이 결과를 또 다시 화면에 주기대문에 새로운 구독 서비스를 등록함으로써 대시보드와 Scan시트의 목록이 바뀌는거야?

ㄴ꼬리질문3 subscriptionsProvider는 whtch()로 SQLite를 감시하는거야?

ㄴ꼬리질문4 한 기능이 다른 기능의 내부 계산 파일을 가져다 쓰기 시작한다는게 무슨 말이야?

ㄴ꼬리질문5 공통된 계산 로직을 미리 만들어두고, 계산을 통한 결과가 필요할때 각각의 기능들이 import를 통해서 함수를 호출한다는 말이지? 이러면 각각의 기능마다 독립적으로 데이터를 사용하는 거니까 서로 몰라도 되잖아

ㄴ꼬리질문6 subscriptionRepositoryProvider가 DB를 받아 저장소를 만든다는게 무슨 의미인지 설명해줘

ㄴ꼬리질문7 Provider가 Repository에게 어느 DB를 사용할지 연결해준다고 했는데 이 말이 중요한 의미를 담고있는 것 같아 이 말의 의미를 설명해줘

ㄴ꼬리질문8 AppDatabase가 원본 데이터베이스 4개를 가지고 있는거야?

ㄴ꼬리질문9 CatalogRepository와 SubscriptionRepository 모두가 appDatabaseProvider에서 DB를 받는다는게 정확히 무슨 의미인지 설명해줘

ㄴ꼬리질문10 ubsciprionsProvider는 상태 변화를 관찰하고 상태가 변하면 그 데이터를 subscriptionRepositor가 보기 위해서는 어떤 DB를 봐야 하는지를 알려주고, subscriptionRepository는 DB에서 데이터를 읽고, 조회하고, 검색하는 것 맞지?

ㄴ꼬리질문11 Repository의 watchAll()을 구독한다는게 Repository가 바뀌면 바뀌었다는 사실을 provider에게 알려주는거야?

ㄴ꼬리질문12 Repository가 제공한 목록이라고 했늗데 Repository가 어떤 목록을 제공하는거야?

ㄴ꼬리질문13 내보내는 형태인 List<Subscription> 는 Dart 객체 데이터 모델인거지?

ㄴ꼬리질문14 Repository가 Dart 객체 데이터 모델로 변환하는 부분으로 인해서 화면은 받은 데이터가 어디로 부터 온 데이터인지 몰라도 된다는 구조가 형성되는거지?

ㄴ꼬리질문15 설치 앱 스캔 결과에서 메모리의 시간이 10분인거지?

ㄴ꼬리질문16 HomeContent는 계산을 실행하는 주체가 아니라 구독 데이터를 받아서 이 데이터를 입력값으로 calculateDashboard에 있는 계산 함수를 호출해서 이결과를 화면에 반영하는 것이지? 즉, 계산 부분에서는 다른 파일의 함수를 호출해서 계산만 하고 받은 결과를 통해 화면을 그리는 것 맞지?

ㄴ꼬리질문17 금액을 수정하면 계산결과를 별도로 수정하는 대신 개 구독 목록으로 다시 계산 한다는게 무슨 말인지 설명해줘

ㄴ꼬리질문18 사용자가 새로운 구독을 추가하면 일단 먼저 Repository가 subscription으로 바꾸고 이를 DB에 기록하고, 기록함으로써 바뀐 DB를 watch를 통해서 Provider가 바뀐 구독 목록을 받아서 상태 변화를 감지하고, 이를 통해 대시보를 바꾸거나 하는거야?

ㄴ꼬리질문19 그럼 Repository가 DB를 새로 기록하고, 이를 watch가 변화를 감지해서 변경된 구독 목록을 읽고, 이를 Repository에게 줘서 이 구독 목록을 Subscription 객체로 변환하는거야? 그리고 이를 subscriptionsProvider가 받는거고?

...

AI 대화 내역 링크:

chatGPT: https://chatgpt.com/s/cx_6ae1facde2ec8191b52647bfd751b54c

오늘 잘한 점: 내가 만들고 있는 구독 관리앱을 제대로 이해하려고 코드레벨까지 공부를 하기 위해 어떤 순서와 흐름으로 어떻게 공부를 해야 할지에 대한 것들을 GPT와 함께 질문을 통해 규칙과 순서를 정하였다. 그냥 단순히 이해를 하는것이 아니라 각각의 단계의 데이터 흐름과 기능들이 데이터를 어떻게 호출하고, 데이터를 주기 위한 과정은 어떻게 되고, 이를 어떤 디자인 패턴이나 라이브러리를 통해 구현을 했는지 등등을 이해해서 모든 내용의 관계를 연결해서 하나의 유기적인 흐름과 구조를 만들게 할 수 있는 형식과 구성을 먼저 정했다.

오늘 한 단계는 총 3단게중에서 1단계 (전체 시스템 알아보기) 부분을 공부한 것이다.

오늘 부족했던 점: 내가 원하는 이해의 방향을 나의 말로 그리고 이를 AI에게 논리적을 잘 전달하는게 많이 어려웠던 것 같다. 그래서 일단 대략적인 내가 원한느 이해의 방향을 말하고, 대화를 통해 계속 구체화 시켜나가는 방법을 사용했던 것 같다.

오늘 총평: 대략적으로 전체적인 흐름과 과정을 좀 더 깊게 이해해볼 수 있는 단계였던 것 같다. 오늘 공부한 전체적인 틀과 구조를 바탕으로 핵심 코드들을 깊게 공부하면 더 쉽고 체게적으로 이해를 할 수 있을 것 같다는 생각이 들었다.

0
0

여기에 파일을 드롭하세요