본문으로 건너뛰기

Application Architecture — HeyMoa

갱신일 2026-08-10 · 기준: 세 저장소 워킹트리 + docs/develop/MVP1/architecture/application-architecture.md 이 문서가 갖는 것: 세 서비스의 내부 구성, 의존 방향 규칙, 그 규칙을 강제하는 장치, 그리고 UI 동작 ↔ API ↔ 저장소의 대응. 여기 없는 것: 화면 트리(information-architecture.md) · 경계별 프로토콜(system-architecture.md) · 인프라(cloud-architecture.md).


1. UI ↔ API ↔ 저장소 대응

대응 관계는 표가 그림보다 정확하다. 한 장에 그리면 노드 60개짜리 그물이 되고, 어느 UI 가 어느 모듈을 부르는지는 오히려 안 보인다. 그림은 의존 방향만 맡는다.

1.1 무엇이 무엇을 부르나

화면 영역하는 일server 모듈저장소
인증Google 로그인 · 토큰 갱신 · 로그아웃auth · userUser · Session
워크스페이스 셸워크스페이스·프로젝트 CRUD · 노트 목록 · 알림workspace · project · notificationWorkspace · Member · Invitation
노트조회 · 참여자 편집 · 삭제noteNote · Participant
노트 — 요약요약 조회 · 항목 확정/기각 · 근거 점프analysisAnalysisJob · MeetingItem · Evidence
녹음마이크 캡처(PCM16 24kHz) · 세션 시작/중지 · 회의 종료transcription (STOMP) · noteTranscriptSegment
챗봇공유·개인 챗 전송 · 스트림 수신 · 도구 승인agentchat (SSE)AgentChat · Message · ToolApproval
설정멤버 초대·역할·추방workspaceMember · Invitation
설정 — 연동Linear·GitHub 연결·해제integrationIntegration

agentcontext 는 표에 없다 — 브라우저가 부르지 않는 유일한 모듈이고, ai 만 호출한다.

1.2 의존 방향 — 화살표는 한쪽으로만

