Skip to content

아키텍처

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

데이터 흐름 요약:

  1. 공시 수집: DisclosureMonitor가 장중(KST 8~20시, 평일) 5분마다 DART API에서 KOSPI/KOSDAQ 신규 공시 수집 → disclosures 테이블 + analysis_cache PENDING 생성
  2. AI 분석: AnalysisScheduler의 submit 워커 풀(워커 수 = AI_CONCURRENCY)이 PENDING/BODY_PENDING 을 클레임 → DART 원문 fetch → vLLM /v1/responses 비동기 submit (background:true) + llm_response_id 저장. 별도 pollLoopLLM_POLL_INTERVAL_MS 마다 in-flight row 들을 GET 으로 회수해서 analysis_cache COMPLETED 업데이트
  3. 사용자 조회: Flutter 앱에서 /disclosures/recent 등 GET 호출 (메타만) → 사용자가 reveal 시 quota 차감 후 분석 결과 노출 (lazy reveal 정책)
  4. 광고 보상: 사용자가 광고 시청 → AdMob → 백엔드 /admob/ssv 콜백 (ECDSA 검증) → quota weekly_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.StartAI_CONCURRENCY 개 submit 워커 + 1개 pollLoop goroutine 을 띄움 — submit 워커가 PENDING > 재시도 FAILED > BODY_PENDING 순으로 claim → DART fetch → vLLM SubmitSetLLMResponseID 까지만 (동기 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) 만 ClaimPendingstaleThreshold=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 / data 3-folder
  • 상태 관리: Riverpod Notifier 기반 (전역 authControllerProvider + 피처별 컨트롤러)
  • 라우팅: go_router StatefulShellRoute — 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
DBRailway PostgreSQL (shuttle.proxy.rlwy.net)로컬 PostgreSQL (~/claudeProjects/local-infra)
모바일 → 백엔드https://api.dartbrief.com (도메인)Tailscale IP 직접 (Makefile LAN_IP)
App CheckPlay Integrity / AppAttest, strictDebug provider, soft
AdMob실 unit ID, SSV 콜백테스트 unit ID, dev 경로(POST /quota/ad-reward)

자세한 운영 환경은 docs/operations/infrastructure.md 참조.


5. 변경 영향 매핑 (어디를 수정하면 어디가 바뀌나)

변경 종류백엔드FlutterDB문서
API 응답 스키마 추가handler + repositoryapi_client + model(조건부) migrationdocs/api/<X>.md
비즈니스 룰 변경service(조건부) UI 분기docs/spec/domain-X.md
새 화면(없음)feature 추가 + routerdocs/spec/screen-X.md
새 외부 서비스service + config + middlewarebootstrap + api_clientdocs/spec/architecture.md (이 문서)
새 백그라운드 잡scheduler 추가 + main.go wiring(없음)(조건부)docs/spec/architecture.md
새 테이블repository + migration(조건부) modelmigration 파일db-schema.md 재생성