SA · 확장 후 — 시스템 구조
갱신일 2026-08-12 · 기준: 확장 개요 · 현재 판 SA 이 문서가 갖는 것: 컨테이너 · 경계별 프로토콜과 인증 · 런타임 흐름 넷 · 시간 상수 · 확장 천장. 여기 없는 것: 화면(IA) · 모듈 배치(AA) · AWS 리소스(CA) · 저장소 소유권(DA) · 물리 스키마(ERD) · 그래프 스키마(온톨로지) · 스트림 개선과 게이트 설계(실시간 경로).
⚠︎ 이건 계획이지 현황이 아니다. 지금 서 있는 것은 현재 판 SA가 갖는다.
1. 뼈대 한 줄이 깨진다 — public edge가 둘이 된다
현재 판 전체가 이 한 줄 위에 서 있다. "server가 유일한 public edge다." ai에 CORS 미들웨어도 인증도 없는 근거이고, 경계가 다섯뿐이던 근거다. MCP 서버(D8)가 이 전제를 깬다 — 기계 클라이언트가 브라우저 밖에서 들어온다.
| 전제 위에 서 있던 것 | 전제가 깨진 뒤 | 어디서 |
|---|---|---|
| ai는 애플리케이션 인증을 안 갖는다 — 보안그룹이 막는다 | "내부니까 괜찮다"가 근거를 잃는다. ⑥ 에 인증을 넣는다 | §3 |
| 인증·감사 주체는 쿠키 세션 하나 | PAT이 두 번째 주체가 된다 — 범위 · 만료 · 상한. 감사 주체도 (user_id, token_id) 쌍이 된다 | §3 · §8 |
| 사람 속도가 자연 rate limit 이었다 | 루프가 초당 수십 번 두드린다. 인증으로는 못 막는다 | §3 · §10 |
| 쓰기는 사람이 승인 버튼을 누른다 | 누를 사람이 없는 호출이 생긴다. 토큰 범위가 버튼을 대신한다 | §3 · §7 |
오디오는 계속 server를 지난다(D1 · 확장 개요 §5). 브라우저 → Soniox 직결은 버렸다 — egress를 아끼는 대신 전사 출처가 클라이언트가 되고 STT 크레덴셜이 브라우저로 내려간다. 경계가 늘지 않는 것도 이 결정의 이득이다.
2. 컨테이너
speech는 평소 0대다. 회의가 끝날 때만 뜬다 — GPU를 쓰면서 원가가 안 터지는 유일한 방법이다(CA §6).
"저장소 다섯"의 정의는 DA §1 표다 — heymoa · heymoa_ai · 벡터 · Neo4j · S3. 그림에서 앞 둘은 Postgres 한 칸에 들어 있다. 브로커·큐는 그 다섯 밖이지만 저장소 칸에 그렸다 — 인스턴스 밖에 상태가 사는 순간부터 백업·장애·용량을 저장소처럼 봐야 한다. enableSimpleBroker 가 JVM 안이라 "인스턴스 1대"가 기능 정확성의 조건이었던 것이 여기서 풀린다. 브로커·큐 소유권도 그 표가 갖는다 — 원본/파생은 휘발, 주인은 server, 재생성은 해당 없음.
3. 경계 — 다섯에서 열다섯으로
확장 개요의 "경계 열하나"는 web↔server(①~④)와 S3(⑩⑪)를 각각 하나로 센 값이다. 여기서는 프로토콜과 인증이 다르면 다른 줄로 센다.
| # | 구간 | 프로토콜 | 인증 | 신규 |
|---|---|---|---|---|
| ① | web → server | REST /v1/** | HttpOnly 쿠키 | |
| ② | web ↔ server | STOMP over WS (제어 · 팬아웃) | 쿠키 (핸드셰이크) | |
| ③ | web ← server | SSE (채팅) | 쿠키 | |
| ④ | web → server | STOMP — 오디오 (Int16 PCM) | 쿠키 | 인코딩만 바뀜 — Opus는 CA §8 확인 후 |
| ⑤ | server ↔ Soniox | WebSocket | 서버 보관 API 키 | 공급자 교체 |
| ⑥ | server → ai | REST /internal/v1/** + SSE 소비 | 대칭 내부 토큰 또는 mTLS ← 지금 없음 | 변경 |
| ⑦ | ai → server | REST /internal/v1/** | X-Internal-Token | |
| ⑧ | server → speech | SQS 메시지 | IAM (큐 정책) + 잡 토큰 | 🆕 |
| ⑨ | speech → server | REST callback | 내부 토큰 (⑦과 같은 규약) | 🆕 |
| ⑩ | speech → S3 | GetObject | 태스크 role · gateway endpoint | 🆕 |
| ⑪ | server → S3 | 멀티파트 업로드 | 태스크 role | 🆕 |
| ⑫ | 외부 에이전트 → mcp | MCP (HTTP · stdio) | PAT (범위 · 만료 · 상한) | 🆕 |
| ⑬ | mcp → server | REST /v1/** | PAT 그대로 전달 | 🆕 |
| ⑭ | server ↔ 브로커 | STOMP 릴레이 — Amazon MQ (RabbitMQ) | 브로커 크레덴셜 | 🆕 |
| ⑮ | ai → Neo4j | Bolt | 읽기 전용 계정 | 🆕 |
브로커는 Amazon MQ로 확정됐다(RabbitMQ 엔진 · CA §3). Redis는 후보에서 빠졌다 — STOMP 릴레이가 그대로 붙는 쪽이 릴레이 코드를 안 고친다.
/internal/v1/** 는 방향 전용 접두사가 아니다. ⑥⑦⑨ 가 같은 네임스페이스를 쓴다(server ↔ ai 계약).
⑥ — 보안그룹으로는 더 이상 안 된다
서비스가 다섯이 되고 태스크가 오토스케일로 뜨고 지면 보안그룹 규칙이 사람이 따라갈 수 있는 물건이 아니게 된다. 게다가 §1에서 "내부니까 괜찮다"의 근거 자체가 사라졌다.
- 대칭 내부 토큰 —
X-Internal-Token을 ⑥ 에도. 채택. ⑦⑨ 가 이미 이 규약이니 셋을 같은 모양으로 맞추는 게 제일 싸다. - mTLS — 더 강하지만 인증서 발급·회전 체계를 새로 들여야 한다. 서비스 다섯 규모에서 값이 안 맞는다.
- 보안그룹만 유지는 버린다. 규칙 수가 서비스² 로 늘고 감사에 "누가 호출했나"가 안 남는다. 바꿀 때는 양쪽을 같은 변경으로 올린다 — server 클라이언트가 헤더를 싣기 전에 ai에서 검증을 켜면 분석과 채팅이 전부 401로 죽는다.
⑫⑬ — 기계 클라이언트는 다른 종류의 위험이다
| 위험 | 방어 |
|---|---|
| 에이전트 루프 | 토큰별 토큰버킷 (분당 · 일당). 인증은 이걸 못 막는다 |
| 응답이 크다 — LLM은 "이 회의 전사 전부"를 원한다 | 응답 크기 상한 + 커서 페이지네이션 강제. 전문은 명시 요청일 때만 |
| 읽기가 무겁다 — 그래프 순회 + 벡터 검색이 한 호출에 | 읽기 복제본 라우팅 · 순회 상한 |
| 재시도가 공격이 된다 | 429 + Retry-After 로 백오프를 유도한다 |
| 쓰기에는 승인자가 없다 | 기본 범위는 읽기 전용. 쓰기·도구 실행은 별도 범위로만 |
| 폭주가 옆을 죽인다 | mcp를 별도 서비스로 둔 값이 여기서 회수된다 — 회의 중인 사람의 WebSocket은 다른 태스크에 있다 |
| 토큰을 20개 발급하면 상한이 20배가 된다 — 토큰별 상한만으로는 워크스페이스가 스스로 상한을 곱한다 | 워크스페이스 총량 상한을 토큰 상한 위에 하나 더 건다. 토큰 상한은 오작동 방지, 총량 상한은 테넌트 공정성 |
①~④ — 사람 edge 에도 상한이 있다
§1이 깬 전제가 여기서 값을 요구한다 — 사람 속도는 더 이상 rate limit이 아니다. 브라우저 콘솔 루프는 PAT를 안 쓰고 쿠키로 들어온다.
| 상한 | 값 | 강제 지점 |
|---|---|---|
| 계정당 동시 WS | 5 | 앱 (STOMP 핸드셰이크에서 거절) |
| 노트당 진행 중 채팅 턴 | 1 | 앱 — chat.lock 계약을 상한으로 승격 |
| 재분석 | 계정당 분당 1 · 노트당 시간당 3 | 앱 |
| 오디오 프레임 | 40–100ms 배치 기준 초당 상한. 초과하면 그 스트림을 끊는다 | 앱 (릴레이) |
| 로그인 · 초대 | IP+계정 5분 10회 | WAF rate rule |
WAF rate rule은 로그인·초대만 건다. 나머지는 전부 앱이다 — 워크스페이스 스코프와 노트 소유를 ALB가 판단하지 못한다. IA §11의 월 10회는 원가 한도이지 초당 상한이 아니다. 둘은 다른 축이고 같은 수로 안 맞춘다.
4. 흐름 1 — 전사 (S1 · 10:00~10:45)
오디오가 server에서 두 갈래로 갈린다. 실시간 전사(⑤)와 배치 보관(⑪). 브라우저는 STOMP로 올리기만 한다 — 인코딩이 Opus로 바뀌는 것 말고 클라이언트 구조 변경이 없다. 직결이었다면 전사 원문이 클라이언트가 보낸 텍스트가 되고 STT 크레덴셜이 브라우저로 내려간다. 그 대신 릴레이 대역폭을 우리가 진다 — 1만에서 3.2MB/s, 10만에서 32MB/s. uploadId 를 DB에 두는 이유는 §9에서 회수된다.
5. 흐름 2 — 회의 중 에이전트 개입 (S1 · 10:23:14)
이서연: "그냥 STT 공급자 ElevenLabs로 다시 가시죠" → 2026-07-22 회의의 결정 "STT는 Soniox로 간다" 와 C3 충돌.
발화→카드 p95 8초. 구간별 예산과 초 단위 타임라인은 실시간 §3 · 게이트는 실시간 §4 · 개입 형태는 실시간 §6이 갖는다.
틱을 ai에 두지 않았다. 세션 상태를 ai 프로세스에 두면 인스턴스 고정이 필요해지고 "ai는 무상태 재시작이 가능해야 한다"가 깨진다(D13). server는 이미 세그먼트를 저장하므로 새 스트림 소비자가 필요 없다.
6. 흐름 3 — 회의 종료 파이프라인 (S1 10:45 · S4 Spot 중단)
| 규칙 | 이유 |
|---|---|
| 단계마다 부분 결과를 커밋한다 | 화면이 단계별로 채워진다(IA §5) |
| Spot 중단은 그 잡만 재시도 | ②단계만 다시 돈다. ①은 끝났고 ④는 아직 안 갔다 |
| Postgres 먼저, 그래프 나중 | 순서가 반대면 그래프가 정본이 된다 — DA §1 |
S4에서 사용자가 보는 것은 "누가 말했는지 정리 중" 하나뿐이다. Spot 중단도 콜드스타트 4분도 그 문구가 흡수한다. 요약은 예정대로 나온다.
7. 흐름 4 — MCP 조회 (S3)
박준호가 IDE에서 TranscriptionStompConfig.kt 를 보다가 "왜 SimpleBroker 지?"
- mcp는 DB에 직접 안 붙고 권한은 server가 다시 본다. 직결하면 권한 로직이 두 벌이 되고, mcp만 믿으면 edge 하나가 뚫릴 때 전부 뚫린다.
- 근거 타임스탬프를 반드시 싣는다. 근거 없는 답은 에이전트가 그대로 코드 주석에 박아 넣는다.
8. 시간 상수 — 다시 잡아야 하는 것
| 상수 | 지금 | 확장 후 | 왜 |
|---|---|---|---|
| 워치독 | 1분 주기 / 10분 고정 | 단계별로 — ① 2분 · ② 오디오 길이 × 계수 · ③ 2분 · ④ 10분 · ⑤ 5분 (제안값) | 하나로 묶으면 GPU 배치엔 짧고 요약엔 길다 |
| speech 잡 타임아웃 | — | 오디오 길이 × 계수 + 콜드스타트 여유 | 계수는 실측 후(AA §3.3) |
| access token TTL | 30분 | 30분 + 스트림 위 갱신 | 2시간 회의의 마지막 90분이 만료 토큰으로 열려 있다. 재핸드셰이크가 곧 녹음 중단이라 갱신 경로가 필수다 |
| Soniox 세션 수명 | — | 회의 길이를 견디거나 무중단 롤오버 | ElevenLabs는 3,600초 상한이 실측됐다. Soniox도 상한이 있다고 가정하고 45분·2시간을 둘 다 견디게 짠다 |
| SSE keepalive | ≤20초 (구현 10초) | 그대로 | ALB idle보다 짧아야 한다 (§9) |
| MCP 토큰 상한 | — | 분당 · 일당 (제안값 분당 60 · 일당 5,000) | 기계 클라이언트에는 상한이 인증만큼 중요하다 |
| 팬아웃 구독자 prefetch | 없음 (JVM 브로커) | 50 | 외부 브로커는 밀어 넣는다. 상한이 없으면 느린 구독자 하나가 브로커 메모리를 먹는다 |
| 구독자당 미확인 | — | 1,000건 또는 8MB (추정) | 넘으면 그 구독을 끊는다. 관전자 하나 때문에 회의 전체 팬아웃이 죽는 것보다 낫다 |
| 끊긴 구독의 복구 | 새로고침 | 클라이언트가 REST로 재동기 | 전사는 이미 DB에 있다. 브로커에 재전송을 요구하지 않는다 |
짧은 쪽이 긴 쪽보다 먼저 끊으면 안 된다. 현재 판의 이 원칙은 그대로다. 항목이 늘 때마다 축을 다시 정렬한다.
단계 타임아웃의 정본은 이 표다. AA §3.3은 시도 상한과 백오프만 갖는다.
팬아웃은 lossy 다. 정본은 Postgres 이고 브로커는 지연 단축 장치다. 10만에서 동시 구독 2,000이라 이 전제를 안 박으면 브로커가 정본처럼 취급되고, 그 순간 느린 구독자 하나가 회의 전체를 인질로 잡는다.
9. S5 — 배포 중에 회의가 3건 돌고 있다
14:00 배포, 진행 중 회의 3건. 배포 동작은 CA §13이 갖는다 — 새 세션 안 받음 · task scale-in protection · 재연결 인계. 여기는 시간 상수만 본다.
| 상수 | 값 | 이유 |
|---|---|---|
| ALB idle timeout | WS 유휴 60초 · STOMP heartbeat 10초보다 길게 | 관전자 STOMP는 유휴할 수 있다. heartbeat가 idle을 리셋하는 게 전제 |
| deregistration delay (draining) | 45분 = 2,700초 (상한 3,600초) | 단독으로는 못 버틴다 — 2시간 회의가 상한을 넘는다. 실제로 회의를 지키는 장치는 task scale-in protection(CA §13) |
태스크 stopTimeout | 120초 (Fargate 상한 · CA §13) | 45분 회의와 자릿수가 다르다. 그래서 CA가 task scale-in protection을 쓴다 |
세 상수 중 어느 것도 회의 길이를 못 덮는다. 그래서 마지막 방어선은 시간이 아니라 인계다 — 강제 종료돼도 브라우저가 재연결하고 DB의 uploadId 로 다른 태스크가 멀티파트를 이어받는다(§4). 인계가 없으면 위 표는 전부 희망사항이고, 배포 말고도 Spot 회수·AZ 장애·OOM이 같은 일을 한다.
재기동 시 전역 전사 세션을 끊는 현재 동작(APP-189)은 이 전환보다 먼저 걷는다. 안 걷으면 새 태스크가 옛 태스크의 라이브 세션을 죽인다.
10. 새 구조의 확장 천장
옛 천장을 걷으면 다음 천장이 나온다. 걸리는 순서는 확장 개요 §4가 갖는다.
| 천장 | 언제 | 넘는 방법 |
|---|---|---|
| GPU 처리량 | 정각 300건 동시 종료 (10만) | 지연 + 지터로 평탄화 → 그다음 인스턴스 수 |
| Neo4j 쓰기 | 그래프 투영이 회의마다 돈다 | 투영을 비동기 배치로. 실시간일 이유가 없다 |
| 그래프 순회 | 프로젝트가 커지면 이웃이 폭발한다 | 깊이·팬아웃 상한 → 온톨로지 §7 |
| 벡터 재색인 | 임베딩이 늘면 HNSW 빌드가 길어진다 | 증분 색인 → 전용 스토어 |
| Postgres 쓰기 | 세그먼트 + 화자 + 카드 + 결정 + 액션 아이템 | 배치 INSERT → 읽기 복제본 분리 → 샤딩 |
| MCP 폭주 | 에이전트 하나가 루프를 돈다 | 토큰별 상한이 1차 방어, 서비스 격리가 2차 |
| 외부 rate limit | OpenAI · Soniox | 여기서부터 우리 인프라 문제가 아니다. 큐잉과 공급자 이중화 |
11. 실패 모드 — 경계가 끊기면 무엇이 죽나
| 끊긴 경계 | degrade | 죽는 것 | S1에서 보이는 것 |
|---|---|---|---|
| ④ 오디오 | — | 그 회의 녹음 | 재연결까지의 발화가 빈다. 지금도 그렇다 |
| ⑤ Soniox | 오디오는 계속 S3로 쌓인다 | 실시간 전사 | "전사 일시 중단". 오디오가 사니 사후 복구 여지가 있다 |
| ⑥ server→ai | 전사 · 녹음 · 팬아웃 정상 | 요약 · 채팅 · 회의 중 카드 | 회의는 멀쩡히 돈다. 파이프라인 ④가 대기 |
| ⑦ ai→server | ai는 일했는데 결과가 안 돌아온다 | 결과 영속화 | 워치독이 유일한 방어선. 현재 판의 "10시간 조용한 실패"가 재발하는 자리 |
| ⑧⑨ speech | ④⑤ 는 돈다 | 화자 분리 · 화자 매칭 | 전원 화자 1~4 로 남고 요약은 나온다 |
| ⑩⑪ S3 | 실시간 전사 정상 | 화자 분리 영구 불가 | ⑪ 이 끊기면 사후 복구 경로 자체가 없다 |
| ⑫⑬ MCP | 브라우저 경로 전부 정상 | 에이전트 조회 | 박준호만 영향 |
| ⑭ 브로커 — 끊김 | 세그먼트는 DB에 커밋된다 | 팬아웃 — 실시간 화면 전부 | 새로고침하면 전사가 다 보인다. 끊긴 걸 아무도 모른다 |
| ⑭ 브로커 — 적체 | 미확인 상한을 넘긴 구독을 끊고 나머지는 산다(§8) | 그 구독자의 실시간 화면 | 느린 관전자만 REST로 재동기. 회의 전체는 안 죽는다 |
| ⑮ Neo4j | 벡터 검색만으로 답한다 | 회의 간 맥락 · C3 충돌 감지 | 10:23:14 카드가 안 뜬다. 아무도 모른다 |
| 한 계정 폭주 (①~④) | 그 계정만 429 | 없다 | 옆 사람 회의는 그대로 돈다. 상한은 §3 |
| Postgres | — | 전부 | 유일하게 전면 장애인 경계 |
⑭ 와 ⑮ 만 조용히 degrade 한다. 팬아웃 성공률과 그래프 조회 성공률을 지표로 세우지 않으면 이 둘은 관측되지 않는다.
12. 설계 전제
- public edge는 둘이다. 어느 경계도 "내부니까 괜찮다"를 근거로 쓰지 않는다. ai·speech도 애플리케이션 인증을 갖는다.
- 오디오는 server를 지난다. egress와 릴레이 부하를 지는 대신 전사 출처가 서버에 남고 STT 크레덴셜이 밖으로 안 나간다.
- ai는 무상태다. 회의 세션 상태를 ai 프로세스에 두지 않는다. 30초 틱은 server가 돌고, 그래프 조회는 한 호출에 한 번으로 끝낸다.
- 정본은 Postgres. 그래프도 벡터도 지우고 다시 만든다. S3 오디오만 예외라서 7일만 산다. 기계 클라이언트에는 상한이 인증만큼 중요하다 — 정상 인증된 폭주를 인증으로는 못 막는다.
- 끊김은 기본값이다. 배포 · Spot 회수 · AZ 장애가 같은 일을 한다. 재연결 인계가 없으면 나머지는 희망사항이다. 조용히 degrade 하는 경계에는 지표를 세운다(§11).
13. 미확인
| 무엇 | 지금 아는 것 | 확인해야 할 것 |
|---|---|---|
| Soniox 세션 수명 · 재연결 규약 | ElevenLabs 3,600초 상한만 실측 | 상한이 있는지, 무중단 롤오버가 되는지. §8이 여기서 정해진다 |
| pyannote 처리 시간 대 오디오 길이 | 없다 | §8의 계수. §10 첫 천장의 위치 |
| 인스턴스당 릴레이 스트림 수 | 시간당 230MB 까지만 | sendBufferSize 4MB 재산정 시점 · 릴레이를 언제 떼는지 |
| 구독자당 미확인 상한 | 1,000건 또는 8MB는 추정 | 실제 카드·세그먼트 메시지 크기 분포. 상한이 낮으면 멀쩡한 구독자를 끊는다 |
| ⑥ 인증 방식 확정 | ⑦은 토큰, ⑥은 없음 | 대칭 토큰인지 mTLS 인지. ADR 자리이고 결정 주체가 아직 없다 |
| ⑫ PAT vs OAuth · 상한 수치 | 없다. §8은 제안값뿐 | 외부 에이전트 생태계가 무엇을 기대하는지. 실사용 분포 전에는 상한도 추측이다 |
| ①~④ 사람 edge 상한 수치 | 축 다섯은 §3에서 정했다 | 동시 WS 5 · 재분석 분당 1이 정상 사용을 막는지. 화면 여러 개를 여는 사용자가 흔하다 |