본문으로 건너뛰기

시스템 아키텍처 (C4 Context · Container)

⚠︎ 지난 판 — 2026-07-26. 현재 판은 여기다. 이 문서는 그때의 기록이고 고치지 않는다.

갱신일 2026-07-26 · 기준: ai@60ee59a · server@bf192cc · web@87e0834 이 문서는 무엇이 있고 무엇과 붙어 있는가만 소유한다. 요청이 오가는 순서는 ../interfaces/server-ai.md의 §1 분석(202 + callback)·§2 채팅(SSE passthrough), 스키마는 ../contracts/가 갖는다.

확인한 사실

컨테이너 넷과 외부 여섯

브라우저는 heymoa-ai에 도달할 수단이 없다. web의 생성 API 클라이언트에는 /internal/** 경로가 아예 없고(openapi3.yml에서 제외), ai는 CORS 미들웨어도 인증도 붙이지 않았다(app.py).

경계 다섯

#구간프로토콜인증근거
web → serverREST /v1/**access_token HttpOnly 쿠키CookieBearerTokenResolver
web ↔ serverSTOMP over WebSocket쿠키 (핸드셰이크)TranscriptionStompConfig
web ← serverSSE (POST 응답이 text/event-stream)쿠키lib/api/sse.ts
server → aiREST /internal/v1/** + SSE 소비없음agent_chats.py
ai → serverREST /internal/v1/**X-Internal-TokenInternalTokenAuthenticationFilter · context_client.py

같은 시크릿을 양쪽이 다른 이름의 환경변수로 읽는다 — server는 INTERNAL_API_TOKEN, ai는 INTERNAL_TOKEN이다. 회전할 때 두 이름을 같이 봐야 한다.

컨테이너가 넷인 이유

컨테이너왜 따로 있나그래서 갖는 제약
web브라우저 능력(마이크·AudioWorklet)이 필요하고 화면 수명주기가 서버와 다르다영속 저장소를 갖지 않는다
server인증·권한·도메인 상태가 한 곳에 모여야 감사와 잠금이 성립한다public edge 전부를 혼자 진다
aiLLM 호출은 초 단위로 느리고 실패율이 다르다. 같은 프로세스에 두면 요청 스레드를 묶는다202로 끊고 callback으로 되돌려준다
DB 둘파생 데이터가 원본과 같은 database에 있으면 "지워도 되는 것"을 구분할 수 없다cross-database 쿼리 금지 (data-architecture.md)

설계 전제

  • server가 유일한 public edge다. 그래서 ai는 인증·CORS를 구현하지 않는다. 이 전제가 깨지면(ai를 외부에 노출하면) ai에 인증 계층을 먼저 만들어야 한다.
  • 브로커가 없다. 비동기는 DB row(분석 잡)와 HTTP callback으로 처리한다. 메시지 브로커·gRPC는 병목이 실측될 때 도입한다.
  • SSE는 변환 없이 통과시킨다. server가 이벤트를 재해석하지 않기 때문에 web 구간과 ai 구간의 포맷이 같고, 이벤트 정의를 한 곳에만 둘 수 있다. 변환을 시작하는 순간 계약이 둘로 갈라진다.
  • 외부 의존은 소유자가 하나씩만 진다. 음성·OAuth는 server, LLM·관측은 ai가 잡는다. 두 서비스가 같은 외부 시스템에 붙으면 크리덴셜도 둘로 늘어난다.

미확인

무엇지금 아는 것어디를 봐야 하나
④의 네트워크 격리 수단코드에는 인증이 없고, 계약이 이를 미결로 등재했다실제 보안그룹·네트워크 구성 (aws-architecture.md)
ai의 운영 주소server application.yml의 기본값은 http://localhost:8000이고 prod 오버라이드가 저장소에 없다EC2의 .env.prod
분석 완료 push 채널설계 스펙은 STOMP push를 적었지만 server 코드의 STOMP 발행은 전사 이벤트뿐이고 web은 폴링한다pm/2-product/open-issues.md 등재 대상