아키텍처
DartBrief 시스템 전체 구조와 백엔드 / Flutter 내부 레이어 구조를 한 곳에 정리. 세부 패키지 / 폴더 구조는 backend-structure.md, flutter-structure.md 참조.
1. 전체 시스템
mermaid
flowchart TB
User([사용자])
subgraph Mobile["📱 Flutter 앱 (mobile/)"]
FlutterApp[DartBriefApp<br/>Riverpod + GoRouter]
end
subgraph Backend["🚀 Go 백엔드 (backend/, Railway)"]
API[HTTP API<br/>net/http + chi 패턴 mux]
Monitor[DisclosureMonitor<br/>5분 주기, 장중]
Scheduler[AnalysisScheduler<br/>연속 워커 풀<br/>BODY_PENDING 회수 포함]
end
subgraph Data["💾 데이터 레이어"]
DB[(PostgreSQL<br/>Railway, schema=dartbrief)]
end
subgraph External["🌐 외부 서비스"]
DART[DART OpenAPI<br/>공시 원문]
Kakao[Kakao OAuth<br/>로그인]
VLLM[vLLM<br/>AI 분석<br/>Cloudflare Access]
AdMob[Google AdMob<br/>리워드 광고 + SSV]
FBAppCheck[Firebase App Check<br/>Play Integrity / AppAttest]
end
User -->|앱 사용| FlutterApp
FlutterApp -->|HTTPS<br/>JWT + X-Firebase-AppCheck| API
FlutterApp -->|로그인| Kakao
FlutterApp -->|광고 표시 / 시청| AdMob
FlutterApp -->|기기 attestation| FBAppCheck
API -->|읽기/쓰기| DB
API -->|토큰 검증| Kakao
API -->|App Check JWT 검증| FBAppCheck
API <-->|GET /admob/ssv<br/>리워드 콜백| AdMob
Monitor -->|공시 폴링| DART
Monitor -->|INSERT disclosures<br/>+ analysis_cache PENDING| DB
Scheduler -->|PENDING / BODY_PENDING 클레임| DB
Scheduler -->|원문 fetch<br/>(인덱싱 지연 회수 포함)| DART
Scheduler -->|분석 요청| VLLM
Scheduler -->|결과 저장 / 24h 경과 시<br/>UNAVAILABLE 마킹| DB데이터 흐름 요약:
- 공시 수집:
DisclosureMonitor가 장중(KST 8~20시, 평일) 5분마다 DART API에서 KOSPI/KOSDAQ 신규 공시 수집 →disclosures테이블 +analysis_cachePENDING 생성 - AI 분석:
AnalysisScheduler의 submit 워커 풀(워커 수 =AI_CONCURRENCY)이 PENDING/BODY_PENDING 을 클레임 → DART 원문 fetch → vLLM/v1/responses비동기 submit (background:true) +llm_response_id저장. 별도pollLoop가LLM_POLL_INTERVAL_MS마다 in-flight row 들을 GET 으로 회수해서analysis_cacheCOMPLETED 업데이트 - 사용자 조회: Flutter 앱에서
/disclosures/recent등 GET 호출 (메타만) → 사용자가 reveal 시 quota 차감 후 분석 결과 노출 (lazy reveal 정책) - 광고 보상: 사용자가 광고 시청 → AdMob → 백엔드
/admob/ssv콜백 (ECDSA 검증) → quotaweekly_used -= 1(V4 2026-05-06: 음수 허용, ad_bonus 컬럼 제거)
자세한 도메인별 동작은 domain-disclosure.md, domain-quota.md, domain-admob.md, domain-app-check.md 참조.
2. 백엔드 레이어 (Go)
mermaid
flowchart TB
Request[HTTP Request]
subgraph OuterChain["외곽 chain (모든 요청 통과)"]
WC[WorkerContext<br/>X-Worker-Secret GET → ctx.IsWorker]
CORS[CORS]
JWT[JWTAuth<br/>토큰 있으면 userID ctx 주입]
end
subgraph RegistryHelpers["pkg/route Registry — endpoint별 ACL helper"]
H_Auth[Authenticated<br/>AppCheck + JWT 필수 + General RL]
H_Anon[Anonymous<br/>AppCheck or Worker + JWT 선택<br/>+ HTTPCache 인증 후 적용]
H_AuthIssue[AuthIssue<br/>AppCheck + JWT 면제 + AuthBucket RL]
H_Admin[Admin<br/>X-Admin-Key 필수]
H_SSV[AdMobSSV<br/>ECDSA — RL bypass]
H_Actuator[Actuator<br/>의도된 무 ACL]
end
subgraph Handler["internal/{도메인}/handler.go"]
H1[auth.Handler]
H2[disclosure.Handler]
H3[ai.Handler<br/>분석 + reveal + quota]
H4[watchlist.Handler]
H5[admob.Handler]
Hx[...]
end
subgraph Service["internal/{도메인}/*.go (서비스)"]
S1[ai.VLLMClient<br/>vLLM 클라이언트]
S2[disclosure.DartClient<br/>DART API 래퍼]
S3[appcheck.Verifier<br/>JWK 캐시]
S4[admob.Verifier<br/>ECDSA 키 캐시]
S5[scheduler.AnalysisScheduler]
S6[scheduler.DisclosureMonitor]
end
subgraph Repo["internal/{도메인}/*_repository.go"]
R1[users]
R2[disclosures]
R3[analysis_cache]
R4[user_analysis_quota]
R5[watch_stocks]
R6[ad_reward_transactions]
Rx[...]
end
subgraph Infra["internal/config + db"]
Cfg[config.Config<br/>.env.local 로딩]
Pool[pgxpool<br/>PostgreSQL]
end
Request --> WC --> CORS --> JWT --> Cache
Cache --> RegistryHelpers
RegistryHelpers --> Handler
Handler --> Service
Handler --> Repo
Service --> Repo
Service -.외부 API.-> External[DART / vLLM / Kakao / AdMob / Firebase]
Repo --> Pool
Cfg -.초기 주입.-> Service
Cfg -.초기 주입.-> Repo핵심 패턴:
- handler → service → repository 의 일반적 3계층 + middleware
- 의존성 주입은
cmd/server/main.go에서 수동 wiring (DI 프레임워크 없음) - 백그라운드 잡(
DisclosureMonitor,AnalysisScheduler)은main.go에서go ...Start(ctx)로 spawn.AnalysisScheduler.Start는AI_CONCURRENCY개 submit 워커 + 1개pollLoopgoroutine 을 띄움 — submit 워커가 PENDING > 재시도 FAILED > BODY_PENDING 순으로 claim → DART fetch → vLLMSubmit→SetLLMResponseID까지만 (동기 LLM 대기 없음).pollLoop가 in-flight (PROCESSING + llm_response_id NOT NULL) row 들을 fan-out 16 goroutine 으로 GET →MarkCompletedForResponse/MarkFailedForResponse(모두llm_response_id가드로 race 차단) - 종료/복구: ctx 취소(SIGTERM) 시 워커 + pollLoop 모두 새 작업을 멈추고 graceful shutdown. in-flight vLLM 호출은 DB (
llm_response_id) 에 영속 — 다음 부팅의ListInflight가 재포착해 이어 회수. 워커 단순 stale (PROCESSING + llm_response_id IS NULL) 만ClaimPending의staleThreshold=5min흡수 회수 (in-flight 는 폴링 루프 책임이라 stale 회수 제외 — orphan submit 비용 누수 차단). vLLM in-flight 상한은AI_CONCURRENCY값을 그대로 사용 (submit 워커 수 = in-flight cap, 단일 knob) —ClaimPending류 SQL 안에서pg_advisory_xact_lock로 claim tx 를 직렬화하고status='PROCESSING'카운트로LIMIT LEAST(N, cap - current_inflight)가드 → 동시 워커 race 가 cap 못 깸 (background:true라 워커가 ~30ms 만에 다음 claim 으로 빠지므로 cap 없으면 PENDING 폭이 클 때 vLLM 메모리 폭주) - 마이그레이션은
cmd/server/migrations/에 SQL 파일 +go:embed로 바이너리에 포함, Goose로 부팅 시 자동 적용
자세한 패키지 구조는 backend-structure.md 참조.
3. Flutter 레이어 (mobile)
mermaid
flowchart TB
Bootstrap[main_dev.dart / main_prod.dart<br/>↓<br/>app/bootstrap.dart]
subgraph App["app/"]
Boot[bootstrap.dart<br/>Kakao + Firebase + App Check + AdMob]
Router[router.dart<br/>GoRouter + StatefulShellRoute]
AppRoot[DartBriefApp<br/>MaterialApp.router]
end
subgraph Presentation["features/{X}/presentation/"]
Pages[Pages<br/>login / dashboard / detail<br/>notifications / settings]
Widgets[Widgets<br/>analysis_card / quota_indicator<br/>watch_stock_chip ...]
end
subgraph Application["features/{X}/application/ + shared/auth/"]
Auth[AuthController<br/>NotifierProvider]
FeatCtrl[Feature Controllers<br/>disclosureDetail / rewardedAd<br/>quota / watchlist ...]
end
subgraph Data["features/{X}/data/"]
Repos[Repositories<br/>disclosure / watchlist / quota]
Models[Models<br/>DisclosureDetail / QuotaStatus<br/>WatchStock ...]
end
subgraph Shared["shared/"]
ApiClient[api/api_client.dart<br/>dio + interceptors]
Token[api/token_storage.dart<br/>FlutterSecureStorage]
Theme[theme/<br/>colors / typography / spacing]
DebugLog[debug/debug_log.dart<br/>인앱 로그 패널]
end
Bootstrap --> Boot --> AppRoot
AppRoot --> Router
Router --> Pages
Pages --> Widgets
Pages --> FeatCtrl
Pages --> Auth
FeatCtrl --> Repos
Auth --> Repos
Repos --> Models
Repos --> ApiClient
ApiClient --> Token
ApiClient -.HTTPS.-> Backend[Go 백엔드]
Pages -.context 접근.-> Theme핵심 패턴:
- feature 모듈 단위(
features/{auth,disclosure,watchlist,quota,notifications,settings}/)로presentation / application / data3-folder - 상태 관리: Riverpod
Notifier기반 (전역authControllerProvider+ 피처별 컨트롤러) - 라우팅:
go_routerStatefulShellRoute— 3탭 (Dashboard / Notifications / Settings) + 로그인 / 디테일 별도 라우트 - Build flavor:
main_dev.dart/main_prod.dart두 진입점 →bootstrap(AppFlavor.dev|prod)분기 (App Check provider, AdMob unit ID 등 차이) - HTTP:
dio+ 인터셉터로 JWT +X-Firebase-AppCheck헤더 자동 첨부
자세한 폴더 구조는 flutter-structure.md 참조.
4. 환경 / 인프라
| 영역 | 운영(prod) | 로컬(local) |
|---|---|---|
| 백엔드 호스팅 | Railway (이미지 빌드) | air hot reload, localhost:8080 |
| DB | Railway PostgreSQL (shuttle.proxy.rlwy.net) | 로컬 PostgreSQL (~/claudeProjects/local-infra) |
| 모바일 → 백엔드 | https://api.dartbrief.com (도메인) | Tailscale IP 직접 (Makefile LAN_IP) |
| App Check | Play Integrity / AppAttest, strict | Debug provider, soft |
| AdMob | 실 unit ID, SSV 콜백 | 테스트 unit ID, dev 경로(POST /quota/ad-reward) |
자세한 운영 환경은 docs/operations/infrastructure.md 참조.
5. 변경 영향 매핑 (어디를 수정하면 어디가 바뀌나)
| 변경 종류 | 백엔드 | Flutter | DB | 문서 |
|---|---|---|---|---|
| API 응답 스키마 추가 | handler + repository | api_client + model | (조건부) migration | docs/api/<X>.md |
| 비즈니스 룰 변경 | service | (조건부) UI 분기 | — | docs/spec/domain-X.md |
| 새 화면 | (없음) | feature 추가 + router | — | docs/spec/screen-X.md |
| 새 외부 서비스 | service + config + middleware | bootstrap + api_client | — | docs/spec/architecture.md (이 문서) |
| 새 백그라운드 잡 | scheduler 추가 + main.go wiring | (없음) | (조건부) | docs/spec/architecture.md |
| 새 테이블 | repository + migration | (조건부) model | migration 파일 | db-schema.md 재생성 |