web 은 ai 를 모른다. 생성된 API 클라이언트에 /internal/** 이 아예 없고, ai 에는 CORS 미들웨어도 인증도 없다 — 브라우저가 도달할 수단 자체가 없다.

1.3 저장소는 어느 DB 에 사는가

heymoa — 원본. 지우면 복구할 수 없다.

저장소성격
User · Session인증
Workspace · Member · Invitation조직
Note · Participant회의
TranscriptSegment전사
AnalysisJob · MeetingItem · Evidence분석 결과
AgentChat · Message · ToolApproval대화
Integration외부 연동 토큰(암호화)

heymoa_ai — 파생. 통째로 날려도 원본에서 다시 만들어진다.

저장소성격
AnalysisResult Cache멱등성 캐시
TranscriptEmbedding · pgvector검색 인덱스
LangGraph Checkpoint그래프 상태

파생 데이터를 원본과 같은 database 에 두지 않는 이유: 지워도 되는 것을 구분할 수 없어진다. heymoa_ai 는 통째로 날려도 재생성되지만 heymoa 는 아니다.


2. 세 서비스가 같은 방향을 쓴다

안쪽은 바깥쪽을 모른다. 프레임워크는 항상 바깥에 있다.

서비스나누는 단위규칙강제 장치
heymoa-ai레이어 4domain ← application ← infrastructure ← presentation. domain·application은 fastapi·langgraph·sqlalchemy·httpx를 import할 수 없다import-linter 계약 6개 — CI가 lint-imports로 검사
heymoa-server도메인 모듈 12모듈마다 presentation · application · domain · infra. Service → Handler → Repository 단방향, Service 간 직접 호출 금지⚠️ AGENTS.md + 리뷰. ArchUnit/Konsist는 도입하지 않기로 결정
heymoa-web층 6route는 prefetch만, feature는 제품 의미를 알고, transport는 연결만 안다⚠️ AGENTS.md + pnpm orval 재생성 + hook. 생성물은 손으로 고치지 않는다

ai만 기계가 막고, 나머지 둘은 문서와 리뷰다. 이것이 이 저장소들의 가장 큰 비대칭이다.


3. heymoa-ai — 레이어 4

src/heymoa/
domain/ entities · value_objects ← 프레임워크 금지
application/ services · policies · interfaces · dto
infrastructure/ agent(orchestrator·branches·stream·analysis·context·providers)
tools · external · llm · persistence · observability · rendering · config · server
presentation/ api/v1 · schemas ← infrastructure를 import할 수 없다
main ← 조립은 여기서만

import-linter가 실제로 강제하는 것

계약무엇을 막나
heymoa의 의존은 안쪽으로만main → presentation → infrastructure → application → domain 역류
웹 표면은 조립을 모른다presentationinfrastructure를 직접 import — 라우트가 Container를 받기 시작하는 것
그래프 → 도구 → 외부 호출 한 방향agent ↔ tools 순환 (실제로 발생했고 리뷰에서야 발견됨 — APP-298)
외부 호출 계층에 프레임워크 금지external이 langchain·langgraph·fastapi를 아는 것
계측 계층에 그래프 프레임워크 금지관측이 그래프를 물어 모델 계층 교체 시 계측이 따라 바뀌는 것
안쪽 레이어에 프레임워크 금지domain·application의 fastapi·langgraph 의존

승인 게이트는 실행 노드에 둔다. 모델 바인딩 단계에 걸면 모델이 도구 이름을 지어내 부르는 경로로 우회된다. 바인딩과 실행이 같은 집합을 보게 유지한다.


4. heymoa-server — 도메인 모듈 12

모듈책임
authGoogle OAuth2 로그인, access 30분 JWT + refresh 30일 불투명 토큰, 회전(유예 60초)
user내 정보 조회
workspace워크스페이스 CRUD, 멤버·역할, 초대 생성·수락·거절
project워크스페이스 하위 프로젝트 CRUD
note노트 CRUD, 참여자, 회의 상태·시간 계산
transcription전사 세션 수명주기, STOMP 오디오 중계, 세그먼트 영속화
analysis잡 생성 → ai 디스패치 → callback 영속화 → 워치독. 비동기 경계 전부를 혼자 진다
agentchat채팅·메시지 영속화, ai SSE passthrough와 tee, 도구 승인 상태 기계
agentcontextai 전용 내부 조회 API. 다른 모듈이 이 경로를 늘리지 않는다
integrationLinear·GitHub OAuth 토큰 암호화 보관. 단기 토큰만 ai로 나간다
notification인앱 알림 조회·읽음
common공통 응답 봉투, Security, 시간·영속·HTTP 지원
com/heymoa/<도메인>/
presentation/api/ Controller
presentation/api/dto/ 요청 · 응답 DTO
presentation/ws/ WebSocket Controller · 인터셉터
application/ Service(유스케이스 하나) · Handler(공유 조율) · Dao(조회)
application/dto/ Command · Result · Projection
domain/ 엔티티 · 도메인 규칙
infra/ Properties · Config · 스케줄러
infra/persistence/ Repository · Dao 구현
infra/<외부명>/ 외부 연동

세부 규칙

  • 서비스 클래스 하나 = 유스케이스 하나. 진입 메서드는 operator fun invoke
  • UseCase 인터페이스를 두지 않는다 (구현 하나뿐인 인터페이스는 보일러플레이트)
  • 화면 조회는 Querydsl QueryDao로 분리. Projections.constructor(...) 금지 → @QueryProjection
  • 엔티티는 private 주 생성자 + 정적 팩토리, public setter 금지
  • 엔티티 20개에 @OneToMany 연관 매핑이 0개 — 전부 raw id + QueryDSL projection. 3초 폴링 화면이라 1+N을 구조적으로 막는다
  • 스키마는 Flyway 단방향 (현재 V1~V24), ddl-auto: validate

백그라운드 스케줄러 2개AnalysisWatchdogScheduler(1분 주기 / 10분 타임아웃), ToolApprovalWatchdogScheduler(1분 주기). 둘 다 플래그로 끌 수 있다.


5. heymoa-web — 층 6

위치하는 일
routeapp/Server Component. params 해석 · 서버 prefetch · HydrationBoundary
전역 providerapp/providers.tsxMockProvider → QueryClientProvider → RecordingProvider → AuthProvider 순서 고정
feature UIcomponents/{workspace,notes,transcription,chat,settings,notification,auth}/Client Component. 제품 의미를 안다
primitivecomponents/ui/ · components/heymoa/shadcn/ui 기반. 제품 의미를 모른다
생성 APIlib/api/generated/orval 산출물 — 태그별 훅 · 모델 · MSW · faker
transportlib/api/fetcher.ts · lib/api/sse.ts · lib/transcription/socket.tsREST mutator · SSE-over-POST · STOMP. 연결과 401 재발급만 알고 이벤트 의미는 모른다
protocollib/transcription/protocol.ts · lib/chat/stream-protocol.tsAsyncAPI 계약의 zod discriminated union. 파싱 실패는 에러
순수 로직lib/transcription/transcript-reducer.ts · lib/notes/meeting-state.ts이벤트 → 화면 상태 투영, 회의 상태 판정
lib/mocks/MSW REST · WebSocket · SSE + 상태 저장 목 DB

상태 경계 — 서버 상태는 TanStack Query 단독 소유. 전역 client 상태는 인증(AuthProvider)과 활성 녹음(RecordingProvider) 둘뿐. 20Hz 오디오 레벨은 RecordingMeterContext로 분리해 전사 화면 리렌더를 막는다. 폴링은 refetchInterval + enabled 게이팅만.

Next.js 16 특이사항 — 미들웨어는 proxy.ts다. middleware.ts를 만들면 404 루프가 된다(hook이 막는다). proxy.ts가 SSR 전에 refresh 토큰을 회전하고 Set-Cookie를 재기록한다. Route Handler·Server Action은 0개.

계약에서 생성한 것을 진실로 삼는다. 수기 구현은 생성이 불가능한 3개 operation뿐:

operation왜 못 쓰나대신
POST /v1/agent-chats/{chatId}/messages응답이 text/event-stream수기 SSE 리더
POST /v1/notes/{noteId}/chat/messages위와 같음수기 SSE 리더
GET /v1/workspaces/{id}/integrations/{provider}/authorize302 리다이렉트브라우저 최상위 이동

6. 확장점 — 늘릴 때 고칠 곳 하나

늘리고 싶은 것고칠 곳
ai 채팅 도구TOOLS 목록 한 줄 — kind가 승인 게이트를, credential이 바인딩 필터를 결정
ai agent 종류PROFILES 딕셔너리 한 줄. 그래프를 늘리지 않는다
ai 실행 수단 · 저장소 교체조립 지점 하나 (DI)
server 도메인모듈 폴더 하나 (4계층을 그 안에)
web API 훅고치지 않는다. 계약을 고치고 pnpm orval로 재생성

7. 계약 동기화 사슬

heymoa-server 코드
→ ./gradlew clean openapi3 (restdocs-api-spec · 빌드 생성물)
→ docs/develop/MVP2/contracts/openapi3-server.yml
→ heymoa-web/openapi3.yml (/internal/** 3경로를 손으로 제거한 미러)
→ pnpm orval → lib/api/generated/**

⚠️ clean을 빼면 삭제된 경로가 계약에 되살아난다 — APP-214에서 실제로 meeting-pause·meeting-resume가 부활했다. ⚠️ /internal 제거가 수작업이다. 경로가 늘면 누락될 수 있고, web의 고정 테스트는 사후 탐지이지 예방이 아니다.

계약 파일원본 소유검증
openapi3-server.yml (39경로 · 51 op)heymoa-server (빌드 생성물)✅ 생성 자체가 검증 + ExposedEnumContractTest
asyncapi-web-server.yml (8채널)전사·노트토픽 = server 미러 / 채팅 SSE = 이 파일이 원본✅ 전사·토픽만 (*WebSocketDocsTest) / ⚠️ SSE는 문법 검증뿐
asyncapi-server-ai.ymlheymoa-ai (계획 문서, 이벤트는 $ref)⚠️ 수동
openapi3-ai.ymlheymoa-ai (수기 원본)⚠️ 수동

web은 어떤 계약도 원본으로 소유하지 않는다. 전부 소비자다.


8. 설계 전제 — 뒤집힐 조건과 함께

전제뒤집히는 조건
프레임워크를 바깥에 둔다. LangGraph와 FastAPI는 둘 다 infrastructure이고 port 뒤에 숨는다이 레포에서 가장 값이 나가는 제약이라 CI가 검사한다. 걷어내려면 계약부터 지운다
승인 게이트는 실행 노드에 둔다모델 바인딩과 실행이 같은 집합을 보지 못하게 되는 변경이 들어올 때
server는 Service 간 호출을 막는다 — 의존 그래프가 모듈 사이로 번지는 것을 막으려고모듈 수가 늘어 Handler 조율이 병목이 될 때
web은 계약 생성물을 진실로 삼는다생성 불가 operation이 3개를 넘어설 때
크기 기준은 검사가 아니다 (함수 30줄 · 파일 250줄 · 폴더 8파일)pytest로 강제했다가 "판단이 사라진다"는 이유로 삭제했다. 다시 기계로 막으려면 그 이유부터 반박한다

9. 미확인

무엇지금 아는 것
CI가 실제로 돌고 있는가heymoa-ai만 ✅. heymoa-server는 Actions 과금 차단으로 중단(로컬 게이트가 유일), heymoa-web은 워크플로 자체가 없다
커버리지 하한없다. 숫자로 막지 않기로 결정
asyncapi 검증세 곳 다 수동 (npx @asyncapi/cli validate)
채팅 SSE 구간의 회귀 방어passthrough라 server에 DTO가 없어 생성·검증 모두 불가능. 세 서비스 중 누가 이 구간을 막을지 정해져 있지 않다
ai 수직 슬라이스 전환 (APP-263)analysis · chat · platform을 최상위로 올리는 계획이 harness rule에만 있다
heymoa-web 위치 이동 (APP-374)apps/web으로 옮겨 Tauri 데스크톱 자리를 만드는 계획. Todo — Vercel Root Directory도 같이 바뀐다
채팅 프롬프트 길이 상한ai가 ToolMessage를 자르지 않아 대화가 길어지면 프롬프트가 계속 커진다