Skip to content

Latest commit

 

History

History
229 lines (168 loc) · 10.6 KB

File metadata and controls

229 lines (168 loc) · 10.6 KB

RealTime Server — Claude 컨텍스트

StackUp RealTime 서버. Go 1.26 + chi + amqp091-go. RabbitMQ consumer + SSE/WebSocket 서버. 본 문서 작성 시점에는 SSE만 활성, WS는 US-Session-03 / US-Voice-01에서 도입.

상위: /CLAUDE.md · 횡단 관심사: /docs/


1. 기술 스택

영역 기술
Language Go 1.26.3 (go.mod)
HTTP framework github.com/go-chi/chi/v5
WebSocket github.com/coder/websocket (현재 미사용, 모듈만 등록)
AMQP github.com/rabbitmq/amqp091-go
Config github.com/caarlos0/env/v11 + godotenv
Logging stdlib log/slog (JSON handler)
Validation github.com/go-playground/validator/v10
빌드 Makefile

2. 디렉토리 구조

realtime/
├── go.mod  go.sum  Makefile  Dockerfile  .env.example
├── cmd/
│   └── realtime/
│       └── main.go            조립 (config → 컴포넌트 → run)
└── internal/
    ├── config/                env-tag 기반 Config struct
    ├── transport/             chi 라우터 + 미들웨어 + SSE 핸들러
    ├── session/               sessionId → Subscriber 레지스트리
    ├── bridge/                MQ envelope 파싱 + 디스패처
    ├── messaging/             AMQP connection + reconnecting consumer
    └── trace/                 X-Trace-Id 미들웨어 + context helper

PDF의 Logical Architecture (Transport / Session State / Bridge / Messaging) 에 1:1 매핑.


3. 모듈 의존성 그래프

cmd/realtime ──→ transport ──→ session
              ├─→ bridge    ──→ session
              └─→ messaging ──→ (amqp091-go)

trace는 transport, cmd가 import
config는 cmd, internal/* 모두에서 import 가능

원칙:

  • internal/ 하위 패키지는 다른 프로젝트에서 import 불가 (Go 표준)
  • transport는 도메인 로직(session, bridge)에 의존하되 역방향 금지
  • messaging은 AMQP 라이브러리만 의존, 도메인 로직 모름

4. 책임 매트릭스

패키지 책임
cmd/realtime 조립. config 로드, 컴포넌트 wiring, signal-aware shutdown
internal/config 환경변수 → 타입 안전 Config
internal/trace X-Trace-Id 추출/생성, context propagation
internal/session sessionId → []*Subscriber. fan-out + slow consumer drop
internal/transport chi 라우터, 미들웨어, /health, /realtime/sessions/{id} SSE
internal/bridge RabbitMQ Envelope 파싱 + Dispatcher (sessionId 라우팅)
internal/messaging AMQP connection + 무한 reconnect 가능한 consumer

5. 비책임 (명시적)

  • ❌ PostgreSQL 직접 접근 — 데이터가 필요하면 Core /internal/* API
  • ❌ JWT 발급 — Core 책임. RealTime은 Stream 토큰 서명 검증만
  • ❌ 비즈니스 로직 (질문 생성, 분석) — AI 또는 Core
  • ❌ RabbitMQ에 publish — Core를 통해서만 (architecture.md §4.1)
  • ❌ PostgreSQL 접근 — WS 답변도 Core 내부 REST(POST /api/internal/sessions/{id}/messages)로 프록시. RealTime은 PG·MQ publish 모두 미접근
  • ❌ AI WS 오디오 프록시(RT3) — 오디오 전용 순수 바이트 파이프, 비즈니스 로직 없음. 음성 인식·메트릭·콜백 발행은 모두 AI 책임. RealTime은 브라우저↔AI WS 양방향 복사만

6. HTTP 엔드포인트

ID Method Path 책임 상태
- GET /health 헬스체크 활성
RT2 GET /realtime/stream/me user 채널 SSE (분석 상태). userId는 토큰에서 활성
RT2 GET /realtime/stream/documents/{id} document 채널 SSE 활성
RT2 GET /realtime/stream/sessions/{id} session 채널 SSE (feedback.ready 등 비-라이브) 활성
RT1 WS /realtime/sessions/{id} 라이브 텍스트 면접 (서버→클라 push + 클라→서버 답변) 활성
RT3 WS /realtime/sessions/{id}/audio 실시간 음성 답변 스트림 (오디오 업 ↔ 자막 다운, AI WS 프록시) 활성

모든 /realtime/stream/* (SSE) 및 /realtime/sessions/{id} (WS) 는 인증 필요 — ?access_token=<stream-token> 쿼리로 Core 발급 토큰 검증 (EventSource/WS 헤더 한계 우회). internal/auth 미들웨어가 처리. WS는 SSE(/realtime/stream/*)와 다른 path라 Upgrade 헤더 분기가 필요 없다. WS 핸들러(coder/websocket)는 session 채널 fan-out을 그대로 구독(서버→클라)하고, 수신한 답변({type:"answer",content,idempotencyKey?})을 internal/core 클라이언트로 Core 내부 REST(POST /api/internal/sessions/{id}/messages)에 프록시한다. RT3(/realtime/sessions/{id}/audio)는 WSAudioHandler(transport/ws_audio.go)가 브라우저 WS를 AI WS(REALTIME_AI_WS_URL + ?sessionId=&messageId=, X-Internal-API-Key 헤더)로 양방향 프록시한다. messageId는 사전에 Core POST /api/sessions/{id}/messages/voice/stream-begin이 만든 placeholder. 오디오 내용은 해석하지 않고 프레임 타입을 보존하며 복사만(copyWS).


7. RabbitMQ 토폴로지

Queue Bind Consumer DLQ
q.realtime.session.notify stackup.realtime exchange, routing keys realtime.session.* · realtime.user.* · realtime.document.* RealTime dlq.q.realtime.session.notify (via stackup.dlx)

단일 큐가 세 채널(session/user/document) 라우팅 키를 모두 바인딩한다. 채널 판별은 envelope messageType(realtime.{kind}.notify) → bridge.Envelope.Channel()context에서 id를 꺼낸다.

발행자: Core 서버. envelope 스키마는 /docs/messaging.md §5.

q.realtime.session.notifyx-dead-letter-exchange=stackup.dlx 인자를 가지므로 본 컨슈머가 Nack(false, false) 로 reject 한 메시지는 자동으로 DLQ 로 격리된다. 큐 자체는 infra/rabbitmq/definitions.json 가 import 시점에 declare 하며, Go 측은 QueueDeclarePassive 로 메타데이터만 확인한다.


8. SSE 와이어 포맷

id: <messageId>
event: <eventType>
data: {"data": <payload data>, "traceId": "<traceId>"}

Heartbeat (proxy keepalive):

: ping <unix-ts>


9. 동시성·세션 라우팅

  • session.Registry는 sync.RWMutex로 보호.
  • 한 sessionId에 여러 SSE 연결(다중 디바이스/탭) 가능 → slice.
  • slow consumer 처리: DispatchslowTimeout을 초과하면 그 구독자만 drop, 다른 구독자는 영향 없음.
  • 단일 인스턴스 가정. 다중 인스턴스 전환 시 fanout exchange + 인스턴스별 자기 큐 패턴 필요.

10. 환경 변수

변수 기본값 용도
REALTIME_LISTEN_ADDR :38020 HTTP 리슨 주소
REALTIME_RABBITMQ_URL amqp://stackup:stackup@localhost:38050/ AMQP 연결
REALTIME_LOG_LEVEL info slog 레벨
REALTIME_QUEUE_NAME q.realtime.session.notify 구독 큐
REALTIME_SSE_PING_INTERVAL 30s SSE heartbeat 주기
REALTIME_SSE_SLOW_CONSUMER_TIMEOUT 5s 구독자 send timeout
REALTIME_SSE_BUFFER_SIZE 16 구독자별 채널 버퍼
REALTIME_JWT_SECRET local-development-jwt-secret-must-be-replaced Core JWT_SECRET과 동일값 필수 (Stream 토큰 검증 키. 키=SHA-256(secret), HS256). 불일치 시 SSE/WS 인증 401
REALTIME_CORE_BASE_URL http://localhost:38010 Core 내부 REST base URL (WS 답변 프록시). compose 내부는 http://backend:38010
REALTIME_INTERNAL_API_KEY local-development-internal-api-key Core CORE_INTERNAL_API_KEY와 동일값 필수 (AI 서버도 공유). X-Internal-API-Key
REALTIME_AI_WS_URL ws://localhost:8000/internal/voice/stream AI 음성 스트림 WS base URL (RT3 오디오 프록시 업스트림). compose 내부는 ws://ai:8000/internal/voice/stream
REALTIME_WS_WRITE_TIMEOUT 10s WS write 타임아웃

11. 빌드·실행

make build           # bin/realtime
make run             # 호스트에서 실행
make test            # go test ./...
go test ./... -race  # 동시성 검증
docker build -t stackup-realtime ./realtime

docker compose up -d realtime은 루트 compose에 등록되어 있음.


12. 코드 스타일

  • gofmt (Makefile make fmt)
  • 패키지명은 단복수 단수
  • 외부에 노출할 타입만 PascalCase, 내부는 camelCase
  • internal/ 활용으로 외부 import 차단
  • 에러는 wrap (fmt.Errorf("...: %w", err))

13. 안티패턴

  • ❌ session.Registry 외부에서 subs 맵 직접 조작 → API만 사용
  • ❌ goroutine 누수 → 모든 long-running goroutine은 context.Context 기반 종료
  • ❌ Subscriber 채널을 두 번 close → Unsubscribe는 한 번만
  • ❌ AMQP delivery에서 panic → recover middleware 또는 handler에서 catch + Nack
  • ❌ blocking write to subscriber.Ch without timeout → 항상 select + slowTimeout

14. 신규 기능 추가 절차

새 SSE 이벤트 타입 추가:

  1. messaging.md §5에 envelope 추가
  2. Core 서버가 publish (별도 PR)
  3. RealTime은 bridge.Dispatch가 자동 처리 (event_type만 다르고 라우팅 동일)

새 WebSocket 엔드포인트 (RT1/RT3):

  1. transport/ws_*.go 추가
  2. coder/websocket로 upgrade
  3. internal/session.Registry를 protocol-agnostic으로 그대로 활용

새 환경 변수:

  1. internal/config/config.go에 필드 + env/envDefault 태그
  2. realtime/.env.example 갱신
  3. 본 문서 §10 갱신

15. 현재 상태 (2026-05 기준)

  • HTTP 서버 + /health 활성
  • 멀티채널 SSE 활성 — /realtime/stream/{me,documents/{id},sessions/{id}} (session/user/document)
  • Stream 토큰 인증 활성 — ?access_token= 검증 (internal/auth, Core와 동일 HS256 규약)
  • AMQP q.realtime.session.notify consumer 활성 — messageType 기반 채널 라우팅 → fan-out
  • WebSocket(RT1 라이브 면접) 활성 — /realtime/sessions/{id} 서버→클라 push + 클라→서버 답변 프록시(Core 내부 REST)
  • WebSocket(RT3 실시간 음성) 활성 — /realtime/sessions/{id}/audio 브라우저↔AI WS 오디오 프록시(WSAudioHandler, REALTIME_AI_WS_URL). 오디오 전용 순수 파이프, STT·메트릭·callback.voice는 AI 책임
  • 리소스 소유권 검증 미구현 — 현재 토큰 진위(userId)만 검증. 후속 플랜에서 리소스 스코프 토큰 또는 Core 조회로 강화
  • DLQ 활성 — handler 실패 메시지는 dlq.q.realtime.session.notify 로 격리
  • Prometheus 노출 미구현

각 도입 시 본 문서 갱신.